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,26 +89,24 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .bfomjevm { | .bfomjevm { | ||||||
| 	> .token { | 	display: flex; | ||||||
| 		display: flex; | 	padding: 16px; | ||||||
| 		padding: 16px; |  | ||||||
| 
 | 
 | ||||||
| 		> .icon { | 	> .icon { | ||||||
| 			display: block; | 		display: block; | ||||||
| 			flex-shrink: 0; | 		flex-shrink: 0; | ||||||
| 			margin: 0 12px 0 0; | 		margin: 0 12px 0 0; | ||||||
| 			width: 50px; | 		width: 50px; | ||||||
| 			height: 50px; | 		height: 50px; | ||||||
| 			border-radius: 8px; | 		border-radius: 8px; | ||||||
| 		} | 	} | ||||||
| 
 | 
 | ||||||
| 		> .body { | 	> .body { | ||||||
| 			width: calc(100% - 62px); | 		width: calc(100% - 62px); | ||||||
| 			position: relative; | 		position: relative; | ||||||
| 
 | 
 | ||||||
| 			> .name { | 		> .name { | ||||||
| 				font-weight: bold; | 			font-weight: bold; | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
							
								
								
									
										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"> |  | ||||||
| 			<MkRadios v-model="serverDisconnectedBehavior"> |  | ||||||
| 				<template #desc>{{ $t('whenServerDisconnected') }}</template> |  | ||||||
| 				<option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option> |  | ||||||
| 				<option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option> |  | ||||||
| 				<option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option> |  | ||||||
| 			</MkRadios> |  | ||||||
| 			<MkSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</MkSwitch> |  | ||||||
| 			<MkSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</MkSwitch> |  | ||||||
| 			<MkSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</MkSwitch> |  | ||||||
| 			<MkSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</MkSwitch> |  | ||||||
| 			<MkSelect v-model:value="lang"> |  | ||||||
| 				<template #label>{{ $t('uiLanguage') }}</template> |  | ||||||
| 				<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> |  | ||||||
| 			</MkSelect> |  | ||||||
| 		</div> |  | ||||||
| 	</section> |  | ||||||
| 
 | 
 | ||||||
| 	<section class="_card _vMargin"> | 	<FormSelect v-model:value="lang"> | ||||||
| 		<div class="_title"><Fa :icon="faCog"/> {{ $t('defaultNavigationBehaviour') }}</div> | 		<template #label>{{ $t('uiLanguage') }}</template> | ||||||
| 		<div class="_content"> | 		<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> | ||||||
| 			<MkSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</MkSwitch> | 		<template #caption> | ||||||
| 		</div> | 			<i18n-t keypath="i18nInfo" tag="span"> | ||||||
| 		<div class="_content"> | 				<template #link> | ||||||
| 			<MkRadios v-model="chatOpenBehavior"> | 					<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink> | ||||||
| 				<template #desc>{{ $t('chatOpenBehavior') }}</template> | 				</template> | ||||||
| 				<option value="page">{{ $t('showInPage') }}</option> | 			</i18n-t> | ||||||
| 				<option value="window">{{ $t('openInWindow') }}</option> | 		</template> | ||||||
| 				<option value="popout">{{ $t('popout') }}</option> | 	</FormSelect> | ||||||
| 			</MkRadios> |  | ||||||
| 		</div> |  | ||||||
| 	</section> |  | ||||||
| 
 | 
 | ||||||
| 	<section class="_card _vMargin"> | 	<FormGroup> | ||||||
| 		<div class="_title"><Fa :icon="faCog"/> {{ $t('appearance') }}</div> | 		<template #label>{{ $t('behavior') }}</template> | ||||||
| 		<div class="_content"> | 		<FormSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</FormSwitch> | ||||||
| 			<MkSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</MkSwitch> | 		<FormSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</FormSwitch> | ||||||
| 			<MkSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</MkSwitch> | 		<FormSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</FormSwitch> | ||||||
| 			<MkSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</MkSwitch> | 	</FormGroup> | ||||||
| 			<MkSwitch v-model:value="useOsNativeEmojis"> |  | ||||||
| 				{{ $t('useOsNativeEmojis') }} |  | ||||||
| 				<template #desc><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template> |  | ||||||
| 			</MkSwitch> |  | ||||||
| 			<MkRadios v-model="fontSize"> |  | ||||||
| 				<template #desc>{{ $t('fontSize') }}</template> |  | ||||||
| 				<option value="small"><span style="font-size: 14px;">Aa</span></option> |  | ||||||
| 				<option :value="null"><span style="font-size: 16px;">Aa</span></option> |  | ||||||
| 				<option value="large"><span style="font-size: 18px;">Aa</span></option> |  | ||||||
| 				<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option> |  | ||||||
| 			</MkRadios> |  | ||||||
| 			<MkRadios v-model="instanceTicker"> |  | ||||||
| 				<template #desc>{{ $t('instanceTicker') }}</template> |  | ||||||
| 				<option value="none">{{ $t('_instanceTicker.none') }}</option> |  | ||||||
| 				<option value="remote">{{ $t('_instanceTicker.remote') }}</option> |  | ||||||
| 				<option value="always">{{ $t('_instanceTicker.always') }}</option> |  | ||||||
| 			</MkRadios> |  | ||||||
| 		</div> |  | ||||||
| 	</section> |  | ||||||
| 
 | 
 | ||||||
| 	<section class="_card _vMargin"> | 	<FormSelect v-model:value="serverDisconnectedBehavior"> | ||||||
| 		<div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</div> | 		<template #label>{{ $t('whenServerDisconnected') }}</template> | ||||||
| 		<div class="_content"> | 		<option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option> | ||||||
| 			<div>{{ $t('defaultNavigationBehaviour') }}</div> | 		<option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option> | ||||||
| 			<MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch> | 		<option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option> | ||||||
| 		</div> | 	</FormSelect> | ||||||
| 		<div class="_content"> |  | ||||||
| 			<MkSwitch v-model:value="deckAlwaysShowMainColumn"> |  | ||||||
| 				{{ $t('_deck.alwaysShowMainColumn') }} |  | ||||||
| 			</MkSwitch> |  | ||||||
| 		</div> |  | ||||||
| 		<div class="_content"> |  | ||||||
| 			<div>{{ $t('_deck.columnAlign') }}</div> |  | ||||||
| 			<MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio> |  | ||||||
| 			<MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio> |  | ||||||
| 		</div> |  | ||||||
| 	</section> |  | ||||||
| 
 | 
 | ||||||
| 	<MkButton @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</MkButton> | 	<FormGroup> | ||||||
| </div> | 		<template #label>{{ $t('appearance') }}</template> | ||||||
|  | 		<FormSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</FormSwitch> | ||||||
|  | 		<FormSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</FormSwitch> | ||||||
|  | 		<FormSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</FormSwitch> | ||||||
|  | 		<FormSwitch v-model:value="useOsNativeEmojis">{{ $t('useOsNativeEmojis') }} | ||||||
|  | 			<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div> | ||||||
|  | 		</FormSwitch> | ||||||
|  | 		<FormSwitch v-model:value="loadRawImages">{{ $t('loadRawImages') }}</FormSwitch> | ||||||
|  | 		<FormSwitch v-model:value="disableShowingAnimatedImages">{{ $t('disableShowingAnimatedImages') }}</FormSwitch> | ||||||
|  | 	</FormGroup> | ||||||
|  | 
 | ||||||
|  | 	<FormRadios v-model="fontSize"> | ||||||
|  | 		<template #desc>{{ $t('fontSize') }}</template> | ||||||
|  | 		<option value="small"><span style="font-size: 14px;">Aa</span></option> | ||||||
|  | 		<option :value="null"><span style="font-size: 16px;">Aa</span></option> | ||||||
|  | 		<option value="large"><span style="font-size: 18px;">Aa</span></option> | ||||||
|  | 		<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option> | ||||||
|  | 	</FormRadios> | ||||||
|  | 
 | ||||||
|  | 	<FormSelect v-model:value="instanceTicker"> | ||||||
|  | 		<template #label>{{ $t('instanceTicker') }}</template> | ||||||
|  | 		<option value="none">{{ $t('_instanceTicker.none') }}</option> | ||||||
|  | 		<option value="remote">{{ $t('_instanceTicker.remote') }}</option> | ||||||
|  | 		<option value="always">{{ $t('_instanceTicker.always') }}</option> | ||||||
|  | 	</FormSelect> | ||||||
|  | 
 | ||||||
|  | 	<FormSelect v-model:value="nsfw"> | ||||||
|  | 		<template #label>{{ $t('nsfw') }}</template> | ||||||
|  | 		<option value="respect">{{ $t('_nsfw.respect') }}</option> | ||||||
|  | 		<option value="ignore">{{ $t('_nsfw.ignore') }}</option> | ||||||
|  | 		<option value="force">{{ $t('_nsfw.force') }}</option> | ||||||
|  | 	</FormSelect> | ||||||
|  | 
 | ||||||
|  | 	<FormGroup> | ||||||
|  | 		<template #label>{{ $t('defaultNavigationBehaviour') }}</template> | ||||||
|  | 		<FormSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</FormSwitch> | ||||||
|  | 	</FormGroup> | ||||||
|  | 
 | ||||||
|  | 	<FormSelect v-model:value="chatOpenBehavior"> | ||||||
|  | 		<template #label>{{ $t('chatOpenBehavior') }}</template> | ||||||
|  | 		<option value="page">{{ $t('showInPage') }}</option> | ||||||
|  | 		<option value="window">{{ $t('openInWindow') }}</option> | ||||||
|  | 		<option value="popout">{{ $t('popout') }}</option> | ||||||
|  | 	</FormSelect> | ||||||
|  | 
 | ||||||
|  | 	<FormLink to="/settings/deck">{{ $t('deck') }}</FormLink> | ||||||
|  | 
 | ||||||
|  | 	<FormButton @click="cacheClear()" danger>{{ $t('cacheClear') }}</FormButton> | ||||||
|  | </FormBase> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faImage, faCog, faColumns, faCogs } from '@fortawesome/free-solid-svg-icons'; | import { faImage, faCog, faColumns, faCogs } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import MkSwitch from '@/components/ui/switch.vue'; | import FormSelect from '@/components/form/select.vue'; | ||||||
| import MkSelect from '@/components/ui/select.vue'; | import FormRadios from '@/components/form/radios.vue'; | ||||||
| import MkRadio from '@/components/ui/radio.vue'; | import FormBase from '@/components/form/base.vue'; | ||||||
| import MkRadios from '@/components/ui/radios.vue'; | import FormGroup from '@/components/form/group.vue'; | ||||||
| import MkRange from '@/components/ui/range.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
|  | import FormButton from '@/components/form/button.vue'; | ||||||
|  | import MkLink from '@/components/link.vue'; | ||||||
| import { langs } from '@/config'; | import { langs } from '@/config'; | ||||||
| import { clientDb, set } from '@/db'; | import { clientDb, set } from '@/db'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		MkLink, | ||||||
| 		MkSwitch, | 		FormSwitch, | ||||||
| 		MkSelect, | 		FormSelect, | ||||||
| 		MkRadio, | 		FormRadios, | ||||||
| 		MkRadios, | 		FormBase, | ||||||
| 		MkRange, | 		FormGroup, | ||||||
|  | 		FormLink, | ||||||
|  | 		FormButton, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['info'], | 	emits: ['info'], | ||||||
|  | @ -167,11 +168,6 @@ export default defineComponent({ | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'defaultSideView', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'defaultSideView', value }); } | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		deckNavWindow: { |  | ||||||
| 			get() { return this.$store.state.device.deckNavWindow; }, |  | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); } |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		chatOpenBehavior: { | 		chatOpenBehavior: { | ||||||
| 			get() { return this.$store.state.device.chatOpenBehavior; }, | 			get() { return this.$store.state.device.chatOpenBehavior; }, | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); } | ||||||
|  | @ -182,20 +178,25 @@ export default defineComponent({ | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); } | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  | 		loadRawImages: { | ||||||
|  | 			get() { return this.$store.state.device.loadRawImages; }, | ||||||
|  | 			set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); } | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		disableShowingAnimatedImages: { | ||||||
|  | 			get() { return this.$store.state.device.disableShowingAnimatedImages; }, | ||||||
|  | 			set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); } | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		nsfw: { | ||||||
|  | 			get() { return this.$store.state.device.nsfw; }, | ||||||
|  | 			set(value) { this.$store.commit('device/set', { key: 'nsfw', value }); } | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
| 		enableInfiniteScroll: { | 		enableInfiniteScroll: { | ||||||
| 			get() { return this.$store.state.device.enableInfiniteScroll; }, | 			get() { return this.$store.state.device.enableInfiniteScroll; }, | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); } | ||||||
| 		}, | 		}, | ||||||
| 
 |  | ||||||
| 		deckAlwaysShowMainColumn: { |  | ||||||
| 			get() { return this.$store.state.device.deckAlwaysShowMainColumn; }, |  | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); } |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		deckColumnAlign: { |  | ||||||
| 			get() { return this.$store.state.device.deckColumnAlign; }, |  | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); } |  | ||||||
| 		}, |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	watch: { | 	watch: { | ||||||
|  |  | ||||||
|  | @ -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"> | 		{{ $t('showFeaturedNotesInTimeline') }} | ||||||
| 			<div class="_content"> | 	</FormSwitch> | ||||||
| 				<MkSwitch v-model:value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote"> | 
 | ||||||
| 					{{ $t('showFeaturedNotesInTimeline') }} | 	<FormLink to="/settings/account-info">{{ $t('accountInfo') }}</FormLink> | ||||||
| 				</MkSwitch> | 
 | ||||||
| 			</div> | 	<FormGroup> | ||||||
| 		</div> | 		<FormSwitch v-model:value="debug" @update:value="changeDebug"> | ||||||
| 	</div> |  | ||||||
| 	<div class="_section"> |  | ||||||
| 		<MkSwitch v-model:value="debug" @update:value="changeDebug"> |  | ||||||
| 			DEBUG MODE | 			DEBUG MODE | ||||||
| 		</MkSwitch> | 		</FormSwitch> | ||||||
| 		<div v-if="debug"> | 		<template v-if="debug"> | ||||||
| 			<MkA to="/settings/regedit">RegEdit</MkA> | 			<FormLink to="/settings/regedit">RegEdit</FormLink> | ||||||
| 			<MkButton @click="taskmanager">Task Manager</MkButton> | 			<FormButton @click="taskmanager">Task Manager</FormButton> | ||||||
| 		</div> | 		</template> | ||||||
| 	</div> | 	</FormGroup> | ||||||
| </div> | </FormBase> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineAsyncComponent, defineComponent } from 'vue'; | import { defineAsyncComponent, defineComponent } from 'vue'; | ||||||
| import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'; | import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkSelect from '@/components/ui/select.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import MkSwitch from '@/components/ui/switch.vue'; | import FormSelect from '@/components/form/select.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
|  | import FormBase from '@/components/form/base.vue'; | ||||||
|  | import FormGroup from '@/components/form/group.vue'; | ||||||
|  | import FormButton from '@/components/form/button.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { debug } from '@/config'; | import { debug } from '@/config'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkSelect, | 		FormBase, | ||||||
| 		MkSwitch, | 		FormSelect, | ||||||
| 		MkButton, | 		FormSwitch, | ||||||
|  | 		FormButton, | ||||||
|  | 		FormLink, | ||||||
|  | 		FormGroup, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['info'], | 	emits: ['info'], | ||||||
|  |  | ||||||
|  | @ -1,36 +1,43 @@ | ||||||
| <template> | <template> | ||||||
| <div class="_section"> | <FormBase> | ||||||
| 	<div class="_card"> | 	<FormGroup> | ||||||
| 		<div class="_content"> | 		<FormSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</FormSwitch> | ||||||
| 			<MkSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</MkSwitch> | 		<FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</FormSwitch> | ||||||
| 			<MkSwitch v-model:value="autoAcceptFollowed" v-if="isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</MkSwitch> | 		<template #caption>{{ $t('lockedAccountInfo') }}</template> | ||||||
| 		</div> | 	</FormGroup> | ||||||
| 		<div class="_content"> | 	<FormSwitch v-model:value="noCrawle" @update:value="save()"> | ||||||
| 			<MkSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</MkSwitch> | 		{{ $t('noCrawle') }} | ||||||
| 			<MkSelect v-model:value="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility"> | 		<template #desc>{{ $t('noCrawleDescription') }}</template> | ||||||
| 				<template #label>{{ $t('defaultNoteVisibility') }}</template> | 	</FormSwitch> | ||||||
| 				<option value="public">{{ $t('_visibility.public') }}</option> | 	<FormSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</FormSwitch> | ||||||
| 				<option value="home">{{ $t('_visibility.home') }}</option> | 	<FormGroup v-if="!rememberNoteVisibility"> | ||||||
| 				<option value="followers">{{ $t('_visibility.followers') }}</option> | 		<template #label>{{ $t('defaultNoteVisibility') }}</template> | ||||||
| 				<option value="specified">{{ $t('_visibility.specified') }}</option> | 		<FormSelect v-model:value="defaultNoteVisibility"> | ||||||
| 			</MkSelect> | 			<option value="public">{{ $t('_visibility.public') }}</option> | ||||||
| 			<MkSwitch v-model:value="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</MkSwitch> | 			<option value="home">{{ $t('_visibility.home') }}</option> | ||||||
| 		</div> | 			<option value="followers">{{ $t('_visibility.followers') }}</option> | ||||||
| 	</div> | 			<option value="specified">{{ $t('_visibility.specified') }}</option> | ||||||
| </div> | 		</FormSelect> | ||||||
|  | 		<FormSwitch v-model:value="defaultNoteLocalOnly">{{ $t('_visibility.localOnly') }}</FormSwitch> | ||||||
|  | 	</FormGroup> | ||||||
|  | </FormBase> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faLockOpen } from '@fortawesome/free-solid-svg-icons'; | import { faLockOpen } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkSelect from '@/components/ui/select.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import MkSwitch from '@/components/ui/switch.vue'; | import FormSelect from '@/components/form/select.vue'; | ||||||
|  | import FormBase from '@/components/form/base.vue'; | ||||||
|  | import FormGroup from '@/components/form/group.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkSelect, | 		FormBase, | ||||||
| 		MkSwitch, | 		FormSelect, | ||||||
|  | 		FormGroup, | ||||||
|  | 		FormSwitch, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['info'], | 	emits: ['info'], | ||||||
|  | @ -43,6 +50,7 @@ export default defineComponent({ | ||||||
| 			}, | 			}, | ||||||
| 			isLocked: false, | 			isLocked: false, | ||||||
| 			autoAcceptFollowed: false, | 			autoAcceptFollowed: false, | ||||||
|  | 			noCrawle: false, | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -66,6 +74,7 @@ export default defineComponent({ | ||||||
| 	created() { | 	created() { | ||||||
| 		this.isLocked = this.$store.state.i.isLocked; | 		this.isLocked = this.$store.state.i.isLocked; | ||||||
| 		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed; | 		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed; | ||||||
|  | 		this.noCrawle = this.$store.state.i.noCrawle; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
|  | @ -77,6 +86,7 @@ export default defineComponent({ | ||||||
| 			os.api('i/update', { | 			os.api('i/update', { | ||||||
| 				isLocked: !!this.isLocked, | 				isLocked: !!this.isLocked, | ||||||
| 				autoAcceptFollowed: !!this.autoAcceptFollowed, | 				autoAcceptFollowed: !!this.autoAcceptFollowed, | ||||||
|  | 				noCrawle: !!this.noCrawle, | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1,79 +1,67 @@ | ||||||
| <template> | <template> | ||||||
| <div class="_section"> | <FormBase class="llvierxe"> | ||||||
| 	<div class="llvierxe _card"> | 	<div class="header _formItem" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner"> | ||||||
| 		<div class="_title"><Fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div> | 		<MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/> | ||||||
| 		<div class="_content"> |  | ||||||
| 			<div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner"> |  | ||||||
| 				<MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/> |  | ||||||
| 			</div> |  | ||||||
| 		 |  | ||||||
| 			<MkInput v-model:value="name" :max="30"> |  | ||||||
| 				<span>{{ $t('_profile.name') }}</span> |  | ||||||
| 			</MkInput> |  | ||||||
| 
 |  | ||||||
| 			<MkTextarea v-model:value="description" :max="500"> |  | ||||||
| 				<span>{{ $t('_profile.description') }}</span> |  | ||||||
| 				<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template> |  | ||||||
| 			</MkTextarea> |  | ||||||
| 
 |  | ||||||
| 			<MkInput v-model:value="location"> |  | ||||||
| 				<span>{{ $t('location') }}</span> |  | ||||||
| 				<template #prefix><Fa :icon="faMapMarkerAlt"/></template> |  | ||||||
| 			</MkInput> |  | ||||||
| 
 |  | ||||||
| 			<MkInput v-model:value="birthday" type="date"> |  | ||||||
| 				<template #title>{{ $t('birthday') }}</template> |  | ||||||
| 				<template #prefix><Fa :icon="faBirthdayCake"/></template> |  | ||||||
| 			</MkInput> |  | ||||||
| 
 |  | ||||||
| 			<details class="fields"> |  | ||||||
| 				<summary>{{ $t('_profile.metadata') }}</summary> |  | ||||||
| 				<div class="row"> |  | ||||||
| 					<MkInput v-model:value="fieldName0">{{ $t('_profile.metadataLabel') }}</MkInput> |  | ||||||
| 					<MkInput v-model:value="fieldValue0">{{ $t('_profile.metadataContent') }}</MkInput> |  | ||||||
| 				</div> |  | ||||||
| 				<div class="row"> |  | ||||||
| 					<MkInput v-model:value="fieldName1">{{ $t('_profile.metadataLabel') }}</MkInput> |  | ||||||
| 					<MkInput v-model:value="fieldValue1">{{ $t('_profile.metadataContent') }}</MkInput> |  | ||||||
| 				</div> |  | ||||||
| 				<div class="row"> |  | ||||||
| 					<MkInput v-model:value="fieldName2">{{ $t('_profile.metadataLabel') }}</MkInput> |  | ||||||
| 					<MkInput v-model:value="fieldValue2">{{ $t('_profile.metadataContent') }}</MkInput> |  | ||||||
| 				</div> |  | ||||||
| 				<div class="row"> |  | ||||||
| 					<MkInput v-model:value="fieldName3">{{ $t('_profile.metadataLabel') }}</MkInput> |  | ||||||
| 					<MkInput v-model:value="fieldValue3">{{ $t('_profile.metadataContent') }}</MkInput> |  | ||||||
| 				</div> |  | ||||||
| 			</details> |  | ||||||
| 
 |  | ||||||
| 			<MkSwitch v-model:value="isBot">{{ $t('flagAsBot') }}</MkSwitch> |  | ||||||
| 			<MkSwitch v-model:value="isCat">{{ $t('flagAsCat') }}</MkSwitch> |  | ||||||
| 		</div> |  | ||||||
| 		<div class="_footer"> |  | ||||||
| 			<MkButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton> |  | ||||||
| 		</div> |  | ||||||
| 	</div> | 	</div> | ||||||
| </div> | 
 | ||||||
|  | 	<FormInput v-model:value="name" :max="30"> | ||||||
|  | 		<span>{{ $t('_profile.name') }}</span> | ||||||
|  | 	</FormInput> | ||||||
|  | 
 | ||||||
|  | 	<FormTextarea v-model:value="description" :max="500"> | ||||||
|  | 		<span>{{ $t('_profile.description') }}</span> | ||||||
|  | 		<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template> | ||||||
|  | 	</FormTextarea> | ||||||
|  | 
 | ||||||
|  | 	<FormInput v-model:value="location"> | ||||||
|  | 		<span>{{ $t('location') }}</span> | ||||||
|  | 		<template #prefix><Fa :icon="faMapMarkerAlt"/></template> | ||||||
|  | 	</FormInput> | ||||||
|  | 
 | ||||||
|  | 	<FormInput v-model:value="birthday" type="date"> | ||||||
|  | 		<span>{{ $t('birthday') }}</span> | ||||||
|  | 		<template #prefix><Fa :icon="faBirthdayCake"/></template> | ||||||
|  | 	</FormInput> | ||||||
|  | 
 | ||||||
|  | 	<FormGroup> | ||||||
|  | 		<FormButton @click="editMetadata" primary>{{ $t('_profile.metadataEdit') }}</FormButton> | ||||||
|  | 		<template #caption>{{ $t('_profile.metadataDescription') }}</template> | ||||||
|  | 	</FormGroup> | ||||||
|  | 
 | ||||||
|  | 	<FormSwitch v-model:value="isCat">{{ $t('flagAsCat') }}<template #desc>{{ $t('flagAsCatDescription') }}</template></FormSwitch> | ||||||
|  | 
 | ||||||
|  | 	<FormSwitch v-model:value="isBot">{{ $t('flagAsBot') }}<template #desc>{{ $t('flagAsBotDescription') }}</template></FormSwitch> | ||||||
|  | 
 | ||||||
|  | 	<FormSwitch v-model:value="alwaysMarkNsfw">{{ $t('alwaysMarkSensitive') }}</FormSwitch> | ||||||
|  | 
 | ||||||
|  | 	<FormButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton> | ||||||
|  | </FormBase> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons'; | import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { faSave } from '@fortawesome/free-regular-svg-icons'; | import { faSave } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import FormButton from '@/components/form/button.vue'; | ||||||
| import MkInput from '@/components/ui/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import MkTextarea from '@/components/ui/textarea.vue'; | import FormTextarea from '@/components/form/textarea.vue'; | ||||||
| import MkSwitch from '@/components/ui/switch.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
|  | import FormTuple from '@/components/form/tuple.vue'; | ||||||
|  | import FormBase from '@/components/form/base.vue'; | ||||||
|  | import FormGroup from '@/components/form/group.vue'; | ||||||
| import { host } from '@/config'; | import { host } from '@/config'; | ||||||
| import { selectFile } from '@/scripts/select-file'; | import { selectFile } from '@/scripts/select-file'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		FormButton, | ||||||
| 		MkInput, | 		FormInput, | ||||||
| 		MkTextarea, | 		FormTextarea, | ||||||
| 		MkSwitch, | 		FormSwitch, | ||||||
|  | 		FormTuple, | ||||||
|  | 		FormBase, | ||||||
|  | 		FormGroup, | ||||||
| 	}, | 	}, | ||||||
| 	 | 	 | ||||||
| 	emits: ['info'], | 	emits: ['info'], | ||||||
|  | @ -101,6 +89,7 @@ export default defineComponent({ | ||||||
| 			bannerId: null, | 			bannerId: null, | ||||||
| 			isBot: false, | 			isBot: false, | ||||||
| 			isCat: false, | 			isCat: false, | ||||||
|  | 			alwaysMarkNsfw: false, | ||||||
| 			saving: false, | 			saving: false, | ||||||
| 			faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake | 			faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake | ||||||
| 		} | 		} | ||||||
|  | @ -115,6 +104,7 @@ export default defineComponent({ | ||||||
| 		this.bannerId = this.$store.state.i.bannerId; | 		this.bannerId = this.$store.state.i.bannerId; | ||||||
| 		this.isBot = this.$store.state.i.isBot; | 		this.isBot = this.$store.state.i.isBot; | ||||||
| 		this.isCat = this.$store.state.i.isCat; | 		this.isCat = this.$store.state.i.isCat; | ||||||
|  | 		this.alwaysMarkNsfw = this.$store.state.i.alwaysMarkNsfw; | ||||||
| 
 | 
 | ||||||
| 		this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null; | 		this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null; | ||||||
| 		this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null; | 		this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null; | ||||||
|  | @ -147,7 +137,60 @@ export default defineComponent({ | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		save(notify) { | 		async editMetadata() { | ||||||
|  | 			const { canceled, result } = await os.form(this.$t('_profile.metadata'), { | ||||||
|  | 				fieldName0: { | ||||||
|  | 					type: 'string', | ||||||
|  | 					label: this.$t('_profile.metadataLabel') + ' 1', | ||||||
|  | 					default: this.fieldName0, | ||||||
|  | 				}, | ||||||
|  | 				fieldValue0: { | ||||||
|  | 					type: 'string', | ||||||
|  | 					label: this.$t('_profile.metadataContent') + ' 1', | ||||||
|  | 					default: this.fieldValue0, | ||||||
|  | 				}, | ||||||
|  | 				fieldName1: { | ||||||
|  | 					type: 'string', | ||||||
|  | 					label: this.$t('_profile.metadataLabel') + ' 2', | ||||||
|  | 					default: this.fieldName1, | ||||||
|  | 				}, | ||||||
|  | 				fieldValue1: { | ||||||
|  | 					type: 'string', | ||||||
|  | 					label: this.$t('_profile.metadataContent') + ' 2', | ||||||
|  | 					default: this.fieldValue1, | ||||||
|  | 				}, | ||||||
|  | 				fieldName2: { | ||||||
|  | 					type: 'string', | ||||||
|  | 					label: this.$t('_profile.metadataLabel') + ' 3', | ||||||
|  | 					default: this.fieldName2, | ||||||
|  | 				}, | ||||||
|  | 				fieldValue2: { | ||||||
|  | 					type: 'string', | ||||||
|  | 					label: this.$t('_profile.metadataContent') + ' 3', | ||||||
|  | 					default: this.fieldValue2, | ||||||
|  | 				}, | ||||||
|  | 				fieldName3: { | ||||||
|  | 					type: 'string', | ||||||
|  | 					label: this.$t('_profile.metadataLabel') + ' 4', | ||||||
|  | 					default: this.fieldName3, | ||||||
|  | 				}, | ||||||
|  | 				fieldValue3: { | ||||||
|  | 					type: 'string', | ||||||
|  | 					label: this.$t('_profile.metadataContent') + ' 4', | ||||||
|  | 					default: this.fieldValue3, | ||||||
|  | 				}, | ||||||
|  | 			}); | ||||||
|  | 			if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 			this.fieldName0 = result.fieldName0; | ||||||
|  | 			this.fieldValue0 = result.fieldValue0; | ||||||
|  | 			this.fieldName1 = result.fieldName1; | ||||||
|  | 			this.fieldValue1 = result.fieldValue1; | ||||||
|  | 			this.fieldName2 = result.fieldName2; | ||||||
|  | 			this.fieldValue2 = result.fieldValue2; | ||||||
|  | 			this.fieldName3 = result.fieldName3; | ||||||
|  | 			this.fieldValue3 = result.fieldValue3; | ||||||
|  | 
 | ||||||
| 			const fields = [ | 			const fields = [ | ||||||
| 				{ name: this.fieldName0, value: this.fieldValue0 }, | 				{ name: this.fieldName0, value: this.fieldValue0 }, | ||||||
| 				{ name: this.fieldName1, value: this.fieldValue1 }, | 				{ name: this.fieldName1, value: this.fieldValue1 }, | ||||||
|  | @ -155,6 +198,19 @@ export default defineComponent({ | ||||||
| 				{ name: this.fieldName3, value: this.fieldValue3 }, | 				{ name: this.fieldName3, value: this.fieldValue3 }, | ||||||
| 			]; | 			]; | ||||||
| 
 | 
 | ||||||
|  | 			os.api('i/update', { | ||||||
|  | 				fields, | ||||||
|  | 			}).then(i => { | ||||||
|  | 				os.success(); | ||||||
|  | 			}).catch(err => { | ||||||
|  | 				os.dialog({ | ||||||
|  | 					type: 'error', | ||||||
|  | 					text: err.id | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		save(notify) { | ||||||
| 			this.saving = true; | 			this.saving = true; | ||||||
| 
 | 
 | ||||||
| 			os.api('i/update', { | 			os.api('i/update', { | ||||||
|  | @ -162,9 +218,9 @@ export default defineComponent({ | ||||||
| 				description: this.description || null, | 				description: this.description || null, | ||||||
| 				location: this.location || null, | 				location: this.location || null, | ||||||
| 				birthday: this.birthday || null, | 				birthday: this.birthday || null, | ||||||
| 				fields, |  | ||||||
| 				isBot: !!this.isBot, | 				isBot: !!this.isBot, | ||||||
| 				isCat: !!this.isCat, | 				isCat: !!this.isCat, | ||||||
|  | 				alwaysMarkNsfw: !!this.alwaysMarkNsfw, | ||||||
| 			}).then(i => { | 			}).then(i => { | ||||||
| 				this.saving = false; | 				this.saving = false; | ||||||
| 				this.$store.state.i.avatarId = i.avatarId; | 				this.$store.state.i.avatarId = i.avatarId; | ||||||
|  | @ -189,41 +245,29 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .llvierxe { | .llvierxe { | ||||||
| 	> ._content { | 	> .header { | ||||||
| 		> .header { | 		position: relative; | ||||||
| 			position: relative; | 		height: 150px; | ||||||
| 			height: 150px; | 		overflow: hidden; | ||||||
| 			overflow: hidden; | 		background-size: cover; | ||||||
| 			background-size: cover; | 		background-position: center; | ||||||
| 			background-position: center; | 		border-radius: 5px; | ||||||
| 			border-radius: 5px; | 		border: solid 1px var(--divider); | ||||||
| 			border: solid 1px var(--divider); | 		box-sizing: border-box; | ||||||
| 			box-sizing: border-box; | 		cursor: pointer; | ||||||
|  | 
 | ||||||
|  | 		> .avatar { | ||||||
|  | 			position: absolute; | ||||||
|  | 			top: 0; | ||||||
|  | 			bottom: 0; | ||||||
|  | 			left: 0; | ||||||
|  | 			right: 0; | ||||||
|  | 			display: block; | ||||||
|  | 			width: 72px; | ||||||
|  | 			height: 72px; | ||||||
|  | 			margin: auto; | ||||||
| 			cursor: pointer; | 			cursor: pointer; | ||||||
| 
 | 			box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5); | ||||||
| 			> .avatar { |  | ||||||
| 				position: absolute; |  | ||||||
| 				top: 0; |  | ||||||
| 				bottom: 0; |  | ||||||
| 				left: 0; |  | ||||||
| 				right: 0; |  | ||||||
| 				display: block; |  | ||||||
| 				width: 72px; |  | ||||||
| 				height: 72px; |  | ||||||
| 				margin: auto; |  | ||||||
| 				cursor: pointer; |  | ||||||
| 				box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		> .fields { |  | ||||||
| 			> .row { |  | ||||||
| 				> * { |  | ||||||
| 					display: inline-block; |  | ||||||
| 					width: 50%; |  | ||||||
| 					margin-bottom: 0; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,9 +1,8 @@ | ||||||
| <template> | <template> | ||||||
| <div class="_section"> | <FormBase> | ||||||
| 	<div class="_card"> | 	<div class="_formItem"> | ||||||
| 		<div class="_title"><Fa :icon="faLaugh"/> {{ $t('reaction') }}</div> | 		<div class="_formLabel">{{ $t('reactionSettingDescription') }}</div> | ||||||
| 		<div class="_content"> | 		<div class="_formPanel"> | ||||||
| 			<div class="_caption" style="padding: 0 8px 8px 8px;">{{ $t('reactionSettingDescription') }}</div> |  | ||||||
| 			<XDraggable class="zoaiodol" :list="reactions" animation="150" delay="100" delay-on-touch-only="true"> | 			<XDraggable class="zoaiodol" :list="reactions" animation="150" delay="100" delay-on-touch-only="true"> | ||||||
| 				<button class="_button item" v-for="reaction in reactions" :key="reaction" @click="remove(reaction, $event)"> | 				<button class="_button item" v-for="reaction in reactions" :key="reaction" @click="remove(reaction, $event)"> | ||||||
| 					<MkEmoji :emoji="reaction" :normal="true"/> | 					<MkEmoji :emoji="reaction" :normal="true"/> | ||||||
|  | @ -12,26 +11,25 @@ | ||||||
| 					<button>a</button> | 					<button>a</button> | ||||||
| 				</template> | 				</template> | ||||||
| 			</XDraggable> | 			</XDraggable> | ||||||
| 			<div class="_caption" style="padding: 8px;">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div> |  | ||||||
| 			<MkRadios v-model="reactionPickerWidth"> |  | ||||||
| 				<template #desc>{{ $t('width') }}</template> |  | ||||||
| 				<option :value="1">{{ $t('small') }}</option> |  | ||||||
| 				<option :value="2">{{ $t('medium') }}</option> |  | ||||||
| 				<option :value="3">{{ $t('large') }}</option> |  | ||||||
| 			</MkRadios> |  | ||||||
| 			<MkRadios v-model="reactionPickerHeight"> |  | ||||||
| 				<template #desc>{{ $t('height') }}</template> |  | ||||||
| 				<option :value="1">{{ $t('small') }}</option> |  | ||||||
| 				<option :value="2">{{ $t('medium') }}</option> |  | ||||||
| 				<option :value="3">{{ $t('large') }}</option> |  | ||||||
| 			</MkRadios> |  | ||||||
| 		</div> |  | ||||||
| 		<div class="_footer"> |  | ||||||
| 			<MkButton inline @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton> |  | ||||||
| 			<MkButton inline @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</MkButton> |  | ||||||
| 		</div> | 		</div> | ||||||
|  | 		<div class="_formCaption">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | 
 | ||||||
|  | 	<FormRadios v-model="reactionPickerWidth"> | ||||||
|  | 		<template #desc>{{ $t('width') }}</template> | ||||||
|  | 		<option :value="1">{{ $t('small') }}</option> | ||||||
|  | 		<option :value="2">{{ $t('medium') }}</option> | ||||||
|  | 		<option :value="3">{{ $t('large') }}</option> | ||||||
|  | 	</FormRadios> | ||||||
|  | 	<FormRadios v-model="reactionPickerHeight"> | ||||||
|  | 		<template #desc>{{ $t('height') }}</template> | ||||||
|  | 		<option :value="1">{{ $t('small') }}</option> | ||||||
|  | 		<option :value="2">{{ $t('medium') }}</option> | ||||||
|  | 		<option :value="3">{{ $t('large') }}</option> | ||||||
|  | 	</FormRadios> | ||||||
|  | 	<FormButton @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</FormButton> | ||||||
|  | 	<FormButton danger @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</FormButton> | ||||||
|  | </FormBase> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|  | @ -39,20 +37,19 @@ import { defineComponent } from 'vue'; | ||||||
| import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons'; | import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons'; | ||||||
| import { faUndo } from '@fortawesome/free-solid-svg-icons'; | import { faUndo } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { VueDraggableNext } from 'vue-draggable-next'; | import { VueDraggableNext } from 'vue-draggable-next'; | ||||||
| import MkInput from '@/components/ui/input.vue'; | import FormInput from '@/components/form/input.vue'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import FormRadios from '@/components/form/radios.vue'; | ||||||
| import MkSwitch from '@/components/ui/switch.vue'; | import FormBase from '@/components/form/base.vue'; | ||||||
| import MkRadios from '@/components/ui/radios.vue'; | import FormButton from '@/components/form/button.vue'; | ||||||
| import { emojiRegexWithCustom } from '../../../misc/emoji-regex'; |  | ||||||
| import { defaultSettings } from '@/store'; | import { defaultSettings } from '@/store'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkInput, | 		FormInput, | ||||||
| 		MkButton, | 		FormButton, | ||||||
| 		MkSwitch, | 		FormBase, | ||||||
| 		MkRadios, | 		FormRadios, | ||||||
| 		XDraggable: VueDraggableNext, | 		XDraggable: VueDraggableNext, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -62,7 +59,11 @@ export default defineComponent({ | ||||||
| 		return { | 		return { | ||||||
| 			INFO: { | 			INFO: { | ||||||
| 				title: this.$t('reaction'), | 				title: this.$t('reaction'), | ||||||
| 				icon: faLaugh | 				icon: faLaugh, | ||||||
|  | 				action: { | ||||||
|  | 					icon: faEye, | ||||||
|  | 					handler: this.preview | ||||||
|  | 				} | ||||||
| 			}, | 			}, | ||||||
| 			reactions: JSON.parse(JSON.stringify(this.$store.state.settings.reactions)), | 			reactions: JSON.parse(JSON.stringify(this.$store.state.settings.reactions)), | ||||||
| 			faLaugh, faSave, faEye, faUndo | 			faLaugh, faSave, faEye, faUndo | ||||||
|  | @ -144,8 +145,6 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .zoaiodol { | .zoaiodol { | ||||||
| 	border: solid 1px var(--divider); |  | ||||||
| 	border-radius: var(--radius); |  | ||||||
| 	padding: 16px; | 	padding: 16px; | ||||||
| 
 | 
 | ||||||
| 	> .item { | 	> .item { | ||||||
|  |  | ||||||
|  | @ -1,29 +1,45 @@ | ||||||
| <template> | <template> | ||||||
| <div> | <FormBase> | ||||||
| 	<div class="_section"> | 	<X2fa/> | ||||||
| 		<X2fa/> | 	<FormLink to="/settings/2fa"><template #icon><Fa :icon="faMobileAlt"/></template>{{ $t('twoStepAuthentication') }}</FormLink> | ||||||
| 	</div> | 	<FormButton primary @click="change()">{{ $t('changePassword') }}</FormButton> | ||||||
| 	<div class="_section"> | 	<FormPagination :pagination="pagination"> | ||||||
| 		<MkButton primary @click="change()" full>{{ $t('changePassword') }}</MkButton> | 		<template #label>{{ $t('signinHistory') }}</template> | ||||||
| 	</div> | 		<template #default="{items}"> | ||||||
| 	<div class="_section"> | 			<div class="_formPanel timnmucd" v-for="item in items" :key="item.id"> | ||||||
| 		<MkButton class="_vMargin" primary @click="regenerateToken" full><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</MkButton> | 				<header> | ||||||
| 		<div class="_caption _vMargin" style="padding: 0 6px;">{{ $t('regenerateLoginTokenDescription') }}</div> | 					<Fa class="icon succ" :icon="faCheck" v-if="item.success"/> | ||||||
| 	</div> | 					<Fa class="icon fail" :icon="faTimesCircle" v-else/> | ||||||
| </div> | 					<code class="ip _monospace">{{ item.ip }}</code> | ||||||
|  | 					<MkTime :time="item.createdAt" class="time"/> | ||||||
|  | 				</header> | ||||||
|  | 			</div> | ||||||
|  | 		</template> | ||||||
|  | 	</FormPagination> | ||||||
|  | 	<FormGroup> | ||||||
|  | 		<FormButton danger @click="regenerateToken"><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</FormButton> | ||||||
|  | 		<template #caption>{{ $t('regenerateLoginTokenDescription') }}</template> | ||||||
|  | 	</FormGroup> | ||||||
|  | </FormBase> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faLock, faSyncAlt } from '@fortawesome/free-solid-svg-icons'; | import { faCheck, faTimesCircle, faLock, faSyncAlt, faMobileAlt } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import FormBase from '@/components/form/base.vue'; | ||||||
| import X2fa from './security.2fa.vue'; | import FormLink from '@/components/form/link.vue'; | ||||||
|  | import FormGroup from '@/components/form/group.vue'; | ||||||
|  | import FormButton from '@/components/form/button.vue'; | ||||||
|  | import FormPagination from '@/components/form/pagination.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		FormBase, | ||||||
| 		X2fa, | 		FormLink, | ||||||
|  | 		FormButton, | ||||||
|  | 		FormPagination, | ||||||
|  | 		FormGroup, | ||||||
| 	}, | 	}, | ||||||
| 	 | 	 | ||||||
| 	emits: ['info'], | 	emits: ['info'], | ||||||
|  | @ -34,7 +50,11 @@ export default defineComponent({ | ||||||
| 				title: this.$t('security'), | 				title: this.$t('security'), | ||||||
| 				icon: faLock | 				icon: faLock | ||||||
| 			}, | 			}, | ||||||
| 			faLock, faSyncAlt | 			pagination: { | ||||||
|  | 				endpoint: 'i/signin-history', | ||||||
|  | 				limit: 5, | ||||||
|  | 			}, | ||||||
|  | 			faLock, faSyncAlt, faCheck, faTimesCircle, faMobileAlt, | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -98,3 +118,32 @@ export default defineComponent({ | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .timnmucd { | ||||||
|  | 	padding: 16px; | ||||||
|  | 
 | ||||||
|  | 	> header { | ||||||
|  | 		display: flex; | ||||||
|  | 		align-items: center; | ||||||
|  | 
 | ||||||
|  | 		> .icon { | ||||||
|  | 			width: 1em; | ||||||
|  | 			margin-right: 0.75em; | ||||||
|  | 
 | ||||||
|  | 			&.succ { | ||||||
|  | 				color: var(--success); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			&.fail { | ||||||
|  | 				color: var(--error); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .time { | ||||||
|  | 			margin-left: auto; | ||||||
|  | 			opacity: 0.7; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | </style> | ||||||
|  |  | ||||||
|  | @ -1,41 +1,41 @@ | ||||||
| <template> | <template> | ||||||
| <div class="_section"> | <FormBase> | ||||||
| 	<div class="_card"> | 	<FormTextarea v-model:value="items" tall> | ||||||
| 		<div class="_content"> | 		<span>{{ $t('sidebar') }}</span> | ||||||
| 			<MkTextarea v-model:value="items" tall> | 		<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template> | ||||||
| 				<span>{{ $t('sidebar') }}</span> | 	</FormTextarea> | ||||||
| 				<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template> | 
 | ||||||
| 			</MkTextarea> | 	<FormRadios v-model="sidebarDisplay"> | ||||||
| 		</div> | 		<template #desc>{{ $t('display') }}</template> | ||||||
| 		<div class="_content"> | 		<option value="full">{{ $t('_sidebar.full') }}</option> | ||||||
| 			<div>{{ $t('display') }}</div> | 		<option value="icon">{{ $t('_sidebar.icon') }}</option> | ||||||
| 			<MkRadio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</MkRadio> | 		<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> | ||||||
| 			<MkRadio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</MkRadio> | 	</FormRadios> | ||||||
| 			<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> | 
 | ||||||
| 		</div> | 	<FormButton @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton> | ||||||
| 		<div class="_footer"> | 	<FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton> | ||||||
| 			<MkButton inline @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton> | </FormBase> | ||||||
| 			<MkButton inline @click="reset()"><Fa :icon="faRedo"/> {{ $t('default') }}</MkButton> |  | ||||||
| 		</div> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons'; | import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import FormSwitch from '@/components/form/switch.vue'; | ||||||
| import MkTextarea from '@/components/ui/textarea.vue'; | import FormTextarea from '@/components/form/textarea.vue'; | ||||||
| import MkRadio from '@/components/ui/radio.vue'; | import FormRadios from '@/components/form/radios.vue'; | ||||||
|  | import FormBase from '@/components/form/base.vue'; | ||||||
|  | import FormGroup from '@/components/form/group.vue'; | ||||||
|  | import FormButton from '@/components/form/button.vue'; | ||||||
| import { defaultDeviceUserSettings } from '@/store'; | import { defaultDeviceUserSettings } from '@/store'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
| import { sidebarDef } from '@/sidebar'; | import { sidebarDef } from '@/sidebar'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		FormBase, | ||||||
| 		MkTextarea, | 		FormButton, | ||||||
| 		MkRadio, | 		FormTextarea, | ||||||
|  | 		FormRadios, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	emits: ['info'], | 	emits: ['info'], | ||||||
|  | @ -102,7 +102,3 @@ export default defineComponent({ | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 |  | ||||||
| <style lang="scss" scoped> |  | ||||||
| 
 |  | ||||||
| </style> |  | ||||||
|  |  | ||||||
|  | @ -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,292 +176,230 @@ export default defineComponent({ | ||||||
| 				this.wallpaper = file.url; | 				this.wallpaper = file.url; | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 |  | ||||||
| 		copyThemeCode() { |  | ||||||
| 			copyToClipboard(this.selectedThemeCode); |  | ||||||
| 			os.success(); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		parseThemeCode(code) { |  | ||||||
| 			let theme; |  | ||||||
| 
 |  | ||||||
| 			try { |  | ||||||
| 				theme = JSON5.parse(code); |  | ||||||
| 			} catch (e) { |  | ||||||
| 				os.dialog({ |  | ||||||
| 					type: 'error', |  | ||||||
| 					text: this.$t('_theme.invalid') |  | ||||||
| 				}); |  | ||||||
| 				return false; |  | ||||||
| 			} |  | ||||||
| 			if (!validateTheme(theme)) { |  | ||||||
| 				os.dialog({ |  | ||||||
| 					type: 'error', |  | ||||||
| 					text: this.$t('_theme.invalid') |  | ||||||
| 				}); |  | ||||||
| 				return false; |  | ||||||
| 			} |  | ||||||
| 			if (this.$store.state.device.themes.some(t => t.id === theme.id)) { |  | ||||||
| 				os.dialog({ |  | ||||||
| 					type: 'info', |  | ||||||
| 					text: this.$t('_theme.alreadyInstalled') |  | ||||||
| 				}); |  | ||||||
| 				return false; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			return theme; |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		preview(code) { |  | ||||||
| 			const theme = this.parseThemeCode(code); |  | ||||||
| 			if (theme) applyTheme(theme, false); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		install(code) { |  | ||||||
| 			const theme = this.parseThemeCode(code); |  | ||||||
| 			if (!theme) return; |  | ||||||
| 			const themes = this.$store.state.device.themes.concat(theme); |  | ||||||
| 			this.$store.commit('device/set', { |  | ||||||
| 				key: 'themes', value: themes |  | ||||||
| 			}); |  | ||||||
| 			os.dialog({ |  | ||||||
| 				type: 'success', |  | ||||||
| 				text: this.$t('_theme.installed', { name: theme.name }) |  | ||||||
| 			}); |  | ||||||
| 		}, |  | ||||||
| 
 |  | ||||||
| 		uninstall() { |  | ||||||
| 			const theme = this.selectedTheme; |  | ||||||
| 			const themes = this.$store.state.device.themes.filter(t => t.id != theme.id); |  | ||||||
| 			this.$store.commit('device/set', { |  | ||||||
| 				key: 'themes', value: themes |  | ||||||
| 			}); |  | ||||||
| 			os.success(); |  | ||||||
| 		}, |  | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .rfqxtzch { | .rfqxtzch { | ||||||
| 	> ._content { | 	padding: 16px; | ||||||
| 		> .darkMode { |  | ||||||
| 			position: relative; |  | ||||||
| 			padding: 32px 0; |  | ||||||
| 
 | 
 | ||||||
| 			&.disabled { | 	> .darkMode { | ||||||
| 				opacity: 0.7; | 		position: relative; | ||||||
|  | 		padding: 32px 0; | ||||||
| 
 | 
 | ||||||
| 				&, * { | 		&.disabled { | ||||||
| 					cursor: not-allowed !important; | 			opacity: 0.7; | ||||||
| 				} | 
 | ||||||
|  | 			&, * { | ||||||
|  | 				cursor: not-allowed !important; | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 			.toggleWrapper { | 		.toggleWrapper { | ||||||
|  | 			position: absolute; | ||||||
|  | 			top: 50%; | ||||||
|  | 			left: 50%; | ||||||
|  | 			overflow: hidden; | ||||||
|  | 			padding: 0 100px; | ||||||
|  | 			transform: translate3d(-50%, -50%, 0); | ||||||
|  | 
 | ||||||
|  | 			input { | ||||||
| 				position: absolute; | 				position: absolute; | ||||||
| 				top: 50%; | 				left: -99em; | ||||||
| 				left: 50%; | 			} | ||||||
| 				overflow: hidden; | 		} | ||||||
| 				padding: 0 100px; |  | ||||||
| 				transform: translate3d(-50%, -50%, 0); |  | ||||||
| 
 | 
 | ||||||
| 				input { | 		.toggle { | ||||||
| 					position: absolute; | 			cursor: pointer; | ||||||
| 					left: -99em; | 			display: inline-block; | ||||||
| 				} | 			position: relative; | ||||||
|  | 			width: 90px; | ||||||
|  | 			height: 50px; | ||||||
|  | 			background-color: #83D8FF; | ||||||
|  | 			border-radius: 90px - 6; | ||||||
|  | 			transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||||
|  | 
 | ||||||
|  | 			> .before, > .after { | ||||||
|  | 				position: absolute; | ||||||
|  | 				top: 15px; | ||||||
|  | 				font-size: 18px; | ||||||
|  | 				transition: color 1s ease; | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			.toggle { | 			> .before { | ||||||
| 				cursor: pointer; | 				left: -70px; | ||||||
| 				display: inline-block; | 				color: var(--accent); | ||||||
| 				position: relative; | 			} | ||||||
| 				width: 90px; |  | ||||||
| 				height: 50px; |  | ||||||
| 				background-color: #83D8FF; |  | ||||||
| 				border-radius: 90px - 6; |  | ||||||
| 				transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; |  | ||||||
| 
 | 
 | ||||||
| 				> .before, > .after { | 			> .after { | ||||||
| 					position: absolute; | 				right: -68px; | ||||||
| 					top: 15px; | 				color: var(--fg); | ||||||
| 					font-size: 18px; | 			} | ||||||
| 					transition: color 1s ease; | 		} | ||||||
| 				} | 
 | ||||||
|  | 		.toggle__handler { | ||||||
|  | 			display: inline-block; | ||||||
|  | 			position: relative; | ||||||
|  | 			z-index: 1; | ||||||
|  | 			top: 3px; | ||||||
|  | 			left: 3px; | ||||||
|  | 			width: 50px - 6; | ||||||
|  | 			height: 50px - 6; | ||||||
|  | 			background-color: #FFCF96; | ||||||
|  | 			border-radius: 50px; | ||||||
|  | 			box-shadow: 0 2px 6px rgba(0,0,0,.3); | ||||||
|  | 			transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; | ||||||
|  | 			transform:  rotate(-45deg); | ||||||
|  | 
 | ||||||
|  | 			.crater { | ||||||
|  | 				position: absolute; | ||||||
|  | 				background-color: #E8CDA5; | ||||||
|  | 				opacity: 0; | ||||||
|  | 				transition: opacity 200ms ease-in-out !important; | ||||||
|  | 				border-radius: 100%; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			.crater--1 { | ||||||
|  | 				top: 18px; | ||||||
|  | 				left: 10px; | ||||||
|  | 				width: 4px; | ||||||
|  | 				height: 4px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			.crater--2 { | ||||||
|  | 				top: 28px; | ||||||
|  | 				left: 22px; | ||||||
|  | 				width: 6px; | ||||||
|  | 				height: 6px; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			.crater--3 { | ||||||
|  | 				top: 10px; | ||||||
|  | 				left: 25px; | ||||||
|  | 				width: 8px; | ||||||
|  | 				height: 8px; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.star { | ||||||
|  | 			position: absolute; | ||||||
|  | 			background-color: #ffffff; | ||||||
|  | 			transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||||
|  | 			border-radius: 50%; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.star--1 { | ||||||
|  | 			top: 10px; | ||||||
|  | 			left: 35px; | ||||||
|  | 			z-index: 0; | ||||||
|  | 			width: 30px; | ||||||
|  | 			height: 3px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.star--2 { | ||||||
|  | 			top: 18px; | ||||||
|  | 			left: 28px; | ||||||
|  | 			z-index: 1; | ||||||
|  | 			width: 30px; | ||||||
|  | 			height: 3px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.star--3 { | ||||||
|  | 			top: 27px; | ||||||
|  | 			left: 40px; | ||||||
|  | 			z-index: 0; | ||||||
|  | 			width: 30px; | ||||||
|  | 			height: 3px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.star--4, | ||||||
|  | 		.star--5, | ||||||
|  | 		.star--6 { | ||||||
|  | 			opacity: 0; | ||||||
|  | 			transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.star--4 { | ||||||
|  | 			top: 16px; | ||||||
|  | 			left: 11px; | ||||||
|  | 			z-index: 0; | ||||||
|  | 			width: 2px; | ||||||
|  | 			height: 2px; | ||||||
|  | 			transform: translate3d(3px,0,0); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.star--5 { | ||||||
|  | 			top: 32px; | ||||||
|  | 			left: 17px; | ||||||
|  | 			z-index: 0; | ||||||
|  | 			width: 3px; | ||||||
|  | 			height: 3px; | ||||||
|  | 			transform: translate3d(3px,0,0); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		.star--6 { | ||||||
|  | 			top: 36px; | ||||||
|  | 			left: 28px; | ||||||
|  | 			z-index: 0; | ||||||
|  | 			width: 2px; | ||||||
|  | 			height: 2px; | ||||||
|  | 			transform: translate3d(3px,0,0); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		input:checked { | ||||||
|  | 			+ .toggle { | ||||||
|  | 				background-color: #749DD6; | ||||||
| 
 | 
 | ||||||
| 				> .before { | 				> .before { | ||||||
| 					left: -70px; | 					color: var(--fg); | ||||||
| 					color: var(--accent); |  | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				> .after { | 				> .after { | ||||||
| 					right: -68px; | 					color: var(--accent); | ||||||
| 					color: var(--fg); |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			.toggle__handler { |  | ||||||
| 				display: inline-block; |  | ||||||
| 				position: relative; |  | ||||||
| 				z-index: 1; |  | ||||||
| 				top: 3px; |  | ||||||
| 				left: 3px; |  | ||||||
| 				width: 50px - 6; |  | ||||||
| 				height: 50px - 6; |  | ||||||
| 				background-color: #FFCF96; |  | ||||||
| 				border-radius: 50px; |  | ||||||
| 				box-shadow: 0 2px 6px rgba(0,0,0,.3); |  | ||||||
| 				transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important; |  | ||||||
| 				transform:  rotate(-45deg); |  | ||||||
| 
 |  | ||||||
| 				.crater { |  | ||||||
| 					position: absolute; |  | ||||||
| 					background-color: #E8CDA5; |  | ||||||
| 					opacity: 0; |  | ||||||
| 					transition: opacity 200ms ease-in-out !important; |  | ||||||
| 					border-radius: 100%; |  | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				.crater--1 { | 				.toggle__handler { | ||||||
| 					top: 18px; | 					background-color: #FFE5B5; | ||||||
| 					left: 10px; | 					transform: translate3d(40px, 0, 0) rotate(0); | ||||||
|  | 
 | ||||||
|  | 					.crater { opacity: 1; } | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				.star--1 { | ||||||
|  | 					width: 2px; | ||||||
|  | 					height: 2px; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				.star--2 { | ||||||
| 					width: 4px; | 					width: 4px; | ||||||
| 					height: 4px; | 					height: 4px; | ||||||
|  | 					transform: translate3d(-5px, 0, 0); | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				.crater--2 { | 				.star--3 { | ||||||
| 					top: 28px; | 					width: 2px; | ||||||
| 					left: 22px; | 					height: 2px; | ||||||
| 					width: 6px; | 					transform: translate3d(-7px, 0, 0); | ||||||
| 					height: 6px; |  | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				.crater--3 { | 				.star--4, | ||||||
| 					top: 10px; | 				.star--5, | ||||||
| 					left: 25px; | 				.star--6 { | ||||||
| 					width: 8px; | 					opacity: 1; | ||||||
| 					height: 8px; | 					transform: translate3d(0,0,0); | ||||||
| 				} | 				} | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			.star { | 				.star--4 { | ||||||
| 				position: absolute; | 					transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||||
| 				background-color: #ffffff; | 				} | ||||||
| 				transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; |  | ||||||
| 				border-radius: 50%; |  | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			.star--1 { | 				.star--5 { | ||||||
| 				top: 10px; | 					transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||||
| 				left: 35px; | 				} | ||||||
| 				z-index: 0; |  | ||||||
| 				width: 30px; |  | ||||||
| 				height: 3px; |  | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			.star--2 { | 				.star--6 { | ||||||
| 				top: 18px; | 					transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||||
| 				left: 28px; |  | ||||||
| 				z-index: 1; |  | ||||||
| 				width: 30px; |  | ||||||
| 				height: 3px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			.star--3 { |  | ||||||
| 				top: 27px; |  | ||||||
| 				left: 40px; |  | ||||||
| 				z-index: 0; |  | ||||||
| 				width: 30px; |  | ||||||
| 				height: 3px; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			.star--4, |  | ||||||
| 			.star--5, |  | ||||||
| 			.star--6 { |  | ||||||
| 				opacity: 0; |  | ||||||
| 				transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			.star--4 { |  | ||||||
| 				top: 16px; |  | ||||||
| 				left: 11px; |  | ||||||
| 				z-index: 0; |  | ||||||
| 				width: 2px; |  | ||||||
| 				height: 2px; |  | ||||||
| 				transform: translate3d(3px,0,0); |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			.star--5 { |  | ||||||
| 				top: 32px; |  | ||||||
| 				left: 17px; |  | ||||||
| 				z-index: 0; |  | ||||||
| 				width: 3px; |  | ||||||
| 				height: 3px; |  | ||||||
| 				transform: translate3d(3px,0,0); |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			.star--6 { |  | ||||||
| 				top: 36px; |  | ||||||
| 				left: 28px; |  | ||||||
| 				z-index: 0; |  | ||||||
| 				width: 2px; |  | ||||||
| 				height: 2px; |  | ||||||
| 				transform: translate3d(3px,0,0); |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			input:checked { |  | ||||||
| 				+ .toggle { |  | ||||||
| 					background-color: #749DD6; |  | ||||||
| 
 |  | ||||||
| 					> .before { |  | ||||||
| 						color: var(--fg); |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .after { |  | ||||||
| 						color: var(--accent); |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					.toggle__handler { |  | ||||||
| 						background-color: #FFE5B5; |  | ||||||
| 						transform: translate3d(40px, 0, 0) rotate(0); |  | ||||||
| 
 |  | ||||||
| 						.crater { opacity: 1; } |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					.star--1 { |  | ||||||
| 						width: 2px; |  | ||||||
| 						height: 2px; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					.star--2 { |  | ||||||
| 						width: 4px; |  | ||||||
| 						height: 4px; |  | ||||||
| 						transform: translate3d(-5px, 0, 0); |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					.star--3 { |  | ||||||
| 						width: 2px; |  | ||||||
| 						height: 2px; |  | ||||||
| 						transform: translate3d(-7px, 0, 0); |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					.star--4, |  | ||||||
| 					.star--5, |  | ||||||
| 					.star--6 { |  | ||||||
| 						opacity: 1; |  | ||||||
| 						transform: translate3d(0,0,0); |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					.star--4 { |  | ||||||
| 						transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					.star--5 { |  | ||||||
| 						transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					.star--6 { |  | ||||||
| 						transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -1,47 +1,53 @@ | ||||||
| <template> | <template> | ||||||
| <div class="_section"> | <div> | ||||||
| 	<div class="_card"> | 	<MkTab v-model:value="tab"> | ||||||
| 		<MkTab v-model:value="tab"> | 		<option value="soft">{{ $t('_wordMute.soft') }}</option> | ||||||
| 			<option value="soft">{{ $t('_wordMute.soft') }}</option> | 		<option value="hard">{{ $t('_wordMute.hard') }}</option> | ||||||
| 			<option value="hard">{{ $t('_wordMute.hard') }}</option> | 	</MkTab> | ||||||
| 		</MkTab> | 	<FormBase> | ||||||
| 		<div class="_content"> | 		<div class="_formItem"> | ||||||
| 			<div v-show="tab === 'soft'"> | 			<div v-show="tab === 'soft'"> | ||||||
| 				<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo> | 				<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo> | ||||||
| 				<MkTextarea v-model:value="softMutedWords"> | 				<FormTextarea v-model:value="softMutedWords"> | ||||||
| 					<span>{{ $t('_wordMute.muteWords') }}</span> | 					<span>{{ $t('_wordMute.muteWords') }}</span> | ||||||
| 					<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template> | 					<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template> | ||||||
| 				</MkTextarea> | 				</FormTextarea> | ||||||
| 			</div> | 			</div> | ||||||
| 			<div v-show="tab === 'hard'"> | 			<div v-show="tab === 'hard'"> | ||||||
| 				<MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo> | 				<MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo> | ||||||
| 				<MkTextarea v-model:value="hardMutedWords" style="margin-bottom: 16px;"> | 				<FormTextarea v-model:value="hardMutedWords"> | ||||||
| 					<span>{{ $t('_wordMute.muteWords') }}</span> | 					<span>{{ $t('_wordMute.muteWords') }}</span> | ||||||
| 					<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template> | 					<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template> | ||||||
| 				</MkTextarea> | 				</FormTextarea> | ||||||
| 				<div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div> | 				<FormKeyValueView v-if="hardWordMutedNotesCount != null"> | ||||||
|  | 					<template #key>{{ $t('_wordMute.mutedNotes') }}</template> | ||||||
|  | 					<template #value>{{ number(hardWordMutedNotesCount) }}</template> | ||||||
|  | 				</FormKeyValueView> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 		<div class="_footer"> | 		<FormButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</FormButton> | ||||||
| 			<MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton> | 	</FormBase> | ||||||
| 		</div> |  | ||||||
| 	</div> |  | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons'; | import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkButton from '@/components/ui/button.vue'; | import FormTextarea from '@/components/form/textarea.vue'; | ||||||
| import MkTextarea from '@/components/ui/textarea.vue'; | import FormBase from '@/components/form/base.vue'; | ||||||
|  | import FormKeyValueView from '@/components/form/key-value-view.vue'; | ||||||
|  | import FormButton from '@/components/form/button.vue'; | ||||||
| import MkTab from '@/components/tab.vue'; | import MkTab from '@/components/tab.vue'; | ||||||
| import MkInfo from '@/components/ui/info.vue'; | import MkInfo from '@/components/ui/info.vue'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  | import number from '@/filters/number'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		FormBase, | ||||||
| 		MkTextarea, | 		FormButton, | ||||||
|  | 		FormTextarea, | ||||||
|  | 		FormKeyValueView, | ||||||
| 		MkTab, | 		MkTab, | ||||||
| 		MkInfo, | 		MkInfo, | ||||||
| 	}, | 	}, | ||||||
|  | @ -97,6 +103,8 @@ export default defineComponent({ | ||||||
| 			}); | 			}); | ||||||
| 			this.changed = false; | 			this.changed = false; | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		number | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -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> | ||||||
| 	<div ref="chart"></div> | 	<template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template> | ||||||
| </div> | 
 | ||||||
|  | 	<div style="padding: 8px;"> | ||||||
|  | 		<div ref="chart"></div> | ||||||
|  | 	</div> | ||||||
|  | </MkContainer> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
| import ApexCharts from 'apexcharts'; | import ApexCharts from 'apexcharts'; | ||||||
|  | import { faChartBar } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  | import MkContainer from '@/components/ui/container.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
|  | 	components: { | ||||||
|  | 		MkContainer, | ||||||
|  | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		user: { | 		user: { | ||||||
| 			type: Object, | 			type: Object, | ||||||
|  | @ -25,7 +34,8 @@ export default defineComponent({ | ||||||
| 		return { | 		return { | ||||||
| 			fetching: true, | 			fetching: true, | ||||||
| 			data: [], | 			data: [], | ||||||
| 			peak: null | 			peak: null, | ||||||
|  | 			faChartBar, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
|  |  | ||||||
|  | @ -1,29 +1,43 @@ | ||||||
| <template> | <template> | ||||||
| <div class="ujigsodd"> | <MkContainer> | ||||||
| 	<MkLoading v-if="fetching"/> | 	<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template> | ||||||
| 	<div class="stream" v-if="!fetching && images.length > 0"> | 	<div class="ujigsodd"> | ||||||
| 		<MkA v-for="image in images" | 		<MkLoading v-if="fetching"/> | ||||||
| 			class="img" | 		<div class="stream" v-if="!fetching && images.length > 0"> | ||||||
| 			:style="`background-image: url(${thumbnail(image.file)})`" | 			<MkA v-for="image in images" | ||||||
| 			:to="notePage(image.note)" | 				class="img" | ||||||
| 		></MkA> | 				:style="`background-image: url(${thumbnail(image.file)})`" | ||||||
|  | 				:to="notePage(image.note)" | ||||||
|  | 			></MkA> | ||||||
|  | 		</div> | ||||||
|  | 		<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p> | ||||||
| 	</div> | 	</div> | ||||||
| 	<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p> | </MkContainer> | ||||||
| </div> |  | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import { defineComponent } from 'vue'; | import { defineComponent } from 'vue'; | ||||||
|  | import { faImage } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||||
| import notePage from '../../filters/note'; | import notePage from '../../filters/note'; | ||||||
| import * as os from '@/os'; | import * as os from '@/os'; | ||||||
|  | import MkContainer from '@/components/ui/container.vue'; | ||||||
| 
 | 
 | ||||||
| export default defineComponent({ | export default defineComponent({ | ||||||
| 	props: ['user'], | 	components: { | ||||||
|  | 		MkContainer, | ||||||
|  | 	}, | ||||||
|  | 	props: { | ||||||
|  | 		user: { | ||||||
|  | 			type: Object, | ||||||
|  | 			required: true | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			fetching: true, | 			fetching: true, | ||||||
| 			images: [] | 			images: [], | ||||||
|  | 			faImage | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
|  | @ -37,7 +51,7 @@ export default defineComponent({ | ||||||
| 		os.api('users/notes', { | 		os.api('users/notes', { | ||||||
| 			userId: this.user.id, | 			userId: this.user.id, | ||||||
| 			fileType: image, | 			fileType: image, | ||||||
| 			excludeNsfw: !this.$store.state.device.alwaysShowNsfw, | 			excludeNsfw: this.$store.state.device.nsfw !== 'ignore', | ||||||
| 			limit: 9, | 			limit: 9, | ||||||
| 		}).then(notes => { | 		}).then(notes => { | ||||||
| 			for (const note of notes) { | 			for (const note of notes) { | ||||||
|  | @ -66,6 +80,8 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .ujigsodd { | .ujigsodd { | ||||||
|  | 	padding: 8px; | ||||||
|  | 
 | ||||||
| 	> .stream { | 	> .stream { | ||||||
| 		display: flex; | 		display: flex; | ||||||
| 		justify-content: center; | 		justify-content: center; | ||||||
|  |  | ||||||
|  | @ -1,115 +1,113 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mk-user-page" v-if="user" v-size="{ max: [500] }"> | <div> | ||||||
| 	<!-- TODO --> | 	<div class="mk-user-page" v-if="user" v-size="{ max: [500] }" :class="{ _section: narrow === false }"> | ||||||
| 	<!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> --> | 		<!-- TODO --> | ||||||
| 	<!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> --> | 		<!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> --> | ||||||
|  | 		<!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> --> | ||||||
| 
 | 
 | ||||||
| 	<div class="profile _section _fitBottom"> | 		<div class="main"> | ||||||
| 		<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/> | 			<div class="profile _vMargin" :class="{ _section: narrow === true }"> | ||||||
|  | 				<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/> | ||||||
| 
 | 
 | ||||||
| 		<div class="_content _vMargin" :key="user.id"> | 				<div class="_content _panel _vMargin" :key="user.id"> | ||||||
| 			<div class="banner-container" :style="style"> | 					<div class="banner-container" :style="style"> | ||||||
| 				<div class="banner" ref="banner" :style="style"></div> | 						<div class="banner" ref="banner" :style="style"></div> | ||||||
| 				<div class="fade"></div> | 						<div class="fade"></div> | ||||||
| 				<div class="title"> | 						<div class="title"> | ||||||
| 					<MkUserName class="name" :user="user" :nowrap="true"/> | 							<MkUserName class="name" :user="user" :nowrap="true"/> | ||||||
| 					<div class="bottom"> | 							<div class="bottom"> | ||||||
| 						<span class="username"><MkAcct :user="user" :detail="true" /></span> | 								<span class="username"><MkAcct :user="user" :detail="true" /></span> | ||||||
| 						<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span> | 								<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span> | ||||||
| 						<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span> | 								<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span> | ||||||
| 						<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span> | 								<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span> | ||||||
| 						<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span> | 								<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span> | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
|  | 						<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span> | ||||||
|  | 						<div class="actions" v-if="$store.getters.isSignedIn"> | ||||||
|  | 							<button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button> | ||||||
|  | 							<MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 					<MkAvatar class="avatar" :user="user" :disable-preview="true"/> | ||||||
|  | 					<div class="title"> | ||||||
|  | 						<MkUserName :user="user" :nowrap="false" class="name"/> | ||||||
|  | 						<div class="bottom"> | ||||||
|  | 							<span class="username"><MkAcct :user="user" :detail="true" /></span> | ||||||
|  | 							<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span> | ||||||
|  | 							<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span> | ||||||
|  | 							<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span> | ||||||
|  | 							<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span> | ||||||
|  | 						</div> | ||||||
|  | 					</div> | ||||||
|  | 					<div class="description"> | ||||||
|  | 						<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> | ||||||
|  | 						<p v-else class="empty">{{ $t('noAccountDescription') }}</p> | ||||||
|  | 					</div> | ||||||
|  | 					<div class="fields system"> | ||||||
|  | 						<dl class="field" v-if="user.location"> | ||||||
|  | 							<dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt> | ||||||
|  | 							<dd class="value">{{ user.location }}</dd> | ||||||
|  | 						</dl> | ||||||
|  | 						<dl class="field" v-if="user.birthday"> | ||||||
|  | 							<dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt> | ||||||
|  | 							<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd> | ||||||
|  | 						</dl> | ||||||
|  | 						<dl class="field"> | ||||||
|  | 							<dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt> | ||||||
|  | 							<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd> | ||||||
|  | 						</dl> | ||||||
|  | 					</div> | ||||||
|  | 					<div class="fields" v-if="user.fields.length > 0"> | ||||||
|  | 						<dl class="field" v-for="(field, i) in user.fields" :key="i"> | ||||||
|  | 							<dt class="name"> | ||||||
|  | 								<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/> | ||||||
|  | 							</dt> | ||||||
|  | 							<dd class="value"> | ||||||
|  | 								<Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/> | ||||||
|  | 							</dd> | ||||||
|  | 						</dl> | ||||||
|  | 					</div> | ||||||
|  | 					<div class="status"> | ||||||
|  | 						<MkA :to="userPage(user)" :class="{ active: page === 'index' }"> | ||||||
|  | 							<b>{{ number(user.notesCount) }}</b> | ||||||
|  | 							<span>{{ $t('notes') }}</span> | ||||||
|  | 						</MkA> | ||||||
|  | 						<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }"> | ||||||
|  | 							<b>{{ number(user.followingCount) }}</b> | ||||||
|  | 							<span>{{ $t('following') }}</span> | ||||||
|  | 						</MkA> | ||||||
|  | 						<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }"> | ||||||
|  | 							<b>{{ number(user.followersCount) }}</b> | ||||||
|  | 							<span>{{ $t('followers') }}</span> | ||||||
|  | 						</MkA> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span> | 			</div> | ||||||
| 				<div class="actions" v-if="$store.getters.isSignedIn"> | 
 | ||||||
| 					<button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button> | 			<template v-if="page === 'index'"> | ||||||
| 					<MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> | 				<div v-if="user.pinnedNotes.length > 0" :class="{ _section: narrow === true, _vMargin: narrow === false }"> | ||||||
|  | 					<XNote v-for="note in user.pinnedNotes" class="note _content _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 				<div v-if="narrow === true" class="_section"> | ||||||
| 			<MkAvatar class="avatar" :user="user" :disable-preview="true"/> | 					<XPhotos class="_content _vMargin" :user="user" :key="user.id"/> | ||||||
| 			<div class="title"> | 					<XActivity class="_content _vMargin" :user="user" :key="user.id"/> | ||||||
| 				<MkUserName :user="user" :nowrap="false" class="name"/> |  | ||||||
| 				<div class="bottom"> |  | ||||||
| 					<span class="username"><MkAcct :user="user" :detail="true" /></span> |  | ||||||
| 					<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span> |  | ||||||
| 					<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span> |  | ||||||
| 					<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span> |  | ||||||
| 					<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span> |  | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 				<div :class="{ _section: narrow === true, _vMargin: narrow === false }"> | ||||||
| 			<div class="description"> | 					<XUserTimeline :user="user" class="_content"/> | ||||||
| 				<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/> | 				</div> | ||||||
| 				<p v-else class="empty">{{ $t('noAccountDescription') }}</p> | 			</template> | ||||||
| 			</div> | 			<XFollowList v-else-if="page === 'following'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="following" :user="user"/> | ||||||
| 			<div class="fields system"> | 			<XFollowList v-else-if="page === 'followers'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="followers" :user="user"/> | ||||||
| 				<dl class="field" v-if="user.location"> | 		</div> | ||||||
| 					<dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt> | 		<div class="side" v-if="narrow === false"> | ||||||
| 					<dd class="value">{{ user.location }}</dd> | 			<XPhotos class="_vMargin" :user="user" :key="user.id"/> | ||||||
| 				</dl> | 			<XActivity class="_vMargin" :user="user" :key="user.id"/> | ||||||
| 				<dl class="field" v-if="user.birthday"> |  | ||||||
| 					<dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt> |  | ||||||
| 					<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd> |  | ||||||
| 				</dl> |  | ||||||
| 				<dl class="field"> |  | ||||||
| 					<dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt> |  | ||||||
| 					<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd> |  | ||||||
| 				</dl> |  | ||||||
| 			</div> |  | ||||||
| 			<div class="fields" v-if="user.fields.length > 0"> |  | ||||||
| 				<dl class="field" v-for="(field, i) in user.fields" :key="i"> |  | ||||||
| 					<dt class="name"> |  | ||||||
| 						<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/> |  | ||||||
| 					</dt> |  | ||||||
| 					<dd class="value"> |  | ||||||
| 						<Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/> |  | ||||||
| 					</dd> |  | ||||||
| 				</dl> |  | ||||||
| 			</div> |  | ||||||
| 			<div class="status"> |  | ||||||
| 				<MkA :to="userPage(user)" :class="{ active: page === 'index' }"> |  | ||||||
| 					<b>{{ number(user.notesCount) }}</b> |  | ||||||
| 					<span>{{ $t('notes') }}</span> |  | ||||||
| 				</MkA> |  | ||||||
| 				<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }"> |  | ||||||
| 					<b>{{ number(user.followingCount) }}</b> |  | ||||||
| 					<span>{{ $t('following') }}</span> |  | ||||||
| 				</MkA> |  | ||||||
| 				<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }"> |  | ||||||
| 					<b>{{ number(user.followersCount) }}</b> |  | ||||||
| 					<span>{{ $t('followers') }}</span> |  | ||||||
| 				</MkA> |  | ||||||
| 			</div> |  | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 	<div v-else-if="error"> | ||||||
| 	<template v-if="page === 'index'"> | 		<MkError @retry="fetch()"/> | ||||||
| 		<div class="_section"> | 	</div> | ||||||
| 			<div class="_content _vMargin" v-if="user.pinnedNotes.length > 0"> |  | ||||||
| 				<XNote v-for="note in user.pinnedNotes" class="note _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/> |  | ||||||
| 			</div> |  | ||||||
| 			<MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-images"> |  | ||||||
| 				<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template> |  | ||||||
| 				<div> |  | ||||||
| 					<XPhotos :user="user" :key="user.id"/> |  | ||||||
| 				</div> |  | ||||||
| 			</MkFolder> |  | ||||||
| 			<MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-activity"> |  | ||||||
| 				<template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template> |  | ||||||
| 				<div> |  | ||||||
| 					<XActivity :user="user" :key="user.id"/> |  | ||||||
| 				</div> |  | ||||||
| 			</MkFolder> |  | ||||||
| 		</div> |  | ||||||
| 		<div class="_section"> |  | ||||||
| 			<XUserTimeline :user="user" class="_content"/> |  | ||||||
| 		</div> |  | ||||||
| 	</template> |  | ||||||
| 	<XFollowList v-else-if="page === 'following'" type="following" :user="user"/> |  | ||||||
| 	<XFollowList v-else-if="page === 'followers'" type="followers" :user="user"/> |  | ||||||
| </div> |  | ||||||
| <div v-else-if="error"> |  | ||||||
| 	<MkError @retry="fetch()"/> |  | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -170,6 +168,7 @@ export default defineComponent({ | ||||||
| 			user: null, | 			user: null, | ||||||
| 			error: null, | 			error: null, | ||||||
| 			parallaxAnimationId: null, | 			parallaxAnimationId: null, | ||||||
|  | 			narrow: null, | ||||||
| 			faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt | 			faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  | @ -197,6 +196,7 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		window.requestAnimationFrame(this.parallaxLoop); | 		window.requestAnimationFrame(this.parallaxLoop); | ||||||
|  | 		this.narrow = this.$el.clientWidth < 1000; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	beforeUnmount() { | 	beforeUnmount() { | ||||||
|  | @ -254,220 +254,234 @@ export default defineComponent({ | ||||||
| 
 | 
 | ||||||
| <style lang="scss" scoped> | <style lang="scss" scoped> | ||||||
| .mk-user-page { | .mk-user-page { | ||||||
| 	> .punished { | 	display: flex; | ||||||
| 		font-size: 0.8em; | 	max-width: 1050px; | ||||||
| 		padding: 16px; | 	margin: 0 auto; | ||||||
| 	} |  | ||||||
| 	 | 	 | ||||||
| 	> .profile { | 	> .main { | ||||||
| 		> ._content { | 		flex: 1; | ||||||
| 			position: relative; |  | ||||||
| 			overflow: hidden; |  | ||||||
| 
 | 
 | ||||||
| 			> .banner-container { | 		> .punished { | ||||||
|  | 			font-size: 0.8em; | ||||||
|  | 			padding: 16px; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		> .profile { | ||||||
|  | 			> ._content { | ||||||
| 				position: relative; | 				position: relative; | ||||||
| 				height: 250px; |  | ||||||
| 				overflow: hidden; | 				overflow: hidden; | ||||||
| 				background-size: cover; |  | ||||||
| 				background-position: center; |  | ||||||
| 				border-radius: 12px; |  | ||||||
| 
 | 
 | ||||||
| 				> .banner { | 				> .banner-container { | ||||||
| 					height: 100%; | 					position: relative; | ||||||
| 					background-color: #4c5e6d; | 					height: 250px; | ||||||
|  | 					overflow: hidden; | ||||||
| 					background-size: cover; | 					background-size: cover; | ||||||
| 					background-position: center; | 					background-position: center; | ||||||
| 					box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; |  | ||||||
| 					will-change: background-position; |  | ||||||
| 				} |  | ||||||
| 
 | 
 | ||||||
| 				> .fade { | 					> .banner { | ||||||
| 					position: absolute; | 						height: 100%; | ||||||
| 					bottom: 0; | 						background-color: #4c5e6d; | ||||||
| 					left: 0; | 						background-size: cover; | ||||||
| 					width: 100%; | 						background-position: center; | ||||||
| 					height: 78px; | 						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; | ||||||
| 					background: linear-gradient(transparent, rgba(#000, 0.7)); | 						will-change: background-position; | ||||||
| 				} | 					} | ||||||
| 
 | 
 | ||||||
| 				> .followed { | 					> .fade { | ||||||
| 					position: absolute; | 						position: absolute; | ||||||
| 					top: 12px; | 						bottom: 0; | ||||||
| 					left: 12px; | 						left: 0; | ||||||
| 					padding: 4px 8px; | 						width: 100%; | ||||||
| 					color: #fff; | 						height: 78px; | ||||||
| 					background: rgba(0, 0, 0, 0.7); | 						background: linear-gradient(transparent, rgba(#000, 0.7)); | ||||||
| 					font-size: 0.7em; | 					} | ||||||
| 					border-radius: 6px; |  | ||||||
| 				} |  | ||||||
| 
 | 
 | ||||||
| 				> .actions { | 					> .followed { | ||||||
| 					position: absolute; | 						position: absolute; | ||||||
| 					top: 12px; | 						top: 12px; | ||||||
| 					right: 12px; | 						left: 12px; | ||||||
| 					-webkit-backdrop-filter: blur(8px); | 						padding: 4px 8px; | ||||||
| 					backdrop-filter: blur(8px); |  | ||||||
| 					background: rgba(0, 0, 0, 0.2); |  | ||||||
| 					padding: 8px; |  | ||||||
| 					border-radius: 24px; |  | ||||||
| 
 |  | ||||||
| 					> .menu { |  | ||||||
| 						vertical-align: bottom; |  | ||||||
| 						height: 31px; |  | ||||||
| 						width: 31px; |  | ||||||
| 						color: #fff; | 						color: #fff; | ||||||
| 						text-shadow: 0 0 8px #000; | 						background: rgba(0, 0, 0, 0.7); | ||||||
| 						font-size: 16px; | 						font-size: 0.7em; | ||||||
|  | 						border-radius: 6px; | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					> .koudoku { | 					> .actions { | ||||||
| 						margin-left: 4px; | 						position: absolute; | ||||||
| 						vertical-align: bottom; | 						top: 12px; | ||||||
| 					} | 						right: 12px; | ||||||
| 				} | 						-webkit-backdrop-filter: blur(8px); | ||||||
|  | 						backdrop-filter: blur(8px); | ||||||
|  | 						background: rgba(0, 0, 0, 0.2); | ||||||
|  | 						padding: 8px; | ||||||
|  | 						border-radius: 24px; | ||||||
| 
 | 
 | ||||||
| 				> .title { | 						> .menu { | ||||||
| 					position: absolute; | 							vertical-align: bottom; | ||||||
| 					bottom: 0; | 							height: 31px; | ||||||
| 					left: 0; | 							width: 31px; | ||||||
| 					width: 100%; | 							color: #fff; | ||||||
| 					padding: 0 0 8px 154px; | 							text-shadow: 0 0 8px #000; | ||||||
| 					box-sizing: border-box; | 							font-size: 16px; | ||||||
| 					color: #fff; | 						} | ||||||
| 
 | 
 | ||||||
| 					> .name { | 						> .koudoku { | ||||||
| 						display: block; | 							margin-left: 4px; | ||||||
| 						margin: 0; | 							vertical-align: bottom; | ||||||
| 						line-height: 32px; | 						} | ||||||
| 						font-weight: bold; |  | ||||||
| 						font-size: 1.8em; |  | ||||||
| 						text-shadow: 0 0 8px #000; |  | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					> .bottom { | 					> .title { | ||||||
| 						> * { | 						position: absolute; | ||||||
| 							display: inline-block; | 						bottom: 0; | ||||||
| 							margin-right: 16px; | 						left: 0; | ||||||
| 							line-height: 20px; | 						width: 100%; | ||||||
| 							opacity: 0.8; | 						padding: 0 0 8px 154px; | ||||||
|  | 						box-sizing: border-box; | ||||||
|  | 						color: #fff; | ||||||
| 
 | 
 | ||||||
| 							&.username { | 						> .name { | ||||||
| 								font-weight: bold; | 							display: block; | ||||||
|  | 							margin: 0; | ||||||
|  | 							line-height: 32px; | ||||||
|  | 							font-weight: bold; | ||||||
|  | 							font-size: 1.8em; | ||||||
|  | 							text-shadow: 0 0 8px #000; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> .bottom { | ||||||
|  | 							> * { | ||||||
|  | 								display: inline-block; | ||||||
|  | 								margin-right: 16px; | ||||||
|  | 								line-height: 20px; | ||||||
|  | 								opacity: 0.8; | ||||||
|  | 
 | ||||||
|  | 								&.username { | ||||||
|  | 									font-weight: bold; | ||||||
|  | 								} | ||||||
| 							} | 							} | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} |  | ||||||
| 
 | 
 | ||||||
| 			> .title { | 				> .title { | ||||||
| 				display: none; | 					display: none; | ||||||
| 				text-align: center; |  | ||||||
| 				padding: 50px 8px 16px 8px; |  | ||||||
| 				font-weight: bold; |  | ||||||
| 				border-bottom: solid 1px var(--divider); |  | ||||||
| 
 |  | ||||||
| 				> .bottom { |  | ||||||
| 					> * { |  | ||||||
| 						display: inline-block; |  | ||||||
| 						margin-right: 8px; |  | ||||||
| 						opacity: 0.8; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .avatar { |  | ||||||
| 				display: block; |  | ||||||
| 				position: absolute; |  | ||||||
| 				top: 170px; |  | ||||||
| 				left: 16px; |  | ||||||
| 				z-index: 2; |  | ||||||
| 				width: 120px; |  | ||||||
| 				height: 120px; |  | ||||||
| 				box-shadow: 1px 1px 3px rgba(#000, 0.2); |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .description { |  | ||||||
| 				padding: 24px 24px 24px 154px; |  | ||||||
| 				font-size: 0.95em; |  | ||||||
| 
 |  | ||||||
| 				> .empty { |  | ||||||
| 					margin: 0; |  | ||||||
| 					opacity: 0.5; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .fields { |  | ||||||
| 				padding: 24px; |  | ||||||
| 				font-size: 0.9em; |  | ||||||
| 				border-top: solid 1px var(--divider); |  | ||||||
| 
 |  | ||||||
| 				> .field { |  | ||||||
| 					display: flex; |  | ||||||
| 					padding: 0; |  | ||||||
| 					margin: 0; |  | ||||||
| 					align-items: center; |  | ||||||
| 
 |  | ||||||
| 					&:not(:last-child) { |  | ||||||
| 						margin-bottom: 8px; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .name { |  | ||||||
| 						width: 30%; |  | ||||||
| 						overflow: hidden; |  | ||||||
| 						white-space: nowrap; |  | ||||||
| 						text-overflow: ellipsis; |  | ||||||
| 						font-weight: bold; |  | ||||||
| 						text-align: center; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					> .value { |  | ||||||
| 						width: 70%; |  | ||||||
| 						overflow: hidden; |  | ||||||
| 						white-space: nowrap; |  | ||||||
| 						text-overflow: ellipsis; |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				&.system > .field > .name { |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			> .status { |  | ||||||
| 				display: flex; |  | ||||||
| 				padding: 24px; |  | ||||||
| 				border-top: solid 1px var(--divider); |  | ||||||
| 
 |  | ||||||
| 				> a { |  | ||||||
| 					flex: 1; |  | ||||||
| 					text-align: center; | 					text-align: center; | ||||||
|  | 					padding: 50px 8px 16px 8px; | ||||||
|  | 					font-weight: bold; | ||||||
|  | 					border-bottom: solid 1px var(--divider); | ||||||
| 
 | 
 | ||||||
| 					&.active { | 					> .bottom { | ||||||
| 						color: var(--accent); | 						> * { | ||||||
|  | 							display: inline-block; | ||||||
|  | 							margin-right: 8px; | ||||||
|  | 							opacity: 0.8; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .avatar { | ||||||
|  | 					display: block; | ||||||
|  | 					position: absolute; | ||||||
|  | 					top: 170px; | ||||||
|  | 					left: 16px; | ||||||
|  | 					z-index: 2; | ||||||
|  | 					width: 120px; | ||||||
|  | 					height: 120px; | ||||||
|  | 					box-shadow: 1px 1px 3px rgba(#000, 0.2); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .description { | ||||||
|  | 					padding: 24px 24px 24px 154px; | ||||||
|  | 					font-size: 0.95em; | ||||||
|  | 
 | ||||||
|  | 					> .empty { | ||||||
|  | 						margin: 0; | ||||||
|  | 						opacity: 0.5; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				> .fields { | ||||||
|  | 					padding: 24px; | ||||||
|  | 					font-size: 0.9em; | ||||||
|  | 					border-top: solid 1px var(--divider); | ||||||
|  | 
 | ||||||
|  | 					> .field { | ||||||
|  | 						display: flex; | ||||||
|  | 						padding: 0; | ||||||
|  | 						margin: 0; | ||||||
|  | 						align-items: center; | ||||||
|  | 
 | ||||||
|  | 						&:not(:last-child) { | ||||||
|  | 							margin-bottom: 8px; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> .name { | ||||||
|  | 							width: 30%; | ||||||
|  | 							overflow: hidden; | ||||||
|  | 							white-space: nowrap; | ||||||
|  | 							text-overflow: ellipsis; | ||||||
|  | 							font-weight: bold; | ||||||
|  | 							text-align: center; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> .value { | ||||||
|  | 							width: 70%; | ||||||
|  | 							overflow: hidden; | ||||||
|  | 							white-space: nowrap; | ||||||
|  | 							text-overflow: ellipsis; | ||||||
|  | 						} | ||||||
| 					} | 					} | ||||||
| 
 | 
 | ||||||
| 					&:hover { | 					&.system > .field > .name { | ||||||
| 						text-decoration: none; |  | ||||||
| 					} | 					} | ||||||
|  | 				} | ||||||
| 
 | 
 | ||||||
| 					> b { | 				> .status { | ||||||
| 						display: block; | 					display: flex; | ||||||
| 						line-height: 16px; | 					padding: 24px; | ||||||
| 					} | 					border-top: solid 1px var(--divider); | ||||||
| 
 | 
 | ||||||
| 					> span { | 					> a { | ||||||
| 						font-size: 70%; | 						flex: 1; | ||||||
|  | 						text-align: center; | ||||||
|  | 
 | ||||||
|  | 						&.active { | ||||||
|  | 							color: var(--accent); | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						&:hover { | ||||||
|  | 							text-decoration: none; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> b { | ||||||
|  | 							display: block; | ||||||
|  | 							line-height: 16px; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						> span { | ||||||
|  | 							font-size: 70%; | ||||||
|  | 						} | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		> .content { | ||||||
|  | 			margin-bottom: var(--margin); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	> .content { | 	> .side { | ||||||
| 		margin-bottom: var(--margin); | 		flex-basis: 300px; | ||||||
|  | 		margin-left: var(--margin); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	&.max-width_500px { | 	&.max-width_500px { | ||||||
| 		> .profile > ._content { | 		display: block; | ||||||
|  | 
 | ||||||
|  | 		> .main > .profile > ._content { | ||||||
| 			> .banner-container { | 			> .banner-container { | ||||||
| 				height: 140px; | 				height: 140px; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue