mirror of
https://codeberg.org/yeentown/barkey
synced 2024-11-22 14:55:12 +00:00
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:
parent
7660839e40
commit
0144408500
106 changed files with 4489 additions and 1734 deletions
|
@ -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: "内容"
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
16
migration/1605965516823-instance-images.ts
Normal file
16
migration/1605965516823-instance-images.ts
Normal 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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
migration/1606191203881-no-crawle.ts
Normal file
16
migration/1606191203881-no-crawle.ts
Normal 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"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
src/client/assets/sounds/syuilo/kick.mp3
Normal file
BIN
src/client/assets/sounds/syuilo/kick.mp3
Normal file
Binary file not shown.
BIN
src/client/assets/sounds/syuilo/snare.mp3
Normal file
BIN
src/client/assets/sounds/syuilo/snare.mp3
Normal file
Binary file not shown.
34
src/client/cold-storage.ts
Normal file
34
src/client/cold-storage.ts
Normal 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));
|
||||||
|
},
|
||||||
|
};
|
|
@ -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>
|
||||||
|
|
56
src/client/components/form/base.vue
Normal file
56
src/client/components/form/base.vue
Normal 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>
|
81
src/client/components/form/button.vue
Normal file
81
src/client/components/form/button.vue
Normal 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>
|
34
src/client/components/form/form.scss
Normal file
34
src/client/components/form/form.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
42
src/client/components/form/group.vue
Normal file
42
src/client/components/form/group.vue
Normal 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>
|
306
src/client/components/form/input.vue
Normal file
306
src/client/components/form/input.vue
Normal 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>
|
30
src/client/components/form/key-value-view.vue
Normal file
30
src/client/components/form/key-value-view.vue
Normal 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>
|
90
src/client/components/form/link.vue
Normal file
90
src/client/components/form/link.vue
Normal 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>
|
42
src/client/components/form/pagination.vue
Normal file
42
src/client/components/form/pagination.vue
Normal 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>
|
106
src/client/components/form/radios.vue
Normal file
106
src/client/components/form/radios.vue
Normal 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>
|
122
src/client/components/form/range.vue
Normal file
122
src/client/components/form/range.vue
Normal 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>
|
147
src/client/components/form/select.vue
Normal file
147
src/client/components/form/select.vue
Normal 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>
|
132
src/client/components/form/switch.vue
Normal file
132
src/client/components/form/switch.vue
Normal 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>
|
136
src/client/components/form/textarea.vue
Normal file
136
src/client/components/form/textarea.vue
Normal 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>
|
36
src/client/components/form/tuple.vue
Normal file
36
src/client/components/form/tuple.vue
Normal 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>
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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([]);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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
|
185
src/client/pages/settings/account-info.vue
Normal file
185
src/client/pages/settings/account-info.vue
Normal 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>
|
|
@ -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'],
|
||||||
|
|
|
@ -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,7 +89,6 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.bfomjevm {
|
.bfomjevm {
|
||||||
> .token {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
||||||
|
@ -98,6 +109,5 @@ export default defineComponent({
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
90
src/client/pages/settings/deck.vue
Normal file
90
src/client/pages/settings/deck.vue
Normal 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>
|
71
src/client/pages/settings/email-address.vue
Normal file
71
src/client/pages/settings/email-address.vue
Normal 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>
|
52
src/client/pages/settings/email.vue
Normal file
52
src/client/pages/settings/email.vue
Normal 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>
|
|
@ -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">
|
<FormSelect v-model:value="lang">
|
||||||
<MkRadios v-model="serverDisconnectedBehavior">
|
<template #label>{{ $t('uiLanguage') }}</template>
|
||||||
<template #desc>{{ $t('whenServerDisconnected') }}</template>
|
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
|
||||||
|
<template #caption>
|
||||||
|
<i18n-t keypath="i18nInfo" tag="span">
|
||||||
|
<template #link>
|
||||||
|
<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</template>
|
||||||
|
</FormSelect>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<template #label>{{ $t('behavior') }}</template>
|
||||||
|
<FormSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</FormSwitch>
|
||||||
|
<FormSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</FormSwitch>
|
||||||
|
<FormSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</FormSwitch>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormSelect v-model:value="serverDisconnectedBehavior">
|
||||||
|
<template #label>{{ $t('whenServerDisconnected') }}</template>
|
||||||
<option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option>
|
<option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option>
|
||||||
<option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option>
|
<option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option>
|
||||||
<option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option>
|
<option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option>
|
||||||
</MkRadios>
|
</FormSelect>
|
||||||
<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">
|
<FormGroup>
|
||||||
<div class="_title"><Fa :icon="faCog"/> {{ $t('defaultNavigationBehaviour') }}</div>
|
<template #label>{{ $t('appearance') }}</template>
|
||||||
<div class="_content">
|
<FormSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</FormSwitch>
|
||||||
<MkSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</MkSwitch>
|
<FormSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</FormSwitch>
|
||||||
</div>
|
<FormSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</FormSwitch>
|
||||||
<div class="_content">
|
<FormSwitch v-model:value="useOsNativeEmojis">{{ $t('useOsNativeEmojis') }}
|
||||||
<MkRadios v-model="chatOpenBehavior">
|
<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
|
||||||
<template #desc>{{ $t('chatOpenBehavior') }}</template>
|
</FormSwitch>
|
||||||
<option value="page">{{ $t('showInPage') }}</option>
|
<FormSwitch v-model:value="loadRawImages">{{ $t('loadRawImages') }}</FormSwitch>
|
||||||
<option value="window">{{ $t('openInWindow') }}</option>
|
<FormSwitch v-model:value="disableShowingAnimatedImages">{{ $t('disableShowingAnimatedImages') }}</FormSwitch>
|
||||||
<option value="popout">{{ $t('popout') }}</option>
|
</FormGroup>
|
||||||
</MkRadios>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _vMargin">
|
<FormRadios v-model="fontSize">
|
||||||
<div class="_title"><Fa :icon="faCog"/> {{ $t('appearance') }}</div>
|
|
||||||
<div class="_content">
|
|
||||||
<MkSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</MkSwitch>
|
|
||||||
<MkSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</MkSwitch>
|
|
||||||
<MkSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</MkSwitch>
|
|
||||||
<MkSwitch v-model:value="useOsNativeEmojis">
|
|
||||||
{{ $t('useOsNativeEmojis') }}
|
|
||||||
<template #desc><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
|
|
||||||
</MkSwitch>
|
|
||||||
<MkRadios v-model="fontSize">
|
|
||||||
<template #desc>{{ $t('fontSize') }}</template>
|
<template #desc>{{ $t('fontSize') }}</template>
|
||||||
<option value="small"><span style="font-size: 14px;">Aa</span></option>
|
<option value="small"><span style="font-size: 14px;">Aa</span></option>
|
||||||
<option :value="null"><span style="font-size: 16px;">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="large"><span style="font-size: 18px;">Aa</span></option>
|
||||||
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
|
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
|
||||||
</MkRadios>
|
</FormRadios>
|
||||||
<MkRadios v-model="instanceTicker">
|
|
||||||
<template #desc>{{ $t('instanceTicker') }}</template>
|
<FormSelect v-model:value="instanceTicker">
|
||||||
|
<template #label>{{ $t('instanceTicker') }}</template>
|
||||||
<option value="none">{{ $t('_instanceTicker.none') }}</option>
|
<option value="none">{{ $t('_instanceTicker.none') }}</option>
|
||||||
<option value="remote">{{ $t('_instanceTicker.remote') }}</option>
|
<option value="remote">{{ $t('_instanceTicker.remote') }}</option>
|
||||||
<option value="always">{{ $t('_instanceTicker.always') }}</option>
|
<option value="always">{{ $t('_instanceTicker.always') }}</option>
|
||||||
</MkRadios>
|
</FormSelect>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="_card _vMargin">
|
<FormSelect v-model:value="nsfw">
|
||||||
<div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</div>
|
<template #label>{{ $t('nsfw') }}</template>
|
||||||
<div class="_content">
|
<option value="respect">{{ $t('_nsfw.respect') }}</option>
|
||||||
<div>{{ $t('defaultNavigationBehaviour') }}</div>
|
<option value="ignore">{{ $t('_nsfw.ignore') }}</option>
|
||||||
<MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch>
|
<option value="force">{{ $t('_nsfw.force') }}</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('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: {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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'],
|
||||||
|
|
|
@ -1,40 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<FormBase>
|
||||||
<div class="_section">
|
<FormSwitch :value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
|
||||||
<div class="_card">
|
|
||||||
<div class="_content">
|
|
||||||
<MkSwitch v-model:value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
|
|
||||||
{{ $t('showFeaturedNotesInTimeline') }}
|
{{ $t('showFeaturedNotesInTimeline') }}
|
||||||
</MkSwitch>
|
</FormSwitch>
|
||||||
</div>
|
|
||||||
</div>
|
<FormLink to="/settings/account-info">{{ $t('accountInfo') }}</FormLink>
|
||||||
</div>
|
|
||||||
<div class="_section">
|
<FormGroup>
|
||||||
<MkSwitch v-model:value="debug" @update:value="changeDebug">
|
<FormSwitch 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'],
|
||||||
|
|
|
@ -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>
|
||||||
|
</FormSwitch>
|
||||||
|
<FormSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</FormSwitch>
|
||||||
|
<FormGroup v-if="!rememberNoteVisibility">
|
||||||
<template #label>{{ $t('defaultNoteVisibility') }}</template>
|
<template #label>{{ $t('defaultNoteVisibility') }}</template>
|
||||||
|
<FormSelect v-model:value="defaultNoteVisibility">
|
||||||
<option value="public">{{ $t('_visibility.public') }}</option>
|
<option value="public">{{ $t('_visibility.public') }}</option>
|
||||||
<option value="home">{{ $t('_visibility.home') }}</option>
|
<option value="home">{{ $t('_visibility.home') }}</option>
|
||||||
<option value="followers">{{ $t('_visibility.followers') }}</option>
|
<option value="followers">{{ $t('_visibility.followers') }}</option>
|
||||||
<option value="specified">{{ $t('_visibility.specified') }}</option>
|
<option value="specified">{{ $t('_visibility.specified') }}</option>
|
||||||
</MkSelect>
|
</FormSelect>
|
||||||
<MkSwitch v-model:value="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</MkSwitch>
|
<FormSwitch v-model:value="defaultNoteLocalOnly">{{ $t('_visibility.localOnly') }}</FormSwitch>
|
||||||
</div>
|
</FormGroup>
|
||||||
</div>
|
</FormBase>
|
||||||
</div>
|
|
||||||
</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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
<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"/>
|
<MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkInput v-model:value="name" :max="30">
|
<FormInput v-model:value="name" :max="30">
|
||||||
<span>{{ $t('_profile.name') }}</span>
|
<span>{{ $t('_profile.name') }}</span>
|
||||||
</MkInput>
|
</FormInput>
|
||||||
|
|
||||||
<MkTextarea v-model:value="description" :max="500">
|
<FormTextarea v-model:value="description" :max="500">
|
||||||
<span>{{ $t('_profile.description') }}</span>
|
<span>{{ $t('_profile.description') }}</span>
|
||||||
<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
|
<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
|
||||||
</MkTextarea>
|
</FormTextarea>
|
||||||
|
|
||||||
<MkInput v-model:value="location">
|
<FormInput v-model:value="location">
|
||||||
<span>{{ $t('location') }}</span>
|
<span>{{ $t('location') }}</span>
|
||||||
<template #prefix><Fa :icon="faMapMarkerAlt"/></template>
|
<template #prefix><Fa :icon="faMapMarkerAlt"/></template>
|
||||||
</MkInput>
|
</FormInput>
|
||||||
|
|
||||||
<MkInput v-model:value="birthday" type="date">
|
<FormInput v-model:value="birthday" type="date">
|
||||||
<template #title>{{ $t('birthday') }}</template>
|
<span>{{ $t('birthday') }}</span>
|
||||||
<template #prefix><Fa :icon="faBirthdayCake"/></template>
|
<template #prefix><Fa :icon="faBirthdayCake"/></template>
|
||||||
</MkInput>
|
</FormInput>
|
||||||
|
|
||||||
<details class="fields">
|
<FormGroup>
|
||||||
<summary>{{ $t('_profile.metadata') }}</summary>
|
<FormButton @click="editMetadata" primary>{{ $t('_profile.metadataEdit') }}</FormButton>
|
||||||
<div class="row">
|
<template #caption>{{ $t('_profile.metadataDescription') }}</template>
|
||||||
<MkInput v-model:value="fieldName0">{{ $t('_profile.metadataLabel') }}</MkInput>
|
</FormGroup>
|
||||||
<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>
|
<FormSwitch v-model:value="isCat">{{ $t('flagAsCat') }}<template #desc>{{ $t('flagAsCatDescription') }}</template></FormSwitch>
|
||||||
<MkSwitch v-model:value="isCat">{{ $t('flagAsCat') }}</MkSwitch>
|
|
||||||
</div>
|
<FormSwitch v-model:value="isBot">{{ $t('flagAsBot') }}<template #desc>{{ $t('flagAsBotDescription') }}</template></FormSwitch>
|
||||||
<div class="_footer">
|
|
||||||
<MkButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
|
<FormSwitch v-model:value="alwaysMarkNsfw">{{ $t('alwaysMarkSensitive') }}</FormSwitch>
|
||||||
</div>
|
|
||||||
</div>
|
<FormButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
|
||||||
</div>
|
</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,7 +245,6 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.llvierxe {
|
.llvierxe {
|
||||||
> ._content {
|
|
||||||
> .header {
|
> .header {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
|
@ -215,16 +270,5 @@ export default defineComponent({
|
||||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .fields {
|
|
||||||
> .row {
|
|
||||||
> * {
|
|
||||||
display: inline-block;
|
|
||||||
width: 50%;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
</div>
|
||||||
<MkRadios v-model="reactionPickerWidth">
|
<div class="_formCaption">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormRadios v-model="reactionPickerWidth">
|
||||||
<template #desc>{{ $t('width') }}</template>
|
<template #desc>{{ $t('width') }}</template>
|
||||||
<option :value="1">{{ $t('small') }}</option>
|
<option :value="1">{{ $t('small') }}</option>
|
||||||
<option :value="2">{{ $t('medium') }}</option>
|
<option :value="2">{{ $t('medium') }}</option>
|
||||||
<option :value="3">{{ $t('large') }}</option>
|
<option :value="3">{{ $t('large') }}</option>
|
||||||
</MkRadios>
|
</FormRadios>
|
||||||
<MkRadios v-model="reactionPickerHeight">
|
<FormRadios v-model="reactionPickerHeight">
|
||||||
<template #desc>{{ $t('height') }}</template>
|
<template #desc>{{ $t('height') }}</template>
|
||||||
<option :value="1">{{ $t('small') }}</option>
|
<option :value="1">{{ $t('small') }}</option>
|
||||||
<option :value="2">{{ $t('medium') }}</option>
|
<option :value="2">{{ $t('medium') }}</option>
|
||||||
<option :value="3">{{ $t('large') }}</option>
|
<option :value="3">{{ $t('large') }}</option>
|
||||||
</MkRadios>
|
</FormRadios>
|
||||||
</div>
|
<FormButton @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</FormButton>
|
||||||
<div class="_footer">
|
<FormButton danger @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</FormButton>
|
||||||
<MkButton inline @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
|
</FormBase>
|
||||||
<MkButton inline @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</MkButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
<FormButton primary @click="change()">{{ $t('changePassword') }}</FormButton>
|
||||||
|
<FormPagination :pagination="pagination">
|
||||||
|
<template #label>{{ $t('signinHistory') }}</template>
|
||||||
|
<template #default="{items}">
|
||||||
|
<div class="_formPanel timnmucd" v-for="item in items" :key="item.id">
|
||||||
|
<header>
|
||||||
|
<Fa class="icon succ" :icon="faCheck" v-if="item.success"/>
|
||||||
|
<Fa class="icon fail" :icon="faTimesCircle" v-else/>
|
||||||
|
<code class="ip _monospace">{{ item.ip }}</code>
|
||||||
|
<MkTime :time="item.createdAt" class="time"/>
|
||||||
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<div class="_section">
|
</template>
|
||||||
<MkButton primary @click="change()" full>{{ $t('changePassword') }}</MkButton>
|
</FormPagination>
|
||||||
</div>
|
<FormGroup>
|
||||||
<div class="_section">
|
<FormButton danger @click="regenerateToken"><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</FormButton>
|
||||||
<MkButton class="_vMargin" primary @click="regenerateToken" full><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</MkButton>
|
<template #caption>{{ $t('regenerateLoginTokenDescription') }}</template>
|
||||||
<div class="_caption _vMargin" style="padding: 0 6px;">{{ $t('regenerateLoginTokenDescription') }}</div>
|
</FormGroup>
|
||||||
</div>
|
</FormBase>
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
|
|
@ -1,41 +1,41 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="_section">
|
<FormBase>
|
||||||
<div class="_card">
|
<FormTextarea v-model:value="items" tall>
|
||||||
<div class="_content">
|
|
||||||
<MkTextarea v-model:value="items" tall>
|
|
||||||
<span>{{ $t('sidebar') }}</span>
|
<span>{{ $t('sidebar') }}</span>
|
||||||
<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
|
<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
|
||||||
</MkTextarea>
|
</FormTextarea>
|
||||||
</div>
|
|
||||||
<div class="_content">
|
<FormRadios v-model="sidebarDisplay">
|
||||||
<div>{{ $t('display') }}</div>
|
<template #desc>{{ $t('display') }}</template>
|
||||||
<MkRadio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</MkRadio>
|
<option value="full">{{ $t('_sidebar.full') }}</option>
|
||||||
<MkRadio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</MkRadio>
|
<option value="icon">{{ $t('_sidebar.icon') }}</option>
|
||||||
<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
|
<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
|
||||||
</div>
|
</FormRadios>
|
||||||
<div class="_footer">
|
|
||||||
<MkButton inline @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
|
<FormButton @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
|
||||||
<MkButton inline @click="reset()"><Fa :icon="faRedo"/> {{ $t('default') }}</MkButton>
|
<FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton>
|
||||||
</div>
|
</FormBase>
|
||||||
</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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
106
src/client/pages/settings/theme.install.vue
Normal file
106
src/client/pages/settings/theme.install.vue
Normal 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>
|
103
src/client/pages/settings/theme.manage.vue
Normal file
103
src/client/pages/settings/theme.manage.vue
Normal 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>
|
|
@ -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,75 +176,14 @@ 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 {
|
> .darkMode {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 32px 0;
|
padding: 32px 0;
|
||||||
|
@ -496,6 +404,5 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
||||||
<div class="_content">
|
<FormBase>
|
||||||
|
<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">
|
||||||
</div>
|
<template #key>{{ $t('_wordMute.mutedNotes') }}</template>
|
||||||
</div>
|
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
|
||||||
<div class="_footer">
|
</FormKeyValueView>
|
||||||
<MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<FormButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
|
||||||
|
</FormBase>
|
||||||
</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>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<MkContainer>
|
||||||
|
<template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template>
|
||||||
|
|
||||||
|
<div style="padding: 8px;">
|
||||||
<div ref="chart"></div>
|
<div ref="chart"></div>
|
||||||
</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() {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ujigsodd">
|
<MkContainer>
|
||||||
|
<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
|
||||||
|
<div class="ujigsodd">
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
<div class="stream" v-if="!fetching && images.length > 0">
|
<div class="stream" v-if="!fetching && images.length > 0">
|
||||||
<MkA v-for="image in images"
|
<MkA v-for="image in images"
|
||||||
|
@ -9,21 +11,33 @@
|
||||||
></MkA>
|
></MkA>
|
||||||
</div>
|
</div>
|
||||||
<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
|
<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</MkContainer>
|
||||||
</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;
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="mk-user-page" v-if="user" v-size="{ max: [500] }">
|
<div>
|
||||||
|
<div class="mk-user-page" v-if="user" v-size="{ max: [500] }" :class="{ _section: narrow === false }">
|
||||||
<!-- TODO -->
|
<!-- TODO -->
|
||||||
<!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</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="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> -->
|
||||||
|
|
||||||
<div class="profile _section _fitBottom">
|
<div class="main">
|
||||||
|
<div class="profile _vMargin" :class="{ _section: narrow === true }">
|
||||||
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/>
|
<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>
|
||||||
|
@ -84,32 +86,28 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="page === 'index'">
|
<template v-if="page === 'index'">
|
||||||
<div class="_section">
|
<div v-if="user.pinnedNotes.length > 0" :class="{ _section: narrow === true, _vMargin: narrow === false }">
|
||||||
<div class="_content _vMargin" v-if="user.pinnedNotes.length > 0">
|
<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"/>
|
||||||
<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>
|
</div>
|
||||||
<MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-images">
|
<div v-if="narrow === true" class="_section">
|
||||||
<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
|
<XPhotos class="_content _vMargin" :user="user" :key="user.id"/>
|
||||||
<div>
|
<XActivity class="_content _vMargin" :user="user" :key="user.id"/>
|
||||||
<XPhotos :user="user" :key="user.id"/>
|
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
<div :class="{ _section: narrow === true, _vMargin: narrow === false }">
|
||||||
<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"/>
|
<XUserTimeline :user="user" class="_content"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<XFollowList v-else-if="page === 'following'" type="following" :user="user"/>
|
<XFollowList v-else-if="page === 'following'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="following" :user="user"/>
|
||||||
<XFollowList v-else-if="page === 'followers'" type="followers" :user="user"/>
|
<XFollowList v-else-if="page === 'followers'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="followers" :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="error">
|
<div class="side" v-if="narrow === false">
|
||||||
|
<XPhotos class="_vMargin" :user="user" :key="user.id"/>
|
||||||
|
<XActivity class="_vMargin" :user="user" :key="user.id"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error">
|
||||||
<MkError @retry="fetch()"/>
|
<MkError @retry="fetch()"/>
|
||||||
|
</div>
|
||||||
</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,6 +254,13 @@ export default defineComponent({
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.mk-user-page {
|
.mk-user-page {
|
||||||
|
display: flex;
|
||||||
|
max-width: 1050px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
> .main {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
> .punished {
|
> .punished {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
@ -270,7 +277,6 @@ export default defineComponent({
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
> .banner {
|
> .banner {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -465,9 +471,17 @@ export default defineComponent({
|
||||||
> .content {
|
> .content {
|
||||||
margin-bottom: var(--margin);
|
margin-bottom: var(--margin);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .side {
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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') },
|
||||||
|
|
24
src/client/scripts/sound.ts
Normal file
24
src/client/scripts/sound.ts
Normal 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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)',
|
||||||
|
|
|
@ -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)',
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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',
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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',
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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',
|
||||||
|
|
|
@ -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',
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
357
src/client/ui/visitor/a.vue
Normal 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
372
src/client/ui/visitor/b.vue
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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----',
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
144
src/server/api/endpoints/users/stats.ts
Normal file
144
src/server/api/endpoints/users/stats.ts
Normal 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,
|
||||||
|
};
|
||||||
|
});
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue