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として設定" | ||||
| flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。" | ||||
| flagAsCat: "Catとして設定" | ||||
| flagAsCatDescription: "このアカウントが猫であることを示す場合は、このフラグをオンにします。" | ||||
| autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認" | ||||
| addAcount: "アカウント追加" | ||||
| loginFailed: "ログインに失敗しました" | ||||
|  | @ -440,6 +441,7 @@ useOsNativeEmojis: "OSネイティブの絵文字を使用" | |||
| youHaveNoGroups: "グループがありません" | ||||
| joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。" | ||||
| noHistory: "履歴はありません" | ||||
| signinHistory: "ログイン履歴" | ||||
| disableAnimatedMfm: "動きのあるMFMを無効にする" | ||||
| doing: "やっています" | ||||
| category: "カテゴリ" | ||||
|  | @ -492,6 +494,7 @@ none: "なし" | |||
| showInPage: "ページで表示" | ||||
| popout: "ポップアウト" | ||||
| volume: "音量" | ||||
| masterVolume: "マスター音量" | ||||
| details: "詳細" | ||||
| chooseEmoji: "絵文字を選択" | ||||
| unableToProcess: "操作を完了できません" | ||||
|  | @ -564,7 +567,8 @@ useStarForReactionFallback: "リアクション絵文字が不明な場合、代 | |||
| emailConfig: "メールサーバー設定" | ||||
| enableEmail: "メール配信機能を有効化する" | ||||
| emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" | ||||
| email: "メールアドレス" | ||||
| email: "メール" | ||||
| emailAddress: "メールアドレス" | ||||
| smtpConfig: "SMTP サーバーの設定" | ||||
| smtpHost: "ホスト" | ||||
| smtpPort: "ポート" | ||||
|  | @ -596,6 +600,7 @@ regenerateLoginTokenDescription: "ログインに使用される内部トーク | |||
| setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。" | ||||
| fileIdOrUrl: "ファイルIDまたはURL" | ||||
| chatOpenBehavior: "チャットを開くときの動作" | ||||
| behavior: "動作" | ||||
| sample: "サンプル" | ||||
| abuseReports: "通報" | ||||
| reportAbuse: "通報" | ||||
|  | @ -619,6 +624,42 @@ createNew: "新規作成" | |||
| optional: "任意" | ||||
| createNewClip: "新しいクリップを作成" | ||||
| 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: | ||||
|   cheatSheet: "MFMチートシート" | ||||
|  | @ -745,6 +786,8 @@ _theme: | |||
|   manage: "テーマの管理" | ||||
|   code: "テーマコード" | ||||
|   installed: "{name}をインストールしました" | ||||
|   installedThemes: "インストールされたテーマ" | ||||
|   builtinThemes: "標準のテーマ" | ||||
|   alreadyInstalled: "そのテーマは既にインストールされています" | ||||
|   invalid: "テーマの形式が間違っています" | ||||
|   make: "テーマを作る" | ||||
|  | @ -820,6 +863,8 @@ _sfx: | |||
|   chatBg: "チャット(バックグラウンド)" | ||||
|   antenna: "アンテナ受信" | ||||
|   channel: "チャンネル通知" | ||||
|   reversiPutBlack: "リバーシ: 黒が打ったとき" | ||||
|   reversiPutWhite: "リバーシ: 白が打ったとき" | ||||
| 
 | ||||
