nanka iroiro (#6853)

* wip

* Update maps.ts

* wip

* wip

* wip

* wip

* Update base.vue

* wip

* wip

* wip

* wip

* Update link.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update privacy.vue

* wip

* wip

* wip

* wip

* Update range.vue

* wip

* wip

* wip

* wip

* Update profile.vue

* wip

* Update a.vue

* Update index.vue

* wip

* Update sidebar.vue

* wip

* wip

* Update account-info.vue

* Update a.vue

* wip

* wip

* Update sounds.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update account-info.vue

* Update account-info.vue

* wip

* wip

* wip

* Update d-persimmon.json5

* wip
This commit is contained in:
syuilo 2020-11-25 21:31:34 +09:00 committed by GitHub
parent 7660839e40
commit 0144408500
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 4489 additions and 1734 deletions

View File

@ -127,6 +127,7 @@ cacheRemoteFilesDescription: "この設定を無効にすると、リモート
flagAsBot: "Botとして設定" flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。" flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "Catとして設定" flagAsCat: "Catとして設定"
flagAsCatDescription: "このアカウントが猫であることを示す場合は、このフラグをオンにします。"
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認" autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
addAcount: "アカウント追加" addAcount: "アカウント追加"
loginFailed: "ログインに失敗しました" loginFailed: "ログインに失敗しました"
@ -440,6 +441,7 @@ useOsNativeEmojis: "OSネイティブの絵文字を使用"
youHaveNoGroups: "グループがありません" youHaveNoGroups: "グループがありません"
joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。" joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。"
noHistory: "履歴はありません" noHistory: "履歴はありません"
signinHistory: "ログイン履歴"
disableAnimatedMfm: "動きのあるMFMを無効にする" disableAnimatedMfm: "動きのあるMFMを無効にする"
doing: "やっています" doing: "やっています"
category: "カテゴリ" category: "カテゴリ"
@ -492,6 +494,7 @@ none: "なし"
showInPage: "ページで表示" showInPage: "ページで表示"
popout: "ポップアウト" popout: "ポップアウト"
volume: "音量" volume: "音量"
masterVolume: "マスター音量"
details: "詳細" details: "詳細"
chooseEmoji: "絵文字を選択" chooseEmoji: "絵文字を選択"
unableToProcess: "操作を完了できません" unableToProcess: "操作を完了できません"
@ -564,7 +567,8 @@ useStarForReactionFallback: "リアクション絵文字が不明な場合、代
emailConfig: "メールサーバー設定" emailConfig: "メールサーバー設定"
enableEmail: "メール配信機能を有効化する" enableEmail: "メール配信機能を有効化する"
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
email: "メールアドレス" email: "メール"
emailAddress: "メールアドレス"
smtpConfig: "SMTP サーバーの設定" smtpConfig: "SMTP サーバーの設定"
smtpHost: "ホスト" smtpHost: "ホスト"
smtpPort: "ポート" smtpPort: "ポート"
@ -596,6 +600,7 @@ regenerateLoginTokenDescription: "ログインに使用される内部トーク
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
fileIdOrUrl: "ファイルIDまたはURL" fileIdOrUrl: "ファイルIDまたはURL"
chatOpenBehavior: "チャットを開くときの動作" chatOpenBehavior: "チャットを開くときの動作"
behavior: "動作"
sample: "サンプル" sample: "サンプル"
abuseReports: "通報" abuseReports: "通報"
reportAbuse: "通報" reportAbuse: "通報"
@ -619,6 +624,42 @@ createNew: "新規作成"
optional: "任意" optional: "任意"
createNewClip: "新しいクリップを作成" createNewClip: "新しいクリップを作成"
public: "パブリック" public: "パブリック"
i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
manageAccessTokens: "アクセストークンの管理"
accountInfo: "アカウント情報"
notesCount: "ノートの数"
repliesCount: "返信した数"
renotesCount: "Renoteした数"
repliedCount: "返信された数"
renotedCount: "Renoteされた数"
followingCount: "フォロー数"
followersCount: "フォロワー数"
sentReactionsCount: "リアクションした数"
receivedReactionsCount: "リアクションされた数"
pollVotesCount: "アンケートに投票した数"
pollVotedCount: "アンケートに投票された数"
yes: "はい"
no: "いいえ"
driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量"
noCrawle: "クローラーによるインデックスを拒否"
noCrawleDescription: "検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要請します。"
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
notSet: "未設定"
emailVerified: "メールアドレスが確認されました"
noteFavoritesCount: "お気に入りノートの数"
pageLikesCount: "Pageにいいねした数"
pageLikedCount: "Pageにいいねされた数"
reversiCount: "リバーシの対局数"
_nsfw:
respect: "閲覧注意のメディアは隠す"
ignore: "閲覧注意のメディアを隠さない"
force: "常にメディアを隠す"
_mfm: _mfm:
cheatSheet: "MFMチートシート" cheatSheet: "MFMチートシート"
@ -745,6 +786,8 @@ _theme:
manage: "テーマの管理" manage: "テーマの管理"
code: "テーマコード" code: "テーマコード"
installed: "{name}をインストールしました" installed: "{name}をインストールしました"
installedThemes: "インストールされたテーマ"
builtinThemes: "標準のテーマ"
alreadyInstalled: "そのテーマは既にインストールされています" alreadyInstalled: "そのテーマは既にインストールされています"
invalid: "テーマの形式が間違っています" invalid: "テーマの形式が間違っています"
make: "テーマを作る" make: "テーマを作る"
@ -820,6 +863,8 @@ _sfx:
chatBg: "チャット(バックグラウンド)" chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信" antenna: "アンテナ受信"
channel: "チャンネル通知" channel: "チャンネル通知"
reversiPutBlack: "リバーシ: 黒が打ったとき"
reversiPutWhite: "リバーシ: 白が打ったとき"
_ago: _ago:
unknown: "謎" unknown: "謎"
@ -999,7 +1044,9 @@ _profile:
username: "ユーザー名" username: "ユーザー名"
description: "自己紹介" description: "自己紹介"
youCanIncludeHashtags: "ハッシュタグを含めることができます。" youCanIncludeHashtags: "ハッシュタグを含めることができます。"
metadata: "補足情報" metadata: "追加情報"
metadataEdit: "追加情報を編集"
metadataDescription: "プロフィールに表として4つまでの追加情報を表示することができます。"
metadataLabel: "ラベル" metadataLabel: "ラベル"
metadataContent: "内容" metadataContent: "内容"

View File

@ -4,7 +4,7 @@ export class instancePinnedPages1605585339718 implements MigrationInterface {
name = 'instancePinnedPages1605585339718' name = 'instancePinnedPages1605585339718'
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{"/announcements", "/featured", "/channels", "/pages", "/explore", "/games/reversi", "/about-misskey"}'::varchar[]`); await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'::varchar[]`);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {

View File

@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instanceImages1605965516823 implements MigrationInterface {
name = 'instanceImages1605965516823'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "backgroundImageUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "logoImageUrl" character varying(512)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "logoImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "backgroundImageUrl"`);
}
}

View File

