summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-10-16 19:55:44 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-10-16 19:55:44 +0900
commit8a1f3a4c0b5732d0f08f0788d93c5934de8960c8 (patch)
treebe6fbcf3a1bbd78306d91e19ef6f3e7023f41561 /src
parentMerge branch 'develop' (diff)
parent12.92.0 (diff)
downloadmisskey-8a1f3a4c0b5732d0f08f0788d93c5934de8960c8.tar.gz
misskey-8a1f3a4c0b5732d0f08f0788d93c5934de8960c8.tar.bz2
misskey-8a1f3a4c0b5732d0f08f0788d93c5934de8960c8.zip
Merge branch 'develop'
Diffstat (limited to 'src')
-rw-r--r--src/argv.ts23
-rw-r--r--src/boot/index.ts8
-rw-r--r--src/boot/master.ts8
-rw-r--r--src/client/account.ts74
-rw-r--r--src/client/components/abuse-report-window.vue2
-rw-r--r--src/client/components/autocomplete.vue44
-rw-r--r--src/client/components/captcha.vue4
-rw-r--r--src/client/components/channel-follow-button.vue2
-rw-r--r--src/client/components/cw-button.vue8
-rw-r--r--src/client/components/debobigego/base.vue (renamed from src/client/components/form/base.vue)24
-rw-r--r--src/client/components/debobigego/button.vue (renamed from src/client/components/form/button.vue)10
-rw-r--r--src/client/components/debobigego/debobigego.scss (renamed from src/client/components/form/form.scss)24
-rw-r--r--src/client/components/debobigego/group.vue (renamed from src/client/components/form/group.vue)26
-rw-r--r--src/client/components/debobigego/info.vue (renamed from src/client/components/form/info.vue)4
-rw-r--r--src/client/components/debobigego/input.vue (renamed from src/client/components/ui/input.vue)173
-rw-r--r--src/client/components/debobigego/key-value-view.vue (renamed from src/client/components/form/key-value-view.vue)8
-rw-r--r--src/client/components/debobigego/link.vue (renamed from src/client/components/form/link.vue)8
-rw-r--r--src/client/components/debobigego/object-view.vue (renamed from src/client/components/form/object-view.vue)10
-rw-r--r--src/client/components/debobigego/pagination.vue (renamed from src/client/components/form/pagination.vue)2
-rw-r--r--src/client/components/debobigego/radios.vue112
-rw-r--r--src/client/components/debobigego/range.vue122
-rw-r--r--src/client/components/debobigego/select.vue145
-rw-r--r--src/client/components/debobigego/suspense.vue (renamed from src/client/components/form/suspense.vue)12
-rw-r--r--src/client/components/debobigego/switch.vue132
-rw-r--r--src/client/components/debobigego/textarea.vue161
-rw-r--r--src/client/components/debobigego/tuple.vue (renamed from src/client/components/form/tuple.vue)2
-rw-r--r--src/client/components/dialog.vue4
-rw-r--r--src/client/components/emoji-picker-window.vue2
-rw-r--r--src/client/components/emoji-picker.vue2
-rw-r--r--src/client/components/follow-button.vue2
-rw-r--r--src/client/components/forgot-password.vue2
-rw-r--r--src/client/components/form-dialog.vue28
-rw-r--r--src/client/components/form/input.vue183
-rw-r--r--src/client/components/form/radio.vue (renamed from src/client/components/ui/radio.vue)0
-rw-r--r--src/client/components/form/radios.vue88
-rw-r--r--src/client/components/form/range.vue141
-rw-r--r--src/client/components/form/section.vue31
-rw-r--r--src/client/components/form/select.vue243
-rw-r--r--src/client/components/form/slot.vue50
-rw-r--r--src/client/components/form/switch.vue182
-rw-r--r--src/client/components/form/textarea.vue195
-rw-r--r--src/client/components/global/header.vue359
-rw-r--r--src/client/components/global/spacer.vue76
-rw-r--r--src/client/components/index.ts4
-rw-r--r--src/client/components/instance-stats.vue2
-rw-r--r--src/client/components/media-caption.vue32
-rw-r--r--src/client/components/mfm.ts2
-rw-r--r--src/client/components/modal-page-window.vue51
-rw-r--r--src/client/components/note-detailed.vue8
-rw-r--r--src/client/components/note-preview.vue35
-rw-r--r--src/client/components/note-simple.vue113
-rw-r--r--src/client/components/note.sub.vue2
-rw-r--r--src/client/components/note.vue10
-rw-r--r--src/client/components/notification-setting-window.vue4
-rw-r--r--src/client/components/notifications.vue13
-rw-r--r--src/client/components/page-window.vue17
-rw-r--r--src/client/components/page/page.number-input.vue2
-rw-r--r--src/client/components/page/page.post.vue2
-rw-r--r--src/client/components/page/page.radio-button.vue2
-rw-r--r--src/client/components/page/page.switch.vue2
-rw-r--r--src/client/components/page/page.text-input.vue2
-rw-r--r--src/client/components/page/page.textarea-input.vue2
-rw-r--r--src/client/components/page/page.textarea.vue2
-rw-r--r--src/client/components/poll-editor.vue6
-rw-r--r--src/client/components/post-form.vue48
-rw-r--r--src/client/components/sample.vue8
-rwxr-xr-xsrc/client/components/signin.vue10
-rw-r--r--src/client/components/signup-dialog.vue6
-rw-r--r--src/client/components/signup.vue93
-rw-r--r--src/client/components/tab.vue21
-rw-r--r--src/client/components/taskmanager.api-window.vue2
-rw-r--r--src/client/components/taskmanager.vue2
-rw-r--r--src/client/components/token-generate-window.vue6
-rw-r--r--src/client/components/ui/button.vue67
-rw-r--r--src/client/components/ui/folder.vue25
-rw-r--r--src/client/components/ui/info.vue11
-rw-r--r--src/client/components/ui/menu.vue85
-rw-r--r--src/client/components/ui/popup-menu.vue4
-rw-r--r--src/client/components/ui/popup.vue5
-rw-r--r--src/client/components/ui/radios.vue58
-rw-r--r--src/client/components/ui/range.vue139
-rw-r--r--src/client/components/ui/select.vue262
-rw-r--r--src/client/components/ui/super-menu.vue151
-rw-r--r--src/client/components/ui/switch.vue144
-rw-r--r--src/client/components/ui/textarea.vue254
-rw-r--r--src/client/components/ui/window.vue23
-rw-r--r--src/client/components/user-select-dialog.vue4
-rw-r--r--src/client/components/widgets.vue2
-rw-r--r--src/client/directives/click-anime.ts3
-rw-r--r--src/client/directives/get-size.ts34
-rw-r--r--src/client/directives/index.ts2
-rw-r--r--src/client/directives/tooltip.ts9
-rw-r--r--src/client/events.ts4
-rw-r--r--src/client/pages/_error_.vue38
-rw-r--r--src/client/pages/about-misskey.vue14
-rw-r--r--src/client/pages/about.vue16
-rw-r--r--src/client/pages/advanced-theme-editor.vue30
-rw-r--r--src/client/pages/announcements.vue37
-rw-r--r--src/client/pages/antenna-timeline.vue2
-rw-r--r--src/client/pages/api-console.vue8
-rw-r--r--src/client/pages/channel-editor.vue4
-rw-r--r--src/client/pages/channels.vue2
-rw-r--r--src/client/pages/docs.vue4
-rw-r--r--src/client/pages/emojis.category.vue9
-rw-r--r--src/client/pages/emojis.emoji.vue14
-rw-r--r--src/client/pages/emojis.vue12
-rw-r--r--src/client/pages/explore.vue160
-rw-r--r--src/client/pages/favorites.vue14
-rw-r--r--src/client/pages/featured.vue17
-rw-r--r--src/client/pages/federation.vue189
-rw-r--r--src/client/pages/gallery/edit.vue24
-rw-r--r--src/client/pages/gallery/index.vue4
-rw-r--r--src/client/pages/instance-info.vue28
-rw-r--r--src/client/pages/instance/abuses.vue11
-rw-r--r--src/client/pages/instance/ads.vue98
-rw-r--r--src/client/pages/instance/announcements.vue59
-rw-r--r--src/client/pages/instance/bot-protection.vue42
-rw-r--r--src/client/pages/instance/database.vue13
-rw-r--r--src/client/pages/instance/email-settings.vue35
-rw-r--r--src/client/pages/instance/emoji-edit-dialog.vue8
-rw-r--r--src/client/pages/instance/emojis.vue33
-rw-r--r--src/client/pages/instance/file-dialog.vue2
-rw-r--r--src/client/pages/instance/files-settings.vue23
-rw-r--r--src/client/pages/instance/files.vue44
-rw-r--r--src/client/pages/instance/index.vue236
-rw-r--r--src/client/pages/instance/instance-block.vue21
-rw-r--r--src/client/pages/instance/instance.vue4
-rw-r--r--src/client/pages/instance/integrations-discord.vue18
-rw-r--r--src/client/pages/instance/integrations-github.vue18
-rw-r--r--src/client/pages/instance/integrations-twitter.vue18
-rw-r--r--src/client/pages/instance/integrations.vue19
-rw-r--r--src/client/pages/instance/logs.vue6
-rw-r--r--src/client/pages/instance/metrics.vue22
-rw-r--r--src/client/pages/instance/object-storage.vue39
-rw-r--r--src/client/pages/instance/other-settings.vue21
-rw-r--r--src/client/pages/instance/overview.vue37
-rw-r--r--src/client/pages/instance/proxy-account.vue19
-rw-r--r--src/client/pages/instance/queue.chart.vue6
-rw-r--r--src/client/pages/instance/queue.vue5
-rw-r--r--src/client/pages/instance/relays.vue11
-rw-r--r--src/client/pages/instance/security.vue24
-rw-r--r--src/client/pages/instance/service-worker.vue21
-rw-r--r--src/client/pages/instance/settings.vue49
-rw-r--r--src/client/pages/instance/users.vue63
-rw-r--r--src/client/pages/mentions.vue15
-rw-r--r--src/client/pages/messages.vue13
-rw-r--r--src/client/pages/messaging/index.vue74
-rw-r--r--src/client/pages/messaging/messaging-room.message.vue2
-rw-r--r--src/client/pages/messaging/messaging-room.vue2
-rw-r--r--src/client/pages/mfm-cheat-sheet.vue2
-rw-r--r--src/client/pages/my-antennas/editor.vue30
-rw-r--r--src/client/pages/my-groups/index.vue2
-rw-r--r--src/client/pages/my-lists/index.vue25
-rw-r--r--src/client/pages/my-lists/list.vue55
-rw-r--r--src/client/pages/notifications.vue66
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.button.vue6
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.canvas.vue2
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.counter.vue2
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.if.vue4
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.note.vue4
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.number-input.vue2
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.post.vue6
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.radio-button.vue4
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.section.vue2
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.switch.vue4
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.text-input.vue2
-rw-r--r--src/client/pages/page-editor/els/page-editor.el.textarea-input.vue4
-rw-r--r--src/client/pages/page-editor/page-editor.blocks.vue13
-rw-r--r--src/client/pages/page-editor/page-editor.script-block.vue94
-rw-r--r--src/client/pages/page-editor/page-editor.vue182
-rw-r--r--src/client/pages/page.vue110
-rw-r--r--src/client/pages/pages.vue59
-rw-r--r--src/client/pages/reset-password.vue12
-rw-r--r--src/client/pages/reversi/game.setting.vue6
-rw-r--r--src/client/pages/room/room.vue2
-rw-r--r--src/client/pages/search.vue15
-rw-r--r--src/client/pages/settings/2fa.vue10
-rw-r--r--src/client/pages/settings/account-info.vue10
-rw-r--r--src/client/pages/settings/accounts.vue15
-rw-r--r--src/client/pages/settings/api.vue11
-rw-r--r--src/client/pages/settings/apps.vue13
-rw-r--r--src/client/pages/settings/custom-css.vue15
-rw-r--r--src/client/pages/settings/deck.vue21
-rw-r--r--src/client/pages/settings/delete-account.vue11
-rw-r--r--src/client/pages/settings/drive.vue21
-rw-r--r--src/client/pages/settings/email-address.vue11
-rw-r--r--src/client/pages/settings/email-notification.vue21
-rw-r--r--src/client/pages/settings/email.vue15
-rw-r--r--src/client/pages/settings/experimental-features.vue10
-rw-r--r--src/client/pages/settings/general.vue61
-rw-r--r--src/client/pages/settings/import-export.vue58
-rw-r--r--src/client/pages/settings/index.vue237
-rw-r--r--src/client/pages/settings/integration.vue23
-rw-r--r--src/client/pages/settings/menu.vue15
-rw-r--r--src/client/pages/settings/mute-block.vue13
-rw-r--r--src/client/pages/settings/notifications.vue13
-rw-r--r--src/client/pages/settings/other.vue17
-rw-r--r--src/client/pages/settings/plugin.install.vue15
-rw-r--r--src/client/pages/settings/plugin.manage.vue21
-rw-r--r--src/client/pages/settings/plugin.vue9
-rw-r--r--src/client/pages/settings/privacy.vue29
-rw-r--r--src/client/pages/settings/profile.vue35
-rw-r--r--src/client/pages/settings/reaction.vue19
-rw-r--r--src/client/pages/settings/registry.keys.vue13
-rw-r--r--src/client/pages/settings/registry.value.vue15
-rw-r--r--src/client/pages/settings/registry.vue13
-rw-r--r--src/client/pages/settings/security.vue15
-rw-r--r--src/client/pages/settings/sounds.vue15
-rw-r--r--src/client/pages/settings/theme.install.vue13
-rw-r--r--src/client/pages/settings/theme.manage.vue11
-rw-r--r--src/client/pages/settings/theme.vue27
-rw-r--r--src/client/pages/settings/update.vue15
-rw-r--r--src/client/pages/settings/word-mute.vue19
-rw-r--r--src/client/pages/signup-complete.vue50
-rw-r--r--src/client/pages/test.vue8
-rw-r--r--src/client/pages/theme-editor.vue30
-rw-r--r--src/client/pages/timeline.vue40
-rw-r--r--src/client/pages/user-ap-info.vue16
-rw-r--r--src/client/pages/user-info.vue26
-rw-r--r--src/client/pages/user-list-timeline.vue2
-rw-r--r--src/client/pages/user/clips.vue7
-rw-r--r--src/client/pages/user/follow-list.vue7
-rw-r--r--src/client/pages/user/gallery.vue7
-rw-r--r--src/client/pages/user/index.timeline.vue4
-rw-r--r--src/client/pages/user/index.vue349
-rw-r--r--src/client/pages/user/pages.vue7
-rw-r--r--src/client/pages/welcome.entrance.a.vue2
-rw-r--r--src/client/pages/welcome.entrance.b.vue2
-rw-r--r--src/client/pages/welcome.entrance.c.vue2
-rw-r--r--src/client/pages/welcome.setup.vue2
-rw-r--r--src/client/router.ts1
-rw-r--r--src/client/scripts/autocomplete.ts35
-rw-r--r--src/client/scripts/idb-proxy.ts5
-rw-r--r--src/client/scripts/physics.ts2
-rw-r--r--src/client/scripts/scroll.ts20
-rw-r--r--src/client/scripts/theme.ts7
-rw-r--r--src/client/style.scss34
-rw-r--r--src/client/themes/_dark.json53
-rw-r--r--src/client/themes/_light.json53
-rw-r--r--src/client/themes/d-astro.json52
-rw-r--r--src/client/themes/d-botanical.json526
-rw-r--r--src/client/themes/d-future.json52
-rw-r--r--src/client/ui/_common_/header.vue302
-rw-r--r--src/client/ui/_common_/sidebar.vue72
-rw-r--r--src/client/ui/chat/index.vue7
-rw-r--r--src/client/ui/chat/note-preview.vue2
-rw-r--r--src/client/ui/chat/note.sub.vue2
-rw-r--r--src/client/ui/chat/note.vue10
-rw-r--r--src/client/ui/chat/post-form.vue2
-rw-r--r--src/client/ui/chat/side.vue6
-rw-r--r--src/client/ui/deck/column.vue17
-rw-r--r--src/client/ui/deck/deck-store.ts10
-rw-r--r--src/client/ui/deck/main-column.vue9
-rw-r--r--src/client/ui/default.header.vue70
-rw-r--r--src/client/ui/default.side.vue8
-rw-r--r--src/client/ui/default.sidebar.vue70
-rw-r--r--src/client/ui/default.vue54
-rw-r--r--src/client/ui/universal.vue39
-rw-r--r--src/client/ui/zen.vue4
-rw-r--r--src/client/widgets/aiscript.vue2
-rw-r--r--src/client/widgets/memo.vue2
-rw-r--r--src/client/widgets/notifications.vue2
-rw-r--r--src/db/postgre.ts6
-rw-r--r--src/docs/en-US/advanced/aiscript.md2
-rw-r--r--src/docs/en-US/advanced/api.md22
-rw-r--r--src/docs/en-US/advanced/create-plugin.md12
-rw-r--r--src/docs/en-US/advanced/develop-bot.md2
-rw-r--r--src/docs/en-US/advanced/stream.md2
-rw-r--r--src/docs/en-US/general/glossary.md2
-rw-r--r--src/docs/en-US/general/troubleshooting.md2
-rw-r--r--src/docs/eo-UY/admin/faq.md2
-rw-r--r--src/docs/eo-UY/advanced/create-plugin.md2
-rw-r--r--src/docs/eo-UY/advanced/stream.md2
-rw-r--r--src/docs/eo-UY/features/favorite.md2
-rw-r--r--src/docs/eo-UY/features/keyboard-shortcut.md12
-rw-r--r--src/docs/eo-UY/features/mfm.md6
-rw-r--r--src/docs/eo-UY/features/note.md20
-rw-r--r--src/docs/eo-UY/general/faq.md2
-rw-r--r--src/docs/eo-UY/general/glossary.md10
-rw-r--r--src/docs/eo-UY/general/misskey.md2
-rw-r--r--src/docs/es-ES/general/troubleshooting.md2
-rw-r--r--src/docs/fr-FR/general/apps.md4
-rw-r--r--src/docs/fr-FR/general/glossary.md66
-rw-r--r--src/docs/fr-FR/general/troubleshooting.md14
-rw-r--r--src/docs/zh-CN/advanced/stream.md18
-rw-r--r--src/docs/zh-CN/features/follow.md4
-rw-r--r--src/docs/zh-CN/features/mfm.md16
-rw-r--r--src/env.ts20
-rw-r--r--src/mfm/from-html.ts124
-rw-r--r--src/misc/download-url.ts21
-rw-r--r--src/misc/fetch.ts67
-rw-r--r--src/misc/hard-limits.ts6
-rw-r--r--src/misc/truncate.ts11
-rw-r--r--src/models/entities/meta.ts5
-rw-r--r--src/models/entities/notification.ts2
-rw-r--r--src/models/entities/user-pending.ts32
-rw-r--r--src/models/entities/user-profile.ts2
-rw-r--r--src/models/index.ts2
-rw-r--r--src/models/repositories/note.ts1
-rw-r--r--src/prelude/url.ts10
-rw-r--r--src/queue/index.ts6
-rw-r--r--src/queue/processors/deliver.ts7
-rw-r--r--src/queue/processors/inbox.ts3
-rw-r--r--src/remote/activitypub/ap-request.ts104
-rw-r--r--src/remote/activitypub/kernel/announce/note.ts3
-rw-r--r--src/remote/activitypub/kernel/create/note.ts3
-rw-r--r--src/remote/activitypub/models/image.ts4
-rw-r--r--src/remote/activitypub/models/note.ts3
-rw-r--r--src/remote/activitypub/models/person.ts11
-rw-r--r--src/remote/activitypub/request.ts162
-rw-r--r--src/server/api/common/signup.ts26
-rw-r--r--src/server/api/endpoints/admin/accounts/create.ts5
-rw-r--r--src/server/api/endpoints/admin/drive/files.ts2
-rw-r--r--src/server/api/endpoints/admin/queue/inbox-delayed.ts2
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts8
-rw-r--r--src/server/api/endpoints/ap/get.ts8
-rw-r--r--src/server/api/endpoints/ap/show.ts8
-rw-r--r--src/server/api/endpoints/blocking/create.ts14
-rw-r--r--src/server/api/endpoints/drive/files/update.ts3
-rw-r--r--src/server/api/endpoints/drive/files/upload-from-url.ts3
-rw-r--r--src/server/api/endpoints/email-address/available.ts37
-rw-r--r--src/server/api/endpoints/i/notifications.ts11
-rw-r--r--src/server/api/endpoints/i/update.ts2
-rw-r--r--src/server/api/endpoints/meta.ts6
-rw-r--r--src/server/api/endpoints/users/groups/leave.ts50
-rw-r--r--src/server/api/index.ts2
-rw-r--r--src/server/api/private/signup-pending.ts35
-rw-r--r--src/server/api/private/signup.ts62
-rw-r--r--src/server/file/send-drive-file.ts5
-rw-r--r--src/server/index.ts4
-rw-r--r--src/server/nodeinfo.ts1
-rw-r--r--src/server/proxy/proxy-media.ts5
-rw-r--r--src/server/web/manifest.json2
-rw-r--r--src/services/blocking/create.ts5
-rw-r--r--src/services/logger.ts8
335 files changed, 6069 insertions, 4387 deletions
diff --git a/src/argv.ts b/src/argv.ts
deleted file mode 100644
index 106ecf2675..0000000000
--- a/src/argv.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Command } from 'commander';
-import config from '@/config/index';
-
-const program = new Command();
-
-program.version(config.version);
-program.option('--no-daemons', 'Disable daemon processes (for debbuging)');
-program.option('--disable-clustering', 'Disable clustering');
-program.option('--only-server', 'Run server only (without job queue processing)');
-program.option('--only-queue', 'Pocessing job queue only (without server)');
-program.option('--quiet', 'Suppress all logs');
-program.option('--verbose', 'Enable all logs');
-program.option('--with-log-time', 'Include timestamp for each logs');
-program.option('--slow', 'Delay all requests (for debbuging)');
-program.option('--color', 'This option is a dummy for some external program\'s (e.g. forever) issue.');
-program.parse(process.argv);
-
-if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true;
-if (process.env.NODE_ENV === 'test') program.disableClustering = true;
-//if (process.env.NODE_ENV === 'test') program.quiet = true;
-if (process.env.NODE_ENV === 'test') program.noDaemons = true;
-
-export { program };
diff --git a/src/boot/index.ts b/src/boot/index.ts
index 20c53a366c..cb4c8536db 100644
--- a/src/boot/index.ts
+++ b/src/boot/index.ts
@@ -3,7 +3,7 @@ import * as chalk from 'chalk';
import Xev from 'xev';
import Logger from '@/services/logger';
-import { program } from '../argv';
+import { envOption } from '../env';
// for typeorm
import 'reflect-metadata';
@@ -20,7 +20,7 @@ const ev = new Xev();
export default async function() {
process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`;
- if (cluster.isMaster || program.disableClustering) {
+ if (cluster.isMaster || envOption.disableClustering) {
await masterMain();
if (cluster.isMaster) {
@@ -28,7 +28,7 @@ export default async function() {
}
}
- if (cluster.isWorker || program.disableClustering) {
+ if (cluster.isWorker || envOption.disableClustering) {
await workerMain();
}
@@ -60,7 +60,7 @@ cluster.on('exit', worker => {
});
// Display detail of unhandled promise rejection
-if (!program.quiet) {
+if (!envOption.quiet) {
process.on('unhandledRejection', console.dir);
}
diff --git a/src/boot/master.ts b/src/boot/master.ts
index d9cc7c16be..071b37b76d 100644
--- a/src/boot/master.ts
+++ b/src/boot/master.ts
@@ -11,7 +11,7 @@ import Logger from '@/services/logger';
import loadConfig from '@/config/load';
import { Config } from '@/config/types';
import { lessThan } from '@/prelude/array';
-import { program } from '../argv';
+import { envOption } from '../env';
import { showMachineInfo } from '@/misc/show-machine-info';
import { initDb } from '../db/postgre';
@@ -25,7 +25,7 @@ const logger = new Logger('core', 'cyan');
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
function greet() {
- if (!program.quiet) {
+ if (!envOption.quiet) {
//#region Misskey logo
const v = `v${meta.version}`;
console.log(' _____ _ _ ');
@@ -73,13 +73,13 @@ export async function masterMain() {
bootLogger.succ('Misskey initialized');
- if (!program.disableClustering) {
+ if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit);
}
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
- if (!program.noDaemons) {
+ if (!envOption.noDaemons) {
require('../daemons/server-stats').default();
require('../daemons/queue-stats').default();
require('../daemons/janitor').default();
diff --git a/src/client/account.ts b/src/client/account.ts
index 6e26ac1f7d..a3fe082a22 100644
--- a/src/client/account.ts
+++ b/src/client/account.ts
@@ -1,9 +1,10 @@
import { del, get, set } from '@client/scripts/idb-proxy';
import { reactive } from 'vue';
import { apiUrl } from '@client/config';
-import { waiting } from '@client/os';
+import { waiting, api, popup, popupMenu, success } from '@client/os';
import { unisonReload, reloadChannel } from '@client/scripts/unison-reload';
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
+import { i18n } from './i18n';
// TODO: 他のタブと永続化されたstateを同期
@@ -129,6 +130,77 @@ export async function login(token: Account['token'], redirect?: string) {
unisonReload();
}
+export async function openAccountMenu(ev: MouseEvent) {
+ function showSigninDialog() {
+ popup(import('@client/components/signin-dialog.vue'), {}, {
+ done: res => {
+ addAccount(res.id, res.i);
+ success();
+ },
+ }, 'closed');
+ }
+
+ function createAccount() {
+ popup(import('@client/components/signup-dialog.vue'), {}, {
+ done: res => {
+ addAccount(res.id, res.i);
+ switchAccountWithToken(res.i);
+ },
+ }, 'closed');
+ }
+
+ async function switchAccount(account: any) {
+ const storedAccounts = await getAccounts();
+ const token = storedAccounts.find(x => x.id === account.id).token;
+ switchAccountWithToken(token);
+ }
+
+ function switchAccountWithToken(token: string) {
+ login(token);
+ }
+
+ const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
+ const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
+
+ const accountItemPromises = storedAccounts.map(a => new Promise(res => {
+ accountsPromise.then(accounts => {
+ const account = accounts.find(x => x.id === a.id);
+ if (account == null) return res(null);
+ res({
+ type: 'user',
+ user: account,
+ action: () => { switchAccount(account); }
+ });
+ });
+ }));
+
+ popupMenu([...[{
+ type: 'link',
+ text: i18n.locale.profile,
+ to: `/@${ $i.username }`,
+ avatar: $i,
+ }, null, ...accountItemPromises, {
+ icon: 'fas fa-plus',
+ text: i18n.locale.addAccount,
+ action: () => {
+ popupMenu([{
+ text: i18n.locale.existingAccount,
+ action: () => { showSigninDialog(); },
+ }, {
+ text: i18n.locale.createAccount,
+ action: () => { createAccount(); },
+ }], ev.currentTarget || ev.target);
+ },
+ }, {
+ type: 'link',
+ icon: 'fas fa-users',
+ text: i18n.locale.manageAccounts,
+ to: `/settings/accounts`,
+ }]], ev.currentTarget || ev.target, {
+ align: 'left'
+ });
+}
+
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
diff --git a/src/client/components/abuse-report-window.vue b/src/client/components/abuse-report-window.vue
index 266c0d566f..21a19385ae 100644
--- a/src/client/components/abuse-report-window.vue
+++ b/src/client/components/abuse-report-window.vue
@@ -25,7 +25,7 @@
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import XWindow from '@client/components/ui/window.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
import MkButton from '@client/components/ui/button.vue';
import * as os from '@client/os';
diff --git a/src/client/components/autocomplete.vue b/src/client/components/autocomplete.vue
index 065ee6de2e..e621b26229 100644
--- a/src/client/components/autocomplete.vue
+++ b/src/client/components/autocomplete.vue
@@ -10,12 +10,12 @@
</li>
<li @click="chooseUser()" @keydown="onKeydown" tabindex="-1" class="choose">{{ $ts.selectUser }}</li>
</ol>
- <ol class="hashtags" ref="suggests" v-if="hashtags.length > 0">
+ <ol class="hashtags" ref="suggests" v-else-if="hashtags.length > 0">
<li v-for="hashtag in hashtags" @click="complete(type, hashtag)" @keydown="onKeydown" tabindex="-1">
<span class="name">{{ hashtag }}</span>
</li>
</ol>
- <ol class="emojis" ref="suggests" v-if="emojis.length > 0">
+ <ol class="emojis" ref="suggests" v-else-if="emojis.length > 0">
<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
<span class="emoji" v-else-if="!$store.state.useOsNativeEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
@@ -24,6 +24,11 @@
<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
</li>
</ol>
+ <ol class="mfmTags" ref="suggests" v-else-if="mfmTags.length > 0">
+ <li v-for="tag in mfmTags" @click="complete(type, tag)" @keydown="onKeydown" tabindex="-1">
+ <span class="tag">{{ tag }}</span>
+ </li>
+ </ol>
</div>
</template>
@@ -106,6 +111,8 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
//#endregion
+const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle'];
+
export default defineComponent({
props: {
type: {
@@ -137,11 +144,6 @@ export default defineComponent({
type: Number,
required: true,
},
-
- showing: {
- type: Boolean,
- required: true
- },
},
emits: ['done', 'closed'],
@@ -154,18 +156,11 @@ export default defineComponent({
hashtags: [],
emojis: [],
items: [],
+ mfmTags: [],
select: -1,
}
},
- watch: {
- showing() {
- if (!this.showing) {
- this.$emit('closed');
- }
- }
- },
-
updated() {
this.setPosition();
this.items = (this.$refs.suggests as Element | undefined)?.children || [];
@@ -236,7 +231,7 @@ export default defineComponent({
}
}
- if (this.type == 'user') {
+ if (this.type === 'user') {
if (this.q == null) {
this.users = [];
this.fetching = false;
@@ -262,7 +257,7 @@ export default defineComponent({
sessionStorage.setItem(cacheKey, JSON.stringify(users));
});
}
- } else if (this.type == 'hashtag') {
+ } else if (this.type === 'hashtag') {
if (this.q == null || this.q == '') {
this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
this.fetching = false;
@@ -286,7 +281,7 @@ export default defineComponent({
});
}
}
- } else if (this.type == 'emoji') {
+ } else if (this.type === 'emoji') {
if (this.q == null || this.q == '') {
// 最近使った絵文字をサジェスト
this.emojis = this.$store.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x != null);
@@ -314,6 +309,13 @@ export default defineComponent({
}
this.emojis = matched;
+ } else if (this.type === 'mfmTag') {
+ if (this.q == null || this.q == '') {
+ this.mfmTags = MFM_TAGS;
+ return;
+ }
+
+ this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
}
},
@@ -490,5 +492,11 @@ export default defineComponent({
margin: 0 0 0 8px;
}
}
+
+ > .mfmTags > li {
+
+ .name {
+ }
+ }
}
</style>
diff --git a/src/client/components/captcha.vue b/src/client/components/captcha.vue
index 5da8ede3b9..baa922506e 100644
--- a/src/client/components/captcha.vue
+++ b/src/client/components/captcha.vue
@@ -39,7 +39,7 @@ export default defineComponent({
type: String,
required: true,
},
- value: {
+ modelValue: {
type: String,
},
},
@@ -116,7 +116,7 @@ export default defineComponent({
}
},
callback(response?: string) {
- this.$emit('update:value', typeof response == 'string' ? response : null);
+ this.$emit('update:modelValue', typeof response == 'string' ? response : null);
},
},
});
diff --git a/src/client/components/channel-follow-button.vue b/src/client/components/channel-follow-button.vue
index 6f9405b97f..bd8627f6e8 100644
--- a/src/client/components/channel-follow-button.vue
+++ b/src/client/components/channel-follow-button.vue
@@ -91,7 +91,7 @@ export default defineComponent({
width: 31px;
}
- &:focus {
+ &:focus-visible {
&:after {
content: "";
pointer-events: none;
diff --git a/src/client/components/cw-button.vue b/src/client/components/cw-button.vue
index d2336085ad..3a172f5d5e 100644
--- a/src/client/components/cw-button.vue
+++ b/src/client/components/cw-button.vue
@@ -1,7 +1,7 @@
<template>
<button class="nrvgflfu _button" @click="toggle">
- <b>{{ value ? $ts._cw.hide : $ts._cw.show }}</b>
- <span v-if="!value">{{ label }}</span>
+ <b>{{ modelValue ? $ts._cw.hide : $ts._cw.show }}</b>
+ <span v-if="!modelValue">{{ label }}</span>
</button>
</template>
@@ -12,7 +12,7 @@ import { concat } from '../../prelude/array';
export default defineComponent({
props: {
- value: {
+ modelValue: {
type: Boolean,
required: true
},
@@ -36,7 +36,7 @@ export default defineComponent({
length,
toggle() {
- this.$emit('update:value', !this.value);
+ this.$emit('update:modelValue', !this.modelValue);
}
}
});
diff --git a/src/client/components/form/base.vue b/src/client/components/debobigego/base.vue
index 132942d527..f551a3478b 100644
--- a/src/client/components/form/base.vue
+++ b/src/client/components/debobigego/base.vue
@@ -21,39 +21,39 @@ export default defineComponent({
<style lang="scss" scoped>
.rbusrurv {
// 他のCSSからも参照されるので消さないように
- --formXPadding: 32px;
- --formYPadding: 32px;
+ --debobigegoXPadding: 32px;
+ --debobigegoYPadding: 32px;
- --formContentHMargin: 16px;
+ --debobigegoContentHMargin: 16px;
font-size: 95%;
line-height: 1.3em;
background: var(--bg);
- padding: var(--formYPadding) var(--formXPadding);
+ padding: var(--debobigegoYPadding) var(--debobigegoXPadding);
max-width: 750px;
margin: 0 auto;
&:not(.wide).max-width_400px {
- --formXPadding: 0px;
+ --debobigegoXPadding: 0px;
> ::v-deep(*) {
- ._formPanel {
+ ._debobigegoPanel {
border: solid 0.5px var(--divider);
border-radius: 0;
border-left: none;
border-right: none;
}
- ._form_group {
- > *:not(._formNoConcat) {
- &:not(:last-child):not(._formNoConcatPrev) {
- &._formPanel, ._formPanel {
+ ._debobigego_group {
+ > *:not(._debobigegoNoConcat) {
+ &:not(:last-child):not(._debobigegoNoConcatPrev) {
+ &._debobigegoPanel, ._debobigegoPanel {
border-bottom: solid 0.5px var(--divider);
}
}
- &:not(:first-child):not(._formNoConcatNext) {
- &._formPanel, ._formPanel {
+ &:not(:first-child):not(._debobigegoNoConcatNext) {
+ &._debobigegoPanel, ._debobigegoPanel {
border-top: none;
}
}
diff --git a/src/client/components/form/button.vue b/src/client/components/debobigego/button.vue
index b4f0890945..b883e817a4 100644
--- a/src/client/components/form/button.vue
+++ b/src/client/components/debobigego/button.vue
@@ -1,7 +1,7 @@
<template>
-<div class="yzpgjkxe _formItem">
- <div class="_formLabel"><slot name="label"></slot></div>
- <button class="main _button _formPanel _formClickable" :class="{ center, primary, danger }">
+<div class="yzpgjkxe _debobigegoItem">
+ <div class="_debobigegoLabel"><slot name="label"></slot></div>
+ <button class="main _button _debobigegoPanel _debobigegoClickable" :class="{ center, primary, danger }">
<slot></slot>
<div class="suffix">
<slot name="suffix"></slot>
@@ -10,13 +10,13 @@
</div>
</div>
</button>
- <div class="_formCaption"><slot name="desc"></slot></div>
+ <div class="_debobigegoCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import './form.scss';
+import './debobigego.scss';
export default defineComponent({
props: {
diff --git a/src/client/components/form/form.scss b/src/client/components/debobigego/debobigego.scss
index 00f40df9b1..833b656b66 100644
--- a/src/client/components/form/form.scss
+++ b/src/client/components/debobigego/debobigego.scss
@@ -1,9 +1,9 @@
-._formPanel {
+._debobigegoPanel {
background: var(--panel);
border-radius: var(--radius);
transition: background 0.2s ease;
- &._formClickable {
+ &._debobigegoClickable {
&:hover {
//background: var(--panelHighlight);
}
@@ -15,8 +15,8 @@
}
}
-._formLabel,
-._formCaption {
+._debobigegoLabel,
+._debobigegoCaption {
font-size: 80%;
color: var(--fgTransparentWeak);
@@ -25,28 +25,28 @@
}
}
-._formLabel {
+._debobigegoLabel {
position: sticky;
top: var(--stickyTop, 0px);
z-index: 2;
- margin: -8px calc(var(--formXPadding) * -1) 0 calc(var(--formXPadding) * -1);
- padding: 8px calc(var(--formContentHMargin) + var(--formXPadding)) 8px calc(var(--formContentHMargin) + var(--formXPadding));
+ margin: -8px calc(var(--debobigegoXPadding) * -1) 0 calc(var(--debobigegoXPadding) * -1);
+ padding: 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding)) 8px calc(var(--debobigegoContentHMargin) + var(--debobigegoXPadding));
background: var(--X17);
-webkit-backdrop-filter: var(--blur, blur(10px));
backdrop-filter: var(--blur, blur(10px));
}
-._themeChanging_ ._formLabel {
+._themeChanging_ ._debobigegoLabel {
transition: none !important;
background: transparent;
}
-._formCaption {
- padding: 8px var(--formContentHMargin) 0 var(--formContentHMargin);
+._debobigegoCaption {
+ padding: 8px var(--debobigegoContentHMargin) 0 var(--debobigegoContentHMargin);
}
-._formItem {
- & + ._formItem {
+._debobigegoItem {
+ & + ._debobigegoItem {
margin-top: 24px;
}
}
diff --git a/src/client/components/form/group.vue b/src/client/components/debobigego/group.vue
index 34ccaeff07..cba2c6ec94 100644
--- a/src/client/components/form/group.vue
+++ b/src/client/components/debobigego/group.vue
@@ -1,10 +1,10 @@
<template>
-<div class="vrtktovg _formItem _formNoConcat" v-size="{ max: [500] }" v-sticky-container>
- <div class="_formLabel"><slot name="label"></slot></div>
- <div class="main _form_group" ref="child">
+<div class="vrtktovg _debobigegoItem _debobigegoNoConcat" v-size="{ max: [500] }" v-sticky-container>
+ <div class="_debobigegoLabel"><slot name="label"></slot></div>
+ <div class="main _debobigego_group" ref="child">
<slot></slot>
</div>
- <div class="_formCaption"><slot name="caption"></slot></div>
+ <div class="_debobigegoCaption"><slot name="caption"></slot></div>
</div>
</template>
@@ -20,9 +20,9 @@ export default defineComponent({
const els = Array.from(child.value.children);
for (let i = 0; i < els.length; i++) {
const el = els[i];
- if (el.classList.contains('_formNoConcat')) {
- if (els[i - 1]) els[i - 1].classList.add('_formNoConcatPrev');
- if (els[i + 1]) els[i + 1].classList.add('_formNoConcatNext');
+ if (el.classList.contains('_debobigegoNoConcat')) {
+ if (els[i - 1]) els[i - 1].classList.add('_debobigegoNoConcatPrev');
+ if (els[i + 1]) els[i + 1].classList.add('_debobigegoNoConcatNext');
}
}
};
@@ -52,21 +52,21 @@ export default defineComponent({
<style lang="scss" scoped>
.vrtktovg {
> .main {
- > ::v-deep(*):not(._formNoConcat) {
- &:not(._formNoConcatNext) {
+ > ::v-deep(*):not(._debobigegoNoConcat) {
+ &:not(._debobigegoNoConcatNext) {
margin: 0;
}
- &:not(:last-child):not(._formNoConcatPrev) {
- &._formPanel, ._formPanel {
+ &:not(:last-child):not(._debobigegoNoConcatPrev) {
+ &._debobigegoPanel, ._debobigegoPanel {
border-bottom: solid 0.5px var(--divider);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
- &:not(:first-child):not(._formNoConcatNext) {
- &._formPanel, ._formPanel {
+ &:not(:first-child):not(._debobigegoNoConcatNext) {
+ &._debobigegoPanel, ._debobigegoPanel {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
diff --git a/src/client/components/form/info.vue b/src/client/components/debobigego/info.vue
index 9fdcbdca62..41afb03304 100644
--- a/src/client/components/form/info.vue
+++ b/src/client/components/debobigego/info.vue
@@ -1,6 +1,6 @@
<template>
-<div class="fzenkabp _formItem">
- <div class="_formPanel" :class="{ warn }">
+<div class="fzenkabp _debobigegoItem">
+ <div class="_debobigegoPanel" :class="{ warn }">
<i v-if="warn" class="fas fa-exclamation-triangle"></i>
<i v-else class="fas fa-info-circle"></i>
<slot></slot>
diff --git a/src/client/components/ui/input.vue b/src/client/components/debobigego/input.vue
index a916a0b035..d113f04d27 100644
--- a/src/client/components/ui/input.vue
+++ b/src/client/components/debobigego/input.vue
@@ -1,49 +1,53 @@
<template>
-<div class="matxzzsk">
- <div class="label" @click="focus"><slot name="label"></slot></div>
- <div class="input" :class="{ inline, disabled, focused }">
- <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
- <input 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>
+<FormGroup class="_debobigegoItem">
+ <template #label><slot></slot></template>
+ <div class="ztzhwixg _debobigegoItem" :class="{ inline, disabled }">
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input _debobigegoPanel">
+ <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+ <input 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>
</div>
- <div class="caption"><slot name="caption"></slot></div>
+ <template #caption><slot name="desc"></slot></template>
- <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-</div>
+ <FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+</FormGroup>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from './button.vue';
-import { debounce } from 'throttle-debounce';
+import './debobigego.scss';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
export default defineComponent({
components: {
- MkButton,
+ FormGroup,
+ FormButton,
},
-
props: {
modelValue: {
- required: true
+ required: false
},
type: {
type: String,
@@ -92,20 +96,13 @@ export default defineComponent({
required: false,
default: false
},
- debounce: {
- type: Boolean,
- required: false,
- default: false
- },
manualSave: {
type: Boolean,
required: false,
default: false
},
},
-
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
-
setup(props, context) {
const { modelValue, type, autofocus } = toRefs(props);
const v = ref(modelValue.value);
@@ -140,19 +137,13 @@ export default defineComponent({
}
};
- const debouncedUpdated = debounce(1000, updated);
-
- watch(modelValue, newValue => {
+ watch(modelValue.value, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (!props.manualSave) {
- if (props.debounce) {
- debouncedUpdated();
- } else {
- updated();
- }
+ updated();
}
invalid.value = inputEl.value.validity.badInput;
@@ -205,68 +196,59 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
-.matxzzsk {
- margin: 1.5em 0;
+.ztzhwixg {
+ position: relative;
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 12px;
- user-select: none;
+ > .icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 24px;
+ text-align: center;
+ line-height: 32px;
- &:empty {
- display: none;
- }
- }
-
- > .caption {
- font-size: 0.8em;
- padding: 8px 0 0 12px;
- color: var(--fgTransparentWeak);
-
- &:empty {
- display: none;
+ &:not(:empty) + .input {
+ margin-left: 28px;
}
}
> .input {
- $height: 42px;
+ $height: 48px;
position: relative;
> input {
- appearance: none;
- -webkit-appearance: none;
display: block;
height: $height;
width: 100%;
margin: 0;
- padding: 0 12px;
+ padding: 0 16px;
font: inherit;
font-weight: normal;
font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 0.5px var(--inputBorder);
- border-radius: 6px;
+ line-height: $height;
+ color: var(--inputText);
+ background: transparent;
+ border: none;
+ border-radius: 0;
outline: none;
box-shadow: none;
box-sizing: border-box;
- transition: border-color 0.1s ease-out;
- &:hover {
- border-color: var(--inputBorderHover);
+ &[type='file'] {
+ display: none;
}
}
> .prefix,
> .suffix {
- display: flex;
- align-items: center;
+ display: block;
position: absolute;
z-index: 1;
top: 0;
- padding: 0 12px;
+ padding: 0 16px;
font-size: 1em;
- height: $height;
+ line-height: $height;
+ color: var(--inputLabel);
pointer-events: none;
&:empty {
@@ -285,32 +267,25 @@ export default defineComponent({
> .prefix {
left: 0;
- padding-right: 6px;
+ padding-right: 8px;
}
> .suffix {
right: 0;
- padding-left: 6px;
- }
-
- &.inline {
- display: inline-block;
- margin: 0;
+ padding-left: 8px;
}
+ }
- &.focused {
- > input {
- border-color: var(--accent);
- //box-shadow: 0 0 0 4px var(--focus);
- }
- }
+ &.inline {
+ display: inline-block;
+ margin: 0;
+ }
- &.disabled {
- opacity: 0.7;
+ &.disabled {
+ opacity: 0.7;
- &, * {
- cursor: not-allowed !important;
- }
+ &, * {
+ cursor: not-allowed !important;
}
}
}
diff --git a/src/client/components/form/key-value-view.vue b/src/client/components/debobigego/key-value-view.vue
index ca4c09867f..0e034a2d54 100644
--- a/src/client/components/form/key-value-view.vue
+++ b/src/client/components/debobigego/key-value-view.vue
@@ -1,6 +1,6 @@
<template>
-<div class="_formItem">
- <div class="_formPanel anocepby">
+<div class="_debobigegoItem">
+ <div class="_debobigegoPanel anocepby">
<span class="key"><slot name="key"></slot></span>
<span class="value"><slot name="value"></slot></span>
</div>
@@ -9,7 +9,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import './form.scss';
+import './debobigego.scss';
export default defineComponent({
@@ -20,7 +20,7 @@ export default defineComponent({
.anocepby {
display: flex;
align-items: center;
- padding: 14px var(--formContentHMargin);
+ padding: 14px var(--debobigegoContentHMargin);
> .key {
margin-right: 12px;
diff --git a/src/client/components/form/link.vue b/src/client/components/debobigego/link.vue
index e1d13c6431..885579eadf 100644
--- a/src/client/components/form/link.vue
+++ b/src/client/components/debobigego/link.vue
@@ -1,6 +1,6 @@
<template>
-<div class="qmfkfnzi _formItem">
- <a class="main _button _formPanel _formClickable" :href="to" target="_blank" v-if="external">
+<div class="qmfkfnzi _debobigegoItem">
+ <a class="main _button _debobigegoPanel _debobigegoClickable" :href="to" target="_blank" v-if="external">
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span>
<span class="right">
@@ -8,7 +8,7 @@
<i class="fas fa-external-link-alt icon"></i>
</span>
</a>
- <MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" :behavior="behavior" v-else>
+ <MkA class="main _button _debobigegoPanel _debobigegoClickable" :class="{ active }" :to="to" :behavior="behavior" v-else>
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span>
<span class="right">
@@ -21,7 +21,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import './form.scss';
+import './debobigego.scss';
export default defineComponent({
props: {
diff --git a/src/client/components/form/object-view.vue b/src/client/components/debobigego/object-view.vue
index 59fb62b5e6..ea79daa915 100644
--- a/src/client/components/form/object-view.vue
+++ b/src/client/components/debobigego/object-view.vue
@@ -1,8 +1,8 @@
<template>
-<FormGroup class="_formItem">
+<FormGroup class="_debobigegoItem">
<template #label><slot></slot></template>
- <div class="drooglns _formItem" :class="{ tall }">
- <div class="input _formPanel">
+ <div class="drooglns _debobigegoItem" :class="{ tall }">
+ <div class="input _debobigegoPanel">
<textarea class="_monospace"
v-model="v"
readonly
@@ -17,7 +17,7 @@
<script lang="ts">
import { defineComponent, ref, toRefs, watch } from 'vue';
import * as JSON5 from 'json5';
-import './form.scss';
+import './debobigego.scss';
import FormGroup from './group.vue';
export default defineComponent({
@@ -75,7 +75,7 @@ export default defineComponent({
max-width: 100%;
min-height: 130px;
margin: 0;
- padding: 16px var(--formContentHMargin);
+ padding: 16px var(--debobigegoContentHMargin);
box-sizing: border-box;
font: inherit;
font-weight: normal;
diff --git a/src/client/components/form/pagination.vue b/src/client/components/debobigego/pagination.vue
index 0a2f1ff0e1..2166f5065f 100644
--- a/src/client/components/form/pagination.vue
+++ b/src/client/components/debobigego/pagination.vue
@@ -1,5 +1,5 @@
<template>
-<FormGroup class="uljviswt _formItem">
+<FormGroup class="uljviswt _debobigegoItem">
<template #label><slot name="label"></slot></template>
<slot :items="items"></slot>
<div class="empty" v-if="empty" key="_empty_">
diff --git a/src/client/components/debobigego/radios.vue b/src/client/components/debobigego/radios.vue
new file mode 100644
index 0000000000..071c013afb
--- /dev/null
+++ b/src/client/components/debobigego/radios.vue
@@ -0,0 +1,112 @@
+<script lang="ts">
+import { defineComponent, h } from 'vue';
+import MkRadio from '@client/components/form/radio.vue';
+import './debobigego.scss';
+
+export default defineComponent({
+ components: {
+ MkRadio
+ },
+ props: {
+ modelValue: {
+ required: false
+ },
+ },
+ data() {
+ return {
+ value: this.modelValue,
+ }
+ },
+ watch: {
+ modelValue() {
+ this.value = this.modelValue;
+ },
+ value() {
+ this.$emit('update:modelValue', this.value);
+ }
+ },
+ render() {
+ const label = this.$slots.desc();
+ let options = this.$slots.default();
+
+ // なぜかFragmentになることがあるため
+ if (options.length === 1 && options[0].props == null) options = options[0].children;
+
+ return h('div', {
+ class: 'cnklmpwm _debobigegoItem'
+ }, [
+ h('div', {
+ class: '_debobigegoLabel',
+ }, label),
+ ...options.map(option => h('button', {
+ class: '_button _debobigegoPanel _debobigegoClickable',
+ key: option.key,
+ 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: 16px;
+ height: 16px;
+ 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>
diff --git a/src/client/components/debobigego/range.vue b/src/client/components/debobigego/range.vue
new file mode 100644
index 0000000000..26fb0f37c6
--- /dev/null
+++ b/src/client/components/debobigego/range.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="ifitouly _debobigegoItem" :class="{ focused, disabled }">
+ <div class="_debobigegoLabel"><slot name="label"></slot></div>
+ <div class="_debobigegoPanel 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="_debobigegoCaption"><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: 22px 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>
diff --git a/src/client/components/debobigego/select.vue b/src/client/components/debobigego/select.vue
new file mode 100644
index 0000000000..7a31371afc
--- /dev/null
+++ b/src/client/components/debobigego/select.vue
@@ -0,0 +1,145 @@
+<template>
+<div class="yrtfrpux _debobigegoItem" :class="{ disabled, inline }">
+ <div class="_debobigegoLabel"><slot name="label"></slot></div>
+ <div class="icon" ref="icon"><slot name="icon"></slot></div>
+ <div class="input _debobigegoPanel _debobigegoClickable" @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">
+ <i class="fas fa-chevron-down"></i>
+ </div>
+ </div>
+ <div class="_debobigegoCaption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './debobigego.scss';
+
+export default defineComponent({
+ props: {
+ modelValue: {
+ required: false
+ },
+ required: {
+ type: Boolean,
+ required: false
+ },
+ disabled: {
+ type: Boolean,
+ required: false
+ },
+ inline: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ },
+ data() {
+ return {
+ };
+ },
+ computed: {
+ v: {
+ get() {
+ return this.modelValue;
+ },
+ set(v) {
+ this.$emit('update:modelValue', 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: 48px;
+ 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>
diff --git a/src/client/components/form/suspense.vue b/src/client/components/debobigego/suspense.vue
index d04dc07624..e59e0ba12d 100644
--- a/src/client/components/form/suspense.vue
+++ b/src/client/components/debobigego/suspense.vue
@@ -1,15 +1,15 @@
<template>
<transition name="fade" mode="out-in">
- <div class="_formItem" v-if="pending">
- <div class="_formPanel">
+ <div class="_debobigegoItem" v-if="pending">
+ <div class="_debobigegoPanel">
<MkLoading/>
</div>
</div>
- <div v-else-if="resolved" class="_formItem">
+ <div v-else-if="resolved" class="_debobigegoItem">
<slot :result="result"></slot>
</div>
- <div class="_formItem" v-else>
- <div class="_formPanel eiurkvay">
+ <div class="_debobigegoItem" v-else>
+ <div class="_debobigegoPanel eiurkvay">
<div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
<MkButton inline @click="retry" class="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
</div>
@@ -19,7 +19,7 @@
<script lang="ts">
import { defineComponent, PropType, ref, watch } from 'vue';
-import './form.scss';
+import './debobigego.scss';
import MkButton from '@client/components/ui/button.vue';
export default defineComponent({
diff --git a/src/client/components/debobigego/switch.vue b/src/client/components/debobigego/switch.vue
new file mode 100644
index 0000000000..9a69e18302
--- /dev/null
+++ b/src/client/components/debobigego/switch.vue
@@ -0,0 +1,132 @@
+<template>
+<div class="ijnpvmgr _debobigegoItem">
+ <div class="main _debobigegoPanel _debobigegoClickable"
+ :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" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
+ <span class="handle"></span>
+ </span>
+ <span class="label">
+ <span><slot></slot></span>
+ </span>
+ </div>
+ <div class="_debobigegoCaption"><slot name="desc"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import './debobigego.scss';
+
+export default defineComponent({
+ props: {
+ modelValue: {
+ type: Boolean,
+ default: false
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ }
+ },
+ computed: {
+ checked(): boolean {
+ return this.modelValue;
+ }
+ },
+ methods: {
+ toggle() {
+ if (this.disabled) return;
+ this.$emit('update:modelValue', !this.checked);
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.ijnpvmgr {
+ > .main {
+ position: relative;
+ display: flex;
+ padding: 14px 16px;
+ cursor: pointer;
+
+ > * {
+ user-select: none;
+ }
+
+ > input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ margin: 0;
+ }
+
+ > .button {
+ position: relative;
+ display: inline-block;
+ flex-shrink: 0;
+ margin: 0;
+ width: 34px;
+ height: 22px;
+ background: var(--switchBg);
+ outline: none;
+ border-radius: 999px;
+ transition: all 0.3s;
+ cursor: pointer;
+
+ > .handle {
+ position: absolute;
+ top: 0;
+ left: 3px;
+ bottom: 0;
+ margin: auto 0;
+ border-radius: 100%;
+ transition: background-color 0.3s, transform 0.3s;
+ width: 16px;
+ height: 16px;
+ background-color: #fff;
+ pointer-events: none;
+ }
+ }
+
+ > .label {
+ margin-left: 12px;
+ display: block;
+ transition: inherit;
+ color: var(--fg);
+
+ > span {
+ display: block;
+ line-height: 20px;
+ transition: inherit;
+ }
+ }
+
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &.checked {
+ > .button {
+ background-color: var(--accent);
+
+ > .handle {
+ transform: translateX(12px);
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/debobigego/textarea.vue b/src/client/components/debobigego/textarea.vue
new file mode 100644
index 0000000000..64e8d47126
--- /dev/null
+++ b/src/client/components/debobigego/textarea.vue
@@ -0,0 +1,161 @@
+<template>
+<FormGroup class="_debobigegoItem">
+ <template #label><slot></slot></template>
+ <div class="rivhosbp _debobigegoItem" :class="{ tall, pre }">
+ <div class="input _debobigegoPanel">
+ <textarea ref="input" :class="{ code, _monospace: code }"
+ v-model="v"
+ :required="required"
+ :readonly="readonly"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="!code"
+ @input="onInput"
+ @focus="focused = true"
+ @blur="focused = false"
+ ></textarea>
+ </div>
+ </div>
+ <template #caption><slot name="desc"></slot></template>
+
+ <FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+</FormGroup>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, toRefs, watch } from 'vue';
+import './debobigego.scss';
+import FormButton from './button.vue';
+import FormGroup from './group.vue';
+
+export default defineComponent({
+ components: {
+ FormGroup,
+ FormButton,
+ },
+ props: {
+ modelValue: {
+ 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
+ },
+ manualSave: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ },
+ setup(props, context) {
+ const { modelValue } = toRefs(props);
+ const v = ref(modelValue.value);
+ const changed = ref(false);
+ const inputEl = ref(null);
+ const focus = () => inputEl.value.focus();
+ const onInput = (ev) => {
+ changed.value = true;
+ context.emit('change', ev);
+ };
+
+ const updated = () => {
+ changed.value = false;
+ context.emit('update:modelValue', v.value);
+ };
+
+ watch(modelValue.value, newValue => {
+ v.value = newValue;
+ });
+
+ watch(v, newValue => {
+ if (!props.manualSave) {
+ updated();
+ }
+ });
+
+ return {
+ v,
+ updated,
+ changed,
+ focus,
+ onInput,
+ };
+ }
+});
+</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;
+ }
+ }
+ }
+
+ &.tall {
+ > .input {
+ > textarea {
+ min-height: 200px;
+ }
+ }
+ }
+
+ &.pre {
+ > .input {
+ > textarea {
+ white-space: pre;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/form/tuple.vue b/src/client/components/debobigego/tuple.vue
index 6c8a22d189..8a4599fd64 100644
--- a/src/client/components/form/tuple.vue
+++ b/src/client/components/debobigego/tuple.vue
@@ -1,5 +1,5 @@
<template>
-<div class="wthhikgt _formItem" v-size="{ max: [500] }">
+<div class="wthhikgt _debobigegoItem" v-size="{ max: [500] }">
<slot></slot>
</div>
</template>
diff --git a/src/client/components/dialog.vue b/src/client/components/dialog.vue
index f3611f050e..dd4932f61f 100644
--- a/src/client/components/dialog.vue
+++ b/src/client/components/dialog.vue
@@ -40,8 +40,8 @@
import { defineComponent } from 'vue';
import MkModal from '@client/components/ui/modal.vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
export default defineComponent({
components: {
diff --git a/src/client/components/emoji-picker-window.vue b/src/client/components/emoji-picker-window.vue
index 53b6ae6b32..b7b884565b 100644
--- a/src/client/components/emoji-picker-window.vue
+++ b/src/client/components/emoji-picker-window.vue
@@ -153,7 +153,7 @@ export default defineComponent({
height: var(--eachSize);
border-radius: 4px;
- &:focus {
+ &:focus-visible {
outline: solid 2px var(--focus);
z-index: 1;
}
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index d8703202c7..85a12a08e6 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -465,7 +465,7 @@ export default defineComponent({
height: var(--eachSize);
border-radius: 4px;
- &:focus {
+ &:focus-visible {
outline: solid 2px var(--focus);
z-index: 1;
}
diff --git a/src/client/components/follow-button.vue b/src/client/components/follow-button.vue
index 5685b86a51..5eba9b1f6b 100644
--- a/src/client/components/follow-button.vue
+++ b/src/client/components/follow-button.vue
@@ -161,7 +161,7 @@ export default defineComponent({
width: 31px;
}
- &:focus {
+ &:focus-visible {
&:after {
content: "";
pointer-events: none;
diff --git a/src/client/components/forgot-password.vue b/src/client/components/forgot-password.vue
index 3b5ad6d6ba..cb2380f483 100644
--- a/src/client/components/forgot-password.vue
+++ b/src/client/components/forgot-password.vue
@@ -35,7 +35,7 @@
import { defineComponent } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/components/form-dialog.vue b/src/client/components/form-dialog.vue
index e13592b488..6353b7287e 100644
--- a/src/client/components/form-dialog.vue
+++ b/src/client/components/form-dialog.vue
@@ -14,23 +14,23 @@
</template>
<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">
+ <FormInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormInput>
- <FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
+ <FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model="values[item]" type="text">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormInput>
- <FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
+ <FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model="values[item]">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormTextarea>
- <FormSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
+ <FormSwitch v-else-if="form[item].type === 'boolean'" v-model="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormSwitch>
- <FormSelect v-else-if="form[item].type === 'enum'" v-model:value="values[item]">
+ <FormSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
</FormSelect>
@@ -38,7 +38,7 @@
<template #desc><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
<option v-for="item in form[item].options" :value="item.value" :key="item.value">{{ item.label }}</option>
</FormRadios>
- <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">
+ <FormRange v-else-if="form[item].type === 'range'" v-model="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"> ({{ $ts.optional }})</span></template>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormRange>
@@ -53,14 +53,14 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.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';
-import FormRadios from './form/radios.vue';
+import FormBase from './debobigego/base.vue';
+import FormInput from './debobigego/input.vue';
+import FormTextarea from './debobigego/textarea.vue';
+import FormSwitch from './debobigego/switch.vue';
+import FormSelect from './debobigego/select.vue';
+import FormRange from './debobigego/range.vue';
+import FormButton from './debobigego/button.vue';
+import FormRadios from './debobigego/radios.vue';
export default defineComponent({
components: {
diff --git a/src/client/components/form/input.vue b/src/client/components/form/input.vue
index 942ac4dfd2..d7b6f77519 100644
--- a/src/client/components/form/input.vue
+++ b/src/client/components/form/input.vue
@@ -1,53 +1,49 @@
<template>
-<FormGroup class="_formItem">
- <template #label><slot></slot></template>
- <div class="ztzhwixg _formItem" :class="{ inline, disabled }">
- <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 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>
+<div class="matxzzsk">
+ <div class="label" @click="focus"><slot name="label"></slot></div>
+ <div class="input" :class="{ inline, disabled, focused }">
+ <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+ <input 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>
- <template #caption><slot name="desc"></slot></template>
+ <div class="caption"><slot name="caption"></slot></div>
- <FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-</FormGroup>
+ <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import './form.scss';
-import FormButton from './button.vue';
-import FormGroup from './group.vue';
+import MkButton from '../ui/button.vue';
+import { debounce } from 'throttle-debounce';
export default defineComponent({
components: {
- FormGroup,
- FormButton,
+ MkButton,
},
+
props: {
- value: {
- required: false
+ modelValue: {
+ required: true
},
type: {
type: String,
@@ -96,16 +92,23 @@ export default defineComponent({
required: false,
default: false
},
+ debounce: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
manualSave: {
type: Boolean,
required: false,
default: false
},
},
- emits: ['change', 'keydown', 'enter'],
+
+ emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+
setup(props, context) {
- const { value, type, autofocus } = toRefs(props);
- const v = ref(value.value);
+ const { modelValue, type, autofocus } = toRefs(props);
+ const v = ref(modelValue.value);
const id = Math.random().toString(); // TODO: uuid?
const focused = ref(false);
const changed = ref(false);
@@ -131,19 +134,25 @@ export default defineComponent({
const updated = () => {
changed.value = false;
if (type?.value === 'number') {
- context.emit('update:value', parseFloat(v.value));
+ context.emit('update:modelValue', parseFloat(v.value));
} else {
- context.emit('update:value', v.value);
+ context.emit('update:modelValue', v.value);
}
};
- watch(value, newValue => {
+ const debouncedUpdated = debounce(1000, updated);
+
+ watch(modelValue, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (!props.manualSave) {
- updated();
+ if (props.debounce) {
+ debouncedUpdated();
+ } else {
+ updated();
+ }
}
invalid.value = inputEl.value.validity.badInput;
@@ -196,59 +205,66 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
-.ztzhwixg {
- position: relative;
+.matxzzsk {
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 12px;
+ user-select: none;
+
+ &:empty {
+ display: none;
+ }
+ }
- > .icon {
- position: absolute;
- top: 0;
- left: 0;
- width: 24px;
- text-align: center;
- line-height: 32px;
+ > .caption {
+ font-size: 0.8em;
+ padding: 8px 0 0 12px;
+ color: var(--fgTransparentWeak);
- &:not(:empty) + .input {
- margin-left: 28px;
+ &:empty {
+ display: none;
}
}
> .input {
- $height: 48px;
+ $height: 42px;
position: relative;
> input {
+ appearance: none;
+ -webkit-appearance: none;
display: block;
height: $height;
width: 100%;
margin: 0;
- padding: 0 16px;
+ padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
- line-height: $height;
- color: var(--inputText);
- background: transparent;
- border: none;
- border-radius: 0;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 0.5px var(--inputBorder);
+ border-radius: 6px;
outline: none;
box-shadow: none;
box-sizing: border-box;
+ transition: border-color 0.1s ease-out;
- &[type='file'] {
- display: none;
+ &:hover {
+ border-color: var(--inputBorderHover);
}
}
> .prefix,
> .suffix {
- display: block;
+ display: flex;
+ align-items: center;
position: absolute;
z-index: 1;
top: 0;
- padding: 0 16px;
+ padding: 0 12px;
font-size: 1em;
- line-height: $height;
- color: var(--inputLabel);
+ height: $height;
pointer-events: none;
&:empty {
@@ -267,25 +283,32 @@ export default defineComponent({
> .prefix {
left: 0;
- padding-right: 8px;
+ padding-right: 6px;
}
> .suffix {
right: 0;
- padding-left: 8px;
+ padding-left: 6px;
}
- }
- &.inline {
- display: inline-block;
- margin: 0;
- }
+ &.inline {
+ display: inline-block;
+ margin: 0;
+ }
+
+ &.focused {
+ > input {
+ border-color: var(--accent);
+ //box-shadow: 0 0 0 4px var(--focus);
+ }
+ }
- &.disabled {
- opacity: 0.7;
+ &.disabled {
+ opacity: 0.7;
- &, * {
- cursor: not-allowed !important;
+ &, * {
+ cursor: not-allowed !important;
+ }
}
}
}
diff --git a/src/client/components/ui/radio.vue b/src/client/components/form/radio.vue
index 0f31d8fa0a..0f31d8fa0a 100644
--- a/src/client/components/ui/radio.vue
+++ b/src/client/components/form/radio.vue
diff --git a/src/client/components/form/radios.vue b/src/client/components/form/radios.vue
index b660c37ace..1d3d80172a 100644
--- a/src/client/components/form/radios.vue
+++ b/src/client/components/form/radios.vue
@@ -1,7 +1,6 @@
<script lang="ts">
import { defineComponent, h } from 'vue';
-import MkRadio from '@client/components/ui/radio.vue';
-import './form.scss';
+import MkRadio from './radio.vue';
export default defineComponent({
components: {
@@ -18,9 +17,6 @@ export default defineComponent({
}
},
watch: {
- modelValue() {
- this.value = this.modelValue;
- },
value() {
this.$emit('update:modelValue', this.value);
}
@@ -33,80 +29,38 @@ export default defineComponent({
if (options.length === 1 && options[0].props == null) options = options[0].children;
return h('div', {
- class: 'cnklmpwm _formItem'
+ class: 'novjtcto'
}, [
- h('div', {
- class: '_formLabel',
- }, label),
- ...options.map(option => h('button', {
- class: '_button _formPanel _formClickable',
+ h('div', { class: 'label' }, label),
+ ...options.map(option => h(MkRadio, {
key: option.key,
- onClick: () => this.value = option.props.value,
- }, [h('span', {
- class: ['check', { checked: this.value === option.props.value }],
- }), option.children]))
+ value: option.props.value,
+ modelValue: this.value,
+ 'onUpdate:modelValue': value => this.value = 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;
- }
+.novjtcto {
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 12px;
+ user-select: none;
- &:not(:last-of-type) {
- border-bottom: solid 0.5px var(--divider);
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
+ &:empty {
+ display: none;
}
+ }
- > .check {
- display: inline-block;
- vertical-align: bottom;
- position: relative;
- width: 16px;
- height: 16px;
- 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);
+ &:first-child {
+ margin-top: 0;
+ }
- &:after {
- background-color: var(--accent);
- transform: scale(1);
- opacity: 1;
- }
- }
- }
+ &:last-child {
+ margin-bottom: 0;
}
}
</style>
diff --git a/src/client/components/form/range.vue b/src/client/components/form/range.vue
index 65d665c70a..4cfe66a8fc 100644
--- a/src/client/components/form/range.vue
+++ b/src/client/components/form/range.vue
@@ -1,21 +1,20 @@
<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 class="timctyfi" :class="{ focused, disabled }">
+ <div class="icon"><slot name="icon"></slot></div>
+ <span class="label"><slot name="label"></slot></span>
+ <input
+ type="range"
+ ref="input"
+ v-model="v"
+ :disabled="disabled"
+ :min="min"
+ :max="max"
+ :step="step"
+ :autofocus="autofocus"
+ @focus="focused = true"
+ @blur="focused = false"
+ @input="$emit('update:value', $event.target.value)"
+ />
</div>
</template>
@@ -49,6 +48,10 @@ export default defineComponent({
required: false,
default: 1
},
+ autofocus: {
+ type: Boolean,
+ required: false
+ }
},
data() {
return {
@@ -61,61 +64,75 @@ export default defineComponent({
this.v = parseFloat(v);
}
},
+ mounted() {
+ if (this.autofocus) {
+ this.$nextTick(() => {
+ this.$refs.input.focus();
+ });
+ }
+ }
});
</script>
<style lang="scss" scoped>
-.ifitouly {
+.timctyfi {
position: relative;
+ margin: 8px;
- > .main {
- padding: 22px 16px;
+ > .icon {
+ display: inline-block;
+ width: 24px;
+ text-align: center;
+ }
- > 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;
+ > .title {
+ pointer-events: none;
+ font-size: 16px;
+ color: var(--inputLabel);
+ overflow: hidden;
+ }
- &.disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
+ > input {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background: var(--X10);
+ height: 7px;
+ margin: 0 8px;
+ outline: 0;
+ border: 0;
+ border-radius: 7px;
- &::-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;
- }
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
- &::-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);
- }
+ &::-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);
}
}
}
diff --git a/src/client/components/form/section.vue b/src/client/components/form/section.vue
new file mode 100644
index 0000000000..8eac40a0db
--- /dev/null
+++ b/src/client/components/form/section.vue
@@ -0,0 +1,31 @@
+<template>
+<div class="vrtktovh" v-size="{ max: [500] }" v-sticky-container>
+ <div class="label"><slot name="label"></slot></div>
+ <div class="main">
+ <slot></slot>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+
+});
+</script>
+
+<style lang="scss" scoped>
+.vrtktovh {
+ border-top: solid 0.5px var(--divider);
+
+ > .label {
+ font-weight: bold;
+ padding: 24px 0 16px 0;
+ }
+
+ > .main {
+ margin-bottom: 32px;
+ }
+}
+</style>
diff --git a/src/client/components/form/select.vue b/src/client/components/form/select.vue
index 1c5a473451..257e2cc990 100644
--- a/src/client/components/form/select.vue
+++ b/src/client/components/form/select.vue
@@ -1,125 +1,216 @@
<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"
+<div class="vblkjoeq">
+ <div class="label" @click="focus"><slot name="label"></slot></div>
+ <div class="input" :class="{ inline, disabled, focused }">
+ <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
+ <select ref="inputEl"
v-model="v"
- :required="required"
:disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
@focus="focused = true"
@blur="focused = false"
+ @input="onInput"
>
<slot></slot>
</select>
- <div class="suffix">
- <i class="fas fa-chevron-down"></i>
- </div>
+ <div class="suffix" ref="suffixEl"><i class="fas fa-chevron-down"></i></div>
</div>
- <div class="_formCaption"><slot name="caption"></slot></div>
+ <div class="caption"><slot name="caption"></slot></div>
+
+ <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
</div>
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
-import './form.scss';
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import MkButton from '../ui/button.vue';
export default defineComponent({
+ components: {
+ MkButton,
+ },
+
props: {
- value: {
- required: false
+ modelValue: {
+ required: true
},
required: {
type: Boolean,
required: false
},
+ readonly: {
+ type: Boolean,
+ required: false
+ },
disabled: {
type: Boolean,
required: false
},
+ placeholder: {
+ type: String,
+ required: false
+ },
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
inline: {
type: Boolean,
required: false,
default: false
},
+ manualSave: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
},
- data() {
- return {
+
+ emits: ['change', 'update:modelValue'],
+
+ setup(props, context) {
+ const { modelValue, autofocus } = toRefs(props);
+ const v = ref(modelValue.value);
+ 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);
};
- },
- computed: {
- v: {
- get() {
- return this.value;
- },
- set(v) {
- this.$emit('update:value', v);
+
+ const updated = () => {
+ changed.value = false;
+ context.emit('update:modelValue', v.value);
+ };
+
+ watch(modelValue, newValue => {
+ v.value = newValue;
+ });
+
+ watch(v, newValue => {
+ if (!props.manualSave) {
+ updated();
}
- },
+
+ 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 {
+ v,
+ focused,
+ invalid,
+ changed,
+ filled,
+ inputEl,
+ prefixEl,
+ suffixEl,
+ focus,
+ onInput,
+ updated,
+ };
},
- methods: {
- focus() {
- this.$refs.input.focus();
- }
- }
});
</script>
<style lang="scss" scoped>
-.yrtfrpux {
- position: relative;
+.vblkjoeq {
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 12px;
+ user-select: none;
- > .icon {
- position: absolute;
- top: 0;
- left: 0;
- width: 24px;
- text-align: center;
- line-height: 32px;
+ &:empty {
+ display: none;
+ }
+ }
- &:not(:empty) + .input {
- margin-left: 28px;
+ > .caption {
+ font-size: 0.8em;
+ padding: 8px 0 0 12px;
+ color: var(--fgTransparentWeak);
+
+ &:empty {
+ display: none;
}
}
> .input {
- display: flex;
+ $height: 42px;
position: relative;
> select {
+ appearance: none;
+ -webkit-appearance: none;
display: block;
- flex: 1;
+ height: $height;
width: 100%;
- padding: 0 16px;
+ margin: 0;
+ padding: 0 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
- height: 48px;
- background: none;
- border: none;
- border-radius: 0;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 1px var(--inputBorder);
+ border-radius: 6px;
outline: none;
box-shadow: none;
- appearance: none;
- -webkit-appearance: none;
- color: var(--fg);
+ box-sizing: border-box;
+ cursor: pointer;
+ transition: border-color 0.1s ease-out;
- option,
- optgroup {
- color: var(--fg);
- background: var(--bg);
+ &:hover {
+ border-color: var(--inputBorderHover);
}
}
> .prefix,
> .suffix {
- display: block;
- align-self: center;
- justify-self: center;
+ display: flex;
+ align-items: center;
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ padding: 0 12px;
font-size: 1em;
- line-height: 32px;
- color: var(--inputLabel);
+ height: $height;
pointer-events: none;
&:empty {
@@ -127,18 +218,42 @@ export default defineComponent({
}
> * {
- display: block;
+ display: inline-block;
min-width: 16px;
+ max-width: 150px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
}
}
> .prefix {
- padding-right: 4px;
+ left: 0;
+ padding-right: 6px;
}
> .suffix {
- padding: 0 16px 0 0;
+ right: 0;
+ padding-left: 6px;
+ }
+
+ &.inline {
+ display: inline-block;
+ margin: 0;
+ }
+
+ &.focused {
+ > select {
+ border-color: var(--accent);
+ }
+ }
+
+ &.disabled {
opacity: 0.7;
+
+ &, * {
+ cursor: not-allowed !important;
+ }
}
}
}
diff --git a/src/client/components/form/slot.vue b/src/client/components/form/slot.vue
new file mode 100644
index 0000000000..8580c1307d
--- /dev/null
+++ b/src/client/components/form/slot.vue
@@ -0,0 +1,50 @@
+<template>
+<div class="adhpbeou">
+ <div class="label" @click="focus"><slot name="label"></slot></div>
+ <div class="content">
+ <slot></slot>
+ </div>
+ <div class="caption"><slot name="caption"></slot></div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+export default defineComponent({
+
+});
+</script>
+
+<style lang="scss" scoped>
+.adhpbeou {
+ margin: 1.5em 0;
+
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 12px;
+ user-select: none;
+
+ &:empty {
+ display: none;
+ }
+ }
+
+ > .caption {
+ font-size: 0.8em;
+ padding: 8px 0 0 12px;
+ color: var(--fgTransparentWeak);
+
+ &:empty {
+ display: none;
+ }
+ }
+
+ > .content {
+ position: relative;
+ background: var(--panel);
+ border: solid 0.5px var(--inputBorder);
+ border-radius: 6px;
+ }
+}
+</style>
diff --git a/src/client/components/form/switch.vue b/src/client/components/form/switch.vue
index e7ef714c49..85f8b7c870 100644
--- a/src/client/components/form/switch.vue
+++ b/src/client/components/form/switch.vue
@@ -1,35 +1,34 @@
<template>
-<div class="ijnpvmgr _formItem">
- <div class="main _formPanel _formClickable"
- :class="{ disabled, checked }"
- :aria-checked="checked"
- :aria-disabled="disabled"
- @click.prevent="toggle"
+<div
+ class="ziffeoms"
+ :class="{ disabled, checked }"
+ role="switch"
+ :aria-checked="checked"
+ :aria-disabled="disabled"
+ @click.prevent="toggle"
+>
+ <input
+ type="checkbox"
+ ref="input"
+ :disabled="disabled"
+ @keydown.enter="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>
+ <span class="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff">
+ <span class="handle"></span>
+ </span>
+ <span class="label">
+ <span><slot></slot></span>
+ <p><slot name="caption"></slot></p>
+ </span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import './form.scss';
export default defineComponent({
props: {
- value: {
+ modelValue: {
type: Boolean,
default: false
},
@@ -40,91 +39,110 @@ export default defineComponent({
},
computed: {
checked(): boolean {
- return this.value;
+ return this.modelValue;
}
},
methods: {
toggle() {
if (this.disabled) return;
- this.$emit('update:value', !this.checked);
+ this.$emit('update:modelValue', !this.checked);
}
}
});
</script>
<style lang="scss" scoped>
-.ijnpvmgr {
- > .main {
- position: relative;
- display: flex;
- padding: 14px 16px;
- cursor: pointer;
+.ziffeoms {
+ position: relative;
+ display: flex;
+ cursor: pointer;
+ transition: all 0.3s;
- > * {
- user-select: none;
- }
+ &:first-child {
+ margin-top: 0;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ > * {
+ user-select: none;
+ }
+
+ > input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ margin: 0;
+ }
- &.disabled {
- opacity: 0.6;
- cursor: not-allowed;
+ > .button {
+ position: relative;
+ display: inline-block;
+ flex-shrink: 0;
+ margin: 0;
+ width: 36px;
+ height: 26px;
+ background: var(--switchBg);
+ outline: none;
+ border-radius: 999px;
+ transition: inherit;
+
+ > .handle {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 5px;
+ margin: auto 0;
+ border-radius: 100%;
+ transition: background-color 0.3s, transform 0.3s;
+ width: 16px;
+ height: 16px;
+ background-color: #fff;
}
+ }
- &.checked {
- > .button {
- background-color: var(--X10);
- border-color: var(--X10);
+ > .label {
+ margin-left: 16px;
+ margin-top: 2px;
+ display: block;
+ cursor: pointer;
+ transition: inherit;
+ color: var(--fg);
- > * {
- background-color: var(--accent);
- transform: translateX(14px);
- }
- }
+ > span {
+ display: block;
+ line-height: 20px;
+ transition: inherit;
}
- > input {
- position: absolute;
- width: 0;
- height: 0;
- opacity: 0;
+ > p {
margin: 0;
+ color: var(--fgTransparentWeak);
+ font-size: 90%;
}
+ }
+ &:hover {
> .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);
- }
+ background-color: var(--accentedBg);
}
+ }
- > .label {
- margin-left: 12px;
- display: block;
- transition: inherit;
- color: var(--fg);
+ &.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ &.checked {
+ > .button {
+ background-color: var(--accent);
+ border-color: var(--accent);
- > span {
- display: block;
- line-height: 20px;
- transition: inherit;
+ > .handle {
+ transform: translateX(10px);
}
}
}
diff --git a/src/client/components/form/textarea.vue b/src/client/components/form/textarea.vue
index 8f42581a9b..50be69f930 100644
--- a/src/client/components/form/textarea.vue
+++ b/src/client/components/form/textarea.vue
@@ -1,40 +1,45 @@
<template>
-<FormGroup class="_formItem">
- <template #label><slot></slot></template>
- <div class="rivhosbp _formItem" :class="{ tall, pre }">
- <div class="input _formPanel">
- <textarea ref="input" :class="{ code, _monospace: code }"
- v-model="v"
- :required="required"
- :readonly="readonly"
- :pattern="pattern"
- :autocomplete="autocomplete"
- :spellcheck="!code"
- @input="onInput"
- @focus="focused = true"
- @blur="focused = false"
- ></textarea>
- </div>
+<div class="adhpbeos">
+ <div class="label" @click="focus"><slot name="label"></slot></div>
+ <div class="input" :class="{ disabled, focused, tall, pre }">
+ <textarea ref="inputEl"
+ :class="{ code, _monospace: code }"
+ v-model="v"
+ :disabled="disabled"
+ :required="required"
+ :readonly="readonly"
+ :placeholder="placeholder"
+ :pattern="pattern"
+ :autocomplete="autocomplete"
+ :spellcheck="spellcheck"
+ @focus="focused = true"
+ @blur="focused = false"
+ @keydown="onKeydown($event)"
+ @input="onInput"
+ ></textarea>
</div>
- <template #caption><slot name="desc"></slot></template>
+ <div class="caption"><slot name="caption"></slot></div>
- <FormButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
-</FormGroup>
+ <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+</div>
</template>
<script lang="ts">
-import { defineComponent, ref, toRefs, watch } from 'vue';
-import './form.scss';
-import FormButton from './button.vue';
-import FormGroup from './group.vue';
+import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
+import MkButton from '../ui/button.vue';
+import { debounce } from 'throttle-debounce';
export default defineComponent({
components: {
- FormGroup,
- FormButton,
+ MkButton,
},
+
props: {
- value: {
+ modelValue: {
+ required: true
+ },
+ type: {
+ type: String,
required: false
},
required: {
@@ -45,14 +50,29 @@ export default defineComponent({
type: Boolean,
required: false
},
+ disabled: {
+ type: Boolean,
+ required: false
+ },
pattern: {
type: String,
required: false
},
- autocomplete: {
+ placeholder: {
type: String,
required: false
},
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ autocomplete: {
+ required: false
+ },
+ spellcheck: {
+ required: false
+ },
code: {
type: Boolean,
required: false
@@ -67,91 +87,162 @@ export default defineComponent({
required: false,
default: false
},
+ debounce: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
manualSave: {
type: Boolean,
required: false,
default: false
},
},
+
+ emits: ['change', 'keydown', 'enter', 'update:modelValue'],
+
setup(props, context) {
- const { value } = toRefs(props);
- const v = ref(value.value);
+ const { modelValue, autofocus } = toRefs(props);
+ const v = ref(modelValue.value);
+ 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 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');
+ }
+ };
const updated = () => {
changed.value = false;
- context.emit('update:value', v.value);
+ context.emit('update:modelValue', v.value);
};
- watch(value, newValue => {
+ const debouncedUpdated = debounce(1000, updated);
+
+ watch(modelValue, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (!props.manualSave) {
- updated();
+ if (props.debounce) {
+ debouncedUpdated();
+ } else {
+ updated();
+ }
}
+
+ invalid.value = inputEl.value.validity.badInput;
});
-
+
+ onMounted(() => {
+ nextTick(() => {
+ if (autofocus.value) {
+ focus();
+ }
+ });
+ });
+
return {
v,
- updated,
+ focused,
+ invalid,
changed,
+ filled,
+ inputEl,
focus,
onInput,
+ onKeydown,
+ updated,
};
- }
+ },
});
</script>
<style lang="scss" scoped>
-.rivhosbp {
- position: relative;
+.adhpbeos {
+ > .label {
+ font-size: 0.85em;
+ padding: 0 0 8px 12px;
+ user-select: none;
+
+ &:empty {
+ display: none;
+ }
+ }
+
+ > .caption {
+ font-size: 0.8em;
+ padding: 8px 0 0 12px;
+ color: var(--fgTransparentWeak);
+
+ &:empty {
+ display: none;
+ }
+ }
> .input {
position: relative;
-
+
> textarea {
+ appearance: none;
+ -webkit-appearance: none;
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 130px;
margin: 0;
- padding: 16px;
- box-sizing: border-box;
+ padding: 12px;
font: inherit;
font-weight: normal;
font-size: 1em;
- background: transparent;
- border: none;
- border-radius: 0;
+ color: var(--fg);
+ background: var(--panel);
+ border: solid 0.5px var(--inputBorder);
+ border-radius: 6px;
outline: none;
box-shadow: none;
- color: var(--fg);
+ box-sizing: border-box;
+ transition: border-color 0.1s ease-out;
- &.code {
- tab-size: 2;
+ &:hover {
+ border-color: var(--inputBorderHover);
+ }
+ }
+
+ &.focused {
+ > textarea {
+ border-color: var(--accent);
}
}
- }
- &.tall {
- > .input {
+ &.disabled {
+ opacity: 0.7;
+
+ &, * {
+ cursor: not-allowed !important;
+ }
+ }
+
+ &.tall {
> textarea {
min-height: 200px;
}
}
- }
- &.pre {
- > .input {
+ &.pre {
> textarea {
white-space: pre;
}
diff --git a/src/client/components/global/header.vue b/src/client/components/global/header.vue
new file mode 100644
index 0000000000..a4466da498
--- /dev/null
+++ b/src/client/components/global/header.vue
@@ -0,0 +1,359 @@
+<template>
+<div class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick" ref="el">
+ <template v-if="info">
+ <div class="titleContainer" @click="showTabsPopup" v-if="!hideTitle">
+ <i v-if="info.icon" class="icon" :class="info.icon"></i>
+ <MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
+
+ <div class="title">
+ <MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="title"/>
+ <div v-else-if="info.title" class="title">{{ info.title }}</div>
+ <div class="subtitle" v-if="!narrow && info.subtitle">
+ {{ info.subtitle }}
+ </div>
+ <div class="subtitle activeTab" v-if="narrow && hasTabs">
+ {{ info.tabs.find(tab => tab.active)?.title }}
+ <i class="chevron fas fa-chevron-down"></i>
+ </div>
+ </div>
+ </div>
+ <div class="tabs" v-if="!narrow || hideTitle">
+ <button class="tab _button" v-for="tab in info.tabs" :class="{ active: tab.active }" @click="tab.onClick" v-tooltip="tab.title">
+ <i v-if="tab.icon" class="icon" :class="tab.icon"></i>
+ <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
+ </button>
+ </div>
+ </template>
+ <div class="buttons right">
+ <template v-if="info && info.actions && !narrow">
+ <template v-for="action in info.actions">
+ <MkButton class="fullButton" v-if="action.asFullButton" @click.stop="action.handler" primary><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
+ <button v-else class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button>
+ </template>
+ </template>
+ <button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, onMounted, onUnmounted, PropType, ref, inject } from 'vue';
+import * as tinycolor from 'tinycolor2';
+import { popupMenu } from '@client/os';
+import { url } from '@client/config';
+import { scrollToTop } from '@client/scripts/scroll';
+import MkButton from '@client/components/ui/button.vue';
+import { i18n } from '@client/i18n';
+import { globalEvents } from '@client/events';
+
+export default defineComponent({
+ components: {
+ MkButton
+ },
+
+ props: {
+ info: {
+ type: Object as PropType<{
+ actions?: {}[];
+ tabs?: {}[];
+ }>,
+ required: true
+ },
+ menu: {
+ required: false
+ },
+ thin: {
+ required: false,
+ default: false
+ },
+ },
+
+ setup(props) {
+ const el = ref<HTMLElement>(null);
+ const bg = ref(null);
+ const narrow = ref(false);
+ const height = ref(0);
+ const hasTabs = computed(() => {
+ return props.info.tabs && props.info.tabs.length > 0;
+ });
+ const shouldShowMenu = computed(() => {
+ if (props.info == null) return false;
+ if (props.info.actions != null && narrow.value) return true;
+ if (props.info.menu != null) return true;
+ if (props.info.share != null) return true;
+ if (props.menu != null) return true;
+ return false;
+ });
+
+ const share = () => {
+ navigator.share({
+ url: url + props.info.path,
+ ...props.info.share,
+ });
+ };
+
+ const showMenu = (ev: MouseEvent) => {
+ let menu = props.info.menu ? props.info.menu() : [];
+ if (narrow.value && props.info.actions) {
+ menu = [...props.info.actions.map(x => ({
+ text: x.text,
+ icon: x.icon,
+ action: x.handler
+ })), menu.length > 0 ? null : undefined, ...menu];
+ }
+ if (props.info.share) {
+ if (menu.length > 0) menu.push(null);
+ menu.push({
+ text: i18n.locale.share,
+ icon: 'fas fa-share-alt',
+ action: share
+ });
+ }
+ if (props.menu) {
+ if (menu.length > 0) menu.push(null);
+ menu = menu.concat(props.menu);
+ }
+ popupMenu(menu, ev.currentTarget || ev.target);
+ };
+
+ const showTabsPopup = (ev: MouseEvent) => {
+ if (!hasTabs.value) return;
+ if (!narrow.value) return;
+ ev.preventDefault();
+ ev.stopPropagation();
+ const menu = props.info.tabs.map(tab => ({
+ text: tab.title,
+ icon: tab.icon,
+ action: tab.onClick,
+ }));
+ popupMenu(menu, ev.currentTarget || ev.target);
+ };
+
+ const preventDrag = (ev: TouchEvent) => {
+ ev.stopPropagation();
+ };
+
+ const onClick = () => {
+ scrollToTop(el.value, { behavior: 'smooth' });
+ };
+
+ const calcBg = () => {
+ const rawBg = props.info?.bg || 'var(--bg)';
+ const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+ tinyBg.setAlpha(0.85);
+ bg.value = tinyBg.toRgbString();
+ };
+
+ onMounted(() => {
+ calcBg();
+ globalEvents.on('themeChanged', calcBg);
+ onUnmounted(() => {
+ globalEvents.off('themeChanged', calcBg);
+ });
+
+ if (el.value.parentElement) {
+ narrow.value = el.value.parentElement.offsetWidth < 500;
+ const ro = new ResizeObserver((entries, observer) => {
+ if (el.value) {
+ narrow.value = el.value.parentElement.offsetWidth < 500;
+ }
+ });
+ ro.observe(el.value.parentElement);
+ onUnmounted(() => {
+ ro.disconnect();
+ });
+ setTimeout(() => {
+ const currentStickyTop = getComputedStyle(el.value.parentElement).getPropertyValue('--stickyTop') || '0px';
+ el.value.style.setProperty('--stickyTop', currentStickyTop);
+ el.value.parentElement.style.setProperty('--stickyTop', `calc(${currentStickyTop} + ${el.value.offsetHeight}px)`);
+ }, 100); // レンダリング順序の関係で親のstickyTopの設定が少し遅れることがあるため
+ }
+ });
+
+ return {
+ el,
+ bg,
+ narrow,
+ height,
+ hasTabs,
+ shouldShowMenu,
+ share,
+ showMenu,
+ showTabsPopup,
+ preventDrag,
+ onClick,
+ hideTitle: inject('shouldOmitHeaderTitle', false),
+ thin_: props.thin || inject('shouldHeaderThin', false)
+ };
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.fdidabkb {
+ --height: 60px;
+ display: flex;
+ position: sticky;
+ top: var(--stickyTop, 0);
+ z-index: 1000;
+ width: 100%;
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+ border-bottom: solid 0.5px var(--divider);
+
+ &.thin {
+ --height: 50px;
+ }
+
+ &.slim {
+ text-align: center;
+
+ > .titleContainer {
+ flex: 1;
+ margin: 0 auto;
+ margin-left: var(--height);
+
+ > *:first-child {
+ margin-left: auto;
+ }
+
+ > *:last-child {
+ margin-right: auto;
+ }
+ }
+ }
+
+ > .buttons {
+ --margin: 8px;
+ display: flex;
+ align-items: center;
+ height: var(--height);
+ margin: 0 var(--margin);
+
+ &.right {
+ margin-left: auto;
+ }
+
+ &:empty {
+ width: var(--height);
+ }
+
+ > .button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: calc(var(--height) - (var(--margin) * 2));
+ width: calc(var(--height) - (var(--margin) * 2));
+ box-sizing: border-box;
+ position: relative;
+ border-radius: 5px;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.05);
+ }
+
+ &.highlighted {
+ color: var(--accent);
+ }
+ }
+
+ > .fullButton {
+ & + .fullButton {
+ margin-left: 12px;
+ }
+ }
+ }
+
+ > .titleContainer {
+ display: flex;
+ align-items: center;
+ overflow: auto;
+ white-space: nowrap;
+ text-align: left;
+ font-weight: bold;
+ flex-shrink: 0;
+ margin-left: 24px;
+
+ > .avatar {
+ $size: 32px;
+ display: inline-block;
+ width: $size;
+ height: $size;
+ vertical-align: bottom;
+ margin: 0 8px;
+ pointer-events: none;
+ }
+
+ > .icon {
+ margin-right: 8px;
+ }
+
+ > .title {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 1.1;
+
+ > .subtitle {
+ opacity: 0.6;
+ font-size: 0.8em;
+ font-weight: normal;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.activeTab {
+ text-align: center;
+
+ > .chevron {
+ display: inline-block;
+ margin-left: 6px;
+ }
+ }
+ }
+ }
+ }
+
+ > .tabs {
+ margin-left: 16px;
+ font-size: 0.8em;
+ overflow: auto;
+ white-space: nowrap;
+
+ > .tab {
+ display: inline-block;
+ position: relative;
+ padding: 0 10px;
+ height: 100%;
+ font-weight: normal;
+ opacity: 0.7;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ &.active {
+ opacity: 1;
+
+ &:after {
+ content: "";
+ display: block;
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ width: 100%;
+ height: 3px;
+ background: var(--accent);
+ }
+ }
+
+ > .icon + .title {
+ margin-left: 8px;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/global/spacer.vue b/src/client/components/global/spacer.vue
new file mode 100644
index 0000000000..1129d54c71
--- /dev/null
+++ b/src/client/components/global/spacer.vue
@@ -0,0 +1,76 @@
+<template>
+<div ref="root" :class="$style.root" :style="{ padding: margin + 'px' }">
+ <div ref="content" :class="$style.content">
+ <slot></slot>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
+
+export default defineComponent({
+ props: {
+ contentMax: {
+ type: Number,
+ required: false,
+ default: null,
+ }
+ },
+
+ setup(props, context) {
+ let ro: ResizeObserver;
+ const root = ref<HTMLElement>(null);
+ const content = ref<HTMLElement>(null);
+ const margin = ref(0);
+ const adjust = (rect: { width: number; height: number; }) => {
+ if (rect.width > (props.contentMax || 500)) {
+ margin.value = 32;
+ } else {
+ margin.value = 12;
+ }
+ };
+
+ onMounted(() => {
+ ro = new ResizeObserver((entries) => {
+ /* iOSが対応していない
+ adjust({
+ width: entries[0].borderBoxSize[0].inlineSize,
+ height: entries[0].borderBoxSize[0].blockSize,
+ });
+ */
+ adjust({
+ width: root.value.offsetWidth,
+ height: root.value.offsetHeight,
+ });
+ });
+ ro.observe(root.value);
+
+ if (props.contentMax) {
+ content.value.style.maxWidth = `${props.contentMax}px`;
+ }
+ });
+
+ onUnmounted(() => {
+ ro.disconnect();
+ });
+
+ return {
+ root,
+ content,
+ margin,
+ };
+ },
+});
+</script>
+
+<style lang="scss" module>
+.root {
+ box-sizing: border-box;
+ width: 100%;
+}
+
+.content {
+ margin: 0 auto;
+}
+</style>
diff --git a/src/client/components/index.ts b/src/client/components/index.ts
index 8b914c5eec..ecf66ea0e8 100644
--- a/src/client/components/index.ts
+++ b/src/client/components/index.ts
@@ -13,6 +13,8 @@ import i18n from './global/i18n';
import loading from './global/loading.vue';
import error from './global/error.vue';
import ad from './global/ad.vue';
+import header from './global/header.vue';
+import spacer from './global/spacer.vue';
export default function(app: App) {
app.component('I18n', i18n);
@@ -28,4 +30,6 @@ export default function(app: App) {
app.component('MkLoading', loading);
app.component('MkError', error);
app.component('MkAd', ad);
+ app.component('MkHeader', header);
+ app.component('MkSpacer', spacer);
}
diff --git a/src/client/components/instance-stats.vue b/src/client/components/instance-stats.vue
index 78044f0b16..5e7c71ea65 100644
--- a/src/client/components/instance-stats.vue
+++ b/src/client/components/instance-stats.vue
@@ -36,7 +36,7 @@
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import Chart from 'chart.js';
-import MkSelect from './ui/select.vue';
+import MkSelect from './form/select.vue';
import number from '@client/filters/number';
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
diff --git a/src/client/components/media-caption.vue b/src/client/components/media-caption.vue
index 690927d4c5..b35b101d06 100644
--- a/src/client/components/media-caption.vue
+++ b/src/client/components/media-caption.vue
@@ -3,10 +3,13 @@
<div class="container">
<div class="fullwidth top-caption">
<div class="mk-dialog">
- <header v-if="title"><Mfm :text="title"/></header>
+ <header>
+ <Mfm v-if="title" class="title" :text="title"/>
+ <span class="text-count" :class="{ over: remainingLength < 0 }">{{ remainingLength }}</span>
+ </header>
<textarea autofocus v-model="inputValue" :placeholder="input.placeholder" @keydown="onInputKeydown"></textarea>
<div class="buttons" v-if="(showOkButton || showCancelButton)">
- <MkButton inline @click="ok" primary>{{ $ts.ok }}</MkButton>
+ <MkButton inline @click="ok" primary :disabled="remainingLength < 0">{{ $ts.ok }}</MkButton>
<MkButton inline @click="cancel" >{{ $ts.cancel }}</MkButton>
</div>
</div>
@@ -26,10 +29,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
+import { length } from 'stringz';
import MkModal from '@client/components/ui/modal.vue';
import MkButton from '@client/components/ui/button.vue';
import bytes from '@client/filters/bytes';
import number from '@client/filters/number';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
export default defineComponent({
components: {
@@ -79,6 +84,13 @@ export default defineComponent({
document.removeEventListener('keydown', this.onKeydown);
},
+ computed: {
+ remainingLength(): number {
+ if (typeof this.inputValue != "string") return DB_MAX_IMAGE_COMMENT_LENGTH;
+ return DB_MAX_IMAGE_COMMENT_LENGTH - length(this.inputValue);
+ }
+ },
+
methods: {
bytes,
number,
@@ -156,8 +168,18 @@ export default defineComponent({
> header {
margin: 0 0 8px 0;
- font-weight: bold;
- font-size: 20px;
+ position: relative;
+
+ > .title {
+ font-weight: bold;
+ font-size: 20px;
+ }
+
+ > .text-count {
+ opacity: 0.7;
+ position: absolute;
+ right: 0;
+ }
}
> .buttons {
@@ -184,7 +206,7 @@ export default defineComponent({
min-width: 100%;
min-height: 90px;
- &:focus {
+ &:focus-visible {
outline: none;
}
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index a228ca4b8d..2bdd7d46ee 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -185,7 +185,7 @@ export default defineComponent({
}
}
if (style == null) {
- return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']);
+ return h('span', {}, ['[', token.props.name, ' ', ...genEl(token.children), ']']);
} else {
return h('span', {
style: 'display: inline-block;' + style,
diff --git a/src/client/components/modal-page-window.vue b/src/client/components/modal-page-window.vue
index e7d96f7a6f..cb81a974f5 100644
--- a/src/client/components/modal-page-window.vue
+++ b/src/client/components/modal-page-window.vue
@@ -2,11 +2,15 @@
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
<div class="hrmcaedk _window _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div class="header" @contextmenu="onContextmenu">
- <span class="title">
- <XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="$refs.modal.close()"/>
+ <button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
+ <span v-else style="display: inline-block; width: 20px"></span>
+ <span v-if="pageInfo" class="title">
+ <i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon"></i>
+ <span>{{ pageInfo.title }}</span>
</span>
+ <button class="_button" @click="$refs.modal.close()"><i class="fas fa-times"></i></button>
</div>
- <div class="body _flat_">
+ <div class="body _fitSide_">
<keep-alive>
<component :is="component" v-bind="props" :ref="changePage"/>
</keep-alive>
@@ -18,7 +22,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkModal from '@client/components/ui/modal.vue';
-import XHeader from '@client/ui/_common_/header.vue';
import { popout } from '@client/scripts/popout';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { resolve } from '@client/router';
@@ -29,7 +32,6 @@ import * as os from '@client/os';
export default defineComponent({
components: {
MkModal,
- XHeader,
},
inject: {
@@ -42,7 +44,8 @@ export default defineComponent({
return {
navHook: (path) => {
this.navigate(path);
- }
+ },
+ shouldHeaderThin: true,
};
},
@@ -172,19 +175,39 @@ export default defineComponent({
$height-narrow: 42px;
display: flex;
flex-shrink: 0;
+ height: $height;
+ line-height: $height;
+ font-weight: bold;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
box-shadow: 0px 1px var(--divider);
- > .title {
- flex: 1;
+ > button {
height: $height;
- font-weight: bold;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+ width: $height;
+
+ &:hover {
+ color: var(--fgHighlighted);
+ }
+ }
+
+ @media (max-width: 500px) {
+ height: $height-narrow;
+ line-height: $height-narrow;
+ padding-left: 16px;
- @media (max-width: 500px) {
+ > button {
height: $height-narrow;
- padding-left: 16px;
+ width: $height-narrow;
+ }
+ }
+
+ > .title {
+ flex: 1;
+
+ > .icon {
+ margin-right: 0.5em;
}
}
}
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index e7f116d1fd..40b0a68c58 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -59,7 +59,7 @@
<div class="body">
<p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
- <XCwButton v-model:value="showContent" :note="appearNote"/>
+ <XCwButton v-model="showContent" :note="appearNote"/>
</p>
<div class="content" v-show="appearNote.cw == null || showContent">
<div class="text">
@@ -80,7 +80,7 @@
</div>
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="true" class="url-preview"/>
- <div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
+ <div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
</div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="fas fa-satellite-dish"></i> {{ appearNote.channel.name }}</MkA>
</div>
@@ -132,7 +132,7 @@ import * as mfm from 'mfm-js';
import { sum } from '../../prelude/array';
import XSub from './note.sub.vue';
import XNoteHeader from './note-header.vue';
-import XNotePreview from './note-preview.vue';
+import XNoteSimple from './note-simple.vue';
import XReactionsViewer from './reactions-viewer.vue';
import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue';
@@ -153,7 +153,7 @@ export default defineComponent({
components: {
XSub,
XNoteHeader,
- XNotePreview,
+ XNoteSimple,
XReactionsViewer,
XMediaList,
XCwButton,
diff --git a/src/client/components/note-preview.vue b/src/client/components/note-preview.vue
index 4248c2bb1d..a474a01341 100644
--- a/src/client/components/note-preview.vue
+++ b/src/client/components/note-preview.vue
@@ -1,15 +1,13 @@
<template>
-<div class="yohlumlk" v-size="{ min: [350, 500] }">
- <MkAvatar class="avatar" :user="note.user"/>
+<div class="fefdfafb" v-size="{ min: [350, 500] }">
+ <MkAvatar class="avatar" :user="$i"/>
<div class="main">
- <XNoteHeader class="header" :note="note" :mini="true"/>
+ <div class="header">
+ <MkUserName :user="$i"/>
+ </div>
<div class="body">
- <p v-if="note.cw != null" class="cw">
- <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
- <XCwButton v-model:value="showContent" :note="note"/>
- </p>
- <div class="content" v-show="note.cw == null || showContent">
- <XSubNote-content class="text" :note="note"/>
+ <div class="content">
+ <Mfm :text="text" :author="$i" :i="$i"/>
</div>
</div>
</div>
@@ -18,35 +16,22 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
-import XCwButton from './cw-button.vue';
-import * as os from '@client/os';
export default defineComponent({
components: {
- XNoteHeader,
- XSubNoteContent,
- XCwButton,
},
props: {
- note: {
- type: Object,
+ text: {
+ type: String,
required: true
}
},
-
- data() {
- return {
- showContent: false
- };
- }
});
</script>
<style lang="scss" scoped>
-.yohlumlk {
+.fefdfafb {
display: flex;
margin: 0;
padding: 0;
diff --git a/src/client/components/note-simple.vue b/src/client/components/note-simple.vue
new file mode 100644
index 0000000000..406a475cd9
--- /dev/null
+++ b/src/client/components/note-simple.vue
@@ -0,0 +1,113 @@
+<template>
+<div class="yohlumlk" v-size="{ min: [350, 500] }">
+ <MkAvatar class="avatar" :user="note.user"/>
+ <div class="main">
+ <XNoteHeader class="header" :note="note" :mini="true"/>
+ <div class="body">
+ <p v-if="note.cw != null" class="cw">
+ <span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
+ <XCwButton v-model="showContent" :note="note"/>
+ </p>
+ <div class="content" v-show="note.cw == null || showContent">
+ <XSubNote-content class="text" :note="note"/>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import XNoteHeader from './note-header.vue';
+import XSubNoteContent from './sub-note-content.vue';
+import XCwButton from './cw-button.vue';
+import * as os from '@client/os';
+
+export default defineComponent({
+ components: {
+ XNoteHeader,
+ XSubNoteContent,
+ XCwButton,
+ },
+
+ props: {
+ note: {
+ type: Object,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ showContent: false
+ };
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.yohlumlk {
+ display: flex;
+ margin: 0;
+ padding: 0;
+ overflow: clip;
+ font-size: 0.95em;
+
+ &.min-width_350px {
+ > .avatar {
+ margin: 0 10px 0 0;
+ width: 44px;
+ height: 44px;
+ }
+ }
+
+ &.min-width_500px {
+ > .avatar {
+ margin: 0 12px 0 0;
+ width: 48px;
+ height: 48px;
+ }
+ }
+
+ > .avatar {
+ flex-shrink: 0;
+ display: block;
+ margin: 0 10px 0 0;
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ }
+
+ > .main {
+ flex: 1;
+ min-width: 0;
+
+ > .header {
+ margin-bottom: 2px;
+ }
+
+ > .body {
+
+ > .cw {
+ cursor: default;
+ display: block;
+ margin: 0;
+ padding: 0;
+ overflow-wrap: break-word;
+
+ > .text {
+ margin-right: 8px;
+ }
+ }
+
+ > .content {
+ > .text {
+ cursor: default;
+ margin: 0;
+ padding: 0;
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/note.sub.vue b/src/client/components/note.sub.vue
index 899c4b2f16..157b65ec5c 100644
--- a/src/client/components/note.sub.vue
+++ b/src/client/components/note.sub.vue
@@ -7,7 +7,7 @@
<div class="body">
<p v-if="note.cw != null" class="cw">
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
- <XCwButton v-model:value="showContent" :note="note"/>
+ <XCwButton v-model="showContent" :note="note"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<XSubNote-content class="text" :note="note"/>
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 38b529dd91..91a3e3b87d 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -43,7 +43,7 @@
<div class="body">
<p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
- <XCwButton v-model:value="showContent" :note="appearNote"/>
+ <XCwButton v-model="showContent" :note="appearNote"/>
</p>
<div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent">
<div class="text">
@@ -64,7 +64,7 @@
</div>
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
- <div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
+ <div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
<button v-if="collapsed" class="fade _button" @click="collapsed = false">
<span>{{ $ts.showMore }}</span>
</button>
@@ -114,7 +114,7 @@ import * as mfm from 'mfm-js';
import { sum } from '../../prelude/array';
import XSub from './note.sub.vue';
import XNoteHeader from './note-header.vue';
-import XNotePreview from './note-preview.vue';
+import XNoteSimple from './note-simple.vue';
import XReactionsViewer from './reactions-viewer.vue';
import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue';
@@ -134,7 +134,7 @@ export default defineComponent({
components: {
XSub,
XNoteHeader,
- XNotePreview,
+ XNoteSimple,
XReactionsViewer,
XMediaList,
XCwButton,
@@ -888,7 +888,7 @@ export default defineComponent({
//content-visibility: auto;
//contain-intrinsic-size: 0 128px;
- &:focus {
+ &:focus-visible {
outline: none;
&:after {
diff --git a/src/client/components/notification-setting-window.vue b/src/client/components/notification-setting-window.vue
index c33106ae15..14e0b76cc6 100644
--- a/src/client/components/notification-setting-window.vue
+++ b/src/client/components/notification-setting-window.vue
@@ -29,10 +29,10 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkSwitch from './ui/switch.vue';
+import MkSwitch from './form/switch.vue';
import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue';
-import { notificationTypes } from '../../types';
+import { notificationTypes } from '@/types';
export default defineComponent({
components: {
diff --git a/src/client/components/notifications.vue b/src/client/components/notifications.vue
index e91f18a693..78c1cce0c7 100644
--- a/src/client/components/notifications.vue
+++ b/src/client/components/notifications.vue
@@ -26,7 +26,7 @@ import paging from '@client/scripts/paging';
import XNotification from './notification.vue';
import XList from './date-separated-list.vue';
import XNote from './note.vue';
-import { notificationTypes } from '../../types';
+import { notificationTypes } from '@/types';
import * as os from '@client/os';
import MkButton from '@client/components/ui/button.vue';
@@ -48,6 +48,11 @@ export default defineComponent({
required: false,
default: null,
},
+ unreadOnly: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
@@ -58,6 +63,7 @@ export default defineComponent({
limit: 10,
params: () => ({
includeTypes: this.allIncludeTypes || undefined,
+ unreadOnly: this.unreadOnly,
})
},
};
@@ -76,6 +82,11 @@ export default defineComponent({
},
deep: true
},
+ unreadOnly: {
+ handler() {
+ this.reload();
+ },
+ },
// TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、
// mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
'$i.mutingNotificationTypes': {
diff --git a/src/client/components/page-window.vue b/src/client/components/page-window.vue
index fbc9f0b7fd..7d15c75d62 100644
--- a/src/client/components/page-window.vue
+++ b/src/client/components/page-window.vue
@@ -3,14 +3,20 @@
:initial-width="500"
:initial-height="500"
:can-resize="true"
- :close-button="false"
+ :close-button="true"
:contextmenu="contextmenu"
@closed="$emit('closed')"
>
<template #header>
- <XHeader :info="pageInfo" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()" :title-only="true"/>
+ <template v-if="pageInfo">
+ <i v-if="pageInfo.icon" class="icon" :class="pageInfo.icon" style="margin-right: 0.5em;"></i>
+ <span>{{ pageInfo.title }}</span>
+ </template>
</template>
- <div class="yrolvcoq _flat_">
+ <template #headerLeft>
+ <button v-if="history.length > 0" class="_button" @click="back()" v-tooltip="$ts.goBack"><i class="fas fa-arrow-left"></i></button>
+ </template>
+ <div class="yrolvcoq _fitSide_">
<component :is="component" v-bind="props" :ref="changePage"/>
</div>
</XWindow>
@@ -19,7 +25,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XWindow from '@client/components/ui/window.vue';
-import XHeader from '@client/ui/_common_/header.vue';
import { popout } from '@client/scripts/popout';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { resolve } from '@client/router';
@@ -29,7 +34,6 @@ import * as symbols from '@client/symbols';
export default defineComponent({
components: {
XWindow,
- XHeader,
},
inject: {
@@ -42,7 +46,8 @@ export default defineComponent({
return {
navHook: (path) => {
this.navigate(path);
- }
+ },
+ shouldHeaderThin: true,
};
},
diff --git a/src/client/components/page/page.number-input.vue b/src/client/components/page/page.number-input.vue
index 9c4a537e15..5d9168f130 100644
--- a/src/client/components/page/page.number-input.vue
+++ b/src/client/components/page/page.number-input.vue
@@ -8,7 +8,7 @@
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
-import MkInput from '../ui/input.vue';
+import MkInput from '../form/input.vue';
import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator';
import { NumberInputVarBlock } from '@client/scripts/hpml/block';
diff --git a/src/client/components/page/page.post.vue b/src/client/components/page/page.post.vue
index 7b061d8cda..c20d7cade1 100644
--- a/src/client/components/page/page.post.vue
+++ b/src/client/components/page/page.post.vue
@@ -10,7 +10,7 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
-import MkTextarea from '../ui/textarea.vue';
+import MkTextarea from '../form/textarea.vue';
import MkButton from '../ui/button.vue';
import { apiUrl } from '@client/config';
import * as os from '@client/os';
diff --git a/src/client/components/page/page.radio-button.vue b/src/client/components/page/page.radio-button.vue
index f6f146b52f..590e59d706 100644
--- a/src/client/components/page/page.radio-button.vue
+++ b/src/client/components/page/page.radio-button.vue
@@ -7,7 +7,7 @@
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
-import MkRadio from '../ui/radio.vue';
+import MkRadio from '../form/radio.vue';
import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator';
import { RadioButtonVarBlock } from '@client/scripts/hpml/block';
diff --git a/src/client/components/page/page.switch.vue b/src/client/components/page/page.switch.vue
index 8818e6cbcf..4d74e5df39 100644
--- a/src/client/components/page/page.switch.vue
+++ b/src/client/components/page/page.switch.vue
@@ -6,7 +6,7 @@
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
-import MkSwitch from '../ui/switch.vue';
+import MkSwitch from '../form/switch.vue';
import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator';
import { SwitchVarBlock } from '@client/scripts/hpml/block';
diff --git a/src/client/components/page/page.text-input.vue b/src/client/components/page/page.text-input.vue
index 752d3d7257..6e9ac0b543 100644
--- a/src/client/components/page/page.text-input.vue
+++ b/src/client/components/page/page.text-input.vue
@@ -8,7 +8,7 @@
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
-import MkInput from '../ui/input.vue';
+import MkInput from '../form/input.vue';
import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator';
import { TextInputVarBlock } from '@client/scripts/hpml/block';
diff --git a/src/client/components/page/page.textarea-input.vue b/src/client/components/page/page.textarea-input.vue
index e6cf5117f9..dfcb398937 100644
--- a/src/client/components/page/page.textarea-input.vue
+++ b/src/client/components/page/page.textarea-input.vue
@@ -8,7 +8,7 @@
<script lang="ts">
import { computed, defineComponent, PropType } from 'vue';
-import MkTextarea from '../ui/textarea.vue';
+import MkTextarea from '../form/textarea.vue';
import * as os from '@client/os';
import { Hpml } from '@client/scripts/hpml/evaluator';
import { HpmlTextInput } from '@client/scripts/hpml';
diff --git a/src/client/components/page/page.textarea.vue b/src/client/components/page/page.textarea.vue
index 974c7f2c57..cf953bf041 100644
--- a/src/client/components/page/page.textarea.vue
+++ b/src/client/components/page/page.textarea.vue
@@ -6,7 +6,7 @@
import { TextBlock } from '@client/scripts/hpml/block';
import { Hpml } from '@client/scripts/hpml/evaluator';
import { defineComponent, PropType } from 'vue';
-import MkTextarea from '../ui/textarea.vue';
+import MkTextarea from '../form/textarea.vue';
export default defineComponent({
components: {
diff --git a/src/client/components/poll-editor.vue b/src/client/components/poll-editor.vue
index 0a9a1c6a03..b28a1c8baa 100644
--- a/src/client/components/poll-editor.vue
+++ b/src/client/components/poll-editor.vue
@@ -51,9 +51,9 @@
import { defineComponent } from 'vue';
import { addTime } from '../../prelude/time';
import { formatDateTimeString } from '@/misc/format-time-string';
-import MkInput from './ui/input.vue';
-import MkSelect from './ui/select.vue';
-import MkSwitch from './ui/switch.vue';
+import MkInput from './form/input.vue';
+import MkSelect from './form/select.vue';
+import MkSwitch from './form/switch.vue';
import MkButton from './ui/button.vue';
export default defineComponent({
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 657053cc93..a1d89d2a2e 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -1,6 +1,6 @@
<template>
<div class="gafaadew" :class="{ modal, _popup: modal }"
- v-size="{ max: [500] }"
+ v-size="{ max: [310, 500] }"
@dragover.stop="onDragover"
@dragenter="onDragenter"
@dragleave="onDragleave"
@@ -17,12 +17,13 @@
<span v-if="visibility === 'followers'"><i class="fas fa-unlock"></i></span>
<span v-if="visibility === 'specified'"><i class="fas fa-envelope"></i></span>
</button>
- <button class="submit _buttonPrimary" :disabled="!canPost" @click="post" data-cy-open-post-form-submit>{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
+ <button class="_button preview" @click="showPreview = !showPreview" :class="{ active: showPreview }" v-tooltip="$ts.previewNoteText"><i class="fas fa-file-code"></i></button>
+ <button class="submit _buttonGradate" :disabled="!canPost" @click="post" data-cy-open-post-form-submit>{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
</div>
</header>
<div class="form" :class="{ fixed }">
- <XNotePreview class="preview" v-if="reply" :note="reply"/>
- <XNotePreview class="preview" v-if="renote" :note="renote"/>
+ <XNoteSimple class="preview" v-if="reply" :note="reply"/>
+ <XNoteSimple class="preview" v-if="renote" :note="renote"/>
<div class="with-quote" v-if="quoteId"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
<div v-if="visibility === 'specified'" class="to-specified">
<span style="margin-right: 8px;">{{ $ts.recipient }}</span>
@@ -40,6 +41,7 @@
<input v-show="withHashtags" ref="hashtags" class="hashtags" v-model="hashtags" :placeholder="$ts.hashtags" list="hashtags">
<XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
<XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
+ <XNotePreview class="preview" v-if="showPreview" :text="text"/>
<footer>
<button class="_button" @click="chooseFileFrom" v-tooltip="$ts.attachFile"><i class="fas fa-photo-video"></i></button>
<button class="_button" @click="togglePoll" :class="{ active: poll }" v-tooltip="$ts.poll"><i class="fas fa-poll-h"></i></button>
@@ -61,6 +63,7 @@ import { defineComponent, defineAsyncComponent } from 'vue';
import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz';
import { toASCII } from 'punycode/';
+import XNoteSimple from './note-simple.vue';
import XNotePreview from './note-preview.vue';
import * as mfm from 'mfm-js';
import { host, url } from '@client/config';
@@ -80,6 +83,7 @@ import { defaultStore } from '@client/store';
export default defineComponent({
components: {
+ XNoteSimple,
XNotePreview,
XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')),
@@ -143,6 +147,7 @@ export default defineComponent({
files: [],
poll: null,
useCw: false,
+ showPreview: false,
cw: null,
localOnly: this.$store.state.rememberNoteVisibility ? this.$store.state.localOnly : this.$store.state.defaultNoteLocalOnly,
visibility: this.$store.state.rememberNoteVisibility ? this.$store.state.visibility : this.$store.state.defaultNoteVisibility,
@@ -717,7 +722,7 @@ export default defineComponent({
> .visibility {
height: 34px;
width: 34px;
- margin: 0 8px;
+ margin: 0 0 0 8px;
& + .localOnly {
margin-left: 0 !important;
@@ -729,6 +734,24 @@ export default defineComponent({
opacity: 0.7;
}
+ > .preview {
+ display: inline-block;
+ padding: 0;
+ margin: 0 8px 0 0;
+ font-size: 16px;
+ width: 34px;
+ height: 34px;
+ border-radius: 6px;
+
+ &:hover {
+ background: var(--X5);
+ }
+
+ &.active {
+ color: var(--accent);
+ }
+ }
+
> .submit {
margin: 16px 16px 16px 0;
padding: 0 12px;
@@ -736,6 +759,7 @@ export default defineComponent({
font-weight: bold;
vertical-align: bottom;
border-radius: 4px;
+ font-size: 0.9em;
&:disabled {
opacity: 0.7;
@@ -819,7 +843,7 @@ export default defineComponent({
color: var(--fg);
font-family: inherit;
- &:focus {
+ &:focus-visible {
outline: none;
}
@@ -914,5 +938,17 @@ export default defineComponent({
}
}
}
+
+ &.max-width_310px {
+ > .form {
+ > footer {
+ > button {
+ font-size: 14px;
+ width: 44px;
+ height: 44px;
+ }
+ }
+ }
+ }
}
</style>
diff --git a/src/client/components/sample.vue b/src/client/components/sample.vue
index bce02466f6..c8b46a80e7 100644
--- a/src/client/components/sample.vue
+++ b/src/client/components/sample.vue
@@ -30,10 +30,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkRadio from '@client/components/ui/radio.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSwitch from '@client/components/form/switch.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkRadio from '@client/components/form/radio.vue';
import * as os from '@client/os';
import * as config from '@client/config';
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
index 69f527b7d6..d6e1ee8b68 100755
--- a/src/client/components/signin.vue
+++ b/src/client/components/signin.vue
@@ -1,17 +1,17 @@
<template>
<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
- <div class="auth _section">
+ <div class="auth _section _formRoot">
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
<div class="normal-signin" v-if="!totpLogin">
- <MkInput v-model="username" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @update:modelValue="onUsernameChange" data-cy-signin-username>
+ <MkInput class="_formBlock" v-model="username" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @update:modelValue="onUsernameChange" data-cy-signin-username>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
- <MkInput v-model="password" :placeholder="$ts.password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required data-cy-signin-password>
+ <MkInput class="_formBlock" v-model="password" :placeholder="$ts.password" type="password" :with-password-toggle="true" v-if="!user || user && !user.usePasswordLessLogin" required data-cy-signin-password>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption><button class="_textButton" @click="resetPassword" type="button">{{ $ts.forgotPassword }}</button></template>
</MkInput>
- <MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
+ <MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
</div>
<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
@@ -49,7 +49,7 @@
import { defineComponent } from 'vue';
import { toUnicode } from 'punycode/';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import { apiUrl, host } from '@client/config';
import { byteify, hexify } from '@client/scripts/2fa';
import * as os from '@client/os';
diff --git a/src/client/components/signup-dialog.vue b/src/client/components/signup-dialog.vue
index df1a525055..9741e8c73b 100644
--- a/src/client/components/signup-dialog.vue
+++ b/src/client/components/signup-dialog.vue
@@ -9,7 +9,7 @@
<div class="_monolithic_">
<div class="_section">
- <XSignup :auto-set="autoSet" @signup="onSignup"/>
+ <XSignup :auto-set="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
</div>
</div>
</XModalWindow>
@@ -40,6 +40,10 @@ export default defineComponent({
onSignup(res) {
this.$emit('done', res);
this.$refs.dialog.close();
+ },
+
+ onSignupEmailPending() {
+ this.$refs.dialog.close();
}
}
});
diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue
index d332274111..cb25eadf06 100644
--- a/src/client/components/signup.vue
+++ b/src/client/components/signup.vue
@@ -1,25 +1,35 @@
<template>
-<form class="qlvuhzng" @submit.prevent="onSubmit" :autocomplete="Math.random()">
+<form class="qlvuhzng _formRoot" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<template v-if="meta">
- <MkInput class="_inputNoTopMargin" v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
+ <MkInput class="_formBlock" v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
<template #label>{{ $ts.invitationCode }}</template>
<template #prefix><i class="fas fa-key"></i></template>
</MkInput>
- <MkInput class="_inputNoTopMargin" v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
+ <MkInput class="_formBlock" v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeUsername" data-cy-signup-username>
<template #label>{{ $ts.username }} <div class="_button _help" v-tooltip:dialog="$ts.usernameInfo"><i class="far fa-question-circle"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #caption>
- <span v-if="usernameState == 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
- <span v-if="usernameState == 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
- <span v-if="usernameState == 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
- <span v-if="usernameState == 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
- <span v-if="usernameState == 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
- <span v-if="usernameState == 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
- <span v-if="usernameState == 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
+ <span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
+ <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
+ <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
+ <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
+ <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span>
+ <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span>
+ <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span>
</template>
</MkInput>
- <MkInput v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword" data-cy-signup-password>
+ <MkInput v-if="meta.emailRequiredForSignup" class="_formBlock" v-model="email" type="email" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeEmail" data-cy-signup-email>
+ <template #label>{{ $ts.emailAddress }} <div class="_button _help" v-tooltip:dialog="$ts._signup.emailAddressInfo"><i class="far fa-question-circle"></i></div></template>
+ <template #prefix><i class="fas fa-envelope"></i></template>
+ <template #caption>
+ <span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span>
+ <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span>
+ <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span>
+ <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span>
+ </template>
+ </MkInput>
+ <MkInput class="_formBlock" v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword" data-cy-signup-password>
<template #label>{{ $ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
@@ -28,7 +38,7 @@
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.strongPassword }}</span>
</template>
</MkInput>
- <MkInput v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePasswordRetype" data-cy-signup-password-retype>
+ <MkInput class="_formBlock" v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePasswordRetype" data-cy-signup-password-retype>
<template #label>{{ $ts.password }} ({{ $ts.retype }})</template>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption>
@@ -36,7 +46,7 @@
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.passwordNotMatched }}</span>
</template>
</MkInput>
- <label v-if="meta.tosUrl" class="tou">
+ <label v-if="meta.tosUrl" class="_formBlock tou">
<input type="checkbox" v-model="ToSAgreement">
<I18n :src="$ts.agreeTo">
<template #0>
@@ -44,9 +54,9 @@
</template>
</I18n>
</label>
- <captcha v-if="meta.enableHcaptcha" class="captcha" provider="hcaptcha" ref="hcaptcha" v-model:value="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
- <captcha v-if="meta.enableRecaptcha" class="captcha" provider="recaptcha" ref="recaptcha" v-model:value="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
- <MkButton type="submit" :disabled="shouldDisableSubmitting" primary data-cy-signup-submit>{{ $ts.start }}</MkButton>
+ <captcha v-if="meta.enableHcaptcha" class="_formBlock captcha" provider="hcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :sitekey="meta.hcaptchaSiteKey"/>
+ <captcha v-if="meta.enableRecaptcha" class="_formBlock captcha" provider="recaptcha" ref="recaptcha" v-model="reCaptchaResponse" :sitekey="meta.recaptchaSiteKey"/>
+ <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
</template>
</form>
</template>
@@ -57,8 +67,8 @@ const getPasswordStrength = require('syuilo-password-strength');
import { toUnicode } from 'punycode/';
import { host, url } from '@client/config';
import MkButton from './ui/button.vue';
-import MkInput from './ui/input.vue';
-import MkSwitch from './ui/switch.vue';
+import MkInput from './form/input.vue';
+import MkSwitch from './form/switch.vue';
import * as os from '@client/os';
import { login } from '@client/account';
@@ -87,8 +97,10 @@ export default defineComponent({
password: '',
retypedPassword: '',
invitationCode: '',
+ email: '',
url,
usernameState: null,
+ emailState: null,
passwordStrength: '',
passwordRetypeState: null,
submitting: false,
@@ -148,6 +160,23 @@ export default defineComponent({
});
},
+ onChangeEmail() {
+ if (this.email == '') {
+ this.emailState = null;
+ return;
+ }
+
+ this.emailState = 'wait';
+
+ os.api('email-address/available', {
+ emailAddress: this.email
+ }).then(result => {
+ this.emailState = result.available ? 'ok' : 'unavailable';
+ }).catch(err => {
+ this.emailState = 'error';
+ });
+ },
+
onChangePassword() {
if (this.password == '') {
this.passwordStrength = '';
@@ -174,20 +203,30 @@ export default defineComponent({
os.api('signup', {
username: this.username,
password: this.password,
+ emailAddress: this.email,
invitationCode: this.invitationCode,
'hcaptcha-response': this.hCaptchaResponse,
'g-recaptcha-response': this.reCaptchaResponse,
}).then(() => {
- return os.api('signin', {
- username: this.username,
- password: this.password
- }).then(res => {
- this.$emit('signup', res);
+ if (this.meta.emailRequiredForSignup) {
+ os.dialog({
+ type: 'success',
+ title: this.$ts._signup.almostThere,
+ text: this.$t('_signup.emailSent', { email: this.email }),
+ });
+ this.$emit('signupEmailPending');
+ } else {
+ os.api('signin', {
+ username: this.username,
+ password: this.password
+ }).then(res => {
+ this.$emit('signup', res);
- if (this.autoSet) {
- return login(res.i);
- }
- });
+ if (this.autoSet) {
+ login(res.i);
+ }
+ });
+ }
}).catch(() => {
this.submitting = false;
this.$refs.hcaptcha?.reset?.();
diff --git a/src/client/components/tab.vue b/src/client/components/tab.vue
index 3902b7f98f..ce86af8f95 100644
--- a/src/client/components/tab.vue
+++ b/src/client/components/tab.vue
@@ -3,7 +3,7 @@ import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
export default defineComponent({
props: {
- value: {
+ modelValue: {
required: true,
},
},
@@ -13,11 +13,11 @@ export default defineComponent({
return withDirectives(h('div', {
class: 'pxhvhrfw',
}, options.map(option => withDirectives(h('button', {
- class: ['_button', { active: this.value === option.props.value }],
+ class: ['_button', { active: this.modelValue === option.props.value }],
key: option.key,
- disabled: this.value === option.props.value,
+ disabled: this.modelValue === option.props.value,
onClick: () => {
- this.$emit('update:value', option.props.value);
+ this.$emit('update:modelValue', option.props.value);
}
}, option.children), [
[resolveDirective('click-anime')]
@@ -35,8 +35,8 @@ export default defineComponent({
> button {
flex: 1;
- padding: 15px 12px 12px 12px;
- border-bottom: solid 3px transparent;
+ padding: 10px 8px;
+ border-radius: 6px;
&:disabled {
opacity: 1 !important;
@@ -45,11 +45,16 @@ export default defineComponent({
&.active {
color: var(--accent);
- border-bottom-color: var(--accent);
+ background: var(--accentedBg);
}
&:not(.active):hover {
color: var(--fgHighlighted);
+ background: var(--panelHighlight);
+ }
+
+ &:not(:first-child) {
+ margin-left: 8px;
}
> .icon {
@@ -61,7 +66,7 @@ export default defineComponent({
font-size: 80%;
> button {
- padding: 11px 8px 8px 8px;
+ padding: 11px 8px;
}
}
}
diff --git a/src/client/components/taskmanager.api-window.vue b/src/client/components/taskmanager.api-window.vue
index c9b2c43413..807e4a0075 100644
--- a/src/client/components/taskmanager.api-window.vue
+++ b/src/client/components/taskmanager.api-window.vue
@@ -9,7 +9,7 @@
<template #header>Req Viewer</template>
<div class="rlkneywz">
- <MkTab v-model:value="tab" style="border-bottom: solid 0.5px var(--divider);">
+ <MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
<option value="req">Request</option>
<option value="res">Response</option>
</MkTab>
diff --git a/src/client/components/taskmanager.vue b/src/client/components/taskmanager.vue
index cb8cb78748..6f3d1b0354 100644
--- a/src/client/components/taskmanager.vue
+++ b/src/client/components/taskmanager.vue
@@ -4,7 +4,7 @@
<i class="fas fa-terminal" style="margin-right: 0.5em;"></i>Task Manager
</template>
<div class="qljqmnzj _monospace">
- <MkTab v-model:value="tab" style="border-bottom: solid 0.5px var(--divider);">
+ <MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
<option value="windows">Windows</option>
<option value="stream">Stream</option>
<option value="streamPool">Stream (Pool)</option>
diff --git a/src/client/components/token-generate-window.vue b/src/client/components/token-generate-window.vue
index fe61f61efa..86312564cc 100644
--- a/src/client/components/token-generate-window.vue
+++ b/src/client/components/token-generate-window.vue
@@ -31,9 +31,9 @@
import { defineComponent } from 'vue';
import { kinds } from '@/misc/api-permissions';
import XModalWindow from '@client/components/ui/modal-window.vue';
-import MkInput from './ui/input.vue';
-import MkTextarea from './ui/textarea.vue';
-import MkSwitch from './ui/switch.vue';
+import MkInput from './form/input.vue';
+import MkTextarea from './form/textarea.vue';
+import MkSwitch from './form/switch.vue';
import MkButton from './ui/button.vue';
import MkInfo from './ui/info.vue';
diff --git a/src/client/components/ui/button.vue b/src/client/components/ui/button.vue
index d6ac42994f..b5f4547c84 100644
--- a/src/client/components/ui/button.vue
+++ b/src/client/components/ui/button.vue
@@ -1,7 +1,6 @@
<template>
-<component class="bghgjjyj _button"
- :is="link ? 'MkA' : 'button'"
- :class="{ inline, primary, danger, full }"
+<button v-if="!link" class="bghgjjyj _button"
+ :class="{ inline, primary, gradate, danger, rounded, full }"
:type="type"
@click="$emit('click', $event)"
@mousedown="onMousedown"
@@ -10,7 +9,17 @@
<div class="content">
<slot></slot>
</div>
-</component>
+</button>
+<MkA v-else class="bghgjjyj _button"
+ :class="{ inline, primary, gradate, danger, rounded, full }"
+ :to="to"
+ @mousedown="onMousedown"
+>
+ <div ref="ripples" class="ripples"></div>
+ <div class="content">
+ <slot></slot>
+ </div>
+</MkA>
</template>
<script lang="ts">
@@ -27,6 +36,16 @@ export default defineComponent({
required: false,
default: false
},
+ gradate: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ rounded: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
inline: {
type: Boolean,
required: false,
@@ -37,6 +56,10 @@ export default defineComponent({
required: false,
default: false
},
+ to: {
+ type: String,
+ required: false
+ },
autofocus: {
type: Boolean,
required: false,
@@ -119,13 +142,13 @@ export default defineComponent({
padding: 8px 14px;
text-align: center;
font-weight: normal;
- font-size: 0.9em;
- line-height: 24px;
+ font-size: 0.8em;
+ line-height: 22px;
box-shadow: none;
text-decoration: none;
background: var(--buttonBg);
- border-radius: 999px;
- overflow: hidden;
+ border-radius: 4px;
+ overflow: clip;
box-sizing: border-box;
transition: background 0.1s ease;
@@ -141,6 +164,10 @@ export default defineComponent({
width: 100%;
}
+ &.rounded {
+ border-radius: 999px;
+ }
+
&.primary {
font-weight: bold;
color: var(--fgOnAccent) !important;
@@ -155,6 +182,20 @@ export default defineComponent({
}
}
+ &.gradate {
+ font-weight: bold;
+ color: var(--fgOnAccent) !important;
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+
+ &:not(:disabled):hover {
+ background: linear-gradient(90deg, var(--X8), var(--X8));
+ }
+
+ &:not(:disabled):active {
+ background: linear-gradient(90deg, var(--X8), var(--X8));
+ }
+ }
+
&.danger {
color: #ff2a2a;
@@ -176,19 +217,11 @@ export default defineComponent({
opacity: 0.7;
}
- &:focus {
+ &:focus-visible {
outline: solid 2px var(--focus);
outline-offset: 2px;
}
- &.inline + .bghgjjyj {
- margin-left: 12px;
- }
-
- &:not(.inline) + .bghgjjyj {
- margin-top: 16px;
- }
-
&.inline {
display: inline-block;
width: auto;
diff --git a/src/client/components/ui/folder.vue b/src/client/components/ui/folder.vue
index eecf1d8be1..d0616a57c1 100644
--- a/src/client/components/ui/folder.vue
+++ b/src/client/components/ui/folder.vue
@@ -1,6 +1,6 @@
<template>
<div class="ssazuxis" v-size="{ max: [500] }">
- <header @click="showBody = !showBody" class="_button">
+ <header @click="showBody = !showBody" class="_button" :style="{ background: bg }">
<div class="title"><slot name="header"></slot></div>
<div class="divider"></div>
<button class="_button">
@@ -23,6 +23,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
+import * as tinycolor from 'tinycolor2';
const localStoragePrefix = 'ui:folder:';
@@ -41,6 +42,7 @@ export default defineComponent({
},
data() {
return {
+ bg: null,
showBody: (this.persistKey && localStorage.getItem(localStoragePrefix + this.persistKey)) ? localStorage.getItem(localStoragePrefix + this.persistKey) === 't' : this.expanded,
};
},
@@ -51,6 +53,21 @@ export default defineComponent({
}
}
},
+ mounted() {
+ function getParentBg(el: Element | null): string {
+ if (el == null || el.tagName === 'BODY') return 'var(--bg)';
+ const bg = el.style.background || el.style.backgroundColor;
+ if (bg) {
+ return bg;
+ } else {
+ return getParentBg(el.parentElement);
+ }
+ }
+ const rawBg = getParentBg(this.$el);
+ const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+ bg.setAlpha(0.85);
+ this.bg = bg.toRgbString();
+ },
methods: {
toggleContent(show: boolean) {
this.showBody = show;
@@ -100,12 +117,8 @@ export default defineComponent({
position: sticky;
top: var(--stickyTop, 0px);
padding: var(--x-padding);
- background: var(--x-header, var(--panel));
- /* TODO panelの半透明バージョンをプログラマティックに作りたい
- background: var(--X17);
-webkit-backdrop-filter: var(--blur, blur(8px));
backdrop-filter: var(--blur, blur(20px));
- */
> .title {
margin: 0;
@@ -141,7 +154,7 @@ export default defineComponent({
}
}
-._flat_ .ssazuxis {
+._fitSide_ .ssazuxis {
> header {
padding: 0 16px;
}
diff --git a/src/client/components/ui/info.vue b/src/client/components/ui/info.vue
index 513682ef55..e16f2736f1 100644
--- a/src/client/components/ui/info.vue
+++ b/src/client/components/ui/info.vue
@@ -27,7 +27,6 @@ export default defineComponent({
<style lang="scss" scoped>
.fpezltsf {
- margin: 16px 0;
padding: 16px;
font-size: 90%;
background: var(--infoBg);
@@ -39,20 +38,12 @@ export default defineComponent({
color: var(--infoWarnFg);
}
- &:first-child {
- margin-top: 0;
- }
-
- &:last-child {
- margin-bottom: 0;
- }
-
> i {
margin-right: 4px;
}
}
-._flat_ .fpezltsf {
+._fitSide_ .fpezltsf {
border-radius: 0;
}
</style>
diff --git a/src/client/components/ui/menu.vue b/src/client/components/ui/menu.vue
index 26b4b04b11..da24d90170 100644
--- a/src/client/components/ui/menu.vue
+++ b/src/client/components/ui/menu.vue
@@ -1,5 +1,5 @@
<template>
-<div class="rrevdjwt" :class="{ left: align === 'left', pointer: point === 'top' }"
+<div class="rrevdjwt" :class="{ center: align === 'center' }"
ref="items"
@contextmenu.self="e => e.preventDefault()"
v-hotkey="keymap"
@@ -27,7 +27,7 @@
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</button>
- <button v-else @click="clicked(item.action, $event)" :tabindex="i" class="_button item" :class="{ danger: item.danger }">
+ <button v-else @click="clicked(item.action, $event)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active">
<i v-if="item.icon" class="fa-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span>
@@ -59,10 +59,6 @@ export default defineComponent({
type: String,
requried: false
},
- point: {
- type: String,
- requried: false
- },
},
emits: ['close'],
data() {
@@ -145,68 +141,83 @@ export default defineComponent({
<style lang="scss" scoped>
.rrevdjwt {
padding: 8px 0;
+ min-width: 200px;
- &.pointer {
- &:before {
- --size: 8px;
- content: '';
- display: block;
- position: absolute;
- top: calc(0px - (var(--size) * 2));
- left: 0;
- right: 0;
- width: 0;
- margin: auto;
- border: solid var(--size) transparent;
- border-bottom-color: var(--popup);
- }
- }
-
- &.left {
+ &.center {
> .item {
- text-align: left;
+ text-align: center;
}
}
> .item {
display: block;
position: relative;
- padding: 8px 16px;
+ padding: 8px 18px;
width: 100%;
box-sizing: border-box;
white-space: nowrap;
font-size: 0.9em;
line-height: 20px;
- text-align: center;
+ text-align: left;
overflow: hidden;
text-overflow: ellipsis;
+ &:before {
+ content: "";
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ margin: auto;
+ width: calc(100% - 16px);
+ height: 100%;
+ border-radius: 6px;
+ }
+
+ > * {
+ position: relative;
+ }
+
&.danger {
color: #ff2a2a;
&:hover {
color: #fff;
- background: #ff4242;
+
+ &:before {
+ background: #ff4242;
+ }
}
&:active {
color: #fff;
- background: #d42e2e;
+
+ &:before {
+ background: #d42e2e;
+ }
}
}
- &:hover {
+ &.active {
color: var(--fgOnAccent);
- background: var(--accent);
- text-decoration: none;
+ opacity: 1;
+
+ &:before {
+ background: var(--accent);
+ }
}
- &:active {
- color: var(--fgOnAccent);
- background: var(--accentDarken);
+ &:not(:disabled):hover {
+ color: var(--accent);
+ text-decoration: none;
+
+ &:before {
+ background: var(--accentedBg);
+ }
}
- &:not(:active):focus {
+ &:not(:active):focus-visible {
box-shadow: 0 0 0 2px var(--focus) inset;
}
@@ -231,12 +242,12 @@ export default defineComponent({
}
> i {
- margin-right: 4px;
+ margin-right: 5px;
width: 20px;
}
> .avatar {
- margin-right: 4px;
+ margin-right: 5px;
width: 20px;
height: 20px;
}
diff --git a/src/client/components/ui/popup-menu.vue b/src/client/components/ui/popup-menu.vue
index 3590426172..23f7c89f3b 100644
--- a/src/client/components/ui/popup-menu.vue
+++ b/src/client/components/ui/popup-menu.vue
@@ -1,6 +1,6 @@
<template>
-<MkPopup ref="popup" :src="src" @closed="$emit('closed')" #default="{point}">
- <MkMenu :items="items" :align="align" :point="point" @close="$refs.popup.close()" class="_popup _shadow"/>
+<MkPopup ref="popup" :src="src" @closed="$emit('closed')">
+ <MkMenu :items="items" :align="align" @close="$refs.popup.close()" class="_popup _shadow"/>
</MkPopup>
</template>
diff --git a/src/client/components/ui/popup.vue b/src/client/components/ui/popup.vue
index 8497eedecb..0fb1780cc5 100644
--- a/src/client/components/ui/popup.vue
+++ b/src/client/components/ui/popup.vue
@@ -1,7 +1,7 @@
<template>
<transition :name="$store.state.animation ? 'popup-menu' : ''" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered">
<div v-show="manualShowing != null ? manualShowing : showing" class="ccczpooj" :class="{ front, fixed, top: position === 'top' }" ref="content" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
- <slot :point="point"></slot>
+ <slot></slot>
</div>
</transition>
</template>
@@ -52,7 +52,6 @@ export default defineComponent({
fixed: false,
transformOrigin: 'center',
contentClicking: false,
- point: null,
};
},
@@ -136,10 +135,8 @@ export default defineComponent({
}
if (top > rect.top + (this.fixed ? 0 : window.pageYOffset)) {
- this.point = 'top';
this.transformOrigin = 'center top';
} else {
- this.point = null;
this.transformOrigin = 'center';
}
diff --git a/src/client/components/ui/radios.vue b/src/client/components/ui/radios.vue
deleted file mode 100644
index 8a62b87683..0000000000
--- a/src/client/components/ui/radios.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script lang="ts">
-import { defineComponent, h } from 'vue';
-import MkRadio from '@client/components/ui/radio.vue';
-
-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();
- let options = this.$slots.default();
-
- // なぜかFragmentになることがあるため
- if (options.length === 1 && options[0].props == null) options = options[0].children;
-
- return h('div', {
- class: 'novjtcto'
- }, [
- h('div', label),
- ...options.map(option => h(MkRadio, {
- key: option.key,
- value: option.props.value,
- modelValue: this.value,
- 'onUpdate:modelValue': value => this.value = value,
- }, option.children))
- ]);
- }
-});
-</script>
-
-<style lang="scss">
-.novjtcto {
- margin: 32px 0;
-
- &:first-child {
- margin-top: 0;
- }
-
- &:last-child {
- margin-bottom: 0;
- }
-}
-</style>
diff --git a/src/client/components/ui/range.vue b/src/client/components/ui/range.vue
deleted file mode 100644
index 4cfe66a8fc..0000000000
--- a/src/client/components/ui/range.vue
+++ /dev/null
@@ -1,139 +0,0 @@
-<template>
-<div class="timctyfi" :class="{ focused, disabled }">
- <div class="icon"><slot name="icon"></slot></div>
- <span class="label"><slot name="label"></slot></span>
- <input
- type="range"
- ref="input"
- v-model="v"
- :disabled="disabled"
- :min="min"
- :max="max"
- :step="step"
- :autofocus="autofocus"
- @focus="focused = true"
- @blur="focused = false"
- @input="$emit('update:value', $event.target.value)"
- />
-</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
- },
- autofocus: {
- type: Boolean,
- required: false
- }
- },
- data() {
- return {
- v: this.value,
- focused: false
- };
- },
- watch: {
- value(v) {
- this.v = parseFloat(v);
- }
- },
- mounted() {
- if (this.autofocus) {
- this.$nextTick(() => {
- this.$refs.input.focus();
- });
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.timctyfi {
- position: relative;
- margin: 8px;
-
- > .icon {
- display: inline-block;
- width: 24px;
- text-align: center;
- }
-
- > .title {
- pointer-events: none;
- font-size: 16px;
- color: var(--inputLabel);
- overflow: hidden;
- }
-
- > input {
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
- background: var(--X10);
- height: 7px;
- margin: 0 8px;
- 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>
diff --git a/src/client/components/ui/select.vue b/src/client/components/ui/select.vue
deleted file mode 100644
index e9d43d8a64..0000000000
--- a/src/client/components/ui/select.vue
+++ /dev/null
@@ -1,262 +0,0 @@
-<template>
-<div class="vblkjoeq">
- <div class="label" @click="focus"><slot name="label"></slot></div>
- <div class="input" :class="{ inline, disabled, focused }">
- <div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
- <select ref="inputEl"
- v-model="v"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- :placeholder="placeholder"
- @focus="focused = true"
- @blur="focused = false"
- @input="onInput"
- >
- <slot></slot>
- </select>
- <div class="suffix" ref="suffixEl"><i class="fas fa-chevron-down"></i></div>
- </div>
- <div class="caption"><slot name="caption"></slot></div>
-
- <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from './button.vue';
-
-export default defineComponent({
- components: {
- MkButton,
- },
-
- props: {
- modelValue: {
- required: true
- },
- required: {
- type: Boolean,
- required: false
- },
- readonly: {
- type: Boolean,
- required: false
- },
- disabled: {
- type: Boolean,
- required: false
- },
- placeholder: {
- type: String,
- required: false
- },
- autofocus: {
- type: Boolean,
- required: false,
- default: false
- },
- inline: {
- type: Boolean,
- required: false,
- default: false
- },
- manualSave: {
- type: Boolean,
- required: false,
- default: false
- },
- },
-
- emits: ['change', 'update:modelValue'],
-
- setup(props, context) {
- const { modelValue, autofocus } = toRefs(props);
- const v = ref(modelValue.value);
- 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 updated = () => {
- changed.value = false;
- context.emit('update:modelValue', v.value);
- };
-
- watch(modelValue, newValue => {
- v.value = newValue;
- });
-
- watch(v, newValue => {
- if (!props.manualSave) {
- updated();
- }
-
- 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 {
- v,
- focused,
- invalid,
- changed,
- filled,
- inputEl,
- prefixEl,
- suffixEl,
- focus,
- onInput,
- updated,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.vblkjoeq {
- margin: 1.5em 0;
-
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 12px;
- user-select: none;
-
- &:empty {
- display: none;
- }
- }
-
- > .caption {
- font-size: 0.8em;
- padding: 8px 0 0 12px;
- color: var(--fgTransparentWeak);
-
- &:empty {
- display: none;
- }
- }
-
- > .input {
- $height: 42px;
- position: relative;
-
- > select {
- appearance: none;
- -webkit-appearance: none;
- display: block;
- height: $height;
- width: 100%;
- margin: 0;
- padding: 0 12px;
- font: inherit;
- font-weight: normal;
- font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--inputBorder);
- border-radius: 6px;
- outline: none;
- box-shadow: none;
- box-sizing: border-box;
- cursor: pointer;
- transition: border-color 0.1s ease-out;
-
- &:hover {
- border-color: var(--inputBorderHover);
- }
- }
-
- > .prefix,
- > .suffix {
- display: flex;
- align-items: center;
- position: absolute;
- z-index: 1;
- top: 0;
- padding: 0 12px;
- font-size: 1em;
- height: $height;
- 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: 6px;
- }
-
- > .suffix {
- right: 0;
- padding-left: 6px;
- }
-
- &.inline {
- display: inline-block;
- margin: 0;
- }
-
- &.focused {
- > select {
- border-color: var(--accent);
- }
- }
-
- &.disabled {
- opacity: 0.7;
-
- &, * {
- cursor: not-allowed !important;
- }
- }
- }
-}
-</style>
diff --git a/src/client/components/ui/super-menu.vue b/src/client/components/ui/super-menu.vue
new file mode 100644
index 0000000000..35fc81550d
--- /dev/null
+++ b/src/client/components/ui/super-menu.vue
@@ -0,0 +1,151 @@
+<template>
+<div class="rrevdjwu" :class="{ grid }">
+ <div class="group" v-for="group in def">
+ <div class="title" v-if="group.title">{{ group.title }}</div>
+
+ <div class="items">
+ <template v-for="(item, i) in group.items">
+ <a v-if="item.type === 'a'" :href="item.href" :target="item.target" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+ <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
+ <span class="text">{{ item.text }}</span>
+ </a>
+ <button v-else-if="item.type === 'button'" @click="ev => item.action(ev)" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active">
+ <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
+ <span class="text">{{ item.text }}</span>
+ </button>
+ <MkA v-else :to="item.to" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }">
+ <i v-if="item.icon" class="icon fa-fw" :class="item.icon"></i>
+ <span class="text">{{ item.text }}</span>
+ </MkA>
+ </template>
+ </div>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, unref } from 'vue';
+
+export default defineComponent({
+ props: {
+ def: {
+ type: Array,
+ required: true
+ },
+ grid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+.rrevdjwu {
+ > .group {
+ & + .group {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: solid 0.5px var(--divider);
+ }
+
+ margin-left: 16px;
+ margin-right: 16px;
+
+ > .title {
+ font-size: 0.9em;
+ opacity: 0.7;
+ margin: 0 0 8px 12px;
+ }
+
+ > .items {
+ > .item {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 10px 16px 10px 8px;
+ border-radius: 9px;
+ font-size: 0.9em;
+
+ &:hover {
+ text-decoration: none;
+ background: var(--panelHighlight);
+ }
+
+ &.active {
+ color: var(--accent);
+ background: var(--accentedBg);
+ }
+
+ &.danger {
+ color: var(--error);
+ }
+
+ > .icon {
+ width: 32px;
+ margin-right: 2px;
+ flex-shrink: 0;
+ text-align: center;
+ opacity: 0.8;
+ }
+
+ > .text {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding-right: 12px;
+ }
+
+ }
+ }
+ }
+
+ &.grid {
+ > .group {
+ & + .group {
+ padding-top: 0;
+ border-top: none;
+ }
+
+ margin-left: 0;
+ margin-right: 0;
+
+ > .title {
+ font-size: 1em;
+ opacity: 0.7;
+ margin: 0 0 8px 16px;
+ }
+
+ > .items {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
+ grid-gap: 8px;
+ padding: 0 16px;
+
+ > .item {
+ flex-direction: column;
+ padding: 18px 16px 16px 16px;
+ background: var(--panel);
+ border-radius: 8px;
+ text-align: center;
+
+ > .icon {
+ display: block;
+ margin-right: 0;
+ margin-bottom: 12px;
+ font-size: 1.5em;
+ }
+
+ > .text {
+ padding-right: 0;
+ width: 100%;
+ font-size: 0.8em;
+ }
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/client/components/ui/switch.vue b/src/client/components/ui/switch.vue
deleted file mode 100644
index 7aa9c0619d..0000000000
--- a/src/client/components/ui/switch.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-<template>
-<div
- class="ziffeoms"
- :class="{ disabled, checked }"
- role="switch"
- :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>
- <p><slot name="caption"></slot></p>
- </span>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
- props: {
- modelValue: {
- type: Boolean,
- default: false
- },
- disabled: {
- type: Boolean,
- default: false
- }
- },
- computed: {
- checked(): boolean {
- return this.modelValue;
- }
- },
- methods: {
- toggle() {
- if (this.disabled) return;
- this.$emit('update:modelValue', !this.checked);
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.ziffeoms {
- position: relative;
- display: flex;
- margin: 32px 0;
- cursor: pointer;
- transition: all 0.3s;
-
- &:first-child {
- margin-top: 0;
- }
-
- &:last-child {
- margin-bottom: 0;
- }
-
- > * {
- 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: inherit;
-
- > * {
- 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: 8px;
- display: block;
- cursor: pointer;
- transition: inherit;
- color: var(--fg);
-
- > span {
- display: block;
- line-height: 20px;
- transition: inherit;
- }
-
- > p {
- margin: 0;
- color: var(--fgTransparentWeak);
- font-size: 90%;
- }
- }
-}
-</style>
diff --git a/src/client/components/ui/textarea.vue b/src/client/components/ui/textarea.vue
deleted file mode 100644
index 08ac3182a9..0000000000
--- a/src/client/components/ui/textarea.vue
+++ /dev/null
@@ -1,254 +0,0 @@
-<template>
-<div class="adhpbeos">
- <div class="label" @click="focus"><slot name="label"></slot></div>
- <div class="input" :class="{ disabled, focused, tall, pre }">
- <textarea ref="inputEl"
- :class="{ code, _monospace: code }"
- v-model="v"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- :placeholder="placeholder"
- :pattern="pattern"
- :autocomplete="autocomplete"
- :spellcheck="spellcheck"
- @focus="focused = true"
- @blur="focused = false"
- @keydown="onKeydown($event)"
- @input="onInput"
- ></textarea>
- </div>
- <div class="caption"><slot name="caption"></slot></div>
-
- <MkButton v-if="manualSave && changed" @click="updated" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
-import MkButton from './button.vue';
-import { debounce } from 'throttle-debounce';
-
-export default defineComponent({
- components: {
- MkButton,
- },
-
- props: {
- modelValue: {
- required: true
- },
- 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
- },
- code: {
- type: Boolean,
- required: false
- },
- tall: {
- type: Boolean,
- required: false,
- default: false
- },
- pre: {
- type: Boolean,
- required: false,
- default: false
- },
- debounce: {
- type: Boolean,
- required: false,
- default: false
- },
- manualSave: {
- type: Boolean,
- required: false,
- default: false
- },
- },
-
- emits: ['change', 'keydown', 'enter', 'update:modelValue'],
-
- setup(props, context) {
- const { modelValue, autofocus } = toRefs(props);
- const v = ref(modelValue.value);
- 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 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');
- }
- };
-
- const updated = () => {
- changed.value = false;
- context.emit('update:modelValue', v.value);
- };
-
- const debouncedUpdated = debounce(1000, updated);
-
- watch(modelValue, newValue => {
- v.value = newValue;
- });
-
- watch(v, newValue => {
- if (!props.manualSave) {
- if (props.debounce) {
- debouncedUpdated();
- } else {
- updated();
- }
- }
-
- invalid.value = inputEl.value.validity.badInput;
- });
-
- onMounted(() => {
- nextTick(() => {
- if (autofocus.value) {
- focus();
- }
- });
- });
-
- return {
- v,
- focused,
- invalid,
- changed,
- filled,
- inputEl,
- focus,
- onInput,
- onKeydown,
- updated,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.adhpbeos {
- margin: 1.5em 0;
-
- > .label {
- font-size: 0.85em;
- padding: 0 0 8px 12px;
- user-select: none;
-
- &:empty {
- display: none;
- }
- }
-
- > .caption {
- font-size: 0.8em;
- padding: 8px 0 0 12px;
- color: var(--fgTransparentWeak);
-
- &:empty {
- display: none;
- }
- }
-
- > .input {
- position: relative;
-
- > textarea {
- appearance: none;
- -webkit-appearance: none;
- display: block;
- width: 100%;
- min-width: 100%;
- max-width: 100%;
- min-height: 130px;
- margin: 0;
- padding: 12px;
- font: inherit;
- font-weight: normal;
- font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 0.5px var(--inputBorder);
- border-radius: 6px;
- outline: none;
- box-shadow: none;
- box-sizing: border-box;
- transition: border-color 0.1s ease-out;
-
- &:hover {
- border-color: var(--inputBorderHover);
- }
- }
-
- &.focused {
- > textarea {
- border-color: var(--accent);
- }
- }
-
- &.disabled {
- opacity: 0.7;
-
- &, * {
- cursor: not-allowed !important;
- }
- }
-
- &.tall {
- > textarea {
- min-height: 200px;
- }
- }
-
- &.pre {
- > textarea {
- white-space: pre;
- }
- }
- }
-}
-</style>
diff --git a/src/client/components/ui/window.vue b/src/client/components/ui/window.vue
index 773c3b9b13..00284b0467 100644
--- a/src/client/components/ui/window.vue
+++ b/src/client/components/ui/window.vue
@@ -3,11 +3,16 @@
<div class="ebkgocck" :class="{ front }" v-if="showing">
<div class="body _window _shadow _narrow_" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
- <button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button>
-
+ <span class="left">
+ <slot name="headerLeft"></slot>
+ </span>
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
<slot name="header"></slot>
</span>
+ <span class="right">
+ <slot name="headerRight"></slot>
+ <button v-if="closeButton" class="_button" @click="close()"><i class="fas fa-times"></i></button>
+ </span>
</div>
<div class="body" v-if="padding">
<div class="_section">
@@ -377,7 +382,7 @@ export default defineComponent({
<style lang="scss" scoped>
.window-enter-active, .window-leave-active {
- transition: opacity 0.3s, transform 0.3s !important;
+ transition: opacity 0.2s, transform 0.2s !important;
}
.window-enter-from, .window-leave-to {
pointer-events: none;
@@ -418,12 +423,14 @@ export default defineComponent({
height: var(--height);
border-bottom: solid 1px var(--divider);
- > ::v-deep(button) {
- height: var(--height);
- width: var(--height);
+ > .left, > .right {
+ > ::v-deep(button) {
+ height: var(--height);
+ width: var(--height);
- &:hover {
- color: var(--fgHighlighted);
+ &:hover {
+ color: var(--fgHighlighted);
+ }
}
}
diff --git a/src/client/components/user-select-dialog.vue b/src/client/components/user-select-dialog.vue
index 87c32dab25..0f3ee2a126 100644
--- a/src/client/components/user-select-dialog.vue
+++ b/src/client/components/user-select-dialog.vue
@@ -10,7 +10,7 @@
<template #header>{{ $ts.selectUser }}</template>
<div class="tbhwbxda _monolithic_">
<div class="_section">
- <div class="_inputSplit _inputNoTopMargin _inputNoBottomMargin">
+ <div class="_inputSplit">
<MkInput v-model="username" class="input" @update:modelValue="search" ref="username">
<template #label>{{ $ts.username }}</template>
<template #prefix>@</template>
@@ -52,7 +52,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import MkInput from './ui/input.vue';
+import MkInput from './form/input.vue';
import XModalWindow from '@client/components/ui/modal-window.vue';
import * as os from '@client/os';
diff --git a/src/client/components/widgets.vue b/src/client/components/widgets.vue
index 150d61c027..aef5de453c 100644
--- a/src/client/components/widgets.vue
+++ b/src/client/components/widgets.vue
@@ -30,7 +30,7 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { v4 as uuid } from 'uuid';
-import MkSelect from '@client/components/ui/select.vue';
+import MkSelect from '@client/components/form/select.vue';
import MkButton from '@client/components/ui/button.vue';
import { widgets as widgetDefs } from '@client/widgets';
diff --git a/src/client/directives/click-anime.ts b/src/client/directives/click-anime.ts
index 9fd583d6dd..0d5a6da94e 100644
--- a/src/client/directives/click-anime.ts
+++ b/src/client/directives/click-anime.ts
@@ -1,7 +1,10 @@
import { Directive } from 'vue';
+import { defaultStore } from '@client/store';
export default {
mounted(el, binding, vn) {
+ if (!defaultStore.state.animation) return;
+
el.classList.add('_anime_bounce_standBy');
el.addEventListener('mousedown', () => {
diff --git a/src/client/directives/get-size.ts b/src/client/directives/get-size.ts
new file mode 100644
index 0000000000..e3b5dea0f3
--- /dev/null
+++ b/src/client/directives/get-size.ts
@@ -0,0 +1,34 @@
+import { Directive } from 'vue';
+
+export default {
+ mounted(src, binding, vn) {
+ const calc = () => {
+ const height = src.clientHeight;
+ const width = src.clientWidth;
+
+ // 要素が(一時的に)DOMに存在しないときは計算スキップ
+ if (height === 0) return;
+
+ binding.value(width, height);
+ };
+
+ calc();
+
+ // Vue3では使えなくなった
+ // 無くても大丈夫か...?
+ // TODO: ↑大丈夫じゃなかったので解決策を探す
+ //vn.context.$on('hook:activated', calc);
+
+ const ro = new ResizeObserver((entries, observer) => {
+ calc();
+ });
+ ro.observe(src);
+
+ src._get_size_ro_ = ro;
+ },
+
+ unmounted(src, binding, vn) {
+ binding.value(0, 0);
+ src._get_size_ro_.unobserve(src);
+ }
+} as Directive;
diff --git a/src/client/directives/index.ts b/src/client/directives/index.ts
index f0a0123771..cd71bc26d3 100644
--- a/src/client/directives/index.ts
+++ b/src/client/directives/index.ts
@@ -2,6 +2,7 @@ import { App } from 'vue';
import userPreview from './user-preview';
import size from './size';
+import getSize from './get-size';
import particle from './particle';
import tooltip from './tooltip';
import hotkey from './hotkey';
@@ -14,6 +15,7 @@ export default function(app: App) {
app.directive('userPreview', userPreview);
app.directive('user-preview', userPreview);
app.directive('size', size);
+ app.directive('get-size', getSize);
app.directive('particle', particle);
app.directive('tooltip', tooltip);
app.directive('hotkey', hotkey);
diff --git a/src/client/directives/tooltip.ts b/src/client/directives/tooltip.ts
index ee690558af..32d137b2e2 100644
--- a/src/client/directives/tooltip.ts
+++ b/src/client/directives/tooltip.ts
@@ -36,7 +36,7 @@ export default {
});
}
- const show = e => {
+ self.show = () => {
if (!document.body.contains(el)) return;
if (self._close) return;
if (self.text == null) return;
@@ -60,7 +60,7 @@ export default {
el.addEventListener(start, () => {
clearTimeout(self.showTimer);
clearTimeout(self.hideTimer);
- self.showTimer = setTimeout(show, delay);
+ self.showTimer = setTimeout(self.show, delay);
}, { passive: true });
el.addEventListener(end, () => {
@@ -75,6 +75,11 @@ export default {
});
},
+ updated(el, binding) {
+ const self = el._tooltipDirective_;
+ self.text = binding.value as string;
+ },
+
unmounted(el, binding, vn) {
const self = el._tooltipDirective_;
clearInterval(self.checkTimer);
diff --git a/src/client/events.ts b/src/client/events.ts
new file mode 100644
index 0000000000..dbbd908b8f
--- /dev/null
+++ b/src/client/events.ts
@@ -0,0 +1,4 @@
+import { EventEmitter } from 'eventemitter3';
+
+// TODO: 型付け
+export const globalEvents = new EventEmitter();
diff --git a/src/client/pages/_error_.vue b/src/client/pages/_error_.vue
index 1d67d9b14d..d1cefad8dd 100644
--- a/src/client/pages/_error_.vue
+++ b/src/client/pages/_error_.vue
@@ -1,9 +1,16 @@
<template>
+<MkLoading v-if="!loaded" />
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
- <div class="mjndxjch">
+ <div class="mjndxjch" v-show="loaded">
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
<p><b><i class="fas fa-exclamation-triangle"></i> {{ $ts.pageLoadError }}</b></p>
- <p>{{ $ts.pageLoadErrorDescription }}</p>
+ <p v-if="version === meta.version">{{ $ts.pageLoadErrorDescription }}</p>
+ <p v-else-if="serverIsDead">{{ $ts.serverIsDead }}</p>
+ <template v-else>
+ <p>{{ $ts.newVersionOfClientAvailable }}</p>
+ <p>{{ $ts.youShouldUpgradeClient }}</p>
+ <MkButton @click="reload" class="button primary">{{ $ts.reload }}</MkButton>
+ </template>
<p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p>
<p v-if="error" class="error">ERROR: {{ error }}</p>
</div>
@@ -14,6 +21,9 @@
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import * as symbols from '@client/symbols';
+import { version } from '@client/config';
+import * as os from '@client/os';
+import { unisonReload } from '@client/scripts/unison-reload';
export default defineComponent({
components: {
@@ -30,8 +40,30 @@ export default defineComponent({
title: this.$ts.error,
icon: 'fas fa-exclamation-triangle'
},
+ loaded: false,
+ serverIsDead: false,
+ meta: {} as any,
+ version,
};
},
+ created() {
+ os.api('meta', {
+ detail: false
+ }).then(meta => {
+ this.loaded = true;
+ this.serverIsDead = false;
+ this.meta = meta;
+ localStorage.setItem('v', meta.version);
+ }, () => {
+ this.loaded = true;
+ this.serverIsDead = true;
+ });
+ },
+ methods: {
+ reload() {
+ unisonReload();
+ },
+ },
});
</script>
@@ -45,7 +77,7 @@ export default defineComponent({
}
> .button {
- margin: 0 auto;
+ margin: 8px auto;
}
> img {
diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue
index 384c7e8ccb..d2c0ec0550 100644
--- a/src/client/pages/about-misskey.vue
+++ b/src/client/pages/about-misskey.vue
@@ -2,15 +2,15 @@
<div style="overflow: clip;">
<FormBase class="znqjceqz">
<div id="debug"></div>
- <section class="_formItem about">
- <div class="_formPanel panel" :class="{ playing: easterEggEngine != null }" ref="about">
+ <section class="_debobigegoItem about">
+ <div class="_debobigegoPanel panel" :class="{ playing: easterEggEngine != null }" ref="about">
<img src="/static-assets/client/about-icon.png" alt="" class="icon" @load="iconLoaded" draggable="false" @click="gravity"/>
<div class="misskey">Misskey</div>
<div class="version">v{{ version }}</div>
<span class="emoji" v-for="emoji in easterEggEmojis" :key="emoji.id" :data-physics-x="emoji.left" :data-physics-y="emoji.top" :class="{ _physics_circle_: !emoji.emoji.startsWith(':') }"><MkEmoji class="emoji" :emoji="emoji.emoji" :custom-emojis="$instance.emojis" :is-reaction="false" :normal="true" :no-style="true"/></span>
</div>
</section>
- <section class="_formItem" style="text-align: center; padding: 0 16px;">
+ <section class="_debobigegoItem" style="text-align: center; padding: 0 16px;">
{{ $ts._aboutMisskey.about }}<br><MkA class="_link" to="/docs/general/misskey">{{ $ts.learnMore }}</MkA>
</section>
<FormGroup>
@@ -55,10 +55,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { version } from '@client/config';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
import MkLink from '@client/components/link.vue';
import { physics } from '@client/scripts/physics';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue
index bdd4c78827..2c580c293a 100644
--- a/src/client/pages/about.vue
+++ b/src/client/pages/about.vue
@@ -1,7 +1,7 @@
<template>
<FormBase>
- <div class="_formItem">
- <div class="_formPanel fwhjspax">
+ <div class="_debobigegoItem">
+ <div class="_debobigegoPanel fwhjspax">
<img :src="$instance.iconUrl || $instance.faviconUrl || '/favicon.ico'" alt="" class="icon"/>
<span class="name">{{ $instance.name || host }}</span>
</div>
@@ -59,12 +59,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { version, instanceName } from '@client/config';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import number from '@client/filters/number';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/advanced-theme-editor.vue b/src/client/pages/advanced-theme-editor.vue
index c03d88b82d..8a63d74887 100644
--- a/src/client/pages/advanced-theme-editor.vue
+++ b/src/client/pages/advanced-theme-editor.vue
@@ -4,7 +4,7 @@
<div class="_content">
<details>
<summary>{{ $ts.import }}</summary>
- <MkTextarea v-model:value="themeToImport">
+ <MkTextarea v-model="themeToImport">
{{ $ts._theme.importInfo }}
</MkTextarea>
<MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
@@ -14,9 +14,9 @@
<section class="_section">
<div class="_content _card _gap">
<div class="_content">
- <MkInput v-model:value="name" required><span>{{ $ts.name }}</span></MkInput>
- <MkInput v-model:value="author" required><span>{{ $ts.author }}</span></MkInput>
- <MkTextarea v-model:value="description"><span>{{ $ts.description }}</span></MkTextarea>
+ <MkInput v-model="name" required><span>{{ $ts.name }}</span></MkInput>
+ <MkInput v-model="author" required><span>{{ $ts.author }}</span></MkInput>
+ <MkTextarea v-model="description"><span>{{ $ts.description }}</span></MkTextarea>
<div class="_inputs">
<div v-text="$ts._theme.base" />
<MkRadio v-model="baseTheme" value="light">{{ $ts.light }}</MkRadio>
@@ -41,31 +41,31 @@
<!-- color -->
<div v-else-if="typeof v === 'string'" class="color">
<input type="color" :value="v" @input="colorChanged($event.target.value, i)"/>
- <MkInput class="select" :value="v" @update:value="colorChanged($event, i)"/>
+ <MkInput class="select" :value="v" @update:modelValue="colorChanged($event, i)"/>
</div>
<!-- ref const -->
- <MkInput v-else-if="v.type === 'refConst'" v-model:value="v.key">
+ <MkInput v-else-if="v.type === 'refConst'" v-model="v.key">
<template #prefix>$</template>
<span>{{ $ts.name }}</span>
</MkInput>
<!-- ref props -->
- <MkSelect class="select" v-else-if="v.type === 'refProp'" v-model:value="v.key">
+ <MkSelect class="select" v-else-if="v.type === 'refProp'" v-model="v.key">
<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
</MkSelect>
<!-- func -->
<template v-else-if="v.type === 'func'">
- <MkSelect class="select" v-model:value="v.name">
+ <MkSelect class="select" v-model="v.name">
<template #label>{{ $ts._theme.funcKind }}</template>
<option v-for="n in ['alpha', 'darken', 'lighten']" :value="n" :key="n">{{ $t('_theme.' + n) }}</option>
</MkSelect>
- <MkInput type="number" v-model:value="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput>
- <MkSelect class="select" v-model:value="v.value">
+ <MkInput type="number" v-model="v.arg"><span>{{ $ts._theme.argument }}</span></MkInput>
+ <MkSelect class="select" v-model="v.value">
<template #label>{{ $ts._theme.basedProp }}</template>
<option v-for="key in themeProps" :value="key" :key="key">{{ $t('_theme.keys.' + key) }}</option>
</MkSelect>
</template>
<!-- CSS -->
- <MkInput v-else-if="v.type === 'css'" v-model:value="v.value">
+ <MkInput v-else-if="v.type === 'css'" v-model="v.value">
<span>CSS</span>
</MkInput>
</div>
@@ -95,11 +95,11 @@ import { defineComponent } from 'vue';
import * as JSON5 from 'json5';
import { toUnicode } from 'punycode/';
-import MkRadio from '@client/components/ui/radio.vue';
+import MkRadio from '@client/components/form/radio.vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkSelect from '@client/components/form/select.vue';
import MkSample from '@client/components/sample.vue';
import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@client/scripts/theme-editor';
diff --git a/src/client/pages/announcements.vue b/src/client/pages/announcements.vue
index a7ccb03588..6a0cbd67ba 100644
--- a/src/client/pages/announcements.vue
+++ b/src/client/pages/announcements.vue
@@ -1,17 +1,20 @@
<template>
-<div class="_section">
- <MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content">
- <section class="_card announcement _gap" v-for="(announcement, i) in items" :key="announcement.id">
- <div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
- <div class="_content">
- <Mfm :text="announcement.text"/>
- <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
- </div>
- <div class="_footer" v-if="$i && !announcement.isRead">
- <MkButton @click="read(items, announcement, i)" primary><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
- </div>
- </section>
- </MkPagination>
+<div>
+ <MkHeader :info="header"/>
+ <div class="_section">
+ <MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content">
+ <section class="_card announcement _gap" v-for="(announcement, i) in items" :key="announcement.id">
+ <div class="_title"><span v-if="$i && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
+ <div class="_content">
+ <Mfm :text="announcement.text"/>
+ <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
+ </div>
+ <div class="_footer" v-if="$i && !announcement.isRead">
+ <MkButton @click="read(items, announcement, i)" primary><i class="fas fa-check"></i> {{ $ts.gotIt }}</MkButton>
+ </div>
+ </section>
+ </MkPagination>
+ </div>
</div>
</template>
@@ -32,7 +35,13 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.announcements,
- icon: 'fas fa-broadcast-tower'
+ icon: 'fas fa-broadcast-tower',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.announcements,
+ icon: 'fas fa-broadcast-tower',
+ bg: 'var(--bg)',
},
pagination: {
endpoint: 'announcements',
diff --git a/src/client/pages/antenna-timeline.vue b/src/client/pages/antenna-timeline.vue
index 425bec6987..c99124dbdc 100644
--- a/src/client/pages/antenna-timeline.vue
+++ b/src/client/pages/antenna-timeline.vue
@@ -89,7 +89,7 @@ export default defineComponent({
},
top() {
- scroll(this.$el, 0);
+ scroll(this.$el, { top: 0 });
},
async timetravel() {
diff --git a/src/client/pages/api-console.vue b/src/client/pages/api-console.vue
index c6d459fd6d..9aa7d4ea4d 100644
--- a/src/client/pages/api-console.vue
+++ b/src/client/pages/api-console.vue
@@ -1,7 +1,7 @@
<template>
<div class="_root">
<div class="_block" style="padding: 24px;">
- <MkInput v-model="endpoint" :datalist="endpoints" @update:modelValue="onEndpointChange()" class="_inputNoTopMargin">
+ <MkInput v-model="endpoint" :datalist="endpoints" @update:modelValue="onEndpointChange()" class="">
<template #label>Endpoint</template>
</MkInput>
<MkTextarea v-model="body" code>
@@ -27,9 +27,9 @@
import { defineComponent } from 'vue';
import * as JSON5 from 'json5';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkSwitch from '@client/components/form/switch.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/channel-editor.vue b/src/client/pages/channel-editor.vue
index eeea0b70aa..67e27896ce 100644
--- a/src/client/pages/channel-editor.vue
+++ b/src/client/pages/channel-editor.vue
@@ -27,9 +27,9 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import { selectFile } from '@client/scripts/select-file';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/channels.vue b/src/client/pages/channels.vue
index 7e3302959b..fd1408c253 100644
--- a/src/client/pages/channels.vue
+++ b/src/client/pages/channels.vue
@@ -1,7 +1,7 @@
<template>
<div>
<div class="_section" style="padding: 0;" v-if="$i">
- <MkTab class="_content" v-model:value="tab">
+ <MkTab class="_content" v-model="tab">
<option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._channel.featured }}</option>
<option value="following"><i class="fas fa-heart"></i> {{ $ts._channel.following }}</option>
<option value="owned"><i class="fas fa-edit"></i> {{ $ts._channel.owned }}</option>
diff --git a/src/client/pages/docs.vue b/src/client/pages/docs.vue
index be4d4255db..629dc2be53 100644
--- a/src/client/pages/docs.vue
+++ b/src/client/pages/docs.vue
@@ -2,7 +2,7 @@
<div class="vtaihdtm">
<div class="body">
<div class="search">
- <MkInput v-model="query" :debounce="true" type="search" class="_inputNoTopMargin _inputNoBottomMargin" :placeholder="$ts.search">
+ <MkInput v-model="query" :debounce="true" type="search" class="" :placeholder="$ts.search">
<template #prefix><i class="fas fa-search"></i></template>
</MkInput>
</div>
@@ -57,7 +57,7 @@ import { defineComponent } from 'vue';
import { url, lang } from '@client/config';
import * as symbols from '@client/symbols';
import MkFolder from '@client/components/ui/folder.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
export default defineComponent({
components: {
diff --git a/src/client/pages/emojis.category.vue b/src/client/pages/emojis.category.vue
index 091c3f20a9..e725bcb31f 100644
--- a/src/client/pages/emojis.category.vue
+++ b/src/client/pages/emojis.category.vue
@@ -1,13 +1,15 @@
<template>
<div class="driuhtrh">
<div class="query">
- <MkInput v-model="q" class="_inputNoTopMargin _inputNoBottomMargin" :placeholder="$ts.search">
+ <MkInput v-model="q" class="" :placeholder="$ts.search">
<template #prefix><i class="fas fa-search"></i></template>
</MkInput>
+ <!-- たくさんあると邪魔
<div class="tags">
<span class="tag _button" v-for="tag in tags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
</div>
+ -->
</div>
<MkFolder class="emojis" v-if="searchEmojis">
@@ -29,8 +31,8 @@
<script lang="ts">
import { defineComponent, computed } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
import MkFolder from '@client/components/ui/folder.vue';
import MkTab from '@client/components/tab.vue';
import * as os from '@client/os';
@@ -120,7 +122,6 @@ export default defineComponent({
}
> .emojis {
- --x-header: var(--bg);
--x-padding: 0 16px;
.zuvgdzyt {
diff --git a/src/client/pages/emojis.emoji.vue b/src/client/pages/emojis.emoji.vue
index 3c9bb4debe..ca0ef2dbb7 100644
--- a/src/client/pages/emojis.emoji.vue
+++ b/src/client/pages/emojis.emoji.vue
@@ -23,12 +23,14 @@ export default defineComponent({
},
mounted() {
- VanillaTilt.init(this.$el, {
- reverse: true,
- gyroscope: false,
- scale: 1.1,
- speed: 500,
- });
+ if (this.$store.animation) {
+ VanillaTilt.init(this.$el, {
+ reverse: true,
+ gyroscope: false,
+ scale: 1.1,
+ speed: 500,
+ });
+ }
},
methods: {
diff --git a/src/client/pages/emojis.vue b/src/client/pages/emojis.vue
index 8918de2338..d61fd25d3c 100644
--- a/src/client/pages/emojis.vue
+++ b/src/client/pages/emojis.vue
@@ -1,6 +1,9 @@
<template>
-<div :class="$style.root">
- <XCategory v-if="tab === 'category'"/>
+<div>
+ <MkHeader :info="header"/>
+ <div :class="$style.root">
+ <XCategory v-if="tab === 'category'"/>
+ </div>
</div>
</template>
@@ -22,6 +25,11 @@ export default defineComponent({
icon: 'fas fa-laugh',
bg: 'var(--bg)',
})),
+ header: computed(() => ({
+ title: this.$ts.customEmojis,
+ icon: 'fas fa-laugh',
+ bg: 'var(--bg)',
+ })),
tab: 'category',
}
},
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
index 7054940a1a..2ca0668611 100644
--- a/src/client/pages/explore.vue
+++ b/src/client/pages/explore.vue
@@ -1,73 +1,80 @@
<template>
-<div class="lznhrdub _root">
- <div>
- <div class="_isolated">
- <MkInput v-model="query" :debounce="true" type="search">
- <template #prefix><i class="fas fa-search"></i></template>
- <template #label>{{ $ts.searchUser }}</template>
- </MkInput>
- </div>
+<div>
+ <MkHeader :info="header"/>
- <XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/>
+ <MkSpacer :content-max="1200">
+ <div class="lznhrdub">
+ <div v-if="tab === 'local'">
+ <div class="localfedi7 _block _isolated" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
+ <header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header>
+ <div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div>
+ </div>
- <div class="localfedi7 _block _isolated" v-if="meta && stats && tag == null" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }">
- <header><span>{{ $t('explore', { host: meta.name || 'Misskey' }) }}</span></header>
- <div><span>{{ $t('exploreUsersCount', { count: num(stats.originalUsersCount) }) }}</span></div>
- </div>
+ <template v-if="tag == null">
+ <MkFolder class="_gap" persist-key="explore-pinned-users">
+ <template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
+ <XUserList :pagination="pinnedUsers"/>
+ </MkFolder>
+ <MkFolder class="_gap" persist-key="explore-popular-users">
+ <template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
+ <XUserList :pagination="popularUsers"/>
+ </MkFolder>
+ <MkFolder class="_gap" persist-key="explore-recently-updated-users">
+ <template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
+ <XUserList :pagination="recentlyUpdatedUsers"/>
+ </MkFolder>
+ <MkFolder class="_gap" persist-key="explore-recently-registered-users">
+ <template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
+ <XUserList :pagination="recentlyRegisteredUsers"/>
+ </MkFolder>
+ </template>
+ </div>
+ <div v-else-if="tab === 'remote'">
+ <div class="localfedi7 _block _isolated" v-if="tag == null" :style="{ backgroundImage: `url(/static-assets/client/fedi.jpg)` }">
+ <header><span>{{ $ts.exploreFediverse }}</span></header>
+ </div>
- <template v-if="tag == null">
- <MkFolder class="_gap" persist-key="explore-pinned-users">
- <template #header><i class="fas fa-bookmark fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.pinnedUsers }}</template>
- <XUserList :pagination="pinnedUsers"/>
- </MkFolder>
- <MkFolder class="_gap" persist-key="explore-popular-users">
- <template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
- <XUserList :pagination="popularUsers"/>
- </MkFolder>
- <MkFolder class="_gap" persist-key="explore-recently-updated-users">
- <template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
- <XUserList :pagination="recentlyUpdatedUsers"/>
- </MkFolder>
- <MkFolder class="_gap" persist-key="explore-recently-registered-users">
- <template #header><i class="fas fa-plus fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyRegisteredUsers }}</template>
- <XUserList :pagination="recentlyRegisteredUsers"/>
- </MkFolder>
- </template>
- </div>
- <div>
- <div class="localfedi7 _block _isolated" v-if="tag == null" :style="{ backgroundImage: `url(/static-assets/client/fedi.jpg)` }">
- <header><span>{{ $ts.exploreFediverse }}</span></header>
- </div>
+ <MkFolder :foldable="true" :expanded="false" ref="tags" class="_gap">
+ <template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
- <MkFolder :foldable="true" :expanded="false" ref="tags" class="_gap">
- <template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularTags }}</template>
+ <div class="vxjfqztj">
+ <MkA v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</MkA>
+ <MkA v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</MkA>
+ </div>
+ </MkFolder>
- <div class="vxjfqztj">
- <MkA v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</MkA>
- <MkA v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</MkA>
- </div>
- </MkFolder>
+ <MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
+ <template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
+ <XUserList :pagination="tagUsers"/>
+ </MkFolder>
- <MkFolder v-if="tag != null" :key="`${tag}`" class="_gap">
- <template #header><i class="fas fa-hashtag fa-fw" style="margin-right: 0.5em;"></i>{{ tag }}</template>
- <XUserList :pagination="tagUsers"/>
- </MkFolder>
+ <template v-if="tag == null">
+ <MkFolder class="_gap">
+ <template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
+ <XUserList :pagination="popularUsersF"/>
+ </MkFolder>
+ <MkFolder class="_gap">
+ <template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
+ <XUserList :pagination="recentlyUpdatedUsersF"/>
+ </MkFolder>
+ <MkFolder class="_gap">
+ <template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
+ <XUserList :pagination="recentlyRegisteredUsersF"/>
+ </MkFolder>
+ </template>
+ </div>
+ <div v-else-if="tab === 'search'">
+ <div class="_isolated">
+ <MkInput v-model="query" :debounce="true" type="search">
+ <template #prefix><i class="fas fa-search"></i></template>
+ <template #label>{{ $ts.searchUser }}</template>
+ </MkInput>
+ </div>
- <template v-if="tag == null">
- <MkFolder class="_gap">
- <template #header><i class="fas fa-chart-line fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.popularUsers }}</template>
- <XUserList :pagination="popularUsersF"/>
- </MkFolder>
- <MkFolder class="_gap">
- <template #header><i class="fas fa-comment-alt fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyUpdatedUsers }}</template>
- <XUserList :pagination="recentlyUpdatedUsersF"/>
- </MkFolder>
- <MkFolder class="_gap">
- <template #header><i class="fas fa-rocket fa-fw" style="margin-right: 0.5em;"></i>{{ $ts.recentlyDiscoveredUsers }}</template>
- <XUserList :pagination="recentlyRegisteredUsersF"/>
- </MkFolder>
- </template>
- </div>
+ <XUserList v-if="query" class="_gap" :pagination="searchPagination" ref="search"/>
+ </div>
+ </div>
+ </MkSpacer>
</div>
</template>
@@ -75,7 +82,7 @@
import { computed, defineComponent } from 'vue';
import XUserList from '@client/components/user-list.vue';
import MkFolder from '@client/components/ui/folder.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import number from '@client/filters/number';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -98,8 +105,28 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.explore,
- icon: 'fas fa-hashtag'
+ icon: 'fas fa-hashtag',
+ bg: 'var(--bg)',
},
+ tab: 'local',
+ header: computed(() => ({
+ title: this.$ts.explore,
+ icon: 'fas fa-hashtag',
+ bg: 'var(--bg)',
+ tabs: [{
+ active: this.tab === 'local',
+ title: this.$ts.local,
+ onClick: () => { this.tab = 'local'; },
+ }, {
+ active: this.tab === 'remote',
+ title: this.$ts.remote,
+ onClick: () => { this.tab = 'remote'; },
+ }, {
+ active: this.tab === 'search',
+ title: this.$ts.search,
+ onClick: () => { this.tab = 'search'; },
+ },]
+ })),
pinnedUsers: { endpoint: 'pinned-users' },
popularUsers: { endpoint: 'users', limit: 10, noPaging: true, params: {
state: 'alive',
@@ -189,11 +216,6 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
-.lznhrdub {
- max-width: 1400px;
- margin: 0 auto;
-}
-
.localfedi7 {
color: #fff;
padding: 16px;
diff --git a/src/client/pages/favorites.vue b/src/client/pages/favorites.vue
index f13723c2d1..bed78d1dbe 100644
--- a/src/client/pages/favorites.vue
+++ b/src/client/pages/favorites.vue
@@ -1,7 +1,10 @@
<template>
-<div class="jmelgwjh">
- <div class="body">
- <XNotes class="notes" :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
+<div>
+ <MkHeader :info="header"/>
+ <div class="jmelgwjh">
+ <div class="body">
+ <XNotes class="notes" :pagination="pagination" :detail="true" :prop="'note'" @before="before()" @after="after()"/>
+ </div>
</div>
</div>
</template>
@@ -25,6 +28,11 @@ export default defineComponent({
icon: 'fas fa-star',
bg: 'var(--bg)',
},
+ header: {
+ title: this.$ts.favorites,
+ icon: 'fas fa-star',
+ bg: 'var(--bg)',
+ },
pagination: {
endpoint: 'i/favorites',
limit: 10,
diff --git a/src/client/pages/featured.vue b/src/client/pages/featured.vue
index 21818ba617..5d8da54541 100644
--- a/src/client/pages/featured.vue
+++ b/src/client/pages/featured.vue
@@ -1,6 +1,9 @@
<template>
-<div class="_section">
- <XNotes class="_content" ref="notes" :pagination="pagination" @before="before" @after="after"/>
+<div>
+ <MkHeader :info="header"/>
+ <div class="_section">
+ <XNotes class="_content" ref="notes" :pagination="pagination" @before="before" @after="after"/>
+ </div>
</div>
</template>
@@ -19,12 +22,18 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.featured,
- icon: 'fas fa-fire-alt'
+ icon: 'fas fa-fire-alt',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.featured,
+ icon: 'fas fa-fire-alt',
+ bg: 'var(--bg)',
},
pagination: {
endpoint: 'notes/featured',
limit: 10,
- offsetMode: true
+ offsetMode: true,
},
};
},
diff --git a/src/client/pages/federation.vue b/src/client/pages/federation.vue
index 2afe70eea6..ae0aed4cc7 100644
--- a/src/client/pages/federation.vue
+++ b/src/client/pages/federation.vue
@@ -1,103 +1,106 @@
<template>
-<div class="taeiyria">
- <div class="query">
- <MkInput v-model="host" :debounce="true" class="_inputNoTopMargin">
- <template #prefix><i class="fas fa-search"></i></template>
- <template #label>{{ $ts.host }}</template>
- </MkInput>
- <div class="_inputSplit _inputNoBottomMargin">
- <MkSelect v-model="state">
- <template #label>{{ $ts.state }}</template>
- <option value="all">{{ $ts.all }}</option>
- <option value="federating">{{ $ts.federating }}</option>
- <option value="subscribing">{{ $ts.subscribing }}</option>
- <option value="publishing">{{ $ts.publishing }}</option>
- <option value="suspended">{{ $ts.suspended }}</option>
- <option value="blocked">{{ $ts.blocked }}</option>
- <option value="notResponding">{{ $ts.notResponding }}</option>
- </MkSelect>
- <MkSelect v-model="sort">
- <template #label>{{ $ts.sort }}</template>
- <option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
- <option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
- <option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
- <option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
- <option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
- <option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
- <option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
- <option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
- <option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
- <option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
- <option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
- <option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
- <option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
- <option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
- <option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option>
- <option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option>
- <option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option>
- <option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option>
- </MkSelect>
+<div>
+ <MkHeader :info="header"/>
+ <div class="taeiyria">
+ <div class="query">
+ <MkInput v-model="host" :debounce="true" class="">
+ <template #prefix><i class="fas fa-search"></i></template>
+ <template #label>{{ $ts.host }}</template>
+ </MkInput>
+ <div class="_inputSplit">
+ <MkSelect v-model="state">
+ <template #label>{{ $ts.state }}</template>
+ <option value="all">{{ $ts.all }}</option>
+ <option value="federating">{{ $ts.federating }}</option>
+ <option value="subscribing">{{ $ts.subscribing }}</option>
+ <option value="publishing">{{ $ts.publishing }}</option>
+ <option value="suspended">{{ $ts.suspended }}</option>
+ <option value="blocked">{{ $ts.blocked }}</option>
+ <option value="notResponding">{{ $ts.notResponding }}</option>
+ </MkSelect>
+ <MkSelect v-model="sort">
+ <template #label>{{ $ts.sort }}</template>
+ <option value="+pubSub">{{ $ts.pubSub }} ({{ $ts.descendingOrder }})</option>
+ <option value="-pubSub">{{ $ts.pubSub }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+notes">{{ $ts.notes }} ({{ $ts.descendingOrder }})</option>
+ <option value="-notes">{{ $ts.notes }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+users">{{ $ts.users }} ({{ $ts.descendingOrder }})</option>
+ <option value="-users">{{ $ts.users }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+following">{{ $ts.following }} ({{ $ts.descendingOrder }})</option>
+ <option value="-following">{{ $ts.following }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+followers">{{ $ts.followers }} ({{ $ts.descendingOrder }})</option>
+ <option value="-followers">{{ $ts.followers }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+caughtAt">{{ $ts.registeredAt }} ({{ $ts.descendingOrder }})</option>
+ <option value="-caughtAt">{{ $ts.registeredAt }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.descendingOrder }})</option>
+ <option value="-lastCommunicatedAt">{{ $ts.lastCommunication }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+driveUsage">{{ $ts.driveUsage }} ({{ $ts.descendingOrder }})</option>
+ <option value="-driveUsage">{{ $ts.driveUsage }} ({{ $ts.ascendingOrder }})</option>
+ <option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option>
+ <option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option>
+ </MkSelect>
+ </div>
</div>
- </div>
- <MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
- <div class="dqokceoi">
- <MkA class="instance" v-for="instance in items" :key="instance.id" :to="`/instance-info/${instance.host}`">
- <div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
- <div class="table">
- <div class="cell">
- <div class="key">{{ $ts.registeredAt }}</div>
- <div class="value"><MkTime :time="instance.caughtAt"/></div>
- </div>
- <div class="cell">
- <div class="key">{{ $ts.software }}</div>
- <div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div>
- </div>
- <div class="cell">
- <div class="key">{{ $ts.version }}</div>
- <div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div>
- </div>
- <div class="cell">
- <div class="key">{{ $ts.users }}</div>
- <div class="value">{{ instance.usersCount }}</div>
- </div>
- <div class="cell">
- <div class="key">{{ $ts.notes }}</div>
- <div class="value">{{ instance.notesCount }}</div>
+ <MkPagination :pagination="pagination" #default="{items}" ref="instances" :key="host + state">
+ <div class="dqokceoi">
+ <MkA class="instance" v-for="instance in items" :key="instance.id" :to="`/instance-info/${instance.host}`">
+ <div class="host"><img :src="instance.faviconUrl">{{ instance.host }}</div>
+ <div class="table">
+ <div class="cell">
+ <div class="key">{{ $ts.registeredAt }}</div>
+ <div class="value"><MkTime :time="instance.caughtAt"/></div>
+ </div>
+ <div class="cell">
+ <div class="key">{{ $ts.software }}</div>
+ <div class="value">{{ instance.softwareName || `(${$ts.unknown})` }}</div>
+ </div>
+ <div class="cell">
+ <div class="key">{{ $ts.version }}</div>
+ <div class="value">{{ instance.softwareVersion || `(${$ts.unknown})` }}</div>
+ </div>
+ <div class="cell">
+ <div class="key">{{ $ts.users }}</div>
+ <div class="value">{{ instance.usersCount }}</div>
+ </div>
+ <div class="cell">
+ <div class="key">{{ $ts.notes }}</div>
+ <div class="value">{{ instance.notesCount }}</div>
+ </div>
+ <div class="cell">
+ <div class="key">{{ $ts.sent }}</div>
+ <div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
+ </div>
+ <div class="cell">
+ <div class="key">{{ $ts.received }}</div>
+ <div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
+ </div>
</div>
- <div class="cell">
- <div class="key">{{ $ts.sent }}</div>
- <div class="value"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
+ <div class="footer">
+ <span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span>
+ <span class="pubSub">
+ <span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span>
+ <span class="sub" v-else><i class="fas fa-caret-down icon"></i>-</span>
+ <span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span>
+ <span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span>
+ </span>
+ <span class="right">
+ <span class="latestStatus">{{ instance.latestStatus || '-' }}</span>
+ <span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span>
+ </span>
</div>
- <div class="cell">
- <div class="key">{{ $ts.received }}</div>
- <div class="value"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
- </div>
- </div>
- <div class="footer">
- <span class="status" :class="getStatus(instance)">{{ getStatus(instance) }}</span>
- <span class="pubSub">
- <span class="sub" v-if="instance.followersCount > 0"><i class="fas fa-caret-down icon"></i>Sub</span>
- <span class="sub" v-else><i class="fas fa-caret-down icon"></i>-</span>
- <span class="pub" v-if="instance.followingCount > 0"><i class="fas fa-caret-up icon"></i>Pub</span>
- <span class="pub" v-else><i class="fas fa-caret-up icon"></i>-</span>
- </span>
- <span class="right">
- <span class="latestStatus">{{ instance.latestStatus || '-' }}</span>
- <span class="lastCommunicatedAt"><MkTime :time="instance.lastCommunicatedAt"/></span>
- </span>
- </div>
- </MkA>
- </div>
- </MkPagination>
+ </MkA>
+ </div>
+ </MkPagination>
+ </div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
import MkPagination from '@client/components/ui/pagination.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -116,7 +119,13 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.federation,
- icon: 'fas fa-globe'
+ icon: 'fas fa-globe',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.federation,
+ icon: 'fas fa-globe',
+ bg: 'var(--bg)',
},
host: '',
state: 'federating',
diff --git a/src/client/pages/gallery/edit.vue b/src/client/pages/gallery/edit.vue
index cd6a0defdd..8e74b068ef 100644
--- a/src/client/pages/gallery/edit.vue
+++ b/src/client/pages/gallery/edit.vue
@@ -1,23 +1,23 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormInput v-model:value="title">
+ <FormInput v-model="title">
<span>{{ $ts.title }}</span>
</FormInput>
- <FormTextarea v-model:value="description" :max="500">
+ <FormTextarea v-model="description" :max="500">
<span>{{ $ts.description }}</span>
</FormTextarea>
<FormGroup>
- <div v-for="file in files" :key="file.id" class="_formItem _formPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
+ <div v-for="file in files" :key="file.id" class="_debobigegoItem _debobigegoPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
<div class="name">{{ file.name }}</div>
<button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button>
</div>
<FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
</FormGroup>
- <FormSwitch v-model:value="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
+ <FormSwitch v-model="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
<FormButton v-if="postId" @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
<FormButton v-else @click="save" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
@@ -29,14 +29,14 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
-import FormButton from '@client/components/form/button.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormTuple from '@client/components/form/tuple.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormTuple from '@client/components/debobigego/tuple.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import { selectFile } from '@client/scripts/select-file';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/gallery/index.vue b/src/client/pages/gallery/index.vue
index 9e726e70f2..ffc599513e 100644
--- a/src/client/pages/gallery/index.vue
+++ b/src/client/pages/gallery/index.vue
@@ -1,6 +1,6 @@
<template>
<div class="xprsixdl _root">
- <MkTab v-model:value="tab" v-if="$i">
+ <MkTab v-model="tab" v-if="$i">
<option value="explore"><i class="fas fa-icons"></i> {{ $ts.gallery }}</option>
<option value="liked"><i class="fas fa-heart"></i> {{ $ts._gallery.liked }}</option>
<option value="my"><i class="fas fa-edit"></i> {{ $ts._gallery.my }}</option>
@@ -46,7 +46,7 @@
import { computed, defineComponent } from 'vue';
import XUserList from '@client/components/user-list.vue';
import MkFolder from '@client/components/ui/folder.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import MkButton from '@client/components/ui/button.vue';
import MkTab from '@client/components/tab.vue';
import MkPagination from '@client/components/ui/pagination.vue';
diff --git a/src/client/pages/instance-info.vue b/src/client/pages/instance-info.vue
index 7d03c0847d..4fbf104f0c 100644
--- a/src/client/pages/instance-info.vue
+++ b/src/client/pages/instance-info.vue
@@ -3,8 +3,8 @@
<FormGroup v-if="instance">
<template #label>{{ instance.host }}</template>
<FormGroup>
- <div class="_formItem">
- <div class="_formPanel fnfelxur">
+ <div class="_debobigegoItem">
+ <div class="_debobigegoPanel fnfelxur">
<img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
</div>
</div>
@@ -60,9 +60,9 @@
<template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template>
</FormKeyValueView>
</FormGroup>
- <div class="_formItem">
- <div class="_formLabel">{{ $ts.statistics }}</div>
- <div class="_formPanel cmhjzshl">
+ <div class="_debobigegoItem">
+ <div class="_debobigegoLabel">{{ $ts.statistics }}</div>
+ <div class="_debobigegoPanel cmhjzshl">
<div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
<option value="requests">{{ $ts._instanceCharts.requests }}</option>
@@ -136,15 +136,15 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import Chart from 'chart.js';
-import FormObjectView from '@client/components/form/object-view.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import FormObjectView from '@client/components/debobigego/object-view.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import MkSelect from '@client/components/form/select.vue';
import * as os from '@client/os';
import number from '@client/filters/number';
import bytes from '@client/filters/bytes';
diff --git a/src/client/pages/instance/abuses.vue b/src/client/pages/instance/abuses.vue
index ac20ebabe5..29da8cc2c5 100644
--- a/src/client/pages/instance/abuses.vue
+++ b/src/client/pages/instance/abuses.vue
@@ -24,10 +24,10 @@
</div>
<!-- TODO
<div class="inputs" style="display: flex; padding-top: 1.2em;">
- <MkInput v-model:value="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.reports.reload()">
+ <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()">
<span>{{ $ts.username }}</span>
</MkInput>
- <MkInput v-model:value="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:value="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
+ <MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.reports.reload()" :disabled="pagination.params().origin === 'local'">
<span>{{ $ts.host }}</span>
</MkInput>
</div>
@@ -65,8 +65,8 @@
import { defineComponent } from 'vue';
import { parseAcct } from '@/misc/acct';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
import MkPagination from '@client/components/ui/pagination.vue';
import { acct } from '@client/filters/user';
import * as os from '@client/os';
@@ -86,7 +86,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.abuseReports,
- icon: 'fas fa-exclamation-circle'
+ icon: 'fas fa-exclamation-circle',
+ bg: 'var(--bg)',
},
searchUsername: '',
searchHost: '',
diff --git a/src/client/pages/instance/ads.vue b/src/client/pages/instance/ads.vue
index 50c8c29cbf..e776f99a4c 100644
--- a/src/client/pages/instance/ads.vue
+++ b/src/client/pages/instance/ads.vue
@@ -1,52 +1,54 @@
<template>
-<div class="uqshojas">
- <MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
- <section class="_card _gap ads" v-for="ad in ads">
- <div class="_content ad">
- <MkAd v-if="ad.url" :specify="ad"/>
- <MkInput v-model="ad.url" type="url">
- <template #label>URL</template>
- </MkInput>
- <MkInput v-model="ad.imageUrl">
- <template #label>{{ $ts.imageUrl }}</template>
- </MkInput>
- <div style="margin: 32px 0;">
- <MkRadio v-model="ad.place" value="square">square</MkRadio>
- <MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
- <MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
+<div>
+ <MkHeader :info="header"/>
+ <div class="uqshojas">
+ <section class="_card _gap ads" v-for="ad in ads">
+ <div class="_content ad">
+ <MkAd v-if="ad.url" :specify="ad"/>
+ <MkInput v-model="ad.url" type="url">
+ <template #label>URL</template>
+ </MkInput>
+ <MkInput v-model="ad.imageUrl">
+ <template #label>{{ $ts.imageUrl }}</template>
+ </MkInput>
+ <div style="margin: 32px 0;">
+ <MkRadio v-model="ad.place" value="square">square</MkRadio>
+ <MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
+ <MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
+ </div>
+ <!--
+ <div style="margin: 32px 0;">
+ {{ $ts.priority }}
+ <MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
+ <MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
+ <MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
+ </div>
+ -->
+ <MkInput v-model="ad.ratio" type="number">
+ <template #label>{{ $ts.ratio }}</template>
+ </MkInput>
+ <MkInput v-model="ad.expiresAt" type="date">
+ <template #label>{{ $ts.expiration }}</template>
+ </MkInput>
+ <MkTextarea v-model="ad.memo">
+ <template #label>{{ $ts.memo }}</template>
+ </MkTextarea>
+ <div class="buttons">
+ <MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+ <MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+ </div>
</div>
- <!--
- <div style="margin: 32px 0;">
- {{ $ts.priority }}
- <MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
- <MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
- <MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
- </div>
- -->
- <MkInput v-model="ad.ratio" type="number">
- <template #label>{{ $ts.ratio }}</template>
- </MkInput>
- <MkInput v-model="ad.expiresAt" type="date">
- <template #label>{{ $ts.expiration }}</template>
- </MkInput>
- <MkTextarea v-model="ad.memo">
- <template #label>{{ $ts.memo }}</template>
- </MkTextarea>
- <div class="buttons">
- <MkButton class="button" inline @click="save(ad)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- <MkButton class="button" inline @click="remove(ad)" danger><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
- </div>
- </div>
- </section>
+ </section>
+ </div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkRadio from '@client/components/ui/radio.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkRadio from '@client/components/form/radio.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -64,7 +66,19 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.ads,
- icon: 'fas fa-audio-description'
+ icon: 'fas fa-audio-description',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.ads,
+ icon: 'fas fa-audio-description',
+ bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: this.$ts.add,
+ handler: this.add,
+ }],
},
ads: [],
}
diff --git a/src/client/pages/instance/announcements.vue b/src/client/pages/instance/announcements.vue
index d48e3737ad..78637c095a 100644
--- a/src/client/pages/instance/announcements.vue
+++ b/src/client/pages/instance/announcements.vue
@@ -1,32 +1,35 @@
<template>
-<div class="ztgjmzrw">
- <MkButton @click="add()" primary style="margin: 0 auto 16px auto;"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
- <section class="_card _gap announcements" v-for="announcement in announcements">
- <div class="_content announcement">
- <MkInput v-model="announcement.title">
- <template #label>{{ $ts.title }}</template>
- </MkInput>
- <MkTextarea v-model="announcement.text">
- <template #label>{{ $ts.text }}</template>
- </MkTextarea>
- <MkInput v-model="announcement.imageUrl">
- <template #label>{{ $ts.imageUrl }}</template>
- </MkInput>
- <p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
- <div class="buttons">
- <MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+<div>
+ <MkHeader :info="header"/>
+
+ <div class="ztgjmzrw">
+ <section class="_card _gap announcements" v-for="announcement in announcements">
+ <div class="_content announcement">
+ <MkInput v-model="announcement.title">
+ <template #label>{{ $ts.title }}</template>
+ </MkInput>
+ <MkTextarea v-model="announcement.text">
+ <template #label>{{ $ts.text }}</template>
+ </MkTextarea>
+ <MkInput v-model="announcement.imageUrl">
+ <template #label>{{ $ts.imageUrl }}</template>
+ </MkInput>
+ <p v-if="announcement.reads">{{ $t('nUsersRead', { n: announcement.reads }) }}</p>
+ <div class="buttons">
+ <MkButton class="button" inline @click="save(announcement)" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+ <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
+ </div>
</div>
- </div>
- </section>
+ </section>
+ </div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -43,7 +46,19 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.announcements,
- icon: 'fas fa-broadcast-tower'
+ icon: 'fas fa-broadcast-tower',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.announcements,
+ icon: 'fas fa-broadcast-tower',
+ bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: this.$ts.add,
+ handler: this.add,
+ }],
},
announcements: [],
}
diff --git a/src/client/pages/instance/bot-protection.vue b/src/client/pages/instance/bot-protection.vue
index 449b8a233d..731f114cc2 100644
--- a/src/client/pages/instance/bot-protection.vue
+++ b/src/client/pages/instance/bot-protection.vue
@@ -9,43 +9,43 @@
</FormRadios>
<template v-if="provider === 'hcaptcha'">
- <div class="_formItem _formNoConcat" v-sticky-container>
- <div class="_formLabel">hCaptcha</div>
+ <div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+ <div class="_debobigegoLabel">hCaptcha</div>
<div class="main">
- <FormInput v-model:value="hcaptchaSiteKey">
+ <FormInput v-model="hcaptchaSiteKey">
<template #prefix><i class="fas fa-key"></i></template>
<span>{{ $ts.hcaptchaSiteKey }}</span>
</FormInput>
- <FormInput v-model:value="hcaptchaSecretKey">
+ <FormInput v-model="hcaptchaSecretKey">
<template #prefix><i class="fas fa-key"></i></template>
<span>{{ $ts.hcaptchaSecretKey }}</span>
</FormInput>
</div>
</div>
- <div class="_formItem _formNoConcat" v-sticky-container>
- <div class="_formLabel">{{ $ts.preview }}</div>
- <div class="_formPanel" style="padding: var(--formContentHMargin);">
+ <div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+ <div class="_debobigegoLabel">{{ $ts.preview }}</div>
+ <div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
</div>
</div>
</template>
<template v-else-if="provider === 'recaptcha'">
- <div class="_formItem _formNoConcat" v-sticky-container>
- <div class="_formLabel">reCAPTCHA</div>
+ <div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+ <div class="_debobigegoLabel">reCAPTCHA</div>
<div class="main">
- <FormInput v-model:value="recaptchaSiteKey">
+ <FormInput v-model="recaptchaSiteKey">
<template #prefix><i class="fas fa-key"></i></template>
<span>{{ $ts.recaptchaSiteKey }}</span>
</FormInput>
- <FormInput v-model:value="recaptchaSecretKey">
+ <FormInput v-model="recaptchaSecretKey">
<template #prefix><i class="fas fa-key"></i></template>
<span>{{ $ts.recaptchaSecretKey }}</span>
</FormInput>
</div>
</div>
- <div v-if="recaptchaSiteKey" class="_formItem _formNoConcat" v-sticky-container>
- <div class="_formLabel">{{ $ts.preview }}</div>
- <div class="_formPanel" style="padding: var(--formContentHMargin);">
+ <div v-if="recaptchaSiteKey" class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+ <div class="_debobigegoLabel">{{ $ts.preview }}</div>
+ <div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
</div>
</div>
@@ -58,13 +58,13 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import FormRadios from '@client/components/form/radios.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormRadios from '@client/components/debobigego/radios.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
diff --git a/src/client/pages/instance/database.vue b/src/client/pages/instance/database.vue
index a41d61ce2b..ffbeed8b30 100644
--- a/src/client/pages/instance/database.vue
+++ b/src/client/pages/instance/database.vue
@@ -18,11 +18,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSuspense from '@client/components/form/suspense.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import bytes from '@client/filters/bytes';
@@ -43,7 +43,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.database,
- icon: 'fas fa-database'
+ icon: 'fas fa-database',
+ bg: 'var(--bg)',
},
databasePromiseFactory: () => os.api('admin/get-table-stats', {}).then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)),
}
diff --git a/src/client/pages/instance/email-settings.vue b/src/client/pages/instance/email-settings.vue
index 9965a1420f..ebf724fcdd 100644
--- a/src/client/pages/instance/email-settings.vue
+++ b/src/client/pages/instance/email-settings.vue
@@ -1,30 +1,30 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormSwitch v-model:value="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
+ <FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
<template v-if="enableEmail">
- <FormInput v-model:value="email" type="email">
+ <FormInput v-model="email" type="email">
<span>{{ $ts.emailAddress }}</span>
</FormInput>
- <div class="_formItem _formNoConcat" v-sticky-container>
- <div class="_formLabel">{{ $ts.smtpConfig }}</div>
+ <div class="_debobigegoItem _debobigegoNoConcat" v-sticky-container>
+ <div class="_debobigegoLabel">{{ $ts.smtpConfig }}</div>
<div class="main">
- <FormInput v-model:value="smtpHost">
+ <FormInput v-model="smtpHost">
<span>{{ $ts.smtpHost }}</span>
</FormInput>
- <FormInput v-model:value="smtpPort" type="number">
+ <FormInput v-model="smtpPort" type="number">
<span>{{ $ts.smtpPort }}</span>
</FormInput>
- <FormInput v-model:value="smtpUser">
+ <FormInput v-model="smtpUser">
<span>{{ $ts.smtpUser }}</span>
</FormInput>
- <FormInput v-model:value="smtpPass" type="password">
+ <FormInput v-model="smtpPass" type="password">
<span>{{ $ts.smtpPass }}</span>
</FormInput>
<FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
- <FormSwitch v-model:value="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
+ <FormSwitch v-model="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
</div>
</div>
@@ -38,13 +38,13 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -66,7 +66,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.emailServer,
- icon: 'fas fa-envelope'
+ icon: 'fas fa-envelope',
+ bg: 'var(--bg)',
},
enableEmail: false,
email: null,
diff --git a/src/client/pages/instance/emoji-edit-dialog.vue b/src/client/pages/instance/emoji-edit-dialog.vue
index 7e9bdc80dd..4854c69884 100644
--- a/src/client/pages/instance/emoji-edit-dialog.vue
+++ b/src/client/pages/instance/emoji-edit-dialog.vue
@@ -11,13 +11,13 @@
<div class="_monolithic_">
<div class="yigymqpb _section">
<img :src="emoji.url" class="img"/>
- <MkInput v-model="name">
+ <MkInput class="_formBlock" v-model="name">
<template #label>{{ $ts.name }}</template>
</MkInput>
- <MkInput v-model="category" :datalist="categories">
+ <MkInput class="_formBlock" v-model="category" :datalist="categories">
<template #label>{{ $ts.category }}</template>
</MkInput>
- <MkInput v-model="aliases">
+ <MkInput class="_formBlock" v-model="aliases">
<template #label>{{ $ts.tags }}</template>
<template #caption>{{ $ts.setMultipleBySeparatingWithSpace }}</template>
</MkInput>
@@ -31,7 +31,7 @@
import { defineComponent } from 'vue';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
import { unique } from '../../../prelude/array';
diff --git a/src/client/pages/instance/emojis.vue b/src/client/pages/instance/emojis.vue
index 7badc9da02..4cd34b046d 100644
--- a/src/client/pages/instance/emojis.vue
+++ b/src/client/pages/instance/emojis.vue
@@ -1,12 +1,8 @@
<template>
<div class="ogwlenmc">
- <MkTab v-model:value="tab">
- <option value="local">{{ $ts.local }}</option>
- <option value="remote">{{ $ts.remote }}</option>
- </MkTab>
+ <MkHeader :info="header"/>
<div class="local" v-if="tab === 'local'">
- <MkButton primary @click="add" style="margin: var(--margin) auto;"><i class="fas fa-plus"></i> {{ $ts.addEmoji }}</MkButton>
<MkInput v-model="query" :debounce="true" type="search" style="margin: var(--margin);">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
@@ -56,7 +52,7 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import MkPagination from '@client/components/ui/pagination.vue';
import MkTab from '@client/components/tab.vue';
import { selectFile } from '@client/scripts/select-file';
@@ -78,11 +74,28 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.customEmojis,
icon: 'fas fa-laugh',
- action: {
- icon: 'fas fa-plus',
- handler: this.add
- }
+ bg: 'var(--bg)',
},
+ header: computed(() => ({
+ title: this.$ts.customEmojis,
+ icon: 'fas fa-laugh',
+ bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: this.$ts.addEmoji,
+ handler: this.add,
+ }],
+ tabs: [{
+ active: this.tab === 'local',
+ title: this.$ts.local,
+ onClick: () => { this.tab = 'local'; },
+ }, {
+ active: this.tab === 'remote',
+ title: this.$ts.remote,
+ onClick: () => { this.tab = 'remote'; },
+ },]
+ })),
tab: 'local',
query: null,
queryRemote: null,
diff --git a/src/client/pages/instance/file-dialog.vue b/src/client/pages/instance/file-dialog.vue
index 8a03a11de7..02d83e5022 100644
--- a/src/client/pages/instance/file-dialog.vue
+++ b/src/client/pages/instance/file-dialog.vue
@@ -37,7 +37,7 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
+import MkSwitch from '@client/components/form/switch.vue';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
import Progress from '@client/scripts/loading';
diff --git a/src/client/pages/instance/files-settings.vue b/src/client/pages/instance/files-settings.vue
index 614c7d4dbb..8aefa9e90d 100644
--- a/src/client/pages/instance/files-settings.vue
+++ b/src/client/pages/instance/files-settings.vue
@@ -1,23 +1,23 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormSwitch v-model:value="cacheRemoteFiles">
+ <FormSwitch v-model="cacheRemoteFiles">
{{ $ts.cacheRemoteFiles }}
<template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
</FormSwitch>
- <FormSwitch v-model:value="proxyRemoteFiles">
+ <FormSwitch v-model="proxyRemoteFiles">
{{ $ts.proxyRemoteFiles }}
<template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
</FormSwitch>
- <FormInput v-model:value="localDriveCapacityMb" type="number">
+ <FormInput v-model="localDriveCapacityMb" type="number">
<span>{{ $ts.driveCapacityPerLocalAccount }}</span>
<template #suffix>MB</template>
<template #desc>{{ $ts.inMb }}</template>
</FormInput>
- <FormInput v-model:value="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
+ <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
<span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
<template #suffix>MB</template>
<template #desc>{{ $ts.inMb }}</template>
@@ -30,12 +30,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -56,7 +56,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.files,
- icon: 'fas fa-cloud'
+ icon: 'fas fa-cloud',
+ bg: 'var(--bg)',
},
cacheRemoteFiles: false,
proxyRemoteFiles: false,
diff --git a/src/client/pages/instance/files.vue b/src/client/pages/instance/files.vue
index b7f472b7c8..55189cfd84 100644
--- a/src/client/pages/instance/files.vue
+++ b/src/client/pages/instance/files.vue
@@ -1,20 +1,14 @@
<template>
<div class="xrmjdkdw">
- <div class="_section">
- <div class="_content">
- <MkButton primary @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearCachedFiles }}</MkButton>
- </div>
- </div>
-
- <div class="_section lookup">
- <div class="_title"><i class="fas fa-search"></i> {{ $ts.lookup }}</div>
- <div class="_content">
- <MkInput class="target" v-model="q" type="text" @enter="find()">
+ <MkContainer :foldable="true" class="lookup">
+ <template #header><i class="fas fa-search"></i> {{ $ts.lookup }}</template>
+ <div class="xrmjdkdw-lookup">
+ <MkInput class="item" v-model="q" type="text" @enter="find()">
<template #label>{{ $ts.fileIdOrUrl }}</template>
</MkInput>
<MkButton @click="find()" primary><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
</div>
- </div>
+ </MkContainer>
<div class="_section">
<div class="_content">
@@ -31,7 +25,7 @@
</div>
<div class="inputs" style="display: flex; padding-top: 1.2em;">
<MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;">
- <template #label>{{ $ts.type }}</template>
+ <template #label>MIME type</template>
</MkInput>
</div>
<MkPagination :pagination="pagination" #default="{items}" class="urempief" ref="files">
@@ -63,9 +57,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
import MkPagination from '@client/components/ui/pagination.vue';
+import MkContainer from '@client/components/ui/container.vue';
import MkDriveFileThumbnail from '@client/components/drive-file-thumbnail.vue';
import bytes from '@client/filters/bytes';
import * as os from '@client/os';
@@ -77,6 +72,7 @@ export default defineComponent({
MkInput,
MkSelect,
MkPagination,
+ MkContainer,
MkDriveFileThumbnail,
},
@@ -86,7 +82,13 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.files,
- icon: 'fas fa-cloud'
+ icon: 'fas fa-cloud',
+ bg: 'var(--bg)',
+ actions: [{
+ text: this.$ts.clearCachedFiles,
+ icon: 'fas fa-trash-alt',
+ handler: this.clear
+ }]
},
q: null,
origin: 'local',
@@ -161,6 +163,10 @@ export default defineComponent({
.xrmjdkdw {
margin: var(--margin);
+ > .lookup {
+ margin-bottom: 16px;
+ }
+
.urempief {
margin-top: var(--margin);
@@ -192,4 +198,12 @@ export default defineComponent({
}
}
}
+
+.xrmjdkdw-lookup {
+ padding: 16px;
+
+ > .item {
+ margin-bottom: 16px;
+ }
+}
</style>
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index 612bfa762a..7b07bf2dde 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -1,51 +1,16 @@
<template>
<div class="hiyeyicy" :class="{ wide: !narrow }" ref="el">
<div class="nav" v-if="!narrow || page == null">
- <FormBase>
- <FormGroup>
- <div class="_formItem">
- <div class="_formPanel lxpfedzu">
- <img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
- </div>
- </div>
- <FormLink :active="page === 'overview'" replace to="/instance/overview"><template #icon><i class="fas fa-tachometer-alt"></i></template>{{ $ts.overview }}</FormLink>
- </FormGroup>
- <FormGroup>
- <template #label>{{ $ts.quickAction }}</template>
- <FormButton @click="lookup"><i class="fas fa-search"></i> {{ $ts.lookup }}</FormButton>
- <FormButton v-if="$instance.disableRegistration" @click="invite"><i class="fas fa-user"></i> {{ $ts.invite }}</FormButton>
- </FormGroup>
- <FormGroup>
- <template #label>{{ $ts.administration }}</template>
- <FormLink :active="page === 'users'" replace to="/instance/users"><template #icon><i class="fas fa-users"></i></template>{{ $ts.users }}</FormLink>
- <FormLink :active="page === 'emojis'" replace to="/instance/emojis"><template #icon><i class="fas fa-laugh"></i></template>{{ $ts.customEmojis }}</FormLink>
- <FormLink :active="page === 'federation'" replace to="/instance/federation"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.federation }}</FormLink>
- <FormLink :active="page === 'queue'" replace to="/instance/queue"><template #icon><i class="fas fa-clipboard-list"></i></template>{{ $ts.jobQueue }}</FormLink>
- <FormLink :active="page === 'files'" replace to="/instance/files"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
- <FormLink :active="page === 'announcements'" replace to="/instance/announcements"><template #icon><i class="fas fa-broadcast-tower"></i></template>{{ $ts.announcements }}</FormLink>
- <FormLink :active="page === 'ads'" replace to="/instance/ads"><template #icon><i class="fas fa-audio-description"></i></template>{{ $ts.ads }}</FormLink>
- <FormLink :active="page === 'abuses'" replace to="/instance/abuses"><template #icon><i class="fas fa-exclamation-circle"></i></template>{{ $ts.abuseReports }}</FormLink>
- </FormGroup>
- <FormGroup>
- <template #label>{{ $ts.settings }}</template>
- <FormLink :active="page === 'settings'" replace to="/instance/settings"><template #icon><i class="fas fa-cog"></i></template>{{ $ts.general }}</FormLink>
- <FormLink :active="page === 'files-settings'" replace to="/instance/files-settings"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.files }}</FormLink>
- <FormLink :active="page === 'email-settings'" replace to="/instance/email-settings"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.emailServer }}</FormLink>
- <FormLink :active="page === 'object-storage'" replace to="/instance/object-storage"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.objectStorage }}</FormLink>
- <FormLink :active="page === 'security'" replace to="/instance/security"><template #icon><i class="fas fa-lock"></i></template>{{ $ts.security }}</FormLink>
- <FormLink :active="page === 'service-worker'" replace to="/instance/service-worker"><template #icon><i class="fas fa-bolt"></i></template>ServiceWorker</FormLink>
- <FormLink :active="page === 'relays'" replace to="/instance/relays"><template #icon><i class="fas fa-globe"></i></template>{{ $ts.relays }}</FormLink>
- <FormLink :active="page === 'integrations'" replace to="/instance/integrations"><template #icon><i class="fas fa-share-alt"></i></template>{{ $ts.integration }}</FormLink>
- <FormLink :active="page === 'instance-block'" replace to="/instance/instance-block"><template #icon><i class="fas fa-ban"></i></template>{{ $ts.instanceBlocking }}</FormLink>
- <FormLink :active="page === 'proxy-account'" replace to="/instance/proxy-account"><template #icon><i class="fas fa-ghost"></i></template>{{ $ts.proxyAccount }}</FormLink>
- <FormLink :active="page === 'other-settings'" replace to="/instance/other-settings"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.other }}</FormLink>
- </FormGroup>
- <FormGroup>
- <template #label>{{ $ts.info }}</template>
- <FormLink :active="page === 'database'" replace to="/instance/database"><template #icon><i class="fas fa-database"></i></template>{{ $ts.database }}</FormLink>
- <FormLink :active="page === 'logs'" replace to="/instance/logs"><template #icon><i class="fas fa-stream"></i></template>{{ $ts.logs }}</FormLink>
- </FormGroup>
- </FormBase>
+ <MkHeader :info="header"></MkHeader>
+
+ <div class="lxpfedzu">
+ <img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
+ </div>
+
+ <MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+
+ <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
</div>
<div class="main">
<component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
@@ -56,11 +21,13 @@
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { i18n } from '@client/i18n';
-import FormLink from '@client/components/form/link.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormButton from '@client/components/form/button.vue';
+import MkSuperMenu from '@client/components/ui/super-menu.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import MkInfo from '@client/components/ui/info.vue';
import { scroll } from '@client/scripts/scroll';
+import { instance } from '@client/instance';
import * as symbols from '@client/symbols';
import * as os from '@client/os';
import { lookupUser } from '@client/scripts/lookup-user';
@@ -68,9 +35,10 @@ import { lookupUser } from '@client/scripts/lookup-user';
export default defineComponent({
components: {
FormBase,
- FormLink,
+ MkSuperMenu,
FormGroup,
FormButton,
+ MkInfo,
},
props: {
@@ -83,7 +51,8 @@ export default defineComponent({
setup(props, context) {
const indexInfo = {
title: i18n.locale.instance,
- icon: 'fas fa-cog'
+ icon: 'fas fa-cog',
+ bg: 'var(--bg)',
};
const INFO = ref(indexInfo);
const page = ref(props.initialPage);
@@ -94,6 +63,151 @@ export default defineComponent({
INFO.value = viewInfo;
};
const pageProps = ref({});
+
+ const isEmpty = (x: any) => x == null || x == '';
+
+ const noMaintainerInformation = ref(false);
+ const noBotProtection = ref(false);
+
+ os.api('meta', { detail: true }).then(meta => {
+ // TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する
+ noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
+ noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
+ });
+
+ const menuDef = computed(() => [{
+ title: i18n.locale.quickAction,
+ items: [{
+ type: 'button',
+ icon: 'fas fa-search',
+ text: i18n.locale.lookup,
+ action: lookup,
+ }, ...(instance.disableRegistration ? [{
+ type: 'button',
+ icon: 'fas fa-user',
+ text: i18n.locale.invite,
+ action: invite,
+ }] : [])],
+ }, {
+ title: i18n.locale.administration,
+ items: [{
+ icon: 'fas fa-tachometer-alt',
+ text: i18n.locale.dashboard,
+ to: '/instance/overview',
+ active: page.value === 'overview',
+ }, {
+ icon: 'fas fa-users',
+ text: i18n.locale.users,
+ to: '/instance/users',
+ active: page.value === 'users',
+ }, {
+ icon: 'fas fa-laugh',
+ text: i18n.locale.customEmojis,
+ to: '/instance/emojis',
+ active: page.value === 'emojis',
+ }, {
+ icon: 'fas fa-globe',
+ text: i18n.locale.federation,
+ to: '/instance/federation',
+ active: page.value === 'federation',
+ }, {
+ icon: 'fas fa-clipboard-list',
+ text: i18n.locale.jobQueue,
+ to: '/instance/queue',
+ active: page.value === 'queue',
+ }, {
+ icon: 'fas fa-cloud',
+ text: i18n.locale.files,
+ to: '/instance/files',
+ active: page.value === 'files',
+ }, {
+ icon: 'fas fa-broadcast-tower',
+ text: i18n.locale.announcements,
+ to: '/instance/announcements',
+ active: page.value === 'announcements',
+ }, {
+ icon: 'fas fa-audio-description',
+ text: i18n.locale.ads,
+ to: '/instance/ads',
+ active: page.value === 'ads',
+ }, {
+ icon: 'fas fa-exclamation-circle',
+ text: i18n.locale.abuseReports,
+ to: '/instance/abuses',
+ active: page.value === 'abuses',
+ }],
+ }, {
+ title: i18n.locale.settings,
+ items: [{
+ icon: 'fas fa-cog',
+ text: i18n.locale.general,
+ to: '/instance/settings',
+ active: page.value === 'settings',
+ }, {
+ icon: 'fas fa-cloud',
+ text: i18n.locale.files,
+ to: '/instance/files-settings',
+ active: page.value === 'files-settings',
+ }, {
+ icon: 'fas fa-envelope',
+ text: i18n.locale.emailServer,
+ to: '/instance/email-settings',
+ active: page.value === 'email-settings',
+ }, {
+ icon: 'fas fa-cloud',
+ text: i18n.locale.objectStorage,
+ to: '/instance/object-storage',
+ active: page.value === 'object-storage',
+ }, {
+ icon: 'fas fa-lock',
+ text: i18n.locale.security,
+ to: '/instance/security',
+ active: page.value === 'security',
+ }, {
+ icon: 'fas fa-bolt',
+ text: 'ServiceWorker',
+ to: '/instance/service-worker',
+ active: page.value === 'service-worker',
+ }, {
+ icon: 'fas fa-globe',
+ text: i18n.locale.relays,
+ to: '/instance/relays',
+ active: page.value === 'relays',
+ }, {
+ icon: 'fas fa-share-alt',
+ text: i18n.locale.integration,
+ to: '/instance/integrations',
+ active: page.value === 'integrations',
+ }, {
+ icon: 'fas fa-ban',
+ text: i18n.locale.instanceBlocking,
+ to: '/instance/instance-block',
+ active: page.value === 'instance-block',
+ }, {
+ icon: 'fas fa-ghost',
+ text: i18n.locale.proxyAccount,
+ to: '/instance/proxy-account',
+ active: page.value === 'proxy-account',
+ }, {
+ icon: 'fas fa-cogs',
+ text: i18n.locale.other,
+ to: '/instance/other-settings',
+ active: page.value === 'other-settings',
+ }],
+ }, {
+ title: i18n.locale.info,
+ items: [{
+ icon: 'fas fa-database',
+ text: i18n.locale.database,
+ to: '/instance/database',
+ active: page.value === 'database',
+ }, {
+ icon: 'fas fa-stream',
+ text: i18n.locale.logs,
+ to: '/instance/logs',
+ active: page.value === 'logs',
+ }],
+ }]);
const component = computed(() => {
if (page.value == null) return null;
switch (page.value) {
@@ -130,7 +244,7 @@ export default defineComponent({
pageProps.value = {};
nextTick(() => {
- scroll(el.value, 0);
+ scroll(el.value, { top: 0 });
});
}, { immediate: true });
@@ -196,6 +310,12 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: INFO,
+ menuDef,
+ header: {
+ title: i18n.locale.controllPanel,
+ },
+ noMaintainerInformation,
+ noBotProtection,
page,
narrow,
view,
@@ -214,28 +334,34 @@ export default defineComponent({
.hiyeyicy {
&.wide {
display: flex;
- max-width: 1100px;
margin: 0 auto;
height: 100%;
> .nav {
width: 32%;
+ max-width: 280px;
box-sizing: border-box;
border-right: solid 0.5px var(--divider);
overflow: auto;
+ height: 100%;
}
> .main {
flex: 1;
min-width: 0;
- overflow: auto;
--baseContentWidth: 100%;
}
}
+
+ > .nav {
+ > .info {
+ margin: 16px;
+ }
+ }
}
.lxpfedzu {
- padding: 16px;
+ margin: 16px;
> .icon {
display: block;
diff --git a/src/client/pages/instance/instance-block.vue b/src/client/pages/instance/instance-block.vue
index ed5740f339..105cdb4941 100644
--- a/src/client/pages/instance/instance-block.vue
+++ b/src/client/pages/instance/instance-block.vue
@@ -1,7 +1,7 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormTextarea v-model:value="blockedHosts">
+ <FormTextarea v-model="blockedHosts">
<span>{{ $ts.blockedInstances }}</span>
<template #desc>{{ $ts.blockedInstancesDescription }}</template>
</FormTextarea>
@@ -13,14 +13,14 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -43,7 +43,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.instanceBlocking,
- icon: 'fas fa-ban'
+ icon: 'fas fa-ban',
+ bg: 'var(--bg)',
},
blockedHosts: '',
}
diff --git a/src/client/pages/instance/instance.vue b/src/client/pages/instance/instance.vue
index c39f0d1ecb..6117f090de 100644
--- a/src/client/pages/instance/instance.vue
+++ b/src/client/pages/instance/instance.vue
@@ -127,9 +127,9 @@ import { defineComponent, markRaw } from 'vue';
import Chart from 'chart.js';
import XModalWindow from '@client/components/ui/modal-window.vue';
import MkUsersDialog from '@client/components/users-dialog.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkSelect from '@client/components/form/select.vue';
import MkButton from '@client/components/ui/button.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
+import MkSwitch from '@client/components/form/switch.vue';
import MkInfo from '@client/components/ui/info.vue';
import bytes from '@client/filters/bytes';
import number from '@client/filters/number';
diff --git a/src/client/pages/instance/integrations-discord.vue b/src/client/pages/instance/integrations-discord.vue
index c7508918f8..c33b24f17f 100644
--- a/src/client/pages/instance/integrations-discord.vue
+++ b/src/client/pages/instance/integrations-discord.vue
@@ -1,19 +1,19 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormSwitch v-model:value="enableDiscordIntegration">
+ <FormSwitch v-model="enableDiscordIntegration">
{{ $ts.enable }}
</FormSwitch>
<template v-if="enableDiscordIntegration">
<FormInfo>Callback URL: {{ `${url}/api/dc/cb` }}</FormInfo>
- <FormInput v-model:value="discordClientId">
+ <FormInput v-model="discordClientId">
<template #prefix><i class="fas fa-key"></i></template>
Client ID
</FormInput>
- <FormInput v-model:value="discordClientSecret">
+ <FormInput v-model="discordClientSecret">
<template #prefix><i class="fas fa-key"></i></template>
Client Secret
</FormInput>
@@ -26,12 +26,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
diff --git a/src/client/pages/instance/integrations-github.vue b/src/client/pages/instance/integrations-github.vue
index 16586b15b4..cdf85868ff 100644
--- a/src/client/pages/instance/integrations-github.vue
+++ b/src/client/pages/instance/integrations-github.vue
@@ -1,19 +1,19 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormSwitch v-model:value="enableGithubIntegration">
+ <FormSwitch v-model="enableGithubIntegration">
{{ $ts.enable }}
</FormSwitch>
<template v-if="enableGithubIntegration">
<FormInfo>Callback URL: {{ `${url}/api/gh/cb` }}</FormInfo>
- <FormInput v-model:value="githubClientId">
+ <FormInput v-model="githubClientId">
<template #prefix><i class="fas fa-key"></i></template>
Client ID
</FormInput>
- <FormInput v-model:value="githubClientSecret">
+ <FormInput v-model="githubClientSecret">
<template #prefix><i class="fas fa-key"></i></template>
Client Secret
</FormInput>
@@ -26,12 +26,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
diff --git a/src/client/pages/instance/integrations-twitter.vue b/src/client/pages/instance/integrations-twitter.vue
index b08b7f40a5..ed7d097d0a 100644
--- a/src/client/pages/instance/integrations-twitter.vue
+++ b/src/client/pages/instance/integrations-twitter.vue
@@ -1,19 +1,19 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormSwitch v-model:value="enableTwitterIntegration">
+ <FormSwitch v-model="enableTwitterIntegration">
{{ $ts.enable }}
</FormSwitch>
<template v-if="enableTwitterIntegration">
<FormInfo>Callback URL: {{ `${url}/api/tw/cb` }}</FormInfo>
- <FormInput v-model:value="twitterConsumerKey">
+ <FormInput v-model="twitterConsumerKey">
<template #prefix><i class="fas fa-key"></i></template>
Consumer Key
</FormInput>
- <FormInput v-model:value="twitterConsumerSecret">
+ <FormInput v-model="twitterConsumerSecret">
<template #prefix><i class="fas fa-key"></i></template>
Consumer Secret
</FormInput>
@@ -26,12 +26,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
diff --git a/src/client/pages/instance/integrations.vue b/src/client/pages/instance/integrations.vue
index 7debedc367..6964ae5704 100644
--- a/src/client/pages/instance/integrations.vue
+++ b/src/client/pages/instance/integrations.vue
@@ -19,14 +19,14 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormLink from '@client/components/form/link.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -49,7 +49,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.integration,
- icon: 'fas fa-share-alt'
+ icon: 'fas fa-share-alt',
+ bg: 'var(--bg)',
},
enableTwitterIntegration: false,
enableGithubIntegration: false,
diff --git a/src/client/pages/instance/logs.vue b/src/client/pages/instance/logs.vue
index 4eee816f96..74aea0fc45 100644
--- a/src/client/pages/instance/logs.vue
+++ b/src/client/pages/instance/logs.vue
@@ -31,9 +31,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSelect from '@client/components/ui/select.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/instance/metrics.vue b/src/client/pages/instance/metrics.vue
index 283b5939f0..1606063aee 100644
--- a/src/client/pages/instance/metrics.vue
+++ b/src/client/pages/instance/metrics.vue
@@ -1,7 +1,7 @@
<template>
-<div class="_formItem">
- <div class="_formLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
- <div class="_formPanel xhexznfu">
+<div class="_debobigegoItem">
+ <div class="_debobigegoLabel"><i class="fas fa-microchip"></i> {{ $ts.cpuAndMemory }}</div>
+ <div class="_debobigegoPanel xhexznfu">
<div>
<canvas :ref="cpumem"></canvas>
</div>
@@ -16,9 +16,9 @@
</div>
</div>
</div>
-<div class="_formItem">
- <div class="_formLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
- <div class="_formPanel xhexznfu">
+<div class="_debobigegoItem">
+ <div class="_debobigegoLabel"><i class="fas fa-hdd"></i> {{ $ts.disk }}</div>
+ <div class="_debobigegoPanel xhexznfu">
<div>
<canvas :ref="disk"></canvas>
</div>
@@ -33,9 +33,9 @@
</div>
</div>
</div>
-<div class="_formItem">
- <div class="_formLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
- <div class="_formPanel xhexznfu">
+<div class="_debobigegoItem">
+ <div class="_debobigegoLabel"><i class="fas fa-exchange-alt"></i> {{ $ts.network }}</div>
+ <div class="_debobigegoPanel xhexznfu">
<div>
<canvas :ref="net"></canvas>
</div>
@@ -54,8 +54,8 @@
import { defineComponent, markRaw } from 'vue';
import Chart from 'chart.js';
import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/ui/select.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkInput from '@client/components/form/input.vue';
import MkContainer from '@client/components/ui/container.vue';
import MkFolder from '@client/components/ui/folder.vue';
import MkwFederation from '../../widgets/federation.vue';
diff --git a/src/client/pages/instance/object-storage.vue b/src/client/pages/instance/object-storage.vue
index 814aeb6e48..2d765270e6 100644
--- a/src/client/pages/instance/object-storage.vue
+++ b/src/client/pages/instance/object-storage.vue
@@ -1,59 +1,59 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormSwitch v-model:value="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
+ <FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
<template v-if="useObjectStorage">
- <FormInput v-model:value="objectStorageBaseUrl">
+ <FormInput v-model="objectStorageBaseUrl">
<span>{{ $ts.objectStorageBaseUrl }}</span>
<template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template>
</FormInput>
- <FormInput v-model:value="objectStorageBucket">
+ <FormInput v-model="objectStorageBucket">
<span>{{ $ts.objectStorageBucket }}</span>
<template #desc>{{ $ts.objectStorageBucketDesc }}</template>
</FormInput>
- <FormInput v-model:value="objectStoragePrefix">
+ <FormInput v-model="objectStoragePrefix">
<span>{{ $ts.objectStoragePrefix }}</span>
<template #desc>{{ $ts.objectStoragePrefixDesc }}</template>
</FormInput>
- <FormInput v-model:value="objectStorageEndpoint">
+ <FormInput v-model="objectStorageEndpoint">
<span>{{ $ts.objectStorageEndpoint }}</span>
<template #desc>{{ $ts.objectStorageEndpointDesc }}</template>
</FormInput>
- <FormInput v-model:value="objectStorageRegion">
+ <FormInput v-model="objectStorageRegion">
<span>{{ $ts.objectStorageRegion }}</span>
<template #desc>{{ $ts.objectStorageRegionDesc }}</template>
</FormInput>
- <FormInput v-model:value="objectStorageAccessKey">
+ <FormInput v-model="objectStorageAccessKey">
<template #prefix><i class="fas fa-key"></i></template>
<span>Access key</span>
</FormInput>
- <FormInput v-model:value="objectStorageSecretKey">
+ <FormInput v-model="objectStorageSecretKey">
<template #prefix><i class="fas fa-key"></i></template>
<span>Secret key</span>
</FormInput>
- <FormSwitch v-model:value="objectStorageUseSSL">
+ <FormSwitch v-model="objectStorageUseSSL">
{{ $ts.objectStorageUseSSL }}
<template #desc>{{ $ts.objectStorageUseSSLDesc }}</template>
</FormSwitch>
- <FormSwitch v-model:value="objectStorageUseProxy">
+ <FormSwitch v-model="objectStorageUseProxy">
{{ $ts.objectStorageUseProxy }}
<template #desc>{{ $ts.objectStorageUseProxyDesc }}</template>
</FormSwitch>
- <FormSwitch v-model:value="objectStorageSetPublicRead">
+ <FormSwitch v-model="objectStorageSetPublicRead">
{{ $ts.objectStorageSetPublicRead }}
</FormSwitch>
- <FormSwitch v-model:value="objectStorageS3ForcePathStyle">
+ <FormSwitch v-model="objectStorageS3ForcePathStyle">
s3ForcePathStyle
</FormSwitch>
</template>
@@ -65,12 +65,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -91,7 +91,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.objectStorage,
- icon: 'fas fa-cloud'
+ icon: 'fas fa-cloud',
+ bg: 'var(--bg)',
},
useObjectStorage: false,
objectStorageBaseUrl: null,
diff --git a/src/client/pages/instance/other-settings.vue b/src/client/pages/instance/other-settings.vue
index 4fa80b2b2c..4e55df41fb 100644
--- a/src/client/pages/instance/other-settings.vue
+++ b/src/client/pages/instance/other-settings.vue
@@ -2,17 +2,17 @@
<FormBase>
<FormSuspense :p="init">
<FormGroup>
- <FormInput v-model:value="summalyProxy">
+ <FormInput v-model="summalyProxy">
<template #prefix><i class="fas fa-link"></i></template>
Summaly Proxy URL
</FormInput>
</FormGroup>
<FormGroup>
- <FormInput v-model:value="deeplAuthKey">
+ <FormInput v-model="deeplAuthKey">
<template #prefix><i class="fas fa-key"></i></template>
DeepL Auth Key
</FormInput>
- <FormSwitch v-model:value="deeplIsPro">
+ <FormSwitch v-model="deeplIsPro">
Pro account
</FormSwitch>
</FormGroup>
@@ -23,12 +23,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -49,7 +49,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.other,
- icon: 'fas fa-cogs'
+ icon: 'fas fa-cogs',
+ bg: 'var(--bg)',
},
summalyProxy: '',
deeplAuthKey: '',
diff --git a/src/client/pages/instance/overview.vue b/src/client/pages/instance/overview.vue
index 0d7a5d1501..c6db9d0c04 100644
--- a/src/client/pages/instance/overview.vue
+++ b/src/client/pages/instance/overview.vue
@@ -1,9 +1,6 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormInfo v-if="noMaintainerInformation" warn>{{ $ts.noMaintainerInformationWarning }} <MkA to="/instance/settings" class="_link">{{ $ts.configure }}</MkA></FormInfo>
- <FormInfo v-if="noBotProtection" warn>{{ $ts.noBotProtectionWarning }} <MkA to="/instance/bot-protection" class="_link">{{ $ts.configure }}</MkA></FormInfo>
-
<FormSuspense :p="fetchStats" v-slot="{ result: stats }">
<FormGroup>
<FormKeyValueView>
@@ -17,8 +14,8 @@
</FormGroup>
</FormSuspense>
- <div class="_formItem">
- <div class="_formPanel">
+ <div class="_debobigegoItem">
+ <div class="_debobigegoPanel">
<MkInstanceStats :chart-limit="300" :detailed="true"/>
</div>
</div>
@@ -47,18 +44,18 @@
<script lang="ts">
import { computed, defineComponent, markRaw } from 'vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import MkInstanceStats from '@client/components/instance-stats.vue';
import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/ui/select.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkInput from '@client/components/form/input.vue';
import MkContainer from '@client/components/ui/container.vue';
import MkFolder from '@client/components/ui/folder.vue';
import { version, url } from '@client/config';
@@ -86,7 +83,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.overview,
- icon: 'fas fa-tachometer-alt'
+ icon: 'fas fa-tachometer-alt',
+ bg: 'var(--bg)',
},
page: 'index',
version,
@@ -97,8 +95,6 @@ export default defineComponent({
fetchServerInfo: () => os.api('admin/server-info', {}),
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
- noMaintainerInformation: false,
- noBotProtection: false,
}
},
@@ -109,11 +105,6 @@ export default defineComponent({
methods: {
async init() {
this.meta = await os.api('meta', { detail: true });
-
- const isEmpty = (x: any) => x == null || x == '';
-
- this.noMaintainerInformation = isEmpty(this.meta.maintainerName) || isEmpty(this.meta.maintainerEmail);
- this.noBotProtection = !this.meta.enableHcaptcha && !this.meta.enableRecaptcha;
},
async showInstanceInfo(q) {
diff --git a/src/client/pages/instance/proxy-account.vue b/src/client/pages/instance/proxy-account.vue
index 3e2df8dcb4..b1ece19710 100644
--- a/src/client/pages/instance/proxy-account.vue
+++ b/src/client/pages/instance/proxy-account.vue
@@ -16,14 +16,14 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -46,7 +46,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.proxyAccount,
- icon: 'fas fa-ghost'
+ icon: 'fas fa-ghost',
+ bg: 'var(--bg)',
},
proxyAccount: null,
proxyAccountId: null,
diff --git a/src/client/pages/instance/queue.chart.vue b/src/client/pages/instance/queue.chart.vue
index 53d790598a..887fe9a574 100644
--- a/src/client/pages/instance/queue.chart.vue
+++ b/src/client/pages/instance/queue.chart.vue
@@ -1,7 +1,7 @@
<template>
-<div class="_formItem">
- <div class="_formLabel"><slot name="title"></slot></div>
- <div class="_formPanel pumxzjhg">
+<div class="_debobigegoItem">
+ <div class="_debobigegoLabel"><slot name="title"></slot></div>
+ <div class="_debobigegoPanel pumxzjhg">
<div class="_table status">
<div class="_row">
<div class="_cell"><div class="_label">Process</div>{{ number(activeSincePrevTick) }}</div>
diff --git a/src/client/pages/instance/queue.vue b/src/client/pages/instance/queue.vue
index e8ec0bc97d..f88825eb19 100644
--- a/src/client/pages/instance/queue.vue
+++ b/src/client/pages/instance/queue.vue
@@ -14,8 +14,8 @@
import { defineComponent, markRaw } from 'vue';
import MkButton from '@client/components/ui/button.vue';
import XQueue from './queue.chart.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -34,6 +34,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.jobQueue,
icon: 'fas fa-clipboard-list',
+ bg: 'var(--bg)',
},
connection: markRaw(os.stream.useChannel('queueStats')),
}
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue
index a3e4e7d1da..7d7888eaa8 100644
--- a/src/client/pages/instance/relays.vue
+++ b/src/client/pages/instance/relays.vue
@@ -2,8 +2,8 @@
<FormBase class="relaycxt">
<FormButton @click="addRelay" primary><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton>
- <div class="_formItem" v-for="relay in relays" :key="relay.inbox">
- <div class="_formPanel" style="padding: 16px;">
+ <div class="_debobigegoItem" v-for="relay in relays" :key="relay.inbox">
+ <div class="_debobigegoPanel" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
@@ -15,9 +15,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormButton from '@client/components/form/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -36,6 +36,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.relays,
icon: 'fas fa-globe',
+ bg: 'var(--bg)',
},
relays: [],
inbox: '',
diff --git a/src/client/pages/instance/security.vue b/src/client/pages/instance/security.vue
index e3397a113b..a854b6dbd0 100644
--- a/src/client/pages/instance/security.vue
+++ b/src/client/pages/instance/security.vue
@@ -8,7 +8,9 @@
<template #suffix v-else>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
</FormLink>
- <FormSwitch v-model:value="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
+ <FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
+
+ <FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch>
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</FormSuspense>
@@ -17,13 +19,13 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import FormLink from '@client/components/form/link.vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -45,11 +47,13 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.security,
- icon: 'fas fa-lock'
+ icon: 'fas fa-lock',
+ bg: 'var(--bg)',
},
enableHcaptcha: false,
enableRecaptcha: false,
enableRegistration: false,
+ emailRequiredForSignup: false,
}
},
@@ -63,11 +67,13 @@ export default defineComponent({
this.enableHcaptcha = meta.enableHcaptcha;
this.enableRecaptcha = meta.enableRecaptcha;
this.enableRegistration = !meta.disableRegistration;
+ this.emailRequiredForSignup = meta.emailRequiredForSignup;
},
save() {
os.apiWithDialog('admin/update-meta', {
disableRegistration: !this.enableRegistration,
+ emailRequiredForSignup: this.emailRequiredForSignup,
}).then(() => {
fetchInstance();
});
diff --git a/src/client/pages/instance/service-worker.vue b/src/client/pages/instance/service-worker.vue
index a52932bb75..430e02ad2e 100644
--- a/src/client/pages/instance/service-worker.vue
+++ b/src/client/pages/instance/service-worker.vue
@@ -1,18 +1,18 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormSwitch v-model:value="enableServiceWorker">
+ <FormSwitch v-model="enableServiceWorker">
{{ $ts.enableServiceworker }}
<template #desc>{{ $ts.serviceworkerInfo }}</template>
</FormSwitch>
<template v-if="enableServiceWorker">
- <FormInput v-model:value="swPublicKey">
+ <FormInput v-model="swPublicKey">
<template #prefix><i class="fas fa-key"></i></template>
Public key
</FormInput>
- <FormInput v-model:value="swPrivateKey">
+ <FormInput v-model="swPrivateKey">
<template #prefix><i class="fas fa-key"></i></template>
Private key
</FormInput>
@@ -25,12 +25,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -51,7 +51,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: 'ServiceWorker',
- icon: 'fas fa-bolt'
+ icon: 'fas fa-bolt',
+ bg: 'var(--bg)',
},
enableServiceWorker: false,
swPublicKey: null,
diff --git a/src/client/pages/instance/settings.vue b/src/client/pages/instance/settings.vue
index b68d784897..7bd363e5f3 100644
--- a/src/client/pages/instance/settings.vue
+++ b/src/client/pages/instance/settings.vue
@@ -1,50 +1,55 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <FormInput v-model:value="name">
+ <FormInput v-model="name">
<span>{{ $ts.instanceName }}</span>
</FormInput>
- <FormTextarea v-model:value="description">
+ <FormTextarea v-model="description">
<span>{{ $ts.instanceDescription }}</span>
</FormTextarea>
- <FormInput v-model:value="iconUrl">
+ <FormInput v-model="iconUrl">
<template #prefix><i class="fas fa-link"></i></template>
<span>{{ $ts.iconUrl }}</span>
</FormInput>
- <FormInput v-model:value="bannerUrl">
+ <FormInput v-model="bannerUrl">
<template #prefix><i class="fas fa-link"></i></template>
<span>{{ $ts.bannerUrl }}</span>
</FormInput>
- <FormInput v-model:value="backgroundImageUrl">
+ <FormInput v-model="backgroundImageUrl">
<template #prefix><i class="fas fa-link"></i></template>
<span>{{ $ts.backgroundImageUrl }}</span>
</FormInput>
- <FormInput v-model:value="tosUrl">
+ <FormInput v-model="tosUrl">
<template #prefix><i class="fas fa-link"></i></template>
<span>{{ $ts.tosUrl }}</span>
</FormInput>
- <FormInput v-model:value="maintainerName">
+ <FormInput v-model="maintainerName">
<span>{{ $ts.maintainerName }}</span>
</FormInput>
- <FormInput v-model:value="maintainerEmail" type="email">
+ <FormInput v-model="maintainerEmail" type="email">
<template #prefix><i class="fas fa-envelope"></i></template>
<span>{{ $ts.maintainerEmail }}</span>
</FormInput>
- <FormInput v-model:value="maxNoteTextLength" type="number">
+ <FormTextarea v-model="pinnedUsers">
+ <span>{{ $ts.pinnedUsers }}</span>
+ <template #desc>{{ $ts.pinnedUsersDescription }}</template>
+ </FormTextarea>
+
+ <FormInput v-model="maxNoteTextLength" type="number">
<template #prefix><i class="fas fa-pencil-alt"></i></template>
<span>{{ $ts.maxNoteTextLength }}</span>
</FormInput>
- <FormSwitch v-model:value="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
- <FormSwitch v-model:value="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
+ <FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
+ <FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo>
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
@@ -54,14 +59,14 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { fetchInstance } from '@client/instance';
@@ -84,7 +89,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.general,
- icon: 'fas fa-cog'
+ icon: 'fas fa-cog',
+ bg: 'var(--bg)',
},
name: null,
description: null,
@@ -97,6 +103,7 @@ export default defineComponent({
maxNoteTextLength: 0,
enableLocalTimeline: false,
enableGlobalTimeline: false,
+ pinnedUsers: '',
}
},
@@ -118,6 +125,7 @@ export default defineComponent({
this.maxNoteTextLength = meta.maxNoteTextLength;
this.enableLocalTimeline = !meta.disableLocalTimeline;
this.enableGlobalTimeline = !meta.disableGlobalTimeline;
+ this.pinnedUsers = meta.pinnedUsers.join('\n');
},
save() {
@@ -133,6 +141,7 @@ export default defineComponent({
maxNoteTextLength: this.maxNoteTextLength,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
+ pinnedUsers: this.pinnedUsers.split('\n'),
}).then(() => {
fetchInstance();
});
diff --git a/src/client/pages/instance/users.vue b/src/client/pages/instance/users.vue
index 8db62683ba..f7f9306b70 100644
--- a/src/client/pages/instance/users.vue
+++ b/src/client/pages/instance/users.vue
@@ -1,20 +1,17 @@
<template>
<div class="lknzcolw">
- <div class="actions">
- <MkButton inline primary @click="addUser()"><i class="fas fa-plus"></i> {{ $ts.addUser }}</MkButton>
- <MkButton inline primary @click="lookupUser()"><i class="fas fa-search"></i> {{ $ts.lookup }}</MkButton>
- </div>
+ <MkHeader :info="header"/>
<div class="users">
- <div class="inputs" style="display: flex;">
- <MkSelect v-model="sort" style="margin: 0; flex: 1;">
+ <div class="inputs">
+ <MkSelect v-model="sort" style="flex: 1;">
<template #label>{{ $ts.sort }}</template>
<option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option>
<option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option>
<option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option>
<option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option>
</MkSelect>
- <MkSelect v-model="state" style="margin: 0; flex: 1;">
+ <MkSelect v-model="state" style="flex: 1;">
<template #label>{{ $ts.state }}</template>
<option value="all">{{ $ts.all }}</option>
<option value="available">{{ $ts.normal }}</option>
@@ -23,18 +20,20 @@
<option value="silenced">{{ $ts.silence }}</option>
<option value="suspended">{{ $ts.suspend }}</option>
</MkSelect>
- <MkSelect v-model="origin" style="margin: 0; flex: 1;">
+ <MkSelect v-model="origin" style="flex: 1;">
<template #label>{{ $ts.instance }}</template>
<option value="combined">{{ $ts.all }}</option>
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
</div>
- <div class="inputs" style="display: flex; padding-top: 1.2em;">
- <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
+ <div class="inputs">
+ <MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()">
+ <template #prefix>@</template>
<template #label>{{ $ts.username }}</template>
</MkInput>
- <MkInput v-model="searchHost" style="margin: 0; flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
+ <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()" :disabled="pagination.params().origin === 'local'">
+ <template #prefix>@</template>
<template #label>{{ $ts.host }}</template>
</MkInput>
</div>
@@ -67,8 +66,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSelect from '@client/components/form/select.vue';
import MkPagination from '@client/components/ui/pagination.vue';
import { acct } from '@client/filters/user';
import * as os from '@client/os';
@@ -90,10 +89,27 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.users,
icon: 'fas fa-users',
- action: {
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.users,
+ icon: 'fas fa-users',
+ bg: 'var(--bg)',
+ actions: [{
icon: 'fas fa-search',
+ text: this.$ts.search,
handler: this.searchUser
- }
+ }, {
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: this.$ts.addUser,
+ handler: this.addUser
+ }, {
+ asFullButton: true,
+ icon: 'fas fa-search',
+ text: this.$ts.lookup,
+ handler: this.lookupUser
+ }]
},
sort: '+createdAt',
state: 'all',
@@ -172,12 +188,21 @@ export default defineComponent({
<style lang="scss" scoped>
.lknzcolw {
- > .actions {
- margin: var(--margin);
- }
-
> .users {
margin: var(--margin);
+
+ > .inputs {
+ display: flex;
+ margin-bottom: 16px;
+
+ > * {
+ margin-right: 16px;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
> .users {
margin-top: var(--margin);
diff --git a/src/client/pages/mentions.vue b/src/client/pages/mentions.vue
index 798d3e342d..e1d2f096e1 100644
--- a/src/client/pages/mentions.vue
+++ b/src/client/pages/mentions.vue
@@ -1,6 +1,9 @@
<template>
-<div class="_section">
- <XNotes class="_content" :pagination="pagination" @before="before()" @after="after()"/>
+<div>
+ <MkHeader :info="header"/>
+ <div class="_section">
+ <XNotes class="_content" :pagination="pagination" @before="before()" @after="after()"/>
+ </div>
</div>
</template>
@@ -19,7 +22,13 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.mentions,
- icon: 'fas fa-at'
+ icon: 'fas fa-at',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.mentions,
+ icon: 'fas fa-at',
+ bg: 'var(--bg)',
},
pagination: {
endpoint: 'notes/mentions',
diff --git a/src/client/pages/messages.vue b/src/client/pages/messages.vue
index 6ac9746d4e..f4c68daab9 100644
--- a/src/client/pages/messages.vue
+++ b/src/client/pages/messages.vue
@@ -1,6 +1,9 @@
<template>
<div>
- <XNotes :pagination="pagination" @before="before()" @after="after()"/>
+ <MkHeader :info="header"/>
+ <div>
+ <XNotes :pagination="pagination" @before="before()" @after="after()"/>
+ </div>
</div>
</template>
@@ -19,7 +22,13 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.directNotes,
- icon: 'fas fa-envelope'
+ icon: 'fas fa-envelope',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.directNotes,
+ icon: 'fas fa-envelope',
+ bg: 'var(--bg)',
},
pagination: {
endpoint: 'notes/mentions',
diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue
index 1e0d4dc64c..fef3b76e10 100644
--- a/src/client/pages/messaging/index.vue
+++ b/src/client/pages/messaging/index.vue
@@ -1,38 +1,42 @@
<template>
-<div class="yweeujhr _root" v-size="{ max: [400] }">
- <MkButton @click="start" primary class="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton>
+<div>
+ <MkHeader :info="header"/>
- <div class="history" v-if="messages.length > 0">
- <MkA v-for="(message, i) in messages"
- class="message _block"
- :class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
- :to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
- :data-index="i"
- :key="message.id"
- v-anim="i"
- >
- <div>
- <MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/>
- <header v-if="message.groupId">
- <span class="name">{{ message.group.name }}</span>
- <MkTime :time="message.createdAt" class="time"/>
- </header>
- <header v-else>
- <span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
- <span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
- <MkTime :time="message.createdAt" class="time"/>
- </header>
- <div class="body">
- <p class="text"><span class="me" v-if="isMe(message)">{{ $ts.you }}:</span>{{ message.text }}</p>
+ <div class="yweeujhr _root" v-size="{ max: [400] }">
+ <MkButton @click="start" primary class="start"><i class="fas fa-plus"></i> {{ $ts.startMessaging }}</MkButton>
+
+ <div class="history" v-if="messages.length > 0">
+ <MkA v-for="(message, i) in messages"
+ class="message _block"
+ :class="{ isMe: isMe(message), isRead: message.groupId ? message.reads.includes($i.id) : message.isRead }"
+ :to="message.groupId ? `/my/messaging/group/${message.groupId}` : `/my/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
+ :data-index="i"
+ :key="message.id"
+ v-anim="i"
+ >
+ <div>
+ <MkAvatar class="avatar" :user="message.groupId ? message.user : isMe(message) ? message.recipient : message.user" :show-indicator="true"/>
+ <header v-if="message.groupId">
+ <span class="name">{{ message.group.name }}</span>
+ <MkTime :time="message.createdAt" class="time"/>
+ </header>
+ <header v-else>
+ <span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
+ <span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
+ <MkTime :time="message.createdAt" class="time"/>
+ </header>
+ <div class="body">
+ <p class="text"><span class="me" v-if="isMe(message)">{{ $ts.you }}:</span>{{ message.text }}</p>
+ </div>
</div>
- </div>
- </MkA>
- </div>
- <div class="_fullinfo" v-if="!fetching && messages.length == 0">
- <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
- <div>{{ $ts.noHistory }}</div>
+ </MkA>
+ </div>
+ <div class="_fullinfo" v-if="!fetching && messages.length == 0">
+ <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+ <div>{{ $ts.noHistory }}</div>
+ </div>
+ <MkLoading v-if="fetching"/>
</div>
- <MkLoading v-if="fetching"/>
</div>
</template>
@@ -53,7 +57,13 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.messaging,
- icon: 'fas fa-comments'
+ icon: 'fas fa-comments',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.messaging,
+ icon: 'fas fa-comments',
+ bg: 'var(--bg)',
},
fetching: true,
moreFetching: false,
diff --git a/src/client/pages/messaging/messaging-room.message.vue b/src/client/pages/messaging/messaging-room.message.vue
index dfac83ad6a..a2740c0bdc 100644
--- a/src/client/pages/messaging/messaging-room.message.vue
+++ b/src/client/pages/messaging/messaging-room.message.vue
@@ -302,7 +302,7 @@ export default defineComponent({
> .text {
&, ::v-deep(*) {
- color: #fff !important;
+ color: var(--fgOnAccent) !important;
}
}
}
diff --git a/src/client/pages/messaging/messaging-room.vue b/src/client/pages/messaging/messaging-room.vue
index b6a2fbd3d4..76e58d5bc9 100644
--- a/src/client/pages/messaging/messaging-room.vue
+++ b/src/client/pages/messaging/messaging-room.vue
@@ -284,7 +284,7 @@ const Component = defineComponent({
},
scrollToBottom() {
- scroll(this.$el, this.$el.offsetHeight);
+ scroll(this.$el, { top: this.$el.offsetHeight });
},
onIndicatorClick() {
diff --git a/src/client/pages/mfm-cheat-sheet.vue b/src/client/pages/mfm-cheat-sheet.vue
index 314b5e2a5f..5ff4317627 100644
--- a/src/client/pages/mfm-cheat-sheet.vue
+++ b/src/client/pages/mfm-cheat-sheet.vue
@@ -286,7 +286,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
import * as symbols from '@client/symbols';
export default defineComponent({
diff --git a/src/client/pages/my-antennas/editor.vue b/src/client/pages/my-antennas/editor.vue
index 882d48e643..93ab640030 100644
--- a/src/client/pages/my-antennas/editor.vue
+++ b/src/client/pages/my-antennas/editor.vue
@@ -1,10 +1,10 @@
<template>
<div class="shaynizk">
<div class="form">
- <MkInput v-model="name" class="_inputNoTopMargin">
+ <MkInput v-model="name" class="_formBlock">
<template #label>{{ $ts.name }}</template>
</MkInput>
- <MkSelect v-model="src">
+ <MkSelect v-model="src" class="_formBlock">
<template #label>{{ $ts.antennaSource }}</template>
<option value="all">{{ $ts._antennaSources.all }}</option>
<option value="home">{{ $ts._antennaSources.homeTimeline }}</option>
@@ -12,30 +12,30 @@
<option value="list">{{ $ts._antennaSources.userList }}</option>
<option value="group">{{ $ts._antennaSources.userGroup }}</option>
</MkSelect>
- <MkSelect v-model="userListId" v-if="src === 'list'">
+ <MkSelect v-model="userListId" v-if="src === 'list'" class="_formBlock">
<template #label>{{ $ts.userList }}</template>
<option v-for="list in userLists" :value="list.id" :key="list.id">{{ list.name }}</option>
</MkSelect>
- <MkSelect v-model="userGroupId" v-else-if="src === 'group'">
+ <MkSelect v-model="userGroupId" v-else-if="src === 'group'" class="_formBlock">
<template #label>{{ $ts.userGroup }}</template>
<option v-for="group in userGroups" :value="group.id" :key="group.id">{{ group.name }}</option>
</MkSelect>
- <MkTextarea v-model="users" v-else-if="src === 'users'">
+ <MkTextarea v-model="users" v-else-if="src === 'users'" class="_formBlock">
<template #label>{{ $ts.users }}</template>
<template #caption>{{ $ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ $ts.addUser }}</button></template>
</MkTextarea>
- <MkSwitch v-model="withReplies">{{ $ts.withReplies }}</MkSwitch>
- <MkTextarea v-model="keywords">
+ <MkSwitch v-model="withReplies" class="_formBlock">{{ $ts.withReplies }}</MkSwitch>
+ <MkTextarea v-model="keywords" class="_formBlock">
<template #label>{{ $ts.antennaKeywords }}</template>
<template #caption>{{ $ts.antennaKeywordsDescription }}</template>
</MkTextarea>
- <MkTextarea v-model="excludeKeywords">
+ <MkTextarea v-model="excludeKeywords" class="_formBlock">
<template #label>{{ $ts.antennaExcludeKeywords }}</template>
<template #caption>{{ $ts.antennaKeywordsDescription }}</template>
</MkTextarea>
- <MkSwitch v-model="caseSensitive">{{ $ts.caseSensitive }}</MkSwitch>
- <MkSwitch v-model="withFile">{{ $ts.withFileAntenna }}</MkSwitch>
- <MkSwitch v-model="notify">{{ $ts.notifyAntenna }}</MkSwitch>
+ <MkSwitch v-model="caseSensitive" class="_formBlock">{{ $ts.caseSensitive }}</MkSwitch>
+ <MkSwitch v-model="withFile" class="_formBlock">{{ $ts.withFileAntenna }}</MkSwitch>
+ <MkSwitch v-model="notify" class="_formBlock">{{ $ts.notifyAntenna }}</MkSwitch>
</div>
<div class="actions">
<MkButton inline @click="saveAntenna()" primary><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
@@ -47,10 +47,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkSelect from '@client/components/ui/select.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkSwitch from '@client/components/form/switch.vue';
import { getAcct } from '@/misc/acct';
import * as os from '@client/os';
diff --git a/src/client/pages/my-groups/index.vue b/src/client/pages/my-groups/index.vue
index 9f153ff9cc..34f82f8a71 100644
--- a/src/client/pages/my-groups/index.vue
+++ b/src/client/pages/my-groups/index.vue
@@ -1,7 +1,7 @@
<template>
<div class="">
<div class="_section" style="padding: 0;">
- <MkTab v-model:value="tab">
+ <MkTab v-model="tab">
<option value="owned">{{ $ts.ownedGroups }}</option>
<option value="joined">{{ $ts.joinedGroups }}</option>
<option value="invites"><i class="fas fa-envelope-open-text"></i> {{ $ts.invites }}</option>
diff --git a/src/client/pages/my-lists/index.vue b/src/client/pages/my-lists/index.vue
index 7de31bb308..b0e9bf9d54 100644
--- a/src/client/pages/my-lists/index.vue
+++ b/src/client/pages/my-lists/index.vue
@@ -1,13 +1,16 @@
<template>
-<div class="qkcjvfiv">
- <MkButton @click="create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
+<div>
+ <MkHeader :info="header"/>
+ <div class="qkcjvfiv">
+ <MkButton @click="create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
- <MkPagination :pagination="pagination" #default="{items}" class="lists _content" ref="list">
- <MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
- <div class="name">{{ list.name }}</div>
- <MkAvatars :user-ids="list.userIds"/>
- </MkA>
- </MkPagination>
+ <MkPagination :pagination="pagination" #default="{items}" class="lists _content" ref="list">
+ <MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
+ <div class="name">{{ list.name }}</div>
+ <MkAvatars :user-ids="list.userIds"/>
+ </MkA>
+ </MkPagination>
+ </div>
</div>
</template>
@@ -31,6 +34,12 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.manageLists,
icon: 'fas fa-list-ul',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.manageLists,
+ icon: 'fas fa-list-ul',
+ bg: 'var(--bg)',
action: {
icon: 'fas fa-plus',
handler: this.create
diff --git a/src/client/pages/my-lists/list.vue b/src/client/pages/my-lists/list.vue
index 049d370b4e..27c979bc88 100644
--- a/src/client/pages/my-lists/list.vue
+++ b/src/client/pages/my-lists/list.vue
@@ -1,34 +1,37 @@
<template>
-<div class="mk-list-page">
- <transition name="zoom" mode="out-in">
- <div v-if="list" class="_section">
- <div class="_content">
- <MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
- <MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton>
- <MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton>
+<div>
+ <MkHeader v-if="header" :info="header"/>
+ <div class="mk-list-page">
+ <transition name="zoom" mode="out-in">
+ <div v-if="list" class="_section">
+ <div class="_content">
+ <MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
+ <MkButton inline @click="renameList()">{{ $ts.rename }}</MkButton>
+ <MkButton inline @click="deleteList()">{{ $ts.delete }}</MkButton>
+ </div>
</div>
- </div>
- </transition>
+ </transition>
- <transition name="zoom" mode="out-in">
- <div v-if="list" class="_section members _gap">
- <div class="_title">{{ $ts.members }}</div>
- <div class="_content">
- <div class="users">
- <div class="user _panel" v-for="user in users" :key="user.id">
- <MkAvatar :user="user" class="avatar" :show-indicator="true"/>
- <div class="body">
- <MkUserName :user="user" class="name"/>
- <MkAcct :user="user" class="acct"/>
- </div>
- <div class="action">
- <button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
+ <transition name="zoom" mode="out-in">
+ <div v-if="list" class="_section members _gap">
+ <div class="_title">{{ $ts.members }}</div>
+ <div class="_content">
+ <div class="users">
+ <div class="user _panel" v-for="user in users" :key="user.id">
+ <MkAvatar :user="user" class="avatar" :show-indicator="true"/>
+ <div class="body">
+ <MkUserName :user="user" class="name"/>
+ <MkAcct :user="user" class="acct"/>
+ </div>
+ <div class="action">
+ <button class="_button" @click="removeUser(user)"><i class="fas fa-times"></i></button>
+ </div>
</div>
</div>
</div>
</div>
- </div>
- </transition>
+ </transition>
+ </div>
</div>
</template>
@@ -50,6 +53,10 @@ export default defineComponent({
title: this.list.name,
icon: 'fas fa-list-ul',
} : null),
+ header: computed(() => this.list ? {
+ title: this.list.name,
+ icon: 'fas fa-list-ul',
+ } : null),
list: null,
users: [],
};
diff --git a/src/client/pages/notifications.vue b/src/client/pages/notifications.vue
index 06f8ad3cba..049d057d02 100644
--- a/src/client/pages/notifications.vue
+++ b/src/client/pages/notifications.vue
@@ -1,15 +1,21 @@
<template>
-<div class="clupoqwt" v-size="{ min: [800] }">
- <XNotifications class="notifications" @before="before" @after="after" page/>
+<div>
+ <MkHeader :info="header"/>
+ <MkSpacer :content-max="800">
+ <div class="clupoqwt">
+ <XNotifications class="notifications" @before="before" @after="after" :include-types="includeTypes" :unread-only="tab === 'unread'"/>
+ </div>
+ </MkSpacer>
</div>
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
import Progress from '@client/scripts/loading';
import XNotifications from '@client/components/notifications.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
+import { notificationTypes } from '@/types';
export default defineComponent({
components: {
@@ -22,14 +28,35 @@ export default defineComponent({
title: this.$ts.notifications,
icon: 'fas fa-bell',
bg: 'var(--bg)',
+ },
+ tab: 'all',
+ includeTypes: null,
+ header: computed(() => ({
+ title: this.$ts.notifications,
+ icon: 'fas fa-bell',
+ bg: 'var(--bg)',
actions: [{
+ text: this.$ts.filter,
+ icon: 'fas fa-filter',
+ highlighted: this.includeTypes != null,
+ handler: this.setFilter,
+ }, {
text: this.$ts.markAllAsRead,
icon: 'fas fa-check',
handler: () => {
os.apiWithDialog('notifications/mark-all-as-read');
- }
- }]
- },
+ },
+ }],
+ tabs: [{
+ active: this.tab === 'all',
+ title: this.$ts.all,
+ onClick: () => { this.tab = 'all'; },
+ }, {
+ active: this.tab === 'unread',
+ title: this.$ts.unread,
+ onClick: () => { this.tab = 'unread'; },
+ },]
+ })),
};
},
@@ -40,6 +67,24 @@ export default defineComponent({
after() {
Progress.done();
+ },
+
+ setFilter(ev) {
+ const typeItems = notificationTypes.map(t => ({
+ text: this.$t(`_notification._types.${t}`),
+ active: this.includeTypes && this.includeTypes.includes(t),
+ action: () => {
+ this.includeTypes = [t];
+ }
+ }));
+ const items = this.includeTypes != null ? [{
+ icon: 'fas fa-times',
+ text: this.$ts.clear,
+ action: () => {
+ this.includeTypes = null;
+ }
+ }, null, ...typeItems] : typeItems;
+ os.popupMenu(items, ev.currentTarget || ev.target);
}
}
});
@@ -47,14 +92,5 @@ export default defineComponent({
<style lang="scss" scoped>
.clupoqwt {
- &.min-width_800px {
- background: var(--bg);
- padding: 32px 0;
-
- > .notifications {
- max-width: 800px;
- margin: 0 auto;
- }
- }
}
</style>
diff --git a/src/client/pages/page-editor/els/page-editor.el.button.vue b/src/client/pages/page-editor/els/page-editor.el.button.vue
index 3a43817cf6..85e9d7e711 100644
--- a/src/client/pages/page-editor/els/page-editor.el.button.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.button.vue
@@ -40,9 +40,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkSelect from '@client/components/ui/select.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSwitch from '@client/components/form/switch.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.canvas.vue b/src/client/pages/page-editor/els/page-editor.el.canvas.vue
index d8d5b990ca..c40d69a7c1 100644
--- a/src/client/pages/page-editor/els/page-editor.el.canvas.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.canvas.vue
@@ -22,7 +22,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.counter.vue b/src/client/pages/page-editor/els/page-editor.el.counter.vue
index 973de50fc2..de7994e3ba 100644
--- a/src/client/pages/page-editor/els/page-editor.el.counter.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.counter.vue
@@ -20,7 +20,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue
index 6eb0c7709f..52f4dac22e 100644
--- a/src/client/pages/page-editor/els/page-editor.el.if.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.if.vue
@@ -19,7 +19,7 @@
</optgroup>
</MkSelect>
- <XBlocks class="children" v-model:value="value.children" :hpml="hpml"/>
+ <XBlocks class="children" v-model="value.children" :hpml="hpml"/>
</section>
</XContainer>
</template>
@@ -28,7 +28,7 @@
import { defineComponent, defineAsyncComponent } from 'vue';
import { v4 as uuid } from 'uuid';
import XContainer from '../page-editor.container.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkSelect from '@client/components/form/select.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.note.vue b/src/client/pages/page-editor/els/page-editor.el.note.vue
index 5766564c1a..9feec395b7 100644
--- a/src/client/pages/page-editor/els/page-editor.el.note.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.note.vue
@@ -18,8 +18,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSwitch from '@client/components/form/switch.vue';
import XNote from '@client/components/note.vue';
import XNoteDetailed from '@client/components/note-detailed.vue';
import * as os from '@client/os';
diff --git a/src/client/pages/page-editor/els/page-editor.el.number-input.vue b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
index 892e7e1caa..57b1397824 100644
--- a/src/client/pages/page-editor/els/page-editor.el.number-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.number-input.vue
@@ -20,7 +20,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.post.vue b/src/client/pages/page-editor/els/page-editor.el.post.vue
index 4215b159d3..e21ccfd345 100644
--- a/src/client/pages/page-editor/els/page-editor.el.post.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.post.vue
@@ -13,9 +13,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSwitch from '@client/components/form/switch.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
index 88be96f35d..62fb231f79 100644
--- a/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.radio-button.vue
@@ -14,8 +14,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue
index 16ef2598f9..75bdf120c0 100644
--- a/src/client/pages/page-editor/els/page-editor.el.section.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.section.vue
@@ -11,7 +11,7 @@
</template>
<section class="ilrvjyvi">
- <XBlocks class="children" v-model:value="value.children" :hpml="hpml"/>
+ <XBlocks class="children" v-model="value.children" :hpml="hpml"/>
</section>
</XContainer>
</template>
diff --git a/src/client/pages/page-editor/els/page-editor.el.switch.vue b/src/client/pages/page-editor/els/page-editor.el.switch.vue
index ade1291410..cf15f58c82 100644
--- a/src/client/pages/page-editor/els/page-editor.el.switch.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.switch.vue
@@ -13,8 +13,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkSwitch from '@client/components/form/switch.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.text-input.vue b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
index 3c8fcc04af..210199befd 100644
--- a/src/client/pages/page-editor/els/page-editor.el.text-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.text-input.vue
@@ -13,7 +13,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
index a4fbb08ffe..14f36db2a1 100644
--- a/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
+++ b/src/client/pages/page-editor/els/page-editor.el.textarea-input.vue
@@ -13,8 +13,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkInput from '@client/components/form/input.vue';
import * as os from '@client/os';
export default defineComponent({
diff --git a/src/client/pages/page-editor/page-editor.blocks.vue b/src/client/pages/page-editor/page-editor.blocks.vue
index 0065b16c8c..c27162a26e 100644
--- a/src/client/pages/page-editor/page-editor.blocks.vue
+++ b/src/client/pages/page-editor/page-editor.blocks.vue
@@ -32,7 +32,7 @@ export default defineComponent({
},
props: {
- value: {
+ modelValue: {
type: Array,
required: true
},
@@ -41,15 +41,15 @@ export default defineComponent({
},
},
- emits: ['update:value'],
+ emits: ['update:modelValue'],
computed: {
blocks: {
get() {
- return this.value;
+ return this.modelValue;
},
set(value) {
- this.$emit('update:value', value);
+ this.$emit('update:modelValue', value);
}
}
},
@@ -62,17 +62,16 @@ export default defineComponent({
v,
...this.blocks.slice(i + 1)
];
- this.$emit('update:value', newValue);
+ this.$emit('update:modelValue', newValue);
},
removeItem(el) {
- console.log(el);
const i = this.blocks.findIndex(x => x.id === el.id);
const newValue = [
...this.blocks.slice(0, i),
...this.blocks.slice(i + 1)
];
- this.$emit('update:value', newValue);
+ this.$emit('update:modelValue', newValue);
},
}
});
diff --git a/src/client/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue
index fedcd7b317..3313fc1ba9 100644
--- a/src/client/pages/page-editor/page-editor.script-block.vue
+++ b/src/client/pages/page-editor/page-editor.script-block.vue
@@ -7,23 +7,23 @@
</button>
</template>
- <section v-if="value.type === null" class="pbglfege" @click="changeType()">
+ <section v-if="modelValue.type === null" class="pbglfege" @click="changeType()">
{{ $ts._pages.script.emptySlot }}
</section>
- <section v-else-if="value.type === 'text'" class="tbwccoaw">
- <input v-model="value.value"/>
+ <section v-else-if="modelValue.type === 'text'" class="tbwccoaw">
+ <input v-model="modelValue.value"/>
</section>
- <section v-else-if="value.type === 'multiLineText'" class="tbwccoaw">
- <textarea v-model="value.value"></textarea>
+ <section v-else-if="modelValue.type === 'multiLineText'" class="tbwccoaw">
+ <textarea v-model="modelValue.value"></textarea>
</section>
- <section v-else-if="value.type === 'textList'" class="tbwccoaw">
- <textarea v-model="value.value" :placeholder="$ts._pages.script.blocks._textList.info"></textarea>
+ <section v-else-if="modelValue.type === 'textList'" class="tbwccoaw">
+ <textarea v-model="modelValue.value" :placeholder="$ts._pages.script.blocks._textList.info"></textarea>
</section>
- <section v-else-if="value.type === 'number'" class="tbwccoaw">
- <input v-model="value.value" type="number"/>
+ <section v-else-if="modelValue.type === 'number'" class="tbwccoaw">
+ <input v-model="modelValue.value" type="number"/>
</section>
- <section v-else-if="value.type === 'ref'" class="hpdwcrvs">
- <select v-model="value.value">
+ <section v-else-if="modelValue.type === 'ref'" class="hpdwcrvs">
+ <select v-model="modelValue.value">
<option v-for="v in hpml.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
<optgroup :label="$ts._pages.script.argVariables">
<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
@@ -36,21 +36,21 @@
</optgroup>
</select>
</section>
- <section v-else-if="value.type === 'aiScriptVar'" class="tbwccoaw">
- <input v-model="value.value"/>
+ <section v-else-if="modelValue.type === 'aiScriptVar'" class="tbwccoaw">
+ <input v-model="modelValue.value"/>
</section>
- <section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
+ <section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
<MkTextarea v-model="slots">
<template #label>{{ $ts._pages.script.blocks._fn.slots }}</template>
<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
</MkTextarea>
- <XV v-if="value.value.expression" v-model:value="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/>
+ <XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="value.value.slots" :name="name"/>
</section>
- <section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;">
- <XV v-for="(x, i) in value.args" v-model:value="value.args[i]" :title="hpml.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name" :key="i"/>
+ <section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
+ <XV v-for="(x, i) in modelValue.args" v-model="value.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name" :key="i"/>
</section>
<section v-else class="" style="padding:16px;">
- <XV v-for="(x, i) in value.args" v-model:value="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots" :key="i"/>
+ <XV v-for="(x, i) in modelValue.args" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots" :key="i"/>
</section>
</XContainer>
</template>
@@ -59,7 +59,7 @@
import { defineAsyncComponent, defineComponent } from 'vue';
import { v4 as uuid } from 'uuid';
import XContainer from './page-editor.container.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
import { blockDefs } from '@client/scripts/hpml/index';
import * as os from '@client/os';
import { isLiteralValue } from '@client/scripts/hpml/expr';
@@ -78,7 +78,7 @@ export default defineComponent({
required: false,
default: null
},
- value: {
+ modelValue: {
required: true
},
title: {
@@ -113,21 +113,21 @@ export default defineComponent({
computed: {
icon(): any {
- if (this.value.type === null) return null;
- if (this.value.type.startsWith('fn:')) return 'fas fa-plug';
- return blockDefs.find(x => x.type === this.value.type).icon;
+ if (this.modelValue.type === null) return null;
+ if (this.modelValue.type.startsWith('fn:')) return 'fas fa-plug';
+ return blockDefs.find(x => x.type === this.modelValue.type).icon;
},
typeText(): any {
- if (this.value.type === null) return null;
- if (this.value.type.startsWith('fn:')) return this.value.type.split(':')[1];
- return this.$t(`_pages.script.blocks.${this.value.type}`);
+ if (this.modelValue.type === null) return null;
+ if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1];
+ return this.$t(`_pages.script.blocks.${this.modelValue.type}`);
},
},
watch: {
slots: {
handler() {
- this.value.value.slots = this.slots.split('\n').map(x => ({
+ this.modelValue.value.slots = this.slots.split('\n').map(x => ({
name: x,
type: null
}));
@@ -137,24 +137,24 @@ export default defineComponent({
},
created() {
- if (this.value.value == null) this.value.value = null;
+ if (this.modelValue.value == null) this.modelValue.value = null;
- if (this.value.value && this.value.value.slots) this.slots = this.value.value.slots.map(x => x.name).join('\n');
+ if (this.modelValue.value && this.modelValue.value.slots) this.slots = this.modelValue.value.slots.map(x => x.name).join('\n');
- this.$watch(() => this.value.type, (t) => {
+ this.$watch(() => this.modelValue.type, (t) => {
this.warn = null;
- if (this.value.type === 'fn') {
+ if (this.modelValue.type === 'fn') {
const id = uuid();
- this.value.value = {
+ this.modelValue.value = {
slots: [],
expression: { id, type: null }
};
return;
}
- if (this.value.type && this.value.type.startsWith('fn:')) {
- const fnName = this.value.type.split(':')[1];
+ if (this.modelValue.type && this.modelValue.type.startsWith('fn:')) {
+ const fnName = this.modelValue.type.split(':')[1];
const fn = this.hpml.getVarByName(fnName);
const empties = [];
@@ -162,29 +162,29 @@ export default defineComponent({
const id = uuid();
empties.push({ id, type: null });
}
- this.value.args = empties;
+ this.modelValue.args = empties;
return;
}
- if (isLiteralValue(this.value)) return;
+ if (isLiteralValue(this.modelValue)) return;
const empties = [];
- for (let i = 0; i < funcDefs[this.value.type].in.length; i++) {
+ for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
const id = uuid();
empties.push({ id, type: null });
}
- this.value.args = empties;
+ this.modelValue.args = empties;
- for (let i = 0; i < funcDefs[this.value.type].in.length; i++) {
- const inType = funcDefs[this.value.type].in[i];
+ for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
+ const inType = funcDefs[this.modelValue.type].in[i];
if (typeof inType !== 'number') {
- if (inType === 'number') this.value.args[i].type = 'number';
- if (inType === 'string') this.value.args[i].type = 'text';
+ if (inType === 'number') this.modelValue.args[i].type = 'number';
+ if (inType === 'string') this.modelValue.args[i].type = 'text';
}
}
});
- this.$watch(() => this.value.args, (args) => {
+ this.$watch(() => this.modelValue.args, (args) => {
if (args == null) {
this.warn = null;
return;
@@ -202,8 +202,8 @@ export default defineComponent({
});
this.$watch(() => this.hpml.variables, () => {
- if (this.type != null && this.value) {
- this.error = this.hpml.typeCheck(this.value);
+ if (this.type != null && this.modelValue) {
+ this.error = this.hpml.typeCheck(this.modelValue);
}
}, {
deep: true
@@ -221,11 +221,11 @@ export default defineComponent({
showCancelButton: true
});
if (canceled) return;
- this.value.type = type;
+ this.modelValue.type = type;
},
_getExpectedType(slot: number) {
- return this.hpml.getExpectedType(this.value, slot);
+ return this.hpml.getExpectedType(this.modelValue, slot);
}
}
});
diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue
index dc6896ba12..e04039e634 100644
--- a/src/client/pages/page-editor/page-editor.vue
+++ b/src/client/pages/page-editor/page-editor.vue
@@ -1,85 +1,84 @@
<template>
-<div class="_root">
- <MkA class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkA>
+<div>
+ <MkHeader :info="header"/>
- <div class="buttons" style="margin: 16px;">
- <MkButton inline @click="save" primary class="save" v-if="!readonly"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- <MkButton inline @click="duplicate" class="duplicate" v-if="pageId"><i class="fas fa-copy"></i> {{ $ts.duplicate }}</MkButton>
- <MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
- </div>
+ <div class="_root">
+ <div class="jqqmcavi" style="margin: 16px;">
+ <MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="fas fa-external-link-square-alt"></i> {{ $ts._pages.viewPage }}</MkButton>
+ <MkButton inline @click="save" primary class="button" v-if="!readonly"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
+ <MkButton inline @click="duplicate" class="button" v-if="pageId"><i class="fas fa-copy"></i> {{ $ts.duplicate }}</MkButton>
+ <MkButton inline @click="del" class="button" v-if="pageId && !readonly" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</MkButton>
+ </div>
- <MkContainer :foldable="true" :expanded="true" class="_gap">
- <template #header><i class="fas fa-cog"></i> {{ $ts._pages.pageSetting }}</template>
- <div style="padding: 16px;">
- <MkInput v-model="title">
- <template #label>{{ $ts._pages.title }}</template>
- </MkInput>
+ <div v-if="tab === 'settings'">
+ <div style="padding: 16px;" class="_formRoot">
+ <MkInput v-model="title" class="_formBlock">
+ <template #label>{{ $ts._pages.title }}</template>
+ </MkInput>
- <MkInput v-model="summary">
- <template #label>{{ $ts._pages.summary }}</template>
- </MkInput>
+ <MkInput v-model="summary" class="_formBlock">
+ <template #label>{{ $ts._pages.summary }}</template>
+ </MkInput>
- <MkInput v-model="name">
- <template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
- <template #label>{{ $ts._pages.url }}</template>
- </MkInput>
+ <MkInput v-model="name" class="_formBlock">
+ <template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
+ <template #label>{{ $ts._pages.url }}</template>
+ </MkInput>
- <MkSwitch v-model="alignCenter">{{ $ts._pages.alignCenter }}</MkSwitch>
+ <MkSwitch v-model="alignCenter" class="_formBlock">{{ $ts._pages.alignCenter }}</MkSwitch>
- <MkSelect v-model="font">
- <template #label>{{ $ts._pages.font }}</template>
- <option value="serif">{{ $ts._pages.fontSerif }}</option>
- <option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option>
- </MkSelect>
+ <MkSelect v-model="font" class="_formBlock">
+ <template #label>{{ $ts._pages.font }}</template>
+ <option value="serif">{{ $ts._pages.fontSerif }}</option>
+ <option value="sans-serif">{{ $ts._pages.fontSansSerif }}</option>
+ </MkSelect>
- <MkSwitch v-model="hideTitleWhenPinned">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch>
+ <MkSwitch v-model="hideTitleWhenPinned" class="_formBlock">{{ $ts._pages.hideTitleWhenPinned }}</MkSwitch>
- <div class="eyeCatch">
- <MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="fas fa-plus"></i> {{ $ts._pages.eyeCatchingImageSet }}</MkButton>
- <div v-else-if="eyeCatchingImage">
- <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/>
- <MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><i class="fas fa-trash-alt"></i> {{ $ts._pages.eyeCatchingImageRemove }}</MkButton>
+ <div class="eyeCatch">
+ <MkButton v-if="eyeCatchingImageId == null && !readonly" @click="setEyeCatchingImage"><i class="fas fa-plus"></i> {{ $ts._pages.eyeCatchingImageSet }}</MkButton>
+ <div v-else-if="eyeCatchingImage">
+ <img :src="eyeCatchingImage.url" :alt="eyeCatchingImage.name" style="max-width: 100%;"/>
+ <MkButton @click="removeEyeCatchingImage()" v-if="!readonly"><i class="fas fa-trash-alt"></i> {{ $ts._pages.eyeCatchingImageRemove }}</MkButton>
+ </div>
</div>
</div>
</div>
- </MkContainer>
- <MkContainer :foldable="true" :expanded="true" class="_gap">
- <template #header><i class="fas fa-sticky-note"></i> {{ $ts._pages.contents }}</template>
- <div style="padding: 16px;">
- <XBlocks class="content" v-model:value="content" :hpml="hpml"/>
+ <div v-else-if="tab === 'contents'">
+ <div style="padding: 16px;">
+ <XBlocks class="content" v-model="content" :hpml="hpml"/>
- <MkButton @click="add()" v-if="!readonly"><i class="fas fa-plus"></i></MkButton>
+ <MkButton @click="add()" v-if="!readonly"><i class="fas fa-plus"></i></MkButton>
+ </div>
</div>
- </MkContainer>
- <MkContainer :foldable="true" class="_gap">
- <template #header><i class="fas fa-magic"></i> {{ $ts._pages.variables }}</template>
- <div class="qmuvgica">
- <XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
- <template #item="{element}">
- <XVariable
- :value="element"
- :removable="true"
- @remove="() => removeVariable(element)"
- :hpml="hpml"
- :name="element.name"
- :title="element.name"
- :draggable="true"
- />
- </template>
- </XDraggable>
+ <div v-else-if="tab === 'variables'">
+ <div class="qmuvgica">
+ <XDraggable tag="div" class="variables" v-show="variables.length > 0" v-model="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
+ <template #item="{element}">
+ <XVariable
+ :modelValue="element"
+ :removable="true"
+ @remove="() => removeVariable(element)"
+ :hpml="hpml"
+ :name="element.name"
+ :title="element.name"
+ :draggable="true"
+ />
+ </template>
+ </XDraggable>
- <MkButton @click="addVariable()" class="add" v-if="!readonly"><i class="fas fa-plus"></i></MkButton>
+ <MkButton @click="addVariable()" class="add" v-if="!readonly"><i class="fas fa-plus"></i></MkButton>
+ </div>
</div>
- </MkContainer>
- <MkContainer :foldable="true" :expanded="true" class="_gap">
- <template #header><i class="fas fa-code"></i> {{ $ts.script }}</template>
- <div>
- <MkTextarea class="_code" v-model="script"/>
+ <div v-else-if="tab === 'script'">
+ <div>
+ <MkTextarea class="_code" v-model="script"/>
+ </div>
</div>
- </MkContainer>
+ </div>
</div>
</template>
@@ -94,12 +93,12 @@ import 'vue-prism-editor/dist/prismeditor.min.css';
import { v4 as uuid } from 'uuid';
import XVariable from './page-editor.script-block.vue';
import XBlocks from './page-editor.blocks.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
import MkContainer from '@client/components/ui/container.vue';
import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/ui/select.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkSelect from '@client/components/form/select.vue';
+import MkSwitch from '@client/components/form/switch.vue';
+import MkInput from '@client/components/form/input.vue';
import { blockDefs } from '@client/scripts/hpml/index';
import { HpmlTypeChecker } from '@client/scripts/hpml/type-checker';
import { url } from '@client/config';
@@ -142,6 +141,43 @@ export default defineComponent({
return {
title: title,
icon: 'fas fa-pencil-alt',
+ bg: 'var(--bg)',
+ };
+ }),
+ tab: 'settings',
+ header: computed(() => {
+ let title = this.$ts._pages.newPage;
+ if (this.initPageId) {
+ title = this.$ts._pages.editPage;
+ }
+ else if (this.initPageName && this.initUser) {
+ title = this.$ts._pages.readPage;
+ }
+ return {
+ title: title,
+ icon: 'fas fa-pencil-alt',
+ bg: 'var(--bg)',
+ tabs: [{
+ active: this.tab === 'settings',
+ title: this.$ts._pages.pageSetting,
+ icon: 'fas fa-cog',
+ onClick: () => { this.tab = 'settings'; },
+ }, {
+ active: this.tab === 'contents',
+ title: this.$ts._pages.contents,
+ icon: 'fas fa-sticky-note',
+ onClick: () => { this.tab = 'contents'; },
+ }, {
+ active: this.tab === 'variables',
+ title: this.$ts._pages.variables,
+ icon: 'fas fa-magic',
+ onClick: () => { this.tab = 'variables'; },
+ }, {
+ active: this.tab === 'script',
+ title: this.$ts.script,
+ icon: 'fas fa-code',
+ onClick: () => { this.tab = 'script'; },
+ }]
};
}),
author: this.$i,
@@ -455,6 +491,14 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
+.jqqmcavi {
+ > .button {
+ & + .button {
+ margin-left: 8px;
+ }
+ }
+}
+
.gwbmwxkm {
position: relative;
@@ -522,11 +566,7 @@ export default defineComponent({
}
.qmuvgica {
- padding: 32px;
-
- @media (max-width: 500px) {
- padding: 16px;
- }
+ padding: 16px;
> .variables {
margin-bottom: 16px;
diff --git a/src/client/pages/page.vue b/src/client/pages/page.vue
index 47a458df9c..b8d7507363 100644
--- a/src/client/pages/page.vue
+++ b/src/client/pages/page.vue
@@ -1,61 +1,65 @@
<template>
-<div class="_root">
- <transition name="fade" mode="out-in">
- <div v-if="page" class="xcukqgmh" :key="page.id" v-size="{ max: [450] }">
- <div class="_block main">
- <!--
- <div class="header">
- <h1>{{ page.title }}</h1>
- </div>
- -->
- <div class="banner">
- <img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/>
- </div>
- <div class="content">
- <XPage :page="page"/>
- </div>
- <div class="actions">
- <div class="like">
- <MkButton class="button" @click="unlike()" v-if="page.isLiked" v-tooltip="$ts._pages.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
- <MkButton class="button" @click="like()" v-else v-tooltip="$ts._pages.like"><i class="far fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
+<div>
+ <MkHeader :info="header"/>
+
+ <div class="_root">
+ <transition name="fade" mode="out-in">
+ <div v-if="page" class="xcukqgmh" :key="page.id" v-size="{ max: [450] }">
+ <div class="_block main">
+ <!--
+ <div class="header">
+ <h1>{{ page.title }}</h1>
</div>
- <div class="other">
- <button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
- <button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
+ -->
+ <div class="banner">
+ <img :src="page.eyeCatchingImage.url" v-if="page.eyeCatchingImageId"/>
</div>
- </div>
- <div class="user">
- <MkAvatar :user="page.user" class="avatar"/>
- <div class="name">
- <MkUserName :user="page.user" style="display: block;"/>
- <MkAcct :user="page.user"/>
+ <div class="content">
+ <XPage :page="page"/>
+ </div>
+ <div class="actions">
+ <div class="like">
+ <MkButton class="button" @click="unlike()" v-if="page.isLiked" v-tooltip="$ts._pages.unlike" primary><i class="fas fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
+ <MkButton class="button" @click="like()" v-else v-tooltip="$ts._pages.like"><i class="far fa-heart"></i><span class="count" v-if="page.likedCount > 0">{{ page.likedCount }}</span></MkButton>
+ </div>
+ <div class="other">
+ <button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
+ <button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
+ </div>
+ </div>
+ <div class="user">
+ <MkAvatar :user="page.user" class="avatar"/>
+ <div class="name">
+ <MkUserName :user="page.user" style="display: block;"/>
+ <MkAcct :user="page.user"/>
+ </div>
+ <MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
+ </div>
+ <div class="links">
+ <MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
+ <template v-if="$i && $i.id === page.userId">
+ <MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
+ <button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button>
+ <button v-else @click="pin(true)" class="link _textButton">{{ $ts.pin }}</button>
+ </template>
</div>
- <MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
</div>
- <div class="links">
- <MkA :to="`/@${username}/pages/${pageName}/view-source`" class="link">{{ $ts._pages.viewSource }}</MkA>
- <template v-if="$i && $i.id === page.userId">
- <MkA :to="`/pages/edit/${page.id}`" class="link">{{ $ts._pages.editThisPage }}</MkA>
- <button v-if="$i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $ts.unpin }}</button>
- <button v-else @click="pin(true)" class="link _textButton">{{ $ts.pin }}</button>
- </template>
+ <div class="footer">
+ <div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
+ <div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
</div>
+ <MkAd :prefer="['horizontal', 'horizontal-big']"/>
+ <MkContainer :max-height="300" :foldable="true" class="other">
+ <template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
+ <MkPagination :pagination="otherPostsPagination" #default="{items}">
+ <MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/>
+ </MkPagination>
+ </MkContainer>
</div>
- <div class="footer">
- <div><i class="far fa-clock"></i> {{ $ts.createdAt }}: <MkTime :time="page.createdAt" mode="detail"/></div>
- <div v-if="page.createdAt != page.updatedAt"><i class="far fa-clock"></i> {{ $ts.updatedAt }}: <MkTime :time="page.updatedAt" mode="detail"/></div>
- </div>
- <MkAd :prefer="['horizontal', 'horizontal-big']"/>
- <MkContainer :max-height="300" :foldable="true" class="other">
- <template #header><i class="fas fa-clock"></i> {{ $ts.recentPosts }}</template>
- <MkPagination :pagination="otherPostsPagination" #default="{items}">
- <MkPagePreview v-for="page in items" :page="page" :key="page.id" class="_gap"/>
- </MkPagination>
- </MkContainer>
- </div>
- <MkError v-else-if="error" @retry="fetch()"/>
- <MkLoading v-else/>
- </transition>
+ <MkError v-else-if="error" @retry="fetch()"/>
+ <MkLoading v-else/>
+ </transition>
+ </div>
</div>
</template>
@@ -97,6 +101,10 @@ export default defineComponent({
[symbols.PAGE_INFO]: computed(() => this.page ? {
title: computed(() => this.page.title || this.page.name),
avatar: this.page.user,
+ } : null),
+ header: computed(() => this.page ? {
+ title: computed(() => this.page.title || this.page.name),
+ avatar: this.page.user,
path: `/@${this.page.user.username}/pages/${this.page.name}`,
share: {
title: this.page.title || this.page.name,
diff --git a/src/client/pages/pages.vue b/src/client/pages/pages.vue
index 52a860be13..8300e8a6e4 100644
--- a/src/client/pages/pages.vue
+++ b/src/client/pages/pages.vue
@@ -1,31 +1,36 @@
<template>
<div>
- <MkTab v-model:value="tab" v-if="$i">
- <option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._pages.featured }}</option>
- <option value="my"><i class="fas fa-edit"></i> {{ $ts._pages.my }}</option>
- <option value="liked"><i class="fas fa-heart"></i> {{ $ts._pages.liked }}</option>
- </MkTab>
+ <MkHeader :info="header"/>
- <div class="_section">
- <div class="rknalgpo _content" v-if="tab === 'featured'">
- <MkPagination :pagination="featuredPagesPagination" #default="{items}">
- <MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
- </MkPagination>
- </div>
+ <MkSpacer>
+ <!-- TODO: MkHeaderに統合 -->
+ <MkTab v-model="tab" v-if="$i">
+ <option value="featured"><i class="fas fa-fire-alt"></i> {{ $ts._pages.featured }}</option>
+ <option value="my"><i class="fas fa-edit"></i> {{ $ts._pages.my }}</option>
+ <option value="liked"><i class="fas fa-heart"></i> {{ $ts._pages.liked }}</option>
+ </MkTab>
- <div class="rknalgpo _content my" v-if="tab === 'my'">
- <MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
- <MkPagination :pagination="myPagesPagination" #default="{items}">
- <MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
- </MkPagination>
- </div>
+ <div class="_section">
+ <div class="rknalgpo _content" v-if="tab === 'featured'">
+ <MkPagination :pagination="featuredPagesPagination" #default="{items}">
+ <MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
+ </MkPagination>
+ </div>
+
+ <div class="rknalgpo _content my" v-if="tab === 'my'">
+ <MkButton class="new" @click="create()"><i class="fas fa-plus"></i></MkButton>
+ <MkPagination :pagination="myPagesPagination" #default="{items}">
+ <MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
+ </MkPagination>
+ </div>
- <div class="rknalgpo _content" v-if="tab === 'liked'">
- <MkPagination :pagination="likedPagesPagination" #default="{items}">
- <MkPagePreview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
- </MkPagination>
+ <div class="rknalgpo _content" v-if="tab === 'liked'">
+ <MkPagination :pagination="likedPagesPagination" #default="{items}">
+ <MkPagePreview v-for="like in items" class="ckltabjg" :page="like.page" :key="like.page.id"/>
+ </MkPagination>
+ </div>
</div>
- </div>
+ </MkSpacer>
</div>
</template>
@@ -46,11 +51,17 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.pages,
icon: 'fas fa-sticky-note',
+ bg: 'var(--bg)',
+ },
+ header: {
+ title: this.$ts.pages,
+ icon: 'fas fa-sticky-note',
+ bg: 'var(--bg)',
actions: [{
icon: 'fas fa-plus',
text: this.$ts.create,
- handler: this.create
- }]
+ handler: this.create,
+ }],
},
tab: 'featured',
featuredPagesPagination: {
diff --git a/src/client/pages/reset-password.vue b/src/client/pages/reset-password.vue
index c331382132..6dd9f24259 100644
--- a/src/client/pages/reset-password.vue
+++ b/src/client/pages/reset-password.vue
@@ -1,6 +1,6 @@
<template>
<FormBase v-if="token">
- <FormInput v-model:value="password" type="password">
+ <FormInput v-model="password" type="password">
<template #prefix><i class="fas fa-lock"></i></template>
<span>{{ $ts.newPassword }}</span>
</FormInput>
@@ -11,11 +11,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/reversi/game.setting.vue b/src/client/pages/reversi/game.setting.vue
index 1cc623b790..eb6f24e4ab 100644
--- a/src/client/pages/reversi/game.setting.vue
+++ b/src/client/pages/reversi/game.setting.vue
@@ -127,8 +127,8 @@
import { defineComponent } from 'vue';
import * as maps from '../../../games/reversi/maps';
import MkButton from '@client/components/ui/button.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
-import MkRadio from '@client/components/ui/radio.vue';
+import MkSwitch from '@client/components/form/switch.vue';
+import MkRadio from '@client/components/form/radio.vue';
export default defineComponent({
components: {
@@ -303,7 +303,7 @@ export default defineComponent({
-moz-appearance: none;
appearance: none;
- &:focus,
+ &:focus-visible,
&:active {
border-color: var(--accent);
}
diff --git a/src/client/pages/room/room.vue b/src/client/pages/room/room.vue
index 365ed5b803..671dca3577 100644
--- a/src/client/pages/room/room.vue
+++ b/src/client/pages/room/room.vue
@@ -57,7 +57,7 @@ import XPreview from './preview.vue';
const storeItems = require('@client/scripts/room/furnitures.json5');
import { query as urlQuery } from '../../../prelude/url';
import MkButton from '@client/components/ui/button.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkSelect from '@client/components/form/select.vue';
import { selectFile } from '@client/scripts/select-file';
import * as os from '@client/os';
import { ColdDeviceStorage } from '@client/store';
diff --git a/src/client/pages/search.vue b/src/client/pages/search.vue
index bf228576be..fec138726f 100644
--- a/src/client/pages/search.vue
+++ b/src/client/pages/search.vue
@@ -1,7 +1,10 @@
<template>
-<div class="_section">
- <div class="_content">
- <XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/>
+<div>
+ <MkHeader :info="header"/>
+ <div class="_section">
+ <div class="_content">
+ <XNotes ref="notes" :pagination="pagination" @before="before" @after="after"/>
+ </div>
</div>
</div>
</template>
@@ -21,7 +24,11 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: computed(() => this.$t('searchWith', { q: this.$route.query.q })),
- icon: 'fas fa-search'
+ icon: 'fas fa-search',
+ },
+ header: {
+ title: computed(() => this.$t('searchWith', { q: this.$route.query.q })),
+ icon: 'fas fa-search',
},
pagination: {
endpoint: 'notes/search',
diff --git a/src/client/pages/settings/2fa.vue b/src/client/pages/settings/2fa.vue
index 48b06eaa24..386e7c635a 100644
--- a/src/client/pages/settings/2fa.vue
+++ b/src/client/pages/settings/2fa.vue
@@ -72,11 +72,11 @@ import { hostname } from '@client/config';
import { byteify, hexify, stringify } from '@client/scripts/2fa';
import MkButton from '@client/components/ui/button.vue';
import MkInfo from '@client/components/ui/info.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSwitch from '@client/components/form/switch.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/settings/account-info.vue b/src/client/pages/settings/account-info.vue
index 4d851b7b12..16ce91b12f 100644
--- a/src/client/pages/settings/account-info.vue
+++ b/src/client/pages/settings/account-info.vue
@@ -134,11 +134,11 @@
import { defineAsyncComponent, defineComponent } from 'vue';
import FormSwitch from '@client/components/form/switch.vue';
import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
import * as os from '@client/os';
import number from '@client/filters/number';
import bytes from '@client/filters/bytes';
diff --git a/src/client/pages/settings/accounts.vue b/src/client/pages/settings/accounts.vue
index ca6f53776a..d2966cc216 100644
--- a/src/client/pages/settings/accounts.vue
+++ b/src/client/pages/settings/accounts.vue
@@ -3,8 +3,8 @@
<FormSuspense :p="init">
<FormButton @click="addAccount" primary><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton>
- <div class="_formItem _button" v-for="account in accounts" :key="account.id" @click="menu(account, $event)">
- <div class="_formPanel lcjjdxlm">
+ <div class="_debobigegoItem _button" v-for="account in accounts" :key="account.id" @click="menu(account, $event)">
+ <div class="_debobigegoPanel lcjjdxlm">
<div class="avatar">
<MkAvatar :user="account" class="avatar"/>
</div>
@@ -24,11 +24,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSuspense from '@client/components/form/suspense.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import { getAccounts, addAccount, login } from '@client/account';
@@ -47,6 +47,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.accounts,
icon: 'fas fa-users',
+ bg: 'var(--bg)',
},
storedAccounts: getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id)),
accounts: null,
diff --git a/src/client/pages/settings/api.vue b/src/client/pages/settings/api.vue
index 396d4405c3..5c7496e2f9 100644
--- a/src/client/pages/settings/api.vue
+++ b/src/client/pages/settings/api.vue
@@ -10,10 +10,10 @@
import { defineComponent } from 'vue';
import FormSwitch from '@client/components/form/switch.vue';
import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -30,7 +30,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: 'API',
- icon: 'fas fa-key'
+ icon: 'fas fa-key',
+ bg: 'var(--bg)',
},
isDesktop: window.innerWidth >= 1100,
};
diff --git a/src/client/pages/settings/apps.vue b/src/client/pages/settings/apps.vue
index c864920ce1..da4f672adf 100644
--- a/src/client/pages/settings/apps.vue
+++ b/src/client/pages/settings/apps.vue
@@ -8,7 +8,7 @@
</div>
</template>
<template #default="{items}">
- <div class="_formPanel bfomjevm" v-for="token in items" :key="token.id">
+ <div class="_debobigegoPanel 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>
@@ -39,12 +39,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormPagination from '@client/components/form/pagination.vue';
+import FormPagination from '@client/components/debobigego/pagination.vue';
import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -61,6 +61,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: {
title: this.$ts.installedApps,
icon: 'fas fa-plug',
+ bg: 'var(--bg)',
},
pagination: {
endpoint: 'i/apps',
diff --git a/src/client/pages/settings/custom-css.vue b/src/client/pages/settings/custom-css.vue
index 0781eeebd7..fd473a11fa 100644
--- a/src/client/pages/settings/custom-css.vue
+++ b/src/client/pages/settings/custom-css.vue
@@ -2,7 +2,7 @@
<FormBase>
<FormInfo warn>{{ $ts.customCssWarn }}</FormInfo>
- <FormTextarea v-model:value="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;">
+ <FormTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;">
<span>{{ $ts.local }}</span>
</FormTextarea>
</FormBase>
@@ -13,11 +13,11 @@ import { defineComponent } from 'vue';
import FormTextarea from '@client/components/form/textarea.vue';
import FormSelect from '@client/components/form/select.vue';
import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormInfo from '@client/components/form/info.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
import * as os from '@client/os';
import { ColdDeviceStorage } from '@client/store';
import { unisonReload } from '@client/scripts/unison-reload';
@@ -42,7 +42,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.customCss,
- icon: 'fas fa-code'
+ icon: 'fas fa-code',
+ bg: 'var(--bg)',
},
localCustomCss: localStorage.getItem('customCss')
}
diff --git a/src/client/pages/settings/deck.vue b/src/client/pages/settings/deck.vue
index 05f3061ca1..e4b5c697c4 100644
--- a/src/client/pages/settings/deck.vue
+++ b/src/client/pages/settings/deck.vue
@@ -2,10 +2,10 @@
<FormBase>
<FormGroup>
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
- <FormSwitch v-model:value="navWindow">{{ $ts.openInWindow }}</FormSwitch>
+ <FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch>
</FormGroup>
- <FormSwitch v-model:value="alwaysShowMainColumn">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
+ <FormSwitch v-model="alwaysShowMainColumn">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
<FormRadios v-model="columnAlign">
<template #desc>{{ $ts._deck.columnAlign }}</template>
@@ -20,7 +20,7 @@
<option :value="48">{{ $ts.wide }}</option>
</FormRadios>
- <FormInput v-model:value="columnMargin" type="number">
+ <FormInput v-model="columnMargin" type="number">
<span>{{ $ts._deck.columnMargin }}</span>
<template #suffix>px</template>
</FormInput>
@@ -31,12 +31,12 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormRadios from '@client/components/form/radios.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormRadios from '@client/components/debobigego/radios.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import { deckStore } from '@client/ui/deck/deck-store';
import * as os from '@client/os';
import { unisonReload } from '@client/scripts/unison-reload';
@@ -58,7 +58,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.deck,
- icon: 'fas fa-columns'
+ icon: 'fas fa-columns',
+ bg: 'var(--bg)',
},
}
},
diff --git a/src/client/pages/settings/delete-account.vue b/src/client/pages/settings/delete-account.vue
index 3af1879857..6bac214e04 100644
--- a/src/client/pages/settings/delete-account.vue
+++ b/src/client/pages/settings/delete-account.vue
@@ -9,10 +9,10 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import { debug } from '@client/config';
import { signout } from '@client/account';
@@ -32,7 +32,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts._accountDelete.accountDelete,
- icon: 'fas fa-exclamation-triangle'
+ icon: 'fas fa-exclamation-triangle',
+ bg: 'var(--bg)',
},
debug,
}
diff --git a/src/client/pages/settings/drive.vue b/src/client/pages/settings/drive.vue
index 83068a8335..177bf058f3 100644
--- a/src/client/pages/settings/drive.vue
+++ b/src/client/pages/settings/drive.vue
@@ -2,8 +2,8 @@
<FormBase class="">
<FormGroup v-if="!fetching">
<template #label>{{ $ts.usageAmount }}</template>
- <div class="_formItem uawsfosz">
- <div class="_formPanel">
+ <div class="_debobigegoItem uawsfosz">
+ <div class="_debobigegoPanel">
<div class="meter"><div :style="meterStyle"></div></div>
</div>
</div>
@@ -17,9 +17,9 @@
</FormKeyValueView>
</FormGroup>
- <div class="_formItem">
- <div class="_formLabel">{{ $ts.statistics }}</div>
- <div class="_formPanel">
+ <div class="_debobigegoItem">
+ <div class="_debobigegoLabel">{{ $ts.statistics }}</div>
+ <div class="_debobigegoPanel">
<div ref="chart"></div>
</div>
</div>
@@ -36,10 +36,10 @@
import { defineComponent } from 'vue';
import * as tinycolor from 'tinycolor2';
import ApexCharts from 'apexcharts';
-import FormButton from '@client/components/form/button.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormBase from '@client/components/form/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormBase from '@client/components/debobigego/base.vue';
import * as os from '@client/os';
import bytes from '@client/filters/bytes';
import * as symbols from '@client/symbols';
@@ -58,7 +58,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.drive,
- icon: 'fas fa-cloud'
+ icon: 'fas fa-cloud',
+ bg: 'var(--bg)',
},
fetching: true,
usage: null,
diff --git a/src/client/pages/settings/email-address.vue b/src/client/pages/settings/email-address.vue
index 28eeeb6b73..f98b22ada7 100644
--- a/src/client/pages/settings/email-address.vue
+++ b/src/client/pages/settings/email-address.vue
@@ -1,7 +1,7 @@
<template>
<FormBase>
<FormGroup>
- <FormInput v-model:value="emailAddress" type="email">
+ <FormInput v-model="emailAddress" type="email">
{{ $ts.emailAddress }}
<template #desc v-if="$i.email && !$i.emailVerified">{{ $ts.verificationEmailSent }}</template>
<template #desc v-else-if="emailAddress === $i.email && $i.emailVerified">{{ $ts.emailVerified }}</template>
@@ -13,10 +13,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormButton from '@client/components/form/button.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import FormInput from '@client/components/form/input.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -34,7 +34,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.emailAddress,
- icon: 'fas fa-envelope'
+ icon: 'fas fa-envelope',
+ bg: 'var(--bg)',
},
emailAddress: null,
code: null,
diff --git a/src/client/pages/settings/email-notification.vue b/src/client/pages/settings/email-notification.vue
index ac3402568a..1b78621c3f 100644
--- a/src/client/pages/settings/email-notification.vue
+++ b/src/client/pages/settings/email-notification.vue
@@ -1,22 +1,22 @@
<template>
<FormBase>
<FormGroup>
- <FormSwitch v-model:value="mention">
+ <FormSwitch v-model="mention">
{{ $ts._notification._types.mention }}
</FormSwitch>
- <FormSwitch v-model:value="reply">
+ <FormSwitch v-model="reply">
{{ $ts._notification._types.reply }}
</FormSwitch>
- <FormSwitch v-model:value="quote">
+ <FormSwitch v-model="quote">
{{ $ts._notification._types.quote }}
</FormSwitch>
- <FormSwitch v-model:value="follow">
+ <FormSwitch v-model="follow">
{{ $ts._notification._types.follow }}
</FormSwitch>
- <FormSwitch v-model:value="receiveFollowRequest">
+ <FormSwitch v-model="receiveFollowRequest">
{{ $ts._notification._types.receiveFollowRequest }}
</FormSwitch>
- <FormSwitch v-model:value="groupInvited">
+ <FormSwitch v-model="groupInvited">
{{ $ts._notification._types.groupInvited }}
</FormSwitch>
</FormGroup>
@@ -25,10 +25,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormButton from '@client/components/form/button.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import FormSwitch from '@client/components/form/switch.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
import * as symbols from '@client/symbols';
@@ -47,7 +47,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.emailNotification,
- icon: 'fas fa-envelope'
+ icon: 'fas fa-envelope',
+ bg: 'var(--bg)',
},
mention: this.$i.emailNotificationTypes.includes('mention'),
diff --git a/src/client/pages/settings/email.vue b/src/client/pages/settings/email.vue
index aa20d9d94e..adc62133ac 100644
--- a/src/client/pages/settings/email.vue
+++ b/src/client/pages/settings/email.vue
@@ -14,7 +14,7 @@
{{ $ts.emailNotification }}
</FormLink>
- <FormSwitch :value="$i.receiveAnnouncementEmail" @update:value="onChangeReceiveAnnouncementEmail">
+ <FormSwitch :value="$i.receiveAnnouncementEmail" @update:modelValue="onChangeReceiveAnnouncementEmail">
{{ $ts.receiveAnnouncementFromInstance }}
</FormSwitch>
</FormBase>
@@ -22,11 +22,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormButton from '@client/components/form/button.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormSwitch from '@client/components/form/switch.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -45,7 +45,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.email,
- icon: 'fas fa-envelope'
+ icon: 'fas fa-envelope',
+ bg: 'var(--bg)',
},
}
},
diff --git a/src/client/pages/settings/experimental-features.vue b/src/client/pages/settings/experimental-features.vue
index f8d5e419e9..971c45a628 100644
--- a/src/client/pages/settings/experimental-features.vue
+++ b/src/client/pages/settings/experimental-features.vue
@@ -8,11 +8,11 @@
import { defineAsyncComponent, defineComponent } from 'vue';
import FormSwitch from '@client/components/form/switch.vue';
import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/settings/general.vue b/src/client/pages/settings/general.vue
index f8e8e6b24b..59dd251948 100644
--- a/src/client/pages/settings/general.vue
+++ b/src/client/pages/settings/general.vue
@@ -1,8 +1,8 @@
<template>
<FormBase>
- <FormSwitch v-model:value="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch>
+ <FormSwitch v-model="showFixedPostForm">{{ $ts.showFixedPostForm }}</FormSwitch>
- <FormSelect v-model:value="lang">
+ <FormSelect v-model="lang">
<template #label>{{ $ts.uiLanguage }}</template>
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
<template #caption>
@@ -16,13 +16,13 @@
<FormGroup>
<template #label>{{ $ts.behavior }}</template>
- <FormSwitch v-model:value="imageNewTab">{{ $ts.openImageInNewTab }}</FormSwitch>
- <FormSwitch v-model:value="enableInfiniteScroll">{{ $ts.enableInfiniteScroll }}</FormSwitch>
- <FormSwitch v-model:value="useReactionPickerForContextMenu">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch>
- <FormSwitch v-model:value="disablePagesScript">{{ $ts.disablePagesScript }}</FormSwitch>
+ <FormSwitch v-model="imageNewTab">{{ $ts.openImageInNewTab }}</FormSwitch>
+ <FormSwitch v-model="enableInfiniteScroll">{{ $ts.enableInfiniteScroll }}</FormSwitch>
+ <FormSwitch v-model="useReactionPickerForContextMenu">{{ $ts.useReactionPickerForContextMenu }}</FormSwitch>
+ <FormSwitch v-model="disablePagesScript">{{ $ts.disablePagesScript }}</FormSwitch>
</FormGroup>
- <FormSelect v-model:value="serverDisconnectedBehavior">
+ <FormSelect v-model="serverDisconnectedBehavior">
<template #label>{{ $ts.whenServerDisconnected }}</template>
<option value="reload">{{ $ts._serverDisconnectedBehavior.reload }}</option>
<option value="dialog">{{ $ts._serverDisconnectedBehavior.dialog }}</option>
@@ -31,22 +31,22 @@
<FormGroup>
<template #label>{{ $ts.appearance }}</template>
- <FormSwitch v-model:value="disableAnimatedMfm">{{ $ts.disableAnimatedMfm }}</FormSwitch>
- <FormSwitch v-model:value="reduceAnimation">{{ $ts.reduceUiAnimation }}</FormSwitch>
- <FormSwitch v-model:value="useBlurEffect">{{ $ts.useBlurEffect }}</FormSwitch>
- <FormSwitch v-model:value="useBlurEffectForModal">{{ $ts.useBlurEffectForModal }}</FormSwitch>
- <FormSwitch v-model:value="showGapBetweenNotesInTimeline">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch>
- <FormSwitch v-model:value="loadRawImages">{{ $ts.loadRawImages }}</FormSwitch>
- <FormSwitch v-model:value="disableShowingAnimatedImages">{{ $ts.disableShowingAnimatedImages }}</FormSwitch>
- <FormSwitch v-model:value="squareAvatars">{{ $ts.squareAvatars }}</FormSwitch>
- <FormSwitch v-model:value="useSystemFont">{{ $ts.useSystemFont }}</FormSwitch>
- <FormSwitch v-model:value="useOsNativeEmojis">{{ $ts.useOsNativeEmojis }}
+ <FormSwitch v-model="disableAnimatedMfm">{{ $ts.disableAnimatedMfm }}</FormSwitch>
+ <FormSwitch v-model="reduceAnimation">{{ $ts.reduceUiAnimation }}</FormSwitch>
+ <FormSwitch v-model="useBlurEffect">{{ $ts.useBlurEffect }}</FormSwitch>
+ <FormSwitch v-model="useBlurEffectForModal">{{ $ts.useBlurEffectForModal }}</FormSwitch>
+ <FormSwitch v-model="showGapBetweenNotesInTimeline">{{ $ts.showGapBetweenNotesInTimeline }}</FormSwitch>
+ <FormSwitch v-model="loadRawImages">{{ $ts.loadRawImages }}</FormSwitch>
+ <FormSwitch v-model="disableShowingAnimatedImages">{{ $ts.disableShowingAnimatedImages }}</FormSwitch>
+ <FormSwitch v-model="squareAvatars">{{ $ts.squareAvatars }}</FormSwitch>
+ <FormSwitch v-model="useSystemFont">{{ $ts.useSystemFont }}</FormSwitch>
+ <FormSwitch v-model="useOsNativeEmojis">{{ $ts.useOsNativeEmojis }}
<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪" :key="useOsNativeEmojis"/></div>
</FormSwitch>
</FormGroup>
<FormGroup>
- <FormSwitch v-model:value="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch>
+ <FormSwitch v-model="aiChanMode">{{ $ts.aiChanMode }}</FormSwitch>
</FormGroup>
<FormRadios v-model="fontSize">
@@ -57,14 +57,14 @@
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
</FormRadios>
- <FormSelect v-model:value="instanceTicker">
+ <FormSelect v-model="instanceTicker">
<template #label>{{ $ts.instanceTicker }}</template>
<option value="none">{{ $ts._instanceTicker.none }}</option>
<option value="remote">{{ $ts._instanceTicker.remote }}</option>
<option value="always">{{ $ts._instanceTicker.always }}</option>
</FormSelect>
- <FormSelect v-model:value="nsfw">
+ <FormSelect v-model="nsfw">
<template #label>{{ $ts.nsfw }}</template>
<option value="respect">{{ $ts._nsfw.respect }}</option>
<option value="ignore">{{ $ts._nsfw.ignore }}</option>
@@ -73,10 +73,10 @@
<FormGroup>
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
- <FormSwitch v-model:value="defaultSideView">{{ $ts.openInSideView }}</FormSwitch>
+ <FormSwitch v-model="defaultSideView">{{ $ts.openInSideView }}</FormSwitch>
</FormGroup>
- <FormSelect v-model:value="chatOpenBehavior">
+ <FormSelect v-model="chatOpenBehavior">
<template #label>{{ $ts.chatOpenBehavior }}</template>
<option value="page">{{ $ts.showInPage }}</option>
<option value="window">{{ $ts.openInWindow }}</option>
@@ -91,13 +91,13 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormSelect from '@client/components/debobigego/select.vue';
+import FormRadios from '@client/components/debobigego/radios.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import MkLink from '@client/components/link.vue';
import { langs } from '@client/config';
import { defaultStore } from '@client/store';
@@ -124,7 +124,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.general,
- icon: 'fas fa-cogs'
+ icon: 'fas fa-cogs',
+ bg: 'var(--bg)'
},
langs,
lang: localStorage.getItem('lang'),
diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue
index e77efb4429..2b49996dda 100644
--- a/src/client/pages/settings/import-export.vue
+++ b/src/client/pages/settings/import-export.vue
@@ -1,45 +1,42 @@
<template>
-<FormBase>
- <FormGroup>
+<div style="margin: 16px;">
+ <FormSection>
<template #label>{{ $ts._exportOrImport.allNotes }}</template>
- <FormButton @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</FormButton>
- </FormGroup>
- <FormGroup>
+ <MkButton :class="$style.button" inline @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ </FormSection>
+ <FormSection>
<template #label>{{ $ts._exportOrImport.followingList }}</template>
- <FormButton @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</FormButton>
- <FormButton @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</FormButton>
- </FormGroup>
- <FormGroup>
+ <MkButton :class="$style.button" inline @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <MkButton :class="$style.button" inline @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+ </FormSection>
+ <FormSection>
<template #label>{{ $ts._exportOrImport.userLists }}</template>
- <FormButton @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</FormButton>
- <FormButton @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</FormButton>
- </FormGroup>
- <FormGroup>
+ <MkButton :class="$style.button" inline @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ <MkButton :class="$style.button" inline @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton>
+ </FormSection>
+ <FormSection>
<template #label>{{ $ts._exportOrImport.muteList }}</template>
- <FormButton @click="doExport('mute')"><i class="fas fa-download"></i> {{ $ts.export }}</FormButton>
- </FormGroup>
- <FormGroup>
+ <MkButton :class="$style.button" inline @click="doExport('mute')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ </FormSection>
+ <FormSection>
<template #label>{{ $ts._exportOrImport.blockingList }}</template>
- <FormButton @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</FormButton>
- </FormGroup>
-</FormBase>
+ <MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton>
+ </FormSection>
+</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import MkButton from '@client/components/ui/button.vue';
+import FormSection from '@client/components/form/section.vue';
import * as os from '@client/os';
import { selectFile } from '@client/scripts/select-file';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
- FormBase,
- FormGroup,
- FormButton,
+ FormSection,
+ MkButton,
},
emits: ['info'],
@@ -48,7 +45,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.importAndExport,
- icon: 'fas fa-boxes'
+ icon: 'fas fa-boxes',
+ bg: 'var(--bg)',
},
}
},
@@ -102,3 +100,9 @@ export default defineComponent({
}
});
</script>
+
+<style module>
+.button {
+ margin-right: 16px;
+}
+</style>
diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue
index 3fb5f5f1e6..9da3031a41 100644
--- a/src/client/pages/settings/index.vue
+++ b/src/client/pages/settings/index.vue
@@ -1,53 +1,12 @@
<template>
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
<div class="nav" v-if="!narrow || page == null">
- <FormBase>
- <FormGroup>
- <div class="_formItem">
- <div class="_formPanel lwjxoukj">
- <MkAvatar :user="$i" class="avatar"/>
- </div>
- </div>
- <FormLink :active="page === 'accounts'" replace to="/settings/accounts"><template #icon><i class="fas fa-users"></i></template>{{ $ts.accounts }}</FormLink>
- </FormGroup>
- <FormInfo v-if="emailNotConfigured" warn>{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></FormInfo>
- <FormGroup>
- <template #label>{{ $ts.basicSettings }}</template>
- <FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><i class="fas fa-user"></i></template>{{ $ts.profile }}</FormLink>
- <FormLink :active="page === 'privacy'" replace to="/settings/privacy"><template #icon><i class="fas fa-lock-open"></i></template>{{ $ts.privacy }}</FormLink>
- <FormLink :active="page === 'reaction'" replace to="/settings/reaction"><template #icon><i class="fas fa-laugh"></i></template>{{ $ts.reaction }}</FormLink>
- <FormLink :active="page === 'drive'" replace to="/settings/drive"><template #icon><i class="fas fa-cloud"></i></template>{{ $ts.drive }}</FormLink>
- <FormLink :active="page === 'notifications'" replace to="/settings/notifications"><template #icon><i class="fas fa-bell"></i></template>{{ $ts.notifications }}</FormLink>
- <FormLink :active="page === 'email'" replace to="/settings/email"><template #icon><i class="fas fa-envelope"></i></template>{{ $ts.email }}</FormLink>
- <FormLink :active="page === 'integration'" replace to="/settings/integration"><template #icon><i class="fas fa-share-alt"></i></template>{{ $ts.integration }}</FormLink>
- <FormLink :active="page === 'security'" replace to="/settings/security"><template #icon><i class="fas fa-lock"></i></template>{{ $ts.security }}</FormLink>
- </FormGroup>
- <FormGroup>
- <template #label>{{ $ts.clientSettings }}</template>
- <FormLink :active="page === 'general'" replace to="/settings/general"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.general }}</FormLink>
- <FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><i class="fas fa-palette"></i></template>{{ $ts.theme }}</FormLink>
- <FormLink :active="page === 'menu'" replace to="/settings/menu"><template #icon><i class="fas fa-list-ul"></i></template>{{ $ts.menu }}</FormLink>
- <FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><i class="fas fa-music"></i></template>{{ $ts.sounds }}</FormLink>
- <FormLink :active="page === 'plugin'" replace to="/settings/plugin"><template #icon><i class="fas fa-plug"></i></template>{{ $ts.plugins }}</FormLink>
- </FormGroup>
- <FormGroup>
- <template #label>{{ $ts.otherSettings }}</template>
- <FormLink :active="page === 'import-export'" replace to="/settings/import-export"><template #icon><i class="fas fa-boxes"></i></template>{{ $ts.importAndExport }}</FormLink>
- <FormLink :active="page === 'mute-block'" replace to="/settings/mute-block"><template #icon><i class="fas fa-ban"></i></template>{{ $ts.muteAndBlock }}</FormLink>
- <FormLink :active="page === 'word-mute'" replace to="/settings/word-mute"><template #icon><i class="fas fa-comment-slash"></i></template>{{ $ts.wordMute }}</FormLink>
- <FormLink :active="page === 'api'" replace to="/settings/api"><template #icon><i class="fas fa-key"></i></template>API</FormLink>
- <FormLink :active="page === 'other'" replace to="/settings/other"><template #icon><i class="fas fa-ellipsis-h"></i></template>{{ $ts.other }}</FormLink>
- </FormGroup>
- <FormGroup>
- <FormButton @click="clear">{{ $ts.clearCache }}</FormButton>
- </FormGroup>
- <FormGroup>
- <FormButton @click="logout" danger>{{ $ts.logout }}</FormButton>
- </FormGroup>
- </FormBase>
+ <div class="title">{{ $ts.settings }}</div>
+ <MkInfo v-if="emailNotConfigured" warn class="info">{{ $ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+ <MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
</div>
<div class="main">
- <component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
+ <component :is="component" :key="page" v-bind="pageProps"/>
</div>
</div>
</template>
@@ -55,11 +14,8 @@
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { i18n } from '@client/i18n';
-import FormLink from '@client/components/form/link.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormInfo from '@client/components/form/info.vue';
+import MkInfo from '@client/components/ui/info.vue';
+import MkSuperMenu from '@client/components/ui/super-menu.vue';
import { scroll } from '@client/scripts/scroll';
import { signout } from '@client/account';
import { unisonReload } from '@client/scripts/unison-reload';
@@ -69,11 +25,8 @@ import { $i } from '@client/account';
export default defineComponent({
components: {
- FormBase,
- FormLink,
- FormGroup,
- FormButton,
- FormInfo,
+ MkInfo,
+ MkSuperMenu,
},
props: {
@@ -94,9 +47,126 @@ export default defineComponent({
const narrow = ref(false);
const view = ref(null);
const el = ref(null);
- const onInfo = (viewInfo) => {
- INFO.value = viewInfo;
- };
+ const menuDef = computed(() => [{
+ title: i18n.locale.basicSettings,
+ items: [{
+ icon: 'fas fa-user',
+ text: i18n.locale.profile,
+ to: '/settings/profile',
+ active: page.value === 'profile',
+ }, {
+ icon: 'fas fa-lock-open',
+ text: i18n.locale.privacy,
+ to: '/settings/privacy',
+ active: page.value === 'privacy',
+ }, {
+ icon: 'fas fa-laugh',
+ text: i18n.locale.reaction,
+ to: '/settings/reaction',
+ active: page.value === 'reaction',
+ }, {
+ icon: 'fas fa-cloud',
+ text: i18n.locale.drive,
+ to: '/settings/drive',
+ active: page.value === 'drive',
+ }, {
+ icon: 'fas fa-bell',
+ text: i18n.locale.notifications,
+ to: '/settings/notifications',
+ active: page.value === 'notifications',
+ }, {
+ icon: 'fas fa-envelope',
+ text: i18n.locale.email,
+ to: '/settings/email',
+ active: page.value === 'email',
+ }, {
+ icon: 'fas fa-share-alt',
+ text: i18n.locale.integration,
+ to: '/settings/integration',
+ active: page.value === 'integration',
+ }, {
+ icon: 'fas fa-lock',
+ text: i18n.locale.security,
+ to: '/settings/security',
+ active: page.value === 'security',
+ }],
+ }, {
+ title: i18n.locale.clientSettings,
+ items: [{
+ icon: 'fas fa-cogs',
+ text: i18n.locale.general,
+ to: '/settings/general',
+ active: page.value === 'general',
+ }, {
+ icon: 'fas fa-palette',
+ text: i18n.locale.theme,
+ to: '/settings/theme',
+ active: page.value === 'theme',
+ }, {
+ icon: 'fas fa-list-ul',
+ text: i18n.locale.menu,
+ to: '/settings/menu',
+ active: page.value === 'menu',
+ }, {
+ icon: 'fas fa-music',
+ text: i18n.locale.sounds,
+ to: '/settings/sounds',
+ active: page.value === 'sounds',
+ }, {
+ icon: 'fas fa-plug',
+ text: i18n.locale.plugins,
+ to: '/settings/plugin',
+ active: page.value === 'plugin',
+ }],
+ }, {
+ title: i18n.locale.otherSettings,
+ items: [{
+ icon: 'fas fa-boxes',
+ text: i18n.locale.importAndExport,
+ to: '/settings/import-export',
+ active: page.value === 'import-export',
+ }, {
+ icon: 'fas fa-ban',
+ text: i18n.locale.muteAndBlock,
+ to: '/settings/mute-block',
+ active: page.value === 'mute-block',
+ }, {
+ icon: 'fas fa-comment-slash',
+ text: i18n.locale.wordMute,
+ to: '/settings/word-mute',
+ active: page.value === 'word-mute',
+ }, {
+ icon: 'fas fa-key',
+ text: 'API',
+ to: '/settings/api',
+ active: page.value === 'api',
+ }, {
+ icon: 'fas fa-ellipsis-h',
+ text: i18n.locale.other,
+ to: '/settings/other',
+ active: page.value === 'other',
+ }],
+ }, {
+ items: [{
+ type: 'button',
+ icon: 'fas fa-trash',
+ text: i18n.locale.clearCache,
+ action: () => {
+ localStorage.removeItem('locale');
+ localStorage.removeItem('theme');
+ unisonReload();
+ },
+ }, {
+ type: 'button',
+ icon: 'fas fa-sign-in-alt fa-flip-horizontal',
+ text: i18n.locale.logout,
+ action: () => {
+ signout();
+ },
+ danger: true,
+ },],
+ }]);
+
const pageProps = ref({});
const component = computed(() => {
if (page.value == null) return null;
@@ -159,7 +229,7 @@ export default defineComponent({
}
nextTick(() => {
- scroll(el.value, 0);
+ scroll(el.value, { top: 0 });
});
}, { immediate: true });
@@ -186,21 +256,13 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: INFO,
page,
+ menuDef,
narrow,
view,
el,
- onInfo,
pageProps,
component,
emailNotConfigured,
- logout: () => {
- signout();
- },
- clear: () => {
- localStorage.removeItem('locale');
- localStorage.removeItem('theme');
- unisonReload();
- },
};
},
});
@@ -208,17 +270,41 @@ export default defineComponent({
<style lang="scss" scoped>
.vvcocwet {
+ > .nav {
+ > .title {
+ margin: 16px;
+ font-size: 1.5em;
+ font-weight: bold;
+ }
+
+ > .info {
+ margin: 0 16px;
+ }
+
+ > .accounts {
+ > .avatar {
+ display: block;
+ width: 50px;
+ height: 50px;
+ margin: 8px auto 16px auto;
+ }
+ }
+ }
+
&.wide {
display: flex;
- max-width: 1100px;
+ max-width: 1000px;
margin: 0 auto;
height: 100%;
> .nav {
width: 32%;
box-sizing: border-box;
- border-right: solid 0.5px var(--divider);
overflow: auto;
+
+ > .title {
+ margin: 24px;
+ }
}
> .main {
@@ -229,15 +315,4 @@ export default defineComponent({
}
}
}
-
-.lwjxoukj {
- padding: 16px;
-
- > .avatar {
- display: block;
- margin: auto;
- width: 42px;
- height: 42px;
- }
-}
</style>
diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue
index f1c0a88afc..7f398dde9d 100644
--- a/src/client/pages/settings/integration.vue
+++ b/src/client/pages/settings/integration.vue
@@ -1,26 +1,26 @@
<template>
<FormBase>
- <div class="_formItem" v-if="enableTwitterIntegration">
- <div class="_formLabel"><i class="fab fa-twitter"></i> Twitter</div>
- <div class="_formPanel" style="padding: 16px;">
+ <div class="_debobigegoItem" v-if="enableTwitterIntegration">
+ <div class="_debobigegoLabel"><i class="fab fa-twitter"></i> Twitter</div>
+ <div class="_debobigegoPanel" style="padding: 16px;">
<p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p>
<MkButton v-if="integrations.twitter" @click="disconnectTwitter" danger>{{ $ts.disconnectService }}</MkButton>
<MkButton v-else @click="connectTwitter" primary>{{ $ts.connectService }}</MkButton>
</div>
</div>
- <div class="_formItem" v-if="enableDiscordIntegration">
- <div class="_formLabel"><i class="fab fa-discord"></i> Discord</div>
- <div class="_formPanel" style="padding: 16px;">
+ <div class="_debobigegoItem" v-if="enableDiscordIntegration">
+ <div class="_debobigegoLabel"><i class="fab fa-discord"></i> Discord</div>
+ <div class="_debobigegoPanel" style="padding: 16px;">
<p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p>
<MkButton v-if="integrations.discord" @click="disconnectDiscord" danger>{{ $ts.disconnectService }}</MkButton>
<MkButton v-else @click="connectDiscord" primary>{{ $ts.connectService }}</MkButton>
</div>
</div>
- <div class="_formItem" v-if="enableGithubIntegration">
- <div class="_formLabel"><i class="fab fa-github"></i> GitHub</div>
- <div class="_formPanel" style="padding: 16px;">
+ <div class="_debobigegoItem" v-if="enableGithubIntegration">
+ <div class="_debobigegoLabel"><i class="fab fa-github"></i> GitHub</div>
+ <div class="_debobigegoPanel" style="padding: 16px;">
<p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p>
<MkButton v-if="integrations.github" @click="disconnectGithub" danger>{{ $ts.disconnectService }}</MkButton>
<MkButton v-else @click="connectGithub" primary>{{ $ts.connectService }}</MkButton>
@@ -32,7 +32,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { apiUrl } from '@client/config';
-import FormBase from '@client/components/form/base.vue';
+import FormBase from '@client/components/debobigego/base.vue';
import MkButton from '@client/components/ui/button.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -49,7 +49,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.integration,
- icon: 'fas fa-share-alt'
+ icon: 'fas fa-share-alt',
+ bg: 'var(--bg)',
},
apiUrl,
twitterForm: null,
diff --git a/src/client/pages/settings/menu.vue b/src/client/pages/settings/menu.vue
index 4b315145e1..31472eb0c1 100644
--- a/src/client/pages/settings/menu.vue
+++ b/src/client/pages/settings/menu.vue
@@ -1,6 +1,6 @@
<template>
<FormBase>
- <FormTextarea v-model:value="items" tall manual-save>
+ <FormTextarea v-model="items" tall manual-save>
<span>{{ $ts.menu }}</span>
<template #desc><button class="_textButton" @click="addItem">{{ $ts.addItem }}</button></template>
</FormTextarea>
@@ -19,12 +19,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormRadios from '@client/components/debobigego/radios.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import { menuDef } from '@client/menu';
import { defaultStore } from '@client/store';
@@ -45,7 +43,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.menu,
- icon: 'fas fa-list-ul'
+ icon: 'fas fa-list-ul',
+ bg: 'var(--bg)',
},
menuDef: menuDef,
items: defaultStore.state.menu.join('\n'),
diff --git a/src/client/pages/settings/mute-block.vue b/src/client/pages/settings/mute-block.vue
index dde0199e18..18b2fc0af4 100644
--- a/src/client/pages/settings/mute-block.vue
+++ b/src/client/pages/settings/mute-block.vue
@@ -1,6 +1,6 @@
<template>
<FormBase>
- <MkTab v-model:value="tab" style="margin-bottom: var(--margin);">
+ <MkTab v-model="tab" style="margin-bottom: var(--margin);">
<option value="mute">{{ $ts.mutedUsers }}</option>
<option value="block">{{ $ts.blockedUsers }}</option>
</MkTab>
@@ -35,10 +35,10 @@
import { defineComponent } from 'vue';
import MkPagination from '@client/components/ui/pagination.vue';
import MkTab from '@client/components/tab.vue';
-import FormInfo from '@client/components/form/info.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import { userPage } from '@client/filters/user';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -59,7 +59,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.muteAndBlock,
- icon: 'fas fa-ban'
+ icon: 'fas fa-ban',
+ bg: 'var(--bg)',
},
tab: 'mute',
mutingPagination: {
diff --git a/src/client/pages/settings/notifications.vue b/src/client/pages/settings/notifications.vue
index ec95452ba2..5f84349474 100644
--- a/src/client/pages/settings/notifications.vue
+++ b/src/client/pages/settings/notifications.vue
@@ -11,11 +11,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormButton from '@client/components/form/button.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import { notificationTypes } from '../../../types';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import { notificationTypes } from '@/types';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -33,7 +33,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.notifications,
- icon: 'fas fa-bell'
+ icon: 'fas fa-bell',
+ bg: 'var(--bg)',
},
}
},
diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue
index 21b5439041..2eb922453f 100644
--- a/src/client/pages/settings/other.vue
+++ b/src/client/pages/settings/other.vue
@@ -2,18 +2,18 @@
<FormBase>
<FormLink to="/settings/update">Misskey Update</FormLink>
- <FormSwitch :value="$i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
+ <FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote">
{{ $ts.showFeaturedNotesInTimeline }}
</FormSwitch>
- <FormSwitch v-model:value="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
+ <FormSwitch v-model="reportError">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
<FormLink to="/settings/account-info">{{ $ts.accountInfo }}</FormLink>
<FormLink to="/settings/experimental-features">{{ $ts.experimentalFeatures }}</FormLink>
<FormGroup>
<template #label>{{ $ts.developer }}</template>
- <FormSwitch v-model:value="debug" @update:value="changeDebug">
+ <FormSwitch v-model="debug" @update:modelValue="changeDebug">
DEBUG MODE
</FormSwitch>
<template v-if="debug">
@@ -34,10 +34,10 @@
import { defineAsyncComponent, defineComponent } from 'vue';
import FormSwitch from '@client/components/form/switch.vue';
import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import { debug } from '@client/config';
import { defaultStore } from '@client/store';
@@ -60,7 +60,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.other,
- icon: 'fas fa-ellipsis-h'
+ icon: 'fas fa-ellipsis-h',
+ bg: 'var(--bg)',
},
debug,
}
diff --git a/src/client/pages/settings/plugin.install.vue b/src/client/pages/settings/plugin.install.vue
index 30cbf58ad7..709ef11abb 100644
--- a/src/client/pages/settings/plugin.install.vue
+++ b/src/client/pages/settings/plugin.install.vue
@@ -3,7 +3,7 @@
<FormInfo warn>{{ $ts._plugin.installWarn }}</FormInfo>
<FormGroup>
- <FormTextarea v-model:value="code" tall>
+ <FormTextarea v-model="code" tall>
<span>{{ $ts.code }}</span>
</FormTextarea>
</FormGroup>
@@ -20,11 +20,11 @@ import { v4 as uuid } from 'uuid';
import FormTextarea from '@client/components/form/textarea.vue';
import FormSelect from '@client/components/form/select.vue';
import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormInfo from '@client/components/form/info.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
import * as os from '@client/os';
import { ColdDeviceStorage } from '@client/store';
import { unisonReload } from '@client/scripts/unison-reload';
@@ -48,7 +48,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts._plugin.install,
- icon: 'fas fa-download'
+ icon: 'fas fa-download',
+ bg: 'var(--bg)',
},
code: null,
}
diff --git a/src/client/pages/settings/plugin.manage.vue b/src/client/pages/settings/plugin.manage.vue
index 3df87ca084..f1c27f1e3c 100644
--- a/src/client/pages/settings/plugin.manage.vue
+++ b/src/client/pages/settings/plugin.manage.vue
@@ -3,9 +3,9 @@
<FormGroup v-for="plugin in plugins" :key="plugin.id">
<template #label><span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span></template>
- <FormSwitch :value="plugin.active" @update:value="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
- <div class="_formItem">
- <div class="_formPanel" style="padding: 16px;">
+ <FormSwitch :value="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
+ <div class="_debobigegoItem">
+ <div class="_debobigegoPanel" style="padding: 16px;">
<div class="_keyValue">
<div>{{ $ts.author }}:</div>
<div>{{ plugin.author }}</div>
@@ -20,8 +20,8 @@
</div>
</div>
</div>
- <div class="_formItem">
- <div class="_formPanel" style="padding: 16px;">
+ <div class="_debobigegoItem">
+ <div class="_debobigegoPanel" style="padding: 16px;">
<MkButton @click="config(plugin)" inline v-if="plugin.config"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton>
<MkButton @click="uninstall(plugin)" inline danger><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton>
</div>
@@ -33,11 +33,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkSelect from '@client/components/ui/select.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkSelect from '@client/components/form/select.vue';
import FormSwitch from '@client/components/form/switch.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import * as os from '@client/os';
import { ColdDeviceStorage } from '@client/store';
import * as symbols from '@client/symbols';
@@ -58,7 +58,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts._plugin.manage,
- icon: 'fas fa-plug'
+ icon: 'fas fa-plug',
+ bg: 'var(--bg)',
},
plugins: ColdDeviceStorage.get('plugins'),
}
diff --git a/src/client/pages/settings/plugin.vue b/src/client/pages/settings/plugin.vue
index 13eaca07fd..23f263bbbd 100644
--- a/src/client/pages/settings/plugin.vue
+++ b/src/client/pages/settings/plugin.vue
@@ -7,9 +7,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormLink from '@client/components/form/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormLink from '@client/components/debobigego/link.vue';
import * as os from '@client/os';
import { ColdDeviceStorage } from '@client/store';
import * as symbols from '@client/symbols';
@@ -26,7 +26,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.plugins,
- icon: 'fas fa-plug'
+ icon: 'fas fa-plug',
+ bg: 'var(--bg)',
},
plugins: ColdDeviceStorage.get('plugins').length,
}
diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue
index 46d8c17ca2..7756158578 100644
--- a/src/client/pages/settings/privacy.vue
+++ b/src/client/pages/settings/privacy.vue
@@ -1,43 +1,43 @@
<template>
<FormBase>
<FormGroup>
- <FormSwitch v-model:value="isLocked" @update:value="save()">{{ $ts.makeFollowManuallyApprove }}</FormSwitch>
- <FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
+ <FormSwitch v-model="isLocked" @update:modelValue="save()">{{ $ts.makeFollowManuallyApprove }}</FormSwitch>
+ <FormSwitch v-model="autoAcceptFollowed" :disabled="!isLocked" @update:modelValue="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
<template #caption>{{ $ts.lockedAccountInfo }}</template>
</FormGroup>
- <FormSwitch v-model:value="hideOnlineStatus" @update:value="save()">
+ <FormSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
{{ $ts.hideOnlineStatus }}
<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
</FormSwitch>
- <FormSwitch v-model:value="noCrawle" @update:value="save()">
+ <FormSwitch v-model="noCrawle" @update:modelValue="save()">
{{ $ts.noCrawle }}
<template #desc>{{ $ts.noCrawleDescription }}</template>
</FormSwitch>
- <FormSwitch v-model:value="isExplorable" @update:value="save()">
+ <FormSwitch v-model="isExplorable" @update:modelValue="save()">
{{ $ts.makeExplorable }}
<template #desc>{{ $ts.makeExplorableDescription }}</template>
</FormSwitch>
- <FormSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $ts.rememberNoteVisibility }}</FormSwitch>
+ <FormSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">{{ $ts.rememberNoteVisibility }}</FormSwitch>
<FormGroup v-if="!rememberNoteVisibility">
<template #label>{{ $ts.defaultNoteVisibility }}</template>
- <FormSelect v-model:value="defaultNoteVisibility">
+ <FormSelect v-model="defaultNoteVisibility">
<option value="public">{{ $ts._visibility.public }}</option>
<option value="home">{{ $ts._visibility.home }}</option>
<option value="followers">{{ $ts._visibility.followers }}</option>
<option value="specified">{{ $ts._visibility.specified }}</option>
</FormSelect>
- <FormSwitch v-model:value="defaultNoteLocalOnly">{{ $ts._visibility.localOnly }}</FormSwitch>
+ <FormSwitch v-model="defaultNoteLocalOnly">{{ $ts._visibility.localOnly }}</FormSwitch>
</FormGroup>
- <FormSwitch v-model:value="keepCw" @update:value="save()">{{ $ts.keepCw }}</FormSwitch>
+ <FormSwitch v-model="keepCw" @update:modelValue="save()">{{ $ts.keepCw }}</FormSwitch>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormSelect from '@client/components/debobigego/select.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import * as os from '@client/os';
import { defaultStore } from '@client/store';
import * as symbols from '@client/symbols';
@@ -56,7 +56,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.privacy,
- icon: 'fas fa-lock-open'
+ icon: 'fas fa-lock-open',
+ bg: 'var(--bg)',
},
isLocked: false,
autoAcceptFollowed: false,
diff --git a/src/client/pages/settings/profile.vue b/src/client/pages/settings/profile.vue
index de7e86bd12..b993b5fc72 100644
--- a/src/client/pages/settings/profile.vue
+++ b/src/client/pages/settings/profile.vue
@@ -1,33 +1,33 @@
<template>
<FormBase>
<FormGroup>
- <div class="_formItem _formPanel llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
+ <div class="_debobigegoItem _debobigegoPanel llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<MkAvatar class="avatar" :user="$i"/>
</div>
<FormButton @click="changeAvatar" primary>{{ $ts._profile.changeAvatar }}</FormButton>
<FormButton @click="changeBanner" primary>{{ $ts._profile.changeBanner }}</FormButton>
</FormGroup>
- <FormInput v-model:value="name" :max="30" manual-save>
+ <FormInput v-model="name" :max="30" manual-save>
<span>{{ $ts._profile.name }}</span>
</FormInput>
- <FormTextarea v-model:value="description" :max="500" tall manual-save>
+ <FormTextarea v-model="description" :max="500" tall manual-save>
<span>{{ $ts._profile.description }}</span>
<template #desc>{{ $ts._profile.youCanIncludeHashtags }}</template>
</FormTextarea>
- <FormInput v-model:value="location" manual-save>
+ <FormInput v-model="location" manual-save>
<span>{{ $ts.location }}</span>
<template #prefix><i class="fas fa-map-marker-alt"></i></template>
</FormInput>
- <FormInput v-model:value="birthday" type="date" manual-save>
+ <FormInput v-model="birthday" type="date" manual-save>
<span>{{ $ts.birthday }}</span>
<template #prefix><i class="fas fa-birthday-cake"></i></template>
</FormInput>
- <FormSelect v-model:value="lang">
+ <FormSelect v-model="lang">
<template #label>{{ $ts.language }}</template>
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
</FormSelect>
@@ -37,23 +37,23 @@
<template #caption>{{ $ts._profile.metadataDescription }}</template>
</FormGroup>
- <FormSwitch v-model:value="isCat">{{ $ts.flagAsCat }}<template #desc>{{ $ts.flagAsCatDescription }}</template></FormSwitch>
+ <FormSwitch v-model="isCat">{{ $ts.flagAsCat }}<template #desc>{{ $ts.flagAsCatDescription }}</template></FormSwitch>
- <FormSwitch v-model:value="isBot">{{ $ts.flagAsBot }}<template #desc>{{ $ts.flagAsBotDescription }}</template></FormSwitch>
+ <FormSwitch v-model="isBot">{{ $ts.flagAsBot }}<template #desc>{{ $ts.flagAsBotDescription }}</template></FormSwitch>
- <FormSwitch v-model:value="alwaysMarkNsfw">{{ $ts.alwaysMarkSensitive }}</FormSwitch>
+ <FormSwitch v-model="alwaysMarkNsfw">{{ $ts.alwaysMarkSensitive }}</FormSwitch>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormButton from '@client/components/form/button.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormSelect from '@client/components/debobigego/select.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import { host, langs } from '@client/config';
import { selectFile } from '@client/scripts/select-file';
import * as os from '@client/os';
@@ -76,7 +76,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.profile,
- icon: 'fas fa-user'
+ icon: 'fas fa-user',
+ bg: 'var(--bg)',
},
host,
langs,
diff --git a/src/client/pages/settings/reaction.vue b/src/client/pages/settings/reaction.vue
index a0024234e4..a5ff46097d 100644
--- a/src/client/pages/settings/reaction.vue
+++ b/src/client/pages/settings/reaction.vue
@@ -1,8 +1,8 @@
<template>
<FormBase>
- <div class="_formItem">
- <div class="_formLabel">{{ $ts.reactionSettingDescription }}</div>
- <div class="_formPanel">
+ <div class="_debobigegoItem">
+ <div class="_debobigegoLabel">{{ $ts.reactionSettingDescription }}</div>
+ <div class="_debobigegoPanel">
<XDraggable class="zoaiodol" v-model="reactions" :item-key="item => item" animation="150" delay="100" delay-on-touch-only="true">
<template #item="{element}">
<button class="_button item" @click="remove(element, $event)">
@@ -14,7 +14,7 @@
</template>
</XDraggable>
</div>
- <div class="_formCaption">{{ $ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ $ts.preview }}</button></div>
+ <div class="_debobigegoCaption">{{ $ts.reactionSettingDescription2 }} <button class="_textButton" @click="preview">{{ $ts.preview }}</button></div>
</div>
<FormRadios v-model="reactionPickerWidth">
@@ -37,10 +37,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import XDraggable from 'vuedraggable';
-import FormInput from '@client/components/form/input.vue';
-import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormInput from '@client/components/debobigego/input.vue';
+import FormRadios from '@client/components/debobigego/radios.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import * as os from '@client/os';
import { defaultStore } from '@client/store';
import * as symbols from '@client/symbols';
@@ -64,7 +64,8 @@ export default defineComponent({
action: {
icon: 'fas fa-eye',
handler: this.preview
- }
+ },
+ bg: 'var(--bg)',
},
reactions: JSON.parse(JSON.stringify(this.$store.state.reactions)),
}
diff --git a/src/client/pages/settings/registry.keys.vue b/src/client/pages/settings/registry.keys.vue
index f71589ba4f..d99002e50f 100644
--- a/src/client/pages/settings/registry.keys.vue
+++ b/src/client/pages/settings/registry.keys.vue
@@ -25,11 +25,11 @@ import { defineAsyncComponent, defineComponent } from 'vue';
import * as JSON5 from 'json5';
import FormSwitch from '@client/components/form/switch.vue';
import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -56,7 +56,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.registry,
- icon: 'fas fa-cogs'
+ icon: 'fas fa-cogs',
+ bg: 'var(--bg)',
},
keys: null,
}
diff --git a/src/client/pages/settings/registry.value.vue b/src/client/pages/settings/registry.value.vue
index 48245ae99f..06be5737e9 100644
--- a/src/client/pages/settings/registry.value.vue
+++ b/src/client/pages/settings/registry.value.vue
@@ -19,7 +19,7 @@
</FormGroup>
<FormGroup>
- <FormTextarea tall v-model:value="valueForEditor" class="_monospace" style="tab-size: 2;">
+ <FormTextarea tall v-model="valueForEditor" class="_monospace" style="tab-size: 2;">
<span>{{ $ts.value }} (JSON)</span>
</FormTextarea>
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
@@ -38,14 +38,14 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import * as JSON5 from 'json5';
-import FormInfo from '@client/components/form/info.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
import FormSwitch from '@client/components/form/switch.vue';
import FormSelect from '@client/components/form/select.vue';
import FormTextarea from '@client/components/form/textarea.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -76,7 +76,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.registry,
- icon: 'fas fa-cogs'
+ icon: 'fas fa-cogs',
+ bg: 'var(--bg)',
},
value: null,
valueForEditor: null,
diff --git a/src/client/pages/settings/registry.vue b/src/client/pages/settings/registry.vue
index 5ba1bc751b..e4fb230d5c 100644
--- a/src/client/pages/settings/registry.vue
+++ b/src/client/pages/settings/registry.vue
@@ -13,11 +13,11 @@ import { defineAsyncComponent, defineComponent } from 'vue';
import * as JSON5 from 'json5';
import FormSwitch from '@client/components/form/switch.vue';
import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -38,7 +38,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.registry,
- icon: 'fas fa-cogs'
+ icon: 'fas fa-cogs',
+ bg: 'var(--bg)',
},
scopes: null,
}
diff --git a/src/client/pages/settings/security.vue b/src/client/pages/settings/security.vue
index b70fa5a9f3..e051685a82 100644
--- a/src/client/pages/settings/security.vue
+++ b/src/client/pages/settings/security.vue
@@ -6,7 +6,7 @@
<FormPagination :pagination="pagination">
<template #label>{{ $ts.signinHistory }}</template>
<template #default="{items}">
- <div class="_formPanel timnmucd" v-for="item in items" :key="item.id">
+ <div class="_debobigegoPanel timnmucd" v-for="item in items" :key="item.id">
<header>
<i v-if="item.success" class="fas fa-check icon succ"></i>
<i v-else class="fas fa-times-circle icon fail"></i>
@@ -25,11 +25,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormBase from '@client/components/form/base.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormPagination from '@client/components/form/pagination.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormPagination from '@client/components/debobigego/pagination.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -48,7 +48,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.security,
- icon: 'fas fa-lock'
+ icon: 'fas fa-lock',
+ bg: 'var(--bg)',
},
pagination: {
endpoint: 'i/signin-history',
diff --git a/src/client/pages/settings/sounds.vue b/src/client/pages/settings/sounds.vue
index 1c51685ce8..07310619c8 100644
--- a/src/client/pages/settings/sounds.vue
+++ b/src/client/pages/settings/sounds.vue
@@ -1,6 +1,6 @@
<template>
<FormBase>
- <FormRange v-model:value="masterVolume" :min="0" :max="1" :step="0.05">
+ <FormRange v-model="masterVolume" :min="0" :max="1" :step="0.05">
<template #label><i class="fas fa-volume-icon"></i> {{ $ts.masterVolume }}</template>
</FormRange>
@@ -19,11 +19,11 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import FormRange from '@client/components/form/range.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormRange from '@client/components/debobigego/range.vue';
+import FormSelect from '@client/components/debobigego/select.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import * as os from '@client/os';
import { ColdDeviceStorage } from '@client/store';
import { playFile } from '@client/scripts/sound';
@@ -71,7 +71,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.sounds,
- icon: 'fas fa-music'
+ icon: 'fas fa-music',
+ bg: 'var(--bg)',
},
sounds: {},
}
diff --git a/src/client/pages/settings/theme.install.vue b/src/client/pages/settings/theme.install.vue
index d719cc801f..9fbb28929d 100644
--- a/src/client/pages/settings/theme.install.vue
+++ b/src/client/pages/settings/theme.install.vue
@@ -1,7 +1,7 @@
<template>
<FormBase>
<FormGroup>
- <FormTextarea v-model:value="installThemeCode">
+ <FormTextarea v-model="installThemeCode">
<span>{{ $ts._theme.code }}</span>
</FormTextarea>
<FormButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
@@ -17,10 +17,10 @@ import * as JSON5 from 'json5';
import FormTextarea from '@client/components/form/textarea.vue';
import FormSelect from '@client/components/form/select.vue';
import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import { applyTheme, validateTheme } from '@client/scripts/theme';
import * as os from '@client/os';
import { ColdDeviceStorage } from '@client/store';
@@ -44,7 +44,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts._theme.install,
- icon: 'fas fa-download'
+ icon: 'fas fa-download',
+ bg: 'var(--bg)',
},
installThemeCode: null,
}
diff --git a/src/client/pages/settings/theme.manage.vue b/src/client/pages/settings/theme.manage.vue
index 7cc7a0169a..da21a47a50 100644
--- a/src/client/pages/settings/theme.manage.vue
+++ b/src/client/pages/settings/theme.manage.vue
@@ -1,6 +1,6 @@
<template>
<FormBase>
- <FormSelect v-model:value="selectedThemeId">
+ <FormSelect v-model="selectedThemeId">
<template #label>{{ $ts.theme }}</template>
<optgroup :label="$ts._theme.installedThemes">
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
@@ -31,10 +31,10 @@ import * as JSON5 from 'json5';
import FormTextarea from '@client/components/form/textarea.vue';
import FormSelect from '@client/components/form/select.vue';
import FormRadios from '@client/components/form/radios.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import FormInput from '@client/components/form/input.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import { Theme, builtinThemes } from '@client/scripts/theme';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
import * as os from '@client/os';
@@ -59,7 +59,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts._theme.manage,
- icon: 'fas fa-folder-open'
+ icon: 'fas fa-folder-open',
+ bg: 'var(--bg)',
},
installedThemes: getThemes(),
builtinThemes,
diff --git a/src/client/pages/settings/theme.vue b/src/client/pages/settings/theme.vue
index 94eddb1b6f..c6be42251c 100644
--- a/src/client/pages/settings/theme.vue
+++ b/src/client/pages/settings/theme.vue
@@ -1,7 +1,7 @@
<template>
<FormBase>
<FormGroup>
- <div class="rfqxtzch _formItem _formPanel">
+ <div class="rfqxtzch _debobigegoItem _debobigegoPanel">
<div class="darkMode">
<div class="toggleWrapper">
<input type="checkbox" class="dn" id="dn" v-model="darkMode"/>
@@ -23,11 +23,11 @@
</div>
</div>
</div>
- <FormSwitch v-model:value="syncDeviceDarkMode">{{ $ts.syncDeviceDarkMode }}</FormSwitch>
+ <FormSwitch v-model="syncDeviceDarkMode">{{ $ts.syncDeviceDarkMode }}</FormSwitch>
</FormGroup>
<template v-if="darkMode">
- <FormSelect v-model:value="darkThemeId">
+ <FormSelect v-model="darkThemeId">
<template #label>{{ $ts.themeForDarkMode }}</template>
<optgroup :label="$ts.darkThemes">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
@@ -36,7 +36,7 @@
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
- <FormSelect v-model:value="lightThemeId">
+ <FormSelect v-model="lightThemeId">
<template #label>{{ $ts.themeForLightMode }}</template>
<optgroup :label="$ts.lightThemes">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
@@ -47,7 +47,7 @@
</FormSelect>
</template>
<template v-else>
- <FormSelect v-model:value="lightThemeId">
+ <FormSelect v-model="lightThemeId">
<template #label>{{ $ts.themeForLightMode }}</template>
<optgroup :label="$ts.lightThemes">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
@@ -56,7 +56,7 @@
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
- <FormSelect v-model:value="darkThemeId">
+ <FormSelect v-model="darkThemeId">
<template #label>{{ $ts.themeForDarkMode }}</template>
<optgroup :label="$ts.darkThemes">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
@@ -86,12 +86,12 @@
<script lang="ts">
import { computed, defineComponent, onActivated, onMounted, ref, watch } from 'vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormSelect from '@client/components/form/select.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormButton from '@client/components/form/button.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormSelect from '@client/components/debobigego/select.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormButton from '@client/components/debobigego/button.vue';
import { builtinThemes } from '@client/scripts/theme';
import { selectFile } from '@client/scripts/select-file';
import { isDeviceDarkmode } from '@client/scripts/is-device-darkmode';
@@ -116,7 +116,8 @@ export default defineComponent({
setup(props, { emit }) {
const INFO = {
title: i18n.locale.theme,
- icon: 'fas fa-palette'
+ icon: 'fas fa-palette',
+ bg: 'var(--bg)',
};
const installedThemes = ref(getThemes());
diff --git a/src/client/pages/settings/update.vue b/src/client/pages/settings/update.vue
index 8000327d0c..8bc459e936 100644
--- a/src/client/pages/settings/update.vue
+++ b/src/client/pages/settings/update.vue
@@ -32,12 +32,12 @@
import { defineAsyncComponent, defineComponent } from 'vue';
import FormSwitch from '@client/components/form/switch.vue';
import FormSelect from '@client/components/form/select.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormInfo from '@client/components/form/info.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
import * as os from '@client/os';
import { version, instanceName } from '@client/config';
import * as symbols from '@client/symbols';
@@ -60,7 +60,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: 'Misskey Update',
- icon: 'fas fa-sync-alt'
+ icon: 'fas fa-sync-alt',
+ bg: 'var(--bg)',
},
version,
instanceName,
diff --git a/src/client/pages/settings/word-mute.vue b/src/client/pages/settings/word-mute.vue
index fe3fece844..53948b1b1e 100644
--- a/src/client/pages/settings/word-mute.vue
+++ b/src/client/pages/settings/word-mute.vue
@@ -1,21 +1,21 @@
<template>
<div>
- <MkTab v-model:value="tab">
+ <MkTab v-model="tab">
<option value="soft">{{ $ts._wordMute.soft }}</option>
<option value="hard">{{ $ts._wordMute.hard }}</option>
</MkTab>
<FormBase>
- <div class="_formItem">
+ <div class="_debobigegoItem">
<div v-show="tab === 'soft'">
<FormInfo>{{ $ts._wordMute.softDescription }}</FormInfo>
- <FormTextarea v-model:value="softMutedWords">
+ <FormTextarea v-model="softMutedWords">
<span>{{ $ts._wordMute.muteWords }}</span>
<template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
</FormTextarea>
</div>
<div v-show="tab === 'hard'">
<FormInfo>{{ $ts._wordMute.hardDescription }}</FormInfo>
- <FormTextarea v-model:value="hardMutedWords">
+ <FormTextarea v-model="hardMutedWords">
<span>{{ $ts._wordMute.muteWords }}</span>
<template #desc>{{ $ts._wordMute.muteWordsDescription }}<br>{{ $ts._wordMute.muteWordsDescription2 }}</template>
</FormTextarea>
@@ -33,10 +33,10 @@
<script lang="ts">
import { defineComponent } from 'vue';
import FormTextarea from '@client/components/form/textarea.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormInfo from '@client/components/form/info.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormInfo from '@client/components/debobigego/info.vue';
import MkTab from '@client/components/tab.vue';
import * as os from '@client/os';
import number from '@client/filters/number';
@@ -58,7 +58,8 @@ export default defineComponent({
return {
[symbols.PAGE_INFO]: {
title: this.$ts.wordMute,
- icon: 'fas fa-comment-slash'
+ icon: 'fas fa-comment-slash',
+ bg: 'var(--bg)',
},
tab: 'soft',
softMutedWords: '',
diff --git a/src/client/pages/signup-complete.vue b/src/client/pages/signup-complete.vue
new file mode 100644
index 0000000000..dada92031a
--- /dev/null
+++ b/src/client/pages/signup-complete.vue
@@ -0,0 +1,50 @@
+<template>
+<div>
+ {{ $ts.processing }}
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+import { login } from '@client/account';
+
+export default defineComponent({
+ components: {
+
+ },
+
+ props: {
+ code: {
+ type: String,
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ [symbols.PAGE_INFO]: {
+ title: this.$ts.signup,
+ icon: 'fas fa-user'
+ },
+ }
+ },
+
+ mounted() {
+ os.apiWithDialog('signup-pending', {
+ code: this.code,
+ }).then(res => {
+ login(res.i, '/');
+ });
+ },
+
+ methods: {
+
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/src/client/pages/test.vue b/src/client/pages/test.vue
index 131571e9dd..fbab0112ed 100644
--- a/src/client/pages/test.vue
+++ b/src/client/pages/test.vue
@@ -133,10 +133,10 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
-import MkSwitch from '@client/components/ui/switch.vue';
-import MkTextarea from '@client/components/ui/textarea.vue';
-import MkRadio from '@client/components/ui/radio.vue';
+import MkInput from '@client/components/form/input.vue';
+import MkSwitch from '@client/components/form/switch.vue';
+import MkTextarea from '@client/components/form/textarea.vue';
+import MkRadio from '@client/components/form/radio.vue';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
diff --git a/src/client/pages/theme-editor.vue b/src/client/pages/theme-editor.vue
index ce8bae4ff5..3b10396ab8 100644
--- a/src/client/pages/theme-editor.vue
+++ b/src/client/pages/theme-editor.vue
@@ -1,8 +1,8 @@
<template>
<FormBase class="cwepdizn">
- <div class="_formItem colorPicker">
- <div class="_formLabel">{{ $ts.backgroundColor }}</div>
- <div class="_formPanel colors">
+ <div class="_debobigegoItem colorPicker">
+ <div class="_debobigegoLabel">{{ $ts.backgroundColor }}</div>
+ <div class="_debobigegoPanel colors">
<div class="row">
<button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" @click="setBgColor(color)" class="color _button" :class="{ active: theme.props.bg === color.color }">
<div class="preview" :style="{ background: color.forPreview }"></div>
@@ -15,9 +15,9 @@
</div>
</div>
</div>
- <div class="_formItem colorPicker">
- <div class="_formLabel">{{ $ts.accentColor }}</div>
- <div class="_formPanel colors">
+ <div class="_debobigegoItem colorPicker">
+ <div class="_debobigegoLabel">{{ $ts.accentColor }}</div>
+ <div class="_debobigegoPanel colors">
<div class="row">
<button v-for="color in accentColors" :key="color" @click="setAccentColor(color)" class="color rounded _button" :class="{ active: theme.props.accent === color }">
<div class="preview" :style="{ background: color }"></div>
@@ -25,9 +25,9 @@
</div>
</div>
</div>
- <div class="_formItem colorPicker">
- <div class="_formLabel">{{ $ts.textColor }}</div>
- <div class="_formPanel colors">
+ <div class="_debobigegoItem colorPicker">
+ <div class="_debobigegoLabel">{{ $ts.textColor }}</div>
+ <div class="_debobigegoPanel colors">
<div class="row">
<button v-for="color in fgColors" :key="color" @click="setFgColor(color)" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }">
<div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
@@ -37,7 +37,7 @@
</div>
<FormGroup v-if="codeEnabled">
- <FormTextarea v-model:value="themeCode" tall>
+ <FormTextarea v-model="themeCode" tall>
<span>{{ $ts._theme.code }}</span>
</FormTextarea>
<FormButton @click="applyThemeCode" primary>{{ $ts.apply }}</FormButton>
@@ -45,7 +45,7 @@
<FormButton v-else @click="codeEnabled = true"><i class="fas fa-code"></i> {{ $ts.editCode }}</FormButton>
<FormGroup v-if="descriptionEnabled">
- <FormTextarea v-model:value="description">
+ <FormTextarea v-model="description">
<span>{{ $ts._theme.description }}</span>
</FormTextarea>
</FormGroup>
@@ -65,10 +65,10 @@ import * as tinycolor from 'tinycolor2';
import { v4 as uuid} from 'uuid';
import * as JSON5 from 'json5';
-import FormBase from '@client/components/form/base.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormGroup from '@client/components/form/group.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
import { Theme, applyTheme, validateTheme, darkTheme, lightTheme } from '@client/scripts/theme';
import { host } from '@client/config';
diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue
index 9dda82462d..dfabcbf84b 100644
--- a/src/client/pages/timeline.vue
+++ b/src/client/pages/timeline.vue
@@ -1,18 +1,21 @@
<template>
-<div class="cmuxhskf" v-hotkey.global="keymap" v-size="{ min: [800] }">
- <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
- <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
+<div v-hotkey.global="keymap">
+ <MkHeader :info="header"/>
+ <div class="cmuxhskf" v-size="{ min: [800] }">
+ <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
+ <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
- <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
- <div class="tl _block">
- <XTimeline ref="tl" class="tl"
- :key="src"
- :src="src"
- :sound="true"
- @before="before()"
- @after="after()"
- @queue="queueUpdated"
- />
+ <div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
+ <div class="tl _block">
+ <XTimeline ref="tl" class="tl"
+ :key="src"
+ :src="src"
+ :sound="true"
+ @before="before()"
+ @after="after()"
+ @queue="queueUpdated"
+ />
+ </div>
</div>
</div>
</template>
@@ -43,6 +46,11 @@ export default defineComponent({
title: this.$ts.timeline,
icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home',
bg: 'var(--bg)',
+ })),
+ header: computed(() => ({
+ title: this.$ts.timeline,
+ icon: this.src === 'local' ? 'fas fa-comments' : this.src === 'social' ? 'fas fa-share-alt' : this.src === 'global' ? 'fas fa-globe' : 'fas fa-home',
+ bg: 'var(--bg)',
actions: [{
icon: 'fas fa-list-ul',
text: this.$ts.lists,
@@ -129,7 +137,7 @@ export default defineComponent({
},
top() {
- scroll(this.$el, 0);
+ scroll(this.$el, { top: 0 });
},
async chooseList(ev) {
@@ -207,6 +215,10 @@ export default defineComponent({
}
}
+ > .post-form {
+ border-radius: var(--radius);
+ }
+
> .tl {
background: var(--bg);
border-radius: var(--radius);
diff --git a/src/client/pages/user-ap-info.vue b/src/client/pages/user-ap-info.vue
index c08a352571..cbdff874ed 100644
--- a/src/client/pages/user-ap-info.vue
+++ b/src/client/pages/user-ap-info.vue
@@ -58,14 +58,14 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import FormObjectView from '@client/components/form/object-view.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormObjectView from '@client/components/debobigego/object-view.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import number from '@client/filters/number';
import bytes from '@client/filters/bytes';
diff --git a/src/client/pages/user-info.vue b/src/client/pages/user-info.vue
index 503982652b..bf67fc853a 100644
--- a/src/client/pages/user-info.vue
+++ b/src/client/pages/user-info.vue
@@ -1,7 +1,7 @@
<template>
<FormBase>
<FormSuspense :p="init">
- <div class="_formItem aeakzknw">
+ <div class="_debobigegoItem aeakzknw">
<MkAvatar class="avatar" :user="user" :show-indicator="true"/>
</div>
@@ -20,9 +20,9 @@
</FormGroup>
<FormGroup v-if="iAmModerator">
- <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:value="toggleModerator" v-model:value="moderator">{{ $ts.moderator }}</FormSwitch>
- <FormSwitch @update:value="toggleSilence" v-model:value="silenced">{{ $ts.silence }}</FormSwitch>
- <FormSwitch @update:value="toggleSuspend" v-model:value="suspended">{{ $ts.suspend }}</FormSwitch>
+ <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" @update:modelValue="toggleModerator" v-model="moderator">{{ $ts.moderator }}</FormSwitch>
+ <FormSwitch @update:modelValue="toggleSilence" v-model="silenced">{{ $ts.silence }}</FormSwitch>
+ <FormSwitch @update:modelValue="toggleSuspend" v-model="suspended">{{ $ts.suspend }}</FormSwitch>
</FormGroup>
<FormGroup>
@@ -56,15 +56,15 @@
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent } from 'vue';
-import FormObjectView from '@client/components/form/object-view.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormLink from '@client/components/form/link.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import FormButton from '@client/components/form/button.vue';
-import FormKeyValueView from '@client/components/form/key-value-view.vue';
-import FormSuspense from '@client/components/form/suspense.vue';
+import FormObjectView from '@client/components/debobigego/object-view.vue';
+import FormTextarea from '@client/components/debobigego/textarea.vue';
+import FormSwitch from '@client/components/debobigego/switch.vue';
+import FormLink from '@client/components/debobigego/link.vue';
+import FormBase from '@client/components/debobigego/base.vue';
+import FormGroup from '@client/components/debobigego/group.vue';
+import FormButton from '@client/components/debobigego/button.vue';
+import FormKeyValueView from '@client/components/debobigego/key-value-view.vue';
+import FormSuspense from '@client/components/debobigego/suspense.vue';
import * as os from '@client/os';
import number from '@client/filters/number';
import bytes from '@client/filters/bytes';
diff --git a/src/client/pages/user-list-timeline.vue b/src/client/pages/user-list-timeline.vue
index 491fe948c1..b5e37d4843 100644
--- a/src/client/pages/user-list-timeline.vue
+++ b/src/client/pages/user-list-timeline.vue
@@ -89,7 +89,7 @@ export default defineComponent({
},
top() {
- scroll(this.$el, 0);
+ scroll(this.$el, { top: 0 });
},
settings() {
diff --git a/src/client/pages/user/clips.vue b/src/client/pages/user/clips.vue
index fc40d583c6..53ee554383 100644
--- a/src/client/pages/user/clips.vue
+++ b/src/client/pages/user/clips.vue
@@ -12,7 +12,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
export default defineComponent({
components: {
@@ -43,12 +42,6 @@ export default defineComponent({
this.$refs.list.reload();
}
},
-
- methods: {
- userPage,
-
- acct
- }
});
</script>
diff --git a/src/client/pages/user/follow-list.vue b/src/client/pages/user/follow-list.vue
index f6df28309f..1f5ab5993c 100644
--- a/src/client/pages/user/follow-list.vue
+++ b/src/client/pages/user/follow-list.vue
@@ -12,7 +12,6 @@
import { defineComponent } from 'vue';
import MkUserInfo from '@client/components/user-info.vue';
import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
export default defineComponent({
components: {
@@ -51,12 +50,6 @@ export default defineComponent({
user() {
this.$refs.list.reload();
}
- },
-
- methods: {
- userPage,
-
- acct
}
});
</script>
diff --git a/src/client/pages/user/gallery.vue b/src/client/pages/user/gallery.vue
index 67a5fac109..c21b3e6428 100644
--- a/src/client/pages/user/gallery.vue
+++ b/src/client/pages/user/gallery.vue
@@ -12,7 +12,6 @@
import { defineComponent } from 'vue';
import MkGalleryPostPreview from '@client/components/gallery-post-preview.vue';
import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
export default defineComponent({
components: {
@@ -43,12 +42,6 @@ export default defineComponent({
user() {
this.$refs.list.reload();
}
- },
-
- methods: {
- userPage,
-
- acct
}
});
</script>
diff --git a/src/client/pages/user/index.timeline.vue b/src/client/pages/user/index.timeline.vue
index 287e6c8b22..c3444f26f6 100644
--- a/src/client/pages/user/index.timeline.vue
+++ b/src/client/pages/user/index.timeline.vue
@@ -1,6 +1,6 @@
<template>
<div class="yrzkoczt" v-sticky-container>
- <MkTab v-model:value="with_" class="_gap tab">
+ <MkTab v-model="with_" class="tab">
<option :value="null">{{ $ts.notes }}</option>
<option value="replies">{{ $ts.notesAndReplies }}</option>
<option value="files">{{ $ts.withFiles }}</option>
@@ -60,6 +60,8 @@ export default defineComponent({
<style lang="scss" scoped>
.yrzkoczt {
> .tab {
+ margin: calc(var(--margin) / 2) 0;
+ padding: calc(var(--margin) / 2) 0;
background: var(--bg);
}
}
diff --git a/src/client/pages/user/index.vue b/src/client/pages/user/index.vue
index 86dc7361b5..0ddf73d572 100644
--- a/src/client/pages/user/index.vue
+++ b/src/client/pages/user/index.vue
@@ -1,98 +1,117 @@
<template>
-<transition name="fade" mode="out-in">
- <div class="ftskorzw wide" v-if="user && narrow === false">
- <MkRemoteCaution v-if="user.host != null" :href="user.url"/>
+<div>
+ <MkHeader :info="header"/>
+ <transition name="fade" mode="out-in">
+ <div class="ftskorzw wide" v-if="user && narrow === false">
+ <MkRemoteCaution v-if="user.host != null" :href="user.url"/>
- <div class="banner-container" :style="style">
- <div class="banner" ref="banner" :style="style"></div>
- </div>
- <div class="contents">
- <div class="side _forceContainerFull_">
- <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
- <div class="name">
- <MkUserName :user="user" :nowrap="false" class="name"/>
- <MkAcct :user="user" :detail="true" class="acct"/>
- </div>
- <div class="followed" v-if="$i && $i.id != user.id && user.isFollowed"><span>{{ $ts.followsYou }}</span></div>
- <div class="status">
- <MkA :to="userPage(user)" :class="{ active: page === 'index' }">
- <b>{{ number(user.notesCount) }}</b>
- <span>{{ $ts.notes }}</span>
- </MkA>
- <MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
- <b>{{ number(user.followingCount) }}</b>
- <span>{{ $ts.following }}</span>
- </MkA>
- <MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
- <b>{{ number(user.followersCount) }}</b>
- <span>{{ $ts.followers }}</span>
- </MkA>
- </div>
- <div class="description">
- <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
- <p v-else class="empty">{{ $ts.noAccountDescription }}</p>
- </div>
- <div class="fields system">
- <dl class="field" v-if="user.location">
- <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
- <dd class="value">{{ user.location }}</dd>
- </dl>
- <dl class="field" v-if="user.birthday">
- <dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
- <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
- </dl>
- <dl class="field">
- <dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.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="$i" :custom-emojis="user.emojis" :colored="false"/>
- </dd>
- </dl>
- </div>
- <XActivity :user="user" :key="user.id" class="_gap"/>
- <XPhotos :user="user" :key="user.id" class="_gap"/>
+ <div class="banner-container" :style="style">
+ <div class="banner" ref="banner" :style="style"></div>
</div>
- <div class="main">
- <div class="actions">
- <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
- <MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
- </div>
- <template v-if="page === 'index'">
- <div v-if="user.pinnedNotes.length > 0" class="_gap">
- <XNote v-for="note in user.pinnedNotes" class="note _gap" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
+ <div class="contents">
+ <div class="side _forceContainerFull_">
+ <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
+ <div class="name">
+ <MkUserName :user="user" :nowrap="false" class="name"/>
+ <MkAcct :user="user" :detail="true" class="acct"/>
</div>
- <div class="_gap">
- <XUserTimeline :user="user"/>
+ <div class="followed" v-if="$i && $i.id != user.id && user.isFollowed"><span>{{ $ts.followsYou }}</span></div>
+ <div class="status">
+ <MkA :to="userPage(user)" :class="{ active: page === 'index' }">
+ <b>{{ number(user.notesCount) }}</b>
+ <span>{{ $ts.notes }}</span>
+ </MkA>
+ <MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
+ <b>{{ number(user.followingCount) }}</b>
+ <span>{{ $ts.following }}</span>
+ </MkA>
+ <MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
+ <b>{{ number(user.followersCount) }}</b>
+ <span>{{ $ts.followers }}</span>
+ </MkA>
</div>
- </template>
- <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_gap"/>
- <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_gap"/>
- <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
- <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
+ <div class="description">
+ <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+ <p v-else class="empty">{{ $ts.noAccountDescription }}</p>
+ </div>
+ <div class="fields system">
+ <dl class="field" v-if="user.location">
+ <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
+ <dd class="value">{{ user.location }}</dd>
+ </dl>
+ <dl class="field" v-if="user.birthday">
+ <dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
+ <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+ </dl>
+ <dl class="field">
+ <dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.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="$i" :custom-emojis="user.emojis" :colored="false"/>
+ </dd>
+ </dl>
+ </div>
+ <XActivity :user="user" :key="user.id" class="_gap"/>
+ <XPhotos :user="user" :key="user.id" class="_gap"/>
+ </div>
+ <div class="main">
+ <div class="actions">
+ <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
+ <MkFollowButton v-if="!$i || $i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
+ </div>
+ <template v-if="page === 'index'">
+ <div v-if="user.pinnedNotes.length > 0" class="_gap">
+ <XNote v-for="note in user.pinnedNotes" class="note _gap" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
+ </div>
+ <div class="_gap">
+ <XUserTimeline :user="user"/>
+ </div>
+ </template>
+ <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_gap"/>
+ <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_gap"/>
+ <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
+ <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
+ </div>
</div>
</div>
- </div>
- <div class="ftskorzw narrow _root" v-else-if="user && narrow === true" v-size="{ max: [500] }">
- <!-- TODO -->
- <!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
- <!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
+ <div class="ftskorzw narrow _root" v-else-if="user && narrow === true" v-size="{ max: [500] }">
+ <!-- TODO -->
+ <!-- <div class="punished" v-if="user.isSuspended"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSuspended }}</div> -->
+ <!-- <div class="punished" v-if="user.isSilenced"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i> {{ $ts.userSilenced }}</div> -->
- <div class="profile">
- <MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
+ <div class="profile">
+ <MkRemoteCaution v-if="user.host != null" :href="user.url" class="warn"/>
- <div class="_block main" :key="user.id">
- <div class="banner-container" :style="style">
- <div class="banner" ref="banner" :style="style"></div>
- <div class="fade"></div>
+ <div class="_block main" :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="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
+ <span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
+ <span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
+ <span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+ </div>
+ </div>
+ <span class="followed" v-if="$i && $i.id != user.id && user.isFollowed">{{ $ts.followsYou }}</span>
+ <div class="actions" v-if="$i">
+ <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
+ <MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+ </div>
+ </div>
+ <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="true"/>
<div class="title">
- <MkUserName class="name" :user="user" :nowrap="true"/>
+ <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="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
@@ -101,92 +120,76 @@
<span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
</div>
</div>
- <span class="followed" v-if="$i && $i.id != user.id && user.isFollowed">{{ $ts.followsYou }}</span>
- <div class="actions" v-if="$i">
- <button @click="menu" class="menu _button"><i class="fas fa-ellipsis-h"></i></button>
- <MkFollowButton v-if="$i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
+ <div class="description">
+ <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
+ <p v-else class="empty">{{ $ts.noAccountDescription }}</p>
</div>
- </div>
- <MkAvatar class="avatar" :user="user" :disable-preview="true" :show-indicator="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="$ts.isAdmin" style="color: var(--badge);"><i class="fas fa-bookmark"></i></span>
- <span v-if="!user.isAdmin && user.isModerator" :title="$ts.isModerator" style="color: var(--badge);"><i class="far fa-bookmark"></i></span>
- <span v-if="user.isLocked" :title="$ts.isLocked"><i class="fas fa-lock"></i></span>
- <span v-if="user.isBot" :title="$ts.isBot"><i class="fas fa-robot"></i></span>
+ <div class="fields system">
+ <dl class="field" v-if="user.location">
+ <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
+ <dd class="value">{{ user.location }}</dd>
+ </dl>
+ <dl class="field" v-if="user.birthday">
+ <dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
+ <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
+ </dl>
+ <dl class="field">
+ <dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.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="$i" :custom-emojis="user.emojis" :colored="false"/>
+ </dd>
+ </dl>
+ </div>
+ <div class="status">
+ <MkA :to="userPage(user)" :class="{ active: page === 'index' }" v-click-anime>
+ <b>{{ number(user.notesCount) }}</b>
+ <span>{{ $ts.notes }}</span>
+ </MkA>
+ <MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }" v-click-anime>
+ <b>{{ number(user.followingCount) }}</b>
+ <span>{{ $ts.following }}</span>
+ </MkA>
+ <MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }" v-click-anime>
+ <b>{{ number(user.followersCount) }}</b>
+ <span>{{ $ts.followers }}</span>
+ </MkA>
</div>
- </div>
- <div class="description">
- <Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$i" :custom-emojis="user.emojis"/>
- <p v-else class="empty">{{ $ts.noAccountDescription }}</p>
- </div>
- <div class="fields system">
- <dl class="field" v-if="user.location">
- <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
- <dd class="value">{{ user.location }}</dd>
- </dl>
- <dl class="field" v-if="user.birthday">
- <dt class="name"><i class="fas fa-birthday-cake fa-fw"></i> {{ $ts.birthday }}</dt>
- <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
- </dl>
- <dl class="field">
- <dt class="name"><i class="fas fa-calendar-alt fa-fw"></i> {{ $ts.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="$i" :custom-emojis="user.emojis" :colored="false"/>
- </dd>
- </dl>
- </div>
- <div class="status">
- <MkA :to="userPage(user)" :class="{ active: page === 'index' }" v-click-anime>
- <b>{{ number(user.notesCount) }}</b>
- <span>{{ $ts.notes }}</span>
- </MkA>
- <MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }" v-click-anime>
- <b>{{ number(user.followingCount) }}</b>
- <span>{{ $ts.following }}</span>
- </MkA>
- <MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }" v-click-anime>
- <b>{{ number(user.followersCount) }}</b>
- <span>{{ $ts.followers }}</span>
- </MkA>
</div>
</div>
- </div>
- <div class="contents">
- <template v-if="page === 'index'">
- <div>
- <div v-if="user.pinnedNotes.length > 0" class="_gap">
- <XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
+ <div class="contents">
+ <template v-if="page === 'index'">
+ <div>
+ <div v-if="user.pinnedNotes.length > 0" class="_gap">
+ <XNote v-for="note in user.pinnedNotes" class="note _block" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :pinned="true"/>
+ </div>
+ <MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
+ <XPhotos :user="user" :key="user.id"/>
+ <XActivity :user="user" :key="user.id"/>
</div>
- <MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
- <XPhotos :user="user" :key="user.id"/>
- <XActivity :user="user" :key="user.id"/>
- </div>
- <div>
- <XUserTimeline :user="user"/>
- </div>
- </template>
- <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
- <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
- <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
- <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
- <XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
+ <div>
+ <XUserTimeline :user="user"/>
+ </div>
+ </template>
+ <XFollowList v-else-if="page === 'following'" type="following" :user="user" class="_content _gap"/>
+ <XFollowList v-else-if="page === 'followers'" type="followers" :user="user" class="_content _gap"/>
+ <XClips v-else-if="page === 'clips'" :user="user" class="_gap"/>
+ <XPages v-else-if="page === 'pages'" :user="user" class="_gap"/>
+ <XGallery v-else-if="page === 'gallery'" :user="user" class="_gap"/>
+ </div>
</div>
- </div>
- <MkError v-else-if="error" @retry="fetch()"/>
- <MkLoading v-else/>
-</transition>
+ <MkError v-else-if="error" @retry="fetch()"/>
+ <MkLoading v-else/>
+ </transition>
+</div>
</template>
<script lang="ts">
@@ -242,6 +245,15 @@ export default defineComponent({
data() {
return {
[symbols.PAGE_INFO]: computed(() => this.user ? {
+ icon: 'fas fa-user',
+ title: this.user.name ? `${this.user.name} (@${this.user.username})` : `@${this.user.username}`,
+ path: `/@${this.user.username}`,
+ share: {
+ title: this.user.name,
+ },
+ bg: 'var(--bg)',
+ } : null),
+ header: computed(() => this.user ? {
title: this.user.name ? `${this.user.name} (@${this.user.username})` : `@${this.user.username}`,
subtitle: `@${getAcct(this.user)}`,
userName: this.user,
@@ -255,21 +267,22 @@ export default defineComponent({
active: this.page === 'index',
title: this.$ts.overview,
icon: 'fas fa-home',
+ onClick: () => { this.$router.push('/@' + getAcct(this.user)); },
}, {
active: this.page === 'clips',
title: this.$ts.clips,
icon: 'fas fa-paperclip',
- onClick: () => { this.page = 'clips'; },
+ onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/clips'); },
}, {
active: this.page === 'pages',
title: this.$ts.pages,
icon: 'fas fa-file-alt',
- onClick: () => { this.page = 'pages'; },
+ onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/pages'); },
}, {
active: this.page === 'gallery',
title: this.$ts.gallery,
icon: 'fas fa-icons',
- onClick: () => { this.page = 'gallery'; },
+ onClick: () => { this.$router.push('/@' + getAcct(this.user) + '/gallery'); },
}]
} : null),
user: null,
@@ -814,7 +827,7 @@ export default defineComponent({
}
}
-._flat_ .ftskorzw.narrow {
+._fitSide_ .ftskorzw.narrow {
> .profile {
> .warn {
margin: 0;
diff --git a/src/client/pages/user/pages.vue b/src/client/pages/user/pages.vue
index 819bd9f2ef..ece418cf62 100644
--- a/src/client/pages/user/pages.vue
+++ b/src/client/pages/user/pages.vue
@@ -10,7 +10,6 @@
import { defineComponent } from 'vue';
import MkPagePreview from '@client/components/page-preview.vue';
import MkPagination from '@client/components/ui/pagination.vue';
-import { userPage, acct } from '@client/filters/user';
export default defineComponent({
components: {
@@ -41,12 +40,6 @@ export default defineComponent({
user() {
this.$refs.list.reload();
}
- },
-
- methods: {
- userPage,
-
- acct
}
});
</script>
diff --git a/src/client/pages/welcome.entrance.a.vue b/src/client/pages/welcome.entrance.a.vue
index 82b439ddd3..13f0993793 100644
--- a/src/client/pages/welcome.entrance.a.vue
+++ b/src/client/pages/welcome.entrance.a.vue
@@ -27,7 +27,7 @@
<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
</div>
<div class="action">
- <MkButton @click="signup()" inline primary data-cy-signup>{{ $ts.signup }}</MkButton>
+ <MkButton @click="signup()" inline gradate data-cy-signup style="margin-right: 12px;">{{ $ts.signup }}</MkButton>
<MkButton @click="signin()" inline data-cy-signin>{{ $ts.login }}</MkButton>
</div>
<div class="status" v-if="onlineUsersCount && stats">
diff --git a/src/client/pages/welcome.entrance.b.vue b/src/client/pages/welcome.entrance.b.vue
index a5c12f09e2..163fc1e35f 100644
--- a/src/client/pages/welcome.entrance.b.vue
+++ b/src/client/pages/welcome.entrance.b.vue
@@ -12,7 +12,7 @@
<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
</div>
<div class="action">
- <MkButton class="signup" @click="signup()" inline primary>{{ $ts.signup }}</MkButton>
+ <MkButton class="signup" @click="signup()" inline gradate>{{ $ts.signup }}</MkButton>
<MkButton class="signin" @click="signin()" inline>{{ $ts.login }}</MkButton>
</div>
<div class="status" v-if="onlineUsersCount && stats">
diff --git a/src/client/pages/welcome.entrance.c.vue b/src/client/pages/welcome.entrance.c.vue
index 2c8db6e264..bf1c9b1998 100644
--- a/src/client/pages/welcome.entrance.c.vue
+++ b/src/client/pages/welcome.entrance.c.vue
@@ -24,7 +24,7 @@
<div class="desc" v-html="meta.description || $ts.headlineMisskey"></div>
</div>
<div class="action">
- <MkButton @click="signup()" inline primary>{{ $ts.signup }}</MkButton>
+ <MkButton @click="signup()" inline gradate>{{ $ts.signup }}</MkButton>
<MkButton @click="signin()" inline>{{ $ts.login }}</MkButton>
</div>
<div class="status" v-if="onlineUsersCount && stats">
diff --git a/src/client/pages/welcome.setup.vue b/src/client/pages/welcome.setup.vue
index d0091bef67..dfefecc8fa 100644
--- a/src/client/pages/welcome.setup.vue
+++ b/src/client/pages/welcome.setup.vue
@@ -24,7 +24,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@client/components/ui/button.vue';
-import MkInput from '@client/components/ui/input.vue';
+import MkInput from '@client/components/form/input.vue';
import { host } from '@client/config';
import * as os from '@client/os';
import { login } from '@client/account';
diff --git a/src/client/router.ts b/src/client/router.ts
index 573f285c79..56dc948669 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -23,6 +23,7 @@ const defaultRoutes = [
{ path: '/@:acct/room', props: true, component: page('room/room') },
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
+ { path: '/signup-complete/:code', component: page('signup-complete'), props: route => ({ code: route.params.code }) },
{ path: '/announcements', component: page('announcements') },
{ path: '/about', component: page('about') },
{ path: '/about-misskey', component: page('about-misskey') },
diff --git a/src/client/scripts/autocomplete.ts b/src/client/scripts/autocomplete.ts
index 924d6a62ee..c0c33b2c7e 100644
--- a/src/client/scripts/autocomplete.ts
+++ b/src/client/scripts/autocomplete.ts
@@ -7,9 +7,9 @@ export class Autocomplete {
private suggestion: {
x: Ref<number>;
y: Ref<number>;
- q: Ref<string>;
+ q: Ref<string | null>;
close: Function;
- };
+ } | null;
private textarea: any;
private vm: any;
private currentType: string;
@@ -70,11 +70,13 @@ export class Autocomplete {
const mentionIndex = text.lastIndexOf('@');
const hashtagIndex = text.lastIndexOf('#');
const emojiIndex = text.lastIndexOf(':');
+ const mfmTagIndex = text.lastIndexOf('$');
const max = Math.max(
mentionIndex,
hashtagIndex,
- emojiIndex);
+ emojiIndex,
+ mfmTagIndex);
if (max == -1) {
this.close();
@@ -83,6 +85,7 @@ export class Autocomplete {
const isMention = mentionIndex != -1;
const isHashtag = hashtagIndex != -1;
+ const isMfmTag = mfmTagIndex != -1;
const isEmoji = emojiIndex != -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
let opened = false;
@@ -114,6 +117,14 @@ export class Autocomplete {
}
}
+ if (isMfmTag && !opened) {
+ const mfmTag = text.substr(mfmTagIndex + 1);
+ if (!mfmTag.includes(' ')) {
+ this.open('mfmTag', mfmTag.replace('[', ''));
+ opened = true;
+ }
+ }
+
if (!opened) {
this.close();
}
@@ -122,7 +133,7 @@ export class Autocomplete {
/**
* サジェストを提示します。
*/
- private async open(type: string, q: string) {
+ private async open(type: string, q: string | null) {
if (type != this.currentType) {
this.close();
}
@@ -244,6 +255,22 @@ export class Autocomplete {
const pos = trimmedBefore.length + value.length;
this.textarea.setSelectionRange(pos, pos);
});
+ } else if (type == 'mfmTag') {
+ const source = this.text;
+
+ const before = source.substr(0, caret);
+ const trimmedBefore = before.substring(0, before.lastIndexOf('$'));
+ const after = source.substr(caret);
+
+ // 挿入
+ this.text = `${trimmedBefore}$[${value} ]${after}`;
+
+ // キャレットを戻す
+ this.vm.$nextTick(() => {
+ this.textarea.focus();
+ const pos = trimmedBefore.length + (value.length + 3);
+ this.textarea.setSelectionRange(pos, pos);
+ });
}
}
}
diff --git a/src/client/scripts/idb-proxy.ts b/src/client/scripts/idb-proxy.ts
index 21c4dcff65..5f76ae30bb 100644
--- a/src/client/scripts/idb-proxy.ts
+++ b/src/client/scripts/idb-proxy.ts
@@ -4,7 +4,6 @@ import {
get as iget,
set as iset,
del as idel,
- createStore,
} from 'idb-keyval';
const fallbackName = (key: string) => `idbfallback::${key}`;
@@ -13,9 +12,9 @@ let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true;
if (idbAvailable) {
try {
- await createStore('keyval-store', 'keyval');
+ await iset('idb-test', 'test');
} catch (e) {
- console.error('idb open error', e);
+ console.error('idb error', e);
idbAvailable = false;
}
}
diff --git a/src/client/scripts/physics.ts b/src/client/scripts/physics.ts
index 8e971d5844..445b6296eb 100644
--- a/src/client/scripts/physics.ts
+++ b/src/client/scripts/physics.ts
@@ -79,7 +79,7 @@ export function physics(container: HTMLElement) {
objEl.offsetWidth,
objEl.offsetHeight,
{
- chamfer: { radius: parseInt(style.borderRadius, 10) },
+ chamfer: { radius: parseInt(style.borderRadius || '0', 10) },
restitution: 0.5
}
);
diff --git a/src/client/scripts/scroll.ts b/src/client/scripts/scroll.ts
index bc6d1530c5..621fe88105 100644
--- a/src/client/scripts/scroll.ts
+++ b/src/client/scripts/scroll.ts
@@ -1,3 +1,5 @@
+type ScrollBehavior = 'auto' | 'smooth' | 'instant';
+
export function getScrollContainer(el: Element | null): Element | null {
if (el == null || el.tagName === 'BODY') return null;
const overflow = window.getComputedStyle(el).getPropertyValue('overflow');
@@ -45,21 +47,25 @@ export function onScrollBottom(el: Element, cb) {
container.addEventListener('scroll', onScroll, { passive: true });
}
-export function scroll(el: Element, top: number) {
+export function scroll(el: Element, options: {
+ top?: number;
+ left?: number;
+ behavior?: ScrollBehavior;
+}) {
const container = getScrollContainer(el);
if (container == null) {
- window.scroll({ top: top, behavior: 'instant' });
+ window.scroll(options);
} else {
- container.scrollTop = top;
+ container.scroll(options);
}
}
-export function scrollToTop(el: Element) {
- scroll(el, 0);
+export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
+ scroll(el, { top: 0, ...options });
}
-export function scrollToBottom(el: Element) {
- scroll(el, 99999); // TODO: ちゃんと計算する
+export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) {
+ scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する
}
export function isBottom(el: Element, asobi = 0) {
diff --git a/src/client/scripts/theme.ts b/src/client/scripts/theme.ts
index 3fb5666a72..e79d54fa6d 100644
--- a/src/client/scripts/theme.ts
+++ b/src/client/scripts/theme.ts
@@ -1,3 +1,4 @@
+import { globalEvents } from '@client/events';
import * as tinycolor from 'tinycolor2';
export type Theme = {
@@ -24,6 +25,7 @@ export const builtinThemes = [
require('@client/themes/d-persimmon.json5'),
require('@client/themes/d-astro.json5'),
require('@client/themes/d-future.json5'),
+ require('@client/themes/d-botanical.json5'),
require('@client/themes/d-black.json5'),
] as Theme[];
@@ -62,6 +64,9 @@ export function applyTheme(theme: Theme, persist = true) {
if (persist) {
localStorage.setItem('theme', JSON.stringify(props));
}
+
+ // 色計算など再度行えるようにクライアント全体に通知
+ globalEvents.emit('themeChanged');
}
function compile(theme: Theme): Record<string, string> {
@@ -87,6 +92,8 @@ function compile(theme: Theme): Record<string, string> {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
+ case 'hue': return color.spin(arg);
+ case 'saturate': return color.saturate(arg);
}
}
diff --git a/src/client/style.scss b/src/client/style.scss
index 0318013f60..d6bad5a24d 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -178,7 +178,7 @@ hr {
pointer-events: none;
}
- &:focus {
+ &:focus-visible {
outline: none;
}
@@ -202,6 +202,20 @@ hr {
}
}
+._buttonGradate {
+ @extend ._buttonPrimary;
+ color: var(--fgOnAccent);
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+
+ &:not(:disabled):hover {
+ background: linear-gradient(90deg, var(--X8), var(--X8));
+ }
+
+ &:not(:disabled):active {
+ background: linear-gradient(90deg, var(--X8), var(--X8));
+ }
+}
+
._help {
color: var(--accent);
cursor: help
@@ -366,7 +380,7 @@ hr {
}
}
-._flat_ {
+._fitSide_ {
--root-margin: 0px;
--baseContentWidth: 100%;
--panelBorder: none;
@@ -425,12 +439,18 @@ hr {
}
}
-._inputNoTopMargin {
- margin-top: 0 !important;
+._formBlock {
+ margin: 20px 0;
}
-._inputNoBottomMargin {
- margin-bottom: 0 !important;
+._formRoot {
+ > ._formBlock:first-child {
+ margin-top: 0;
+ }
+
+ > ._formBlock:last-child {
+ margin-bottom: 0;
+ }
}
._table {
@@ -503,7 +523,7 @@ hr {
padding: 5px;
}
-.prism-editor__textarea:focus {
+.prism-editor__textarea:focus-visible {
outline: none;
}
diff --git a/src/client/themes/_dark.json5 b/src/client/themes/_dark.json5
index e1d5779a80..d8be16f60a 100644
--- a/src/client/themes/_dark.json5
+++ b/src/client/themes/_dark.json5
@@ -51,11 +51,14 @@
infoFg: '#fff',
infoWarnBg: '#42321c',
infoWarnFg: '#ffbd3e',
+ switchBg: 'rgba(255, 255, 255, 0.15)',
cwBg: '#687390',
cwFg: '#393f4f',
cwHoverBg: '#707b97',
buttonBg: 'rgba(255, 255, 255, 0.05)',
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+ buttonGradateA: '@accent',
+ buttonGradateB: ':hue<20<@accent',
inputBorder: 'rgba(255, 255, 255, 0.1)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
diff --git a/src/client/themes/_light.json5 b/src/client/themes/_light.json5
index 87895e6406..251aa36c7a 100644
--- a/src/client/themes/_light.json5
+++ b/src/client/themes/_light.json5
@@ -51,11 +51,14 @@
infoFg: '#72818a',
infoWarnBg: '#fff0db',
infoWarnFg: '#8f6e31',
+ switchBg: 'rgba(0, 0, 0, 0.15)',
cwBg: '#b1b9c1',
cwFg: '#fff',
cwHoverBg: '#bbc4ce',
buttonBg: 'rgba(0, 0, 0, 0.05)',
buttonHoverBg: 'rgba(0, 0, 0, 0.1)',
+ buttonGradateA: '@accent',
+ buttonGradateB: ':hue<20<@accent',
inputBorder: 'rgba(0, 0, 0, 0.1)',
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
diff --git a/src/client/themes/d-astro.json5 b/src/client/themes/d-astro.json5
index 08846dec20..2350e3d46d 100644
--- a/src/client/themes/d-astro.json5
+++ b/src/client/themes/d-astro.json5
@@ -46,6 +46,8 @@
navIndicator: '@accent',
accentLighten: ':lighten<10<@accent',
buttonHoverBg: 'rgba(255, 255, 255, 0.1)',
+ buttonGradateA: '@accent',
+ buttonGradateB: ':hue<-20<@accent',
driveFolderBg: ':alpha<0.3<@accent',
fgHighlighted: ':lighten<3<@fg',
panelHeaderBg: ':lighten<3<@panel',
diff --git a/src/client/themes/d-botanical.json5 b/src/client/themes/d-botanical.json5
new file mode 100644
index 0000000000..c03b95e2d7
--- /dev/null
+++ b/src/client/themes/d-botanical.json5
@@ -0,0 +1,26 @@
+{
+ id: '504debaf-4912-6a4c-5059-1db08a76b737',
+
+ name: 'Mi Botanical Dark',
+ author: 'syuilo',
+
+ base: 'dark',
+
+ props: {
+ accent: 'rgb(148, 179, 0)',
+ bg: 'rgb(37, 38, 36)',
+ fg: 'rgb(216, 212, 199)',
+ fgHighlighted: '#fff',
+ divider: 'rgba(255, 255, 255, 0.14)',
+ panel: 'rgb(47, 47, 44)',
+ panelHeaderBg: '@panel',
+ panelHeaderDivider: '@divider',
+ header: ':alpha<0.7<@panel',
+ navBg: '#363636',
+ renote: '@accent',
+ mention: 'rgb(212, 153, 76)',
+ mentionMe: 'rgb(212, 210, 76)',
+ hashtag: '#5bcbb0',
+ link: '@accent',
+ },
+}
diff --git a/src/client/themes/d-future.json5 b/src/client/themes/d-future.json5
index 05ffe87bf0..1882609121 100644
--- a/src/client/themes/d-future.json5
+++ b/src/client/themes/d-future.json5
@@ -21,5 +21,7 @@
mentionMe: '@accent',
hashtag: '#70c0e8',
link: '#e88080',
+ buttonGradateA: '@accent',
+ buttonGradateB: ':saturate<30<:hue<30<@accent',
},
}
diff --git a/src/client/ui/_common_/header.vue b/src/client/ui/_common_/header.vue
deleted file mode 100644
index 1e0db9a3a1..0000000000
--- a/src/client/ui/_common_/header.vue
+++ /dev/null
@@ -1,302 +0,0 @@
-<template>
-<div class="fdidabkb" :class="{ slim: titleOnly || narrow }" :style="`--height:${height};`" :key="key">
- <transition :name="$store.state.animation ? 'header' : ''" mode="out-in" appear>
- <div class="buttons left" v-if="backButton">
- <button class="_button button back" @click.stop="$emit('back')" @touchstart="preventDrag" v-tooltip="$ts.goBack"><i class="fas fa-chevron-left"></i></button>
- </div>
- </transition>
- <template v-if="info">
- <div class="titleContainer" @click="showTabsPopup">
- <i v-if="info.icon" class="icon" :class="info.icon"></i>
- <MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true" :show-indicator="true"/>
-
- <div class="title">
- <MkUserName v-if="info.userName" :user="info.userName" :nowrap="false" class="title"/>
- <div v-else-if="info.title" class="title">{{ info.title }}</div>
- <div class="subtitle" v-if="!narrow && info.subtitle">
- {{ info.subtitle }}
- </div>
- <div class="subtitle activeTab" v-if="narrow && hasTabs">
- {{ info.tabs.find(tab => tab.active)?.title }}
- <i class="chevron fas fa-chevron-down"></i>
- </div>
- </div>
- </div>
- <div class="tabs" v-if="!narrow">
- <button class="tab _button" v-for="tab in info.tabs" :class="{ active: tab.active }" @click="tab.onClick" v-tooltip="tab.title">
- <i v-if="tab.icon" class="icon" :class="tab.icon"></i>
- <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
- </button>
- </div>
- </template>
- <div class="buttons right">
- <template v-if="info && info.actions && !narrow">
- <button v-for="action in info.actions" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag" v-tooltip="action.text"><i :class="action.icon"></i></button>
- </template>
- <button v-if="shouldShowMenu" class="_button button" @click.stop="showMenu" @touchstart="preventDrag" v-tooltip="$ts.menu"><i class="fas fa-ellipsis-h"></i></button>
- <button v-if="closeButton" class="_button button" @click.stop="$emit('close')" @touchstart="preventDrag" v-tooltip="$ts.close"><i class="fas fa-times"></i></button>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { popupMenu } from '@client/os';
-import { url } from '@client/config';
-
-export default defineComponent({
- props: {
- info: {
- required: true
- },
- menu: {
- required: false
- },
- backButton: {
- type: Boolean,
- required: false,
- default: false,
- },
- closeButton: {
- type: Boolean,
- required: false,
- default: false,
- },
- titleOnly: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- data() {
- return {
- narrow: false,
- height: 0,
- key: 0,
- };
- },
-
- computed: {
- hasTabs(): boolean {
- return this.info.tabs && this.info.tabs.length > 0;
- },
-
- shouldShowMenu() {
- if (this.info == null) return false;
- if (this.info.actions != null && this.narrow) return true;
- if (this.info.menu != null) return true;
- if (this.info.share != null) return true;
- if (this.menu != null) return true;
- return false;
- }
- },
-
- watch: {
- info() {
- this.key++;
- },
- },
-
- mounted() {
- this.height = this.$el.parentElement.offsetHeight + 'px';
- this.narrow = this.titleOnly || this.$el.parentElement.offsetWidth < 500;
- new ResizeObserver((entries, observer) => {
- this.height = this.$el.parentElement.offsetHeight + 'px';
- this.narrow = this.titleOnly || this.$el.parentElement.offsetWidth < 500;
- }).observe(this.$el);
- },
-
- methods: {
- share() {
- navigator.share({
- url: url + this.info.path,
- ...this.info.share,
- });
- },
-
- showMenu(ev) {
- let menu = this.info.menu ? this.info.menu() : [];
- if (this.narrow && this.info.actions) {
- menu = [...this.info.actions.map(x => ({
- text: x.text,
- icon: x.icon,
- action: x.handler
- })), menu.length > 0 ? null : undefined, ...menu];
- }
- if (this.info.share) {
- if (menu.length > 0) menu.push(null);
- menu.push({
- text: this.$ts.share,
- icon: 'fas fa-share-alt',
- action: this.share
- });
- }
- if (this.menu) {
- if (menu.length > 0) menu.push(null);
- menu = menu.concat(this.menu);
- }
- popupMenu(menu, ev.currentTarget || ev.target);
- },
-
- showTabsPopup(ev) {
- if (!this.hasTabs) return;
- ev.preventDefault();
- ev.stopPropagation();
- const menu = this.info.tabs.map(tab => ({
- text: tab.title,
- icon: tab.icon,
- action: tab.onClick,
- }));
- popupMenu(menu, ev.currentTarget || ev.target);
- },
-
- preventDrag(ev) {
- ev.stopPropagation();
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.fdidabkb {
- display: flex;
-
- &.slim {
- text-align: center;
-
- > .titleContainer {
- margin: 0 auto;
- }
-
- > .buttons {
- &.right {
- margin-left: 0;
- }
- }
- }
-
- > .buttons {
- --margin: 8px;
- display: flex;
- align-items: center;
- height: var(--height);
- margin: 0 var(--margin);
-
- &.right {
- margin-left: auto;
- }
-
- &:empty {
- width: var(--height);
- }
-
- > .button {
- display: flex;
- align-items: center;
- justify-content: center;
- height: calc(var(--height) - (var(--margin) * 2));
- width: calc(var(--height) - (var(--margin) * 2));
- box-sizing: border-box;
- position: relative;
- border-radius: 5px;
-
- &:hover {
- background: rgba(0, 0, 0, 0.05);
- }
-
- &.highlighted {
- color: var(--accent);
- }
- }
- }
-
- > .titleContainer {
- display: flex;
- align-items: center;
- overflow: auto;
- white-space: nowrap;
- text-align: left;
- font-weight: bold;
-
- > .avatar {
- $size: 32px;
- display: inline-block;
- width: $size;
- height: $size;
- vertical-align: bottom;
- margin: 0 8px;
- pointer-events: none;
- }
-
- > .icon {
- margin-right: 8px;
- }
-
- > .title {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- line-height: 1.1;
-
- > .subtitle {
- opacity: 0.6;
- font-size: 0.8em;
- font-weight: normal;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-
- &.activeTab {
- text-align: center;
-
- > .chevron {
- display: inline-block;
- margin-left: 6px;
- }
- }
- }
- }
- }
-
- > .tabs {
- margin-left: 16px;
- font-size: 0.8em;
-
- > .tab {
- display: inline-block;
- position: relative;
- padding: 0 10px;
- height: 100%;
- font-weight: normal;
- opacity: 0.7;
-
- &:hover {
- opacity: 1;
- }
-
- &.active {
- opacity: 1;
-
- &:after {
- content: "";
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- margin: 0 auto;
- width: 100%;
- height: 3px;
- background: var(--accent);
- }
- }
-
- > .icon + .title {
- margin-left: 8px;
- }
- }
- }
-}
-</style>
diff --git a/src/client/ui/_common_/sidebar.vue b/src/client/ui/_common_/sidebar.vue
index 9817a46e30..d00327b096 100644
--- a/src/client/ui/_common_/sidebar.vue
+++ b/src/client/ui/_common_/sidebar.vue
@@ -50,7 +50,7 @@ import { host } from '@client/config';
import { search } from '@client/scripts/search';
import * as os from '@client/os';
import { menuDef } from '@client/menu';
-import { getAccounts, addAccount, login } from '@client/account';
+import { openAccountMenu } from '@client/account';
export default defineComponent({
props: {
@@ -134,76 +134,12 @@ export default defineComponent({
search();
},
- async openAccountMenu(ev) {
- const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id));
- const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
-
- const accountItemPromises = storedAccounts.map(a => new Promise(res => {
- accountsPromise.then(accounts => {
- const account = accounts.find(x => x.id === a.id);
- if (account == null) return res(null);
- res({
- type: 'user',
- user: account,
- action: () => { this.switchAccount(account); }
- });
- });
- }));
-
- os.popupMenu([...[{
- type: 'link',
- text: this.$ts.profile,
- to: `/@${ this.$i.username }`,
- avatar: this.$i,
- }, null, ...accountItemPromises, {
- icon: 'fas fa-plus',
- text: this.$ts.addAccount,
- action: () => {
- os.popupMenu([{
- text: this.$ts.existingAccount,
- action: () => { this.addAccount(); },
- }, {
- text: this.$ts.createAccount,
- action: () => { this.createAccount(); },
- }], ev.currentTarget || ev.target);
- },
- }]], ev.currentTarget || ev.target, {
- align: 'left'
- });
- },
-
more(ev) {
os.popup(import('@client/components/launch-pad.vue'), {}, {
}, 'closed');
},
- addAccount() {
- os.popup(import('@client/components/signin-dialog.vue'), {}, {
- done: res => {
- addAccount(res.id, res.i);
- os.success();
- },
- }, 'closed');
- },
-
- createAccount() {
- os.popup(import('@client/components/signup-dialog.vue'), {}, {
- done: res => {
- addAccount(res.id, res.i);
- this.switchAccountWithToken(res.i);
- },
- }, 'closed');
- },
-
- async switchAccount(account: any) {
- const storedAccounts = await getAccounts();
- const token = storedAccounts.find(x => x.id === account.id).token;
- this.switchAccountWithToken(token);
- },
-
- switchAccountWithToken(token: string) {
- login(token);
- },
+ openAccountMenu,
}
});
</script>
@@ -395,7 +331,7 @@ export default defineComponent({
left: 0;
right: 0;
bottom: 0;
- border-radius: 8px;
+ border-radius: 999px;
background: var(--accentedBg);
}
}
@@ -436,7 +372,7 @@ export default defineComponent({
right: 0;
bottom: 0;
border-radius: 999px;
- background: var(--accent);
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
}
&:hover, &.active {
diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue
index e8275def81..4c068b0d94 100644
--- a/src/client/ui/chat/index.vue
+++ b/src/client/ui/chat/index.vue
@@ -74,7 +74,7 @@
<main class="main" @contextmenu.stop="onContextmenu">
<header class="header">
- <XHeader class="header" :info="pageInfo" :menu="menu" :center="false" :back-button="true" @back="back()" @click="onHeaderClick"/>
+ <MkHeader class="header" :info="pageInfo" :menu="menu" :center="false" @click="onHeaderClick"/>
</header>
<router-view v-slot="{ Component }">
<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
@@ -101,7 +101,6 @@ import XSidebar from '@client/ui/_common_/sidebar.vue';
import XWidgets from './widgets.vue';
import XCommon from '../_common_/common.vue';
import XSide from './side.vue';
-import XHeader from '../_common_/header.vue';
import XHeaderClock from './header-clock.vue';
import * as os from '@client/os';
import { router } from '@client/router';
@@ -110,6 +109,7 @@ import { search } from '@client/scripts/search';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { store } from './store';
import * as symbols from '@client/symbols';
+import { openAccountMenu } from '@client/account';
export default defineComponent({
components: {
@@ -117,7 +117,6 @@ export default defineComponent({
XSidebar,
XWidgets,
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
- XHeader,
XHeaderClock,
},
@@ -255,6 +254,8 @@ export default defineComponent({
}
}], e);
},
+
+ openAccountMenu,
}
});
</script>
diff --git a/src/client/ui/chat/note-preview.vue b/src/client/ui/chat/note-preview.vue
index 77949e314b..beb38de644 100644
--- a/src/client/ui/chat/note-preview.vue
+++ b/src/client/ui/chat/note-preview.vue
@@ -6,7 +6,7 @@
<div class="body">
<p v-if="note.cw != null" class="cw">
<span class="text" v-if="note.cw != ''">{{ note.cw }}</span>
- <XCwButton v-model:value="showContent" :note="note"/>
+ <XCwButton v-model="showContent" :note="note"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<XSubNote-content class="text" :note="note"/>
diff --git a/src/client/ui/chat/note.sub.vue b/src/client/ui/chat/note.sub.vue
index bb528dd936..a284ba2bf4 100644
--- a/src/client/ui/chat/note.sub.vue
+++ b/src/client/ui/chat/note.sub.vue
@@ -7,7 +7,7 @@
<div class="body">
<p v-if="note.cw != null" class="cw">
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis" />
- <XCwButton v-model:value="showContent" :note="note"/>
+ <XCwButton v-model="showContent" :note="note"/>
</p>
<div class="content" v-show="note.cw == null || showContent">
<XSubNote-content class="text" :note="note"/>
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index 6d2b9bbf54..0a054d1057 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -42,7 +42,7 @@
<div class="body">
<p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
- <XCwButton v-model:value="showContent" :note="appearNote"/>
+ <XCwButton v-model="showContent" :note="appearNote"/>
</p>
<div class="content" :class="{ collapsed }" v-show="appearNote.cw == null || showContent">
<div class="text">
@@ -56,7 +56,7 @@
</div>
<XPoll v-if="appearNote.poll" :note="appearNote" ref="pollViewer" class="poll"/>
<MkUrlPreview v-for="url in urls" :url="url" :key="url" :compact="true" :detail="false" class="url-preview"/>
- <div class="renote" v-if="appearNote.renote"><XNotePreview :note="appearNote.renote"/></div>
+ <div class="renote" v-if="appearNote.renote"><XNoteSimple :note="appearNote.renote"/></div>
<button v-if="collapsed" class="fade _button" @click="collapsed = false">
<span>{{ $ts.showMore }}</span>
</button>
@@ -106,7 +106,7 @@ import * as mfm from 'mfm-js';
import { sum } from '../../../prelude/array';
import XSub from './note.sub.vue';
import XNoteHeader from './note-header.vue';
-import XNotePreview from './note-preview.vue';
+import XNoteSimple from './note-preview.vue';
import XReactionsViewer from '@client/components/reactions-viewer.vue';
import XMediaList from '@client/components/media-list.vue';
import XCwButton from '@client/components/cw-button.vue';
@@ -126,7 +126,7 @@ export default defineComponent({
components: {
XSub,
XNoteHeader,
- XNotePreview,
+ XNoteSimple,
XReactionsViewer,
XMediaList,
XCwButton,
@@ -872,7 +872,7 @@ export default defineComponent({
//content-visibility: auto;
//contain-intrinsic-size: 0 128px;
- &:focus {
+ &:focus-visible {
outline: none;
}
diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue
index 0f9a206fab..0cacaf77e7 100644
--- a/src/client/ui/chat/post-form.vue
+++ b/src/client/ui/chat/post-form.vue
@@ -681,7 +681,7 @@ export default defineComponent({
color: var(--fg);
font-family: inherit;
- &:focus {
+ &:focus-visible {
outline: none;
}
diff --git a/src/client/ui/chat/side.vue b/src/client/ui/chat/side.vue
index ebf1cf9979..3fd0a0e77b 100644
--- a/src/client/ui/chat/side.vue
+++ b/src/client/ui/chat/side.vue
@@ -1,15 +1,14 @@
<template>
<div class="mrajymqm _narrow_" v-if="component">
<header class="header" @contextmenu.prevent.stop="onContextmenu">
- <XHeader class="title" :info="pageInfo" :center="false" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()"/>
+ <MkHeader class="title" :info="pageInfo" :center="false"/>
</header>
- <component :is="component" v-bind="props" :ref="changePage" class="body _flat_"/>
+ <component :is="component" v-bind="props" :ref="changePage" class="body _fitSide_"/>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import XHeader from '../_common_/header.vue';
import * as os from '@client/os';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { resolve } from '@client/router';
@@ -18,7 +17,6 @@ import * as symbols from '@client/symbols';
export default defineComponent({
components: {
- XHeader
},
provide() {
diff --git a/src/client/ui/deck/column.vue b/src/client/ui/deck/column.vue
index 842a6ff59f..c04297e384 100644
--- a/src/client/ui/deck/column.vue
+++ b/src/client/ui/deck/column.vue
@@ -37,6 +37,11 @@ import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownCo
import { deckStore } from './deck-store';
export default defineComponent({
+ provide: {
+ shouldHeaderThin: true,
+ shouldOmitHeaderTitle: true,
+ },
+
props: {
column: {
type: Object,
@@ -267,6 +272,7 @@ export default defineComponent({
height: 100%;
overflow: hidden;
contain: content;
+ box-shadow: 0 0 8px 0 var(--shadow);
&.draghover {
box-shadow: 0 0 0 2px var(--focus);
@@ -320,15 +326,6 @@ export default defineComponent({
&.paged {
background: var(--bg) !important;
-
- > header {
- background: transparent;
- box-shadow: none;
-
- > button {
- color: var(--fg);
- }
- }
}
> header {
@@ -365,7 +362,7 @@ export default defineComponent({
}
> .toggleActive,
- > .action > *,
+ > .action > ::v-deep(*),
> .menu {
z-index: 1;
width: var(--deckColumnHeaderHeight);
diff --git a/src/client/ui/deck/deck-store.ts b/src/client/ui/deck/deck-store.ts
index aa389d7610..6c61bf5539 100644
--- a/src/client/ui/deck/deck-store.ts
+++ b/src/client/ui/deck/deck-store.ts
@@ -219,10 +219,20 @@ export function stackLeftColumn(id: Column['id']) {
export function popRightColumn(id: Column['id']) {
let layout = copy(deckStore.state.layout);
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
+ const affected = layout[i];
layout = layout.map(ids => ids.filter(_id => _id !== id));
layout.splice(i + 1, 0, [id]);
layout = layout.filter(ids => ids.length > 0);
deckStore.set('layout', layout);
+
+ const columns = copy(deckStore.state.columns);
+ for (const column of columns) {
+ if (affected.includes(column.id)) {
+ column.active = true;
+ }
+ }
+ deckStore.set('columns', columns);
+
saveDeck();
}
diff --git a/src/client/ui/deck/main-column.vue b/src/client/ui/deck/main-column.vue
index 4c591022a5..baf88a9721 100644
--- a/src/client/ui/deck/main-column.vue
+++ b/src/client/ui/deck/main-column.vue
@@ -1,10 +1,13 @@
<template>
<XColumn v-if="deckStore.state.alwaysShowMainColumn || $route.name !== 'index'" :column="column" :is-stacked="isStacked">
<template #header>
- <XHeader :info="pageInfo" :back-button="true" @back="back()"/>
+ <template v-if="pageInfo">
+ <i :class="pageInfo.icon"></i>
+ {{ pageInfo.title }}
+ </template>
</template>
- <router-view v-slot="{ Component }" class="_flat_">
+ <router-view v-slot="{ Component }" class="_fitSide_">
<transition>
<keep-alive :include="['timeline']">
<component :is="Component" :ref="changePage" @contextmenu.stop="onContextmenu"/>
@@ -18,7 +21,6 @@
import { defineComponent } from 'vue';
import XColumn from './column.vue';
import XNotes from '@client/components/notes.vue';
-import XHeader from '@client/ui/_common_/header.vue';
import { deckStore } from '@client/ui/deck/deck-store';
import * as os from '@client/os';
import * as symbols from '@client/symbols';
@@ -26,7 +28,6 @@ import * as symbols from '@client/symbols';
export default defineComponent({
components: {
XColumn,
- XHeader,
XNotes
},
diff --git a/src/client/ui/default.header.vue b/src/client/ui/default.header.vue
index 6fbdd625c7..4f6363e82d 100644
--- a/src/client/ui/default.header.vue
+++ b/src/client/ui/default.header.vue
@@ -29,7 +29,7 @@
<MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/>
</button>
<div class="post" @click="post">
- <MkButton class="button" primary full>
+ <MkButton class="button" gradate full rounded>
<i class="fas fa-pencil-alt fa-fw"></i>
</MkButton>
</div>
@@ -44,7 +44,7 @@ import { host } from '@client/config';
import { search } from '@client/scripts/search';
import * as os from '@client/os';
import { menuDef } from '@client/menu';
-import { getAccounts, addAccount, login } from '@client/account';
+import { openAccountMenu } from '@client/account';
import MkButton from '@client/components/ui/button.vue';
export default defineComponent({
@@ -100,76 +100,12 @@ export default defineComponent({
search();
},
- async openAccountMenu(ev) {
- const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id));
- const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
-
- const accountItemPromises = storedAccounts.map(a => new Promise(res => {
- accountsPromise.then(accounts => {
- const account = accounts.find(x => x.id === a.id);
- if (account == null) return res(null);
- res({
- type: 'user',
- user: account,
- action: () => { this.switchAccount(account); }
- });
- });
- }));
-
- os.popupMenu([...[{
- type: 'link',
- text: this.$ts.profile,
- to: `/@${ this.$i.username }`,
- avatar: this.$i,
- }, null, ...accountItemPromises, {
- icon: 'fas fa-plus',
- text: this.$ts.addAccount,
- action: () => {
- os.popupMenu([{
- text: this.$ts.existingAccount,
- action: () => { this.addAccount(); },
- }, {
- text: this.$ts.createAccount,
- action: () => { this.createAccount(); },
- }], ev.currentTarget || ev.target);
- },
- }]], ev.currentTarget || ev.target, {
- align: 'left'
- });
- },
-
more(ev) {
os.popup(import('@client/components/launch-pad.vue'), {}, {
}, 'closed');
},
- addAccount() {
- os.popup(import('@client/components/signin-dialog.vue'), {}, {
- done: res => {
- addAccount(res.id, res.i);
- os.success();
- },
- }, 'closed');
- },
-
- createAccount() {
- os.popup(import('@client/components/signup-dialog.vue'), {}, {
- done: res => {
- addAccount(res.id, res.i);
- this.switchAccountWithToken(res.i);
- },
- }, 'closed');
- },
-
- async switchAccount(account: any) {
- const storedAccounts = await getAccounts();
- const token = storedAccounts.find(x => x.id === account.id).token;
- this.switchAccountWithToken(token);
- },
-
- switchAccountWithToken(token: string) {
- login(token);
- },
+ openAccountMenu,
}
});
</script>
diff --git a/src/client/ui/default.side.vue b/src/client/ui/default.side.vue
index 4d65779612..c7d2abff26 100644
--- a/src/client/ui/default.side.vue
+++ b/src/client/ui/default.side.vue
@@ -4,9 +4,10 @@
<header class="header" @contextmenu.prevent.stop="onContextmenu">
<button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
<button class="_button" style="pointer-events: none;" v-else><!-- マージンのバランスを取るためのダミー --></button>
- <XHeader class="title" :info="pageInfo" :back-button="false"/>
+ <span class="title">{{ pageInfo.title }}</span>
<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
</header>
+ <MkHeader class="pageHeader" :info="pageInfo"/>
<component :is="component" v-bind="props" :ref="changePage"/>
</div>
</div>
@@ -14,7 +15,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import XHeader from './_common_/header.vue';
import * as os from '@client/os';
import copyToClipboard from '@client/scripts/copy-to-clipboard';
import { resolve } from '@client/router';
@@ -22,10 +22,6 @@ import { url } from '@client/config';
import * as symbols from '@client/symbols';
export default defineComponent({
- components: {
- XHeader
- },
-
provide() {
return {
navHook: (path) => {
diff --git a/src/client/ui/default.sidebar.vue b/src/client/ui/default.sidebar.vue
index be907aa2a4..e36febb7fa 100644
--- a/src/client/ui/default.sidebar.vue
+++ b/src/client/ui/default.sidebar.vue
@@ -4,7 +4,7 @@
<MkAvatar :user="$i" class="avatar"/><MkAcct class="text" :user="$i"/>
</button>
<div class="post" @click="post" data-cy-open-post-form>
- <MkButton class="button" primary full>
+ <MkButton class="button" gradate full rounded>
<i class="fas fa-pencil-alt fa-fw"></i><span class="text" v-if="!iconOnly">{{ $ts.note }}</span>
</MkButton>
</div>
@@ -46,7 +46,7 @@ import { host } from '@client/config';
import { search } from '@client/scripts/search';
import * as os from '@client/os';
import { menuDef } from '@client/menu';
-import { getAccounts, addAccount, login } from '@client/account';
+import { openAccountMenu } from '@client/account';
import MkButton from '@client/components/ui/button.vue';
import { StickySidebar } from '@client/scripts/sticky-sidebar';
import MisskeyLogo from '@/../assets/client/misskey.svg';
@@ -120,76 +120,12 @@ export default defineComponent({
search();
},
- async openAccountMenu(ev) {
- const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== this.$i.id));
- const accountsPromise = os.api('users/show', { userIds: storedAccounts.map(x => x.id) });
-
- const accountItemPromises = storedAccounts.map(a => new Promise(res => {
- accountsPromise.then(accounts => {
- const account = accounts.find(x => x.id === a.id);
- if (account == null) return res(null);
- res({
- type: 'user',
- user: account,
- action: () => { this.switchAccount(account); }
- });
- });
- }));
-
- os.popupMenu([...[{
- type: 'link',
- text: this.$ts.profile,
- to: `/@${ this.$i.username }`,
- avatar: this.$i,
- }, null, ...accountItemPromises, {
- icon: 'fas fa-plus',
- text: this.$ts.addAccount,
- action: () => {
- os.popupMenu([{
- text: this.$ts.existingAccount,
- action: () => { this.addAccount(); },
- }, {
- text: this.$ts.createAccount,
- action: () => { this.createAccount(); },
- }], ev.currentTarget || ev.target);
- },
- }]], ev.currentTarget || ev.target, {
- align: 'left'
- });
- },
-
more(ev) {
os.popup(import('@client/components/launch-pad.vue'), {}, {
}, 'closed');
},
- addAccount() {
- os.popup(import('@client/components/signin-dialog.vue'), {}, {
- done: res => {
- addAccount(res.id, res.i);
- os.success();
- },
- }, 'closed');
- },
-
- createAccount() {
- os.popup(import('@client/components/signup-dialog.vue'), {}, {
- done: res => {
- addAccount(res.id, res.i);
- this.switchAccountWithToken(res.i);
- },
- }, 'closed');
- },
-
- async switchAccount(account: any) {
- const storedAccounts = await getAccounts();
- const token = storedAccounts.find(x => x.id === account.id).token;
- this.switchAccountWithToken(token);
- },
-
- switchAccountWithToken(token: string) {
- login(token);
- },
+ openAccountMenu,
}
});
</script>
diff --git a/src/client/ui/default.vue b/src/client/ui/default.vue
index a5ec243e9e..3518b1a91a 100644
--- a/src/client/ui/default.vue
+++ b/src/client/ui/default.vue
@@ -1,6 +1,6 @@
<template>
-<div class="mk-app" :class="{ wallpaper, isMobile }">
- <XHeaderMenu v-if="showMenuOnTop"/>
+<div class="mk-app" :class="{ wallpaper, isMobile }" :style="`--globalHeaderHeight:${globalHeaderHeight}px`">
+ <XHeaderMenu v-if="showMenuOnTop" v-get-size="(w, h) => globalHeaderHeight = h"/>
<div class="columns" :class="{ fullView, withGlobalHeader: showMenuOnTop }">
<template v-if="!isMobile">
@@ -13,10 +13,7 @@
</template>
<main class="main" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }">
- <header class="header" @click="onHeaderClick">
- <XHeader :info="pageInfo" :back-button="true" @back="back()"/>
- </header>
- <div class="content" :class="{ _flat_: !fullView }">
+ <div class="content" :class="{ _fitSide_: !fullView }">
<router-view v-slot="{ Component }">
<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
<keep-alive :include="['timeline']">
@@ -67,7 +64,6 @@ import { StickySidebar } from '@client/scripts/sticky-sidebar';
import XSidebar from './default.sidebar.vue';
import XDrawerSidebar from '@client/ui/_common_/sidebar.vue';
import XCommon from './_common_/common.vue';
-import XHeader from './_common_/header.vue';
import * as os from '@client/os';
import { menuDef } from '@client/menu';
import * as symbols from '@client/symbols';
@@ -80,15 +76,21 @@ export default defineComponent({
XCommon,
XSidebar,
XDrawerSidebar,
- XHeader,
XHeaderMenu: defineAsyncComponent(() => import('./default.header.vue')),
XWidgets: defineAsyncComponent(() => import('./default.widgets.vue')),
},
+ provide() {
+ return {
+ shouldHeaderThin: this.showMenuOnTop,
+ };
+ },
+
data() {
return {
pageInfo: null,
menuDef: menuDef,
+ globalHeaderHeight: 0,
isMobile: window.innerWidth <= MOBILE_THRESHOLD,
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
widgetsShowing: false,
@@ -193,10 +195,6 @@ export default defineComponent({
if (window._scroll) window._scroll();
},
- onHeaderClick() {
- window.scroll({ top: 0, behavior: 'smooth' });
- },
-
onContextmenu(e) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
@@ -257,7 +255,6 @@ export default defineComponent({
}
.mk-app {
- $header-height: 50px;
$ui-font-size: 1em;
$widgets-hide-threshold: 1200px;
$nav-icon-only-width: 78px; // TODO: どこかに集約したい
@@ -282,10 +279,6 @@ export default defineComponent({
border: none;
width: 100%;
border-radius: 0;
-
- > .header {
- width: 100%;
- }
}
}
}
@@ -325,30 +318,6 @@ export default defineComponent({
border-radius: 0;
overflow: clip;
--margin: 12px;
-
- > .header {
- position: sticky;
- z-index: 1000;
- top: var(--globalHeaderHeight, 0px);
- height: $header-height;
- -webkit-backdrop-filter: var(--blur, blur(32px));
- backdrop-filter: var(--blur, blur(32px));
- background-color: var(--header);
- border-bottom: solid 0.5px var(--divider);
- }
-
- > .content {
- --stickyTop: calc(var(--globalHeaderHeight, 0px) + #{$header-height});
- }
-
- @media (max-width: 850px) {
- padding-top: $header-height;
-
- > .header {
- position: fixed;
- width: calc(100% - #{$nav-icon-only-width});
- }
- }
}
> .widgets {
@@ -370,12 +339,11 @@ export default defineComponent({
}
&.withGlobalHeader {
- --globalHeaderHeight: 60px; // TODO: 60pxと決め打ちしているのを直す
-
> .main {
margin-top: 0;
border: solid 1px var(--divider);
border-radius: var(--radius);
+ --stickyTop: var(--globalHeaderHeight);
}
> .widgets {
diff --git a/src/client/ui/universal.vue b/src/client/ui/universal.vue
index ec9254b697..7c25d71bb3 100644
--- a/src/client/ui/universal.vue
+++ b/src/client/ui/universal.vue
@@ -3,9 +3,6 @@
<XSidebar ref="nav" class="sidebar"/>
<div class="contents" ref="contents" @contextmenu.stop="onContextmenu" :style="{ background: pageInfo?.bg }">
- <header class="header" ref="header" @click="onHeaderClick" :style="{ background: pageInfo?.bg }">
- <XHeader :info="pageInfo" :back-button="true" @back="back()"/>
- </header>
<main ref="main">
<div class="content">
<router-view v-slot="{ Component }">
@@ -58,7 +55,6 @@ import { instanceName } from '@client/config';
import { StickySidebar } from '@client/scripts/sticky-sidebar';
import XSidebar from '@client/ui/_common_/sidebar.vue';
import XCommon from './_common_/common.vue';
-import XHeader from './_common_/header.vue';
import XSide from './default.side.vue';
import * as os from '@client/os';
import { menuDef } from '@client/menu';
@@ -70,7 +66,6 @@ export default defineComponent({
components: {
XCommon,
XSidebar,
- XHeader,
XWidgets: defineAsyncComponent(() => import('./universal.widgets.vue')),
XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
},
@@ -151,9 +146,6 @@ export default defineComponent({
adjustUI() {
const navWidth = this.$refs.nav.$el.offsetWidth;
this.navHidden = navWidth === 0;
- if (this.$refs.contents == null) return;
- const width = this.$refs.contents.offsetWidth;
- if (this.$refs.header) this.$refs.header.style.width = `${width}px`;
},
showNav() {
@@ -183,10 +175,6 @@ export default defineComponent({
if (window._scroll) window._scroll();
},
- onHeaderClick() {
- window.scroll({ top: 0, behavior: 'smooth' });
- },
-
onContextmenu(e) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
@@ -243,7 +231,6 @@ export default defineComponent({
}
.mk-app {
- $header-height: 58px; // TODO: どこかに集約したい
$ui-font-size: 1em; // TODO: どこかに集約したい
$widgets-hide-threshold: 1090px;
@@ -263,37 +250,11 @@ export default defineComponent({
> .contents {
width: 100%;
min-width: 0;
- --stickyTop: #{$header-height};
- padding-top: $header-height;
background: var(--panel);
- > .header {
- position: fixed;
- z-index: 1000;
- top: 0;
- height: $header-height;
- width: 100%;
- line-height: $header-height;
- text-align: center;
- font-weight: bold;
- //background-color: var(--panel);
- -webkit-backdrop-filter: var(--blur, blur(32px));
- backdrop-filter: var(--blur, blur(32px));
- background-color: var(--header);
- border-bottom: solid 0.5px var(--divider);
- user-select: none;
- }
-
> main {
min-width: 0;
- > .content {
- > * {
- // ほんとは単に calc(100vh - #{$header-height}) と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
- min-height: calc((var(--vh, 1vh) * 100) - #{$header-height});
- }
- }
-
> .spacer {
height: 82px;
diff --git a/src/client/ui/zen.vue b/src/client/ui/zen.vue
index 3756ddb5c3..98e2b8dac6 100644
--- a/src/client/ui/zen.vue
+++ b/src/client/ui/zen.vue
@@ -2,7 +2,7 @@
<div class="mk-app">
<div class="contents">
<header class="header">
- <XHeader :info="pageInfo"/>
+ <MkHeader :info="pageInfo"/>
</header>
<main ref="main">
<div class="content">
@@ -24,14 +24,12 @@
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import { host } from '@client/config';
-import XHeader from './_common_/header.vue';
import XCommon from './_common_/common.vue';
import * as symbols from '@client/symbols';
export default defineComponent({
components: {
XCommon,
- XHeader,
},
data() {
diff --git a/src/client/widgets/aiscript.vue b/src/client/widgets/aiscript.vue
index 2ea6d09ff5..aaf0a0372e 100644
--- a/src/client/widgets/aiscript.vue
+++ b/src/client/widgets/aiscript.vue
@@ -125,7 +125,7 @@ export default defineComponent({
box-sizing: border-box;
font: inherit;
- &:focus {
+ &:focus-visible {
outline: none;
}
}
diff --git a/src/client/widgets/memo.vue b/src/client/widgets/memo.vue
index 13ab628f24..3f11e6409e 100644
--- a/src/client/widgets/memo.vue
+++ b/src/client/widgets/memo.vue
@@ -81,7 +81,7 @@ export default defineComponent({
font: inherit;
font-size: 0.9em;
- &:focus {
+ &:focus-visible {
outline: none;
}
}
diff --git a/src/client/widgets/notifications.vue b/src/client/widgets/notifications.vue
index 01c76850d8..b0245eed6a 100644
--- a/src/client/widgets/notifications.vue
+++ b/src/client/widgets/notifications.vue
@@ -3,7 +3,7 @@
<template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template>
<template #func><button @click="configure()" class="_button"><i class="fas fa-cog"></i></button></template>
- <div class="_flat_">
+ <div class="_fitSide_">
<XNotifications :include-types="props.includingTypes"/>
</div>
</MkContainer>
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index c963242488..0b635ea18e 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -63,7 +63,7 @@ import { Antenna } from '@/models/entities/antenna';
import { AntennaNote } from '@/models/entities/antenna-note';
import { PromoNote } from '@/models/entities/promo-note';
import { PromoRead } from '@/models/entities/promo-read';
-import { program } from '../argv';
+import { envOption } from '../env';
import { Relay } from '@/models/entities/relay';
import { MutedNote } from '@/models/entities/muted-note';
import { Channel } from '@/models/entities/channel';
@@ -72,6 +72,7 @@ import { ChannelNotePining } from '@/models/entities/channel-note-pining';
import { RegistryItem } from '@/models/entities/registry-item';
import { Ad } from '@/models/entities/ad';
import { PasswordResetRequest } from '@/models/entities/password-reset-request';
+import { UserPending } from '@/models/entities/user-pending';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@@ -83,7 +84,7 @@ class MyCustomLogger implements Logger {
}
public logQuery(query: string, parameters?: any[]) {
- if (program.verbose) {
+ if (envOption.verbose) {
sqlLogger.info(this.highlight(query));
}
}
@@ -173,6 +174,7 @@ export const entities = [
RegistryItem,
Ad,
PasswordResetRequest,
+ UserPending,
...charts as any
];
diff --git a/src/docs/en-US/advanced/aiscript.md b/src/docs/en-US/advanced/aiscript.md
index fc2802fcd4..cfcdfebf96 100644
--- a/src/docs/en-US/advanced/aiscript.md
+++ b/src/docs/en-US/advanced/aiscript.md
@@ -3,5 +3,5 @@ AiScript is a scripting language for Misskey.
<div class="info">ℹ️ AiScript is open source and hosted in a separate repository from Misskey. </a></div>
-## 使い方
+## Usage
AiScript documentation such as syntax and built-in functions can be found [here](https://github.com/syuilo/aiscript/tree/master/docs).
diff --git a/src/docs/en-US/advanced/api.md b/src/docs/en-US/advanced/api.md
index 76019b6145..ef1995b18e 100644
--- a/src/docs/en-US/advanced/api.md
+++ b/src/docs/en-US/advanced/api.md
@@ -4,7 +4,7 @@ MisskeyAPIを使ってMisskeyクライアント、Misskey連携Webサービス
APIを使い始めるには、まずアクセストークンを取得する必要があります。 このドキュメントでは、アクセストークンを取得する手順を説明した後、基本的なAPIの使い方を説明します。
-## アクセストークンの取得
+## Obtain an access token
基本的に、APIはリクエストにはアクセストークンが必要となります。 APIにリクエストするのが自分自身なのか、不特定の利用者に使ってもらうアプリケーションなのかによって取得手順は異なります。
* 前者の場合: [「自分自身のアクセストークンを手動発行する」](#自分自身のアクセストークンを手動発行する)に進む
@@ -20,7 +20,7 @@ APIを使い始めるには、まずアクセストークンを取得する必
#### Step 1
-UUIDを生成する。以後これをセッションIDと呼びます。
+Generate UUID.以後これをセッションIDと呼びます。
> このセッションIDは毎回生成し、使いまわさないようにしてください。
@@ -30,14 +30,14 @@ UUIDを生成する。以後これをセッションIDと呼びます。
> 例: `{_URL_}/miauth/c1f6d42b-468b-4fd2-8274-e58abdedef6f`
表示する際、URLにクエリパラメータとしていくつかのオプションを設定できます:
-* `name` ... アプリケーション名
+* `name` ... App name
* > 例: `MissDeck`
-* `icon` ... アプリケーションのアイコン画像URL
+* `icon` ... App icon URL
* > 例: `https://missdeck.example.com/icon.png`
* `callback` ... 認証が終わった後にリダイレクトするURL
* > 例: `https://missdeck.example.com/callback`
* リダイレクト時には、`session`というクエリパラメータでセッションIDが付きます
-* `permission` ... アプリケーションが要求する権限
+* `permission` ... App permissions
* > 例: `write:notes,write:following,read:drive`
* 要求する権限を`,`で区切って列挙します
* どのような権限があるかは[APIリファレンス](/api-doc)で確認できます
@@ -46,13 +46,13 @@ UUIDを生成する。以後これをセッションIDと呼びます。
ユーザーが発行を許可した後、`{_URL_}/api/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。
レスポンスに含まれるプロパティ:
-* `token` ... ユーザーのアクセストークン
-* `user` ... ユーザーの情報
+* `token` ... User access token
+* `user` ... User info
[「APIの使い方」へ進む](#APIの使い方)
-## APIの使い方
-**APIはすべてPOSTで、リクエスト/レスポンスともにJSON形式です。RESTではありません。** アクセストークンは、`i`というパラメータ名でリクエストに含めます。
+## API usage
+**APIはすべてPOSTで、リクエスト/レスポンスともにJSON形式です。There is no REST support.** アクセストークンは、`i`というパラメータ名でリクエストに含めます。
-* [APIリファレンス](/api-doc)
-* [ストリーミングAPI](./stream)
+* [API Reference](/api-doc)
+* [Streaming API](./stream)
diff --git a/src/docs/en-US/advanced/create-plugin.md b/src/docs/en-US/advanced/create-plugin.md
index ec17b95186..3b8763dfd8 100644
--- a/src/docs/en-US/advanced/create-plugin.md
+++ b/src/docs/en-US/advanced/create-plugin.md
@@ -1,20 +1,20 @@
-# プラグインの作成
+# New Plugin
Misskey Webクライアントのプラグイン機能を使うと、クライアントを拡張し、様々な機能を追加できます。 ここではプラグインの作成にあたってのメタデータ定義や、AiScript APIリファレンスを掲載します。
## Metadata
プラグインは、AiScriptのメタデータ埋め込み機能を使って、デフォルトとしてプラグインのメタデータを定義する必要があります。 メタデータは次のプロパティを含むオブジェクトです。
### name
-プラグイン名
+Plugin name
### author
-プラグイン作者
+Plugin author
### version
-プラグインバージョン。数値を指定してください。
+プラグインバージョン。A number must be specified.
### description
-プラグインの説明
+Plugin description
### permissions
プラグインが要求する権限。MisskeyAPIにリクエストする際に用いられます。
@@ -34,7 +34,7 @@ Misskey Webクライアントのプラグイン機能を使うと、クライア
#### default
設定のデフォルト値
-## APIリファレンス
+## API Reference
AiScript標準で組み込まれているAPIは掲載しません。
### Mk:dialog(title text type)
diff --git a/src/docs/en-US/advanced/develop-bot.md b/src/docs/en-US/advanced/develop-bot.md
index 7f825e9bc4..c6a312e79e 100644
--- a/src/docs/en-US/advanced/develop-bot.md
+++ b/src/docs/en-US/advanced/develop-bot.md
@@ -1,4 +1,4 @@
-# Botの作成
+# Create a bot
[Misskey API](./api)を利用してBotの開発が可能です。 また、いくつかのBot実装が公開されているため、ぜひ参考にしてください。
- [syuilo/ai](https://github.com/syuilo/ai) ... Node.js上で動く、TypeScript製Bot実装
diff --git a/src/docs/en-US/advanced/stream.md b/src/docs/en-US/advanced/stream.md
index c0d0efc910..16b15c7619 100644
--- a/src/docs/en-US/advanced/stream.md
+++ b/src/docs/en-US/advanced/stream.md
@@ -1,4 +1,4 @@
-# ストリーミングAPI
+# Streaming API
ストリーミングAPIを使うと、リアルタイムで様々な情報(例えばタイムラインに新しい投稿が流れてきた、メッセージが届いた、フォローされた、など)を受け取ったり、様々な操作を行ったりすることができます。
diff --git a/src/docs/en-US/general/glossary.md b/src/docs/en-US/general/glossary.md
index 53164a0a59..139c3eab82 100644
--- a/src/docs/en-US/general/glossary.md
+++ b/src/docs/en-US/general/glossary.md
@@ -73,7 +73,7 @@ A feature allowing users to organize the files they have uploaded to Misskey.For
## Notes
Content which may include text, images, surveys and others that has been posted to Misskey.For details, see [here.](../features/note)
-## Misskist
+## Misskeyist
Users of Misskey.
## Moderator
diff --git a/src/docs/en-US/general/troubleshooting.md b/src/docs/en-US/general/troubleshooting.md
index e3dd5129e0..75051debe6 100644
--- a/src/docs/en-US/general/troubleshooting.md
+++ b/src/docs/en-US/general/troubleshooting.md
@@ -34,7 +34,7 @@ A blinking light indicates unread content.In cases where this light won't go awa
Followers-only notes cannot be renoted.
## Specific parts of the UI are not being displayed
-Problems like these can arise if you are using an Adblocker.Please turn these off on Misskey.
+Problems like these can arise if you are using an Adblocker. For an optimized experience on Misskey, please turn it off.
## Some parts of the UI are untranslated
In most cases, this is simply a matter of the translation not having been done yet instead of being a bug.Please wait until the translation of this area has been completed. You can alternatively also [participate in translation](./misskey) yourself.
diff --git a/src/docs/eo-UY/admin/faq.md b/src/docs/eo-UY/admin/faq.md
index 317b4e0655..5341c0f16f 100644
--- a/src/docs/eo-UY/admin/faq.md
+++ b/src/docs/eo-UY/admin/faq.md
@@ -1,4 +1,4 @@
-# よくある質問
+# Oftaj demandoj
ここでは、サーバー管理者向けのよくある質問を掲載しています。
## デフォルトテーマを設定したい
diff --git a/src/docs/eo-UY/advanced/create-plugin.md b/src/docs/eo-UY/advanced/create-plugin.md
index e7826037b8..7ed29bb824 100644
--- a/src/docs/eo-UY/advanced/create-plugin.md
+++ b/src/docs/eo-UY/advanced/create-plugin.md
@@ -1,4 +1,4 @@
-# プラグインの作成
+# Krei kromaĵo
Misskey Webクライアントのプラグイン機能を使うと、クライアントを拡張し、様々な機能を追加できます。 ここではプラグインの作成にあたってのメタデータ定義や、AiScript APIリファレンスを掲載します。
## Metadatumoj
diff --git a/src/docs/eo-UY/advanced/stream.md b/src/docs/eo-UY/advanced/stream.md
index 932da90f25..9f5cdbbcb0 100644
--- a/src/docs/eo-UY/advanced/stream.md
+++ b/src/docs/eo-UY/advanced/stream.md
@@ -1,4 +1,4 @@
-# ストリーミングAPI
+# API de Flui
ストリーミングAPIを使うと、リアルタイムで様々な情報(例えばタイムラインに新しい投稿が流れてきた、メッセージが届いた、フォローされた、など)を受け取ったり、様々な操作を行ったりすることができます。
diff --git a/src/docs/eo-UY/features/favorite.md b/src/docs/eo-UY/features/favorite.md
index 05c03fa2db..747c871a5d 100644
--- a/src/docs/eo-UY/features/favorite.md
+++ b/src/docs/eo-UY/features/favorite.md
@@ -1,4 +1,4 @@
-# Preferataĵoj
+# Preferaĵoj
[ノート](./node)をお気に入りとして登録できる機能です。 お気に入り登録したノートは、[お気に入りページ](./my/favorites)で一覧することができます。 お気に入りに登録したことは相手に通知されず、お気に入りは自分しか見ることができません。
ノートをお気に入り登録するには、ノートメニューの「お気に入り」を押します。お気に入り解除するには、ノートメニューの「お気に入り解除」を押します。
diff --git a/src/docs/eo-UY/features/keyboard-shortcut.md b/src/docs/eo-UY/features/keyboard-shortcut.md
index b4bb35b763..413bef16c1 100644
--- a/src/docs/eo-UY/features/keyboard-shortcut.md
+++ b/src/docs/eo-UY/features/keyboard-shortcut.md
@@ -4,21 +4,21 @@
これらのショートカットは基本的にどこでも使えます。
<table>
<thead>
- <tr><th>Fulmoklavoj</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Fulmoklavoj</th><th>Rezultato</th><th>Deveno (angla)</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>Skribi novan noton</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
<tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
<tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>Malfermi sekcio de sciigoj</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>Serĉi</td><td><b>S</b>earch</td></tr>
- <tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
+ <tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>Montru helpon</td><td><b>H</b>elp</td></tr>
</tbody>
</table>
## 投稿にフォーカスされた状態
<table>
<thead>
- <tr><th>Fulmoklavoj</th><th>効果</th><th>Deveno (angla)</th></tr>
+ <tr><th>Fulmoklavoj</th><th>Rezultato</th><th>Deveno (angla)</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>上の投稿にフォーカスを移動</td><td>-</td></tr>
@@ -28,7 +28,7 @@
<tr><td><kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">Q</kbd></kbd></td><td>Tuj plusendos (sen la fasado)</td><td>-</td></tr>
<tr><td><kbd class="key">E</kbd>, <kbd class="key">A</kbd>, <kbd class="key">+</kbd></td><td>リアクションフォームを開く</td><td><b>E</b>mote, re<b>A</b>ction</td></tr>
<tr><td><kbd class="key">0</kbd>-<kbd class="key">9</kbd></td><td>数字に対応したリアクションをする(対応については後述)</td><td>-</td></tr>
- <tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>Aldoni vian liston de preferaĵoj</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
+ <tr><td><kbd class="key">F</kbd>, <kbd class="key">B</kbd></td><td>Aldoni al vian liston de preferaĵoj</td><td><b>F</b>avorite, <b>B</b>ookmark</td></tr>
<tr><td><kbd class="key">Del</kbd>, <kbd class="group"><kbd class="key">Ctrl</kbd> + <kbd class="key">D</kbd></kbd></td><td>Forviŝi la noton</td><td><b>D</b>elete</tr>
<tr><td><kbd class="key">M</kbd>, <kbd class="key">O</kbd></td><td>Malfelmi poŝtaĵan menuon</td><td><b>M</b>ore, <b>O</b>ther</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>CWで隠された部分を表示 or 隠す</td><td><b>S</b>how, <b>S</b>ee</td></tr>
@@ -48,11 +48,11 @@
</tbody>
</table>
-## リアクションフォーム
+## Elektilo de reago
デフォルトで「👍」にフォーカスが当たっている状態です。
<table>
<thead>
- <tr><th>Fulmoklavoj</th><th>効果</th><th>Deveno (angla)</th></tr>
+ <tr><th>Fulmoklavoj</th><th>Rezultato</th><th>Deveno (angla)</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd></td><td>上のリアクションにフォーカスを移動</td><td>-</td></tr>
diff --git a/src/docs/eo-UY/features/mfm.md b/src/docs/eo-UY/features/mfm.md
index 2970fbd7b2..7257033d8d 100644
--- a/src/docs/eo-UY/features/mfm.md
+++ b/src/docs/eo-UY/features/mfm.md
@@ -2,10 +2,10 @@
MFMは、Misskey Flavored Markdownの略で、Misskeyの様々な場所で使用できる専用のマークアップ言語です。 MFMで使用可能な構文は[MFMチートシート](/mfm-cheat-sheet)で確認できます。
## MFMが使用可能な場所の例
-- ノート本文
+- Teksto de notoj
- CW注釈
-- Nomo de uzanto
-- Profilo de uzanto
+- La nomo de uzantoj
+- La sinprezento de profiloj
## Informoj por programistoj
MFMのパーサー実装はライブラリとして公開されており、簡単にクライアントにMFMを組み込むことが可能です。
diff --git a/src/docs/eo-UY/features/note.md b/src/docs/eo-UY/features/note.md
index aa72981e24..2bfe10951c 100644
--- a/src/docs/eo-UY/features/note.md
+++ b/src/docs/eo-UY/features/note.md
@@ -1,5 +1,5 @@
# Notoj
-ノートは、Misskeyに投稿される、文章、ファイル、アンケートなどを含むコンテンツで、Misskeyの中心的概念です。また、そのノートを作成する行為自体もノートと呼ばれます。
+Notoj estas centraj konceptoj en Misskey kaj enhavoj kiuj konsistas el teksto, bildoj, dosieroj, balotujo k.t.p.Ankaŭ krei notojn estas nomata "noto" same kiel ili
ノートが作成されると、[タイムライン](./timeline)に追加され、自分の[フォロワー](./follow)やサーバーのユーザーが見れるようになります。
@@ -7,14 +7,14 @@
ノートを[お気に入り](./favorite)登録することで、後で簡単に見返すことができます。
-## ノートを作成する
+## Skribi notojn
ノートを作成するには、画面上にある鉛筆マークのボタンを押して、作成フォームを開きます。作成フォームに内容を入力し、「ノート」ボタンを押すことでノートが作成されます。 ノートには、画像、動画など任意のファイルや、[アンケート](./poll)を添付することができます。また、本文中には[MFM](./mfm)が使用でき、[メンション](./mention)や[ハッシュタグ](./hashtag)を含めることもできます。 他にも、CWや公開範囲といった設定も行えます(詳細は後述)。
<div class="info">ℹ️ コンピューターのクリップボードに画像データがある状態で、フォーム内のテキストボックスにペーストするとその画像を添付することができます。</div>
<div class="info">ℹ️ テキストボックス内で<kbd class="key">Ctrl + Enter</kbd>を押すことでも投稿できます。</div>
-## Plusendi la noton
+## Plusendi noton
既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノートをRenoteと呼びます。 自分がフォローしているユーザーの、気に入ったノートを自分のフォロワーに共有したい場合や、過去の自分のノートを再度共有したい場合に使います。 同じノートに対して無制限にRenoteを行うことができますが、あまり連続して使用すると迷惑になる場合もあるので、注意しましょう。
-<div class="warn">⚠️ 公開範囲がフォロワーやダイレクトのノートはRenoteできません</div>
+<div class="warn">⚠️ Se oni sendas notojn nur al sekvantoj aŭ rekte, iliaj ne estas plusendeblaj.</div>
Renoteを削除するには、Renoteの時刻表示の隣にある「...」を押し、「Renote解除」を選択します。
@@ -31,10 +31,10 @@ Contents Warningの略で、ノートの内容を、閲覧者の操作なしに
### Hejma
全ての人に対してノートが公開されますが、フォロワー以外のローカルタイムライン、ソーシャルタイムライン、グローバルタイムラインにはノートは流れません。
-### Sekvantoj
-自分のフォロワーに対してのみノートを公開します。フォロワーの全てのタイムラインに流れます。
+### Nur al sekvantoj
+Viaj notoj estos senditaj nur al viaj sekvantoj.La noto aperos sur ĉiuj templinioj de viaj sekvantoj.
-### Rekta
+### Rekte
指定したユーザーに対してのみノートを公開します。指定したユーザーの全てのタイムラインに流れます。
### 「ローカルのみ」オプション
@@ -42,13 +42,13 @@ Contents Warningの略で、ノートの内容を、閲覧者の操作なしに
### 公開範囲の比較
<table>
- <tr><th></th><th>Publika</th><th>Hejma</th><th>Sekvantoj</th><th>Rekta</th></tr>
+ <tr><th></th><th>Publika</th><th>Hejma</th><th>Nur al sekvantoj</th><th>Rekte</th></tr>
<tr><th>フォロワーのLTL/STL/GTL</th><td>✔</td><td>✔</td><td>✔</td><td></td></tr>
<tr><th>非フォロワーのLTL/STL/GTL</th><td>✔</td><td></td><td></td><td></td></tr>
</table>
-## Alpingli al la profilo
+## Alpingli sur profilo
ノートをピン留めすると、ユーザーページに常にそのノートを表示しておくことができます。 ノートのメニューを開き、「ピン留め」を選択してピン留めできます。 複数のノートをピン留めできます。
## Observi
-Vi povas ricevi sciigojn pri reagoj, respondoj, ktp al noto, kiuj ne apartenas al vi. Por observu, malfermu respektivan menuon de noto, kaj elektu la "Observi" el ĝi.
+Vi povas ricevi tiuj sciigoj pri reagoj, respondoj, k.t.p al noto kiuj ne apartenas al vi estas ankaŭ ricevebla. Por komenci tion elektu la "Observi" el la menuon kuntekstan de la notoj respektivaj.
diff --git a/src/docs/eo-UY/general/faq.md b/src/docs/eo-UY/general/faq.md
index c272b2ad42..f7ede4c6cc 100644
--- a/src/docs/eo-UY/general/faq.md
+++ b/src/docs/eo-UY/general/faq.md
@@ -1,4 +1,4 @@
-# よくある質問
+# Oftaj demandoj
ここでは利用上のよくある質問について掲載しています。 Misskeyのプロジェクト自体についてのよくある質問は[こちら](./misskey)に掲載されています。
## iOS/Androidのアプリはありますか?
diff --git a/src/docs/eo-UY/general/glossary.md b/src/docs/eo-UY/general/glossary.md
index fe3b034181..25dd5f82c1 100644
--- a/src/docs/eo-UY/general/glossary.md
+++ b/src/docs/eo-UY/general/glossary.md
@@ -1,4 +1,4 @@
-# 用語集
+# Glosaro
Misskeyに関する用語集です。
## ActivityPub
@@ -49,7 +49,7 @@ Ai estas oficiala maskoto de Misskey.
## Nodo
todo
-## Personecigitaj emoĵioj
+## Emoĵioj personecigitaj
サーバーで用意された絵文字。カスタム絵文字ではない通常の絵文字は「Unicode絵文字」と区別して呼ばれる。
## Ŝaltpodio
@@ -68,10 +68,10 @@ todo
アカウントが使用不可に設定されている状態。
## Disko
-Misskeyにアップロードしたファイルを管理する機能。詳細は[こちら。](../features/drive)
+Funkcio ebligas al uzantoj administri dosierojn kiujn ili alŝutis al Misskey.Rigardu por sciu pli tie[.](../features/drive)
## Notoj
-Misskeyに投稿される、文章、ファイル、アンケートなどを含めることができるコンテンツ。Rigardu por sciu pli tie[.](../features/note)
+Enpoŝtigaĵoj en Misskey kiuj konsistas el teksto, dosiero, balotujo, ktp.Rigardu por sciu pli tie[.](../features/note)
## Miskiisto
Uzuloj de Misskey.
@@ -82,7 +82,7 @@ Uzuloj de Misskey.
## Transa, Surloka
他サーバーのことを指します。リモートユーザーといったように接頭辞としても使われます。ローカルの逆です。
-## Kunfederado
+## Federado
サーバー上で作成された情報が他のサーバーに伝わること。
## Loka
diff --git a/src/docs/eo-UY/general/misskey.md b/src/docs/eo-UY/general/misskey.md
index 506f7a8f24..ef287e5964 100644
--- a/src/docs/eo-UY/general/misskey.md
+++ b/src/docs/eo-UY/general/misskey.md
@@ -43,7 +43,7 @@ Misskeyはビジネスではなく、利用は無料であるため、収益は
## クレジット
Misskeyの開発者や、Misskeyに寄付をしてくださった方の一覧は[こちら](/about-misskey)で見ることができます。
-## よくある質問
+## Oftaj demandoj
### プロジェクトは何を目指していますか?
強いて言うと、漠然的になりますが広く使われる汎用的なプラットフォームになることを目指しています。 Misskeyは他のプロジェクトとは違い、何らかの思想(例えば、反中央集権)やビジョンに基づいて開発が行われているわけではなく、その点ではフラットです。 それが逆に、特定の方向性に縛られないフレキシブルさを生み出すことに繋がっていると感じています。
<!-- TODO: ここにロードマップへのリンク -->
diff --git a/src/docs/es-ES/general/troubleshooting.md b/src/docs/es-ES/general/troubleshooting.md
index f895b49847..2bedfc3129 100644
--- a/src/docs/es-ES/general/troubleshooting.md
+++ b/src/docs/es-ES/general/troubleshooting.md
@@ -1,4 +1,4 @@
-# トラブルシューティング
+# Solución de problemas
<div class="info">ℹ️ <a href="./faq">よくある質問</a>も合わせてお役立てください。</div>
問題が発生したときは、まずこちらをご確認ください。 該当する項目が無い、もしくは手順を試しても効果がない場合は、サーバーの管理者に連絡するか[不具合を報告](./report-issue)してください。
diff --git a/src/docs/fr-FR/general/apps.md b/src/docs/fr-FR/general/apps.md
index 7f9165a306..32a1274a59 100644
--- a/src/docs/fr-FR/general/apps.md
+++ b/src/docs/fr-FR/general/apps.md
@@ -1,6 +1,6 @@
# Liste des applications tierces
-## クライアント
+## Client
todo
-## 連携サービス
+## Services connexes
todo
diff --git a/src/docs/fr-FR/general/glossary.md b/src/docs/fr-FR/general/glossary.md
index 441a2b5bc4..d26d2e8ee6 100644
--- a/src/docs/fr-FR/general/glossary.md
+++ b/src/docs/fr-FR/general/glossary.md
@@ -1,65 +1,65 @@
-# 用語集
-Misskeyに関する用語集です。
+# Glossaire
+Glossaire des termes utilisés dans Misskey.
## ActivityPub
-(読み: あくてぃびてぃぱぶ) 分散型を実現するために用いられるプロトコル(仕様)。このプロトコルに則ってサーバー同士通信を行うことで、連合が行われ、Fediverseを形成しています。
+Nom du protocole (procédé technique) utilisé par Misskey pour pouvoir fonctionner comme service décentralisé. Ce protocole permet à tous les serveurs l'ayant adopté de communiquer entre eux et de former une sorte de fédération que l'on appelle « Fédiverse ».
## AiScript
-(読み: あいすくりぷと) Misskey上で使用できるプログラミング言語です。詳細は[こちら。](../advanced/aiscript)
+Langage de programmation qui peut être utilisé sur Misskey. [Voir ici pour plus d'informations.](../advanced/aiscript)
## API
-(読み: えーぴーあい) Misskeyのサーバーが公開している、プログラムからMisskeyを扱うためのインターフェース。詳細は[こちら。](../advanced/api)
+(読み: えーぴーあい) Misskeyのサーバーが公開している、プログラムからMisskeyを扱うためのインターフェース。[Voir ici pour plus d'informations. ](../advanced/api)
## Bot
-(読み: ぼっと) プログラムによって動作しているアカウント。
+Anglicisme désignant un compte géré par un programme informatique (vous le trouverez parfois aussi sous le terme de « robot »).
## CW
-(読み: こんてんつわーにんぐ) Contents Warningの略。ノートの内容を、操作なしには表示しないようにできる機能。主に長大な内容を隠すためや、ネタバレ防止などに使われます。
+Abréviation de « Content Warning » (qui signifie littéralement « alerte de contenu »). Fonctionnalité permettant d'assujettir l'affichage du contenu d'une note à une intervention de l'utilisateur·rice par le biais d'un bouton de masquage automatique. Principalement employée pour cacher le contenu des notes très longues, ou pour éviter de dévoiler publiquement des spoils potentiels, etc.
-## Fediverse
-(読み: ふぇでぃばーす) Misskeyを含む様々な分散型ソフトウェアのサーバーで構成されたネットワーク。
+## Fédiverse
+Nom du réseau social fédéré qui rassemble une multitude d'instances appartenant à différents services et dont fait partie Misskey.
-## GTL
-グローバルタイムライン(Global TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+## FG
+Abréviation de « Fil global ». Pour en savoir plus sur les différents fils, [voir ici.](../features/timeline)
-## HTL
-ホームタイムライン(Home TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+## FP
+Abréviation de « Fil principal ». Pour en savoir plus sur les différents fils, [voir ici.](../features/timeline)
-## LTL
-ローカルタイムライン(Local TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+## FL
+Abréviation de « Fil local ». Pour en savoir plus sur les différents fils, [voir ici.](../features/timeline)
## MFM
-(読み: えむえふえむ) Misskey Flavored Markdownの略で、Misskey上で使用できるマークアップ言語です。詳細は[こちら。](../features/mfm)
+Abréviation de « Misskey Flavored Markdown », un langage Markdown qui peut être utilisé sur Misskey. [Voir ici pour plus d'informations.](../features/mfm)
## NSFW
-(読み: のっとせーふふぉーわーく) Not Safe For Workの略。画像を「閲覧注意」扱いにし、操作なしには表示しないようにすることができる機能。
+Abréviation de « Not Safe For Work » (qui signifie littéralement « pas sûr pour le travail »). Fonctionnalité permettant d'avertir que le contenu d'une note n'est pas approprié sur le lieu de travail et d'en assujettir l'affichage à une intervention de l'utilisateur·rice par le biais d'un bouton de masquage automatique.
## Renoter
-(読み: りのーと) 既にあるノートを引用、もしくはそのノートを新しいノートとして共有する行為、またそれによって作成されたノート。詳細は[こちら。](../features/note)
+Il s'agit du fait de citer une note existante ou de partager une note existante dans une nouvelle note. La note créée par l'un de ces deux biais est alors appelée une « Renote ». [Voir ici pour plus d'informations.](../features/note)
-## STL
-ソーシャルタイムライン(Social TimeLine)の略。タイムラインの詳細は[こちら。](../features/timeline)
+## FS
+Abréviation de « Fil social ». Pour en savoir plus sur les différents fils, [voir ici.](../features/timeline)
-## 藍
-(読み: あい) Misskeyの看板娘(公式キャラクター)です。
+## Ai
+Nom de la mascotte officielle de Misskey. (Le mot japonais, à prononcer « a-i », signifie littéralement « indigo »).
-## アクティブユーザー
-インスタンスにアカウントを作っているユーザーのうち、現在も実際にサービスを利用しているユーザーのこと。
+## Utilisateurices actif·ve·s
+Désigne les utilisateur·rice·s, parmi tou·te·s celleux inscrit·e·s sur l'instance, qui utilisent effectivement leur compte au moment présent.
## Instance
todo
## Émojis personnalisés
-サーバーで用意された絵文字。カスタム絵文字ではない通常の絵文字は「Unicode絵文字」と区別して呼ばれる。
+Désigne les émojis mis à disposition par votre instance. Par opposition, les émojis disponibles par défaut (donc pas « personnalisés ») sont appelés « émojis unicode ».
-## コントロールパネル
-インスタンスの設定画面のこと。
+## Panneau de contrôle
+Écran de contrôle des paramètres d'instance.
## Serveurs
todo
## Mettre en sourdine
-ノートをパブリックな公開範囲で投稿できなくされている状態。モデレーターの判断でユーザーごとに設定されます。詳細は[こちら。](../features/silence)
+ノートをパブリックな公開範囲で投稿できなくされている状態。モデレーターの判断でユーザーごとに設定されます。[Voir ici pour plus d'informations. ](../features/silence)
## File d’attente
アクティビティ配送などを順番に行うためのシステム。
@@ -68,15 +68,15 @@ todo
アカウントが使用不可に設定されている状態。
## Drive
-Misskeyにアップロードしたファイルを管理する機能。詳細は[こちら。](../features/drive)
+Fonctionnalité vous permettant de gérer les fichiers que vous avez téléversés sur Misskey. [Voir ici pour plus d'informations. ](../features/drive)
## Notes
-Misskeyに投稿される、文章、ファイル、アンケートなどを含めることができるコンテンツ。詳細は[こちら。](../features/note)
+Nom des publications sur Misskey. Leur contenu peut être du texte, mais aussi des fichiers, des enquêtes, etc. [Voir ici pour plus d'informations.](../features/note)
-## ミスキスト
-Misskeyを使う人のこと。
+## Misskeynaute
+Désigne les utilisateur·rice·s de Misskey.
-## Modérateurs
+## Modérateur·rice·s
スパムの凍結およびサイレンスや不適切な投稿の削除など、コミュニティ運営に関する権限を持つユーザー。
## Distant
diff --git a/src/docs/fr-FR/general/troubleshooting.md b/src/docs/fr-FR/general/troubleshooting.md
index f6a6771df2..3abcf34006 100644
--- a/src/docs/fr-FR/general/troubleshooting.md
+++ b/src/docs/fr-FR/general/troubleshooting.md
@@ -21,20 +21,20 @@ Essayez les solutions proposées ci-dessous :
- activer l'option « Réduire les animations dans l'interface » dans les paramètres du client
- désactiver l'option « Utiliser un effet de flou pour les modals » dans les paramètres du client
- activer l'accélération matérielle dans les paramètres de votre navigateur
-- お使いのデバイスのスペックを上げる
+- effectuer les mises à jour de votre appareil.
-## UIの一部の表示がおかしい(背景が透明になっている等)
-アップデートによりUIの改修が行われたときに、テーマのキャッシュシステムの影響でそのような表示になることがあります。 クライアントの設定の「キャッシュをクリア」すると直ります。
-<div class="warn">⚠️ 「クライアントの」キャッシュクリアです。「ブラウザの」キャッシュクリアは行わないでください。</div>
+## Certaines parties de l'interface ne s'affichent pas correctement (arrière-plan transparent, etc.)
+Cela peut être lié au système de mise en cache du thème lorsqu'une mise à jour visant à améliorer l'interface a eu lieu. Pour résoudre le problème, sélectionnez la touche « Vider le cache » dans les paramètres du client.
+<div class="warn">⚠️ Attention de bien vider le cache du -client-... et pas celui du -navigateur- !</div>
-## 通知やアンテナ等の点滅が消えない
-点滅は、未読のコンテンツがあることを示しています。通常点滅が消えない場合は、コンテンツを遡ると未読なコンテンツが残っています。 すべて既読にしたと思われるのに、それでもなお点滅が続く場合(おそらく不具合と思われます)は設定から強制的にすべて既読扱いにすることができます。
+## Les pastilles de notification clignotantes ne disparaissent pas
+Une pastille clignotante indique la présence de nouveau contenu que vous n'avez pas encore lu. Lorsque cette pastille ne disparaît pas, c'est généralement parce que du contenu laissé non lu a été repoussé par la réception de nouveau contenu. S'il s'avère que vous avez déjà lu le contenu dans sa totalité mais que la pastille continue tout de même de clignoter, il s'agit alors vraisemblablement d'un bug et vous pouvez forcer Misskey à tout marquer comme lu depuis vos paramètres généraux.
## La fonction « Renoter » ne fonctionne pas
Les notes dont l'audience est limitée aux « Abonné·e·s uniquement » ne peuvent pas être renotées.
## Des éléments spécifiques de l'interface ne s'affichent pas
-広告ブロッカーを使用しているとそのような不具合が発生することがあります。Misskeyではオフにしてご利用ください。
+Ce type de dysfonctionnement survient lorsque vous utilisez des bloqueurs de publicité. Désactivez-les pour profiter d'une expérience optimale sur Misskey.
## Certaines parties de l'interface ne sont pas traduites
La plupart du temps, cela n'est pas un bug mais simplement un problème de traduction qui n'a pas encore été faite. Merci de patienter jusqu'à ce que la traduction de la portion en question soit achevée. Vous pouvez également [aider à traduire](./misskey) Misskey.
diff --git a/src/docs/zh-CN/advanced/stream.md b/src/docs/zh-CN/advanced/stream.md
index 090f8475ea..3351f2d839 100644
--- a/src/docs/zh-CN/advanced/stream.md
+++ b/src/docs/zh-CN/advanced/stream.md
@@ -50,7 +50,7 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
}
```
-ここで、
+其中:
* `channel`には接続したいチャンネル名を設定します。チャンネルの種類については後述します。
* `id`にはそのチャンネルとやり取りするための任意のIDを設定します。ストリームでは様々なメッセージが流れるので、そのメッセージがどのチャンネルからのものなのか識別する必要があるからです。このIDは、UUIDや、乱数のようなもので構いません。
* `params`はチャンネルに接続する際のパラメータです。チャンネルによって接続時に必要とされるパラメータは異なります。パラメータ不要のチャンネルに接続する際は、このプロパティは省略可能です。
@@ -74,7 +74,7 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
}
```
-ここで、
+其中:
* `id`には前述したそのチャンネルに接続する際に設定したIDが設定されています。これで、このメッセージがどのチャンネルからのものなのか知ることができます。
* `type`にはメッセージの種類が設定されます。チャンネルによって、どのような種類のメッセージが流れてくるかは異なります。
* `body`にはメッセージの内容が設定されます。チャンネルによって、どのような内容のメッセージが流れてくるかは異なります。
@@ -96,7 +96,7 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
}
```
-ここで、
+其中:
* `id`には前述したそのチャンネルに接続する際に設定したIDを設定します。これで、このメッセージがどのチャンネルに向けたものなのか識別させることができます。
* `type`にはメッセージの種類を設定します。チャンネルによって、どのような種類のメッセージを受け付けるかは異なります。
* `body`にはメッセージの内容を設定します。チャンネルによって、どのような内容のメッセージを受け付けるかは異なります。
@@ -113,7 +113,7 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
}
```
-ここで、
+其中:
* `id`には前述したそのチャンネルに接続する際に設定したIDを設定します。
## ストリームを経由してAPIリクエストする
@@ -134,7 +134,7 @@ MisskeyのストリーミングAPIにはチャンネルという概念があり
}
```
-ここで、
+其中:
* `id`には、APIのレスポンスを識別するための、APIリクエストごとの一意なIDを設定する必要があります。UUIDや、簡単な乱数のようなもので構いません。
* `endpoint`には、あなたがリクエストしたいAPIのエンドポイントを指定します。
* `data`には、エンドポイントのパラメータを含めます。
@@ -154,7 +154,7 @@ APIへリクエストすると、レスポンスがストリームから次の
}
```
-ここで、
+其中:
* `xxxxxxxxxxxxxxxx`の部分には、リクエストの際に設定された`id`が含まれています。これにより、どのリクエストに対するレスポンスなのか判別することができます。
* `body`には、レスポンスが含まれています。
@@ -181,7 +181,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
}
```
-ここで、
+其中:
* `id`にキャプチャしたい投稿の`id`を設定します。
このメッセージを送信すると、Misskeyにキャプチャを要請したことになり、以後、その投稿に関するイベントが流れてくるようになります。
@@ -202,7 +202,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
}
```
-ここで、
+其中:
* `body`内の`id`に、イベントを発生させた投稿のIDが設定されます。
* `body`内の`type`に、イベントの種類が設定されます。
* `body`内の`body`に、イベントの詳細が設定されます。
@@ -285,7 +285,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
}
```
-ここで、
+其中:
* `id`にキャプチャを解除したい投稿の`id`を設定します。
このメッセージを送信すると、以後、その投稿に関するイベントは流れてこないようになります。
diff --git a/src/docs/zh-CN/features/follow.md b/src/docs/zh-CN/features/follow.md
index 115a2786f6..2a95aa6dc4 100644
--- a/src/docs/zh-CN/features/follow.md
+++ b/src/docs/zh-CN/features/follow.md
@@ -1,2 +1,2 @@
-# 关注中
-ユーザーをフォローすると、タイムラインにそのユーザーの投稿が表示されるようになります。ただし、他のユーザーに対する返信は含まれません。 ユーザーをフォローするには、ユーザーページの「フォロー」ボタンをクリックします。フォローを解除するには、もう一度クリックします。
+# 关注
+当您关注一名用户时,您可以在您的时间线上看到该用户的发帖。但是不包含该用户对其他用户的回复。 要关注一名用户,请点击该用户页面上的“关注”按钮。如果需要取消关注,请再次点击该按钮。
diff --git a/src/docs/zh-CN/features/mfm.md b/src/docs/zh-CN/features/mfm.md
index d42350edf0..8bcaacbed7 100644
--- a/src/docs/zh-CN/features/mfm.md
+++ b/src/docs/zh-CN/features/mfm.md
@@ -1,12 +1,12 @@
# MFM
-MFMは、Misskey Flavored Markdownの略で、Misskeyの様々な場所で使用できる専用のマークアップ言語です。 MFMで使用可能な構文は[MFMチートシート](/mfm-cheat-sheet)で確認できます。
+MFM是Misskey Flavored Markdown的缩写,是一种专用的标记语言,可以用在Misskey的任何地方。 MFM中可用的语法可以在[MFM代码速查表](/mfm-cheat-sheet)中找到。
-## MFMが使用可能な場所の例
-- ノート本文
+## 使用 MFM 的位置示例
+- 帖子正文
- CW注释
-- ユーザーの名前
-- ユーザーの自己紹介
+- 用户姓名
+- 用户自我介绍
-## 開発者向け情報
-MFMのパーサー実装はライブラリとして公開されており、簡単にクライアントにMFMを組み込むことが可能です。
-- [misskey-dev/mfm.js](https://github.com/misskey-dev/mfm.js) - JavaScriptパーサー実装
+## 面向开发者的信息
+MFM 的解析器实现作为库发布,可以轻松地将 MFM 嵌入到客户端中。
+- [misskey-dev/mfm.js](https://github.com/misskey-dev/mfm.js) - JavaScript的解析器实现
diff --git a/src/env.ts b/src/env.ts
new file mode 100644
index 0000000000..1b678edc44
--- /dev/null
+++ b/src/env.ts
@@ -0,0 +1,20 @@
+const envOption = {
+ onlyQueue: false,
+ onlyServer: false,
+ noDaemons: false,
+ disableClustering: false,
+ verbose: false,
+ withLogTime: false,
+ quiet: false,
+ slow: false,
+};
+
+for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
+ if (process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]) envOption[key] = true;
+}
+
+if (process.env.NODE_ENV === 'test') envOption.disableClustering = true;
+if (process.env.NODE_ENV === 'test') envOption.quiet = true;
+if (process.env.NODE_ENV === 'test') envOption.noDaemons = true;
+
+export { envOption };
diff --git a/src/mfm/from-html.ts b/src/mfm/from-html.ts
index 4c8e2dbec8..14279f3383 100644
--- a/src/mfm/from-html.ts
+++ b/src/mfm/from-html.ts
@@ -5,7 +5,9 @@ import { URL } from 'url';
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
-export function fromHtml(html: string, hashtagNames?: string[]): string {
+export function fromHtml(html: string, hashtagNames?: string[]): string | null {
+ if (html == null) return null;
+
const dom = parse5.parseFragment(html);
let text = '';
@@ -19,6 +21,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
function getText(node: parse5.Node): string {
if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return '';
+ if (node.nodeName === 'br') return '\n';
if (node.childNodes) {
return node.childNodes.map(n => getText(n)).join('');
@@ -27,6 +30,14 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
return '';
}
+ function appendChildren(childNodes: parse5.ChildNode[]): void {
+ if (childNodes) {
+ for (const n of childNodes) {
+ analyze(n);
+ }
+ }
+ }
+
function analyze(node: parse5.Node) {
if (treeAdapter.isTextNode(node)) {
text += node.value;
@@ -42,6 +53,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
break;
case 'a':
+ {
const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href');
@@ -87,23 +99,111 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
text += generateLink();
}
break;
+ }
- case 'p':
- text += '\n\n';
- if (node.childNodes) {
- for (const n of node.childNodes) {
- analyze(n);
- }
+ case 'h1':
+ {
+ text += '【';
+ appendChildren(node.childNodes);
+ text += '】\n';
+ break;
+ }
+
+ case 'b':
+ case 'strong':
+ {
+ text += '**';
+ appendChildren(node.childNodes);
+ text += '**';
+ break;
+ }
+
+ case 'small':
+ {
+ text += '<small>';
+ appendChildren(node.childNodes);
+ text += '</small>';
+ break;
+ }
+
+ case 's':
+ case 'del':
+ {
+ text += '~~';
+ appendChildren(node.childNodes);
+ text += '~~';
+ break;
+ }
+
+ case 'i':
+ case 'em':
+ {
+ text += '<i>';
+ appendChildren(node.childNodes);
+ text += '</i>';
+ break;
+ }
+
+ // block code (<pre><code>)
+ case 'pre': {
+ if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
+ text += '```\n';
+ text += getText(node.childNodes[0]);
+ text += '\n```\n';
+ } else {
+ appendChildren(node.childNodes);
}
break;
+ }
- default:
- if (node.childNodes) {
- for (const n of node.childNodes) {
- analyze(n);
- }
+ // inline code (<code>)
+ case 'code': {
+ text += '`';
+ appendChildren(node.childNodes);
+ text += '`';
+ break;
+ }
+
+ case 'blockquote': {
+ const t = getText(node);
+ if (t) {
+ text += '> ';
+ text += t.split('\n').join(`\n> `);
}
break;
+ }
+
+ case 'p':
+ case 'h2':
+ case 'h3':
+ case 'h4':
+ case 'h5':
+ case 'h6':
+ {
+ text += '\n\n';
+ appendChildren(node.childNodes);
+ break;
+ }
+
+ // other block elements
+ case 'div':
+ case 'header':
+ case 'footer':
+ case 'article':
+ case 'li':
+ case 'dt':
+ case 'dd':
+ {
+ text += '\n';
+ appendChildren(node.childNodes);
+ break;
+ }
+
+ default: // includes inline elements
+ {
+ appendChildren(node.childNodes);
+ break;
+ }
}
}
}
diff --git a/src/misc/download-url.ts b/src/misc/download-url.ts
index 8a8640a8cd..c96b4fd1d6 100644
--- a/src/misc/download-url.ts
+++ b/src/misc/download-url.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as stream from 'stream';
import * as util from 'util';
import got, * as Got from 'got';
-import { httpAgent, httpsAgent } from './fetch';
+import { httpAgent, httpsAgent, StatusError } from './fetch';
import config from '@/config/index';
import * as chalk from 'chalk';
import Logger from '@/services/logger';
@@ -37,6 +37,7 @@ export async function downloadUrl(url: string, path: string) {
http: httpAgent,
https: httpsAgent,
},
+ http2: false, // default
retry: 0,
}).on('response', (res: Got.Response) => {
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) {
@@ -59,17 +60,17 @@ export async function downloadUrl(url: string, path: string) {
logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
req.destroy();
}
- }).on('error', (e: any) => {
- if (e.name === 'HTTPError') {
- const statusCode = e.response?.statusCode;
- const statusMessage = e.response?.statusMessage;
- e.name = `StatusError`;
- e.statusCode = statusCode;
- e.message = `${statusCode} ${statusMessage}`;
- }
});
- await pipeline(req, fs.createWriteStream(path));
+ try {
+ await pipeline(req, fs.createWriteStream(path));
+ } catch (e) {
+ if (e instanceof Got.HTTPError) {
+ throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
+ } else {
+ throw e;
+ }
+ }
logger.succ(`Download finished: ${chalk.cyan(url)}`);
}
diff --git a/src/misc/fetch.ts b/src/misc/fetch.ts
index 82db2f2f8c..f4f16a27e2 100644
--- a/src/misc/fetch.ts
+++ b/src/misc/fetch.ts
@@ -1,51 +1,62 @@
import * as http from 'http';
import * as https from 'https';
import CacheableLookup from 'cacheable-lookup';
-import fetch, { HeadersInit } from 'node-fetch';
+import fetch from 'node-fetch';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import config from '@/config/index';
import { URL } from 'url';
-export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: HeadersInit) {
- const res = await fetch(url, {
+export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) {
+ const res = await getResponse({
+ url,
+ method: 'GET',
headers: Object.assign({
'User-Agent': config.userAgent,
Accept: accept
}, headers || {}),
- timeout,
- agent: getAgentByUrl,
+ timeout
});
- if (!res.ok) {
- throw {
- name: `StatusError`,
- statusCode: res.status,
- message: `${res.status} ${res.statusText}`,
- };
- }
-
return await res.json();
}
-export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: HeadersInit) {
- const res = await fetch(url, {
+export async function getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record<string, string>) {
+ const res = await getResponse({
+ url,
+ method: 'GET',
headers: Object.assign({
'User-Agent': config.userAgent,
Accept: accept
}, headers || {}),
+ timeout
+ });
+
+ return await res.text();
+}
+
+export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) {
+ const timeout = args?.timeout || 10 * 1000;
+
+ const controller = new AbortController();
+ setTimeout(() => {
+ controller.abort();
+ }, timeout * 6);
+
+ const res = await fetch(args.url, {
+ method: args.method,
+ headers: args.headers,
+ body: args.body,
timeout,
+ size: args?.size || 10 * 1024 * 1024,
agent: getAgentByUrl,
+ signal: controller.signal,
});
if (!res.ok) {
- throw {
- name: `StatusError`,
- statusCode: res.status,
- message: `${res.status} ${res.statusText}`,
- };
+ throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
}
- return await res.text();
+ return res;
}
const cache = new CacheableLookup({
@@ -114,3 +125,17 @@ export function getAgentByUrl(url: URL, bypassProxy = false) {
return url.protocol == 'http:' ? httpAgent : httpsAgent;
}
}
+
+export class StatusError extends Error {
+ public statusCode: number;
+ public statusMessage?: string;
+ public isClientError: boolean;
+
+ constructor(message: string, statusCode: number, statusMessage?: string) {
+ super(message);
+ this.name = 'StatusError';
+ this.statusCode = statusCode;
+ this.statusMessage = statusMessage;
+ this.isClientError = typeof this.statusCode === 'number' && this.statusCode >= 400 && this.statusCode < 500;
+ }
+}
diff --git a/src/misc/hard-limits.ts b/src/misc/hard-limits.ts
index 2a61cb321b..1039f7335a 100644
--- a/src/misc/hard-limits.ts
+++ b/src/misc/hard-limits.ts
@@ -6,3 +6,9 @@
* Surrogate pairs count as one
*/
export const DB_MAX_NOTE_TEXT_LENGTH = 8192;
+
+/**
+ * Maximum image description length that can be stored in DB.
+ * Surrogate pairs count as one
+ */
+export const DB_MAX_IMAGE_COMMENT_LENGTH = 512;
diff --git a/src/misc/truncate.ts b/src/misc/truncate.ts
new file mode 100644
index 0000000000..cb120331a1
--- /dev/null
+++ b/src/misc/truncate.ts
@@ -0,0 +1,11 @@
+import { substring } from 'stringz';
+
+export function truncate(input: string, size: number): string;
+export function truncate(input: string | undefined, size: number): string | undefined;
+export function truncate(input: string | undefined, size: number): string | undefined {
+ if (!input) {
+ return input;
+ } else {
+ return substring(input, 0, size);
+ }
+}
diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts
index 6428aacdf1..9a1a87c155 100644
--- a/src/models/entities/meta.ts
+++ b/src/models/entities/meta.ts
@@ -151,6 +151,11 @@ export class Meta {
@Column('boolean', {
default: false,
})
+ public emailRequiredForSignup: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
public enableHcaptcha: boolean;
@Column('varchar', {
diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts
index 988fdb341f..47184caacc 100644
--- a/src/models/entities/notification.ts
+++ b/src/models/entities/notification.ts
@@ -5,7 +5,7 @@ import { Note } from './note';
import { FollowRequest } from './follow-request';
import { UserGroupInvitation } from './user-group-invitation';
import { AccessToken } from './access-token';
-import { notificationTypes } from '../../types';
+import { notificationTypes } from '@/types';
@Entity()
export class Notification {
diff --git a/src/models/entities/user-pending.ts b/src/models/entities/user-pending.ts
new file mode 100644
index 0000000000..40482af333
--- /dev/null
+++ b/src/models/entities/user-pending.ts
@@ -0,0 +1,32 @@
+import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
+import { id } from '../id';
+
+@Entity()
+export class UserPending {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone')
+ public createdAt: Date;
+
+ @Index({ unique: true })
+ @Column('varchar', {
+ length: 128,
+ })
+ public code: string;
+
+ @Column('varchar', {
+ length: 128,
+ })
+ public username: string;
+
+ @Column('varchar', {
+ length: 128,
+ })
+ public email: string;
+
+ @Column('varchar', {
+ length: 128,
+ })
+ public password: string;
+}
diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts
index 3a9043fac6..a2da07d76f 100644
--- a/src/models/entities/user-profile.ts
+++ b/src/models/entities/user-profile.ts
@@ -2,7 +2,7 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'type
import { id } from '../id';
import { User } from './user';
import { Page } from './page';
-import { notificationTypes } from '../../types';
+import { notificationTypes } from '@/types';
// TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも
// ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン
diff --git a/src/models/index.ts b/src/models/index.ts
index 9f8bd104e9..059a3d7b87 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -62,6 +62,7 @@ import { ChannelNotePining } from './entities/channel-note-pining';
import { RegistryItem } from './entities/registry-item';
import { Ad } from './entities/ad';
import { PasswordResetRequest } from './entities/password-reset-request';
+import { UserPending } from './entities/user-pending';
export const Announcements = getRepository(Announcement);
export const AnnouncementReads = getRepository(AnnouncementRead);
@@ -76,6 +77,7 @@ export const PollVotes = getRepository(PollVote);
export const Users = getCustomRepository(UserRepository);
export const UserProfiles = getRepository(UserProfile);
export const UserKeypairs = getRepository(UserKeypair);
+export const UserPendings = getRepository(UserPending);
export const AttestationChallenges = getRepository(AttestationChallenge);
export const UserSecurityKeys = getRepository(UserSecurityKey);
export const UserPublickeys = getRepository(UserPublickey);
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index c0ac22b2db..0f00c34c9c 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -272,6 +272,7 @@ export class NoteRepository extends Repository<Note> {
const tokens = packed.text ? mfm.parse(packed.text) : [];
mfm.inspect(tokens, node => {
if (node.type === 'text') {
+ // TODO: quoteなtextはskip
node.props.text = nyaize(node.props.text);
}
});
diff --git a/src/prelude/url.ts b/src/prelude/url.ts
index a3613fc9b9..c7f2b7c1e7 100644
--- a/src/prelude/url.ts
+++ b/src/prelude/url.ts
@@ -1,9 +1,11 @@
-import { stringify } from 'querystring';
-
export function query(obj: {}): string {
- return stringify(Object.entries(obj)
+ const params = Object.entries(obj)
.filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
- .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>));
+ .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>);
+
+ return Object.entries(params)
+ .map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
+ .join('&');
}
export function appendQuery(url: string, query: string): string {
diff --git a/src/queue/index.ts b/src/queue/index.ts
index 0ce10a4c60..1e1d5da5a2 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -1,7 +1,7 @@
import * as httpSignature from 'http-signature';
import config from '@/config/index';
-import { program } from '../argv';
+import { envOption } from '../env';
import processDeliver from './processors/deliver';
import processInbox from './processors/inbox';
@@ -173,7 +173,7 @@ export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']
});
}
-export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; }) {
+export function createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) {
return dbQueue.add('deleteAccount', {
user: user,
soft: opts.soft
@@ -200,7 +200,7 @@ export function createCleanRemoteFilesJob() {
}
export default function() {
- if (!program.onlyServer) {
+ if (!envOption.onlyServer) {
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
processDb(dbQueue);
diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts
index 373e57cbd5..3c61896a2f 100644
--- a/src/queue/processors/deliver.ts
+++ b/src/queue/processors/deliver.ts
@@ -11,6 +11,7 @@ import { toPuny } from '@/misc/convert-host';
import { Cache } from '@/misc/cache';
import { Instance } from '@/models/entities/instance';
import { DeliverJobData } from '../types';
+import { StatusError } from '@/misc/fetch';
const logger = new Logger('deliver');
@@ -68,16 +69,16 @@ export default async (job: Bull.Job<DeliverJobData>) => {
registerOrFetchInstanceDoc(host).then(i => {
Instances.update(i.id, {
latestRequestSentAt: new Date(),
- latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null,
+ latestStatus: res instanceof StatusError ? res.statusCode : null,
isNotResponding: true
});
instanceChart.requestSent(i.host, false);
});
- if (res != null && res.hasOwnProperty('statusCode')) {
+ if (res instanceof StatusError) {
// 4xx
- if (res.statusCode >= 400 && res.statusCode < 500) {
+ if (res.isClientError) {
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
// 何回再送しても成功することはないということなのでエラーにはしないでおく
return `${res.statusCode} ${res.statusMessage}`;
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index e2c271cdf8..4032ce8653 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -14,6 +14,7 @@ import { InboxJobData } from '../types';
import DbResolver from '@/remote/activitypub/db-resolver';
import { resolvePerson } from '@/remote/activitypub/models/person';
import { LdSignature } from '@/remote/activitypub/misc/ld-signature';
+import { StatusError } from '@/misc/fetch';
const logger = new Logger('inbox');
@@ -53,7 +54,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
authUser = await dbResolver.getAuthUserFromApId(getApId(activity.actor));
} catch (e) {
// 対象が4xxならスキップ
- if (e.statusCode >= 400 && e.statusCode < 500) {
+ if (e instanceof StatusError && e.isClientError) {
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
}
throw `Error in actor ${activity.actor} - ${e.statusCode || e}`;
diff --git a/src/remote/activitypub/ap-request.ts b/src/remote/activitypub/ap-request.ts
new file mode 100644
index 0000000000..76a3857140
--- /dev/null
+++ b/src/remote/activitypub/ap-request.ts
@@ -0,0 +1,104 @@
+import * as crypto from 'crypto';
+import { URL } from 'url';
+
+type Request = {
+ url: string;
+ method: string;
+ headers: Record<string, string>;
+};
+
+type PrivateKey = {
+ privateKeyPem: string;
+ keyId: string;
+};
+
+export function createSignedPost(args: { key: PrivateKey, url: string, body: string, additionalHeaders: Record<string, string> }) {
+ const u = new URL(args.url);
+ const digestHeader = `SHA-256=${crypto.createHash('sha256').update(args.body).digest('base64')}`;
+
+ const request: Request = {
+ url: u.href,
+ method: 'POST',
+ headers: objectAssignWithLcKey({
+ 'Date': new Date().toUTCString(),
+ 'Host': u.hostname,
+ 'Content-Type': 'application/activity+json',
+ 'Digest': digestHeader,
+ }, args.additionalHeaders),
+ };
+
+ const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']);
+
+ return {
+ request,
+ signingString: result.signingString,
+ signature: result.signature,
+ signatureHeader: result.signatureHeader,
+ };
+}
+
+export function createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record<string, string> }) {
+ const u = new URL(args.url);
+
+ const request: Request = {
+ url: u.href,
+ method: 'GET',
+ headers: objectAssignWithLcKey({
+ 'Accept': 'application/activity+json, application/ld+json',
+ 'Date': new Date().toUTCString(),
+ 'Host': new URL(args.url).hostname,
+ }, args.additionalHeaders),
+ };
+
+ const result = signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']);
+
+ return {
+ request,
+ signingString: result.signingString,
+ signature: result.signature,
+ signatureHeader: result.signatureHeader,
+ };
+}
+
+function signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]) {
+ const signingString = genSigningString(request, includeHeaders);
+ const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64');
+ const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`;
+
+ request.headers = objectAssignWithLcKey(request.headers, {
+ Signature: signatureHeader
+ });
+
+ return {
+ request,
+ signingString,
+ signature,
+ signatureHeader,
+ };
+}
+
+function genSigningString(request: Request, includeHeaders: string[]) {
+ request.headers = lcObjectKey(request.headers);
+
+ const results: string[] = [];
+
+ for (const key of includeHeaders.map(x => x.toLowerCase())) {
+ if (key === '(request-target)') {
+ results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`);
+ } else {
+ results.push(`${key}: ${request.headers[key]}`);
+ }
+ }
+
+ return results.join('\n');
+}
+
+function lcObjectKey(src: Record<string, string>) {
+ const dst: Record<string, string> = {};
+ for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
+ return dst;
+}
+
+function objectAssignWithLcKey(a: Record<string, string>, b: Record<string, string>) {
+ return Object.assign(lcObjectKey(a), lcObjectKey(b));
+}
diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts
index b6ec090b99..5230867f24 100644
--- a/src/remote/activitypub/kernel/announce/note.ts
+++ b/src/remote/activitypub/kernel/announce/note.ts
@@ -8,6 +8,7 @@ import { extractDbHost } from '@/misc/convert-host';
import { fetchMeta } from '@/misc/fetch-meta';
import { getApLock } from '@/misc/app-lock';
import { parseAudience } from '../../audience';
+import { StatusError } from '@/misc/fetch';
const logger = apLogger;
@@ -41,7 +42,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
renote = await resolveNote(targetUri);
} catch (e) {
// 対象が4xxならスキップ
- if (e.statusCode >= 400 && e.statusCode < 500) {
+ if (e instanceof StatusError && e.isClientError) {
logger.warn(`Ignored announce target ${targetUri} - ${e.statusCode}`);
return;
}
diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts
index 5dda85d0f5..14e311e4cd 100644
--- a/src/remote/activitypub/kernel/create/note.ts
+++ b/src/remote/activitypub/kernel/create/note.ts
@@ -4,6 +4,7 @@ import { createNote, fetchNote } from '../../models/note';
import { getApId, IObject, ICreate } from '../../type';
import { getApLock } from '@/misc/app-lock';
import { extractDbHost } from '@/misc/convert-host';
+import { StatusError } from '@/misc/fetch';
/**
* 投稿作成アクティビティを捌きます
@@ -32,7 +33,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, note: IObj
await createNote(note, resolver, silent);
return 'ok';
} catch (e) {
- if (e.statusCode >= 400 && e.statusCode < 500) {
+ if (e instanceof StatusError && e.isClientError) {
return `skip ${e.statusCode}`;
} else {
throw e;
diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts
index cd28d59a16..d0a96e4313 100644
--- a/src/remote/activitypub/models/image.ts
+++ b/src/remote/activitypub/models/image.ts
@@ -5,6 +5,8 @@ import { fetchMeta } from '@/misc/fetch-meta';
import { apLogger } from '../logger';
import { DriveFile } from '@/models/entities/drive-file';
import { DriveFiles } from '@/models/index';
+import { truncate } from '@/misc/truncate';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
const logger = apLogger;
@@ -28,7 +30,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
const instance = await fetchMeta();
const cache = instance.cacheRemoteFiles;
- let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, image.name);
+ let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache, truncate(image.name, DB_MAX_IMAGE_COMMENT_LENGTH));
if (file.isLink) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index 25004cb4d2..cf68f3005d 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -26,6 +26,7 @@ import { createMessage } from '@/services/messages/create';
import { parseAudience } from '../audience';
import { extractApMentions } from './mention';
import DbResolver from '../db-resolver';
+import { StatusError } from '@/misc/fetch';
const logger = apLogger;
@@ -177,7 +178,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
}
} catch (e) {
return {
- status: e.statusCode >= 400 && e.statusCode < 500 ? 'permerror' : 'temperror'
+ status: (e instanceof StatusError && e.isClientError) ? 'permerror' : 'temperror'
};
}
};
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index 4823def7cb..84b2f0c51c 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -28,22 +28,13 @@ import { getConnection } from 'typeorm';
import { toArray } from '@/prelude/array';
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata';
import { normalizeForSearch } from '@/misc/normalize-for-search';
+import { truncate } from '@/misc/truncate';
const logger = apLogger;
const nameLength = 128;
const summaryLength = 2048;
-function truncate(input: string, size: number): string;
-function truncate(input: string | undefined, size: number): string | undefined;
-function truncate(input: string | undefined, size: number): string | undefined {
- if (!input || input.length <= size) {
- return input;
- } else {
- return input.substring(0, size);
- }
-}
-
/**
* Validate and convert to actor object
* @param x Fetched object
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index fe1009243c..d6ced630c1 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -1,66 +1,31 @@
-import * as http from 'http';
-import * as https from 'https';
-import { sign } from 'http-signature';
-import * as crypto from 'crypto';
-
import config from '@/config/index';
-import { User } from '@/models/entities/user';
-import { getAgentByUrl } from '@/misc/fetch';
-import { URL } from 'url';
-import got from 'got';
-import * as Got from 'got';
import { getUserKeypair } from '@/misc/keypair-store';
+import { User } from '@/models/entities/user';
+import { getResponse } from '../../misc/fetch';
+import { createSignedPost, createSignedGet } from './ap-request';
export default async (user: { id: User['id'] }, url: string, object: any) => {
- const timeout = 10 * 1000;
-
- const { protocol, hostname, port, pathname, search } = new URL(url);
-
- const data = JSON.stringify(object);
-
- const sha256 = crypto.createHash('sha256');
- sha256.update(data);
- const hash = sha256.digest('base64');
+ const body = JSON.stringify(object);
const keypair = await getUserKeypair(user.id);
- await new Promise<void>((resolve, reject) => {
- const req = https.request({
- agent: getAgentByUrl(new URL(`https://example.net`)),
- protocol,
- hostname,
- port,
- method: 'POST',
- path: pathname + search,
- timeout,
- headers: {
- 'User-Agent': config.userAgent,
- 'Content-Type': 'application/activity+json',
- 'Digest': `SHA-256=${hash}`
- }
- }, res => {
- if (res.statusCode! >= 400) {
- reject(res);
- } else {
- resolve();
- }
- });
-
- sign(req, {
- authorizationHeaderName: 'Signature',
- key: keypair.privateKey,
- keyId: `${config.url}/users/${user.id}#main-key`,
- headers: ['(request-target)', 'date', 'host', 'digest']
- });
-
- req.on('timeout', () => req.abort());
-
- req.on('error', e => {
- if (req.aborted) reject('timeout');
- reject(e);
- });
+ const req = createSignedPost({
+ key: {
+ privateKeyPem: keypair.privateKey,
+ keyId: `${config.url}/users/${user.id}#main-key`
+ },
+ url,
+ body,
+ additionalHeaders: {
+ 'User-Agent': config.userAgent,
+ }
+ });
- req.end(data);
+ await getResponse({
+ url,
+ method: req.request.method,
+ headers: req.request.headers,
+ body,
});
};
@@ -70,87 +35,24 @@ export default async (user: { id: User['id'] }, url: string, object: any) => {
* @param url URL to fetch
*/
export async function signedGet(url: string, user: { id: User['id'] }) {
- const timeout = 10 * 1000;
-
const keypair = await getUserKeypair(user.id);
- const req = got.get<any>(url, {
- headers: {
- 'Accept': 'application/activity+json, application/ld+json',
- 'User-Agent': config.userAgent,
+ const req = createSignedGet({
+ key: {
+ privateKeyPem: keypair.privateKey,
+ keyId: `${config.url}/users/${user.id}#main-key`
},
- responseType: 'json',
- timeout,
- hooks: {
- beforeRequest: [
- options => {
- options.request = (url: URL, opt: http.RequestOptions, callback?: (response: any) => void) => {
- // Select custom agent by URL
- opt.agent = getAgentByUrl(url, false);
-
- // Wrap original https?.request
- const requestFunc = url.protocol === 'http:' ? http.request : https.request;
- const clientRequest = requestFunc(url, opt, callback) as http.ClientRequest;
-
- // HTTP-Signature
- sign(clientRequest, {
- authorizationHeaderName: 'Signature',
- key: keypair.privateKey,
- keyId: `${config.url}/users/${user.id}#main-key`,
- headers: ['(request-target)', 'host', 'date', 'accept']
- });
-
- return clientRequest;
- };
- },
- ],
- },
- retry: 0,
- });
-
- const res = await receiveResponce(req, 10 * 1024 * 1024);
-
- return res.body;
-}
-
-/**
- * Receive response (with size limit)
- * @param req Request
- * @param maxSize size limit
- */
-export async function receiveResponce<T>(req: Got.CancelableRequest<Got.Response<T>>, maxSize: number) {
- // 応答ヘッダでサイズチェック
- req.on('response', (res: Got.Response) => {
- const contentLength = res.headers['content-length'];
- if (contentLength != null) {
- const size = Number(contentLength);
- if (size > maxSize) {
- req.cancel();
- }
- }
- });
-
- // 受信中のデータでサイズチェック
- req.on('downloadProgress', (progress: Got.Progress) => {
- if (progress.transferred > maxSize) {
- req.cancel();
+ url,
+ additionalHeaders: {
+ 'User-Agent': config.userAgent,
}
});
- // 応答取得 with ステータスコードエラーの整形
- const res = await req.catch(e => {
- if (e.name === 'HTTPError') {
- const statusCode = (e as Got.HTTPError).response.statusCode;
- const statusMessage = (e as Got.HTTPError).response.statusMessage;
- throw {
- name: `StatusError`,
- statusCode,
- message: `${statusCode} ${statusMessage}`,
- };
- } else {
- throw e;
- }
+ const res = await getResponse({
+ url,
+ method: req.request.method,
+ headers: req.request.headers
});
- return res;
+ return await res.json();
}
diff --git a/src/server/api/common/signup.ts b/src/server/api/common/signup.ts
index eb3aa09c8c..2ba0d8e479 100644
--- a/src/server/api/common/signup.ts
+++ b/src/server/api/common/signup.ts
@@ -11,20 +11,30 @@ import { UserKeypair } from '@/models/entities/user-keypair';
import { usersChart } from '@/services/chart/index';
import { UsedUsername } from '@/models/entities/used-username';
-export async function signup(username: User['username'], password: UserProfile['password'], host: string | null = null) {
+export async function signup(opts: {
+ username: User['username'];
+ password?: string | null;
+ passwordHash?: UserProfile['password'] | null;
+ host?: string | null;
+}) {
+ const { username, password, passwordHash, host } = opts;
+ let hash = passwordHash;
+
// Validate username
if (!Users.validateLocalUsername.ok(username)) {
throw new Error('INVALID_USERNAME');
}
- // Validate password
- if (!Users.validatePassword.ok(password)) {
- throw new Error('INVALID_PASSWORD');
- }
+ if (password != null && passwordHash == null) {
+ // Validate password
+ if (!Users.validatePassword.ok(password)) {
+ throw new Error('INVALID_PASSWORD');
+ }
- // Generate hash of password
- const salt = await bcrypt.genSalt(8);
- const hash = await bcrypt.hash(password, salt);
+ // Generate hash of password
+ const salt = await bcrypt.genSalt(8);
+ hash = await bcrypt.hash(password, salt);
+ }
// Generate secret
const secret = generateUserToken();
diff --git a/src/server/api/endpoints/admin/accounts/create.ts b/src/server/api/endpoints/admin/accounts/create.ts
index 9691b9c7e3..fa15e84f77 100644
--- a/src/server/api/endpoints/admin/accounts/create.ts
+++ b/src/server/api/endpoints/admin/accounts/create.ts
@@ -35,7 +35,10 @@ export default define(meta, async (ps, _me) => {
})) === 0;
if (!noUsers && !me?.isAdmin) throw new Error('access denied');
- const { account, secret } = await signup(ps.username, ps.password);
+ const { account, secret } = await signup({
+ username: ps.username,
+ password: ps.password,
+ });
const res = await Users.pack(account, account, {
detail: true,
diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts
index c0788c8e02..fe1c799805 100644
--- a/src/server/api/endpoints/admin/drive/files.ts
+++ b/src/server/api/endpoints/admin/drive/files.ts
@@ -25,7 +25,7 @@ export const meta = {
},
type: {
- validator: $.optional.nullable.str.match(/^[a-zA-Z\/\-*]+$/)
+ validator: $.optional.nullable.str.match(/^[a-zA-Z0-9\/\-*]+$/)
},
origin: {
diff --git a/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index 59e5c834ed..1925906c28 100644
--- a/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -1,6 +1,6 @@
import { URL } from 'url';
import define from '../../../define';
-import { inboxQueue } from '@/queue/index';
+import { inboxQueue } from '@/queue/queues';
export const meta = {
tags: ['admin'],
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index 46f30fef7d..55447098dc 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -93,6 +93,10 @@ export const meta = {
validator: $.optional.bool,
},
+ emailRequiredForSignup: {
+ validator: $.optional.bool,
+ },
+
enableHcaptcha: {
validator: $.optional.bool,
},
@@ -374,6 +378,10 @@ export default define(meta, async (ps, me) => {
set.proxyRemoteFiles = ps.proxyRemoteFiles;
}
+ if (ps.emailRequiredForSignup !== undefined) {
+ set.emailRequiredForSignup = ps.emailRequiredForSignup;
+ }
+
if (ps.enableHcaptcha !== undefined) {
set.enableHcaptcha = ps.enableHcaptcha;
}
diff --git a/src/server/api/endpoints/ap/get.ts b/src/server/api/endpoints/ap/get.ts
index 2cffce1f16..78919f43b0 100644
--- a/src/server/api/endpoints/ap/get.ts
+++ b/src/server/api/endpoints/ap/get.ts
@@ -2,11 +2,17 @@ import $ from 'cafy';
import define from '../../define';
import Resolver from '@/remote/activitypub/resolver';
import { ApiError } from '../../error';
+import * as ms from 'ms';
export const meta = {
tags: ['federation'],
- requireCredential: false as const,
+ requireCredential: true as const,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 30
+ },
params: {
uri: {
diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts
index aa0dae070c..2280d93724 100644
--- a/src/server/api/endpoints/ap/show.ts
+++ b/src/server/api/endpoints/ap/show.ts
@@ -11,11 +11,17 @@ import { Note } from '@/models/entities/note';
import { User } from '@/models/entities/user';
import { fetchMeta } from '@/misc/fetch-meta';
import { isActor, isPost, getApId } from '@/remote/activitypub/type';
+import * as ms from 'ms';
export const meta = {
tags: ['federation'],
- requireCredential: false as const,
+ requireCredential: true as const,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 30
+ },
params: {
uri: {
diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts
index 1bf5cf374b..4deaa39974 100644
--- a/src/server/api/endpoints/blocking/create.ts
+++ b/src/server/api/endpoints/blocking/create.ts
@@ -43,6 +43,12 @@ export const meta = {
code: 'ALREADY_BLOCKING',
id: '787fed64-acb9-464a-82eb-afbd745b9614'
},
+
+ cannotBlockModerator: {
+ message: 'Cannot block a moderator or an admin.',
+ code: 'CANNOT_BLOCK_MODERATOR',
+ id: '8544aaef-89fb-e470-9f6c-385d38b474f5'
+ }
},
res: {
@@ -76,8 +82,12 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.alreadyBlocking);
}
- // Create blocking
- await create(blocker, blockee);
+ try {
+ await create(blocker, blockee);
+ } catch (e) {
+ if (e.id === 'e42b7890-5e4d-9d9c-d54b-cf4dd30adfb5') throw new ApiError(meta.errors.cannotBlockModerator);
+ throw e;
+ }
NoteWatchings.delete({
userId: blocker.id,
diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts
index 1ef445625c..f277a9c3dc 100644
--- a/src/server/api/endpoints/drive/files/update.ts
+++ b/src/server/api/endpoints/drive/files/update.ts
@@ -4,6 +4,7 @@ import { publishDriveStream } from '@/services/stream';
import define from '../../../define';
import { ApiError } from '../../../error';
import { DriveFiles, DriveFolders } from '@/models/index';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
export const meta = {
tags: ['drive'],
@@ -33,7 +34,7 @@ export const meta = {
},
comment: {
- validator: $.optional.nullable.str,
+ validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
default: undefined as any,
}
},
diff --git a/src/server/api/endpoints/drive/files/upload-from-url.ts b/src/server/api/endpoints/drive/files/upload-from-url.ts
index f37f316efb..9f10a42d24 100644
--- a/src/server/api/endpoints/drive/files/upload-from-url.ts
+++ b/src/server/api/endpoints/drive/files/upload-from-url.ts
@@ -5,6 +5,7 @@ import uploadFromUrl from '@/services/drive/upload-from-url';
import define from '../../../define';
import { DriveFiles } from '@/models/index';
import { publishMainStream } from '@/services/stream';
+import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits';
export const meta = {
tags: ['drive'],
@@ -35,7 +36,7 @@ export const meta = {
},
comment: {
- validator: $.optional.nullable.str,
+ validator: $.optional.nullable.str.max(DB_MAX_IMAGE_COMMENT_LENGTH),
default: null,
},
diff --git a/src/server/api/endpoints/email-address/available.ts b/src/server/api/endpoints/email-address/available.ts
new file mode 100644
index 0000000000..65fe6f9178
--- /dev/null
+++ b/src/server/api/endpoints/email-address/available.ts
@@ -0,0 +1,37 @@
+import $ from 'cafy';
+import define from '../../define';
+import { UserProfiles } from '@/models/index';
+
+export const meta = {
+ tags: ['users'],
+
+ requireCredential: false as const,
+
+ params: {
+ emailAddress: {
+ validator: $.str
+ }
+ },
+
+ res: {
+ type: 'object' as const,
+ optional: false as const, nullable: false as const,
+ properties: {
+ available: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const,
+ }
+ }
+ }
+};
+
+export default define(meta, async (ps) => {
+ const exist = await UserProfiles.count({
+ emailVerified: true,
+ email: ps.emailAddress,
+ });
+
+ return {
+ available: exist === 0
+ };
+});
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 3c265a10c1..fcabbbc3dd 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -4,7 +4,7 @@ import { readNotification } from '../../common/read-notification';
import define from '../../define';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { Notifications, Followings, Mutings, Users } from '@/models/index';
-import { notificationTypes } from '../../../../types';
+import { notificationTypes } from '@/types';
import read from '@/services/note/read';
export const meta = {
@@ -33,6 +33,11 @@ export const meta = {
default: false
},
+ unreadOnly: {
+ validator: $.optional.bool,
+ default: false
+ },
+
markAsRead: {
validator: $.optional.bool,
default: true
@@ -105,6 +110,10 @@ export default define(meta, async (ps, user) => {
query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes });
}
+ if (ps.unreadOnly) {
+ query.andWhere(`notification.isRead = false`);
+ }
+
const notifications = await query.take(ps.limit!).getMany();
// Mark all as read
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index fb7e12760e..9dd637251d 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -13,7 +13,7 @@ import { ApiError } from '../../error';
import { Users, DriveFiles, UserProfiles, Pages } from '@/models/index';
import { User } from '@/models/entities/user';
import { UserProfile } from '@/models/entities/user-profile';
-import { notificationTypes } from '../../../../types';
+import { notificationTypes } from '@/types';
import { normalizeForSearch } from '@/misc/normalize-for-search';
export const meta = {
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 3f422dff07..ce21556243 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -104,6 +104,10 @@ export const meta = {
type: 'boolean' as const,
optional: false as const, nullable: false as const
},
+ emailRequiredForSignup: {
+ type: 'boolean' as const,
+ optional: false as const, nullable: false as const
+ },
enableHcaptcha: {
type: 'boolean' as const,
optional: false as const, nullable: false as const
@@ -488,6 +492,7 @@ export default define(meta, async (ps, me) => {
disableGlobalTimeline: instance.disableGlobalTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
+ emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableRecaptcha: instance.enableRecaptcha,
@@ -537,6 +542,7 @@ export default define(meta, async (ps, me) => {
registration: !instance.disableRegistration,
localTimeLine: !instance.disableLocalTimeline,
globalTimeLine: !instance.disableGlobalTimeline,
+ emailRequiredForSignup: instance.emailRequiredForSignup,
elasticsearch: config.elasticsearch ? true : false,
hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha,
diff --git a/src/server/api/endpoints/users/groups/leave.ts b/src/server/api/endpoints/users/groups/leave.ts
new file mode 100644
index 0000000000..0e52f2abdf
--- /dev/null
+++ b/src/server/api/endpoints/users/groups/leave.ts
@@ -0,0 +1,50 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { UserGroups, UserGroupJoinings } from '@/models/index';
+
+export const meta = {
+ tags: ['groups', 'users'],
+
+ requireCredential: true as const,
+
+ kind: 'write:user-groups',
+
+ params: {
+ groupId: {
+ validator: $.type(ID),
+ },
+ },
+
+ errors: {
+ noSuchGroup: {
+ message: 'No such group.',
+ code: 'NO_SUCH_GROUP',
+ id: '62780270-1f67-5dc0-daca-3eb510612e31'
+ },
+
+ youAreOwner: {
+ message: 'Your are the owner.',
+ code: 'YOU_ARE_OWNER',
+ id: 'b6d6e0c2-ef8a-9bb8-653d-79f4a3107c69'
+ },
+ }
+};
+
+export default define(meta, async (ps, me) => {
+ // Fetch the group
+ const userGroup = await UserGroups.findOne({
+ id: ps.groupId,
+ });
+
+ if (userGroup == null) {
+ throw new ApiError(meta.errors.noSuchGroup);
+ }
+
+ if (me.id === userGroup.userId) {
+ throw new ApiError(meta.errors.youAreOwner);
+ }
+
+ await UserGroupJoinings.delete({ userGroupId: userGroup.id, userId: me.id });
+});
diff --git a/src/server/api/index.ts b/src/server/api/index.ts
index db35fdf9e0..82579075eb 100644
--- a/src/server/api/index.ts
+++ b/src/server/api/index.ts
@@ -12,6 +12,7 @@ import endpoints from './endpoints';
import handler from './api-handler';
import signup from './private/signup';
import signin from './private/signin';
+import signupPending from './private/signup-pending';
import discord from './service/discord';
import github from './service/github';
import twitter from './service/twitter';
@@ -65,6 +66,7 @@ for (const endpoint of endpoints) {
router.post('/signup', signup);
router.post('/signin', signin);
+router.post('/signup-pending', signupPending);
router.use(discord.routes());
router.use(github.routes());
diff --git a/src/server/api/private/signup-pending.ts b/src/server/api/private/signup-pending.ts
new file mode 100644
index 0000000000..c0638a1cda
--- /dev/null
+++ b/src/server/api/private/signup-pending.ts
@@ -0,0 +1,35 @@
+import * as Koa from 'koa';
+import { Users, UserPendings, UserProfiles } from '@/models/index';
+import { signup } from '../common/signup';
+import signin from '../common/signin';
+
+export default async (ctx: Koa.Context) => {
+ const body = ctx.request.body;
+
+ const code = body['code'];
+
+ try {
+ const pendingUser = await UserPendings.findOneOrFail({ code });
+
+ const { account, secret } = await signup({
+ username: pendingUser.username,
+ passwordHash: pendingUser.password,
+ });
+
+ UserPendings.delete({
+ id: pendingUser.id,
+ });
+
+ const profile = await UserProfiles.findOneOrFail(account.id);
+
+ await UserProfiles.update({ userId: profile.userId }, {
+ email: pendingUser.email,
+ emailVerified: true,
+ emailVerifyCode: null,
+ });
+
+ signin(ctx, account);
+ } catch (e) {
+ ctx.throw(400, e);
+ }
+};
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index ef61767f65..93caaea935 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -1,8 +1,13 @@
import * as Koa from 'koa';
+import rndstr from 'rndstr';
+import * as bcrypt from 'bcryptjs';
import { fetchMeta } from '@/misc/fetch-meta';
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha';
-import { Users, RegistrationTickets } from '@/models/index';
+import { Users, RegistrationTickets, UserPendings } from '@/models/index';
import { signup } from '../common/signup';
+import config from '@/config';
+import { sendEmail } from '@/services/send-email';
+import { genId } from '@/misc/gen-id';
export default async (ctx: Koa.Context) => {
const body = ctx.request.body;
@@ -29,8 +34,16 @@ export default async (ctx: Koa.Context) => {
const password = body['password'];
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null;
const invitationCode = body['invitationCode'];
+ const emailAddress = body['emailAddress'];
- if (instance && instance.disableRegistration) {
+ if (instance.emailRequiredForSignup) {
+ if (emailAddress == null || typeof emailAddress != 'string') {
+ ctx.status = 400;
+ return;
+ }
+ }
+
+ if (instance.disableRegistration) {
if (invitationCode == null || typeof invitationCode != 'string') {
ctx.status = 400;
return;
@@ -48,18 +61,45 @@ export default async (ctx: Koa.Context) => {
RegistrationTickets.delete(ticket.id);
}
- try {
- const { account, secret } = await signup(username, password, host);
+ if (instance.emailRequiredForSignup) {
+ const code = rndstr('a-z0-9', 16);
+
+ // Generate hash of password
+ const salt = await bcrypt.genSalt(8);
+ const hash = await bcrypt.hash(password, salt);
- const res = await Users.pack(account, account, {
- detail: true,
- includeSecrets: true
+ await UserPendings.insert({
+ id: genId(),
+ createdAt: new Date(),
+ code,
+ email: emailAddress,
+ username: username,
+ password: hash,
});
- (res as any).token = secret;
+ const link = `${config.url}/signup-complete/${code}`;
+
+ sendEmail(emailAddress, 'Signup',
+ `To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
+ `To complete signup, please click this link: ${link}`);
- ctx.body = res;
- } catch (e) {
- ctx.throw(400, e);
+ ctx.status = 204;
+ } else {
+ try {
+ const { account, secret } = await signup({
+ username, password, host
+ });
+
+ const res = await Users.pack(account, account, {
+ detail: true,
+ includeSecrets: true
+ });
+
+ (res as any).token = secret;
+
+ ctx.body = res;
+ } catch (e) {
+ ctx.throw(400, e);
+ }
}
};
diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts
index a73164ed21..1908c969a5 100644
--- a/src/server/file/send-drive-file.ts
+++ b/src/server/file/send-drive-file.ts
@@ -13,6 +13,7 @@ import { downloadUrl } from '@/misc/download-url';
import { detectType } from '@/misc/get-file-info';
import { convertToJpeg, convertToPngOrJpeg } from '@/services/drive/image-processor';
import { GenerateVideoThumbnail } from '@/services/drive/generate-video-thumbnail';
+import { StatusError } from '@/misc/fetch';
//const _filename = fileURLToPath(import.meta.url);
const _filename = __filename;
@@ -83,9 +84,9 @@ export default async function(ctx: Koa.Context) {
ctx.set('Content-Type', image.type);
ctx.set('Cache-Control', 'max-age=31536000, immutable');
} catch (e) {
- serverLogger.error(e.statusCode);
+ serverLogger.error(`${e}`);
- if (typeof e.statusCode === 'number' && e.statusCode >= 400 && e.statusCode < 500) {
+ if (e instanceof StatusError && e.isClientError) {
ctx.status = e.statusCode;
ctx.set('Cache-Control', 'max-age=86400');
} else {
diff --git a/src/server/index.ts b/src/server/index.ts
index fb4e48c1c6..c891596140 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -20,7 +20,7 @@ import config from '@/config/index';
import apiServer from './api/index';
import { sum } from '@/prelude/array';
import Logger from '@/services/logger';
-import { program } from '../argv';
+import { envOption } from '../env';
import { UserProfiles, Users } from '@/models/index';
import { networkChart } from '@/services/chart/index';
import { genAvatar } from '@/misc/gen-avatar';
@@ -40,7 +40,7 @@ if (!['production', 'test'].includes(process.env.NODE_ENV || '')) {
}));
// Delay
- if (program.slow) {
+ if (envOption.slow) {
app.use(slow({
delay: 3000
}));
diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts
index dec2615086..6a864fcc52 100644
--- a/src/server/nodeinfo.ts
+++ b/src/server/nodeinfo.ts
@@ -68,6 +68,7 @@ const nodeinfo2 = async () => {
disableRegistration: meta.disableRegistration,
disableLocalTimeline: meta.disableLocalTimeline,
disableGlobalTimeline: meta.disableGlobalTimeline,
+ emailRequiredForSignup: meta.emailRequiredForSignup,
enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha,
maxNoteTextLength: meta.maxNoteTextLength,
diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts
index 3bd65dfe67..9e13c0877f 100644
--- a/src/server/proxy/proxy-media.ts
+++ b/src/server/proxy/proxy-media.ts
@@ -5,6 +5,7 @@ import { IImage, convertToPng, convertToJpeg } from '@/services/drive/image-proc
import { createTemp } from '@/misc/create-temp';
import { downloadUrl } from '@/misc/download-url';
import { detectType } from '@/misc/get-file-info';
+import { StatusError } from '@/misc/fetch';
export async function proxyMedia(ctx: Koa.Context) {
const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
@@ -37,9 +38,9 @@ export async function proxyMedia(ctx: Koa.Context) {
ctx.set('Cache-Control', 'max-age=31536000, immutable');
ctx.body = image.data;
} catch (e) {
- serverLogger.error(e);
+ serverLogger.error(`${e}`);
- if (typeof e.statusCode === 'number' && e.statusCode >= 400 && e.statusCode < 500) {
+ if (e instanceof StatusError && e.isClientError) {
ctx.status = e.statusCode;
} else {
ctx.status = 500;
diff --git a/src/server/web/manifest.json b/src/server/web/manifest.json
index 48030a2980..db97531bbf 100644
--- a/src/server/web/manifest.json
+++ b/src/server/web/manifest.json
@@ -2,7 +2,7 @@
"short_name": "Misskey",
"name": "Misskey",
"start_url": "/",
- "display": "standalone",
+ "display": "minimal-ui",
"background_color": "#313a42",
"theme_color": "#86b300",
"icons": [
diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts
index 76c4bda9dc..defe377514 100644
--- a/src/services/blocking/create.ts
+++ b/src/services/blocking/create.ts
@@ -9,8 +9,13 @@ import { User } from '@/models/entities/user';
import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index';
import { perUserFollowingChart } from '@/services/chart/index';
import { genId } from '@/misc/gen-id';
+import { IdentifiableError } from '@/misc/identifiable-error';
export default async function(blocker: User, blockee: User) {
+ if (blockee.isAdmin || blockee.isModerator) {
+ throw new IdentifiableError('e42b7890-5e4d-9d9c-d54b-cf4dd30adfb5');
+ }
+
await Promise.all([
cancelRequest(blocker, blockee),
cancelRequest(blockee, blocker),
diff --git a/src/services/logger.ts b/src/services/logger.ts
index 229be891e1..8e783e67f6 100644
--- a/src/services/logger.ts
+++ b/src/services/logger.ts
@@ -2,7 +2,7 @@ import * as cluster from 'cluster';
import * as os from 'os';
import * as chalk from 'chalk';
import * as dateformat from 'dateformat';
-import { program } from '../argv';
+import { envOption } from '../env';
import { getRepository } from 'typeorm';
import { Log } from '@/models/entities/log';
import { genId } from '@/misc/gen-id';
@@ -52,7 +52,7 @@ export default class Logger {
}
private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subDomains: Domain[] = [], store = true): void {
- if (program.quiet) return;
+ if (envOption.quiet) return;
if (!this.store) store = false;
if (level === 'debug') store = false;
@@ -80,7 +80,7 @@ export default class Logger {
null;
let log = `${l} ${worker}\t[${domains.join(' ')}]\t${m}`;
- if (program.withLogTime) log = chalk.gray(time) + ' ' + log;
+ if (envOption.withLogTime) log = chalk.gray(time) + ' ' + log;
console.log(important ? chalk.bold(log) : log);
@@ -132,7 +132,7 @@ export default class Logger {
}
public debug(message: string, data?: Record<string, any> | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
- if (process.env.NODE_ENV != 'production' || program.verbose) {
+ if (process.env.NODE_ENV != 'production' || envOption.verbose) {
this.log('debug', message, data, important);
}
}