| _ago: | ||||
|   unknown: "謎" | ||||
|  | @ -999,7 +1044,9 @@ _profile: | |||
|   username: "ユーザー名" | ||||
|   description: "自己紹介" | ||||
|   youCanIncludeHashtags: "ハッシュタグを含めることができます。" | ||||
|   metadata: "補足情報" | ||||
|   metadata: "追加情報" | ||||
|   metadataEdit: "追加情報を編集" | ||||
|   metadataDescription: "プロフィールに表として4つまでの追加情報を表示することができます。" | ||||
|   metadataLabel: "ラベル" | ||||
|   metadataContent: "内容" | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ export class instancePinnedPages1605585339718 implements MigrationInterface { | |||
|     name = 'instancePinnedPages1605585339718' | ||||
| 
 | ||||
|     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> { | ||||
|  |  | |||
							
								
								
									
										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> | ||||
| <XModalWindow ref="dialog" | ||||
| 	:width="400" | ||||
| 	:width="450" | ||||
| 	:can-close="false" | ||||
| 	:with-ok-button="true" | ||||
| 	:ok-button-disabled="false" | ||||
|  | @ -12,42 +12,61 @@ | |||
| 	<template #header> | ||||
| 		{{ title }} | ||||
| 	</template> | ||||
| 	<div class="xkpnjxcv _section"> | ||||
| 		<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item"> | ||||
| 			<MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1"> | ||||
| 	<FormBase class="xkpnjxcv"> | ||||
| 		<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)"> | ||||
| 			<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> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 			</MkInput> | ||||
| 			<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text"> | ||||
| 			</FormInput> | ||||
| 			<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> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 			</MkInput> | ||||
| 			<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]"> | ||||
| 			</FormInput> | ||||
| 			<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> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 			</MkTextarea> | ||||
| 			<MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]"> | ||||
| 			</FormTextarea> | ||||
| 			<FormSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]"> | ||||
| 				<span v-text="form[item].label || item"></span> | ||||
| 				<template v-if="form[item].description" #desc>{{ form[item].description }}</template> | ||||
| 			</MkSwitch> | ||||
| 		</label> | ||||
| 	</div> | ||||
| 			</FormSwitch> | ||||
| 			<FormSelect v-else-if="form[item].type === 'enum'" v-model:value="values[item]"> | ||||
| 				<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> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import XModalWindow from '@/components/ui/modal-window.vue'; | ||||
| import MkInput from './ui/input.vue'; | ||||
| import MkTextarea from './ui/textarea.vue'; | ||||
| import MkSwitch from './ui/switch.vue'; | ||||
| import FormBase from './form/base.vue'; | ||||
| import FormInput from './form/input.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({ | ||||
| 	components: { | ||||
| 		XModalWindow, | ||||
| 		MkInput, | ||||
| 		MkTextarea, | ||||
| 		MkSwitch, | ||||
| 		FormBase, | ||||
| 		FormInput, | ||||
| 		FormTextarea, | ||||
| 		FormSwitch, | ||||
| 		FormSelect, | ||||
| 		FormRange, | ||||
| 		FormButton, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
|  | @ -95,12 +114,6 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .xkpnjxcv { | ||||
| 	> label { | ||||
| 		display: block; | ||||
| 
 | ||||
| 		&:not(:last-child) { | ||||
| 			margin-bottom: 32px; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| </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() { | ||||
| 		// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする | ||||
| 		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) { | ||||
| 				this.color = extractAvgColorFromBlurhash(this.image.blurhash); | ||||
| 			} | ||||
|  |  | |||
|  | @ -48,7 +48,7 @@ export default defineComponent({ | |||
| 		} | ||||
| 	}, | ||||
| 	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> | ||||
|  |  | |||
|  | @ -14,8 +14,8 @@ | |||
| 			<option value="res">Response</option> | ||||
| 		</MkTab> | ||||
| 
 | ||||
| 		<code v-if="tab === 'req'">{{ reqStr }}</code> | ||||
| 		<code v-if="tab === 'res'">{{ resStr }}</code> | ||||
| 		<code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code> | ||||
| 		<code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code> | ||||
| 	</div> | ||||
| </XWindow> | ||||
| </template> | ||||
|  | @ -67,7 +67,6 @@ export default defineComponent({ | |||
| 		font-size: 0.9em; | ||||
| 		tab-size: 2; | ||||
| 		white-space: pre; | ||||
| 		font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 	<template #header> | ||||
| 		<Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager | ||||
| 	</template> | ||||
| 	<div class="qljqmnzj"> | ||||
| 	<div class="qljqmnzj _monospace"> | ||||
| 		<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);"> | ||||
| 			<option value="windows">Windows</option> | ||||
| 			<option value="stream">Stream</option> | ||||
|  | @ -150,7 +150,6 @@ export default defineComponent({ | |||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| 	height: 100%; | ||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | ||||
| 
 | ||||
| 	> .content { | ||||
| 		flex: 1; | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| import { defineComponent } from 'vue'; | ||||
| import XNotes from './notes.vue'; | ||||
| import * as os from '@/os'; | ||||
| import * as sound from '@/scripts/sound'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -65,7 +66,7 @@ export default defineComponent({ | |||
| 			this.$emit('note'); | ||||
| 
 | ||||
| 			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> | ||||
| <div class="timctyfi" :class="{ focused, disabled }"> | ||||
| 	<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 | ||||
| 		type="range" | ||||
| 		ref="input" | ||||
|  | @ -19,7 +19,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue';import * as os from '@/os'; | ||||
| import { defineComponent } from 'vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: { | ||||
|  |  | |||
|  | @ -17,10 +17,8 @@ | |||
| 		<span></span> | ||||
| 	</span> | ||||
| 	<span class="label"> | ||||
| 		<span :aria-hidden="!checked"><slot></slot></span> | ||||
| 		<p :aria-hidden="!checked"> | ||||
| 			<slot name="desc"></slot> | ||||
| 		</p> | ||||
| 		<span><slot></slot></span> | ||||
| 		<p><slot name="desc"></slot></p> | ||||
| 	</span> | ||||
| </div> | ||||
| </template> | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| <div class="adhpbeos" :class="{ focused, filled, tall, pre }"> | ||||
| 	<div class="input"> | ||||
| 		<span class="label" ref="label"><slot></slot></span> | ||||
| 		<textarea ref="input" :class="{ code }" | ||||
| 		<textarea ref="input" :class="{ code, _monospace: code }" | ||||
| 			:value="value" | ||||
| 			:required="required" | ||||
| 			:readonly="readonly" | ||||
|  | @ -166,7 +166,6 @@ export default defineComponent({ | |||
| 
 | ||||
| 			&.code { | ||||
| 				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 { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; | ||||
| 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}`); | ||||
| 
 | ||||
|  | @ -50,7 +51,7 @@ if (_DEV_) { | |||
| document.addEventListener('touchend', () => {}, { passive: true }); | ||||
| 
 | ||||
| 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/
 | ||||
|  | @ -307,7 +308,7 @@ if (store.getters.isSignedIn) { | |||
| 			hasUnreadMessagingMessage: true | ||||
| 		}); | ||||
| 
 | ||||
| 		sound('chatBg'); | ||||
| 		sound.play('chatBg'); | ||||
| 	}); | ||||
| 
 | ||||
| 	main.on('readAllAntennas', () => { | ||||
|  | @ -321,7 +322,7 @@ if (store.getters.isSignedIn) { | |||
| 			hasUnreadAntenna: true | ||||
| 		}); | ||||
| 
 | ||||
| 		sound('antenna'); | ||||
| 		sound.play('antenna'); | ||||
| 	}); | ||||
| 
 | ||||
| 	main.on('readAllAnnouncements', () => { | ||||
|  | @ -341,7 +342,7 @@ if (store.getters.isSignedIn) { | |||
| 			hasUnreadChannel: true | ||||
| 		}); | ||||
| 
 | ||||
| 		sound('channel'); | ||||
| 		sound.play('channel'); | ||||
| 	}); | ||||
| 
 | ||||
| 	main.on('readAllAnnouncements', () => { | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import { apiUrl, debug } from '@/config'; | |||
| import MkPostFormDialog from '@/components/post-form-dialog.vue'; | ||||
| import MkWaitingDialog from '@/components/waiting-dialog.vue'; | ||||
| import { resolve } from '@/router'; | ||||
| import { device } from './cold-storage'; | ||||
| 
 | ||||
| const ua = navigator.userAgent.toLowerCase(); | ||||
| 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 uploads = ref([]); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <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"> | ||||
| 			<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div> | ||||
| 			<div class="_content"> | ||||
|  |  | |||
|  | @ -7,6 +7,8 @@ | |||
| 			<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="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="maintainerName">{{ $t('maintainerName') }}</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, | ||||
| 			bannerUrl: null, | ||||
| 			iconUrl: null, | ||||
| 			logoImageUrl: null, | ||||
| 			backgroundImageUrl: null, | ||||
| 			maxNoteTextLength: 0, | ||||
| 			enableRegistration: false, | ||||
| 			enableLocalTimeline: false, | ||||
|  | @ -345,6 +349,8 @@ export default defineComponent({ | |||
| 		this.tosUrl = this.meta.tosUrl; | ||||
| 		this.bannerUrl = this.meta.bannerUrl; | ||||
| 		this.iconUrl = this.meta.iconUrl; | ||||
| 		this.logoImageUrl = this.meta.logoImageUrl; | ||||
| 		this.backgroundImageUrl = this.meta.backgroundImageUrl; | ||||
| 		this.enableEmail = this.meta.enableEmail; | ||||
| 		this.email = this.meta.email; | ||||
| 		this.maintainerName = this.meta.maintainerName; | ||||
|  | @ -498,6 +504,8 @@ export default defineComponent({ | |||
| 				tosUrl: this.tosUrl, | ||||
| 				bannerUrl: this.bannerUrl, | ||||
| 				iconUrl: this.iconUrl, | ||||
| 				logoImageUrl: this.logoImageUrl, | ||||
| 				backgroundImageUrl: this.backgroundImageUrl, | ||||
| 				maintainerName: this.maintainerName, | ||||
| 				maintainerEmail: this.maintainerEmail, | ||||
| 				maxNoteTextLength: this.maxNoteTextLength, | ||||
|  |  | |||
|  | @ -38,6 +38,7 @@ import parseAcct from '../../../misc/acct/parse'; | |||
| import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll'; | ||||
| import * as os from '@/os'; | ||||
| import { popout } from '@/scripts/popout'; | ||||
| import * as sound from '@/scripts/sound'; | ||||
| 
 | ||||
| const Component = defineComponent({ | ||||
| 	components: { | ||||
|  | @ -218,7 +219,7 @@ const Component = defineComponent({ | |||
| 		}, | ||||
| 
 | ||||
| 		onMessage(message) { | ||||
| 			os.sound('chat'); | ||||
| 			sound.play('chat'); | ||||
| 
 | ||||
| 			const _isBottom = isBottom(this.$el, 64); | ||||
| 
 | ||||
|  |  | |||
|  | @ -94,6 +94,7 @@ import { url } from '@/config'; | |||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import { userPage } from '@/filters/user'; | ||||
| import * as os from '@/os'; | ||||
| import * as sound from '@/scripts/sound'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -245,11 +246,7 @@ export default defineComponent({ | |||
| 			this.o.put(this.myColor, pos); | ||||
| 
 | ||||
| 			// サウンドを再生する | ||||
| 			if (this.$store.state.device.enableSounds) { | ||||
| 				const sound = new Audio(`${url}/assets/reversi-put-me.mp3`); | ||||
| 				sound.volume = this.$store.state.device.soundVolume; | ||||
| 				sound.play(); | ||||
| 			} | ||||
| 			sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite'); | ||||
| 
 | ||||
| 			this.connection.send('set', { | ||||
| 				pos: pos | ||||
|  | @ -268,10 +265,8 @@ export default defineComponent({ | |||
| 			this.$forceUpdate(); | ||||
| 
 | ||||
| 			// サウンドを再生する | ||||
| 			if (this.$store.state.device.enableSounds && x.color != this.myColor) { | ||||
| 				const sound = new Audio(`${url}/assets/reversi-put-you.mp3`); | ||||
| 				sound.volume = this.$store.state.device.soundVolume; | ||||
| 				sound.play(); | ||||
| 			if (x.color !== this.myColor) { | ||||
| 				sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite'); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -75,14 +75,25 @@ import MkButton from '@/components/ui/button.vue'; | |||
| import MkInfo from '@/components/ui/info.vue'; | ||||
| import MkInput from '@/components/ui/input.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'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormBase, | ||||
| 		MkButton, MkInfo, MkInput, MkSwitch | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			INFO: { | ||||
| 				title: this.$t('twoStepAuthentication'), | ||||
| 				icon: faLock | ||||
| 			}, | ||||
| 			data: null, | ||||
| 			supportsCredentials: !!navigator.credentials, | ||||
| 			usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin, | ||||
|  | @ -92,6 +103,7 @@ export default defineComponent({ | |||
| 			faLock | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		register() { | ||||
| 			os.dialog({ | ||||
|  | @ -225,6 +237,7 @@ export default defineComponent({ | |||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		updatePasswordLessLogin() { | ||||
| 			os.api('i/2fa/password-less', { | ||||
| 				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> | ||||
| <div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_content"> | ||||
| 			<MkButton @click="generateToken">{{ $t('generateAccessToken') }}</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<MkA to="/api-console" :behavior="isDesktop ? 'window' : null">API console</MkA> | ||||
| 	</div> | ||||
| </div> | ||||
| <FormBase> | ||||
| 	<FormButton @click="generateToken" primary>{{ $t('generateAccessToken') }}</FormButton> | ||||
| 	<FormLink to="/settings/apps">{{ $t('manageAccessTokens') }}</FormLink> | ||||
| 	<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faKey } from '@fortawesome/free-solid-svg-icons'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/ui/input.vue'; | ||||
| 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 * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, MkInput | ||||
| 		FormBase, | ||||
| 		FormButton, | ||||
| 		FormLink, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<MkPagination :pagination="pagination" class="bfomjevm" ref="list"> | ||||
| <FormBase> | ||||
| 	<FormPagination :pagination="pagination" ref="list"> | ||||
| 		<template #empty> | ||||
| 			<div class="_fullinfo"> | ||||
| 				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/> | ||||
|  | @ -8,8 +8,8 @@ | |||
| 			</div> | ||||
| 		</template> | ||||
| 		<template #default="{items}"> | ||||
| 			<div class="token _panel" v-for="token in items" :key="token.id"> | ||||
| 				<img class="icon" :src="token.iconUrl" alt=""/> | ||||
| 			<div class="_formPanel bfomjevm" v-for="token in items" :key="token.id"> | ||||
| 				<img class="icon" :src="token.iconUrl" alt="" v-if="token.iconUrl"/> | ||||
| 				<div class="body"> | ||||
| 					<div class="name">{{ token.name }}</div> | ||||
| 					<div class="description">{{ token.description }}</div> | ||||
|  | @ -33,21 +33,29 @@ | |||
| 				</div> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 	</MkPagination> | ||||
| </div> | ||||
| 	</FormPagination> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| 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'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkPagination | ||||
| 		FormBase, | ||||
| 		FormPagination, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			INFO: { | ||||
|  | @ -65,6 +73,10 @@ export default defineComponent({ | |||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this.INFO); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		revoke(token) { | ||||
| 			os.api('i/revoke-token', { tokenId: token.id }).then(() => { | ||||
|  | @ -77,26 +89,24 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .bfomjevm { | ||||
| 	> .token { | ||||
| 		display: flex; | ||||
| 		padding: 16px; | ||||
| 	display: flex; | ||||
| 	padding: 16px; | ||||
| 
 | ||||
| 		> .icon { | ||||
| 			display: block; | ||||
| 			flex-shrink: 0; | ||||
| 			margin: 0 12px 0 0; | ||||
| 			width: 50px; | ||||
| 			height: 50px; | ||||
| 			border-radius: 8px; | ||||
| 		} | ||||
| 	> .icon { | ||||
| 		display: block; | ||||
| 		flex-shrink: 0; | ||||
| 		margin: 0 12px 0 0; | ||||
| 		width: 50px; | ||||
| 		height: 50px; | ||||
| 		border-radius: 8px; | ||||
| 	} | ||||
| 
 | ||||
| 		> .body { | ||||
| 			width: calc(100% - 62px); | ||||
| 			position: relative; | ||||
| 	> .body { | ||||
| 		width: calc(100% - 62px); | ||||
| 		position: relative; | ||||
| 
 | ||||
| 			> .name { | ||||
| 				font-weight: bold; | ||||
| 			} | ||||
| 		> .name { | ||||
| 			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> | ||||
| <div class=""> | ||||
| 	<section class="_card _vMargin"> | ||||
| 		<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> | ||||
| <FormBase> | ||||
| 	<FormSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</FormSwitch> | ||||
| 
 | ||||
| 	<section class="_card _vMargin"> | ||||
| 		<div class="_title"><Fa :icon="faCog"/> {{ $t('defaultNavigationBehaviour') }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</MkSwitch> | ||||
| 		</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkRadios v-model="chatOpenBehavior"> | ||||
| 				<template #desc>{{ $t('chatOpenBehavior') }}</template> | ||||
| 				<option value="page">{{ $t('showInPage') }}</option> | ||||
| 				<option value="window">{{ $t('openInWindow') }}</option> | ||||
| 				<option value="popout">{{ $t('popout') }}</option> | ||||
| 			</MkRadios> | ||||
| 		</div> | ||||
| 	</section> | ||||
| 	<FormSelect v-model:value="lang"> | ||||
| 		<template #label>{{ $t('uiLanguage') }}</template> | ||||
| 		<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option> | ||||
| 		<template #caption> | ||||
| 			<i18n-t keypath="i18nInfo" tag="span"> | ||||
| 				<template #link> | ||||
| 					<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink> | ||||
| 				</template> | ||||
| 			</i18n-t> | ||||
| 		</template> | ||||
| 	</FormSelect> | ||||
| 
 | ||||
| 	<section class="_card _vMargin"> | ||||
| 		<div class="_title"><Fa :icon="faCog"/> {{ $t('appearance') }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</MkSwitch> | ||||
| 			<MkSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</MkSwitch> | ||||
| 			<MkSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</MkSwitch> | ||||
| 			<MkSwitch v-model:value="useOsNativeEmojis"> | ||||
| 				{{ $t('useOsNativeEmojis') }} | ||||
| 				<template #desc><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template> | ||||
| 			</MkSwitch> | ||||
| 			<MkRadios v-model="fontSize"> | ||||
| 				<template #desc>{{ $t('fontSize') }}</template> | ||||
| 				<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> | ||||
| 	<FormGroup> | ||||
| 		<template #label>{{ $t('behavior') }}</template> | ||||
| 		<FormSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</FormSwitch> | ||||
| 		<FormSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</FormSwitch> | ||||
| 		<FormSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</FormSwitch> | ||||
| 	</FormGroup> | ||||
| 
 | ||||
| 	<section class="_card _vMargin"> | ||||
| 		<div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</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> | ||||
| 	<FormSelect v-model:value="serverDisconnectedBehavior"> | ||||
| 		<template #label>{{ $t('whenServerDisconnected') }}</template> | ||||
| 		<option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option> | ||||
| 		<option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option> | ||||
| 		<option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option> | ||||
| 	</FormSelect> | ||||
| 
 | ||||
| 	<MkButton @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</MkButton> | ||||
| </div> | ||||
| 	<FormGroup> | ||||
| 		<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> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faImage, faCog, faColumns, faCogs } 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 FormLink from '@/components/form/link.vue'; | ||||
| import FormButton from '@/components/form/button.vue'; | ||||
| import MkLink from '@/components/link.vue'; | ||||
| import { langs } from '@/config'; | ||||
| import { clientDb, set } from '@/db'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		MkSwitch, | ||||
| 		MkSelect, | ||||
| 		MkRadio, | ||||
| 		MkRadios, | ||||
| 		MkRange, | ||||
| 		MkLink, | ||||
| 		FormSwitch, | ||||
| 		FormSelect, | ||||
| 		FormRadios, | ||||
| 		FormBase, | ||||
| 		FormGroup, | ||||
| 		FormLink, | ||||
| 		FormButton, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
|  | @ -167,11 +168,6 @@ export default defineComponent({ | |||
| 			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: { | ||||
| 			get() { return this.$store.state.device.chatOpenBehavior; }, | ||||
| 			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 }); } | ||||
| 		}, | ||||
| 
 | ||||
| 		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: { | ||||
| 			get() { return this.$store.state.device.enableInfiniteScroll; }, | ||||
| 			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: { | ||||
|  |  | |||
|  | @ -1,35 +1,36 @@ | |||
| <template> | ||||
| <div class="vvcocwet" :class="{ wide: !narrow }" ref="el"> | ||||
| 	<div class="nav" v-if="!narrow || page == null"> | ||||
| 		<div class="menu"> | ||||
| 			<div class="label">{{ $t('basicSettings') }}</div> | ||||
| 			<MkA class="item" :class="{ active: page === 'profile' }" replace to="/settings/profile"><Fa :icon="faUser" fixed-width class="icon"/>{{ $t('profile') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'privacy' }" replace to="/settings/privacy"><Fa :icon="faLockOpen" fixed-width class="icon"/>{{ $t('privacy') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'reaction' }" replace to="/settings/reaction"><Fa :icon="faLaugh" fixed-width class="icon"/>{{ $t('reaction') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'notifications' }" replace to="/settings/notifications"><Fa :icon="faBell" fixed-width class="icon"/>{{ $t('notifications') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'integration' }" replace to="/settings/integration"><Fa :icon="faShareAlt" fixed-width class="icon"/>{{ $t('integration') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'security' }" replace to="/settings/security"><Fa :icon="faLock" fixed-width class="icon"/>{{ $t('security') }}</MkA> | ||||
| 		</div> | ||||
| 		<div class="menu"> | ||||
| 			<div class="label">{{ $t('clientSettings') }}</div> | ||||
| 			<MkA class="item" :class="{ active: page === 'general' }" replace to="/settings/general"><Fa :icon="faCogs" fixed-width class="icon"/>{{ $t('general') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'theme' }" replace to="/settings/theme"><Fa :icon="faPalette" fixed-width class="icon"/>{{ $t('theme') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'sidebar' }" replace to="/settings/sidebar"><Fa :icon="faListUl" fixed-width class="icon"/>{{ $t('sidebar') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'sounds' }" replace to="/settings/sounds"><Fa :icon="faMusic" fixed-width class="icon"/>{{ $t('sounds') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'plugins' }" replace to="/settings/plugins"><Fa :icon="faPlug" fixed-width class="icon"/>{{ $t('plugins') }}</MkA> | ||||
| 		</div> | ||||
| 		<div class="menu"> | ||||
| 			<div class="label">{{ $t('otherSettings') }}</div> | ||||
| 			<MkA class="item" :class="{ active: page === 'import-export' }" replace to="/settings/import-export"><Fa :icon="faBoxes" fixed-width class="icon"/>{{ $t('importAndExport') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'mute-block' }" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'word-mute' }" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'api' }" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</MkA> | ||||
| 			<MkA class="item" :class="{ active: page === 'other' }" replace to="/settings/other"><Fa :icon="faEllipsisH" fixed-width class="icon"/>{{ $t('other') }}</MkA> | ||||
| 		</div> | ||||
| 		<div class="menu"> | ||||
| 			<button class="_button item" @click="logout">{{ $t('logout') }}</button> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<FormBase class="nav" v-if="!narrow || page == null" :force-wide="!narrow"> | ||||
| 		<FormGroup> | ||||
| 			<template #label>{{ $t('basicSettings') }}</template> | ||||
| 			<FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><Fa :icon="faUser"/></template>{{ $t('profile') }}</FormLink> | ||||
| 			<FormLink :active="page === 'privacy'" replace to="/settings/privacy"><template #icon><Fa :icon="faLockOpen"/></template>{{ $t('privacy') }}</FormLink> | ||||
| 			<FormLink :active="page === 'reaction'" replace to="/settings/reaction"><template #icon><Fa :icon="faLaugh"/></template>{{ $t('reaction') }}</FormLink> | ||||
| 			<FormLink :active="page === 'notifications'" replace to="/settings/notifications"><template #icon><Fa :icon="faBell"/></template>{{ $t('notifications') }}</FormLink> | ||||
| 			<FormLink :active="page === 'email'" replace to="/settings/email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('email') }}</FormLink> | ||||
| 			<FormLink :active="page === 'integration'" replace to="/settings/integration"><template #icon><Fa :icon="faShareAlt"/></template>{{ $t('integration') }}</FormLink> | ||||
| 			<FormLink :active="page === 'security'" replace to="/settings/security"><template #icon><Fa :icon="faLock"/></template>{{ $t('security') }}</FormLink> | ||||
| 		</FormGroup> | ||||
| 		<FormGroup> | ||||
| 			<template #label>{{ $t('clientSettings') }}</template> | ||||
| 			<FormLink :active="page === 'general'" replace to="/settings/general"><template #icon><Fa :icon="faCogs"/></template>{{ $t('general') }}</FormLink> | ||||
| 			<FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><Fa :icon="faPalette"/></template>{{ $t('theme') }}</FormLink> | ||||
| 			<FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><Fa :icon="faListUl"/></template>{{ $t('sidebar') }}</FormLink> | ||||
| 			<FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><Fa :icon="faMusic"/></template>{{ $t('sounds') }}</FormLink> | ||||
| 			<FormLink :active="page === 'plugins'" replace to="/settings/plugins"><template #icon><Fa :icon="faPlug"/></template>{{ $t('plugins') }}</FormLink> | ||||
| 		</FormGroup> | ||||
| 		<FormGroup> | ||||
| 			<template #label>{{ $t('otherSettings') }}</template> | ||||
| 			<FormLink :active="page === 'import-export'" replace to="/settings/import-export"><template #icon><Fa :icon="faBoxes"/></template>{{ $t('importAndExport') }}</FormLink> | ||||
| 			<FormLink :active="page === 'mute-block'" replace to="/settings/mute-block"><template #icon><Fa :icon="faBan"/></template>{{ $t('muteAndBlock') }}</FormLink> | ||||
| 			<FormLink :active="page === 'word-mute'" replace to="/settings/word-mute"><template #icon><Fa :icon="faCommentSlash"/></template>{{ $t('wordMute') }}</FormLink> | ||||
| 			<FormLink :active="page === 'api'" replace to="/settings/api"><template #icon><Fa :icon="faKey"/></template>API</FormLink> | ||||
| 			<FormLink :active="page === 'other'" replace to="/settings/other"><template #icon><Fa :icon="faEllipsisH"/></template>{{ $t('other') }}</FormLink> | ||||
| 		</FormGroup> | ||||
| 		<FormGroup> | ||||
| 			<FormButton @click="logout" danger>{{ $t('logout') }}</FormButton> | ||||
| 		</FormGroup> | ||||
| 	</FormBase> | ||||
| 	<div class="main"> | ||||
| 		<component :is="component" @info="onInfo"/> | ||||
| 	</div> | ||||
|  | @ -37,13 +38,25 @@ | |||
| </template> | ||||
| 
 | ||||
| <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 { faLaugh, faBell } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { faLaugh, faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { store } from '@/store'; | ||||
| 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({ | ||||
| 	components: { | ||||
| 		FormBase, | ||||
| 		FormLink, | ||||
| 		FormGroup, | ||||
| 		FormButton, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		page: { | ||||
| 			type: String, | ||||
|  | @ -72,21 +85,35 @@ export default defineComponent({ | |||
| 				case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue')); | ||||
| 				case 'integration': return defineAsyncComponent(() => import('./integration.vue')); | ||||
| 				case 'security': return defineAsyncComponent(() => import('./security.vue')); | ||||
| 				case '2fa': return defineAsyncComponent(() => import('./2fa.vue')); | ||||
| 				case 'api': return defineAsyncComponent(() => import('./api.vue')); | ||||
| 				case 'apps': return defineAsyncComponent(() => import('./apps.vue')); | ||||
| 				case 'other': return defineAsyncComponent(() => import('./other.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/install': return defineAsyncComponent(() => import('./theme.install.vue')); | ||||
| 				case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue')); | ||||
| 				case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue')); | ||||
| 				case 'sounds': return defineAsyncComponent(() => import('./sounds.vue')); | ||||
| 				case 'deck': return defineAsyncComponent(() => import('./deck.vue')); | ||||
| 				case 'plugins': return defineAsyncComponent(() => import('./plugins.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')); | ||||
| 				default: return null; | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		watch(component, () => { | ||||
| 			nextTick(() => { | ||||
| 				scroll(el.value, 0); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			narrow.value = el.value.offsetWidth < 650; | ||||
| 			narrow.value = el.value.offsetWidth < 1025; | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
|  | @ -100,7 +127,7 @@ export default defineComponent({ | |||
| 				store.dispatch('logout'); | ||||
| 				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> | ||||
| .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 { | ||||
| 		display: flex; | ||||
| 		max-width: 1100px; | ||||
| 		margin: 0 auto; | ||||
| 
 | ||||
| 		> .nav { | ||||
| 			width: 30%; | ||||
| 			max-width: 300px; | ||||
| 			font-size: 0.95em; | ||||
| 			border-right: solid 1px var(--divider); | ||||
| 			width: 32%; | ||||
| 			box-sizing: border-box; | ||||
| 			border-right: solid 0.5px var(--divider); | ||||
| 		} | ||||
| 
 | ||||
| 		> .main { | ||||
| 			flex: 1; | ||||
| 			padding: 32px; | ||||
| 			--baseContentWidth: 100%; | ||||
| 
 | ||||
| 			::v-deep(._section) { | ||||
|  |  | |||
|  | @ -1,29 +1,31 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div class="_section"> | ||||
| 		<MkButton full primary @click="configure"><Fa :icon="faCog"/> {{ $t('notificationSetting') }}</MkButton> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<MkButton full @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</MkButton> | ||||
| 		<MkButton full @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</MkButton> | ||||
| 		<MkButton full @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</MkButton> | ||||
| 	</div> | ||||
| </div> | ||||
| <FormBase> | ||||
| 	<FormLink @click="configure">{{ $t('notificationSetting') }}</FormLink> | ||||
| 	<FormGroup> | ||||
| 		<FormButton @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</FormButton> | ||||
| 		<FormButton @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</FormButton> | ||||
| 		<FormButton @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</FormButton> | ||||
| 	</FormGroup> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faCog } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faBell } from '@fortawesome/free-regular-svg-icons'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkSwitch from '@/components/ui/switch.vue'; | ||||
| 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 { notificationTypes } from '../../../types'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		MkSwitch, | ||||
| 		FormBase, | ||||
| 		FormLink, | ||||
| 		FormButton, | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
|  |  | |||
|  | @ -1,40 +1,43 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div class="_section"> | ||||
| 		<div class="_card"> | ||||
| 			<div class="_content"> | ||||
| 				<MkSwitch v-model:value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote"> | ||||
| 					{{ $t('showFeaturedNotesInTimeline') }} | ||||
| 				</MkSwitch> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<MkSwitch v-model:value="debug" @update:value="changeDebug"> | ||||
| <FormBase> | ||||
| 	<FormSwitch :value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote"> | ||||
| 		{{ $t('showFeaturedNotesInTimeline') }} | ||||
| 	</FormSwitch> | ||||
| 
 | ||||
| 	<FormLink to="/settings/account-info">{{ $t('accountInfo') }}</FormLink> | ||||
| 
 | ||||
| 	<FormGroup> | ||||
| 		<FormSwitch v-model:value="debug" @update:value="changeDebug"> | ||||
| 			DEBUG MODE | ||||
| 		</MkSwitch> | ||||
| 		<div v-if="debug"> | ||||
| 			<MkA to="/settings/regedit">RegEdit</MkA> | ||||
| 			<MkButton @click="taskmanager">Task Manager</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| 		</FormSwitch> | ||||
| 		<template v-if="debug"> | ||||
| 			<FormLink to="/settings/regedit">RegEdit</FormLink> | ||||
| 			<FormButton @click="taskmanager">Task Manager</FormButton> | ||||
| 		</template> | ||||
| 	</FormGroup> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineAsyncComponent, defineComponent } from 'vue'; | ||||
| import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'; | ||||
| import MkSelect from '@/components/ui/select.vue'; | ||||
| import MkSwitch from '@/components/ui/switch.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| 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 * as os from '@/os'; | ||||
| import { debug } from '@/config'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkSelect, | ||||
| 		MkSwitch, | ||||
| 		MkButton, | ||||
| 		FormBase, | ||||
| 		FormSelect, | ||||
| 		FormSwitch, | ||||
| 		FormButton, | ||||
| 		FormLink, | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
|  |  | |||
|  | @ -1,36 +1,43 @@ | |||
| <template> | ||||
| <div class="_section"> | ||||
| 	<div class="_card"> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</MkSwitch> | ||||
| 			<MkSwitch v-model:value="autoAcceptFollowed" v-if="isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</MkSwitch> | ||||
| 		</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</MkSwitch> | ||||
| 			<MkSelect v-model:value="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility"> | ||||
| 				<template #label>{{ $t('defaultNoteVisibility') }}</template> | ||||
| 				<option value="public">{{ $t('_visibility.public') }}</option> | ||||
| 				<option value="home">{{ $t('_visibility.home') }}</option> | ||||
| 				<option value="followers">{{ $t('_visibility.followers') }}</option> | ||||
| 				<option value="specified">{{ $t('_visibility.specified') }}</option> | ||||
| 			</MkSelect> | ||||
| 			<MkSwitch v-model:value="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</MkSwitch> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| <FormBase> | ||||
| 	<FormGroup> | ||||
| 		<FormSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</FormSwitch> | ||||
| 		<FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</FormSwitch> | ||||
| 		<template #caption>{{ $t('lockedAccountInfo') }}</template> | ||||
| 	</FormGroup> | ||||
| 	<FormSwitch v-model:value="noCrawle" @update:value="save()"> | ||||
| 		{{ $t('noCrawle') }} | ||||
| 		<template #desc>{{ $t('noCrawleDescription') }}</template> | ||||
| 	</FormSwitch> | ||||
| 	<FormSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</FormSwitch> | ||||
| 	<FormGroup v-if="!rememberNoteVisibility"> | ||||
| 		<template #label>{{ $t('defaultNoteVisibility') }}</template> | ||||
| 		<FormSelect v-model:value="defaultNoteVisibility"> | ||||
| 			<option value="public">{{ $t('_visibility.public') }}</option> | ||||
| 			<option value="home">{{ $t('_visibility.home') }}</option> | ||||
| 			<option value="followers">{{ $t('_visibility.followers') }}</option> | ||||
| 			<option value="specified">{{ $t('_visibility.specified') }}</option> | ||||
| 		</FormSelect> | ||||
| 		<FormSwitch v-model:value="defaultNoteLocalOnly">{{ $t('_visibility.localOnly') }}</FormSwitch> | ||||
| 	</FormGroup> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faLockOpen } from '@fortawesome/free-solid-svg-icons'; | ||||
| import MkSelect from '@/components/ui/select.vue'; | ||||
| import MkSwitch from '@/components/ui/switch.vue'; | ||||
| import FormSwitch from '@/components/form/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'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkSelect, | ||||
| 		MkSwitch, | ||||
| 		FormBase, | ||||
| 		FormSelect, | ||||
| 		FormGroup, | ||||
| 		FormSwitch, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
|  | @ -43,6 +50,7 @@ export default defineComponent({ | |||
| 			}, | ||||
| 			isLocked: false, | ||||
| 			autoAcceptFollowed: false, | ||||
| 			noCrawle: false, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -66,6 +74,7 @@ export default defineComponent({ | |||
| 	created() { | ||||
| 		this.isLocked = this.$store.state.i.isLocked; | ||||
| 		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed; | ||||
| 		this.noCrawle = this.$store.state.i.noCrawle; | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
|  | @ -77,6 +86,7 @@ export default defineComponent({ | |||
| 			os.api('i/update', { | ||||
| 				isLocked: !!this.isLocked, | ||||
| 				autoAcceptFollowed: !!this.autoAcceptFollowed, | ||||
| 				noCrawle: !!this.noCrawle, | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,79 +1,67 @@ | |||
| <template> | ||||
| <div class="_section"> | ||||
| 	<div class="llvierxe _card"> | ||||
| 		<div class="_title"><Fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div> | ||||
| 		<div class="_content"> | ||||
| 			<div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner"> | ||||
| 				<MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/> | ||||
| 			</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> | ||||
| <FormBase class="llvierxe"> | ||||
| 	<div class="header _formItem" :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> | ||||
| </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> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faSave } from '@fortawesome/free-regular-svg-icons'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkInput from '@/components/ui/input.vue'; | ||||
| import MkTextarea from '@/components/ui/textarea.vue'; | ||||
| import MkSwitch from '@/components/ui/switch.vue'; | ||||
| import FormButton from '@/components/form/button.vue'; | ||||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormTextarea from '@/components/form/textarea.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 { selectFile } from '@/scripts/select-file'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		MkInput, | ||||
| 		MkTextarea, | ||||
| 		MkSwitch, | ||||
| 		FormButton, | ||||
| 		FormInput, | ||||
| 		FormTextarea, | ||||
| 		FormSwitch, | ||||
| 		FormTuple, | ||||
| 		FormBase, | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 	 | ||||
| 	emits: ['info'], | ||||
|  | @ -101,6 +89,7 @@ export default defineComponent({ | |||
| 			bannerId: null, | ||||
| 			isBot: false, | ||||
| 			isCat: false, | ||||
| 			alwaysMarkNsfw: false, | ||||
| 			saving: false, | ||||
| 			faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake | ||||
| 		} | ||||
|  | @ -115,6 +104,7 @@ export default defineComponent({ | |||
| 		this.bannerId = this.$store.state.i.bannerId; | ||||
| 		this.isBot = this.$store.state.i.isBot; | ||||
| 		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.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 = [ | ||||
| 				{ name: this.fieldName0, value: this.fieldValue0 }, | ||||
| 				{ name: this.fieldName1, value: this.fieldValue1 }, | ||||
|  | @ -155,6 +198,19 @@ export default defineComponent({ | |||
| 				{ 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; | ||||
| 
 | ||||
| 			os.api('i/update', { | ||||
|  | @ -162,9 +218,9 @@ export default defineComponent({ | |||
| 				description: this.description || null, | ||||
| 				location: this.location || null, | ||||
| 				birthday: this.birthday || null, | ||||
| 				fields, | ||||
| 				isBot: !!this.isBot, | ||||
| 				isCat: !!this.isCat, | ||||
| 				alwaysMarkNsfw: !!this.alwaysMarkNsfw, | ||||
| 			}).then(i => { | ||||
| 				this.saving = false; | ||||
| 				this.$store.state.i.avatarId = i.avatarId; | ||||
|  | @ -189,41 +245,29 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .llvierxe { | ||||
| 	> ._content { | ||||
| 		> .header { | ||||
| 			position: relative; | ||||
| 			height: 150px; | ||||
| 			overflow: hidden; | ||||
| 			background-size: cover; | ||||
| 			background-position: center; | ||||
| 			border-radius: 5px; | ||||
| 			border: solid 1px var(--divider); | ||||
| 			box-sizing: border-box; | ||||
| 	> .header { | ||||
| 		position: relative; | ||||
| 		height: 150px; | ||||
| 		overflow: hidden; | ||||
| 		background-size: cover; | ||||
| 		background-position: center; | ||||
| 		border-radius: 5px; | ||||
| 		border: solid 1px var(--divider); | ||||
| 		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; | ||||
| 
 | ||||
| 			> .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; | ||||
| 				} | ||||
| 			} | ||||
| 			box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -1,9 +1,8 @@ | |||
| <template> | ||||
| <div class="_section"> | ||||
| 	<div class="_card"> | ||||
| 		<div class="_title"><Fa :icon="faLaugh"/> {{ $t('reaction') }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<div class="_caption" style="padding: 0 8px 8px 8px;">{{ $t('reactionSettingDescription') }}</div> | ||||
| <FormBase> | ||||
| 	<div class="_formItem"> | ||||
| 		<div class="_formLabel">{{ $t('reactionSettingDescription') }}</div> | ||||
| 		<div class="_formPanel"> | ||||
| 			<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)"> | ||||
| 					<MkEmoji :emoji="reaction" :normal="true"/> | ||||
|  | @ -12,26 +11,25 @@ | |||
| 					<button>a</button> | ||||
| 				</template> | ||||
| 			</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 class="_formCaption">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></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> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -39,20 +37,19 @@ import { defineComponent } from 'vue'; | |||
| import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons'; | ||||
| import { faUndo } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { VueDraggableNext } from 'vue-draggable-next'; | ||||
| import MkInput from '@/components/ui/input.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkSwitch from '@/components/ui/switch.vue'; | ||||
| import MkRadios from '@/components/ui/radios.vue'; | ||||
| import { emojiRegexWithCustom } from '../../../misc/emoji-regex'; | ||||
| import FormInput from '@/components/form/input.vue'; | ||||
| import FormRadios from '@/components/form/radios.vue'; | ||||
| import FormBase from '@/components/form/base.vue'; | ||||
| import FormButton from '@/components/form/button.vue'; | ||||
| import { defaultSettings } from '@/store'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkInput, | ||||
| 		MkButton, | ||||
| 		MkSwitch, | ||||
| 		MkRadios, | ||||
| 		FormInput, | ||||
| 		FormButton, | ||||
| 		FormBase, | ||||
| 		FormRadios, | ||||
| 		XDraggable: VueDraggableNext, | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -62,7 +59,11 @@ export default defineComponent({ | |||
| 		return { | ||||
| 			INFO: { | ||||
| 				title: this.$t('reaction'), | ||||
| 				icon: faLaugh | ||||
| 				icon: faLaugh, | ||||
| 				action: { | ||||
| 					icon: faEye, | ||||
| 					handler: this.preview | ||||
| 				} | ||||
| 			}, | ||||
| 			reactions: JSON.parse(JSON.stringify(this.$store.state.settings.reactions)), | ||||
| 			faLaugh, faSave, faEye, faUndo | ||||
|  | @ -144,8 +145,6 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .zoaiodol { | ||||
| 	border: solid 1px var(--divider); | ||||
| 	border-radius: var(--radius); | ||||
| 	padding: 16px; | ||||
| 
 | ||||
| 	> .item { | ||||
|  |  | |||
|  | @ -1,29 +1,45 @@ | |||
| <template> | ||||
| <div> | ||||
| 	<div class="_section"> | ||||
| 		<X2fa/> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<MkButton primary @click="change()" full>{{ $t('changePassword') }}</MkButton> | ||||
| 	</div> | ||||
| 	<div class="_section"> | ||||
| 		<MkButton class="_vMargin" primary @click="regenerateToken" full><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</MkButton> | ||||
| 		<div class="_caption _vMargin" style="padding: 0 6px;">{{ $t('regenerateLoginTokenDescription') }}</div> | ||||
| 	</div> | ||||
| </div> | ||||
| <FormBase> | ||||
| 	<X2fa/> | ||||
| 	<FormLink to="/settings/2fa"><template #icon><Fa :icon="faMobileAlt"/></template>{{ $t('twoStepAuthentication') }}</FormLink> | ||||
| 	<FormButton primary @click="change()">{{ $t('changePassword') }}</FormButton> | ||||
| 	<FormPagination :pagination="pagination"> | ||||
| 		<template #label>{{ $t('signinHistory') }}</template> | ||||
| 		<template #default="{items}"> | ||||
| 			<div class="_formPanel timnmucd" v-for="item in items" :key="item.id"> | ||||
| 				<header> | ||||
| 					<Fa class="icon succ" :icon="faCheck" v-if="item.success"/> | ||||
| 					<Fa class="icon fail" :icon="faTimesCircle" v-else/> | ||||
| 					<code class="ip _monospace">{{ item.ip }}</code> | ||||
| 					<MkTime :time="item.createdAt" class="time"/> | ||||
| 				</header> | ||||
| 			</div> | ||||
| 		</template> | ||||
| 	</FormPagination> | ||||
| 	<FormGroup> | ||||
| 		<FormButton danger @click="regenerateToken"><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</FormButton> | ||||
| 		<template #caption>{{ $t('regenerateLoginTokenDescription') }}</template> | ||||
| 	</FormGroup> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faLock, faSyncAlt } from '@fortawesome/free-solid-svg-icons'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import X2fa from './security.2fa.vue'; | ||||
| import { faCheck, faTimesCircle, faLock, faSyncAlt, faMobileAlt } from '@fortawesome/free-solid-svg-icons'; | ||||
| import FormBase from '@/components/form/base.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'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		X2fa, | ||||
| 		FormBase, | ||||
| 		FormLink, | ||||
| 		FormButton, | ||||
| 		FormPagination, | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 	 | ||||
| 	emits: ['info'], | ||||
|  | @ -34,7 +50,11 @@ export default defineComponent({ | |||
| 				title: this.$t('security'), | ||||
| 				icon: faLock | ||||
| 			}, | ||||
| 			faLock, faSyncAlt | ||||
| 			pagination: { | ||||
| 				endpoint: 'i/signin-history', | ||||
| 				limit: 5, | ||||
| 			}, | ||||
| 			faLock, faSyncAlt, faCheck, faTimesCircle, faMobileAlt, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -98,3 +118,32 @@ export default defineComponent({ | |||
| 	} | ||||
| }); | ||||
| </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> | ||||
| <div class="_section"> | ||||
| 	<div class="_card"> | ||||
| 		<div class="_content"> | ||||
| 			<MkTextarea v-model:value="items" tall> | ||||
| 				<span>{{ $t('sidebar') }}</span> | ||||
| 				<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template> | ||||
| 			</MkTextarea> | ||||
| 		</div> | ||||
| 		<div class="_content"> | ||||
| 			<div>{{ $t('display') }}</div> | ||||
| 			<MkRadio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</MkRadio> | ||||
| 			<MkRadio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</MkRadio> | ||||
| 			<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> | ||||
| 		</div> | ||||
| 		<div class="_footer"> | ||||
| 			<MkButton inline @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton> | ||||
| 			<MkButton inline @click="reset()"><Fa :icon="faRedo"/> {{ $t('default') }}</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| <FormBase> | ||||
| 	<FormTextarea v-model:value="items" tall> | ||||
| 		<span>{{ $t('sidebar') }}</span> | ||||
| 		<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template> | ||||
| 	</FormTextarea> | ||||
| 
 | ||||
| 	<FormRadios v-model="sidebarDisplay"> | ||||
| 		<template #desc>{{ $t('display') }}</template> | ||||
| 		<option value="full">{{ $t('_sidebar.full') }}</option> | ||||
| 		<option value="icon">{{ $t('_sidebar.icon') }}</option> | ||||
| 		<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると、別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 --> | ||||
| 	</FormRadios> | ||||
| 
 | ||||
| 	<FormButton @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton> | ||||
| 	<FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkTextarea from '@/components/ui/textarea.vue'; | ||||
| import MkRadio from '@/components/ui/radio.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import FormTextarea from '@/components/form/textarea.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 * as os from '@/os'; | ||||
| import { sidebarDef } from '@/sidebar'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		MkTextarea, | ||||
| 		MkRadio, | ||||
| 		FormBase, | ||||
| 		FormButton, | ||||
| 		FormTextarea, | ||||
| 		FormRadios, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
|  | @ -102,7 +102,3 @@ export default defineComponent({ | |||
| 	}, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| 
 | ||||
| </style> | ||||
|  |  | |||
|  | @ -1,62 +1,35 @@ | |||
| <template> | ||||
| <div class="_section"> | ||||
| 	<div class="_card"> | ||||
| 		<div class="_title"><Fa :icon="faMusic"/> {{ $t('sounds') }}</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkRange v-model:value="sfxVolume" :min="0" :max="1" :step="0.1"> | ||||
| 				<Fa slot="icon" :icon="volumeIcon"/> | ||||
| 				<span slot="title">{{ $t('volume') }}</span> | ||||
| 			</MkRange> | ||||
| 		</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSelect v-model:value="sfxNote"> | ||||
| 				<template #label>{{ $t('_sfx.note') }}</template> | ||||
| 				<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option> | ||||
| 				<template #text><button class="_textButton" @click="listen(sfxNote)" v-if="sfxNote"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template> | ||||
| 			</MkSelect> | ||||
| 			<MkSelect v-model:value="sfxNoteMy"> | ||||
| 				<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> | ||||
| <FormBase> | ||||
| 	<FormRange v-model:value="masterVolume" :min="0" :max="1" :step="0.05"> | ||||
| 		<template #label><Fa :icon="volumeIcon" :key="volumeIcon"/> {{ $t('masterVolume') }}</template> | ||||
| 	</FormRange> | ||||
| 
 | ||||
| 	<FormGroup> | ||||
| 		<template #label>{{ $t('sounds') }}</template> | ||||
| 		<FormButton v-for="type in Object.keys(sounds)" :key="type" :center="false" @click="edit(type)"> | ||||
| 			{{ $t('_sfx.' + type) }} | ||||
| 			<template #suffix>{{ sounds[type].type || $t('none') }}</template> | ||||
| 			<template #suffixIcon><Fa :icon="faChevronDown"/></template> | ||||
| 		</FormButton> | ||||
| 	</FormGroup> | ||||
| 
 | ||||
| 	<FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons'; | ||||
| import MkSelect from '@/components/ui/select.vue'; | ||||
| import MkRange from '@/components/ui/range.vue'; | ||||
| import { faMusic, faPlay, faVolumeUp, faVolumeMute, faChevronDown, faRedo } from '@fortawesome/free-solid-svg-icons'; | ||||
| import FormRange from '@/components/form/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 { device, defaultDeviceSettings } from '@/cold-storage'; | ||||
| import { playFile } from '@/scripts/sound'; | ||||
| 
 | ||||
| const sounds = [ | ||||
| const soundsTypes = [ | ||||
| 	null, | ||||
| 	'syuilo/up', | ||||
| 	'syuilo/down', | ||||
|  | @ -73,6 +46,8 @@ const sounds = [ | |||
| 	'syuilo/square-pico', | ||||
| 	'syuilo/reverved', | ||||
| 	'syuilo/ryukyu', | ||||
| 	'syuilo/kick', | ||||
| 	'syuilo/snare', | ||||
| 	'aisha/1', | ||||
| 	'aisha/2', | ||||
| 	'aisha/3', | ||||
|  | @ -82,71 +57,98 @@ const sounds = [ | |||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkSelect, | ||||
| 		MkRange, | ||||
| 		FormSelect, | ||||
| 		FormButton, | ||||
| 		FormBase, | ||||
| 		FormRange, | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			sounds, | ||||
| 			faMusic, faPlay, faVolumeUp, faVolumeMute, | ||||
| 			INFO: { | ||||
| 				title: this.$t('sounds'), | ||||
| 				icon: faMusic | ||||
| 			}, | ||||
| 			sounds: {}, | ||||
| 			faMusic, faPlay, faVolumeUp, faVolumeMute, faChevronDown, faRedo, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		sfxVolume: { | ||||
| 			get() { return this.$store.state.device.sfxVolume; }, | ||||
| 			set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); } | ||||
| 		masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す | ||||
| 			get() { return device.get('sound_masterVolume'); }, | ||||
| 			set(value) { device.set('sound_masterVolume', value); } | ||||
| 		}, | ||||
| 
 | ||||
| 		sfxNote: { | ||||
| 			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; | ||||
| 			} | ||||
| 		volumeIcon() { | ||||
| 			return this.masterVolume === 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: { | ||||
| 		listen(sound) { | ||||
| 			const audio = new Audio(`/assets/sounds/${sound}.mp3`); | ||||
| 			audio.volume = this.$store.state.device.sfxVolume; | ||||
| 			audio.play(); | ||||
| 		async edit(type) { | ||||
| 			const { canceled, result } = await os.form(this.$t('_sfx.' + type), { | ||||
| 				type: { | ||||
| 					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> | ||||
|  |  | |||
							
								
								
									
										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> | ||||
| <div class=""> | ||||
| 	<div class="rfqxtzch _card _vMargin"> | ||||
| 		<div class="_content"> | ||||
| <FormBase> | ||||
| 	<FormSelect v-model:value="lightTheme" v-if="!darkMode"> | ||||
| 		<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="toggleWrapper"> | ||||
| 					<input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/> | ||||
|  | @ -23,85 +42,47 @@ | |||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_content"> | ||||
| 			<MkSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</MkSwitch> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="_card _vMargin"> | ||||
| 		<div class="_content"> | ||||
| 			<MkSelect v-model:value="lightTheme"> | ||||
| 				<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> | ||||
| 			</MkSelect> | ||||
| 			<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> | ||||
| 		<FormSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</FormSwitch> | ||||
| 	</FormGroup> | ||||
| 
 | ||||
| 	<FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</FormButton> | ||||
| 	<FormButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</FormButton> | ||||
| 
 | ||||
| 	<FormGroup> | ||||
| 		<FormLink to="https://assets.msky.cafe/theme/list" external>{{ $t('_theme.explore') }}</FormLink> | ||||
| 		<FormLink to="/theme-editor">{{ $t('_theme.make') }}</FormLink> | ||||
| 	</FormGroup> | ||||
| 
 | ||||
| 	<FormLink to="/settings/theme/install"><template #icon><Fa :icon="faDownload"/></template>{{ $t('_theme.install') }}</FormLink> | ||||
| 
 | ||||
| 	<FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $t('_theme.manage') }}</FormLink> | ||||
| </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 MkButton from '@/components/ui/button.vue'; | ||||
| import MkSelect from '@/components/ui/select.vue'; | ||||
| import MkSwitch from '@/components/ui/switch.vue'; | ||||
| import MkTextarea from '@/components/ui/textarea.vue'; | ||||
| import { Theme, builtinThemes, applyTheme, validateTheme } from '@/scripts/theme'; | ||||
| 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 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 { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; | ||||
| import copyToClipboard from '@/scripts/copy-to-clipboard'; | ||||
| import * as os from '@/os'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		MkSelect, | ||||
| 		MkSwitch, | ||||
| 		MkTextarea, | ||||
| 		FormSwitch, | ||||
| 		FormSelect, | ||||
| 		FormRadios, | ||||
| 		FormBase, | ||||
| 		FormGroup, | ||||
| 		FormLink, | ||||
| 		FormButton, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
|  | @ -113,8 +94,6 @@ export default defineComponent({ | |||
| 				icon: faPalette | ||||
| 			}, | ||||
| 			builtinThemes, | ||||
| 			installThemeCode: null, | ||||
| 			selectedThemeId: null, | ||||
| 			wallpaper: localStorage.getItem('wallpaper'), | ||||
| 			faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye | ||||
| 		} | ||||
|  | @ -156,16 +135,6 @@ export default defineComponent({ | |||
| 			get() { return this.$store.state.device.syncDeviceDarkMode; }, | ||||
| 			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: { | ||||
|  | @ -207,292 +176,230 @@ export default defineComponent({ | |||
| 				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> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .rfqxtzch { | ||||
| 	> ._content { | ||||
| 		> .darkMode { | ||||
| 			position: relative; | ||||
| 			padding: 32px 0; | ||||
| 	padding: 16px; | ||||
| 
 | ||||
| 			&.disabled { | ||||
| 				opacity: 0.7; | ||||
| 	> .darkMode { | ||||
| 		position: relative; | ||||
| 		padding: 32px 0; | ||||
| 
 | ||||
| 				&, * { | ||||
| 					cursor: not-allowed !important; | ||||
| 				} | ||||
| 		&.disabled { | ||||
| 			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; | ||||
| 				top: 50%; | ||||
| 				left: 50%; | ||||
| 				overflow: hidden; | ||||
| 				padding: 0 100px; | ||||
| 				transform: translate3d(-50%, -50%, 0); | ||||
| 				left: -99em; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 				input { | ||||
| 					position: absolute; | ||||
| 					left: -99em; | ||||
| 				} | ||||
| 		.toggle { | ||||
| 			cursor: pointer; | ||||
| 			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 { | ||||
| 				cursor: pointer; | ||||
| 				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 { | ||||
| 				left: -70px; | ||||
| 				color: var(--accent); | ||||
| 			} | ||||
| 
 | ||||
| 				> .before, > .after { | ||||
| 					position: absolute; | ||||
| 					top: 15px; | ||||
| 					font-size: 18px; | ||||
| 					transition: color 1s ease; | ||||
| 				} | ||||
| 			> .after { | ||||
| 				right: -68px; | ||||
| 				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 { | ||||
| 				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 { | ||||
| 					left: -70px; | ||||
| 					color: var(--accent); | ||||
| 					color: var(--fg); | ||||
| 				} | ||||
| 
 | ||||
| 				> .after { | ||||
| 					right: -68px; | ||||
| 					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%; | ||||
| 					color: var(--accent); | ||||
| 				} | ||||
| 
 | ||||
| 				.crater--1 { | ||||
| 					top: 18px; | ||||
| 					left: 10px; | ||||
| 				.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); | ||||
| 				} | ||||
| 
 | ||||
| 				.crater--2 { | ||||
| 					top: 28px; | ||||
| 					left: 22px; | ||||
| 					width: 6px; | ||||
| 					height: 6px; | ||||
| 				.star--3 { | ||||
| 					width: 2px; | ||||
| 					height: 2px; | ||||
| 					transform: translate3d(-7px, 0, 0); | ||||
| 				} | ||||
| 
 | ||||
| 				.crater--3 { | ||||
| 					top: 10px; | ||||
| 					left: 25px; | ||||
| 					width: 8px; | ||||
| 					height: 8px; | ||||
| 				.star--4, | ||||
| 				.star--5, | ||||
| 				.star--6 { | ||||
| 					opacity: 1; | ||||
| 					transform: translate3d(0,0,0); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			.star { | ||||
| 				position: absolute; | ||||
| 				background-color: #ffffff; | ||||
| 				transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||
| 				border-radius: 50%; | ||||
| 			} | ||||
| 				.star--4 { | ||||
| 					transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||
| 				} | ||||
| 
 | ||||
| 			.star--1 { | ||||
| 				top: 10px; | ||||
| 				left: 35px; | ||||
| 				z-index: 0; | ||||
| 				width: 30px; | ||||
| 				height: 3px; | ||||
| 			} | ||||
| 				.star--5 { | ||||
| 					transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||
| 				} | ||||
| 
 | ||||
| 			.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 { | ||||
| 						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; | ||||
| 					} | ||||
| 				.star--6 { | ||||
| 					transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -1,47 +1,53 @@ | |||
| <template> | ||||
| <div class="_section"> | ||||
| 	<div class="_card"> | ||||
| 		<MkTab v-model:value="tab"> | ||||
| 			<option value="soft">{{ $t('_wordMute.soft') }}</option> | ||||
| 			<option value="hard">{{ $t('_wordMute.hard') }}</option> | ||||
| 		</MkTab> | ||||
| 		<div class="_content"> | ||||
| <div> | ||||
| 	<MkTab v-model:value="tab"> | ||||
| 		<option value="soft">{{ $t('_wordMute.soft') }}</option> | ||||
| 		<option value="hard">{{ $t('_wordMute.hard') }}</option> | ||||
| 	</MkTab> | ||||
| 	<FormBase> | ||||
| 		<div class="_formItem"> | ||||
| 			<div v-show="tab === 'soft'"> | ||||
| 				<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo> | ||||
| 				<MkTextarea v-model:value="softMutedWords"> | ||||
| 				<FormTextarea v-model:value="softMutedWords"> | ||||
| 					<span>{{ $t('_wordMute.muteWords') }}</span> | ||||
| 					<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template> | ||||
| 				</MkTextarea> | ||||
| 				</FormTextarea> | ||||
| 			</div> | ||||
| 			<div v-show="tab === 'hard'"> | ||||
| 				<MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo> | ||||
| 				<MkTextarea v-model:value="hardMutedWords" style="margin-bottom: 16px;"> | ||||
| 				<FormTextarea v-model:value="hardMutedWords"> | ||||
| 					<span>{{ $t('_wordMute.muteWords') }}</span> | ||||
| 					<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template> | ||||
| 				</MkTextarea> | ||||
| 				<div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div> | ||||
| 				</FormTextarea> | ||||
| 				<FormKeyValueView v-if="hardWordMutedNotesCount != null"> | ||||
| 					<template #key>{{ $t('_wordMute.mutedNotes') }}</template> | ||||
| 					<template #value>{{ number(hardWordMutedNotesCount) }}</template> | ||||
| 				</FormKeyValueView> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="_footer"> | ||||
| 			<MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 		<FormButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</FormButton> | ||||
| 	</FormBase> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkTextarea from '@/components/ui/textarea.vue'; | ||||
| import FormTextarea from '@/components/form/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 MkInfo from '@/components/ui/info.vue'; | ||||
| import * as os from '@/os'; | ||||
| import number from '@/filters/number'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkButton, | ||||
| 		MkTextarea, | ||||
| 		FormBase, | ||||
| 		FormButton, | ||||
| 		FormTextarea, | ||||
| 		FormKeyValueView, | ||||
| 		MkTab, | ||||
| 		MkInfo, | ||||
| 	}, | ||||
|  | @ -97,6 +103,8 @@ export default defineComponent({ | |||
| 			}); | ||||
| 			this.changed = false; | ||||
| 		}, | ||||
| 
 | ||||
| 		number | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <div class="_section"> | ||||
| <div> | ||||
| 	<MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers _content" ref="list"> | ||||
| 		<div class="users"> | ||||
| 			<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> | ||||
| <div> | ||||
| 	<div ref="chart"></div> | ||||
| </div> | ||||
| <MkContainer> | ||||
| 	<template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template> | ||||
| 
 | ||||
| 	<div style="padding: 8px;"> | ||||
| 		<div ref="chart"></div> | ||||
| 	</div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import ApexCharts from 'apexcharts'; | ||||
| import { faChartBar } from '@fortawesome/free-solid-svg-icons'; | ||||
| import * as os from '@/os'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
|  | @ -25,7 +34,8 @@ export default defineComponent({ | |||
| 		return { | ||||
| 			fetching: true, | ||||
| 			data: [], | ||||
| 			peak: null | ||||
| 			peak: null, | ||||
| 			faChartBar, | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
|  |  | |||
|  | @ -1,29 +1,43 @@ | |||
| <template> | ||||
| <div class="ujigsodd"> | ||||
| 	<MkLoading v-if="fetching"/> | ||||
| 	<div class="stream" v-if="!fetching && images.length > 0"> | ||||
| 		<MkA v-for="image in images" | ||||
| 			class="img" | ||||
| 			:style="`background-image: url(${thumbnail(image.file)})`" | ||||
| 			:to="notePage(image.note)" | ||||
| 		></MkA> | ||||
| <MkContainer> | ||||
| 	<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template> | ||||
| 	<div class="ujigsodd"> | ||||
| 		<MkLoading v-if="fetching"/> | ||||
| 		<div class="stream" v-if="!fetching && images.length > 0"> | ||||
| 			<MkA v-for="image in images" | ||||
| 				class="img" | ||||
| 				: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> | ||||
| 	<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p> | ||||
| </div> | ||||
| </MkContainer> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { faImage } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { getStaticImageUrl } from '@/scripts/get-static-image-url'; | ||||
| import notePage from '../../filters/note'; | ||||
| import * as os from '@/os'; | ||||
| import MkContainer from '@/components/ui/container.vue'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	props: ['user'], | ||||
| 	components: { | ||||
| 		MkContainer, | ||||
| 	}, | ||||
| 	props: { | ||||
| 		user: { | ||||
| 			type: Object, | ||||
| 			required: true | ||||
| 		}, | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
| 			fetching: true, | ||||
| 			images: [] | ||||
| 			images: [], | ||||
| 			faImage | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
|  | @ -37,7 +51,7 @@ export default defineComponent({ | |||
| 		os.api('users/notes', { | ||||
| 			userId: this.user.id, | ||||
| 			fileType: image, | ||||
| 			excludeNsfw: !this.$store.state.device.alwaysShowNsfw, | ||||
| 			excludeNsfw: this.$store.state.device.nsfw !== 'ignore', | ||||
| 			limit: 9, | ||||
| 		}).then(notes => { | ||||
| 			for (const note of notes) { | ||||
|  | @ -66,6 +80,8 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .ujigsodd { | ||||
| 	padding: 8px; | ||||
| 
 | ||||
| 	> .stream { | ||||
| 		display: flex; | ||||
| 		justify-content: center; | ||||
|  |  | |||
|  | @ -1,115 +1,113 @@ | |||
| <template> | ||||
| <div class="mk-user-page" v-if="user" v-size="{ max: [500] }"> | ||||
| 	<!-- TODO --> | ||||
| 	<!-- <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> | ||||
| 	<div class="mk-user-page" v-if="user" v-size="{ max: [500] }" :class="{ _section: narrow === false }"> | ||||
| 		<!-- TODO --> | ||||
| 		<!-- <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"> | ||||
| 		<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/> | ||||
| 		<div class="main"> | ||||
| 			<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="banner-container" :style="style"> | ||||
| 				<div class="banner" ref="banner" :style="style"></div> | ||||
| 				<div class="fade"></div> | ||||
| 				<div class="title"> | ||||
| 					<MkUserName class="name" :user="user" :nowrap="true"/> | ||||
| 					<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 class="_content _panel _vMargin" :key="user.id"> | ||||
| 					<div class="banner-container" :style="style"> | ||||
| 						<div class="banner" ref="banner" :style="style"></div> | ||||
| 						<div class="fade"></div> | ||||
| 						<div class="title"> | ||||
| 							<MkUserName class="name" :user="user" :nowrap="true"/> | ||||
| 							<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> | ||||
| 						<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> | ||||
| 				<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> | ||||
| 
 | ||||
| 			<template v-if="page === 'index'"> | ||||
| 				<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> | ||||
| 			<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 v-if="narrow === true" class="_section"> | ||||
| 					<XPhotos class="_content _vMargin" :user="user" :key="user.id"/> | ||||
| 					<XActivity class="_content _vMargin" :user="user" :key="user.id"/> | ||||
| 				</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 :class="{ _section: narrow === true, _vMargin: narrow === false }"> | ||||
| 					<XUserTimeline :user="user" class="_content"/> | ||||
| 				</div> | ||||
| 			</template> | ||||
| 			<XFollowList v-else-if="page === 'following'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="following" :user="user"/> | ||||
| 			<XFollowList v-else-if="page === 'followers'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="followers" :user="user"/> | ||||
| 		</div> | ||||
| 		<div class="side" v-if="narrow === false"> | ||||
| 			<XPhotos class="_vMargin" :user="user" :key="user.id"/> | ||||
| 			<XActivity class="_vMargin" :user="user" :key="user.id"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<template v-if="page === 'index'"> | ||||
| 		<div class="_section"> | ||||
| 			<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 v-else-if="error"> | ||||
| 		<MkError @retry="fetch()"/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -170,6 +168,7 @@ export default defineComponent({ | |||
| 			user: null, | ||||
| 			error: null, | ||||
| 			parallaxAnimationId: null, | ||||
| 			narrow: null, | ||||
| 			faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt | ||||
| 		}; | ||||
| 	}, | ||||
|  | @ -197,6 +196,7 @@ export default defineComponent({ | |||
| 
 | ||||
| 	mounted() { | ||||
| 		window.requestAnimationFrame(this.parallaxLoop); | ||||
| 		this.narrow = this.$el.clientWidth < 1000; | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeUnmount() { | ||||
|  | @ -254,220 +254,234 @@ export default defineComponent({ | |||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .mk-user-page { | ||||
| 	> .punished { | ||||
| 		font-size: 0.8em; | ||||
| 		padding: 16px; | ||||
| 	} | ||||
| 	display: flex; | ||||
| 	max-width: 1050px; | ||||
| 	margin: 0 auto; | ||||
| 	 | ||||
| 	> .profile { | ||||
| 		> ._content { | ||||
| 			position: relative; | ||||
| 			overflow: hidden; | ||||
| 	> .main { | ||||
| 		flex: 1; | ||||
| 
 | ||||
| 			> .banner-container { | ||||
| 		> .punished { | ||||
| 			font-size: 0.8em; | ||||
| 			padding: 16px; | ||||
| 		} | ||||
| 
 | ||||
| 		> .profile { | ||||
| 			> ._content { | ||||
| 				position: relative; | ||||
| 				height: 250px; | ||||
| 				overflow: hidden; | ||||
| 				background-size: cover; | ||||
| 				background-position: center; | ||||
| 				border-radius: 12px; | ||||
| 
 | ||||
| 				> .banner { | ||||
| 					height: 100%; | ||||
| 					background-color: #4c5e6d; | ||||
| 				> .banner-container { | ||||
| 					position: relative; | ||||
| 					height: 250px; | ||||
| 					overflow: hidden; | ||||
| 					background-size: cover; | ||||
| 					background-position: center; | ||||
| 					box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; | ||||
| 					will-change: background-position; | ||||
| 				} | ||||
| 
 | ||||
| 				> .fade { | ||||
| 					position: absolute; | ||||
| 					bottom: 0; | ||||
| 					left: 0; | ||||
| 					width: 100%; | ||||
| 					height: 78px; | ||||
| 					background: linear-gradient(transparent, rgba(#000, 0.7)); | ||||
| 				} | ||||
| 					> .banner { | ||||
| 						height: 100%; | ||||
| 						background-color: #4c5e6d; | ||||
| 						background-size: cover; | ||||
| 						background-position: center; | ||||
| 						box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset; | ||||
| 						will-change: background-position; | ||||
| 					} | ||||
| 
 | ||||
| 				> .followed { | ||||
| 					position: absolute; | ||||
| 					top: 12px; | ||||
| 					left: 12px; | ||||
| 					padding: 4px 8px; | ||||
| 					color: #fff; | ||||
| 					background: rgba(0, 0, 0, 0.7); | ||||
| 					font-size: 0.7em; | ||||
| 					border-radius: 6px; | ||||
| 				} | ||||
| 					> .fade { | ||||
| 						position: absolute; | ||||
| 						bottom: 0; | ||||
| 						left: 0; | ||||
| 						width: 100%; | ||||
| 						height: 78px; | ||||
| 						background: linear-gradient(transparent, rgba(#000, 0.7)); | ||||
| 					} | ||||
| 
 | ||||
| 				> .actions { | ||||
| 					position: absolute; | ||||
| 					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; | ||||
| 
 | ||||
| 					> .menu { | ||||
| 						vertical-align: bottom; | ||||
| 						height: 31px; | ||||
| 						width: 31px; | ||||
| 					> .followed { | ||||
| 						position: absolute; | ||||
| 						top: 12px; | ||||
| 						left: 12px; | ||||
| 						padding: 4px 8px; | ||||
| 						color: #fff; | ||||
| 						text-shadow: 0 0 8px #000; | ||||
| 						font-size: 16px; | ||||
| 						background: rgba(0, 0, 0, 0.7); | ||||
| 						font-size: 0.7em; | ||||
| 						border-radius: 6px; | ||||
| 					} | ||||
| 
 | ||||
| 					> .koudoku { | ||||
| 						margin-left: 4px; | ||||
| 						vertical-align: bottom; | ||||
| 					} | ||||
| 				} | ||||
| 					> .actions { | ||||
| 						position: absolute; | ||||
| 						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 { | ||||
| 					position: absolute; | ||||
| 					bottom: 0; | ||||
| 					left: 0; | ||||
| 					width: 100%; | ||||
| 					padding: 0 0 8px 154px; | ||||
| 					box-sizing: border-box; | ||||
| 					color: #fff; | ||||
| 						> .menu { | ||||
| 							vertical-align: bottom; | ||||
| 							height: 31px; | ||||
| 							width: 31px; | ||||
| 							color: #fff; | ||||
| 							text-shadow: 0 0 8px #000; | ||||
| 							font-size: 16px; | ||||
| 						} | ||||
| 
 | ||||
| 					> .name { | ||||
| 						display: block; | ||||
| 						margin: 0; | ||||
| 						line-height: 32px; | ||||
| 						font-weight: bold; | ||||
| 						font-size: 1.8em; | ||||
| 						text-shadow: 0 0 8px #000; | ||||
| 						> .koudoku { | ||||
| 							margin-left: 4px; | ||||
| 							vertical-align: bottom; | ||||
| 						} | ||||
| 					} | ||||
| 
 | ||||
| 					> .bottom { | ||||
| 						> * { | ||||
| 							display: inline-block; | ||||
| 							margin-right: 16px; | ||||
| 							line-height: 20px; | ||||
| 							opacity: 0.8; | ||||
| 					> .title { | ||||
| 						position: absolute; | ||||
| 						bottom: 0; | ||||
| 						left: 0; | ||||
| 						width: 100%; | ||||
| 						padding: 0 0 8px 154px; | ||||
| 						box-sizing: border-box; | ||||
| 						color: #fff; | ||||
| 
 | ||||
| 							&.username { | ||||
| 								font-weight: bold; | ||||
| 						> .name { | ||||
| 							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 { | ||||
| 				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; | ||||
| 				> .title { | ||||
| 					display: none; | ||||
| 					text-align: center; | ||||
| 					padding: 50px 8px 16px 8px; | ||||
| 					font-weight: bold; | ||||
| 					border-bottom: solid 1px var(--divider); | ||||
| 
 | ||||
| 					&.active { | ||||
| 						color: var(--accent); | ||||
| 					> .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; | ||||
| 						} | ||||
| 					} | ||||
| 
 | ||||
| 					&:hover { | ||||
| 						text-decoration: none; | ||||
| 					&.system > .field > .name { | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 					> b { | ||||
| 						display: block; | ||||
| 						line-height: 16px; | ||||
| 					} | ||||
| 				> .status { | ||||
| 					display: flex; | ||||
| 					padding: 24px; | ||||
| 					border-top: solid 1px var(--divider); | ||||
| 
 | ||||
| 					> span { | ||||
| 						font-size: 70%; | ||||
| 					> a { | ||||
| 						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 { | ||||
| 		margin-bottom: var(--margin); | ||||
| 	> .side { | ||||
| 		flex-basis: 300px; | ||||
| 		margin-left: var(--margin); | ||||
| 	} | ||||
| 
 | ||||
| 	&.max-width_500px { | ||||
| 		> .profile > ._content { | ||||
| 		display: block; | ||||
| 
 | ||||
| 		> .main > .profile > ._content { | ||||
| 			> .banner-container { | ||||
| 				height: 140px; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,5 @@ | |||
| <template> | ||||
| <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"> | ||||
| 		<XBlock class="block" v-for="path in meta.pinnedPages" :initial-path="path" :key="path"/> | ||||
| 	</div> | ||||
|  | @ -68,28 +62,6 @@ export default defineComponent({ | |||
| .rsqzvsbo { | ||||
| 	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 { | ||||
| 		display: grid; | ||||
| 		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/: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: '/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: '/about', component: page('about') }, | ||||
| 		{ 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/antennas', component: page('my-antennas/index') }, | ||||
| 		{ path: '/my/clips', component: page('my-clips/index') }, | ||||
| 		{ path: '/my/apps', component: page('apps') }, | ||||
| 		{ path: '/scratchpad', component: page('scratchpad') }, | ||||
| 		{ path: '/instance', component: page('instance/index') }, | ||||
| 		{ 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 builtinThemes = [ | ||||
| 	require('../themes/l-white.json5'), | ||||
| 	require('../themes/l-red.json5'), | ||||
| 	require('../themes/l-green.json5'), | ||||
| 	require('../themes/l-blue.json5'), | ||||
| 	require('../themes/l-light.json5'), | ||||
| 	require('../themes/l-apricot.json5'), | ||||
| 
 | ||||
| 	require('../themes/d-black.json5'), | ||||
| 	require('../themes/d-red.json5'), | ||||
| 	require('../themes/d-green.json5'), | ||||
| 	require('../themes/d-blue.json5'), | ||||
| 	require('../themes/d-dark.json5'), | ||||
| 	require('../themes/d-persimmon.json5'), | ||||
| 
 | ||||
| 	require('../themes/d-battery-saver.json5'), | ||||
| 	require('../themes/d-black.json5'), | ||||
| ] as Theme[]; | ||||
| 
 | ||||
| let timeout = null; | ||||
|  |  | |||
|  | @ -55,7 +55,7 @@ export const defaultDeviceUserSettings = { | |||
| export const defaultDeviceSettings = { | ||||
| 	lang: null, | ||||
| 	loadRawImages: false, | ||||
| 	alwaysShowNsfw: false, | ||||
| 	nsfw: 'respect', // respect, force, ignore
 | ||||
| 	useOsNativeEmojis: false, | ||||
| 	serverDisconnectedBehavior: 'quiet', | ||||
| 	accounts: [], | ||||
|  | @ -87,14 +87,6 @@ export const defaultDeviceSettings = { | |||
| 	deckColumnAlign: 'left', | ||||
| 	deckAlwaysShowMainColumn: true, | ||||
| 	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: {}, | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -448,10 +448,14 @@ hr { | |||
| 	opacity: 0.7; | ||||
| } | ||||
| 
 | ||||
| ._monospace { | ||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | ||||
| } | ||||
| 
 | ||||
| ._code { | ||||
| 	@extend ._monospace; | ||||
| 	background: #2d2d2d; | ||||
| 	color: #ccc; | ||||
| 	font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace; | ||||
| 	font-size: 14px; | ||||
| 	line-height: 1.5; | ||||
| 	padding: 5px; | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ | |||
| 		divider: 'rgba(255, 255, 255, 0.1)', | ||||
| 		indicator: '@accent', | ||||
| 		panel: '#000', | ||||
| 		panelHighlight: ':lighten<3<@panel', | ||||
| 		panelHeaderBg: ':lighten<3<@panel', | ||||
| 		panelHeaderFg: '@fg', | ||||
| 		panelHeaderDivider: 'rgba(0, 0, 0, 0)', | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ | |||
| 		divider: 'rgba(0, 0, 0, 0.1)', | ||||
| 		indicator: '@accent', | ||||
| 		panel: '#fff', | ||||
| 		panelHighlight: ':darken<3<@panel', | ||||
| 		panelHeaderBg: ':lighten<3<@panel', | ||||
| 		panelHeaderFg: '@fg', | ||||
| 		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', | ||||
| 	author: 'syuilo', | ||||
| 	desc: 'Default light theme', | ||||
| 
 | ||||
| 	base: 'dark', | ||||
| 
 | ||||
| 	props: { | ||||
| 		bg: '#272727', | ||||
| 		fg: 'rgb(199, 209, 216)', | ||||
| 		fgHighlighted: '#fff', | ||||
| 		divider: 'rgba(255, 255, 255, 0.14)', | ||||
| 		panel: '@bg', | ||||
| 		panelShadow: '" 0 0 0 1px var(--divider)', | ||||
| 		divider: '#2d2d2d', | ||||
| 		panel: '#0a0a0a', | ||||
| 		panelHeaderBg: '@panel', | ||||
| 		panelHeaderDivider: '@divider', | ||||
| 		infoFg: '@accent', | ||||
| 		infoBg: 'rgb(0, 0, 0)', | ||||
| 		header: ':alpha<0.7<@bg', | ||||
| 		navBg: '#363636', | ||||
| 		renote: '@accent', | ||||
| 		mention: '#da6d35', | ||||
| 		mentionMe: '#d44c4c', | ||||
| 		hashtag: '#4cb8d4', | ||||
| 		link: '@accent', | ||||
| 		panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)', | ||||
| 		shadow: 'rgba(255, 255, 255, 0.05)', | ||||
| 		modalBg: 'rgba(255, 255, 255, 0.1)', | ||||
| 		messageBg: '#1d1d1d', | ||||
| 	}, | ||||
| } | ||||
|  |  | |||
|  | @ -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', | ||||
| 	desc: 'Default light theme', | ||||
| 
 | ||||
| 	base: 'dark', | ||||
| 
 | ||||
| 	props: { | ||||
| 		accent: 'rgb(196 115 69)', | ||||
| 		bg: 'rgb(54, 54, 54)', | ||||
| 		bg: '#232323', | ||||
| 		fg: 'rgb(199, 209, 216)', | ||||
| 		fgHighlighted: '#fff', | ||||
| 		divider: 'rgba(255, 255, 255, 0.14)', | ||||
| 		panel: '@bg', | ||||
| 		panelShadow: '" 0 0 0 1px var(--divider)', | ||||
| 		panel: '#2d2d2d', | ||||
| 		panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)', | ||||
| 		panelHeaderBg: '@panel', | ||||
| 		panelHeaderDivider: '@divider', | ||||
| 		infoFg: '@accent', | ||||
| 		infoBg: 'rgb(0, 0, 0)', | ||||
| 		header: ':alpha<0.7<@bg', | ||||
| 		navBg: 'rgb(71, 71, 71)', | ||||
| 		navBg: '#363636', | ||||
| 		renote: '@accent', | ||||
| 		mention: '#da6d35', | ||||
| 		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', | ||||
| 
 | ||||
| 	name: 'Ai Persimmon', | ||||
| 	name: 'Mi Persimmon', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	base: 'dark', | ||||
| 
 | ||||
| 	props: { | ||||
| 		accent: 'rgb(206, 102, 65)', | ||||
| 		bg: 'rgb(41, 43, 41)', | ||||
| 		bg: 'rgb(31, 33, 31)', | ||||
| 		fg: '#cdd8c7', | ||||
| 		fgHighlighted: '#fff', | ||||
| 		divider: 'rgba(255, 255, 255, 0.14)', | ||||
| 		panel: '@bg', | ||||
| 		panelShadow: '" 0 0 0 1px var(--divider)', | ||||
| 		panel: 'rgb(41, 43, 41)', | ||||
| 		panelShadow: '" 0 8px 24px rgb(0 0 0 / 25%)', | ||||
| 		panelHeaderBg: '@panel', | ||||
| 		panelHeaderDivider: '@divider', | ||||
| 		infoFg: '@accent', | ||||
| 		infoBg: 'rgb(0, 0, 0)', | ||||
| 		infoFg: '@fg', | ||||
| 		infoBg: '#333c3b', | ||||
| 		header: ':alpha<0.7<@bg', | ||||
| 		navBg: '#1f211f', | ||||
| 		renote: '@accent', | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
| 	id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', | ||||
| 
 | ||||
| 	name: 'Ai Apricot', | ||||
| 	name: 'Mi Apricot', | ||||
| 	author: 'syuilo', | ||||
| 
 | ||||
| 	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', | ||||
| 
 | ||||
| 	name: 'Mi White', | ||||
| 	name: 'Mi Light', | ||||
| 	author: 'syuilo', | ||||
| 	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"> | ||||
| 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 * as sound from '@/scripts/sound'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
|  | @ -38,7 +39,7 @@ export default defineComponent({ | |||
| 				}, {}, 'closed'); | ||||
| 			} | ||||
| 
 | ||||
| 			sound('notification'); | ||||
| 			sound.play('notification'); | ||||
| 		}; | ||||
| 
 | ||||
| 		if (store.getters.isSignedIn) { | ||||
|  |  | |||
|  | @ -1,209 +1,19 @@ | |||
| <template> | ||||
| <div class="mk-app"> | ||||
| 	<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 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> | ||||
| <DesignA/> | ||||
| <XCommon/> | ||||
| </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 XHeader from './_common_/header.vue'; | ||||
| import DesignA from './visitor/a.vue'; | ||||
| import DesignB from './visitor/b.vue'; | ||||
| import XCommon from './_common_/common.vue'; | ||||
| 
 | ||||
| const DESKTOP_THRESHOLD = 1100; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		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> | ||||
| 
 | ||||
| <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> | ||||
| <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 v-text="hh"></span> | ||||
| 		<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span> | ||||
|  | @ -74,7 +74,6 @@ export default defineComponent({ | |||
| <style lang="scss" scoped> | ||||
| .mkw-digitalClock { | ||||
| 	padding: 16px 0; | ||||
| 	font-family: Lucida Console, Courier, monospace; | ||||
| 	text-align: center; | ||||
| } | ||||
| </style> | ||||
|  |  | |||
|  | @ -878,3 +878,19 @@ export const test7: Map = { | |||
| 		'--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[]; | ||||
| 
 | ||||
| 	@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[]; | ||||
| 
 | ||||
|  | @ -94,6 +94,18 @@ export class Meta { | |||
| 	}) | ||||
| 	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', { | ||||
| 		length: 512, | ||||
| 		nullable: true, | ||||
|  |  | |||
|  | @ -35,6 +35,8 @@ export class NoteReaction { | |||
| 	@JoinColumn() | ||||
| 	public note: Note | null; | ||||
| 
 | ||||
| 	// TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため)
 | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 260 | ||||
| 	}) | ||||
|  |  | |||
|  | @ -111,6 +111,12 @@ export class UserProfile { | |||
| 	}) | ||||
| 	public autoAcceptFollowed: boolean; | ||||
| 
 | ||||
| 	@Column('boolean', { | ||||
| 		default: false, | ||||
| 		comment: 'Whether reject index by crawler.' | ||||
| 	}) | ||||
| 	public noCrawle: boolean; | ||||
| 
 | ||||
| 	@Column('boolean', { | ||||
| 		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); | ||||
| 	} | ||||
| 
 | ||||
| 	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 { sum } = await this | ||||
|  | @ -60,7 +60,7 @@ export class DriveFileRepository extends Repository<DriveFile> { | |||
| 		return parseInt(sum, 10) || 0; | ||||
| 	} | ||||
| 
 | ||||
| 	public async clacDriveUsageOfHost(host: string): Promise<number> { | ||||
| 	public async calcDriveUsageOfHost(host: string): Promise<number> { | ||||
| 		const { sum } = await this | ||||
| 			.createQueryBuilder('file') | ||||
| 			.where('file.userHost = :host', { host: toPuny(host) }) | ||||
|  | @ -70,7 +70,7 @@ export class DriveFileRepository extends Repository<DriveFile> { | |||
| 		return parseInt(sum, 10) || 0; | ||||
| 	} | ||||
| 
 | ||||
| 	public async clacDriveUsageOfLocal(): Promise<number> { | ||||
| 	public async calcDriveUsageOfLocal(): Promise<number> { | ||||
| 		const { sum } = await this | ||||
| 			.createQueryBuilder('file') | ||||
| 			.where('file.userHost IS NULL') | ||||
|  | @ -80,7 +80,7 @@ export class DriveFileRepository extends Repository<DriveFile> { | |||
| 		return parseInt(sum, 10) || 0; | ||||
| 	} | ||||
| 
 | ||||
| 	public async clacDriveUsageOfRemote(): Promise<number> { | ||||
| 	public async calcDriveUsageOfRemote(): Promise<number> { | ||||
| 		const { sum } = await this | ||||
| 			.createQueryBuilder('file') | ||||
| 			.where('file.userHost IS NOT NULL') | ||||
|  |  | |||
|  | @ -239,6 +239,7 @@ export class UserRepository extends Repository<User> { | |||
| 				alwaysMarkNsfw: profile!.alwaysMarkNsfw, | ||||
| 				carefulBot: profile!.carefulBot, | ||||
| 				autoAcceptFollowed: profile!.autoAcceptFollowed, | ||||
| 				noCrawle: profile!.noCrawle, | ||||
| 				hasUnreadSpecifiedNotes: NoteUnreads.count({ | ||||
| 					where: { userId: user.id, isSpecified: true }, | ||||
| 					take: 1 | ||||
|  |  | |||
|  | @ -94,6 +94,14 @@ export const meta = { | |||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		backgroundImageUrl: { | ||||
| 			validator: $.optional.nullable.str, | ||||
| 		}, | ||||
| 
 | ||||
| 		logoImageUrl: { | ||||
| 			validator: $.optional.nullable.str, | ||||
| 		}, | ||||
| 
 | ||||
| 		name: { | ||||
| 			validator: $.optional.nullable.str, | ||||
| 			desc: { | ||||
|  | @ -473,6 +481,14 @@ export default define(meta, async (ps, me) => { | |||
| 		set.iconUrl = ps.iconUrl; | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.backgroundImageUrl !== undefined) { | ||||
| 		set.backgroundImageUrl = ps.backgroundImageUrl; | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.logoImageUrl !== undefined) { | ||||
| 		set.logoImageUrl = ps.logoImageUrl; | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.name !== undefined) { | ||||
| 		set.name = ps.name; | ||||
| 	} | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ export default define(meta, async (ps, user) => { | |||
| 	const instance = await fetchMeta(true); | ||||
| 
 | ||||
| 	// Calculate drive usage
 | ||||
| 	const usage = await DriveFiles.clacDriveUsageOf(user); | ||||
| 	const usage = await DriveFiles.calcDriveUsageOf(user); | ||||
| 
 | ||||
| 	return { | ||||
| 		capacity: 1024 * 1024 * instance.localDriveCapacityMb, | ||||
|  |  | |||
|  | @ -106,6 +106,13 @@ export const meta = { | |||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		noCrawle: { | ||||
| 			validator: $.optional.bool, | ||||
| 			desc: { | ||||
| 				'ja-JP': '検索エンジンによるインデックスを拒否するか否か' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		isBot: { | ||||
| 			validator: $.optional.bool, | ||||
| 			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.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; | ||||
| 	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.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; | ||||
| 	if (typeof ps.alwaysMarkNsfw === 'boolean') profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; | ||||
|  |  | |||
|  | @ -129,6 +129,8 @@ export default define(meta, async (ps, me) => { | |||
| 		bannerUrl: instance.bannerUrl, | ||||
| 		errorImageUrl: instance.errorImageUrl, | ||||
| 		iconUrl: instance.iconUrl, | ||||
| 		backgroundImageUrl: instance.backgroundImageUrl, | ||||
| 		logoImageUrl: instance.logoImageUrl, | ||||
| 		maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH), | ||||
| 		emojis: await Emojis.packMany(emojis), | ||||
| 		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 Logger from '../services/logger'; | ||||
| import { program } from '../argv'; | ||||
| import { UserProfiles } from '../models'; | ||||
| import { UserProfiles, Users } from '../models'; | ||||
| import { networkChart } from '../services/chart'; | ||||
| import { genAvatar } from '../misc/gen-avatar'; | ||||
| import { createTemp } from '../misc/create-temp'; | ||||
| import { publishMainStream } from '../services/stream'; | ||||
| 
 | ||||
| export const serverLogger = new Logger('server', 'gray', false); | ||||
| 
 | ||||
|  | @ -83,10 +84,15 @@ router.get('/verify-email/:code', async ctx => { | |||
| 		ctx.body = 'Verify succeeded!'; | ||||
| 		ctx.status = 200; | ||||
| 
 | ||||
| 		UserProfiles.update({ userId: profile.userId }, { | ||||
| 		await UserProfiles.update({ userId: profile.userId }, { | ||||
| 			emailVerified: true, | ||||
| 			emailVerifyCode: null | ||||
| 		}); | ||||
| 
 | ||||
| 		publishMainStream(profile.userId, 'meUpdated', await Users.pack(profile.userId, profile.userId, { | ||||
| 			detail: true, | ||||
| 			includeSecrets: true | ||||
| 		})); | ||||
| 	} else { | ||||
| 		ctx.status = 404; | ||||
| 	} | ||||
|  |  | |||
|  | @ -242,9 +242,11 @@ router.get('/notes/:note', async ctx => { | |||
| 
 | ||||
| 	if (note) { | ||||
| 		const _note = await Notes.pack(note); | ||||
| 		const profile = await UserProfiles.findOne(note.userId).then(ensure); | ||||
| 		const meta = await fetchMeta(); | ||||
| 		await ctx.render('note', { | ||||
| 			note: _note, | ||||
| 			profile, | ||||
| 			// TODO: Let locale changeable by instance setting
 | ||||
| 			summary: getNoteSummary(_note, locales['ja-JP']), | ||||
| 			instanceName: meta.name || 'Misskey', | ||||
|  | @ -280,9 +282,11 @@ router.get('/@:user/pages/:page', async ctx => { | |||
| 
 | ||||
| 	if (page) { | ||||
| 		const _page = await Pages.pack(page); | ||||
| 		const profile = await UserProfiles.findOne(page.userId).then(ensure); | ||||
| 		const meta = await fetchMeta(); | ||||
| 		await ctx.render('page', { | ||||
| 			page: _page, | ||||
| 			profile, | ||||
| 			instanceName: meta.name || 'Misskey' | ||||
| 		}); | ||||
| 
 | ||||
|  | @ -307,9 +311,11 @@ router.get('/clips/:clip', async ctx => { | |||
| 
 | ||||
| 	if (clip) { | ||||
| 		const _clip = await Clips.pack(clip); | ||||
| 		const profile = await UserProfiles.findOne(clip.userId).then(ensure); | ||||
| 		const meta = await fetchMeta(); | ||||
| 		await ctx.render('clip', { | ||||
| 			clip: _clip, | ||||
| 			profile, | ||||
| 			instanceName: meta.name || 'Misskey' | ||||
| 		}); | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,6 +19,9 @@ block og | |||
| 	meta(property='og:image'       content= user.avatarUrl) | ||||
| 
 | ||||
| block meta | ||||
| 	if profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
| 
 | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
| 	meta(name='misskey:user-id' content=user.id) | ||||
| 	meta(name='misskey:clip-id' content=clip.id) | ||||
|  |  | |||
|  | @ -19,6 +19,9 @@ block og | |||
| 	meta(property='og:image'       content= user.avatarUrl) | ||||
| 
 | ||||
| block meta | ||||
| 	if user.host || profile.noCrawle | ||||
| 		meta(name='robots' content='noindex') | ||||
| 
 | ||||
| 	meta(name='misskey:user-username' content=user.username) | ||||
| 	meta(name='misskey:user-id' content=user.id) | ||||
| 	meta(name='misskey:note-id' content=note.id) | ||||
|  | @ -26,9 +29,6 @@ block meta | |||
| 	meta(name='twitter:card' content='summary') | ||||
| 
 | ||||
| 	// todo | ||||
| 	if user.host | ||||
| 		meta(name='robots' content='noindex') | ||||
| 
 | ||||
| 	if user.twitter | ||||
| 		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