@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class noCrawle1606191203881 implements MigrationInterface {
name = 'noCrawle1606191203881'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "noCrawle" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "noCrawle"`);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,34 @@
// 常にメモリにロードしておく必要がないような設定情報を保管するストレージ
const PREFIX = 'miux:';
export const defaultDeviceSettings = {
sound_masterVolume: 0.3,
sound_note: { type: 'syuilo/down', volume: 1 },
sound_noteMy: { type: 'syuilo/up', volume: 1 },
sound_notification: { type: 'syuilo/pope2', volume: 1 },
sound_chat: { type: 'syuilo/pope1', volume: 1 },
sound_chatBg: { type: 'syuilo/waon', volume: 1 },
sound_antenna: { type: 'syuilo/triple', volume: 1 },
sound_channel: { type: 'syuilo/square-pico', volume: 1 },
sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
};
export const device = {
get<T extends keyof typeof defaultDeviceSettings>(key: T): typeof defaultDeviceSettings[T] {
// TODO: indexedDBにする
// ただしその際はnullチェックではなくキー存在チェックにしないとダメ
// (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
const value = localStorage.getItem(PREFIX + key);
if (value == null) {
return defaultDeviceSettings[key];
} else {
return JSON.parse(value);
}
},
set(key: keyof typeof defaultDeviceSettings, value: any): any {
localStorage.setItem(PREFIX + key, JSON.stringify(value));
},
};

View File

@ -1,6 +1,6 @@
<template> <template>
<XModalWindow ref="dialog" <XModalWindow ref="dialog"
:width="400" :width="450"
:can-close="false" :can-close="false"
:with-ok-button="true" :with-ok-button="true"
:ok-button-disabled="false" :ok-button-disabled="false"
@ -12,42 +12,61 @@
<template #header> <template #header>
{{ title }} {{ title }}
</template> </template>
<div class="xkpnjxcv _section"> <FormBase class="xkpnjxcv">
<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item"> <template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
<MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1"> <FormInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span> <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkInput> </FormInput>
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text"> <FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span> <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkInput> </FormInput>
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]"> <FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span> <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkTextarea> </FormTextarea>
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]"> <FormSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
<span v-text="form[item].label || item"></span> <span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template> <template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkSwitch> </FormSwitch>
</label> <FormSelect v-else-if="form[item].type === 'enum'" v-model:value="values[item]">
</div> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span></template>
<option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
</FormSelect>
<FormRange v-else-if="form[item].type === 'range'" v-model:value="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span></template>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormRange>
<FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
<span v-text="form[item].content || item"></span>
</FormButton>
</template>
</FormBase>
</XModalWindow> </XModalWindow>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue'; import XModalWindow from '@/components/ui/modal-window.vue';
import MkInput from './ui/input.vue'; import FormBase from './form/base.vue';
import MkTextarea from './ui/textarea.vue'; import FormInput from './form/input.vue';
import MkSwitch from './ui/switch.vue'; import FormTextarea from './form/textarea.vue';
import FormSwitch from './form/switch.vue';
import FormSelect from './form/select.vue';
import FormRange from './form/range.vue';
import FormButton from './form/button.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
XModalWindow, XModalWindow,
MkInput, FormBase,
MkTextarea, FormInput,
MkSwitch, FormTextarea,
FormSwitch,
FormSelect,
FormRange,
FormButton,
}, },
props: { props: {
@ -95,12 +114,6 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.xkpnjxcv { .xkpnjxcv {
> label {
display: block;
&:not(:last-child) {
margin-bottom: 32px;
}
}
} }
</style> </style>

View File

@ -0,0 +1,56 @@
<template>
<div class="rbusrurv" :class="{ wide: forceWide }" v-size="{ max: [400] }">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
forceWide: {
type: Boolean,
required: false,
default: false,
}
}
});
</script>
<style lang="scss" scoped>
.rbusrurv {
line-height: 1.4em;
background: var(--bg);
padding: 32px;
&:not(.wide).max-width_400px {
padding: 32px 0;
> ::v-deep(*) {
._formPanel {
border: solid 0.5px var(--divider);
border-radius: 0;
border-left: none;
border-right: none;
}
._form_group {
> * {
&:not(:first-child) {
&._formPanel, ._formPanel {
border-top: none;
}
}
&:not(:last-child) {
&._formPanel, ._formPanel {
border-bottom: solid 0.5px var(--divider);
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,81 @@
<template>
<div class="yzpgjkxe _formItem">
<div class="_formLabel"><slot name="label"></slot></div>
<button class="main _button _formPanel _formClickable" :class="{ center, primary, danger }">
<slot></slot>
<div class="suffix">
<slot name="suffix"></slot>
<div class="icon">
<slot name="suffixIcon"></slot>
</div>
</div>
</button>
<div class="_formCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({
props: {
primary: {
type: Boolean,
required: false,
default: false,
},
danger: {
type: Boolean,
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
center: {
type: Boolean,
required: false,
default: true,
}
},
});
</script>
<style lang="scss" scoped>
.yzpgjkxe {
> .main {
display: flex;
width: 100%;
box-sizing: border-box;
padding: 14px 16px;
text-align: left;
align-items: center;
&.center {
display: block;
text-align: center;
}
&.primary {
color: var(--accent);
}
&.danger {
color: #ff2a2a;
}
> .suffix {
display: inline-flex;
margin-left: auto;
opacity: 0.7;
> .icon {
margin-left: 1em;
}
}
}
}
</style>

View File

@ -0,0 +1,34 @@
._formPanel {
background: var(--panel);
border-radius: var(--radius);
&._formClickable {
&:hover {
background: var(--panelHighlight);
}
}
}
._formLabel {
font-size: 80%;
padding: 0 16px 8px 16px;
&:empty {
display: none;
}
}
._formCaption {
font-size: 80%;
padding: 8px 16px 0 16px;
&:empty {
display: none;
}
}
._formItem {
& + ._formItem {
margin-top: 24px;
}
}

View File

@ -0,0 +1,42 @@
<template>
<div class="vrtktovg _formItem" v-size="{ max: [500] }">
<div class="_formLabel"><slot name="label"></slot></div>
<div class="main _form_group">
<slot></slot>
</div>
<div class="_formCaption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.vrtktovg {
> .main {
> ::v-deep(*) {
margin: 0;
&:not(:first-child) {
&._formPanel, ._formPanel {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
&:not(:last-child) {
&._formPanel, ._formPanel {
border-bottom: solid 0.5px var(--divider);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}
}
}
</style>

View File

@ -0,0 +1,306 @@
<template>
<div class="ztzhwixg _formItem" :class="{ inline, disabled }">
<div class="_formLabel"><slot></slot></div>
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input _formPanel">
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
<input v-if="debounce" ref="inputEl"
v-debounce="500"
:type="type"
v-model.lazy="v"
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
:step="step"
@focus="focused = true"
@blur="focused = false"
@keydown="onKeydown($event)"
@input="onInput"
:list="id"
>
<input v-else ref="inputEl"
:type="type"
v-model="v"
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
:step="step"
@focus="focused = true"
@blur="focused = false"
@keydown="onKeydown($event)"
@input="onInput"
:list="id"
>
<datalist :id="id" v-if="datalist">
<option v-for="data in datalist" :value="data"/>
</datalist>
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
</div>
<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
<div class="_formCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import debounce from 'v-debounce';
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import './form.scss';
export default defineComponent({
directives: {
debounce
},
props: {
value: {
required: false
},
type: {
type: String,
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
placeholder: {
type: String,
required: false
},
autofocus: {
type: Boolean,
required: false,
default: false
},
autocomplete: {
required: false
},
spellcheck: {
required: false
},
step: {
required: false
},
debounce: {
required: false
},
datalist: {
type: Array,
required: false,
},
inline: {
type: Boolean,
required: false,
default: false
},
save: {
type: Function,
required: false,
},
},
emits: ['change', 'keydown', 'enter'],
setup(props, context) {
const { value, type, autofocus } = toRefs(props);
const v = ref(value.value);
const id = Math.random().toString(); // TODO: uuid?
const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = ref(null);
const prefixEl = ref(null);
const suffixEl = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
changed.value = true;
context.emit('change', ev);
};
const onKeydown = (ev: KeyboardEvent) => {
context.emit('keydown', ev);
if (ev.code === 'Enter') {
context.emit('enter');
}
};
watch(value, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (type?.value === 'number') {
context.emit('update:value', parseFloat(newValue));
} else {
context.emit('update:value', newValue);
}
invalid.value = inputEl.value.validity.badInput;
});
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
focus();
}
//
// 0
const clock = setInterval(() => {
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
}
}
if (suffixEl.value) {
if (suffixEl.value.offsetWidth) {
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
}
}
}, 100);
onUnmounted(() => {
clearInterval(clock);
});
});
});
return {
id,
v,
focused,
invalid,
changed,
filled,
inputEl,
prefixEl,
suffixEl,
focus,
onInput,
onKeydown,
faExclamationCircle,
};
},
});
</script>
<style lang="scss" scoped>
.ztzhwixg {
position: relative;
> .icon {
position: absolute;
top: 0;
left: 0;
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input {
margin-left: 28px;
}
}
> .input {
$height: 52px;
position: relative;
> input {
display: block;
height: $height;
width: 100%;
margin: 0;
padding: 0 16px;
font: inherit;
font-weight: normal;
font-size: 1em;
line-height: $height;
color: var(--inputText);
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
box-sizing: border-box;
&[type='file'] {
display: none;
}
}
> .prefix,
> .suffix {
display: block;
position: absolute;
z-index: 1;
top: 0;
padding: 0 16px;
font-size: 1em;
line-height: $height;
color: var(--inputLabel);
pointer-events: none;
&:empty {
display: none;
}
> * {
display: inline-block;
min-width: 16px;
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
> .prefix {
left: 0;
padding-right: 8px;
}
> .suffix {
right: 0;
padding-left: 8px;
}
}
> .save {
margin: 6px 0 0 0;
font-size: 0.8em;
}
&.inline {
display: inline-block;
margin: 0;
}
&.disabled {
opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
}
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="_formItem">
<div class="_formPanel anocepby">
<span class="key"><slot name="key"></slot></span>
<span class="value"><slot name="value"></slot></span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.anocepby {
display: flex;
align-items: center;
padding: 14px 16px;
> .value {
margin-left: auto;
opacity: 0.7;
}
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<div class="qmfkfnzi _formItem">
<a class="main _button _formPanel _formClickable" :href="to" target="_blank" v-if="external">
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span>
<Fa :icon="faExternalLinkAlt" class="right"/>
</a>
<MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" v-else>
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span>
<Fa :icon="faChevronRight" class="right"/>
</MkA>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faChevronRight, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
import './form.scss';
export default defineComponent({
props: {
to: {
type: String,
required: true
},
active: {
type: Boolean,
required: false
},
external: {
type: Boolean,
required: false
},
},
data() {
return {
faChevronRight, faExternalLinkAlt
};
}
});
</script>
<style lang="scss" scoped>
.qmfkfnzi {
> .main {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 14px 16px 14px 14px;
&:hover {
text-decoration: none;
}
&.active {
color: var(--accent);
}
> .icon {
width: 32px;
margin-right: 2px;
flex-shrink: 0;
text-align: center;
opacity: 0.8;
&:empty {
display: none;
& + .text {
padding-left: 4px;
}
}
}
> .text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding-right: 12px;
}
> .right {
margin-left: auto;
opacity: 0.7;
}
}
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<FormGroup class="uljviswt _formItem">
<template #label><slot name="label"></slot></template>
<slot :items="items"></slot>
<div class="empty" v-if="empty" key="_empty_">
<slot name="empty"></slot>
</div>
<FormButton v-show="more" class="button" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</FormButton>
</FormGroup>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FormButton from './button.vue';
import FormGroup from './group.vue';
import paging from '@/scripts/paging';
export default defineComponent({
components: {
FormButton,
FormGroup,
},
mixins: [
paging({}),
],
props: {
pagination: {
required: true
},
},
});
</script>
<style lang="scss" scoped>
.uljviswt {
}
</style>

View File

@ -0,0 +1,106 @@
<script lang="ts">
import { defineComponent, h } from 'vue';
import MkRadio from '@/components/ui/radio.vue';
import './form.scss';
export default defineComponent({
components: {
MkRadio
},
props: {
modelValue: {
required: false
},
},
data() {
return {
value: this.modelValue,
}
},
watch: {
value() {
this.$emit('update:modelValue', this.value);
}
},
render() {
const label = this.$slots.desc();
const options = this.$slots.default();
return h('div', {
class: 'cnklmpwm _formItem'
}, [
h('div', {
class: '_formLabel',
}, label),
...options.map(option => h('button', {
class: '_button _formPanel _formClickable',
key: option.props.value,
onClick: () => this.value = option.props.value,
}, [h('span', {
class: ['check', { checked: this.value === option.props.value }],
}), option.children]))
]);
}
});
</script>
<style lang="scss">
.cnklmpwm {
> button {
display: block;
width: 100%;
box-sizing: border-box;
padding: 14px 18px;
text-align: left;
&:not(:first-of-type) {
border-top: none !important;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&:not(:last-of-type) {
border-bottom: solid 0.5px var(--divider);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
> .check {
display: inline-block;
vertical-align: bottom;
position: relative;
width: 20px;
height: 20px;
margin-right: 8px;
background: none;
border: 2px solid var(--inputBorder);
border-radius: 100%;
transition: inherit;
&:after {
content: "";
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: .4s cubic-bezier(.25,.8,.25,1);
}
&.checked {
border-color: var(--accent);
&:after {
background-color: var(--accent);
transform: scale(1);
opacity: 1;
}
}
}
}
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div class="ifitouly _formItem" :class="{ focused, disabled }">
<div class="_formLabel"><slot name="label"></slot></div>
<div class="_formPanel main">
<input
type="range"
ref="input"
v-model="v"
:disabled="disabled"
:min="min"
:max="max"
:step="step"
@focus="focused = true"
@blur="focused = false"
@input="$emit('update:value', $event.target.value)"
/>
</div>
<div class="_formCaption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
value: {
type: Number,
required: false,
default: 0
},
disabled: {
type: Boolean,
required: false,
default: false
},
min: {
type: Number,
required: false,
default: 0
},
max: {
type: Number,
required: false,
default: 100
},
step: {
type: Number,
required: false,
default: 1
},
},
data() {
return {
v: this.value,
focused: false
};
},
watch: {
value(v) {
this.v = parseFloat(v);
}
},
});
</script>
<style lang="scss" scoped>
.ifitouly {
position: relative;
> .main {
padding: 24px 16px;
> input {
display: block;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: var(--X10);
height: 4px;
width: 100%;
box-sizing: border-box;
margin: 0;
outline: 0;
border: 0;
border-radius: 7px;
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
box-sizing: content-box;
}
&::-moz-range-thumb {
-moz-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
}
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div class="yrtfrpux _formItem" :class="{ disabled, inline }">
<div class="_formLabel"><slot name="label"></slot></div>
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input _formPanel _formClickable" @click="focus">
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<select ref="input"
v-model="v"
:required="required"
:disabled="disabled"
@focus="focused = true"
@blur="focused = false"
>
<slot></slot>
</select>
<div class="suffix">
<Fa :icon="faChevronDown"/>
</div>
</div>
<div class="_formCaption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
import './form.scss';
export default defineComponent({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
inline: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
faChevronDown,
};
},
computed: {
v: {
get() {
return this.value;
},
set(v) {
this.$emit('update:value', v);
}
},
},
methods: {
focus() {
this.$refs.input.focus();
}
}
});
</script>
<style lang="scss" scoped>
.yrtfrpux {
position: relative;
> .icon {
position: absolute;
top: 0;
left: 0;
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input {
margin-left: 28px;
}
}
> .input {
display: flex;
position: relative;
> select {
display: block;
flex: 1;
width: 100%;
padding: 0 16px;
font: inherit;
font-weight: normal;
font-size: 1em;
height: 52px;
background: none;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
appearance: none;
-webkit-appearance: none;
color: var(--fg);
option,
optgroup {
color: var(--fg);
background: var(--bg);
}
}
> .prefix,
> .suffix {
display: block;
align-self: center;
justify-self: center;
font-size: 1em;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
&:empty {
display: none;
}
> * {
display: block;
min-width: 16px;
}
}
> .prefix {
padding-right: 4px;
}
> .suffix {
padding: 0 16px 0 0;
opacity: 0.7;
}
}
}
</style>

View File

@ -0,0 +1,132 @@
<template>
<div class="ijnpvmgr _formItem">
<div class="main _formPanel _formClickable"
:class="{ disabled, checked }"
:aria-checked="checked"
:aria-disabled="disabled"
@click.prevent="toggle"
>
<input
type="checkbox"
ref="input"
:disabled="disabled"
@keydown.enter="toggle"
>
<span class="button">
<span></span>
</span>
<span class="label">
<span><slot></slot></span>
</span>
</div>
<div class="_formCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({
props: {
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.value;
}
},
methods: {
toggle() {
if (this.disabled) return;
this.$emit('update:value', !this.checked);
}
}
});
</script>
<style lang="scss" scoped>
.ijnpvmgr {
> .main {
position: relative;
display: flex;
padding: 16px;
cursor: pointer;
> * {
user-select: none;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--X10);
border-color: var(--X10);
> * {
background-color: var(--accent);
transform: translateX(14px);
}
}
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative;
display: inline-block;
flex-shrink: 0;
margin: 3px 0 0 0;
width: 34px;
height: 14px;
background: var(--X6);
outline: none;
border-radius: 14px;
transition: all 0.3s;
cursor: pointer;
> * {
position: absolute;
top: -3px;
left: 0;
border-radius: 100%;
transition: background-color 0.3s, transform 0.3s;
width: 20px;
height: 20px;
background-color: #fff;
box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12);
}
}
> .label {
margin-left: 12px;
display: block;
transition: inherit;
color: var(--fg);
> span {
display: block;
line-height: 20px;
transition: inherit;
}
}
}
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<div class="rivhosbp _formItem" :class="{ tall, pre }">
<div class="_formLabel"><slot></slot></div>
<div class="input _formPanel">
<textarea ref="input" :class="{ code, _monospace: code }"
:value="value"
:required="required"
:readonly="readonly"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="!code"
@input="onInput"
@focus="focused = true"
@blur="focused = false"
></textarea>
</div>
<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
<div class="_formCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
autocomplete: {
type: String,
required: false
},
code: {
type: Boolean,
required: false
},
tall: {
type: Boolean,
required: false,
default: false
},
pre: {
type: Boolean,
required: false,
default: false
},
save: {
type: Function,
required: false,
},
},
data() {
return {
changed: false,
}
},
methods: {
focus() {
this.$refs.input.focus();
},
onInput(ev) {
this.changed = true;
this.$emit('update:value', ev.target.value);
}
}
});
</script>
<style lang="scss" scoped>
.rivhosbp {
position: relative;
> .input {
position: relative;
> textarea {
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 130px;
margin: 0;
padding: 16px;
box-sizing: border-box;
font: inherit;
font-weight: normal;
font-size: 1em;
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
color: var(--fg);
&.code {
tab-size: 2;
}
}
}
> .save {
margin: 6px 0 0 0;
font-size: 0.8em;
}
&.tall {
> .input {
> textarea {
min-height: 200px;
}
}
}
&.pre {
> .input {
> textarea {
white-space: pre;
}
}
}
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div class="wthhikgt _formItem" v-size="{ max: [500] }">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.wthhikgt {
position: relative;
display: flex;
> ::v-deep(*) {
flex: 1;
margin: 0;
&:not(:last-child) {
margin-right: 16px;
}
}
&.max-width_500px {
display: block;
> ::v-deep(*) {
margin: inherit;
}
}
}
</style>

View File

@ -68,7 +68,7 @@ export default defineComponent({
created() { created() {
// Plugin:register_note_view_interruptor 使watch // Plugin:register_note_view_interruptor 使watch
this.$watch('image', () => { this.$watch('image', () => {
this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw; this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
if (this.image.blurhash) { if (this.image.blurhash) {
this.color = extractAvgColorFromBlurhash(this.image.blurhash); this.color = extractAvgColorFromBlurhash(this.image.blurhash);
} }

View File

@ -48,7 +48,7 @@ export default defineComponent({
} }
}, },
created() { created() {
this.hide = this.video.isSensitive && !this.$store.state.device.alwaysShowNsfw; this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
}, },
}); });
</script> </script>

View File

@ -14,8 +14,8 @@
<option value="res">Response</option> <option value="res">Response</option>
</MkTab> </MkTab>
<code v-if="tab === 'req'">{{ reqStr }}</code> <code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code>
<code v-if="tab === 'res'">{{ resStr }}</code> <code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code>
</div> </div>
</XWindow> </XWindow>
</template> </template>
@ -67,7 +67,6 @@ export default defineComponent({
font-size: 0.9em; font-size: 0.9em;
tab-size: 2; tab-size: 2;
white-space: pre; white-space: pre;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
} }
} }
</style> </style>

View File

@ -3,7 +3,7 @@
<template #header> <template #header>
<Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager <Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager
</template> </template>
<div class="qljqmnzj"> <div class="qljqmnzj _monospace">
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);"> <MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="windows">Windows</option> <option value="windows">Windows</option>
<option value="stream">Stream</option> <option value="stream">Stream</option>
@ -150,7 +150,6 @@ export default defineComponent({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
> .content { > .content {
flex: 1; flex: 1;

View File

@ -6,6 +6,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import XNotes from './notes.vue'; import XNotes from './notes.vue';
import * as os from '@/os'; import * as os from '@/os';
import * as sound from '@/scripts/sound';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -65,7 +66,7 @@ export default defineComponent({
this.$emit('note'); this.$emit('note');
if (this.sound) { if (this.sound) {
os.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note'); sound.play(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
} }
}; };

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="timctyfi" :class="{ focused, disabled }"> <div class="timctyfi" :class="{ focused, disabled }">
<div class="icon"><slot name="icon"></slot></div> <div class="icon"><slot name="icon"></slot></div>
<span class="title"><slot name="title"></slot></span> <span class="label"><slot name="label"></slot></span>
<input <input
type="range" type="range"
ref="input" ref="input"
@ -19,7 +19,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue';import * as os from '@/os'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {

View File

@ -17,10 +17,8 @@
<span></span> <span></span>
</span> </span>
<span class="label"> <span class="label">
<span :aria-hidden="!checked"><slot></slot></span> <span><slot></slot></span>
<p :aria-hidden="!checked"> <p><slot name="desc"></slot></p>
<slot name="desc"></slot>
</p>
</span> </span>
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@
<div class="adhpbeos" :class="{ focused, filled, tall, pre }"> <div class="adhpbeos" :class="{ focused, filled, tall, pre }">
<div class="input"> <div class="input">
<span class="label" ref="label"><slot></slot></span> <span class="label" ref="label"><slot></slot></span>
<textarea ref="input" :class="{ code }" <textarea ref="input" :class="{ code, _monospace: code }"
:value="value" :value="value"
:required="required" :required="required"
:readonly="readonly" :readonly="readonly"
@ -166,7 +166,6 @@ export default defineComponent({
&.code { &.code {
tab-size: 2; tab-size: 2;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
} }
} }
} }

View File

@ -16,7 +16,8 @@ import { router } from './router';
import { applyTheme } from '@/scripts/theme'; import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n, lang } from './i18n'; import { i18n, lang } from './i18n';
import { stream, sound, isMobile, dialog } from '@/os'; import { stream, isMobile, dialog } from '@/os';
import * as sound from './scripts/sound';
console.info(`Misskey v${version}`); console.info(`Misskey v${version}`);
@ -50,7 +51,7 @@ if (_DEV_) {
document.addEventListener('touchend', () => {}, { passive: true }); document.addEventListener('touchend', () => {}, { passive: true });
if (localStorage.getItem('theme') == null) { if (localStorage.getItem('theme') == null) {
applyTheme(require('@/themes/l-white.json5')); applyTheme(require('@/themes/l-light.json5'));
} }
//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ //#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
@ -307,7 +308,7 @@ if (store.getters.isSignedIn) {
hasUnreadMessagingMessage: true hasUnreadMessagingMessage: true
}); });
sound('chatBg'); sound.play('chatBg');
}); });
main.on('readAllAntennas', () => { main.on('readAllAntennas', () => {
@ -321,7 +322,7 @@ if (store.getters.isSignedIn) {
hasUnreadAntenna: true hasUnreadAntenna: true
}); });
sound('antenna'); sound.play('antenna');
}); });
main.on('readAllAnnouncements', () => { main.on('readAllAnnouncements', () => {
@ -341,7 +342,7 @@ if (store.getters.isSignedIn) {
hasUnreadChannel: true hasUnreadChannel: true
}); });
sound('channel'); sound.play('channel');
}); });
main.on('readAllAnnouncements', () => { main.on('readAllAnnouncements', () => {

View File

@ -6,6 +6,7 @@ import { apiUrl, debug } from '@/config';
import MkPostFormDialog from '@/components/post-form-dialog.vue'; import MkPostFormDialog from '@/components/post-form-dialog.vue';
import MkWaitingDialog from '@/components/waiting-dialog.vue'; import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { resolve } from '@/router'; import { resolve } from '@/router';
import { device } from './cold-storage';
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
export const isMobile = /mobile|iphone|ipad|android/.test(ua); export const isMobile = /mobile|iphone|ipad|android/.test(ua);
@ -344,15 +345,6 @@ export function post(props: Record<string, any>) {
}); });
} }
export function sound(type: string) {
if (store.state.device.sfxVolume === 0) return;
const sound = store.state.device['sfx' + type.substr(0, 1).toUpperCase() + type.substr(1)];
if (sound == null) return;
const audio = new Audio(`/assets/sounds/${sound}.mp3`);
audio.volume = store.state.device.sfxVolume;
audio.play();
}
export const deckGlobalEvents = new EventEmitter(); export const deckGlobalEvents = new EventEmitter();
export const uploads = ref([]); export const uploads = ref([]);

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="_section"> <div class="_section">
<MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content" ref="list"> <MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content">
<section class="_card announcement _vMargin" v-for="(announcement, i) in items" :key="announcement.id"> <section class="_card announcement _vMargin" v-for="(announcement, i) in items" :key="announcement.id">
<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> <div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<div class="_content"> <div class="_content">

View File

@ -7,6 +7,8 @@
<MkTextarea v-model:value="description">{{ $t('instanceDescription') }}</MkTextarea> <MkTextarea v-model:value="description">{{ $t('instanceDescription') }}</MkTextarea>
<MkInput v-model:value="iconUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('iconUrl') }}</MkInput> <MkInput v-model:value="iconUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('iconUrl') }}</MkInput>
<MkInput v-model:value="bannerUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</MkInput> <MkInput v-model:value="bannerUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</MkInput>
<MkInput v-model:value="backgroundImageUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('backgroundImageUrl') }}</MkInput>
<MkInput v-model:value="logoImageUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('logoImageUrl') }}</MkInput>
<MkInput v-model:value="tosUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('tosUrl') }}</MkInput> <MkInput v-model:value="tosUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('tosUrl') }}</MkInput>
<MkInput v-model:value="maintainerName">{{ $t('maintainerName') }}</MkInput> <MkInput v-model:value="maintainerName">{{ $t('maintainerName') }}</MkInput>
<MkInput v-model:value="maintainerEmail" type="email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</MkInput> <MkInput v-model:value="maintainerEmail" type="email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</MkInput>
@ -292,6 +294,8 @@ export default defineComponent({
email: null, email: null,
bannerUrl: null, bannerUrl: null,
iconUrl: null, iconUrl: null,
logoImageUrl: null,
backgroundImageUrl: null,
maxNoteTextLength: 0, maxNoteTextLength: 0,
enableRegistration: false, enableRegistration: false,
enableLocalTimeline: false, enableLocalTimeline: false,
@ -345,6 +349,8 @@ export default defineComponent({
this.tosUrl = this.meta.tosUrl; this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl; this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl; this.iconUrl = this.meta.iconUrl;
this.logoImageUrl = this.meta.logoImageUrl;
this.backgroundImageUrl = this.meta.backgroundImageUrl;
this.enableEmail = this.meta.enableEmail; this.enableEmail = this.meta.enableEmail;
this.email = this.meta.email; this.email = this.meta.email;
this.maintainerName = this.meta.maintainerName; this.maintainerName = this.meta.maintainerName;
@ -498,6 +504,8 @@ export default defineComponent({
tosUrl: this.tosUrl, tosUrl: this.tosUrl,
bannerUrl: this.bannerUrl, bannerUrl: this.bannerUrl,
iconUrl: this.iconUrl, iconUrl: this.iconUrl,
logoImageUrl: this.logoImageUrl,
backgroundImageUrl: this.backgroundImageUrl,
maintainerName: this.maintainerName, maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail, maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength, maxNoteTextLength: this.maxNoteTextLength,

View File

@ -38,6 +38,7 @@ import parseAcct from '../../../misc/acct/parse';
import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll'; import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
import * as os from '@/os'; import * as os from '@/os';
import { popout } from '@/scripts/popout'; import { popout } from '@/scripts/popout';
import * as sound from '@/scripts/sound';
const Component = defineComponent({ const Component = defineComponent({
components: { components: {
@ -218,7 +219,7 @@ const Component = defineComponent({
}, },
onMessage(message) { onMessage(message) {
os.sound('chat'); sound.play('chat');
const _isBottom = isBottom(this.$el, 64); const _isBottom = isBottom(this.$el, 64);

View File

@ -94,6 +94,7 @@ import { url } from '@/config';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import { userPage } from '@/filters/user'; import { userPage } from '@/filters/user';
import * as os from '@/os'; import * as os from '@/os';
import * as sound from '@/scripts/sound';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -245,11 +246,7 @@ export default defineComponent({
this.o.put(this.myColor, pos); this.o.put(this.myColor, pos);
// //
if (this.$store.state.device.enableSounds) { sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite');
const sound = new Audio(`${url}/assets/reversi-put-me.mp3`);
sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
this.connection.send('set', { this.connection.send('set', {
pos: pos pos: pos
@ -268,10 +265,8 @@ export default defineComponent({
this.$forceUpdate(); this.$forceUpdate();
// //
if (this.$store.state.device.enableSounds && x.color != this.myColor) { if (x.color !== this.myColor) {
const sound = new Audio(`${url}/assets/reversi-put-you.mp3`); sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
sound.volume = this.$store.state.device.soundVolume;
sound.play();
} }
}, },

View File

@ -75,14 +75,25 @@ import MkButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue'; import MkInfo from '@/components/ui/info.vue';
import MkInput from '@/components/ui/input.vue'; import MkInput from '@/components/ui/input.vue';
import MkSwitch from '@/components/ui/switch.vue'; import MkSwitch from '@/components/ui/switch.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
FormBase,
MkButton, MkInfo, MkInput, MkSwitch MkButton, MkInfo, MkInput, MkSwitch
}, },
emits: ['info'],
data() { data() {
return { return {
INFO: {
title: this.$t('twoStepAuthentication'),
icon: faLock
},
data: null, data: null,
supportsCredentials: !!navigator.credentials, supportsCredentials: !!navigator.credentials,
usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin, usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
@ -92,6 +103,7 @@ export default defineComponent({
faLock faLock
}; };
}, },
methods: { methods: {
register() { register() {
os.dialog({ os.dialog({
@ -225,6 +237,7 @@ export default defineComponent({
}); });
}); });
}, },
updatePasswordLessLogin() { updatePasswordLessLogin() {
os.api('i/2fa/password-less', { os.api('i/2fa/password-less', {
value: !!this.usePasswordLessLogin value: !!this.usePasswordLessLogin

View File

@ -0,0 +1,185 @@
<template>
<FormBase>
<FormKeyValueView>
<template #key>ID</template>
<template #value><span class="_monospace">{{ $store.state.i.id }}</span></template>
</FormKeyValueView>
<FormGroup>
<FormKeyValueView>
<template #key>{{ $t('registeredDate') }}</template>
<template #value><MkTime :time="$store.state.i.createdAt" mode="detail"/></template>
</FormKeyValueView>
</FormGroup>
<FormGroup v-if="stats">
<template #label>{{ $t('statistics') }}</template>
<FormKeyValueView>
<template #key>{{ $t('notesCount') }}</template>
<template #value>{{ number(stats.notesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('repliesCount') }}</template>
<template #value>{{ number(stats.repliesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('renotesCount') }}</template>
<template #value>{{ number(stats.renotesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('repliedCount') }}</template>
<template #value>{{ number(stats.repliedCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('renotedCount') }}</template>
<template #value>{{ number(stats.renotedCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('pollVotesCount') }}</template>
<template #value>{{ number(stats.pollVotesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('pollVotedCount') }}</template>
<template #value>{{ number(stats.pollVotedCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('sentReactionsCount') }}</template>
<template #value>{{ number(stats.sentReactionsCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('receivedReactionsCount') }}</template>
<template #value>{{ number(stats.receivedReactionsCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('noteFavoritesCount') }}</template>
<template #value>{{ number(stats.noteFavoritesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followingCount') }}</template>
<template #value>{{ number(stats.followingCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followingCount') }} ({{ $t('local') }})</template>
<template #value>{{ number(stats.localFollowingCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followingCount') }} ({{ $t('remote') }})</template>
<template #value>{{ number(stats.remoteFollowingCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followersCount') }}</template>
<template #value>{{ number(stats.followersCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followersCount') }} ({{ $t('local') }})</template>
<template #value>{{ number(stats.localFollowersCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followersCount') }} ({{ $t('remote') }})</template>
<template #value>{{ number(stats.remoteFollowersCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('pageLikesCount') }}</template>
<template #value>{{ number(stats.pageLikesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('pageLikedCount') }}</template>
<template #value>{{ number(stats.pageLikedCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('driveFilesCount') }}</template>
<template #value>{{ number(stats.driveFilesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('driveUsage') }}</template>
<template #value>{{ bytes(stats.driveUsage) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('reversiCount') }}</template>
<template #value>{{ number(stats.reversiCount) }}</template>
</FormKeyValueView>
</FormGroup>
<FormGroup>
<template #label>{{ $t('other') }}</template>
<FormKeyValueView>
<template #key>emailVerified</template>
<template #value>{{ $store.state.i.emailVerified ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>twoFactorEnabled</template>
<template #value>{{ $store.state.i.twoFactorEnabled ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>securityKeys</template>
<template #value>{{ $store.state.i.securityKeys ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>usePasswordLessLogin</template>
<template #value>{{ $store.state.i.usePasswordLessLogin ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>isModerator</template>
<template #value>{{ $store.state.i.isModerator ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>isAdmin</template>
<template #value>{{ $store.state.i.isAdmin ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import FormKeyValueView from '@/components/form/key-value-view.vue';
import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
export default defineComponent({
components: {
FormBase,
FormSelect,
FormSwitch,
FormButton,
FormLink,
FormGroup,
FormKeyValueView,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('accountInfo'),
icon: faInfoCircle
},
stats: null
}
},
mounted() {
this.$emit('info', this.INFO);
os.api('users/stats', {
userId: this.$store.state.i.id
}).then(stats => {
this.stats = stats;
});
},
methods: {
number,
bytes,
}
});
</script>

View File

@ -1,26 +1,27 @@
<template> <template>
<div> <FormBase>
<div class="_section"> <FormButton @click="generateToken" primary>{{ $t('generateAccessToken') }}</FormButton>
<div class="_content"> <FormLink to="/settings/apps">{{ $t('manageAccessTokens') }}</FormLink>
<MkButton @click="generateToken">{{ $t('generateAccessToken') }}</MkButton> <FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink>
</div> </FormBase>
</div>
<div class="_section">
<MkA to="/api-console" :behavior="isDesktop ? 'window' : null">API console</MkA>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faKey } from '@fortawesome/free-solid-svg-icons'; import { faKey } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue'; import FormSwitch from '@/components/form/switch.vue';
import MkInput from '@/components/ui/input.vue'; import FormSelect from '@/components/form/select.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, MkInput FormBase,
FormButton,
FormLink,
}, },
emits: ['info'], emits: ['info'],

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <FormBase>
<MkPagination :pagination="pagination" class="bfomjevm" ref="list"> <FormPagination :pagination="pagination" ref="list">
<template #empty> <template #empty>
<div class="_fullinfo"> <div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
@ -8,8 +8,8 @@
</div> </div>
</template> </template>
<template #default="{items}"> <template #default="{items}">
<div class="token _panel" v-for="token in items" :key="token.id"> <div class="_formPanel bfomjevm" v-for="token in items" :key="token.id">
<img class="icon" :src="token.iconUrl" alt=""/> <img class="icon" :src="token.iconUrl" alt="" v-if="token.iconUrl"/>
<div class="body"> <div class="body">
<div class="name">{{ token.name }}</div> <div class="name">{{ token.name }}</div>
<div class="description">{{ token.description }}</div> <div class="description">{{ token.description }}</div>
@ -33,21 +33,29 @@
</div> </div>
</div> </div>
</template> </template>
</MkPagination> </FormPagination>
</div> </FormBase>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons'; import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
import MkPagination from '@/components/ui/pagination.vue'; import FormPagination from '@/components/form/pagination.vue';
import FormSelect from '@/components/form/select.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkPagination FormBase,
FormPagination,
}, },
emits: ['info'],
data() { data() {
return { return {
INFO: { INFO: {
@ -65,6 +73,10 @@ export default defineComponent({
}; };
}, },
mounted() {
this.$emit('info', this.INFO);
},
methods: { methods: {
revoke(token) { revoke(token) {
os.api('i/revoke-token', { tokenId: token.id }).then(() => { os.api('i/revoke-token', { tokenId: token.id }).then(() => {
@ -77,26 +89,24 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.bfomjevm { .bfomjevm {
> .token { display: flex;
display: flex; padding: 16px;
padding: 16px;
> .icon { > .icon {
display: block; display: block;
flex-shrink: 0; flex-shrink: 0;
margin: 0 12px 0 0; margin: 0 12px 0 0;
width: 50px; width: 50px;
height: 50px; height: 50px;
border-radius: 8px; border-radius: 8px;
} }
> .body { > .body {
width: calc(100% - 62px); width: calc(100% - 62px);
position: relative; position: relative;
> .name { > .name {
font-weight: bold; font-weight: bold;
}
} }
} }
} }

View File

@ -0,0 +1,90 @@
<template>
<FormBase>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faColumns"/> </div>
<div class="_content">
<div>{{ $t('defaultNavigationBehaviour') }}</div>
<MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch>
</div>
<div class="_content">
<MkSwitch v-model:value="deckAlwaysShowMainColumn">
{{ $t('_deck.alwaysShowMainColumn') }}
</MkSwitch>
</div>
<div class="_content">
<div>{{ $t('_deck.columnAlign') }}</div>
<MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
<MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
</div>
</section>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faImage, faCog, faColumns } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkSelect from '@/components/ui/select.vue';
import MkRadio from '@/components/ui/radio.vue';
import MkRadios from '@/components/ui/radios.vue';
import MkRange from '@/components/ui/range.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import { clientDb, set } from '@/db';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
MkSelect,
MkRadio,
MkRadios,
MkRange,
FormSwitch,
FormSelect,
FormRadios,
FormBase,
FormGroup,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('deck'),
icon: faColumns
},
faImage, faCog,
}
},
computed: {
deckNavWindow: {
get() { return this.$store.state.device.deckNavWindow; },
set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); }
},
deckAlwaysShowMainColumn: {
get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
},
deckColumnAlign: {
get() { return this.$store.state.device.deckColumnAlign; },
set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
},
},
mounted() {
this.$emit('info', this.INFO);
},
});
</script>

View File

@ -0,0 +1,71 @@
<template>
<FormBase>
<FormGroup>
<FormInput v-model:value="emailAddress" type="email">
{{ $t('emailAddress') }}
<template #desc v-if="$store.state.i.email && !$store.state.i.emailVerified">{{ $t('verificationEmailSent') }}</template>
<template #desc v-else-if="emailAddress === $store.state.i.email && $store.state.i.emailVerified">{{ $t('emailVerified') }}</template>
</FormInput>
</FormGroup>
<FormButton @click="save" primary>{{ $t('save') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import FormButton from '@/components/form/button.vue';
import FormInput from '@/components/form/input.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
export default defineComponent({
components: {
FormBase,
FormInput,
FormButton,
FormGroup,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('emailAddress'),
icon: faEnvelope
},
emailAddress: null,
code: null,
faCog
}
},
created() {
this.emailAddress = this.$store.state.i.email;
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
save() {
os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/update-email', {
password: password,
email: this.emailAddress,
});
});
}
}
});
</script>

View File

@ -0,0 +1,52 @@
<template>
<FormBase>
<FormGroup>
<template #label>{{ $t('emailAddress') }}</template>
<FormLink to="/settings/email/address">
<template v-if="$store.state.i.email && !$store.state.i.emailVerified" #icon><Fa :icon="faExclamationTriangle" style="color: var(--warn);"/></template>
<template v-else-if="$store.state.i.email && $store.state.i.emailVerified" #icon><Fa :icon="faCheck" style="color: var(--success);"/></template>
{{ $store.state.i.email || $t('notSet') }}
</FormLink>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCog, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import FormButton from '@/components/form/button.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
export default defineComponent({
components: {
FormBase,
FormLink,
FormButton,
FormGroup,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('email'),
icon: faEnvelope
},
faCog, faExclamationTriangle, faCheck
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
}
});
</script>

View File

@ -1,109 +1,110 @@
<template> <template>
<div class=""> <FormBase>
<section class="_card _vMargin"> <FormSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</FormSwitch>
<div class="_title"><Fa :icon="faCog"/> {{ $t('general') }}</div>
<div class="_content">
<MkRadios v-model="serverDisconnectedBehavior">
<template #desc>{{ $t('whenServerDisconnected') }}</template>
<option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option>
<option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option>
<option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option>
</MkRadios>
<MkSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</MkSwitch>
<MkSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</MkSwitch>
<MkSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</MkSwitch>
<MkSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</MkSwitch>
<MkSelect v-model:value="lang">
<template #label>{{ $t('uiLanguage') }}</template>
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
</MkSelect>
</div>
</section>
<section class="_card _vMargin"> <FormSelect v-model:value="lang">
<div class="_title"><Fa :icon="faCog"/> {{ $t('defaultNavigationBehaviour') }}</div> <template #label>{{ $t('uiLanguage') }}</template>
<div class="_content"> <option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
<MkSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</MkSwitch> <template #caption>
</div> <i18n-t keypath="i18nInfo" tag="span">
<div class="_content"> <template #link>
<MkRadios v-model="chatOpenBehavior"> <MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
<template #desc>{{ $t('chatOpenBehavior') }}</template> </template>
<option value="page">{{ $t('showInPage') }}</option> </i18n-t>
<option value="window">{{ $t('openInWindow') }}</option> </template>
<option value="popout">{{ $t('popout') }}</option> </FormSelect>
</MkRadios>
</div>
</section>
<section class="_card _vMargin"> <FormGroup>
<div class="_title"><Fa :icon="faCog"/> {{ $t('appearance') }}</div> <template #label>{{ $t('behavior') }}</template>
<div class="_content"> <FormSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</FormSwitch>
<MkSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</MkSwitch> <FormSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</FormSwitch>
<MkSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</MkSwitch> <FormSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</FormSwitch>
<MkSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</MkSwitch> </FormGroup>
<MkSwitch v-model:value="useOsNativeEmojis">
{{ $t('useOsNativeEmojis') }}
<template #desc><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
</MkSwitch>
<MkRadios v-model="fontSize">
<template #desc>{{ $t('fontSize') }}</template>
<option value="small"><span style="font-size: 14px;">Aa</span></option>
<option :value="null"><span style="font-size: 16px;">Aa</span></option>
<option value="large"><span style="font-size: 18px;">Aa</span></option>
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
</MkRadios>
<MkRadios v-model="instanceTicker">
<template #desc>{{ $t('instanceTicker') }}</template>
<option value="none">{{ $t('_instanceTicker.none') }}</option>
<option value="remote">{{ $t('_instanceTicker.remote') }}</option>
<option value="always">{{ $t('_instanceTicker.always') }}</option>
</MkRadios>
</div>
</section>
<section class="_card _vMargin"> <FormSelect v-model:value="serverDisconnectedBehavior">
<div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</div> <template #label>{{ $t('whenServerDisconnected') }}</template>
<div class="_content"> <option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option>
<div>{{ $t('defaultNavigationBehaviour') }}</div> <option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option>
<MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch> <option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option>
</div> </FormSelect>
<div class="_content">
<MkSwitch v-model:value="deckAlwaysShowMainColumn">
{{ $t('_deck.alwaysShowMainColumn') }}
</MkSwitch>
</div>
<div class="_content">
<div>{{ $t('_deck.columnAlign') }}</div>
<MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
<MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
</div>
</section>
<MkButton @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</MkButton> <FormGroup>
</div> <template #label>{{ $t('appearance') }}</template>
<FormSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</FormSwitch>
<FormSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</FormSwitch>
<FormSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</FormSwitch>
<FormSwitch v-model:value="useOsNativeEmojis">{{ $t('useOsNativeEmojis') }}
<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
</FormSwitch>
<FormSwitch v-model:value="loadRawImages">{{ $t('loadRawImages') }}</FormSwitch>
<FormSwitch v-model:value="disableShowingAnimatedImages">{{ $t('disableShowingAnimatedImages') }}</FormSwitch>
</FormGroup>
<FormRadios v-model="fontSize">
<template #desc>{{ $t('fontSize') }}</template>
<option value="small"><span style="font-size: 14px;">Aa</span></option>
<option :value="null"><span style="font-size: 16px;">Aa</span></option>
<option value="large"><span style="font-size: 18px;">Aa</span></option>
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
</FormRadios>
<FormSelect v-model:value="instanceTicker">
<template #label>{{ $t('instanceTicker') }}</template>
<option value="none">{{ $t('_instanceTicker.none') }}</option>
<option value="remote">{{ $t('_instanceTicker.remote') }}</option>
<option value="always">{{ $t('_instanceTicker.always') }}</option>
</FormSelect>
<FormSelect v-model:value="nsfw">
<template #label>{{ $t('nsfw') }}</template>
<option value="respect">{{ $t('_nsfw.respect') }}</option>
<option value="ignore">{{ $t('_nsfw.ignore') }}</option>
<option value="force">{{ $t('_nsfw.force') }}</option>
</FormSelect>
<FormGroup>
<template #label>{{ $t('defaultNavigationBehaviour') }}</template>
<FormSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</FormSwitch>
</FormGroup>
<FormSelect v-model:value="chatOpenBehavior">
<template #label>{{ $t('chatOpenBehavior') }}</template>
<option value="page">{{ $t('showInPage') }}</option>
<option value="window">{{ $t('openInWindow') }}</option>
<option value="popout">{{ $t('popout') }}</option>
</FormSelect>
<FormLink to="/settings/deck">{{ $t('deck') }}</FormLink>
<FormButton @click="cacheClear()" danger>{{ $t('cacheClear') }}</FormButton>
</FormBase>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faImage, faCog, faColumns, faCogs } from '@fortawesome/free-solid-svg-icons'; import { faImage, faCog, faColumns, faCogs } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue'; import FormSwitch from '@/components/form/switch.vue';
import MkSwitch from '@/components/ui/switch.vue'; import FormSelect from '@/components/form/select.vue';
import MkSelect from '@/components/ui/select.vue'; import FormRadios from '@/components/form/radios.vue';
import MkRadio from '@/components/ui/radio.vue'; import FormBase from '@/components/form/base.vue';
import MkRadios from '@/components/ui/radios.vue'; import FormGroup from '@/components/form/group.vue';
import MkRange from '@/components/ui/range.vue'; import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/form/button.vue';
import MkLink from '@/components/link.vue';
import { langs } from '@/config'; import { langs } from '@/config';
import { clientDb, set } from '@/db'; import { clientDb, set } from '@/db';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, MkLink,
MkSwitch, FormSwitch,
MkSelect, FormSelect,
MkRadio, FormRadios,
MkRadios, FormBase,
MkRange, FormGroup,
FormLink,
FormButton,
}, },
emits: ['info'], emits: ['info'],
@ -167,11 +168,6 @@ export default defineComponent({
set(value) { this.$store.commit('device/set', { key: 'defaultSideView', value }); } set(value) { this.$store.commit('device/set', { key: 'defaultSideView', value }); }
}, },
deckNavWindow: {
get() { return this.$store.state.device.deckNavWindow; },
set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); }
},
chatOpenBehavior: { chatOpenBehavior: {
get() { return this.$store.state.device.chatOpenBehavior; }, get() { return this.$store.state.device.chatOpenBehavior; },
set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); } set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); }
@ -182,20 +178,25 @@ export default defineComponent({
set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); } set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); }
}, },
loadRawImages: {
get() { return this.$store.state.device.loadRawImages; },
set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); }
},
disableShowingAnimatedImages: {
get() { return this.$store.state.device.disableShowingAnimatedImages; },
set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); }
},
nsfw: {
get() { return this.$store.state.device.nsfw; },
set(value) { this.$store.commit('device/set', { key: 'nsfw', value }); }
},
enableInfiniteScroll: { enableInfiniteScroll: {
get() { return this.$store.state.device.enableInfiniteScroll; }, get() { return this.$store.state.device.enableInfiniteScroll; },
set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); } set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }
}, },
deckAlwaysShowMainColumn: {
get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
},
deckColumnAlign: {
get() { return this.$store.state.device.deckColumnAlign; },
set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
},
}, },
watch: { watch: {

View File

@ -1,35 +1,36 @@
<template> <template>
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el"> <div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
<div class="nav" v-if="!narrow || page == null"> <FormBase class="nav" v-if="!narrow || page == null" :force-wide="!narrow">
<div class="menu"> <FormGroup>
<div class="label">{{ $t('basicSettings') }}</div> <template #label>{{ $t('basicSettings') }}</template>
<MkA class="item" :class="{ active: page === 'profile' }" replace to="/settings/profile"><Fa :icon="faUser" fixed-width class="icon"/>{{ $t('profile') }}</MkA> <FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><Fa :icon="faUser"/></template>{{ $t('profile') }}</FormLink>
<MkA class="item" :class="{ active: page === 'privacy' }" replace to="/settings/privacy"><Fa :icon="faLockOpen" fixed-width class="icon"/>{{ $t('privacy') }}</MkA> <FormLink :active="page === 'privacy'" replace to="/settings/privacy"><template #icon><Fa :icon="faLockOpen"/></template>{{ $t('privacy') }}</FormLink>
<MkA class="item" :class="{ active: page === 'reaction' }" replace to="/settings/reaction"><Fa :icon="faLaugh" fixed-width class="icon"/>{{ $t('reaction') }}</MkA> <FormLink :active="page === 'reaction'" replace to="/settings/reaction"><template #icon><Fa :icon="faLaugh"/></template>{{ $t('reaction') }}</FormLink>
<MkA class="item" :class="{ active: page === 'notifications' }" replace to="/settings/notifications"><Fa :icon="faBell" fixed-width class="icon"/>{{ $t('notifications') }}</MkA> <FormLink :active="page === 'notifications'" replace to="/settings/notifications"><template #icon><Fa :icon="faBell"/></template>{{ $t('notifications') }}</FormLink>
<MkA class="item" :class="{ active: page === 'integration' }" replace to="/settings/integration"><Fa :icon="faShareAlt" fixed-width class="icon"/>{{ $t('integration') }}</MkA> <FormLink :active="page === 'email'" replace to="/settings/email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('email') }}</FormLink>
<MkA class="item" :class="{ active: page === 'security' }" replace to="/settings/security"><Fa :icon="faLock" fixed-width class="icon"/>{{ $t('security') }}</MkA> <FormLink :active="page === 'integration'" replace to="/settings/integration"><template #icon><Fa :icon="faShareAlt"/></template>{{ $t('integration') }}</FormLink>
</div> <FormLink :active="page === 'security'" replace to="/settings/security"><template #icon><Fa :icon="faLock"/></template>{{ $t('security') }}</FormLink>
<div class="menu"> </FormGroup>
<div class="label">{{ $t('clientSettings') }}</div> <FormGroup>
<MkA class="item" :class="{ active: page === 'general' }" replace to="/settings/general"><Fa :icon="faCogs" fixed-width class="icon"/>{{ $t('general') }}</MkA> <template #label>{{ $t('clientSettings') }}</template>
<MkA class="item" :class="{ active: page === 'theme' }" replace to="/settings/theme"><Fa :icon="faPalette" fixed-width class="icon"/>{{ $t('theme') }}</MkA> <FormLink :active="page === 'general'" replace to="/settings/general"><template #icon><Fa :icon="faCogs"/></template>{{ $t('general') }}</FormLink>
<MkA class="item" :class="{ active: page === 'sidebar' }" replace to="/settings/sidebar"><Fa :icon="faListUl" fixed-width class="icon"/>{{ $t('sidebar') }}</MkA> <FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><Fa :icon="faPalette"/></template>{{ $t('theme') }}</FormLink>
<MkA class="item" :class="{ active: page === 'sounds' }" replace to="/settings/sounds"><Fa :icon="faMusic" fixed-width class="icon"/>{{ $t('sounds') }}</MkA> <FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><Fa :icon="faListUl"/></template>{{ $t('sidebar') }}</FormLink>
<MkA class="item" :class="{ active: page === 'plugins' }" replace to="/settings/plugins"><Fa :icon="faPlug" fixed-width class="icon"/>{{ $t('plugins') }}</MkA> <FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><Fa :icon="faMusic"/></template>{{ $t('sounds') }}</FormLink>
</div> <FormLink :active="page === 'plugins'" replace to="/settings/plugins"><template #icon><Fa :icon="faPlug"/></template>{{ $t('plugins') }}</FormLink>
<div class="menu"> </FormGroup>
<div class="label">{{ $t('otherSettings') }}</div> <FormGroup>
<MkA class="item" :class="{ active: page === 'import-export' }" replace to="/settings/import-export"><Fa :icon="faBoxes" fixed-width class="icon"/>{{ $t('importAndExport') }}</MkA> <template #label>{{ $t('otherSettings') }}</template>
<MkA class="item" :class="{ active: page === 'mute-block' }" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</MkA> <FormLink :active="page === 'import-export'" replace to="/settings/import-export"><template #icon><Fa :icon="faBoxes"/></template>{{ $t('importAndExport') }}</FormLink>
<MkA class="item" :class="{ active: page === 'word-mute' }" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</MkA> <FormLink :active="page === 'mute-block'" replace to="/settings/mute-block"><template #icon><Fa :icon="faBan"/></template>{{ $t('muteAndBlock') }}</FormLink>
<MkA class="item" :class="{ active: page === 'api' }" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</MkA> <FormLink :active="page === 'word-mute'" replace to="/settings/word-mute"><template #icon><Fa :icon="faCommentSlash"/></template>{{ $t('wordMute') }}</FormLink>
<MkA class="item" :class="{ active: page === 'other' }" replace to="/settings/other"><Fa :icon="faEllipsisH" fixed-width class="icon"/>{{ $t('other') }}</MkA> <FormLink :active="page === 'api'" replace to="/settings/api"><template #icon><Fa :icon="faKey"/></template>API</FormLink>
</div> <FormLink :active="page === 'other'" replace to="/settings/other"><template #icon><Fa :icon="faEllipsisH"/></template>{{ $t('other') }}</FormLink>
<div class="menu"> </FormGroup>
<button class="_button item" @click="logout">{{ $t('logout') }}</button> <FormGroup>
</div> <FormButton @click="logout" danger>{{ $t('logout') }}</FormButton>
</div> </FormGroup>
</FormBase>
<div class="main"> <div class="main">
<component :is="component" @info="onInfo"/> <component :is="component" @info="onInfo"/>
</div> </div>
@ -37,13 +38,25 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineAsyncComponent, defineComponent, onMounted, ref } from 'vue'; import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, ref, watch } from 'vue';
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons'; import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons';
import { faLaugh, faBell } from '@fortawesome/free-regular-svg-icons'; import { faLaugh, faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { store } from '@/store'; import { store } from '@/store';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import FormLink from '@/components/form/link.vue';
import FormGroup from '@/components/form/group.vue';
import FormBase from '@/components/form/base.vue';
import FormButton from '@/components/form/button.vue';
import { scroll } from '../../scripts/scroll';
export default defineComponent({ export default defineComponent({
components: {
FormBase,
FormLink,
FormGroup,
FormButton,
},
props: { props: {
page: { page: {
type: String, type: String,
@ -72,21 +85,35 @@ export default defineComponent({
case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue')); case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
case 'integration': return defineAsyncComponent(() => import('./integration.vue')); case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue')); case 'security': return defineAsyncComponent(() => import('./security.vue'));
case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
case 'api': return defineAsyncComponent(() => import('./api.vue')); case 'api': return defineAsyncComponent(() => import('./api.vue'));
case 'apps': return defineAsyncComponent(() => import('./apps.vue'));
case 'other': return defineAsyncComponent(() => import('./other.vue')); case 'other': return defineAsyncComponent(() => import('./other.vue'));
case 'general': return defineAsyncComponent(() => import('./general.vue')); case 'general': return defineAsyncComponent(() => import('./general.vue'));
case 'email': return defineAsyncComponent(() => import('./email.vue'));
case 'email/address': return defineAsyncComponent(() => import('./email-address.vue'));
case 'theme': return defineAsyncComponent(() => import('./theme.vue')); case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue')); case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue'));
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue')); case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue')); case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue')); case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
case 'regedit': return defineAsyncComponent(() => import('./regedit.vue')); case 'regedit': return defineAsyncComponent(() => import('./regedit.vue'));
default: return null; default: return null;
} }
}); });
watch(component, () => {
nextTick(() => {
scroll(el.value, 0);
});
});
onMounted(() => { onMounted(() => {
narrow.value = el.value.offsetWidth < 650; narrow.value = el.value.offsetWidth < 1025;
}); });
return { return {
@ -100,7 +127,7 @@ export default defineComponent({
store.dispatch('logout'); store.dispatch('logout');
location.href = '/'; location.href = '/';
}, },
faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes, faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes, faEnvelope,
}; };
}, },
}); });
@ -108,63 +135,19 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.vvcocwet { .vvcocwet {
> .nav {
> .menu {
margin: 16px 0;
> .label {
padding: 8px 32px;
font-size: 80%;
opacity: 0.7;
}
> .item {
display: block;
width: 100%;
box-sizing: border-box;
padding: 0 32px;
line-height: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
//background: var(--panel);
//border-bottom: solid 1px var(--divider);
transition: padding 0.2s ease, color 0.1s ease;
&:first-of-type {
//border-top: solid 1px var(--divider);
}
&.active {
color: var(--accent);
padding-left: 42px;
}
&:hover {
text-decoration: none;
padding-left: 42px;
}
> .icon {
margin-right: 0.5em;
}
}
}
}
&.wide { &.wide {
display: flex; display: flex;
max-width: 1100px;
margin: 0 auto;
> .nav { > .nav {
width: 30%; width: 32%;
max-width: 300px; box-sizing: border-box;
font-size: 0.95em; border-right: solid 0.5px var(--divider);
border-right: solid 1px var(--divider);
} }
> .main { > .main {
flex: 1; flex: 1;
padding: 32px;
--baseContentWidth: 100%; --baseContentWidth: 100%;
::v-deep(._section) { ::v-deep(._section) {

View File

@ -1,29 +1,31 @@
<template> <template>
<div> <FormBase>
<div class="_section"> <FormLink @click="configure">{{ $t('notificationSetting') }}</FormLink>
<MkButton full primary @click="configure"><Fa :icon="faCog"/> {{ $t('notificationSetting') }}</MkButton> <FormGroup>
</div> <FormButton @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</FormButton>
<div class="_section"> <FormButton @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</FormButton>
<MkButton full @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</MkButton> <FormButton @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</FormButton>
<MkButton full @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</MkButton> </FormGroup>
<MkButton full @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</MkButton> </FormBase>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons'; import { faCog } from '@fortawesome/free-solid-svg-icons';
import { faBell } from '@fortawesome/free-regular-svg-icons'; import { faBell } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue'; import FormButton from '@/components/form/button.vue';
import MkSwitch from '@/components/ui/switch.vue'; import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import { notificationTypes } from '../../../types'; import { notificationTypes } from '../../../types';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, FormBase,
MkSwitch, FormLink,
FormButton,
FormGroup,
}, },
emits: ['info'], emits: ['info'],

View File

@ -1,40 +1,43 @@
<template> <template>
<div> <FormBase>
<div class="_section"> <FormSwitch :value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
<div class="_card"> {{ $t('showFeaturedNotesInTimeline') }}
<div class="_content"> </FormSwitch>
<MkSwitch v-model:value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
{{ $t('showFeaturedNotesInTimeline') }} <FormLink to="/settings/account-info">{{ $t('accountInfo') }}</FormLink>
</MkSwitch>
</div> <FormGroup>
</div> <FormSwitch v-model:value="debug" @update:value="changeDebug">
</div>
<div class="_section">
<MkSwitch v-model:value="debug" @update:value="changeDebug">
DEBUG MODE DEBUG MODE
</MkSwitch> </FormSwitch>
<div v-if="debug"> <template v-if="debug">
<MkA to="/settings/regedit">RegEdit</MkA> <FormLink to="/settings/regedit">RegEdit</FormLink>
<MkButton @click="taskmanager">Task Manager</MkButton> <FormButton @click="taskmanager">Task Manager</FormButton>
</div> </template>
</div> </FormGroup>
</div> </FormBase>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue'; import { defineAsyncComponent, defineComponent } from 'vue';
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'; import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue'; import FormSwitch from '@/components/form/switch.vue';
import MkSwitch from '@/components/ui/switch.vue'; import FormSelect from '@/components/form/select.vue';
import MkButton from '@/components/ui/button.vue'; import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import { debug } from '@/config'; import { debug } from '@/config';
export default defineComponent({ export default defineComponent({
components: { components: {
MkSelect, FormBase,
MkSwitch, FormSelect,
MkButton, FormSwitch,
FormButton,
FormLink,
FormGroup,
}, },
emits: ['info'], emits: ['info'],

View File

@ -1,36 +1,43 @@
<template> <template>
<div class="_section"> <FormBase>
<div class="_card"> <FormGroup>
<div class="_content"> <FormSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</FormSwitch>
<MkSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</MkSwitch> <FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</FormSwitch>
<MkSwitch v-model:value="autoAcceptFollowed" v-if="isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</MkSwitch> <template #caption>{{ $t('lockedAccountInfo') }}</template>
</div> </FormGroup>
<div class="_content"> <FormSwitch v-model:value="noCrawle" @update:value="save()">
<MkSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</MkSwitch> {{ $t('noCrawle') }}
<MkSelect v-model:value="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility"> <template #desc>{{ $t('noCrawleDescription') }}</template>
<template #label>{{ $t('defaultNoteVisibility') }}</template> </FormSwitch>
<option value="public">{{ $t('_visibility.public') }}</option> <FormSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</FormSwitch>
<option value="home">{{ $t('_visibility.home') }}</option> <FormGroup v-if="!rememberNoteVisibility">
<option value="followers">{{ $t('_visibility.followers') }}</option> <template #label>{{ $t('defaultNoteVisibility') }}</template>
<option value="specified">{{ $t('_visibility.specified') }}</option> <FormSelect v-model:value="defaultNoteVisibility">
</MkSelect> <option value="public">{{ $t('_visibility.public') }}</option>
<MkSwitch v-model:value="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</MkSwitch> <option value="home">{{ $t('_visibility.home') }}</option>
</div> <option value="followers">{{ $t('_visibility.followers') }}</option>
</div> <option value="specified">{{ $t('_visibility.specified') }}</option>
</div> </FormSelect>
<FormSwitch v-model:value="defaultNoteLocalOnly">{{ $t('_visibility.localOnly') }}</FormSwitch>
</FormGroup>
</FormBase>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faLockOpen } from '@fortawesome/free-solid-svg-icons'; import { faLockOpen } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue'; import FormSwitch from '@/components/form/switch.vue';
import MkSwitch from '@/components/ui/switch.vue'; import FormSelect from '@/components/form/select.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkSelect, FormBase,
MkSwitch, FormSelect,
FormGroup,
FormSwitch,
}, },
emits: ['info'], emits: ['info'],
@ -43,6 +50,7 @@ export default defineComponent({
}, },
isLocked: false, isLocked: false,
autoAcceptFollowed: false, autoAcceptFollowed: false,
noCrawle: false,
} }
}, },
@ -66,6 +74,7 @@ export default defineComponent({
created() { created() {
this.isLocked = this.$store.state.i.isLocked; this.isLocked = this.$store.state.i.isLocked;
this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed; this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
this.noCrawle = this.$store.state.i.noCrawle;
}, },
mounted() { mounted() {
@ -77,6 +86,7 @@ export default defineComponent({
os.api('i/update', { os.api('i/update', {
isLocked: !!this.isLocked, isLocked: !!this.isLocked,
autoAcceptFollowed: !!this.autoAcceptFollowed, autoAcceptFollowed: !!this.autoAcceptFollowed,
noCrawle: !!this.noCrawle,
}); });
} }
} }

View File

@ -1,79 +1,67 @@
<template> <template>
<div class="_section"> <FormBase class="llvierxe">
<div class="llvierxe _card"> <div class="header _formItem" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
<div class="_title"><Fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div> <MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
<div class="_content">
<div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
<MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
</div>
<MkInput v-model:value="name" :max="30">
<span>{{ $t('_profile.name') }}</span>
</MkInput>
<MkTextarea v-model:value="description" :max="500">
<span>{{ $t('_profile.description') }}</span>
<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
</MkTextarea>
<MkInput v-model:value="location">
<span>{{ $t('location') }}</span>
<template #prefix><Fa :icon="faMapMarkerAlt"/></template>
</MkInput>
<MkInput v-model:value="birthday" type="date">
<template #title>{{ $t('birthday') }}</template>
<template #prefix><Fa :icon="faBirthdayCake"/></template>
</MkInput>
<details class="fields">
<summary>{{ $t('_profile.metadata') }}</summary>
<div class="row">
<MkInput v-model:value="fieldName0">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue0">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName1">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue1">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName2">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue2">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName3">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue3">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
</details>
<MkSwitch v-model:value="isBot">{{ $t('flagAsBot') }}</MkSwitch>
<MkSwitch v-model:value="isCat">{{ $t('flagAsCat') }}</MkSwitch>
</div>
<div class="_footer">
<MkButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</div> </div>
</div>
<FormInput v-model:value="name" :max="30">
<span>{{ $t('_profile.name') }}</span>
</FormInput>
<FormTextarea v-model:value="description" :max="500">
<span>{{ $t('_profile.description') }}</span>
<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
</FormTextarea>
<FormInput v-model:value="location">
<span>{{ $t('location') }}</span>
<template #prefix><Fa :icon="faMapMarkerAlt"/></template>
</FormInput>
<FormInput v-model:value="birthday" type="date">
<span>{{ $t('birthday') }}</span>
<template #prefix><Fa :icon="faBirthdayCake"/></template>
</FormInput>
<FormGroup>
<FormButton @click="editMetadata" primary>{{ $t('_profile.metadataEdit') }}</FormButton>
<template #caption>{{ $t('_profile.metadataDescription') }}</template>
</FormGroup>
<FormSwitch v-model:value="isCat">{{ $t('flagAsCat') }}<template #desc>{{ $t('flagAsCatDescription') }}</template></FormSwitch>
<FormSwitch v-model:value="isBot">{{ $t('flagAsBot') }}<template #desc>{{ $t('flagAsBotDescription') }}</template></FormSwitch>
<FormSwitch v-model:value="alwaysMarkNsfw">{{ $t('alwaysMarkSensitive') }}</FormSwitch>
<FormButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
</FormBase>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons'; import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons';
import { faSave } from '@fortawesome/free-regular-svg-icons'; import { faSave } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue'; import FormButton from '@/components/form/button.vue';
import MkInput from '@/components/ui/input.vue'; import FormInput from '@/components/form/input.vue';
import MkTextarea from '@/components/ui/textarea.vue'; import FormTextarea from '@/components/form/textarea.vue';
import MkSwitch from '@/components/ui/switch.vue'; import FormSwitch from '@/components/form/switch.vue';
import FormTuple from '@/components/form/tuple.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import { host } from '@/config'; import { host } from '@/config';
import { selectFile } from '@/scripts/select-file'; import { selectFile } from '@/scripts/select-file';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, FormButton,
MkInput, FormInput,
MkTextarea, FormTextarea,
MkSwitch, FormSwitch,
FormTuple,
FormBase,
FormGroup,
}, },
emits: ['info'], emits: ['info'],
@ -101,6 +89,7 @@ export default defineComponent({
bannerId: null, bannerId: null,
isBot: false, isBot: false,
isCat: false, isCat: false,
alwaysMarkNsfw: false,
saving: false, saving: false,
faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake
} }
@ -115,6 +104,7 @@ export default defineComponent({
this.bannerId = this.$store.state.i.bannerId; this.bannerId = this.$store.state.i.bannerId;
this.isBot = this.$store.state.i.isBot; this.isBot = this.$store.state.i.isBot;
this.isCat = this.$store.state.i.isCat; this.isCat = this.$store.state.i.isCat;
this.alwaysMarkNsfw = this.$store.state.i.alwaysMarkNsfw;
this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null; this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null;
this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null; this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null;
@ -147,7 +137,60 @@ export default defineComponent({
}); });
}, },
save(notify) { async editMetadata() {
const { canceled, result } = await os.form(this.$t('_profile.metadata'), {
fieldName0: {
type: 'string',
label: this.$t('_profile.metadataLabel') + ' 1',
default: this.fieldName0,
},
fieldValue0: {
type: 'string',
label: this.$t('_profile.metadataContent') + ' 1',
default: this.fieldValue0,
},
fieldName1: {
type: 'string',
label: this.$t('_profile.metadataLabel') + ' 2',
default: this.fieldName1,
},
fieldValue1: {
type: 'string',
label: this.$t('_profile.metadataContent') + ' 2',
default: this.fieldValue1,
},
fieldName2: {
type: 'string',
label: this.$t('_profile.metadataLabel') + ' 3',
default: this.fieldName2,
},
fieldValue2: {
type: 'string',
label: this.$t('_profile.metadataContent') + ' 3',
default: this.fieldValue2,
},
fieldName3: {
type: 'string',
label: this.$t('_profile.metadataLabel') + ' 4',
default: this.fieldName3,
},
fieldValue3: {
type: 'string',
label: this.$t('_profile.metadataContent') + ' 4',
default: this.fieldValue3,
},
});
if (canceled) return;
this.fieldName0 = result.fieldName0;
this.fieldValue0 = result.fieldValue0;
this.fieldName1 = result.fieldName1;
this.fieldValue1 = result.fieldValue1;
this.fieldName2 = result.fieldName2;
this.fieldValue2 = result.fieldValue2;
this.fieldName3 = result.fieldName3;
this.fieldValue3 = result.fieldValue3;
const fields = [ const fields = [
{ name: this.fieldName0, value: this.fieldValue0 }, { name: this.fieldName0, value: this.fieldValue0 },
{ name: this.fieldName1, value: this.fieldValue1 }, { name: this.fieldName1, value: this.fieldValue1 },
@ -155,6 +198,19 @@ export default defineComponent({
{ name: this.fieldName3, value: this.fieldValue3 }, { name: this.fieldName3, value: this.fieldValue3 },
]; ];
os.api('i/update', {
fields,
}).then(i => {
os.success();
}).catch(err => {
os.dialog({
type: 'error',
text: err.id
});
});
},
save(notify) {
this.saving = true; this.saving = true;
os.api('i/update', { os.api('i/update', {
@ -162,9 +218,9 @@ export default defineComponent({
description: this.description || null, description: this.description || null,
location: this.location || null, location: this.location || null,
birthday: this.birthday || null, birthday: this.birthday || null,
fields,
isBot: !!this.isBot, isBot: !!this.isBot,
isCat: !!this.isCat, isCat: !!this.isCat,
alwaysMarkNsfw: !!this.alwaysMarkNsfw,
}).then(i => { }).then(i => {
this.saving = false; this.saving = false;
this.$store.state.i.avatarId = i.avatarId; this.$store.state.i.avatarId = i.avatarId;
@ -189,41 +245,29 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.llvierxe { .llvierxe {
> ._content { > .header {
> .header { position: relative;
position: relative; height: 150px;
height: 150px; overflow: hidden;
overflow: hidden; background-size: cover;
background-size: cover; background-position: center;
background-position: center; border-radius: 5px;
border-radius: 5px; border: solid 1px var(--divider);
border: solid 1px var(--divider); box-sizing: border-box;
box-sizing: border-box; cursor: pointer;
> .avatar {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: block;
width: 72px;
height: 72px;
margin: auto;
cursor: pointer; cursor: pointer;
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
> .avatar {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: block;
width: 72px;
height: 72px;
margin: auto;
cursor: pointer;
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
}
}
> .fields {
> .row {
> * {
display: inline-block;
width: 50%;
margin-bottom: 0;
}
}
} }
} }
} }

View File

@ -1,9 +1,8 @@
<template> <template>
<div class="_section"> <FormBase>
<div class="_card"> <div class="_formItem">
<div class="_title"><Fa :icon="faLaugh"/> {{ $t('reaction') }}</div> <div class="_formLabel">{{ $t('reactionSettingDescription') }}</div>
<div class="_content"> <div class="_formPanel">
<div class="_caption" style="padding: 0 8px 8px 8px;">{{ $t('reactionSettingDescription') }}</div>
<XDraggable class="zoaiodol" :list="reactions" animation="150" delay="100" delay-on-touch-only="true"> <XDraggable class="zoaiodol" :list="reactions" animation="150" delay="100" delay-on-touch-only="true">
<button class="_button item" v-for="reaction in reactions" :key="reaction" @click="remove(reaction, $event)"> <button class="_button item" v-for="reaction in reactions" :key="reaction" @click="remove(reaction, $event)">
<MkEmoji :emoji="reaction" :normal="true"/> <MkEmoji :emoji="reaction" :normal="true"/>
@ -12,26 +11,25 @@
<button>a</button> <button>a</button>
</template> </template>
</XDraggable> </XDraggable>
<div class="_caption" style="padding: 8px;">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
<MkRadios v-model="reactionPickerWidth">
<template #desc>{{ $t('width') }}</template>
<option :value="1">{{ $t('small') }}</option>
<option :value="2">{{ $t('medium') }}</option>
<option :value="3">{{ $t('large') }}</option>
</MkRadios>
<MkRadios v-model="reactionPickerHeight">
<template #desc>{{ $t('height') }}</template>
<option :value="1">{{ $t('small') }}</option>
<option :value="2">{{ $t('medium') }}</option>
<option :value="3">{{ $t('large') }}</option>
</MkRadios>
</div>
<div class="_footer">
<MkButton inline @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
<MkButton inline @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</MkButton>
</div> </div>
<div class="_formCaption">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
</div> </div>
</div>
<FormRadios v-model="reactionPickerWidth">
<template #desc>{{ $t('width') }}</template>
<option :value="1">{{ $t('small') }}</option>
<option :value="2">{{ $t('medium') }}</option>
<option :value="3">{{ $t('large') }}</option>
</FormRadios>
<FormRadios v-model="reactionPickerHeight">
<template #desc>{{ $t('height') }}</template>
<option :value="1">{{ $t('small') }}</option>
<option :value="2">{{ $t('medium') }}</option>
<option :value="3">{{ $t('large') }}</option>
</FormRadios>
<FormButton @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</FormButton>
<FormButton danger @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</FormButton>
</FormBase>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -39,20 +37,19 @@ import { defineComponent } from 'vue';
import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons'; import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
import { faUndo } from '@fortawesome/free-solid-svg-icons'; import { faUndo } from '@fortawesome/free-solid-svg-icons';
import { VueDraggableNext } from 'vue-draggable-next'; import { VueDraggableNext } from 'vue-draggable-next';
import MkInput from '@/components/ui/input.vue'; import FormInput from '@/components/form/input.vue';
import MkButton from '@/components/ui/button.vue'; import FormRadios from '@/components/form/radios.vue';
import MkSwitch from '@/components/ui/switch.vue'; import FormBase from '@/components/form/base.vue';
import MkRadios from '@/components/ui/radios.vue'; import FormButton from '@/components/form/button.vue';
import { emojiRegexWithCustom } from '../../../misc/emoji-regex';
import { defaultSettings } from '@/store'; import { defaultSettings } from '@/store';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkInput, FormInput,
MkButton, FormButton,
MkSwitch, FormBase,
MkRadios, FormRadios,
XDraggable: VueDraggableNext, XDraggable: VueDraggableNext,
}, },
@ -62,7 +59,11 @@ export default defineComponent({
return { return {
INFO: { INFO: {
title: this.$t('reaction'), title: this.$t('reaction'),
icon: faLaugh icon: faLaugh,
action: {
icon: faEye,
handler: this.preview
}
}, },
reactions: JSON.parse(JSON.stringify(this.$store.state.settings.reactions)), reactions: JSON.parse(JSON.stringify(this.$store.state.settings.reactions)),
faLaugh, faSave, faEye, faUndo faLaugh, faSave, faEye, faUndo
@ -144,8 +145,6 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.zoaiodol { .zoaiodol {
border: solid 1px var(--divider);
border-radius: var(--radius);
padding: 16px; padding: 16px;
> .item { > .item {

View File

@ -1,29 +1,45 @@
<template> <template>
<div> <FormBase>
<div class="_section"> <X2fa/>
<X2fa/> <FormLink to="/settings/2fa"><template #icon><Fa :icon="faMobileAlt"/></template>{{ $t('twoStepAuthentication') }}</FormLink>
</div> <FormButton primary @click="change()">{{ $t('changePassword') }}</FormButton>
<div class="_section"> <FormPagination :pagination="pagination">
<MkButton primary @click="change()" full>{{ $t('changePassword') }}</MkButton> <template #label>{{ $t('signinHistory') }}</template>
</div> <template #default="{items}">
<div class="_section"> <div class="_formPanel timnmucd" v-for="item in items" :key="item.id">
<MkButton class="_vMargin" primary @click="regenerateToken" full><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</MkButton> <header>
<div class="_caption _vMargin" style="padding: 0 6px;">{{ $t('regenerateLoginTokenDescription') }}</div> <Fa class="icon succ" :icon="faCheck" v-if="item.success"/>
</div> <Fa class="icon fail" :icon="faTimesCircle" v-else/>
</div> <code class="ip _monospace">{{ item.ip }}</code>
<MkTime :time="item.createdAt" class="time"/>
</header>
</div>
</template>
</FormPagination>
<FormGroup>
<FormButton danger @click="regenerateToken"><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</FormButton>
<template #caption>{{ $t('regenerateLoginTokenDescription') }}</template>
</FormGroup>
</FormBase>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faLock, faSyncAlt } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faTimesCircle, faLock, faSyncAlt, faMobileAlt } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue'; import FormBase from '@/components/form/base.vue';
import X2fa from './security.2fa.vue'; import FormLink from '@/components/form/link.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import FormPagination from '@/components/form/pagination.vue';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, FormBase,
X2fa, FormLink,
FormButton,
FormPagination,
FormGroup,
}, },
emits: ['info'], emits: ['info'],
@ -34,7 +50,11 @@ export default defineComponent({
title: this.$t('security'), title: this.$t('security'),
icon: faLock icon: faLock
}, },
faLock, faSyncAlt pagination: {
endpoint: 'i/signin-history',
limit: 5,
},
faLock, faSyncAlt, faCheck, faTimesCircle, faMobileAlt,
} }
}, },
@ -98,3 +118,32 @@ export default defineComponent({
} }
}); });
</script> </script>
<style lang="scss" scoped>
.timnmucd {
padding: 16px;
> header {
display: flex;
align-items: center;
> .icon {
width: 1em;
margin-right: 0.75em;
&.succ {
color: var(--success);
}
&.fail {
color: var(--error);
}
}
> .time {
margin-left: auto;
opacity: 0.7;
}
}
}
</style>

View File

@ -1,41 +1,41 @@
<template> <template>
<div class="_section"> <FormBase>
<div class="_card"> <FormTextarea v-model:value="items" tall>
<div class="_content"> <span>{{ $t('sidebar') }}</span>
<MkTextarea v-model:value="items" tall> <template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
<span>{{ $t('sidebar') }}</span> </FormTextarea>
<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
</MkTextarea> <FormRadios v-model="sidebarDisplay">
</div> <template #desc>{{ $t('display') }}</template>
<div class="_content"> <option value="full">{{ $t('_sidebar.full') }}</option>
<div>{{ $t('display') }}</div> <option value="icon">{{ $t('_sidebar.icon') }}</option>
<MkRadio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</MkRadio> <!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
<MkRadio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</MkRadio> </FormRadios>
<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
</div> <FormButton @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
<div class="_footer"> <FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton>
<MkButton inline @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton> </FormBase>
<MkButton inline @click="reset()"><Fa :icon="faRedo"/> {{ $t('default') }}</MkButton>
</div>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons'; import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue'; import FormSwitch from '@/components/form/switch.vue';
import MkTextarea from '@/components/ui/textarea.vue'; import FormTextarea from '@/components/form/textarea.vue';
import MkRadio from '@/components/ui/radio.vue'; import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import { defaultDeviceUserSettings } from '@/store'; import { defaultDeviceUserSettings } from '@/store';
import * as os from '@/os'; import * as os from '@/os';
import { sidebarDef } from '@/sidebar'; import { sidebarDef } from '@/sidebar';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, FormBase,
MkTextarea, FormButton,
MkRadio, FormTextarea,
FormRadios,
}, },
emits: ['info'], emits: ['info'],
@ -102,7 +102,3 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style lang="scss" scoped>
</style>

View File

@ -1,62 +1,35 @@
<template> <template>
<div class="_section"> <FormBase>
<div class="_card"> <FormRange v-model:value="masterVolume" :min="0" :max="1" :step="0.05">
<div class="_title"><Fa :icon="faMusic"/> {{ $t('sounds') }}</div> <template #label><Fa :icon="volumeIcon" :key="volumeIcon"/> {{ $t('masterVolume') }}</template>
<div class="_content"> </FormRange>
<MkRange v-model:value="sfxVolume" :min="0" :max="1" :step="0.1">
<Fa slot="icon" :icon="volumeIcon"/> <FormGroup>
<span slot="title">{{ $t('volume') }}</span> <template #label>{{ $t('sounds') }}</template>
</MkRange> <FormButton v-for="type in Object.keys(sounds)" :key="type" :center="false" @click="edit(type)">
</div> {{ $t('_sfx.' + type) }}
<div class="_content"> <template #suffix>{{ sounds[type].type || $t('none') }}</template>
<MkSelect v-model:value="sfxNote"> <template #suffixIcon><Fa :icon="faChevronDown"/></template>
<template #label>{{ $t('_sfx.note') }}</template> </FormButton>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option> </FormGroup>
<template #text><button class="_textButton" @click="listen(sfxNote)" v-if="sfxNote"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect> <FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton>
<MkSelect v-model:value="sfxNoteMy"> </FormBase>
<template #label>{{ $t('_sfx.noteMy') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxNoteMy)" v-if="sfxNoteMy"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxNotification">
<template #label>{{ $t('_sfx.notification') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxNotification)" v-if="sfxNotification"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChat">
<template #label>{{ $t('_sfx.chat') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChat)" v-if="sfxChat"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChatBg">
<template #label>{{ $t('_sfx.chatBg') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChatBg)" v-if="sfxChatBg"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxAntenna">
<template #label>{{ $t('_sfx.antenna') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxAntenna)" v-if="sfxAntenna"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChannel">
<template #label>{{ $t('_sfx.channel') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChannel)" v-if="sfxChannel"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
</div>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons'; import { faMusic, faPlay, faVolumeUp, faVolumeMute, faChevronDown, faRedo } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue'; import FormRange from '@/components/form/range.vue';
import MkRange from '@/components/ui/range.vue'; import FormSelect from '@/components/form/select.vue';
import FormBase from '@/components/form/base.vue';
import FormButton from '@/components/form/button.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os'; import * as os from '@/os';
import { device, defaultDeviceSettings } from '@/cold-storage';
import { playFile } from '@/scripts/sound';
const sounds = [ const soundsTypes = [
null, null,
'syuilo/up', 'syuilo/up',
'syuilo/down', 'syuilo/down',
@ -73,6 +46,8 @@ const sounds = [
'syuilo/square-pico', 'syuilo/square-pico',
'syuilo/reverved', 'syuilo/reverved',
'syuilo/ryukyu', 'syuilo/ryukyu',
'syuilo/kick',
'syuilo/snare',
'aisha/1', 'aisha/1',
'aisha/2', 'aisha/2',
'aisha/3', 'aisha/3',
@ -82,71 +57,98 @@ const sounds = [
export default defineComponent({ export default defineComponent({
components: { components: {
MkSelect, FormSelect,
MkRange, FormButton,
FormBase,
FormRange,
FormGroup,
}, },
emits: ['info'],
data() { data() {
return { return {
sounds, INFO: {
faMusic, faPlay, faVolumeUp, faVolumeMute, title: this.$t('sounds'),
icon: faMusic
},
sounds: {},
faMusic, faPlay, faVolumeUp, faVolumeMute, faChevronDown, faRedo,
} }
}, },
computed: { computed: {
sfxVolume: { masterVolume: { // TODO: ()computed使
get() { return this.$store.state.device.sfxVolume; }, get() { return device.get('sound_masterVolume'); },
set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); } set(value) { device.set('sound_masterVolume', value); }
}, },
volumeIcon() {
sfxNote: { return this.masterVolume === 0 ? faVolumeMute : faVolumeUp;
get() { return this.$store.state.device.sfxNote; },
set(value) { this.$store.commit('device/set', { key: 'sfxNote', value }); }
},
sfxNoteMy: {
get() { return this.$store.state.device.sfxNoteMy; },
set(value) { this.$store.commit('device/set', { key: 'sfxNoteMy', value }); }
},
sfxNotification: {
get() { return this.$store.state.device.sfxNotification; },
set(value) { this.$store.commit('device/set', { key: 'sfxNotification', value }); }
},
sfxChat: {
get() { return this.$store.state.device.sfxChat; },
set(value) { this.$store.commit('device/set', { key: 'sfxChat', value }); }
},
sfxChatBg: {
get() { return this.$store.state.device.sfxChatBg; },
set(value) { this.$store.commit('device/set', { key: 'sfxChatBg', value }); }
},
sfxAntenna: {
get() { return this.$store.state.device.sfxAntenna; },
set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
},
sfxChannel: {
get() { return this.$store.state.device.sfxChannel; },
set(value) { this.$store.commit('device/set', { key: 'sfxChannel', value }); }
},
volumeIcon: {
get() {
return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;
}
} }
}, },
created() {
this.sounds.note = device.get('sound_note');
this.sounds.noteMy = device.get('sound_noteMy');
this.sounds.notification = device.get('sound_notification');
this.sounds.chat = device.get('sound_chat');
this.sounds.chatBg = device.get('sound_chatBg');
this.sounds.antenna = device.get('sound_antenna');
this.sounds.channel = device.get('sound_channel');
this.sounds.reversiPutBlack = device.get('sound_reversiPutBlack');
this.sounds.reversiPutWhite = device.get('sound_reversiPutWhite');
},
mounted() {
this.$emit('info', this.INFO);
},
methods: { methods: {
listen(sound) { async edit(type) {
const audio = new Audio(`/assets/sounds/${sound}.mp3`); const { canceled, result } = await os.form(this.$t('_sfx.' + type), {
audio.volume = this.$store.state.device.sfxVolume; type: {
audio.play(); type: 'enum',
enum: soundsTypes.map(x => ({
value: x,
label: x == null ? this.$t('none') : x,
})),
label: this.$t('sound'),
default: this.sounds[type].type,
},
volume: {
type: 'range',
mim: 0,
max: 1,
step: 0.05,
label: this.$t('volume'),
default: this.sounds[type].volume
},
listen: {
type: 'button',
content: this.$t('listen'),
action: (_, values) => {
playFile(values.type, values.volume);
}
}
});
if (canceled) return;
const v = {
type: result.type,
volume: result.volume,
};
device.set('sound_' + type, v);
this.sounds[type] = v;
}, },
reset() {
for (const sound of Object.keys(this.sounds)) {
const v = defaultDeviceSettings['sound_' + sound];
device.set('sound_' + sound, v);
this.sounds[sound] = v;
}
}
} }
}); });
</script> </script>

View File

@ -0,0 +1,106 @@
<template>
<FormBase>
<FormGroup>
<FormTextarea v-model:value="installThemeCode">
<span>{{ $t('_theme.code') }}</span>
</FormTextarea>
<FormButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><Fa :icon="faEye"/> {{ $t('preview') }}</FormButton>
</FormGroup>
<FormButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><Fa :icon="faCheck"/> {{ $t('install') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/form/button.vue';
import { applyTheme, validateTheme } from '@/scripts/theme';
import * as os from '@/os';
export default defineComponent({
components: {
FormTextarea,
FormSelect,
FormRadios,
FormBase,
FormGroup,
FormLink,
FormButton,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('_theme.install'),
icon: faDownload
},
installThemeCode: null,
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
parseThemeCode(code) {
let theme;
try {
theme = JSON5.parse(code);
} catch (e) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (!validateTheme(theme)) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
os.dialog({
type: 'info',
text: this.$t('_theme.alreadyInstalled')
});
return false;
}
return theme;
},
preview(code) {
const theme = this.parseThemeCode(code);
if (theme) applyTheme(theme, false);
},
install(code) {
const theme = this.parseThemeCode(code);
if (!theme) return;
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
},
}
});
</script>

View File

@ -0,0 +1,103 @@
<template>
<FormBase>
<FormSelect v-model:value="selectedThemeId">
<template #label>{{ $t('installedThemes') }}</template>
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
<optgroup :label="$t('builtinThemes')">
<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
<template v-if="selectedTheme">
<FormInput readonly :value="selectedTheme.author">
<span>{{ $t('author') }}</span>
</FormInput>
<FormTextarea readonly tall :value="selectedThemeCode">
<span>{{ $t('_theme.code') }}</span>
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $t('copy') }}</button></template>
</FormTextarea>
<FormButton @click="uninstall()" danger v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</FormButton>
</template>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/form/button.vue';
import { Theme, builtinThemes } from '@/scripts/theme';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
export default defineComponent({
components: {
FormTextarea,
FormSelect,
FormRadios,
FormBase,
FormGroup,
FormInput,
FormButton,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('_theme.manage'),
icon: faFolderOpen
},
builtinThemes,
selectedThemeId: null,
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
computed: {
themes(): Theme[] {
return builtinThemes.concat(this.$store.state.device.themes);
},
installedThemes(): Theme[] {
return this.$store.state.device.themes;
},
selectedTheme() {
if (this.selectedThemeId == null) return null;
return this.themes.find(x => x.id === this.selectedThemeId);
},
selectedThemeCode() {
if (this.selectedTheme == null) return null;
return JSON5.stringify(this.selectedTheme, null, '\t');
},
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
copyThemeCode() {
copyToClipboard(this.selectedThemeCode);
os.success();
},
uninstall() {
const theme = this.selectedTheme;
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.success();
},
}
});
</script>

View File

@ -1,7 +1,26 @@
<template> <template>
<div class=""> <FormBase>
<div class="rfqxtzch _card _vMargin"> <FormSelect v-model:value="lightTheme" v-if="!darkMode">
<div class="_content"> <template #label>{{ $t('themeForLightMode') }}</template>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
<FormSelect v-model:value="darkTheme" v-else>
<template #label>{{ $t('themeForDarkMode') }}</template>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
<FormGroup>
<div class="rfqxtzch _formItem _formPanel">
<div class="darkMode" :class="{ disabled: syncDeviceDarkMode }"> <div class="darkMode" :class="{ disabled: syncDeviceDarkMode }">
<div class="toggleWrapper"> <div class="toggleWrapper">
<input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/> <input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/>
@ -23,85 +42,47 @@
</div> </div>
</div> </div>
</div> </div>
<div class="_content"> <FormSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</FormSwitch>
<MkSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</MkSwitch> </FormGroup>
</div>
</div> <FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</FormButton>
<div class="_card _vMargin"> <FormButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</FormButton>
<div class="_content">
<MkSelect v-model:value="lightTheme"> <FormGroup>
<template #label>{{ $t('themeForLightMode') }}</template> <FormLink to="https://assets.msky.cafe/theme/list" external>{{ $t('_theme.explore') }}</FormLink>
<optgroup :label="$t('lightThemes')"> <FormLink to="/theme-editor">{{ $t('_theme.make') }}</FormLink>
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option> </FormGroup>
</optgroup>
<optgroup :label="$t('darkThemes')"> <FormLink to="/settings/theme/install"><template #icon><Fa :icon="faDownload"/></template>{{ $t('_theme.install') }}</FormLink>
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup> <FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $t('_theme.manage') }}</FormLink>
</MkSelect> </FormBase>
<MkSelect v-model:value="darkTheme">
<template #label>{{ $t('themeForDarkMode') }}</template>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</MkSelect>
<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a><MkA to="/theme-editor" class="_link">{{ $t('_theme.make') }}</MkA>
</div>
<div class="_content">
<MkButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</MkButton>
<MkButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</MkButton>
</div>
</div>
<div class="_card _vMargin">
<div class="_title"><Fa :icon="faDownload"/> {{ $t('_theme.install') }}</div>
<div class="_content">
<MkTextarea v-model:value="installThemeCode">
<span>{{ $t('_theme.code') }}</span>
</MkTextarea>
<MkButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><Fa :icon="faCheck"/> {{ $t('install') }}</MkButton>
<MkButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
</div>
</div>
<div class="_card _vMargin">
<div class="_title"><Fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</div>
<div class="_content">
<MkSelect v-model:value="selectedThemeId">
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</MkSelect>
<template v-if="selectedTheme">
<MkTextarea readonly tall :value="selectedThemeCode">
<span>{{ $t('_theme.code') }}</span>
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $t('copy') }}</button></template>
</MkTextarea>
<MkButton @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</MkButton>
</template>
</div>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons'; import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5'; import FormSwitch from '@/components/form/switch.vue';
import MkButton from '@/components/ui/button.vue'; import FormSelect from '@/components/form/select.vue';
import MkSelect from '@/components/ui/select.vue'; import FormRadios from '@/components/form/radios.vue';
import MkSwitch from '@/components/ui/switch.vue'; import FormBase from '@/components/form/base.vue';
import MkTextarea from '@/components/ui/textarea.vue'; import FormGroup from '@/components/form/group.vue';
import { Theme, builtinThemes, applyTheme, validateTheme } from '@/scripts/theme'; import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/form/button.vue';
import { Theme, builtinThemes, applyTheme } from '@/scripts/theme';
import { selectFile } from '@/scripts/select-file'; import { selectFile } from '@/scripts/select-file';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os'; import * as os from '@/os';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, FormSwitch,
MkSelect, FormSelect,
MkSwitch, FormRadios,
MkTextarea, FormBase,
FormGroup,
FormLink,
FormButton,
}, },
emits: ['info'], emits: ['info'],
@ -113,8 +94,6 @@ export default defineComponent({
icon: faPalette icon: faPalette
}, },
builtinThemes, builtinThemes,
installThemeCode: null,
selectedThemeId: null,
wallpaper: localStorage.getItem('wallpaper'), wallpaper: localStorage.getItem('wallpaper'),
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
} }
@ -156,16 +135,6 @@ export default defineComponent({
get() { return this.$store.state.device.syncDeviceDarkMode; }, get() { return this.$store.state.device.syncDeviceDarkMode; },
set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); } set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); }
}, },
selectedTheme() {
if (this.selectedThemeId == null) return null;
return this.themes.find(x => x.id === this.selectedThemeId);
},
selectedThemeCode() {
if (this.selectedTheme == null) return null;
return JSON5.stringify(this.selectedTheme, null, '\t');
},
}, },
watch: { watch: {
@ -207,292 +176,230 @@ export default defineComponent({
this.wallpaper = file.url; this.wallpaper = file.url;
}); });
}, },
copyThemeCode() {
copyToClipboard(this.selectedThemeCode);
os.success();
},
parseThemeCode(code) {
let theme;
try {
theme = JSON5.parse(code);
} catch (e) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (!validateTheme(theme)) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
os.dialog({
type: 'info',
text: this.$t('_theme.alreadyInstalled')
});
return false;
}
return theme;
},
preview(code) {
const theme = this.parseThemeCode(code);
if (theme) applyTheme(theme, false);
},
install(code) {
const theme = this.parseThemeCode(code);
if (!theme) return;
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
},
uninstall() {
const theme = this.selectedTheme;
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.success();
},
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.rfqxtzch { .rfqxtzch {
> ._content { padding: 16px;
> .darkMode {
position: relative;
padding: 32px 0;
&.disabled { > .darkMode {
opacity: 0.7; position: relative;
padding: 32px 0;
&, * { &.disabled {
cursor: not-allowed !important; opacity: 0.7;
}
&, * {
cursor: not-allowed !important;
} }
}
.toggleWrapper { .toggleWrapper {
position: absolute;
top: 50%;
left: 50%;
overflow: hidden;
padding: 0 100px;
transform: translate3d(-50%, -50%, 0);
input {
position: absolute; position: absolute;
top: 50%; left: -99em;
left: 50%; }
overflow: hidden; }
padding: 0 100px;
transform: translate3d(-50%, -50%, 0);
input { .toggle {
position: absolute; cursor: pointer;
left: -99em; display: inline-block;
} position: relative;
width: 90px;
height: 50px;
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
> .before, > .after {
position: absolute;
top: 15px;
font-size: 18px;
transition: color 1s ease;
} }
.toggle { > .before {
cursor: pointer; left: -70px;
display: inline-block; color: var(--accent);
position: relative; }
width: 90px;
height: 50px;
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
> .before, > .after { > .after {
position: absolute; right: -68px;
top: 15px; color: var(--fg);
font-size: 18px; }
transition: color 1s ease; }
}
.toggle__handler {
display: inline-block;
position: relative;
z-index: 1;
top: 3px;
left: 3px;
width: 50px - 6;
height: 50px - 6;
background-color: #FFCF96;
border-radius: 50px;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
transform: rotate(-45deg);
.crater {
position: absolute;
background-color: #E8CDA5;
opacity: 0;
transition: opacity 200ms ease-in-out !important;
border-radius: 100%;
}
.crater--1 {
top: 18px;
left: 10px;
width: 4px;
height: 4px;
}
.crater--2 {
top: 28px;
left: 22px;
width: 6px;
height: 6px;
}
.crater--3 {
top: 10px;
left: 25px;
width: 8px;
height: 8px;
}
}
.star {
position: absolute;
background-color: #ffffff;
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
border-radius: 50%;
}
.star--1 {
top: 10px;
left: 35px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--2 {
top: 18px;
left: 28px;
z-index: 1;
width: 30px;
height: 3px;
}
.star--3 {
top: 27px;
left: 40px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--4,
.star--5,
.star--6 {
opacity: 0;
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--4 {
top: 16px;
left: 11px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
.star--5 {
top: 32px;
left: 17px;
z-index: 0;
width: 3px;
height: 3px;
transform: translate3d(3px,0,0);
}
.star--6 {
top: 36px;
left: 28px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
input:checked {
+ .toggle {
background-color: #749DD6;
> .before { > .before {
left: -70px; color: var(--fg);
color: var(--accent);
} }
> .after { > .after {
right: -68px; color: var(--accent);
color: var(--fg);
}
}
.toggle__handler {
display: inline-block;
position: relative;
z-index: 1;
top: 3px;
left: 3px;
width: 50px - 6;
height: 50px - 6;
background-color: #FFCF96;
border-radius: 50px;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
transform: rotate(-45deg);
.crater {
position: absolute;
background-color: #E8CDA5;
opacity: 0;
transition: opacity 200ms ease-in-out !important;
border-radius: 100%;
} }
.crater--1 { .toggle__handler {
top: 18px; background-color: #FFE5B5;
left: 10px; transform: translate3d(40px, 0, 0) rotate(0);
.crater { opacity: 1; }
}
.star--1 {
width: 2px;
height: 2px;
}
.star--2 {
width: 4px; width: 4px;
height: 4px; height: 4px;
transform: translate3d(-5px, 0, 0);
} }
.crater--2 { .star--3 {
top: 28px; width: 2px;
left: 22px; height: 2px;
width: 6px; transform: translate3d(-7px, 0, 0);
height: 6px;
} }
.crater--3 { .star--4,
top: 10px; .star--5,
left: 25px; .star--6 {
width: 8px; opacity: 1;
height: 8px; transform: translate3d(0,0,0);
} }
}
.star { .star--4 {
position: absolute; transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
background-color: #ffffff; }
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
border-radius: 50%;
}
.star--1 { .star--5 {
top: 10px; transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
left: 35px; }
z-index: 0;
width: 30px;
height: 3px;
}
.star--2 { .star--6 {
top: 18px; transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
left: 28px;
z-index: 1;
width: 30px;
height: 3px;
}
.star--3 {
top: 27px;
left: 40px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--4,
.star--5,
.star--6 {
opacity: 0;
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--4 {
top: 16px;
left: 11px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
.star--5 {
top: 32px;
left: 17px;
z-index: 0;
width: 3px;
height: 3px;
transform: translate3d(3px,0,0);
}
.star--6 {
top: 36px;
left: 28px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
input:checked {
+ .toggle {
background-color: #749DD6;
> .before {
color: var(--fg);
}
> .after {
color: var(--accent);
}
.toggle__handler {
background-color: #FFE5B5;
transform: translate3d(40px, 0, 0) rotate(0);
.crater { opacity: 1; }
}
.star--1 {
width: 2px;
height: 2px;
}
.star--2 {
width: 4px;
height: 4px;
transform: translate3d(-5px, 0, 0);
}
.star--3 {
width: 2px;
height: 2px;
transform: translate3d(-7px, 0, 0);
}
.star--4,
.star--5,
.star--6 {
opacity: 1;
transform: translate3d(0,0,0);
}
.star--4 {
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--5 {
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--6 {
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
} }
} }
} }

View File

@ -1,47 +1,53 @@
<template> <template>
<div class="_section"> <div>
<div class="_card"> <MkTab v-model:value="tab">
<MkTab v-model:value="tab"> <option value="soft">{{ $t('_wordMute.soft') }}</option>
<option value="soft">{{ $t('_wordMute.soft') }}</option> <option value="hard">{{ $t('_wordMute.hard') }}</option>
<option value="hard">{{ $t('_wordMute.hard') }}</option> </MkTab>
</MkTab> <FormBase>
<div class="_content"> <div class="_formItem">
<div v-show="tab === 'soft'"> <div v-show="tab === 'soft'">
<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo> <MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo>
<MkTextarea v-model:value="softMutedWords"> <FormTextarea v-model:value="softMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span> <span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template> <template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
</MkTextarea> </FormTextarea>
</div> </div>
<div v-show="tab === 'hard'"> <div v-show="tab === 'hard'">
<MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo> <MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo>
<MkTextarea v-model:value="hardMutedWords" style="margin-bottom: 16px;"> <FormTextarea v-model:value="hardMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span> <span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template> <template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
</MkTextarea> </FormTextarea>
<div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div> <FormKeyValueView v-if="hardWordMutedNotesCount != null">
<template #key>{{ $t('_wordMute.mutedNotes') }}</template>
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
</FormKeyValueView>
</div> </div>
</div> </div>
<div class="_footer"> <FormButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
<MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton> </FormBase>
</div>
</div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons'; import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue'; import FormTextarea from '@/components/form/textarea.vue';
import MkTextarea from '@/components/ui/textarea.vue'; import FormBase from '@/components/form/base.vue';
import FormKeyValueView from '@/components/form/key-value-view.vue';
import FormButton from '@/components/form/button.vue';
import MkTab from '@/components/tab.vue'; import MkTab from '@/components/tab.vue';
import MkInfo from '@/components/ui/info.vue'; import MkInfo from '@/components/ui/info.vue';
import * as os from '@/os'; import * as os from '@/os';
import number from '@/filters/number';
export default defineComponent({ export default defineComponent({
components: { components: {
MkButton, FormBase,
MkTextarea, FormButton,
FormTextarea,
FormKeyValueView,
MkTab, MkTab,
MkInfo, MkInfo,
}, },
@ -97,6 +103,8 @@ export default defineComponent({
}); });
this.changed = false; this.changed = false;
}, },
number
} }
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="_section"> <div>
<MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers _content" ref="list"> <MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers _content" ref="list">
<div class="users"> <div class="users">
<MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/> <MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/>

View File

@ -1,15 +1,24 @@
<template> <template>
<div> <MkContainer>
<div ref="chart"></div> <template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template>
</div>
<div style="padding: 8px;">
<div ref="chart"></div>
</div>
</MkContainer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import ApexCharts from 'apexcharts'; import ApexCharts from 'apexcharts';
import { faChartBar } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os'; import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
export default defineComponent({ export default defineComponent({
components: {
MkContainer,
},
props: { props: {
user: { user: {
type: Object, type: Object,
@ -25,7 +34,8 @@ export default defineComponent({
return { return {
fetching: true, fetching: true,
data: [], data: [],
peak: null peak: null,
faChartBar,
}; };
}, },
mounted() { mounted() {

View File

@ -1,29 +1,43 @@
<template> <template>
<div class="ujigsodd"> <MkContainer>
<MkLoading v-if="fetching"/> <template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
<div class="stream" v-if="!fetching && images.length > 0"> <div class="ujigsodd">
<MkA v-for="image in images" <MkLoading v-if="fetching"/>
class="img" <div class="stream" v-if="!fetching && images.length > 0">
:style="`background-image: url(${thumbnail(image.file)})`" <MkA v-for="image in images"
:to="notePage(image.note)" class="img"
></MkA> :style="`background-image: url(${thumbnail(image.file)})`"
:to="notePage(image.note)"
></MkA>
</div>
<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
</div> </div>
<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p> </MkContainer>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { faImage } from '@fortawesome/free-solid-svg-icons';
import { getStaticImageUrl } from '@/scripts/get-static-image-url'; import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import notePage from '../../filters/note'; import notePage from '../../filters/note';
import * as os from '@/os'; import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
export default defineComponent({ export default defineComponent({
props: ['user'], components: {
MkContainer,
},
props: {
user: {
type: Object,
required: true
},
},
data() { data() {
return { return {
fetching: true, fetching: true,
images: [] images: [],
faImage
}; };
}, },
mounted() { mounted() {
@ -37,7 +51,7 @@ export default defineComponent({
os.api('users/notes', { os.api('users/notes', {
userId: this.user.id, userId: this.user.id,
fileType: image, fileType: image,
excludeNsfw: !this.$store.state.device.alwaysShowNsfw, excludeNsfw: this.$store.state.device.nsfw !== 'ignore',
limit: 9, limit: 9,
}).then(notes => { }).then(notes => {
for (const note of notes) { for (const note of notes) {
@ -66,6 +80,8 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.ujigsodd { .ujigsodd {
padding: 8px;
> .stream { > .stream {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -1,115 +1,113 @@
<template> <template>
<div class="mk-user-page" v-if="user" v-size="{ max: [500] }"> <div>
<!-- TODO --> <div class="mk-user-page" v-if="user" v-size="{ max: [500] }" :class="{ _section: narrow === false }">
<!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> --> <!-- TODO -->
<!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> --> <!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> -->
<!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> -->
<div class="profile _section _fitBottom"> <div class="main">
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/> <div class="profile _vMargin" :class="{ _section: narrow === true }">
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/>
<div class="_content _vMargin" :key="user.id"> <div class="_content _panel _vMargin" :key="user.id">
<div class="banner-container" :style="style"> <div class="banner-container" :style="style">
<div class="banner" ref="banner" :style="style"></div> <div class="banner" ref="banner" :style="style"></div>
<div class="fade"></div> <div class="fade"></div>
<div class="title"> <div class="title">
<MkUserName class="name" :user="user" :nowrap="true"/> <MkUserName class="name" :user="user" :nowrap="true"/>
<div class="bottom"> <div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true" /></span> <span class="username"><MkAcct :user="user" :detail="true" /></span>
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span> <span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span> <span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span> <span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span> <span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
</div>
</div>
<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
<div class="actions" v-if="$store.getters.isSignedIn">
<button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button>
<MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div>
</div>
<MkAvatar class="avatar" :user="user" :disable-preview="true"/>
<div class="title">
<MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true" /></span>
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
</div>
</div>
<div class="description">
<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
<p v-else class="empty">{{ $t('noAccountDescription') }}</p>
</div>
<div class="fields system">
<dl class="field" v-if="user.location">
<dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
<dd class="value">{{ user.location }}</dd>
</dl>
<dl class="field" v-if="user.birthday">
<dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
</dl>
<dl class="field">
<dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
</dl>
</div>
<div class="fields" v-if="user.fields.length > 0">
<dl class="field" v-for="(field, i) in user.fields" :key="i">
<dt class="name">
<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
</dt>
<dd class="value">
<Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
</dd>
</dl>
</div>
<div class="status">
<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
<b>{{ number(user.notesCount) }}</b>
<span>{{ $t('notes') }}</span>
</MkA>
<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
<b>{{ number(user.followingCount) }}</b>
<span>{{ $t('following') }}</span>
</MkA>
<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
<b>{{ number(user.followersCount) }}</b>
<span>{{ $t('followers') }}</span>
</MkA>
</div> </div>
</div> </div>
<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span> </div>
<div class="actions" v-if="$store.getters.isSignedIn">
<button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button> <template v-if="page === 'index'">
<MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> <div v-if="user.pinnedNotes.length > 0" :class="{ _section: narrow === true, _vMargin: narrow === false }">
<XNote v-for="note in user.pinnedNotes" class="note _content _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
</div> </div>
</div> <div v-if="narrow === true" class="_section">
<MkAvatar class="avatar" :user="user" :disable-preview="true"/> <XPhotos class="_content _vMargin" :user="user" :key="user.id"/>
<div class="title"> <XActivity class="_content _vMargin" :user="user" :key="user.id"/>
<MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true" /></span>
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
</div> </div>
</div> <div :class="{ _section: narrow === true, _vMargin: narrow === false }">
<div class="description"> <XUserTimeline :user="user" class="_content"/>
<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> </div>
<p v-else class="empty">{{ $t('noAccountDescription') }}</p> </template>
</div> <XFollowList v-else-if="page === 'following'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="following" :user="user"/>
<div class="fields system"> <XFollowList v-else-if="page === 'followers'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="followers" :user="user"/>
<dl class="field" v-if="user.location"> </div>
<dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt> <div class="side" v-if="narrow === false">
<dd class="value">{{ user.location }}</dd> <XPhotos class="_vMargin" :user="user" :key="user.id"/>
</dl> <XActivity class="_vMargin" :user="user" :key="user.id"/>
<dl class="field" v-if="user.birthday">
<dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
</dl>
<dl class="field">
<dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
</dl>
</div>
<div class="fields" v-if="user.fields.length > 0">
<dl class="field" v-for="(field, i) in user.fields" :key="i">
<dt class="name">
<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
</dt>
<dd class="value">
<Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
</dd>
</dl>
</div>
<div class="status">
<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
<b>{{ number(user.notesCount) }}</b>
<span>{{ $t('notes') }}</span>
</MkA>
<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
<b>{{ number(user.followingCount) }}</b>
<span>{{ $t('following') }}</span>
</MkA>
<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
<b>{{ number(user.followersCount) }}</b>
<span>{{ $t('followers') }}</span>
</MkA>
</div>
</div> </div>
</div> </div>
<div v-else-if="error">
<template v-if="page === 'index'"> <MkError @retry="fetch()"/>
<div class="_section"> </div>
<div class="_content _vMargin" v-if="user.pinnedNotes.length > 0">
<XNote v-for="note in user.pinnedNotes" class="note _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
</div>
<MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-images">
<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
<div>
<XPhotos :user="user" :key="user.id"/>
</div>
</MkFolder>
<MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-activity">
<template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template>
<div>
<XActivity :user="user" :key="user.id"/>
</div>
</MkFolder>
</div>
<div class="_section">
<XUserTimeline :user="user" class="_content"/>
</div>
</template>
<XFollowList v-else-if="page === 'following'" type="following" :user="user"/>
<XFollowList v-else-if="page === 'followers'" type="followers" :user="user"/>
</div>
<div v-else-if="error">
<MkError @retry="fetch()"/>
</div> </div>
</template> </template>
@ -170,6 +168,7 @@ export default defineComponent({
user: null, user: null,
error: null, error: null,
parallaxAnimationId: null, parallaxAnimationId: null,
narrow: null,
faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt
}; };
}, },
@ -197,6 +196,7 @@ export default defineComponent({
mounted() { mounted() {
window.requestAnimationFrame(this.parallaxLoop); window.requestAnimationFrame(this.parallaxLoop);
this.narrow = this.$el.clientWidth < 1000;
}, },
beforeUnmount() { beforeUnmount() {
@ -254,220 +254,234 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.mk-user-page { .mk-user-page {
> .punished { display: flex;
font-size: 0.8em; max-width: 1050px;
padding: 16px; margin: 0 auto;
}
> .main {
flex: 1;
> .profile { > .punished {
> ._content { font-size: 0.8em;
position: relative; padding: 16px;
overflow: hidden; }
> .banner-container { > .profile {
> ._content {
position: relative; position: relative;
height: 250px;
overflow: hidden; overflow: hidden;
background-size: cover;
background-position: center;
border-radius: 12px;
> .banner { > .banner-container {
height: 100%; position: relative;
background-color: #4c5e6d; height: 250px;
overflow: hidden;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
will-change: background-position;
}
> .fade { > .banner {
position: absolute; height: 100%;
bottom: 0; background-color: #4c5e6d;
left: 0; background-size: cover;
width: 100%; background-position: center;
height: 78px; box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
background: linear-gradient(transparent, rgba(#000, 0.7)); will-change: background-position;
} }
> .followed { > .fade {
position: absolute; position: absolute;
top: 12px; bottom: 0;
left: 12px; left: 0;
padding: 4px 8px; width: 100%;
color: #fff; height: 78px;
background: rgba(0, 0, 0, 0.7); background: linear-gradient(transparent, rgba(#000, 0.7));
font-size: 0.7em; }
border-radius: 6px;
}
> .actions { > .followed {
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 12px; left: 12px;
-webkit-backdrop-filter: blur(8px); padding: 4px 8px;
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.2);
padding: 8px;
border-radius: 24px;
> .menu {
vertical-align: bottom;
height: 31px;
width: 31px;
color: #fff; color: #fff;
text-shadow: 0 0 8px #000; background: rgba(0, 0, 0, 0.7);
font-size: 16px; font-size: 0.7em;
border-radius: 6px;
} }
> .koudoku { > .actions {
margin-left: 4px; position: absolute;
vertical-align: bottom; top: 12px;
} right: 12px;
} -webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.2);
padding: 8px;
border-radius: 24px;
> .title { > .menu {
position: absolute; vertical-align: bottom;
bottom: 0; height: 31px;
left: 0; width: 31px;
width: 100%; color: #fff;
padding: 0 0 8px 154px; text-shadow: 0 0 8px #000;
box-sizing: border-box; font-size: 16px;
color: #fff; }
> .name { > .koudoku {
display: block; margin-left: 4px;
margin: 0; vertical-align: bottom;
line-height: 32px; }
font-weight: bold;
font-size: 1.8em;
text-shadow: 0 0 8px #000;
} }
> .bottom { > .title {
> * { position: absolute;
display: inline-block; bottom: 0;
margin-right: 16px; left: 0;
line-height: 20px; width: 100%;
opacity: 0.8; padding: 0 0 8px 154px;
box-sizing: border-box;
color: #fff;
&.username { > .name {
font-weight: bold; display: block;
margin: 0;
line-height: 32px;
font-weight: bold;
font-size: 1.8em;
text-shadow: 0 0 8px #000;
}
> .bottom {
> * {
display: inline-block;
margin-right: 16px;
line-height: 20px;
opacity: 0.8;
&.username {
font-weight: bold;
}
} }
} }
} }
} }
}
> .title { > .title {
display: none; display: none;
text-align: center;
padding: 50px 8px 16px 8px;
font-weight: bold;
border-bottom: solid 1px var(--divider);
> .bottom {
> * {
display: inline-block;
margin-right: 8px;
opacity: 0.8;
}
}
}
> .avatar {
display: block;
position: absolute;
top: 170px;
left: 16px;
z-index: 2;
width: 120px;
height: 120px;
box-shadow: 1px 1px 3px rgba(#000, 0.2);
}
> .description {
padding: 24px 24px 24px 154px;
font-size: 0.95em;
> .empty {
margin: 0;
opacity: 0.5;
}
}
> .fields {
padding: 24px;
font-size: 0.9em;
border-top: solid 1px var(--divider);
> .field {
display: flex;
padding: 0;
margin: 0;
align-items: center;
&:not(:last-child) {
margin-bottom: 8px;
}
> .name {
width: 30%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
text-align: center;
}
> .value {
width: 70%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
&.system > .field > .name {
}
}
> .status {
display: flex;
padding: 24px;
border-top: solid 1px var(--divider);
> a {
flex: 1;
text-align: center; text-align: center;
padding: 50px 8px 16px 8px;
font-weight: bold;
border-bottom: solid 1px var(--divider);
&.active { > .bottom {
color: var(--accent); > * {
display: inline-block;
margin-right: 8px;
opacity: 0.8;
}
}
}
> .avatar {
display: block;
position: absolute;
top: 170px;
left: 16px;
z-index: 2;
width: 120px;
height: 120px;
box-shadow: 1px 1px 3px rgba(#000, 0.2);
}
> .description {
padding: 24px 24px 24px 154px;
font-size: 0.95em;
> .empty {
margin: 0;
opacity: 0.5;
}
}
> .fields {
padding: 24px;
font-size: 0.9em;
border-top: solid 1px var(--divider);
> .field {
display: flex;
padding: 0;
margin: 0;
align-items: center;
&:not(:last-child) {
margin-bottom: 8px;
}
> .name {
width: 30%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
text-align: center;
}
> .value {
width: 70%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
} }
&:hover { &.system > .field > .name {
text-decoration: none;
} }
}
> b { > .status {
display: block; display: flex;
line-height: 16px; padding: 24px;
} border-top: solid 1px var(--divider);
> span { > a {
font-size: 70%; flex: 1;
text-align: center;
&.active {
color: var(--accent);
}
&:hover {
text-decoration: none;
}
> b {
display: block;
line-height: 16px;
}
> span {
font-size: 70%;
}
} }
} }
} }
} }
> .content {
margin-bottom: var(--margin);
}
} }
> .content { > .side {
margin-bottom: var(--margin); flex-basis: 300px;
margin-left: var(--margin);
} }
&.max-width_500px { &.max-width_500px {
> .profile > ._content { display: block;
> .main > .profile > ._content {
> .banner-container { > .banner-container {
height: 140px; height: 140px;

View File

@ -1,11 +1,5 @@
<template> <template>
<div class="rsqzvsbo _section" v-if="meta"> <div class="rsqzvsbo _section" v-if="meta">
<div class="about">
<h1>{{ instanceName }}</h1>
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
<MkButton @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</MkButton>
<MkButton @click="signin()" style="display: inline-block;">{{ $t('login') }}</MkButton>
</div>
<div class="blocks"> <div class="blocks">
<XBlock class="block" v-for="path in meta.pinnedPages" :initial-path="path" :key="path"/> <XBlock class="block" v-for="path in meta.pinnedPages" :initial-path="path" :key="path"/>
</div> </div>
@ -68,28 +62,6 @@ export default defineComponent({
.rsqzvsbo { .rsqzvsbo {
text-align: center; text-align: center;
> .about {
display: inline-block;
padding: 24px;
margin-bottom: var(--margin);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.5);
border-radius: var(--radius);
text-align: center;
box-sizing: border-box;
min-width: 300px;
max-width: 800px;
&, * {
color: #fff !important;
}
> h1 {
margin: 0 0 16px 0;
}
}
> .blocks { > .blocks {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));

View File

@ -22,7 +22,7 @@ export const router = createRouter({
{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) }, { path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) }, { path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
{ path: '/@:acct/room', props: true, component: page('room/room') }, { path: '/@:acct/room', props: true, component: page('room/room') },
{ path: '/settings/:page?', name: 'settings', component: page('settings/index'), props: route => ({ page: route.params.page || null }) }, { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ page: route.params.page || null }) },
{ path: '/announcements', component: page('announcements') }, { path: '/announcements', component: page('announcements') },
{ path: '/about', component: page('about') }, { path: '/about', component: page('about') },
{ path: '/about-misskey', component: page('about-misskey') }, { path: '/about-misskey', component: page('about-misskey') },
@ -57,7 +57,6 @@ export const router = createRouter({
{ path: '/my/groups/:group', component: page('my-groups/group') }, { path: '/my/groups/:group', component: page('my-groups/group') },
{ path: '/my/antennas', component: page('my-antennas/index') }, { path: '/my/antennas', component: page('my-antennas/index') },
{ path: '/my/clips', component: page('my-clips/index') }, { path: '/my/clips', component: page('my-clips/index') },
{ path: '/my/apps', component: page('apps') },
{ path: '/scratchpad', component: page('scratchpad') }, { path: '/scratchpad', component: page('scratchpad') },
{ path: '/instance', component: page('instance/index') }, { path: '/instance', component: page('instance/index') },
{ path: '/instance/emojis', component: page('instance/emojis') }, { path: '/instance/emojis', component: page('instance/emojis') },

View File

@ -0,0 +1,24 @@
import { device } from '@/cold-storage';
const cache = new Map<string, HTMLAudioElement>();
export function play(type: string) {
const sound = device.get('sound_' + type as any);
if (sound.type == null) return;
playFile(sound.type, sound.volume);
}
export function playFile(file: string, volume: number) {
const masterVolume = device.get('sound_masterVolume');
if (masterVolume === 0) return;
let audio: HTMLAudioElement;
if (cache.has(file)) {
audio = cache.get(file);
} else {
audio = new Audio(`/assets/sounds/${file}.mp3`);
cache.set(file, audio);
}
audio.volume = masterVolume - ((1 - volume) * masterVolume);
audio.play();
}

View File

@ -15,19 +15,12 @@ export const darkTheme: Theme = require('../themes/_dark.json5');
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
export const builtinThemes = [ export const builtinThemes = [
require('../themes/l-white.json5'), require('../themes/l-light.json5'),
require('../themes/l-red.json5'),
require('../themes/l-green.json5'),
require('../themes/l-blue.json5'),
require('../themes/l-apricot.json5'), require('../themes/l-apricot.json5'),
require('../themes/d-black.json5'), require('../themes/d-dark.json5'),
require('../themes/d-red.json5'),
require('../themes/d-green.json5'),
require('../themes/d-blue.json5'),
require('../themes/d-persimmon.json5'), require('../themes/d-persimmon.json5'),
require('../themes/d-black.json5'),
require('../themes/d-battery-saver.json5'),
] as Theme[]; ] as Theme[];
let timeout = null; let timeout = null;

View File

@ -55,7 +55,7 @@ export const defaultDeviceUserSettings = {
export const defaultDeviceSettings = { export const defaultDeviceSettings = {
lang: null, lang: null,
loadRawImages: false, loadRawImages: false,
alwaysShowNsfw: false, nsfw: 'respect', // respect, force, ignore
useOsNativeEmojis: false, useOsNativeEmojis: false,
serverDisconnectedBehavior: 'quiet', serverDisconnectedBehavior: 'quiet',
accounts: [], accounts: [],
@ -87,14 +87,6 @@ export const defaultDeviceSettings = {
deckColumnAlign: 'left', deckColumnAlign: 'left',
deckAlwaysShowMainColumn: true, deckAlwaysShowMainColumn: true,
deckMainColumnPlace: 'left', deckMainColumnPlace: 'left',
sfxVolume: 0.3,
sfxNote: 'syuilo/down',
sfxNoteMy: 'syuilo/up',
sfxNotification: 'syuilo/pope2',
sfxChat: 'syuilo/pope1',
sfxChatBg: 'syuilo/waon',
sfxAntenna: 'syuilo/triple',
sfxChannel: 'syuilo/square-pico',
userData: {}, userData: {},
}; };

View File

@ -448,10 +448,14 @@ hr {
opacity: 0.7; opacity: 0.7;
} }
._monospace {
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
._code { ._code {
@extend ._monospace;
background: #2d2d2d; background: #2d2d2d;
color: #ccc; color: #ccc;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
padding: 5px; padding: 5px;

View File

@ -19,6 +19,7 @@
divider: 'rgba(255, 255, 255, 0.1)', divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent', indicator: '@accent',
panel: '#000', panel: '#000',
panelHighlight: ':lighten<3<@panel',
panelHeaderBg: ':lighten<3<@panel', panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg', panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@ -19,6 +19,7 @@
divider: 'rgba(0, 0, 0, 0.1)', divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent', indicator: '@accent',
panel: '#fff', panel: '#fff',
panelHighlight: ':darken<3<@panel',
panelHeaderBg: ':lighten<3<@panel', panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg', panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)', panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@ -1,18 +0,0 @@
{
id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
name: 'Battery Saver',
author: 'syuilo',
base: 'dark',
props: {
divider: '#2d2d2d',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
panelShadow: '" 0 0 0 1px var(--divider)',
shadow: 'rgba(255, 255, 255, 0.05)',
modalBg: 'rgba(255, 255, 255, 0.1)',
messageBg: '#1d1d1d',
},
}

View File

@ -1,29 +1,19 @@
{ {
id: '8050783a-7f63-445a-b270-36d0f6ba1677', id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
name: 'Mi Black', name: 'Mi Black',
author: 'syuilo', author: 'syuilo',
desc: 'Default light theme',
base: 'dark', base: 'dark',
props: { props: {
bg: '#272727', divider: '#2d2d2d',
fg: 'rgb(199, 209, 216)', panel: '#0a0a0a',
fgHighlighted: '#fff',
divider: 'rgba(255, 255, 255, 0.14)',
panel: '@bg',
panelShadow: '" 0 0 0 1px var(--divider)',
panelHeaderBg: '@panel', panelHeaderBg: '@panel',
panelHeaderDivider: '@divider', panelHeaderDivider: '@divider',
infoFg: '@accent', panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)',
infoBg: 'rgb(0, 0, 0)', shadow: 'rgba(255, 255, 255, 0.05)',
header: ':alpha<0.7<@bg', modalBg: 'rgba(255, 255, 255, 0.1)',
navBg: '#363636', messageBg: '#1d1d1d',
renote: '@accent',
mention: '#da6d35',
mentionMe: '#d44c4c',
hashtag: '#4cb8d4',
link: '@accent',
}, },
} }

View File

@ -1,29 +0,0 @@
{
id: 'ab4eb6d5-dcc0-4457-8a3c-98aad8ea3979',
name: 'Mi D Blue',
author: 'syuilo',
base: 'dark',
props: {
accent: 'rgb(81 185 189)',
bg: 'rgb(54, 54, 54)',
fg: 'rgb(199, 209, 216)',
fgHighlighted: '#fff',
divider: 'rgba(255, 255, 255, 0.14)',
panel: '@bg',
panelShadow: '" 0 0 0 1px var(--divider)',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
infoFg: '@accent',
infoBg: 'rgb(0, 0, 0)',
header: ':alpha<0.7<@bg',
navBg: 'rgb(71, 71, 71)',
renote: '@accent',
mention: '#da6d35',
mentionMe: '#d44c4c',
hashtag: '#4cb8d4',
link: '@accent',
},
}

View File

@ -1,25 +1,25 @@
{ {
id: '60960086-26da-4f3c-bb0c-f6a4f89e0f60', id: '8050783a-7f63-445a-b270-36d0f6ba1677',
name: 'Mi D Red', name: 'Mi Dark',
author: 'syuilo', author: 'syuilo',
desc: 'Default light theme',
base: 'dark', base: 'dark',
props: { props: {
accent: 'rgb(196 115 69)', bg: '#232323',
bg: 'rgb(54, 54, 54)',
fg: 'rgb(199, 209, 216)', fg: 'rgb(199, 209, 216)',
fgHighlighted: '#fff', fgHighlighted: '#fff',
divider: 'rgba(255, 255, 255, 0.14)', divider: 'rgba(255, 255, 255, 0.14)',
panel: '@bg', panel: '#2d2d2d',
panelShadow: '" 0 0 0 1px var(--divider)', panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)',
panelHeaderBg: '@panel', panelHeaderBg: '@panel',
panelHeaderDivider: '@divider', panelHeaderDivider: '@divider',
infoFg: '@accent', infoFg: '@accent',
infoBg: 'rgb(0, 0, 0)', infoBg: 'rgb(0, 0, 0)',
header: ':alpha<0.7<@bg', header: ':alpha<0.7<@bg',
navBg: 'rgb(71, 71, 71)', navBg: '#363636',
renote: '@accent', renote: '@accent',
mention: '#da6d35', mention: '#da6d35',
mentionMe: '#d44c4c', mentionMe: '#d44c4c',

View File

@ -1,29 +0,0 @@
{
id: '326dc4bf-29d9-45b4-889e-bdc33e84919b',
name: 'Mi D Green',
author: 'syuilo',
base: 'dark',
props: {
accent: 'rgb(152, 196, 69)',
bg: 'rgb(54, 54, 54)',
fg: 'rgb(199, 209, 216)',
fgHighlighted: '#fff',
divider: 'rgba(255, 255, 255, 0.14)',
panel: '@bg',
panelShadow: '" 0 0 0 1px var(--divider)',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
infoFg: '@accent',
infoBg: 'rgb(0, 0, 0)',
header: ':alpha<0.7<@bg',
navBg: 'rgb(71, 71, 71)',
renote: '@accent',
mention: '#da6d35',
mentionMe: '#d44c4c',
hashtag: '#4cb8d4',
link: '@accent',
},
}

View File

@ -1,23 +1,23 @@
{ {
id: 'c503d768-7c70-4db2-a4e6-08264304bc8d', id: 'c503d768-7c70-4db2-a4e6-08264304bc8d',
name: 'Ai Persimmon', name: 'Mi Persimmon',
author: 'syuilo', author: 'syuilo',
base: 'dark', base: 'dark',
props: { props: {
accent: 'rgb(206, 102, 65)', accent: 'rgb(206, 102, 65)',
bg: 'rgb(41, 43, 41)', bg: 'rgb(31, 33, 31)',
fg: '#cdd8c7', fg: '#cdd8c7',
fgHighlighted: '#fff', fgHighlighted: '#fff',
divider: 'rgba(255, 255, 255, 0.14)', divider: 'rgba(255, 255, 255, 0.14)',
panel: '@bg', panel: 'rgb(41, 43, 41)',
panelShadow: '" 0 0 0 1px var(--divider)', panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)',
panelHeaderBg: '@panel', panelHeaderBg: '@panel',
panelHeaderDivider: '@divider', panelHeaderDivider: '@divider',
infoFg: '@accent', infoFg: '@fg',
infoBg: 'rgb(0, 0, 0)', infoBg: '#333c3b',
header: ':alpha<0.7<@bg', header: ':alpha<0.7<@bg',
navBg: '#1f211f', navBg: '#1f211f',
renote: '@accent', renote: '@accent',

View File

@ -1,7 +1,7 @@
{ {
id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b',
name: 'Ai Apricot', name: 'Mi Apricot',
author: 'syuilo', author: 'syuilo',
base: 'light', base: 'light',

View File

@ -1,21 +0,0 @@
{
id: 'ad18a23b-6af6-4af0-9ed4-600568250574',
name: 'Mi L Blue',
author: 'syuilo',
base: 'light',
props: {
accent: '#4dbccc',
bg: '#fff',
fg: '#5d5d5d',
divider: 'rgb(223, 223, 223)',
header: ':alpha<0.7<@bg',
navBg: '@bg',
panel: '@bg',
panelShadow: '" 0 0 0 1px var(--divider)',
panelHeaderDivider: '@divider',
messageBg: '#dedede',
},
}

View File

@ -1,21 +0,0 @@
{
id: 'a55af79a-12bf-4f8d-a0cc-718957ad59b4',
name: 'Mi L Green',
author: 'syuilo',
base: 'light',
props: {
accent: '#8bcc4d',
bg: '#fff',
fg: '#5d5d5d',
divider: 'rgb(223, 223, 223)',
header: ':alpha<0.7<@bg',
navBg: '@bg',
panel: '@bg',
panelShadow: '" 0 0 0 1px var(--divider)',
panelHeaderDivider: '@divider',
messageBg: '#dedede',
},
}

View File

@ -1,7 +1,7 @@
{ {
id: '4eea646f-7afa-4645-83e9-83af0333cd37', id: '4eea646f-7afa-4645-83e9-83af0333cd37',
name: 'Mi White', name: 'Mi Light',
author: 'syuilo', author: 'syuilo',
desc: 'Default light theme', desc: 'Default light theme',

View File

@ -1,21 +0,0 @@
{
id: '957db7cb-30fb-4c80-bf0b-04198e7ae7e3',
name: 'Mi L Red',
author: 'syuilo',
base: 'light',
props: {
accent: '#fb734d',
bg: '#fff',
fg: '#5d5d5d',
divider: 'rgb(223, 223, 223)',
header: ':alpha<0.7<@bg',
navBg: '@bg',
panel: '@bg',
panelShadow: '" 0 0 0 1px var(--divider)',
panelHeaderDivider: '@divider',
messageBg: '#dedede',
},
}

View File

@ -15,8 +15,9 @@
<script lang="ts"> <script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue'; import { defineAsyncComponent, defineComponent } from 'vue';
import { stream, sound, popup, popups, uploads, pendingApiRequestsCount } from '@/os'; import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os';
import { store } from '@/store'; import { store } from '@/store';
import * as sound from '@/scripts/sound';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -38,7 +39,7 @@ export default defineComponent({
}, {}, 'closed'); }, {}, 'closed');
} }
sound('notification'); sound.play('notification');
}; };
if (store.getters.isSignedIn) { if (store.getters.isSignedIn) {

View File

@ -1,209 +1,19 @@
<template> <template>
<div class="mk-app"> <DesignA/>
<header> <XCommon/>
<MkA class="link" to="/">{{ $t('home') }}</MkA>
<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
<MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
</header>
<div class="banner" :class="{ asBg: $route.path === '/' }" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<h1 v-if="$route.path !== '/'">{{ instanceName }}</h1>
</div>
<div class="contents" ref="contents" :class="{ wallpaper }">
<header class="header" ref="header" v-show="$route.path !== '/'">
<XHeader :info="pageInfo"/>
</header>
<main ref="main">
<router-view v-slot="{ Component }">
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
<component :is="Component" :ref="changePage"/>
</transition>
</router-view>
</main>
<div class="powered-by">
<b><MkA to="/">{{ host }}</MkA></b>
<small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
</div>
</div>
<XCommon/>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue'; import { defineComponent, defineAsyncComponent } from 'vue';
import { } from '@fortawesome/free-solid-svg-icons'; import DesignA from './visitor/a.vue';
import { host, instanceName } from '@/config'; import DesignB from './visitor/b.vue';
import { search } from '@/scripts/search';
import * as os from '@/os';
import XHeader from './_common_/header.vue';
import XCommon from './_common_/common.vue'; import XCommon from './_common_/common.vue';
const DESKTOP_THRESHOLD = 1100;
export default defineComponent({ export default defineComponent({
components: { components: {
XCommon, XCommon,
XHeader, DesignA,
DesignB,
}, },
data() {
return {
host,
instanceName,
pageKey: 0,
pageInfo: null,
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
};
},
computed: {
keymap(): any {
return {
'd': () => {
if (this.$store.state.device.syncDeviceDarkMode) return;
this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
},
's': search,
'h|/': this.help
};
},
},
watch: {
$route(to, from) {
this.pageKey++;
},
},
created() {
document.documentElement.style.overflowY = 'scroll';
},
mounted() {
if (!this.isDesktop) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
}, { passive: true });
}
},
methods: {
changePage(page) {
if (page == null) return;
if (page.INFO) {
this.pageInfo = page.INFO;
}
},
top() {
window.scroll({ top: 0, behavior: 'smooth' });
},
help() {
this.$router.push('/docs/keyboard-shortcut');
},
onTransition() {
if (window._scroll) window._scroll();
},
}
}); });
</script> </script>
<style lang="scss" scoped>
.mk-app {
min-height: 100vh;
> header {
position: relative;
z-index: 1;
background: var(--panel);
padding: 0 16px;
text-align: center;
overflow: auto;
white-space: nowrap;
> .link {
display: inline-block;
line-height: 60px;
padding: 0 0.7em;
&.MkA-active {
box-shadow: 0 -2px 0 0 var(--accent) inset;
}
}
}
> .banner {
position: relative;
width: 100%;
height: 200px;
background-size: cover;
background-position: center;
&.asBg {
position: absolute;
left: 0;
height: 320px;
}
&:after {
content: "";
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(transparent, var(--bg));
}
> h1 {
margin: 0;
text-align: center;
color: #fff;
text-shadow: 0 0 8px #000;
line-height: 200px;
}
}
> .contents {
position: relative;
z-index: 1;
> .header {
position: sticky;
top: 0;
left: 0;
z-index: 1000;
height: 60px;
width: 100%;
line-height: 60px;
text-align: center;
-webkit-backdrop-filter: blur(32px);
backdrop-filter: blur(32px);
background-color: var(--header);
border-bottom: 1px solid var(--divider);
}
> .powered-by {
padding: 28px;
font-size: 14px;
text-align: center;
border-top: 1px solid var(--divider);
> small {
display: block;
margin-top: 8px;
opacity: 0.5;
}
}
}
}
</style>
<style lang="scss">
</style>

357
src/client/ui/visitor/a.vue Normal file
View File

@ -0,0 +1,357 @@
<template>
<div class="mk-app">
<div class="banner" v-if="$route.path === '/'" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<div>
<header>
<MkA class="link" to="/">{{ $t('home') }}</MkA>
<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
<MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
</header>
<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
<div class="about" v-if="meta">
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
</div>
<div class="action">
<button class="_button primary" @click="signup()">{{ $t('signup') }}</button>
<button class="_button" @click="signin()">{{ $t('login') }}</button>
</div>
</div>
</div>
<div class="banner-mini" v-else :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<div>
<header>
<MkA class="link" to="/">{{ $t('home') }}</MkA>
<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
<MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
<div class="action">
<button class="_button primary" @click="signup()">{{ $t('signup') }}</button>
<button class="_button" @click="signin()">{{ $t('login') }}</button>
</div>
</header>
<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
</div>
</div>
<div class="main">
<div class="contents" ref="contents" :class="{ wallpaper }">
<header class="header" ref="header" v-show="$route.path !== '/'">
<XHeader :info="pageInfo"/>
</header>
<main ref="main">
<router-view v-slot="{ Component }">
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
<component :is="Component" :ref="changePage"/>
</transition>
</router-view>
</main>
<div class="powered-by">
<b><MkA to="/">{{ host }}</MkA></b>
<small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { } from '@fortawesome/free-solid-svg-icons';
import { host, instanceName } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
import MkPagination from '@/components/ui/pagination.vue';
import XSigninDialog from '@/components/signin-dialog.vue';
import XSignupDialog from '@/components/signup-dialog.vue';
import MkButton from '@/components/ui/button.vue';
import XHeader from '../_common_/header.vue';
const DESKTOP_THRESHOLD = 1100;
export default defineComponent({
components: {
XHeader,
MkPagination,
MkButton,
},
data() {
return {
host,
instanceName,
pageKey: 0,
pageInfo: null,
meta: null,
narrow: window.innerWidth < 1280,
announcements: {
endpoint: 'announcements',
limit: 10,
},
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
};
},
computed: {
keymap(): any {
return {
'd': () => {
if (this.$store.state.device.syncDeviceDarkMode) return;
this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
},
's': search,
'h|/': this.help
};
},
},
watch: {
$route(to, from) {
this.pageKey++;
},
},
created() {
document.documentElement.style.overflowY = 'scroll';
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
},
mounted() {
if (!this.isDesktop) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
}, { passive: true });
}
},
methods: {
setParallax(el) {
//new simpleParallax(el);
},
changePage(page) {
if (page == null) return;
if (page.INFO) {
this.pageInfo = page.INFO;
}
},
top() {
window.scroll({ top: 0, behavior: 'smooth' });
},
help() {
this.$router.push('/docs/keyboard-shortcut');
},
onTransition() {
if (window._scroll) window._scroll();
},
signin() {
os.popup(XSigninDialog, {
autoSet: true
}, {}, 'closed');
},
signup() {
os.popup(XSignupDialog, {
autoSet: true
}, {}, 'closed');
}
}
});
</script>
<style lang="scss" scoped>
.mk-app {
min-height: 100vh;
> .banner {
position: relative;
width: 100%;
text-align: center;
background-position: center;
background-size: cover;
> div {
height: 100%;
background: rgba(0, 0, 0, 0.3);
* {
color: #fff;
}
> h1 {
margin: 0;
padding: 96px 32px 0 32px;
text-shadow: 0 0 8px black;
> .logo {
vertical-align: bottom;
max-height: 150px;
}
}
> .about {
padding: 32px;
max-width: 580px;
margin: 0 auto;
box-sizing: border-box;
text-shadow: 0 0 8px black;
}
> .action {
padding-bottom: 64px;
> button {
display: inline-block;
padding: 10px 20px;
box-sizing: border-box;
text-align: center;
border-radius: 999px;
background: var(--panel);
color: var(--fg);
&.primary {
background: var(--accent);
color: #fff;
}
&:first-child {
margin-right: 16px;
}
}
}
}
}
> .banner-mini {
position: relative;
width: 100%;
text-align: center;
background-position: center;
background-size: cover;
> div {
position: relative;
z-index: 1;
height: 100%;
background: rgba(0, 0, 0, 0.3);
* {
color: #fff !important;
}
> header {
}
> h1 {
margin: 0;
padding: 32px;
text-shadow: 0 0 8px black;
> .logo {
vertical-align: bottom;
max-height: 100px;
}
}
}
}
> .main {
> header {
position: relative;
z-index: 1;
background: var(--panel);
padding: 0 32px;
text-align: left;
overflow: auto;
white-space: nowrap;
> .link {
display: inline-block;
line-height: 60px;
padding: 0 0.7em;
&.MkA-active {
box-shadow: 0 -2px 0 0 var(--accent) inset;
}
}
}
> .banner {
position: relative;
width: 100%;
height: 200px;
background-size: cover;
background-position: center;
&.asBg {
position: absolute;
left: 0;
height: 320px;
}
&:after {
content: "";
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(transparent, var(--bg));
}
> h1 {
margin: 0;
text-align: center;
color: #fff;
text-shadow: 0 0 8px #000;
line-height: 200px;
}
}
> .contents {
position: relative;
z-index: 1;
> .header {
position: sticky;
top: 0;
left: 0;
z-index: 1000;
height: 60px;
width: 100%;
line-height: 60px;
text-align: center;
-webkit-backdrop-filter: blur(32px);
backdrop-filter: blur(32px);
background-color: var(--header);
border-bottom: 1px solid var(--divider);
}
> .powered-by {
padding: 28px;
font-size: 14px;
text-align: center;
border-top: 1px solid var(--divider);
> small {
display: block;
margin-top: 8px;
opacity: 0.5;
}
}
}
}
}
</style>
<style lang="scss">
</style>

372
src/client/ui/visitor/b.vue Normal file
View File

@ -0,0 +1,372 @@
<template>
<div class="mk-app">
<div class="side" v-if="!narrow">
<div :style="{ backgroundImage: `url(${ $store.state.instance.meta.backgroundImageUrl })` }">
<div class="fade"></div>
<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
<div class="about _panel" v-if="meta">
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
</div>
<div class="action">
<button class="_button primary" @click="signup()">{{ $t('signup') }}</button>
<button class="_button" @click="signin()">{{ $t('login') }}</button>
</div>
<div class="announcements panel">
<header>{{ $t('announcements') }}</header>
<MkPagination :pagination="announcements" #default="{items}" class="list">
<section class="item" v-for="(announcement, i) in items" :key="announcement.id">
<div class="title">{{ announcement.title }}</div>
<div class="content">
<Mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</div>
</section>
</MkPagination>
</div>
</div>
</div>
<div class="main">
<header>
<MkA class="link" to="/">{{ $t('home') }}</MkA>
<MkA class="link" to="/announcements">{{ $t('announcements') }}</MkA>
<MkA class="link" to="/channels">{{ $t('channel') }}</MkA>
<MkA class="link" to="/about">{{ $t('aboutX', { x: instanceName }) }}</MkA>
</header>
<div v-if="narrow" class="banner" :style="{ backgroundImage: `url(${ $store.state.instance.meta.bannerUrl })` }">
<h1 v-if="meta"><img class="logo" v-if="meta.logoImageUrl" :src="meta.logoImageUrl"><span v-else class="text">{{ instanceName }}</span></h1>
</div>
<div class="contents" ref="contents" :class="{ wallpaper }">
<header class="header" ref="header" v-show="$route.path !== '/'">
<XHeader :info="pageInfo"/>
</header>
<main ref="main">
<router-view v-slot="{ Component }">
<transition :name="$store.state.device.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
<component :is="Component" :ref="changePage"/>
</transition>
</router-view>
</main>
<div class="powered-by">
<b><MkA to="/">{{ host }}</MkA></b>
<small>Powered by <a href="https://github.com/syuilo/misskey" target="_blank">Misskey</a></small>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { } from '@fortawesome/free-solid-svg-icons';
import { host, instanceName } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
import MkPagination from '@/components/ui/pagination.vue';
import XSigninDialog from '@/components/signin-dialog.vue';
import XSignupDialog from '@/components/signup-dialog.vue';
import MkButton from '@/components/ui/button.vue';
import XHeader from '../_common_/header.vue';
const DESKTOP_THRESHOLD = 1100;
export default defineComponent({
components: {
XHeader,
MkPagination,
MkButton,
},
data() {
return {
host,
instanceName,
pageKey: 0,
pageInfo: null,
meta: null,
narrow: window.innerWidth < 1280,
announcements: {
endpoint: 'announcements',
limit: 10,
},
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
};
},
computed: {
keymap(): any {
return {
'd': () => {
if (this.$store.state.device.syncDeviceDarkMode) return;
this.$store.commit('device/set', { key: 'darkMode', value: !this.$store.state.device.darkMode });
},
's': search,
'h|/': this.help
};
},
},
watch: {
$route(to, from) {
this.pageKey++;
},
},
created() {
document.documentElement.style.overflowY = 'scroll';
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
},
mounted() {
if (!this.isDesktop) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) this.isDesktop = true;
}, { passive: true });
}
},
methods: {
changePage(page) {
if (page == null) return;
if (page.INFO) {
this.pageInfo = page.INFO;
}
},
top() {
window.scroll({ top: 0, behavior: 'smooth' });
},
help() {
this.$router.push('/docs/keyboard-shortcut');
},
onTransition() {
if (window._scroll) window._scroll();
},
signin() {
os.popup(XSigninDialog, {
autoSet: true
}, {}, 'closed');
},
signup() {
os.popup(XSignupDialog, {
autoSet: true
}, {}, 'closed');
}
}
});
</script>
<style lang="scss" scoped>
.mk-app {
display: flex;
min-height: 100vh;
> .side {
width: 500px;
height: 100vh;
text-align: center;
> div {
position: fixed;
top: 0;
left: 0;
width: 500px;
height: 100vh;
background-position: center;
background-size: cover;
> .panel {
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.5);
border-radius: var(--radius);
&, * {
color: #fff !important;
}
}
> .fade {
position: absolute;
z-index: -1;
top: 0;
left: 0;
width: 100%;
height: 300px;
background: linear-gradient(rgba(#000, 0.5), transparent);
}
> h1 {
display: block;
margin: 0;
padding: 64px 32px 48px 32px;
color: #fff;
> .logo {
vertical-align: bottom;
max-height: 150px;
}
}
> .about {
display: block;
margin: 0 64px 16px 64px;
padding: 24px;
text-align: center;
box-sizing: border-box;
}
> .action {
padding: 0 64px;
> button {
display: block;
width: 100%;
padding: 10px;
box-sizing: border-box;
text-align: center;
border-radius: 999px;
background: var(--panel);
&.primary {
background: var(--accent);
color: #fff;
}
&:first-child {
margin-bottom: 16px;
}
}
}
> .announcements {
margin: 64px 64px 16px 64px;
text-align: left;
> header {
padding: 12px 16px;
border-bottom: solid 1px rgba(255, 255, 255, 0.5);
}
> .list {
max-height: 300px;
overflow: auto;
> .item {
padding: 12px 16px;
& + .item {
border-top: solid 1px rgba(255, 255, 255, 0.5);
}
> .title {
font-weight: bold;
}
}
}
}
}
}
> .main {
flex: 1;
> header {
position: relative;
z-index: 1;
background: var(--panel);
padding: 0 32px;
text-align: left;
overflow: auto;
white-space: nowrap;
> .link {
display: inline-block;
line-height: 60px;
padding: 0 0.7em;
&.MkA-active {
box-shadow: 0 -2px 0 0 var(--accent) inset;
}
}
}
> .banner {
position: relative;
width: 100%;
height: 200px;
background-size: cover;
background-position: center;
&:after {
content: "";
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(transparent, var(--bg));
}
> h1 {
margin: 0;
padding: 32px;
text-align: center;
color: #fff;
text-shadow: 0 0 8px #000;
> .logo {
vertical-align: bottom;
max-height: 150px;
}
}
}
> .contents {
position: relative;
z-index: 1;
> .header {
position: sticky;
top: 0;
left: 0;
z-index: 1000;
height: 60px;
width: 100%;
line-height: 60px;
text-align: center;
-webkit-backdrop-filter: blur(32px);
backdrop-filter: blur(32px);
background-color: var(--header);
border-bottom: 1px solid var(--divider);
}
> .powered-by {
padding: 28px;
font-size: 14px;
text-align: center;
border-top: 1px solid var(--divider);
> small {
display: block;
margin-top: 8px;
opacity: 0.5;
}
}
}
}
}
</style>
<style lang="scss">
</style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="mkw-digitalClock" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }"> <div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
<span> <span>
<span v-text="hh"></span> <span v-text="hh"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> <span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
@ -74,7 +74,6 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.mkw-digitalClock { .mkw-digitalClock {
padding: 16px 0; padding: 16px 0;
font-family: Lucida Console, Courier, monospace;
text-align: center; text-align: center;
} }
</style> </style>

View File

@ -878,3 +878,19 @@ export const test7: Map = {
'--wwww--', '--wwww--',
] ]
}; };
// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
export const test8: Map = {
name: 'Test8',
category: 'Test',
data: [
'--------',
'-----w--',
'w--www--',
'wwwwww--',
'bbbbwww-',
'wwwwww--',
'--www---',
'--ww----',
]
};

View File

@ -77,7 +77,7 @@ export class Meta {
public blockedHosts: string[]; public blockedHosts: string[];
@Column('varchar', { @Column('varchar', {
length: 512, array: true, default: '{"/announcements", "/featured", "/channels", "/explore", "/games/reversi", "/about-misskey"}' length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'
}) })
public pinnedPages: string[]; public pinnedPages: string[];
@ -94,6 +94,18 @@ export class Meta {
}) })
public bannerUrl: string | null; public bannerUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public backgroundImageUrl: string | null;
@Column('varchar', {
length: 512,
nullable: true
})
public logoImageUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 512, length: 512,
nullable: true, nullable: true,

View File

@ -35,6 +35,8 @@ export class NoteReaction {
@JoinColumn() @JoinColumn()
public note: Note | null; public note: Note | null;
// TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため)
@Column('varchar', { @Column('varchar', {
length: 260 length: 260
}) })

View File

@ -111,6 +111,12 @@ export class UserProfile {
}) })
public autoAcceptFollowed: boolean; public autoAcceptFollowed: boolean;
@Column('boolean', {
default: false,
comment: 'Whether reject index by crawler.'
})
public noCrawle: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
}) })

View File

@ -48,7 +48,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url); return thumbnail ? (file.thumbnailUrl || (isImage ? (file.webpublicUrl || file.url) : null)) : (file.webpublicUrl || file.url);
} }
public async clacDriveUsageOf(user: User['id'] | User): Promise<number> { public async calcDriveUsageOf(user: User['id'] | User): Promise<number> {
const id = typeof user === 'object' ? user.id : user; const id = typeof user === 'object' ? user.id : user;
const { sum } = await this const { sum } = await this
@ -60,7 +60,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
return parseInt(sum, 10) || 0; return parseInt(sum, 10) || 0;
} }
public async clacDriveUsageOfHost(host: string): Promise<number> { public async calcDriveUsageOfHost(host: string): Promise<number> {
const { sum } = await this const { sum } = await this
.createQueryBuilder('file') .createQueryBuilder('file')
.where('file.userHost = :host', { host: toPuny(host) }) .where('file.userHost = :host', { host: toPuny(host) })
@ -70,7 +70,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
return parseInt(sum, 10) || 0; return parseInt(sum, 10) || 0;
} }
public async clacDriveUsageOfLocal(): Promise<number> { public async calcDriveUsageOfLocal(): Promise<number> {
const { sum } = await this const { sum } = await this
.createQueryBuilder('file') .createQueryBuilder('file')
.where('file.userHost IS NULL') .where('file.userHost IS NULL')
@ -80,7 +80,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
return parseInt(sum, 10) || 0; return parseInt(sum, 10) || 0;
} }
public async clacDriveUsageOfRemote(): Promise<number> { public async calcDriveUsageOfRemote(): Promise<number> {
const { sum } = await this const { sum } = await this
.createQueryBuilder('file') .createQueryBuilder('file')
.where('file.userHost IS NOT NULL') .where('file.userHost IS NOT NULL')

View File

@ -239,6 +239,7 @@ export class UserRepository extends Repository<User> {
alwaysMarkNsfw: profile!.alwaysMarkNsfw, alwaysMarkNsfw: profile!.alwaysMarkNsfw,
carefulBot: profile!.carefulBot, carefulBot: profile!.carefulBot,
autoAcceptFollowed: profile!.autoAcceptFollowed, autoAcceptFollowed: profile!.autoAcceptFollowed,
noCrawle: profile!.noCrawle,
hasUnreadSpecifiedNotes: NoteUnreads.count({ hasUnreadSpecifiedNotes: NoteUnreads.count({
where: { userId: user.id, isSpecified: true }, where: { userId: user.id, isSpecified: true },
take: 1 take: 1

View File

@ -94,6 +94,14 @@ export const meta = {
} }
}, },
backgroundImageUrl: {
validator: $.optional.nullable.str,
},
logoImageUrl: {
validator: $.optional.nullable.str,
},
name: { name: {
validator: $.optional.nullable.str, validator: $.optional.nullable.str,
desc: { desc: {
@ -473,6 +481,14 @@ export default define(meta, async (ps, me) => {
set.iconUrl = ps.iconUrl; set.iconUrl = ps.iconUrl;
} }
if (ps.backgroundImageUrl !== undefined) {
set.backgroundImageUrl = ps.backgroundImageUrl;
}
if (ps.logoImageUrl !== undefined) {
set.logoImageUrl = ps.logoImageUrl;
}
if (ps.name !== undefined) { if (ps.name !== undefined) {
set.name = ps.name; set.name = ps.name;
} }

View File

@ -34,7 +34,7 @@ export default define(meta, async (ps, user) => {
const instance = await fetchMeta(true); const instance = await fetchMeta(true);
// Calculate drive usage // Calculate drive usage
const usage = await DriveFiles.clacDriveUsageOf(user); const usage = await DriveFiles.calcDriveUsageOf(user);
return { return {
capacity: 1024 * 1024 * instance.localDriveCapacityMb, capacity: 1024 * 1024 * instance.localDriveCapacityMb,

View File

@ -106,6 +106,13 @@ export const meta = {
} }
}, },
noCrawle: {
validator: $.optional.bool,
desc: {
'ja-JP': '検索エンジンによるインデックスを拒否するか否か'
}
},
isBot: { isBot: {
validator: $.optional.bool, validator: $.optional.bool,
desc: { desc: {
@ -204,6 +211,7 @@ export default define(meta, async (ps, user, token) => {
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw;

View File

@ -129,6 +129,8 @@ export default define(meta, async (ps, me) => {
bannerUrl: instance.bannerUrl, bannerUrl: instance.bannerUrl,
errorImageUrl: instance.errorImageUrl, errorImageUrl: instance.errorImageUrl,
iconUrl: instance.iconUrl, iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH), maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
emojis: await Emojis.packMany(emojis), emojis: await Emojis.packMany(emojis),
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,

View File

@ -0,0 +1,144 @@
import $ from 'cafy';
import define from '../../define';
import { ApiError } from '../../error';
import { ID } from '../../../../misc/cafy-id';
import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, ReversiGames, Users } from '../../../../models';
export const meta = {
tags: ['users'],
requireCredential: false as const,
params: {
userId: {
validator: $.type(ID),
},
},
errors: {
noSuchUser: {
message: 'No such user.',
code: 'NO_SUCH_USER',
id: '9e638e45-3b25-4ef7-8f95-07e8498f1819'
},
}
};
export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId);
if (user == null) {
throw new ApiError(meta.errors.noSuchUser);
}
const [
notesCount,
repliesCount,
renotesCount,
repliedCount,
renotedCount,
pollVotesCount,
pollVotedCount,
localFollowingCount,
remoteFollowingCount,
localFollowersCount,
remoteFollowersCount,
sentReactionsCount,
receivedReactionsCount,
noteFavoritesCount,
pageLikesCount,
pageLikedCount,
driveFilesCount,
driveUsage,
reversiCount,
] = await Promise.all([
Notes.createQueryBuilder('note')
.where('note.userId = :userId', { userId: user.id })
.getCount(),
Notes.createQueryBuilder('note')
.where('note.userId = :userId', { userId: user.id })
.andWhere('note.replyId IS NOT NULL')
.getCount(),
Notes.createQueryBuilder('note')
.where('note.userId = :userId', { userId: user.id })
.andWhere('note.renoteId IS NOT NULL')
.getCount(),
Notes.createQueryBuilder('note')
.where('note.replyUserId = :userId', { userId: user.id })
.getCount(),
Notes.createQueryBuilder('note')
.where('note.renoteUserId = :userId', { userId: user.id })
.getCount(),
PollVotes.createQueryBuilder('vote')
.where('vote.userId = :userId', { userId: user.id })
.getCount(),
PollVotes.createQueryBuilder('vote')
.innerJoin('vote.note', 'note')
.where('note.userId = :userId', { userId: user.id })
.getCount(),
Followings.createQueryBuilder('following')
.where('following.followerId = :userId', { userId: user.id })
.andWhere('following.followeeHost IS NULL')
.getCount(),
Followings.createQueryBuilder('following')
.where('following.followerId = :userId', { userId: user.id })
.andWhere('following.followeeHost IS NOT NULL')
.getCount(),
Followings.createQueryBuilder('following')
.where('following.followeeId = :userId', { userId: user.id })
.andWhere('following.followerHost IS NULL')
.getCount(),
Followings.createQueryBuilder('following')
.where('following.followeeId = :userId', { userId: user.id })
.andWhere('following.followerHost IS NOT NULL')
.getCount(),
NoteReactions.createQueryBuilder('reaction')
.where('reaction.userId = :userId', { userId: user.id })
.getCount(),
NoteReactions.createQueryBuilder('reaction')
.innerJoin('reaction.note', 'note')
.where('note.userId = :userId', { userId: user.id })
.getCount(),
NoteFavorites.createQueryBuilder('favorite')
.where('favorite.userId = :userId', { userId: user.id })
.getCount(),
PageLikes.createQueryBuilder('like')
.where('like.userId = :userId', { userId: user.id })
.getCount(),
PageLikes.createQueryBuilder('like')
.innerJoin('like.page', 'page')
.where('page.userId = :userId', { userId: user.id })
.getCount(),
DriveFiles.createQueryBuilder('file')
.where('file.userId = :userId', { userId: user.id })
.getCount(),
DriveFiles.calcDriveUsageOf(user),
ReversiGames.createQueryBuilder('game')
.where('game.user1Id = :userId', { userId: user.id })
.orWhere('game.user2Id = :userId', { userId: user.id })
.getCount(),
]);
return {
notesCount,
repliesCount,
renotesCount,
repliedCount,
renotedCount,
pollVotesCount,
pollVotedCount,
localFollowingCount,
remoteFollowingCount,
localFollowersCount,
remoteFollowersCount,
followingCount: localFollowingCount + remoteFollowingCount,
followersCount: localFollowersCount + remoteFollowersCount,
sentReactionsCount,
receivedReactionsCount,
noteFavoritesCount,
pageLikesCount,
pageLikedCount,
driveFilesCount,
driveUsage,
reversiCount,
};
});

View File

@ -21,10 +21,11 @@ import apiServer from './api';
import { sum } from '../prelude/array'; import { sum } from '../prelude/array';
import Logger from '../services/logger'; import Logger from '../services/logger';
import { program } from '../argv'; import { program } from '../argv';
import { UserProfiles } from '../models'; import { UserProfiles, Users } from '../models';
import { networkChart } from '../services/chart'; import { networkChart } from '../services/chart';
import { genAvatar } from '../misc/gen-avatar'; import { genAvatar } from '../misc/gen-avatar';
import { createTemp } from '../misc/create-temp'; import { createTemp } from '../misc/create-temp';
import { publishMainStream } from '../services/stream';
export const serverLogger = new Logger('server', 'gray', false); export const serverLogger = new Logger('server', 'gray', false);
@ -83,10 +84,15 @@ router.get('/verify-email/:code', async ctx => {
ctx.body = 'Verify succeeded!'; ctx.body = 'Verify succeeded!';
ctx.status = 200; ctx.status = 200;
UserProfiles.update({ userId: profile.userId }, { await UserProfiles.update({ userId: profile.userId }, {
emailVerified: true, emailVerified: true,
emailVerifyCode: null emailVerifyCode: null
}); });
publishMainStream(profile.userId, 'meUpdated', await Users.pack(profile.userId, profile.userId, {
detail: true,
includeSecrets: true
}));
} else { } else {
ctx.status = 404; ctx.status = 404;
} }

View File

@ -242,9 +242,11 @@ router.get('/notes/:note', async ctx => {
if (note) { if (note) {
const _note = await Notes.pack(note); const _note = await Notes.pack(note);
const profile = await UserProfiles.findOne(note.userId).then(ensure);
const meta = await fetchMeta(); const meta = await fetchMeta();
await ctx.render('note', { await ctx.render('note', {
note: _note, note: _note,
profile,
// TODO: Let locale changeable by instance setting // TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note, locales['ja-JP']), summary: getNoteSummary(_note, locales['ja-JP']),
instanceName: meta.name || 'Misskey', instanceName: meta.name || 'Misskey',
@ -280,9 +282,11 @@ router.get('/@:user/pages/:page', async ctx => {
if (page) { if (page) {
const _page = await Pages.pack(page); const _page = await Pages.pack(page);
const profile = await UserProfiles.findOne(page.userId).then(ensure);
const meta = await fetchMeta(); const meta = await fetchMeta();
await ctx.render('page', { await ctx.render('page', {
page: _page, page: _page,
profile,
instanceName: meta.name || 'Misskey' instanceName: meta.name || 'Misskey'
}); });
@ -307,9 +311,11 @@ router.get('/clips/:clip', async ctx => {
if (clip) { if (clip) {
const _clip = await Clips.pack(clip); const _clip = await Clips.pack(clip);
const profile = await UserProfiles.findOne(clip.userId).then(ensure);
const meta = await fetchMeta(); const meta = await fetchMeta();
await ctx.render('clip', { await ctx.render('clip', {
clip: _clip, clip: _clip,
profile,
instanceName: meta.name || 'Misskey' instanceName: meta.name || 'Misskey'
}); });

View File

@ -19,6 +19,9 @@ block og
meta(property='og:image' content= user.avatarUrl) meta(property='og:image' content= user.avatarUrl)
block meta block meta
if profile.noCrawle
meta(name='robots' content='noindex')
meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)
meta(name='misskey:clip-id' content=clip.id) meta(name='misskey:clip-id' content=clip.id)

View File

@ -19,6 +19,9 @@ block og
meta(property='og:image' content= user.avatarUrl) meta(property='og:image' content= user.avatarUrl)
block meta block meta
if user.host || profile.noCrawle
meta(name='robots' content='noindex')
meta(name='misskey:user-username' content=user.username) meta(name='misskey:user-username' content=user.username)
meta(name='misskey:user-id' content=user.id) meta(name='misskey:user-id' content=user.id)
meta(name='misskey:note-id' content=note.id) meta(name='misskey:note-id' content=note.id)
@ -26,9 +29,6 @@ block meta
meta(name='twitter:card' content='summary') meta(name='twitter:card' content='summary')
// todo // todo
if user.host
meta(name='robots' content='noindex')
if user.twitter if user.twitter
meta(name='twitter:creator' content=`@${user.twitter.screenName}`) meta(name='twitter:creator' content=`@${user.twitter.screenName}`)

Some files were not shown because too many files have changed in this diff Show More