summaryrefslogtreecommitdiff
path: root/packages/client/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2022-01-27 00:17:13 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2022-01-27 00:17:13 +0900
commit5f5f68cdcd31653cef2ae6bd29ce8bfcf60113ff (patch)
tree51e9e6179f6d1bda3013d1412f6e43f9f8f70e86 /packages/client/src
parentMerge branch 'develop' (diff)
parent12.102.0 (diff)
downloadmisskey-5f5f68cdcd31653cef2ae6bd29ce8bfcf60113ff.tar.gz
misskey-5f5f68cdcd31653cef2ae6bd29ce8bfcf60113ff.tar.bz2
misskey-5f5f68cdcd31653cef2ae6bd29ce8bfcf60113ff.zip
Merge branch 'develop'
Diffstat (limited to 'packages/client/src')
-rw-r--r--packages/client/src/account.ts93
-rw-r--r--packages/client/src/components/MkNoteSub.vue (renamed from packages/client/src/components/note.sub.vue)75
-rw-r--r--packages/client/src/components/abuse-report-window.vue76
-rw-r--r--packages/client/src/components/abuse-report.vue102
-rw-r--r--packages/client/src/components/analog-clock.vue140
-rw-r--r--packages/client/src/components/autocomplete.vue493
-rw-r--r--packages/client/src/components/avatars.vue32
-rw-r--r--packages/client/src/components/captcha.vue168
-rw-r--r--packages/client/src/components/channel-follow-button.vue76
-rw-r--r--packages/client/src/components/channel-preview.vue41
-rw-r--r--packages/client/src/components/chart.vue8
-rw-r--r--packages/client/src/components/code-core.vue36
-rw-r--r--packages/client/src/components/code.vue30
-rw-r--r--packages/client/src/components/cw-button.vue52
-rw-r--r--packages/client/src/components/date-separated-list.vue53
-rw-r--r--packages/client/src/components/debobigego/base.vue65
-rw-r--r--packages/client/src/components/debobigego/button.vue81
-rw-r--r--packages/client/src/components/debobigego/debobigego.scss52
-rw-r--r--packages/client/src/components/debobigego/group.vue78
-rw-r--r--packages/client/src/components/debobigego/info.vue47
-rw-r--r--packages/client/src/components/debobigego/input.vue292
-rw-r--r--packages/client/src/components/debobigego/key-value-view.vue38
-rw-r--r--packages/client/src/components/debobigego/link.vue103
-rw-r--r--packages/client/src/components/debobigego/object-view.vue102
-rw-r--r--packages/client/src/components/debobigego/pagination.vue42
-rw-r--r--packages/client/src/components/debobigego/radios.vue112
-rw-r--r--packages/client/src/components/debobigego/range.vue122
-rw-r--r--packages/client/src/components/debobigego/select.vue145
-rw-r--r--packages/client/src/components/debobigego/suspense.vue101
-rw-r--r--packages/client/src/components/debobigego/switch.vue132
-rw-r--r--packages/client/src/components/debobigego/textarea.vue161
-rw-r--r--packages/client/src/components/debobigego/tuple.vue36
-rw-r--r--packages/client/src/components/dialog.vue188
-rw-r--r--packages/client/src/components/drive-file-thumbnail.vue95
-rw-r--r--packages/client/src/components/drive-select-dialog.vue71
-rw-r--r--packages/client/src/components/drive-window.vue39
-rw-r--r--packages/client/src/components/drive.file.vue288
-rw-r--r--packages/client/src/components/drive.folder.vue394
-rw-r--r--packages/client/src/components/drive.nav-folder.vue169
-rw-r--r--packages/client/src/components/drive.vue1027
-rw-r--r--packages/client/src/components/emoji-picker-dialog.vue95
-rw-r--r--packages/client/src/components/emoji-picker-window.vue51
-rw-r--r--packages/client/src/components/emoji-picker.section.vue38
-rw-r--r--packages/client/src/components/emoji-picker.vue471
-rw-r--r--packages/client/src/components/featured-photos.vue22
-rw-r--r--packages/client/src/components/file-type-icon.vue27
-rw-r--r--packages/client/src/components/follow-button.vue172
-rw-r--r--packages/client/src/components/forgot-password.vue76
-rw-r--r--packages/client/src/components/form/folder.vue107
-rw-r--r--packages/client/src/components/form/group.vue3
-rw-r--r--packages/client/src/components/form/input.vue4
-rw-r--r--packages/client/src/components/form/pagination.vue44
-rw-r--r--packages/client/src/components/form/radio.vue20
-rw-r--r--packages/client/src/components/form/section.vue14
-rw-r--r--packages/client/src/components/form/select.vue4
-rw-r--r--packages/client/src/components/form/split.vue27
-rw-r--r--packages/client/src/components/form/suspense.vue2
-rw-r--r--packages/client/src/components/form/switch.vue5
-rw-r--r--packages/client/src/components/global/a.vue202
-rw-r--r--packages/client/src/components/global/acct.vue29
-rw-r--r--packages/client/src/components/global/ad.vue6
-rw-r--r--packages/client/src/components/global/avatar.vue94
-rw-r--r--packages/client/src/components/global/error.vue13
-rw-r--r--packages/client/src/components/global/loading.vue30
-rw-r--r--packages/client/src/components/global/misskey-flavored-markdown.vue22
-rw-r--r--packages/client/src/components/global/spacer.vue2
-rw-r--r--packages/client/src/components/global/sticky-container.vue2
-rw-r--r--packages/client/src/components/global/time.vue112
-rw-r--r--packages/client/src/components/global/user-name.vue21
-rw-r--r--packages/client/src/components/google.vue35
-rw-r--r--packages/client/src/components/image-viewer.vue34
-rw-r--r--packages/client/src/components/img-with-blurhash.vue86
-rw-r--r--packages/client/src/components/instance-stats.vue15
-rw-r--r--packages/client/src/components/instance-ticker.vue41
-rw-r--r--packages/client/src/components/key-value.vue27
-rw-r--r--packages/client/src/components/link.vue84
-rw-r--r--packages/client/src/components/media-banner.vue46
-rw-r--r--packages/client/src/components/media-list.vue36
-rw-r--r--packages/client/src/components/media-video.vue28
-rw-r--r--packages/client/src/components/mini-chart.vue4
-rw-r--r--packages/client/src/components/modal-page-window.vue4
-rw-r--r--packages/client/src/components/note-detailed.vue862
-rw-r--r--packages/client/src/components/note-header.vue28
-rw-r--r--packages/client/src/components/note-preview.vue18
-rw-r--r--packages/client/src/components/note-simple.vue34
-rw-r--r--packages/client/src/components/note.vue867
-rw-r--r--packages/client/src/components/notes.vue124
-rw-r--r--packages/client/src/components/notification-toast.vue4
-rw-r--r--packages/client/src/components/notification.vue5
-rw-r--r--packages/client/src/components/notifications.vue191
-rw-r--r--packages/client/src/components/object-view.value.vue108
-rw-r--r--packages/client/src/components/object-view.vue33
-rw-r--r--packages/client/src/components/poll-editor.vue214
-rw-r--r--packages/client/src/components/post-form-attaches.vue9
-rw-r--r--packages/client/src/components/post-form.vue1106
-rw-r--r--packages/client/src/components/reaction-icon.vue28
-rw-r--r--packages/client/src/components/reaction-tooltip.vue35
-rw-r--r--packages/client/src/components/reactions-viewer.details.vue45
-rw-r--r--packages/client/src/components/reactions-viewer.vue34
-rw-r--r--packages/client/src/components/remote-caution.vue20
-rw-r--r--packages/client/src/components/renote.details.vue34
-rw-r--r--packages/client/src/components/ripple.vue2
-rw-r--r--packages/client/src/components/signin-dialog.vue42
-rw-r--r--packages/client/src/components/signup-dialog.vue46
-rw-r--r--packages/client/src/components/sub-note-content.vue38
-rw-r--r--packages/client/src/components/taskmanager.api-window.vue72
-rw-r--r--packages/client/src/components/taskmanager.vue233
-rw-r--r--packages/client/src/components/timeline.vue278
-rw-r--r--packages/client/src/components/toast.vue40
-rw-r--r--packages/client/src/components/ui/button.vue6
-rw-r--r--packages/client/src/components/ui/container.vue2
-rw-r--r--packages/client/src/components/ui/folder.vue2
-rw-r--r--packages/client/src/components/ui/menu.vue2
-rw-r--r--packages/client/src/components/ui/modal.vue2
-rw-r--r--packages/client/src/components/ui/pagination.vue282
-rw-r--r--packages/client/src/components/ui/tooltip.vue2
-rw-r--r--packages/client/src/components/ui/window.vue4
-rw-r--r--packages/client/src/components/url-preview-popup.vue2
-rw-r--r--packages/client/src/components/url-preview.vue158
-rw-r--r--packages/client/src/components/user-info.vue28
-rw-r--r--packages/client/src/components/user-list.vue104
-rw-r--r--packages/client/src/components/user-online-indicator.vue31
-rw-r--r--packages/client/src/components/user-preview.vue2
-rw-r--r--packages/client/src/components/user-select-dialog.vue148
-rw-r--r--packages/client/src/components/visibility-picker.vue81
-rw-r--r--packages/client/src/components/waiting-dialog.vue57
-rw-r--r--packages/client/src/components/widgets.vue79
-rw-r--r--packages/client/src/const.ts44
-rw-r--r--packages/client/src/directives/anim.ts2
-rw-r--r--packages/client/src/directives/tooltip.ts18
-rw-r--r--packages/client/src/directives/user-preview.ts30
-rw-r--r--packages/client/src/init.ts20
-rw-r--r--packages/client/src/menu.ts18
-rw-r--r--packages/client/src/os.ts54
-rw-r--r--packages/client/src/pages/_error_.vue89
-rw-r--r--packages/client/src/pages/_loading_.vue6
-rw-r--r--packages/client/src/pages/about-misskey.vue116
-rw-r--r--packages/client/src/pages/about.vue51
-rw-r--r--packages/client/src/pages/admin/abuses.vue88
-rw-r--r--packages/client/src/pages/admin/ads.vue10
-rw-r--r--packages/client/src/pages/admin/announcements.vue4
-rw-r--r--packages/client/src/pages/admin/bot-protection.vue105
-rw-r--r--packages/client/src/pages/admin/database.vue35
-rw-r--r--packages/client/src/pages/admin/email-settings.vue100
-rw-r--r--packages/client/src/pages/admin/emoji-edit-dialog.vue2
-rw-r--r--packages/client/src/pages/admin/emojis.vue361
-rw-r--r--packages/client/src/pages/admin/files-settings.vue93
-rw-r--r--packages/client/src/pages/admin/files.vue28
-rw-r--r--packages/client/src/pages/admin/index.vue34
-rw-r--r--packages/client/src/pages/admin/instance-block.vue30
-rw-r--r--packages/client/src/pages/admin/instance.vue291
-rw-r--r--packages/client/src/pages/admin/integrations.discord.vue (renamed from packages/client/src/pages/admin/integrations-discord.vue)40
-rw-r--r--packages/client/src/pages/admin/integrations.github.vue (renamed from packages/client/src/pages/admin/integrations-github.vue)40
-rw-r--r--packages/client/src/pages/admin/integrations.twitter.vue (renamed from packages/client/src/pages/admin/integrations-twitter.vue)40
-rw-r--r--packages/client/src/pages/admin/integrations.vue58
-rw-r--r--packages/client/src/pages/admin/metrics.vue6
-rw-r--r--packages/client/src/pages/admin/object-storage.vue126
-rw-r--r--packages/client/src/pages/admin/other-settings.vue54
-rw-r--r--packages/client/src/pages/admin/overview.vue14
-rw-r--r--packages/client/src/pages/admin/proxy-account.vue42
-rw-r--r--packages/client/src/pages/admin/queue.vue15
-rw-r--r--packages/client/src/pages/admin/relays.vue52
-rw-r--r--packages/client/src/pages/admin/security.vue73
-rw-r--r--packages/client/src/pages/admin/service-worker.vue85
-rw-r--r--packages/client/src/pages/admin/settings.vue226
-rw-r--r--packages/client/src/pages/admin/users.vue26
-rw-r--r--packages/client/src/pages/advanced-theme-editor.vue349
-rw-r--r--packages/client/src/pages/announcements.vue2
-rw-r--r--packages/client/src/pages/channel.vue6
-rw-r--r--packages/client/src/pages/channels.vue6
-rw-r--r--packages/client/src/pages/clip.vue6
-rw-r--r--packages/client/src/pages/drive.vue28
-rw-r--r--packages/client/src/pages/emojis.emoji.vue42
-rw-r--r--packages/client/src/pages/emojis.vue76
-rw-r--r--packages/client/src/pages/explore.vue4
-rw-r--r--packages/client/src/pages/favorites.vue74
-rw-r--r--packages/client/src/pages/featured.vue31
-rw-r--r--packages/client/src/pages/federation.vue97
-rw-r--r--packages/client/src/pages/follow-requests.vue88
-rw-r--r--packages/client/src/pages/gallery/edit.vue25
-rw-r--r--packages/client/src/pages/gallery/index.vue10
-rw-r--r--packages/client/src/pages/gallery/post.vue8
-rw-r--r--packages/client/src/pages/instance-info.vue298
-rw-r--r--packages/client/src/pages/mentions.vue29
-rw-r--r--packages/client/src/pages/messages.vue35
-rw-r--r--packages/client/src/pages/messaging/index.vue3
-rw-r--r--packages/client/src/pages/messaging/messaging-room.form.vue7
-rw-r--r--packages/client/src/pages/messaging/messaging-room.vue11
-rw-r--r--packages/client/src/pages/my-antennas/create.vue60
-rw-r--r--packages/client/src/pages/my-antennas/index.vue2
-rw-r--r--packages/client/src/pages/my-clips/index.vue103
-rw-r--r--packages/client/src/pages/my-groups/group.vue4
-rw-r--r--packages/client/src/pages/my-groups/index.vue6
-rw-r--r--packages/client/src/pages/my-lists/index.vue63
-rw-r--r--packages/client/src/pages/my-lists/list.vue4
-rw-r--r--packages/client/src/pages/not-found.vue18
-rw-r--r--packages/client/src/pages/note.vue14
-rw-r--r--packages/client/src/pages/notifications.vue106
-rw-r--r--packages/client/src/pages/page.vue8
-rw-r--r--packages/client/src/pages/pages.vue6
-rw-r--r--packages/client/src/pages/preview.vue24
-rw-r--r--packages/client/src/pages/reset-password.vue92
-rw-r--r--packages/client/src/pages/reversi/game.board.vue528
-rw-r--r--packages/client/src/pages/reversi/game.setting.vue390
-rw-r--r--packages/client/src/pages/reversi/game.vue76
-rw-r--r--packages/client/src/pages/reversi/index.vue279
-rw-r--r--packages/client/src/pages/room/preview.vue107
-rw-r--r--packages/client/src/pages/room/room.vue279
-rw-r--r--packages/client/src/pages/search.vue48
-rw-r--r--packages/client/src/pages/settings/2fa.vue3
-rw-r--r--packages/client/src/pages/settings/account-info.vue152
-rw-r--r--packages/client/src/pages/settings/accounts.vue36
-rw-r--r--packages/client/src/pages/settings/api.vue23
-rw-r--r--packages/client/src/pages/settings/apps.vue20
-rw-r--r--packages/client/src/pages/settings/custom-css.vue27
-rw-r--r--packages/client/src/pages/settings/deck.vue36
-rw-r--r--packages/client/src/pages/settings/delete-account.vue24
-rw-r--r--packages/client/src/pages/settings/drive.vue10
-rw-r--r--packages/client/src/pages/settings/email.vue6
-rw-r--r--packages/client/src/pages/settings/experimental-features.vue52
-rw-r--r--packages/client/src/pages/settings/general.vue4
-rw-r--r--packages/client/src/pages/settings/import-export.vue4
-rw-r--r--packages/client/src/pages/settings/index.vue30
-rw-r--r--packages/client/src/pages/settings/instance-mute.vue5
-rw-r--r--packages/client/src/pages/settings/integration.vue52
-rw-r--r--packages/client/src/pages/settings/menu.vue6
-rw-r--r--packages/client/src/pages/settings/mute-block.vue79
-rw-r--r--packages/client/src/pages/settings/notifications.vue6
-rw-r--r--packages/client/src/pages/settings/other.vue37
-rw-r--r--packages/client/src/pages/settings/plugin.install.vue36
-rw-r--r--packages/client/src/pages/settings/plugin.manage.vue116
-rw-r--r--packages/client/src/pages/settings/plugin.vue86
-rw-r--r--packages/client/src/pages/settings/privacy.vue90
-rw-r--r--packages/client/src/pages/settings/profile.vue349
-rw-r--r--packages/client/src/pages/settings/reaction.vue4
-rw-r--r--packages/client/src/pages/settings/registry.keys.vue114
-rw-r--r--packages/client/src/pages/settings/registry.value.vue147
-rw-r--r--packages/client/src/pages/settings/registry.vue90
-rw-r--r--packages/client/src/pages/settings/security.vue16
-rw-r--r--packages/client/src/pages/settings/sounds.vue6
-rw-r--r--packages/client/src/pages/settings/theme.install.vue146
-rw-r--r--packages/client/src/pages/settings/theme.manage.vue10
-rw-r--r--packages/client/src/pages/settings/theme.vue4
-rw-r--r--packages/client/src/pages/settings/update.vue95
-rw-r--r--packages/client/src/pages/settings/word-mute.vue6
-rw-r--r--packages/client/src/pages/share.vue2
-rw-r--r--packages/client/src/pages/signup-complete.vue56
-rw-r--r--packages/client/src/pages/tag.vue53
-rw-r--r--packages/client/src/pages/test.vue260
-rw-r--r--packages/client/src/pages/theme-editor.vue466
-rw-r--r--packages/client/src/pages/timeline.tutorial.vue24
-rw-r--r--packages/client/src/pages/timeline.vue258
-rw-r--r--packages/client/src/pages/user-ap-info.vue124
-rw-r--r--packages/client/src/pages/user-info.vue126
-rw-r--r--packages/client/src/pages/user/clips.vue2
-rw-r--r--packages/client/src/pages/user/follow-list.vue62
-rw-r--r--packages/client/src/pages/user/gallery.vue14
-rw-r--r--packages/client/src/pages/user/index.activity.vue27
-rw-r--r--packages/client/src/pages/user/index.timeline.vue60
-rw-r--r--packages/client/src/pages/user/index.vue876
-rw-r--r--packages/client/src/pages/user/pages.vue45
-rw-r--r--packages/client/src/pages/user/reactions.vue48
-rw-r--r--packages/client/src/pages/v.vue29
-rw-r--r--packages/client/src/pizzax.ts18
-rw-r--r--packages/client/src/router.ts12
-rw-r--r--packages/client/src/scripts/autocomplete.ts26
-rw-r--r--packages/client/src/scripts/check-word-mute.ts2
-rw-r--r--packages/client/src/scripts/emojilist.ts12
-rw-r--r--packages/client/src/scripts/form.ts30
-rw-r--r--packages/client/src/scripts/games/reversi/core.ts263
-rw-r--r--packages/client/src/scripts/games/reversi/maps.ts896
-rw-r--r--packages/client/src/scripts/games/reversi/package.json18
-rw-r--r--packages/client/src/scripts/games/reversi/tsconfig.json21
-rw-r--r--packages/client/src/scripts/get-note-menu.ts310
-rw-r--r--packages/client/src/scripts/get-user-menu.ts4
-rw-r--r--packages/client/src/scripts/paging.ts246
-rw-r--r--packages/client/src/scripts/physics.ts4
-rw-r--r--packages/client/src/scripts/popout.ts4
-rw-r--r--packages/client/src/scripts/room/furniture.ts21
-rw-r--r--packages/client/src/scripts/room/furnitures.json5407
-rw-r--r--packages/client/src/scripts/room/room.ts775
-rw-r--r--packages/client/src/scripts/select-file.ts3
-rw-r--r--packages/client/src/scripts/theme.ts4
-rw-r--r--packages/client/src/scripts/touch.ts4
-rw-r--r--packages/client/src/scripts/use-leave-guard.ts46
-rw-r--r--packages/client/src/scripts/use-note-capture.ts123
-rw-r--r--packages/client/src/store.ts8
-rw-r--r--packages/client/src/stream.ts8
-rw-r--r--packages/client/src/style.scss11
-rw-r--r--packages/client/src/themes/_dark.json53
-rw-r--r--packages/client/src/themes/_light.json53
-rw-r--r--packages/client/src/ui/_common_/common.vue3
-rw-r--r--packages/client/src/ui/_common_/sidebar-for-mobile.vue6
-rw-r--r--packages/client/src/ui/_common_/sidebar.vue6
-rw-r--r--packages/client/src/ui/_common_/stream-indicator.vue52
-rw-r--r--packages/client/src/ui/_common_/upload.vue14
-rw-r--r--packages/client/src/ui/chat/date-separated-list.vue157
-rw-r--r--packages/client/src/ui/chat/header-clock.vue62
-rw-r--r--packages/client/src/ui/chat/index.vue463
-rw-r--r--packages/client/src/ui/chat/note-header.vue99
-rw-r--r--packages/client/src/ui/chat/note-preview.vue112
-rw-r--r--packages/client/src/ui/chat/note.sub.vue137
-rw-r--r--packages/client/src/ui/chat/note.vue1142
-rw-r--r--packages/client/src/ui/chat/notes.vue94
-rw-r--r--packages/client/src/ui/chat/pages/channel.vue258
-rw-r--r--packages/client/src/ui/chat/pages/timeline.vue221
-rw-r--r--packages/client/src/ui/chat/post-form.vue769
-rw-r--r--packages/client/src/ui/chat/side.vue157
-rw-r--r--packages/client/src/ui/chat/store.ts17
-rw-r--r--packages/client/src/ui/chat/sub-note-content.vue62
-rw-r--r--packages/client/src/ui/chat/widgets.vue62
-rw-r--r--packages/client/src/ui/classic.header.vue6
-rw-r--r--packages/client/src/ui/classic.side.vue4
-rw-r--r--packages/client/src/ui/classic.sidebar.vue6
-rw-r--r--packages/client/src/ui/classic.vue14
-rw-r--r--packages/client/src/ui/deck.vue4
-rw-r--r--packages/client/src/ui/deck/column.vue6
-rw-r--r--packages/client/src/ui/deck/direct-column.vue45
-rw-r--r--packages/client/src/ui/deck/main-column.vue10
-rw-r--r--packages/client/src/ui/deck/mentions-column.vue39
-rw-r--r--packages/client/src/ui/universal.vue10
-rw-r--r--packages/client/src/ui/visitor/b.vue4
-rw-r--r--packages/client/src/ui/zen.vue2
-rw-r--r--packages/client/src/widgets/activity.vue125
-rw-r--r--packages/client/src/widgets/aichan.vue94
-rw-r--r--packages/client/src/widgets/aiscript.vue174
-rw-r--r--packages/client/src/widgets/button.vue149
-rw-r--r--packages/client/src/widgets/calendar.vue140
-rw-r--r--packages/client/src/widgets/clock.vue73
-rw-r--r--packages/client/src/widgets/define.ts75
-rw-r--r--packages/client/src/widgets/digital-clock.vue119
-rw-r--r--packages/client/src/widgets/federation.vue110
-rw-r--r--packages/client/src/widgets/job-queue.vue177
-rw-r--r--packages/client/src/widgets/memo.vue88
-rw-r--r--packages/client/src/widgets/notifications.vue95
-rw-r--r--packages/client/src/widgets/online-users.vue80
-rw-r--r--packages/client/src/widgets/photos.vue116
-rw-r--r--packages/client/src/widgets/post-form.vue38
-rw-r--r--packages/client/src/widgets/rss.vue105
-rw-r--r--packages/client/src/widgets/server-metric/disk.vue33
-rw-r--r--packages/client/src/widgets/server-metric/index.vue120
-rw-r--r--packages/client/src/widgets/server-metric/pie.vue33
-rw-r--r--packages/client/src/widgets/slideshow.vue184
-rw-r--r--packages/client/src/widgets/timeline.vue201
-rw-r--r--packages/client/src/widgets/trends.vue88
-rw-r--r--packages/client/src/widgets/widget.ts71
346 files changed, 9728 insertions, 23674 deletions
diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts
index 4c83b78c91..5a935e1dc7 100644
--- a/packages/client/src/account.ts
+++ b/packages/client/src/account.ts
@@ -16,6 +16,8 @@ const data = localStorage.getItem('account');
// TODO: 外部からはreadonlyに
export const $i = data ? reactive(JSON.parse(data) as Account) : null;
+export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
+
export async function signout() {
waiting();
localStorage.removeItem('account');
@@ -127,7 +129,12 @@ export async function login(token: Account['token'], redirect?: string) {
unisonReload();
}
-export async function openAccountMenu(ev: MouseEvent) {
+export async function openAccountMenu(opts: {
+ includeCurrentAccount?: boolean;
+ withExtraOperation: boolean;
+ active?: misskey.entities.UserDetailed['id'];
+ onChoose?: (account: misskey.entities.UserDetailed) => void;
+}, ev: MouseEvent) {
function showSigninDialog() {
popup(import('@/components/signin-dialog.vue'), {}, {
done: res => {
@@ -146,7 +153,7 @@ export async function openAccountMenu(ev: MouseEvent) {
}, 'closed');
}
- async function switchAccount(account: any) {
+ async function switchAccount(account: misskey.entities.UserDetailed) {
const storedAccounts = await getAccounts();
const token = storedAccounts.find(x => x.id === account.id).token;
switchAccountWithToken(token);
@@ -159,48 +166,58 @@ export async function openAccountMenu(ev: MouseEvent) {
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
+ function createItem(account: misskey.entities.UserDetailed) {
+ return {
+ type: 'user',
+ user: account,
+ active: opts.active != null ? opts.active === account.id : false,
+ action: () => {
+ if (opts.onChoose) {
+ opts.onChoose(account);
+ } else {
+ switchAccount(account);
+ }
+ },
+ };
+ }
+
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); }
- });
+ res(createItem(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 {
- $i: typeof $i;
+ if (opts.withExtraOperation) {
+ popupMenu([...[{
+ type: 'link',
+ text: i18n.locale.profile,
+ to: `/@${ $i.username }`,
+ avatar: $i,
+ }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...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'
+ });
+ } else {
+ popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget || ev.target, {
+ align: 'left'
+ });
}
}
diff --git a/packages/client/src/components/note.sub.vue b/packages/client/src/components/MkNoteSub.vue
index de4218e535..30c27e6235 100644
--- a/packages/client/src/components/note.sub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -10,13 +10,13 @@
<XCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent" class="content">
- <XSubNote-content class="text" :note="note"/>
+ <MkNoteSubNoteContent class="text" :note="note"/>
</div>
</div>
</div>
</div>
<template v-if="depth < 5">
- <XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/>
+ <MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/>
</template>
<div v-else class="more">
<MkA class="text _link" :to="notePage(note)">{{ $ts.continueThread }} <i class="fas fa-angle-double-right"></i></MkA>
@@ -24,63 +24,36 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
import { notePage } from '@/filters/note';
import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
+import MkNoteSubNoteContent from './sub-note-content.vue';
import XCwButton from './cw-button.vue';
import * as os from '@/os';
-export default defineComponent({
- name: 'XSub',
+const props = withDefaults(defineProps<{
+ note: misskey.entities.Note;
+ detail?: boolean;
- components: {
- XNoteHeader,
- XSubNoteContent,
- XCwButton,
- },
-
- props: {
- note: {
- type: Object,
- required: true
- },
- detail: {
- type: Boolean,
- required: false,
- default: false
- },
- // how many notes are in between this one and the note being viewed in detail
- depth: {
- type: Number,
- required: false,
- default: 1
- },
- },
-
- data() {
- return {
- showContent: false,
- replies: [],
- };
- },
+ // how many notes are in between this one and the note being viewed in detail
+ depth?: number;
+}>(), {
+ depth: 1,
+});
- created() {
- if (this.detail) {
- os.api('notes/children', {
- noteId: this.note.id,
- limit: 5
- }).then(replies => {
- this.replies = replies;
- });
- }
- },
+let showContent = $ref(false);
+let replies: misskey.entities.Note[] = $ref([]);
- methods: {
- notePage,
- }
-});
+if (props.detail) {
+ os.api('notes/children', {
+ noteId: props.note.id,
+ limit: 5
+ }).then(res => {
+ replies = res;
+ });
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/abuse-report-window.vue b/packages/client/src/components/abuse-report-window.vue
index 6b07639f6d..cd04f62bca 100644
--- a/packages/client/src/components/abuse-report-window.vue
+++ b/packages/client/src/components/abuse-report-window.vue
@@ -1,8 +1,8 @@
<template>
-<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="$emit('closed')">
+<XWindow ref="window" :initial-width="400" :initial-height="500" :can-resize="true" @closed="emit('closed')">
<template #header>
<i class="fas fa-exclamation-circle" style="margin-right: 0.5em;"></i>
- <I18n :src="$ts.reportAbuseOf" tag="span">
+ <I18n :src="i18n.locale.reportAbuseOf" tag="span">
<template #name>
<b><MkAcct :user="user"/></b>
</template>
@@ -11,65 +11,51 @@
<div class="dpvffvvy _monolithic_">
<div class="_section">
<MkTextarea v-model="comment">
- <template #label>{{ $ts.details }}</template>
- <template #caption>{{ $ts.fillAbuseReportDescription }}</template>
+ <template #label>{{ i18n.locale.details }}</template>
+ <template #caption>{{ i18n.locale.fillAbuseReportDescription }}</template>
</MkTextarea>
</div>
<div class="_section">
- <MkButton primary full :disabled="comment.length === 0" @click="send">{{ $ts.send }}</MkButton>
+ <MkButton primary full :disabled="comment.length === 0" @click="send">{{ i18n.locale.send }}</MkButton>
</div>
</div>
</XWindow>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script setup lang="ts">
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
import XWindow from '@/components/ui/window.vue';
import MkTextarea from '@/components/form/textarea.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XWindow,
- MkTextarea,
- MkButton,
- },
+const props = defineProps<{
+ user: Misskey.entities.User;
+ initialComment?: string;
+}>();
- props: {
- user: {
- type: Object,
- required: true,
- },
- initialComment: {
- type: String,
- required: false,
- },
- },
+const emit = defineEmits<{
+ (e: 'closed'): void;
+}>();
- emits: ['closed'],
+const window = ref<InstanceType<typeof XWindow>>();
+const comment = ref(props.initialComment || '');
- data() {
- return {
- comment: this.initialComment || '',
- };
- },
-
- methods: {
- send() {
- os.apiWithDialog('users/report-abuse', {
- userId: this.user.id,
- comment: this.comment,
- }, undefined, res => {
- os.alert({
- type: 'success',
- text: this.$ts.abuseReported
- });
- this.$refs.window.close();
- });
- }
- },
-});
+function send() {
+ os.apiWithDialog('users/report-abuse', {
+ userId: props.user.id,
+ comment: comment.value,
+ }, undefined).then(res => {
+ os.alert({
+ type: 'success',
+ text: i18n.locale.abuseReported
+ });
+ window.value?.close();
+ emit('closed');
+ });
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/abuse-report.vue b/packages/client/src/components/abuse-report.vue
new file mode 100644
index 0000000000..b67cef209b
--- /dev/null
+++ b/packages/client/src/components/abuse-report.vue
@@ -0,0 +1,102 @@
+<template>
+<div class="bcekxzvu _card _gap">
+ <div class="_content target">
+ <MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
+ <MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId">
+ <MkUserName class="name" :user="report.targetUser"/>
+ <MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
+ </MkA>
+ </div>
+ <div class="_content">
+ <div>
+ <Mfm :text="report.comment"/>
+ </div>
+ <hr/>
+ <div>{{ $ts.reporter }}: <MkAcct :user="report.reporter"/></div>
+ <div v-if="report.assignee">
+ {{ $ts.moderator }}:
+ <MkAcct :user="report.assignee"/>
+ </div>
+ <div><MkTime :time="report.createdAt"/></div>
+ </div>
+ <div class="_footer">
+ <MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
+ {{ $ts.forwardReport }}
+ <template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
+ </MkSwitch>
+ <MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue';
+
+import MkButton from '@/components/ui/button.vue';
+import MkSwitch from '@/components/form/switch.vue';
+import { acct, userPage } from '@/filters/user';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkButton,
+ MkSwitch,
+ },
+
+ emits: ['resolved'],
+
+ props: {
+ report: {
+ type: Object,
+ required: true,
+ }
+ }
+
+ data() {
+ return {
+ forward: this.report.forwarded,
+ };
+ }
+
+ methods: {
+ acct,
+ userPage,
+
+ resolve() {
+ os.apiWithDialog('admin/resolve-abuse-user-report', {
+ forward: this.forward,
+ reportId: this.report.id,
+ }).then(() => {
+ this.$emit('resolved', this.report.id);
+ });
+ }
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.bcekxzvu {
+ > .target {
+ display: flex;
+ width: 100%;
+ box-sizing: border-box;
+ text-align: left;
+ align-items: center;
+
+ > .avatar {
+ width: 42px;
+ height: 42px;
+ }
+
+ > .info {
+ margin-left: 0.3em;
+ padding: 0 8px;
+ flex: 1;
+
+ > .name {
+ font-weight: bold;
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/components/analog-clock.vue b/packages/client/src/components/analog-clock.vue
index 450488b198..59b8e97304 100644
--- a/packages/client/src/components/analog-clock.vue
+++ b/packages/client/src/components/analog-clock.vue
@@ -40,106 +40,64 @@
</svg>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import * as tinycolor from 'tinycolor2';
-export default defineComponent({
- props: {
- thickness: {
- type: Number,
- default: 0.1
- }
- },
-
- data() {
- return {
- now: new Date(),
- enabled: true,
-
- graduationsPadding: 0.5,
- handsPadding: 1,
- handsTailLength: 0.7,
- hHandLengthRatio: 0.75,
- mHandLengthRatio: 1,
- sHandLengthRatio: 1,
-
- computedStyle: getComputedStyle(document.documentElement)
- };
- },
-
- computed: {
- dark(): boolean {
- return tinycolor(this.computedStyle.getPropertyValue('--bg')).isDark();
- },
-
- majorGraduationColor(): string {
- return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
- },
- minorGraduationColor(): string {
- return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
- },
+withDefaults(defineProps<{
+ thickness: number;
+}>(), {
+ thickness: 0.1,
+});
- sHandColor(): string {
- return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
- },
- mHandColor(): string {
- return tinycolor(this.computedStyle.getPropertyValue('--fg')).toHexString();
- },
- hHandColor(): string {
- return tinycolor(this.computedStyle.getPropertyValue('--accent')).toHexString();
- },
+const now = ref(new Date());
+const enabled = ref(true);
+const graduationsPadding = ref(0.5);
+const handsPadding = ref(1);
+const handsTailLength = ref(0.7);
+const hHandLengthRatio = ref(0.75);
+const mHandLengthRatio = ref(1);
+const sHandLengthRatio = ref(1);
+const computedStyle = getComputedStyle(document.documentElement);
- s(): number {
- return this.now.getSeconds();
- },
- m(): number {
- return this.now.getMinutes();
- },
- h(): number {
- return this.now.getHours();
- },
+const dark = computed(() => tinycolor(computedStyle.getPropertyValue('--bg')).isDark());
+const majorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)');
+const minorGraduationColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)');
+const sHandColor = computed(() => dark.value ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)');
+const mHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--fg')).toHexString());
+const hHandColor = computed(() => tinycolor(computedStyle.getPropertyValue('--accent')).toHexString());
+const s = computed(() => now.value.getSeconds());
+const m = computed(() => now.value.getMinutes());
+const h = computed(() => now.value.getHours());
+const hAngle = computed(() => Math.PI * (h.value % 12 + (m.value + s.value / 60) / 60) / 6);
+const mAngle = computed(() => Math.PI * (m.value + s.value / 60) / 30);
+const sAngle = computed(() => Math.PI * s.value / 30);
+const graduations = computed(() => {
+ const angles: number[] = [];
+ for (let i = 0; i < 60; i++) {
+ const angle = Math.PI * i / 30;
+ angles.push(angle);
+ }
- hAngle(): number {
- return Math.PI * (this.h % 12 + (this.m + this.s / 60) / 60) / 6;
- },
- mAngle(): number {
- return Math.PI * (this.m + this.s / 60) / 30;
- },
- sAngle(): number {
- return Math.PI * this.s / 30;
- },
+ return angles;
+});
- graduations(): any {
- const angles = [];
- for (let i = 0; i < 60; i++) {
- const angle = Math.PI * i / 30;
- angles.push(angle);
- }
+function tick() {
+ now.value = new Date();
+}
- return angles;
+onMounted(() => {
+ const update = () => {
+ if (enabled.value) {
+ tick();
+ window.setTimeout(update, 1000);
}
- },
-
- mounted() {
- const update = () => {
- if (this.enabled) {
- this.tick();
- setTimeout(update, 1000);
- }
- };
- update();
- },
-
- beforeUnmount() {
- this.enabled = false;
- },
+ };
+ update();
+});
- methods: {
- tick() {
- this.now = new Date();
- }
- }
+onBeforeUnmount(() => {
+ enabled.value = false;
});
</script>
diff --git a/packages/client/src/components/autocomplete.vue b/packages/client/src/components/autocomplete.vue
index 30be2ac741..7ba83b7cb1 100644
--- a/packages/client/src/components/autocomplete.vue
+++ b/packages/client/src/components/autocomplete.vue
@@ -1,5 +1,5 @@
<template>
-<div class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
+<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
<ol v-if="type === 'user'" ref="suggests" class="users">
<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown">
<img class="avatar" :src="user.avatarUrl"/>
@@ -8,7 +8,7 @@
</span>
<span class="username">@{{ acct(user) }}</span>
</li>
- <li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ $ts.selectUser }}</li>
+ <li tabindex="-1" class="choose" @click="chooseUser()" @keydown="onKeydown">{{ i18n.locale.selectUser }}</li>
</ol>
<ol v-else-if="hashtags.length > 0" ref="suggests" class="hashtags">
<li v-for="hashtag in hashtags" tabindex="-1" @click="complete(type, hashtag)" @keydown="onKeydown">
@@ -17,8 +17,8 @@
</ol>
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
<li v-for="emoji in emojis" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
- <span v-if="emoji.isCustomEmoji" class="emoji"><img :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
- <span v-else-if="!$store.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
+ <span v-if="emoji.isCustomEmoji" class="emoji"><img :src="defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url" :alt="emoji.emoji"/></span>
+ <span v-else-if="!defaultStore.state.useOsNativeEmojis" class="emoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
<span v-else class="emoji">{{ emoji.emoji }}</span>
<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
<span v-if="emoji.aliasOf" class="alias">({{ emoji.aliasOf }})</span>
@@ -33,15 +33,17 @@
</template>
<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { emojilist } from '@/scripts/emojilist';
+import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import contains from '@/scripts/contains';
-import { twemojiSvgBase } from '@/scripts/twemoji-base';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { acct } from '@/filters/user';
import * as os from '@/os';
-import { instance } from '@/instance';
import { MFM_TAGS } from '@/scripts/mfm-tags';
+import { defaultStore } from '@/store';
+import { emojilist } from '@/scripts/emojilist';
+import { instance } from '@/instance';
+import { twemojiSvgBase } from '@/scripts/twemoji-base';
+import { i18n } from '@/i18n';
type EmojiDef = {
emoji: string;
@@ -54,16 +56,14 @@ type EmojiDef = {
const lib = emojilist.filter(x => x.category !== 'flags');
const char2file = (char: string) => {
- let codes = Array.from(char).map(x => x.codePointAt(0).toString(16));
+ let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
- codes = codes.filter(x => x && x.length);
- return codes.join('-');
+ return codes.filter(x => x && x.length).join('-');
};
const emjdb: EmojiDef[] = lib.map(x => ({
emoji: x.char,
name: x.name,
- aliasOf: null,
url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
}));
@@ -112,291 +112,270 @@ emojiDefinitions.sort((a, b) => a.name.length - b.name.length);
const emojiDb = markRaw(emojiDefinitions.concat(emjdb));
//#endregion
-export default defineComponent({
- props: {
- type: {
- type: String,
- required: true,
- },
+export default {
+ emojiDb,
+ emojiDefinitions,
+ emojilist,
+ customEmojis,
+};
+</script>
- q: {
- type: String,
- required: false,
- },
+<script lang="ts" setup>
+const props = defineProps<{
+ type: string;
+ q: string | null;
+ textarea: HTMLTextAreaElement;
+ close: () => void;
+ x: number;
+ y: number;
+}>();
- textarea: {
- type: HTMLTextAreaElement,
- required: true,
- },
+const emit = defineEmits<{
+ (e: 'done', v: { type: string; value: any }): void;
+ (e: 'closed'): void;
+}>();
- close: {
- type: Function,
- required: true,
- },
+const suggests = ref<Element>();
+const rootEl = ref<HTMLDivElement>();
- x: {
- type: Number,
- required: true,
- },
+const fetching = ref(true);
+const users = ref<any[]>([]);
+const hashtags = ref<any[]>([]);
+const emojis = ref<(EmojiDef)[]>([]);
+const items = ref<Element[] | HTMLCollection>([]);
+const mfmTags = ref<string[]>([]);
+const select = ref(-1);
+const zIndex = os.claimZIndex('high');
- y: {
- type: Number,
- required: true,
- },
- },
+function complete(type: string, value: any) {
+ emit('done', { type, value });
+ emit('closed');
+ if (type === 'emoji') {
+ let recents = defaultStore.state.recentlyUsedEmojis;
+ recents = recents.filter((e: any) => e !== value);
+ recents.unshift(value);
+ defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+ }
+}
- emits: ['done', 'closed'],
+function setPosition() {
+ if (!rootEl.value) return;
+ if (props.x + rootEl.value.offsetWidth > window.innerWidth) {
+ rootEl.value.style.left = (window.innerWidth - rootEl.value.offsetWidth) + 'px';
+ } else {
+ rootEl.value.style.left = `${props.x}px`;
+ }
+ if (props.y + rootEl.value.offsetHeight > window.innerHeight) {
+ rootEl.value.style.top = (props.y - rootEl.value.offsetHeight) + 'px';
+ rootEl.value.style.marginTop = '0';
+ } else {
+ rootEl.value.style.top = props.y + 'px';
+ rootEl.value.style.marginTop = 'calc(1em + 8px)';
+ }
+}
- data() {
- return {
- getStaticImageUrl,
- fetching: true,
- users: [],
- hashtags: [],
- emojis: [],
- items: [],
- mfmTags: [],
- select: -1,
- zIndex: os.claimZIndex('high'),
+function exec() {
+ select.value = -1;
+ if (suggests.value) {
+ for (const el of Array.from(items.value)) {
+ el.removeAttribute('data-selected');
}
- },
-
- updated() {
- this.setPosition();
- this.items = (this.$refs.suggests as Element | undefined)?.children || [];
- },
-
- mounted() {
- this.setPosition();
-
- this.textarea.addEventListener('keydown', this.onKeydown);
-
- for (const el of Array.from(document.querySelectorAll('body *'))) {
- el.addEventListener('mousedown', this.onMousedown);
+ }
+ if (props.type === 'user') {
+ if (!props.q) {
+ users.value = [];
+ fetching.value = false;
+ return;
}
- this.$nextTick(() => {
- this.exec();
+ const cacheKey = `autocomplete:user:${props.q}`;
+ const cache = sessionStorage.getItem(cacheKey);
- this.$watch('q', () => {
- this.$nextTick(() => {
- this.exec();
- });
+ if (cache) {
+ const users = JSON.parse(cache);
+ users.value = users;
+ fetching.value = false;
+ } else {
+ os.api('users/search-by-username-and-host', {
+ username: props.q,
+ limit: 10,
+ detail: false
+ }).then(searchedUsers => {
+ users.value = searchedUsers as any[];
+ fetching.value = false;
+ // キャッシュ
+ sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
});
- });
- },
-
- beforeUnmount() {
- this.textarea.removeEventListener('keydown', this.onKeydown);
-
- for (const el of Array.from(document.querySelectorAll('body *'))) {
- el.removeEventListener('mousedown', this.onMousedown);
}
- },
-
- methods: {
- complete(type, value) {
- this.$emit('done', { type, value });
- this.$emit('closed');
-
- if (type === 'emoji') {
- let recents = this.$store.state.recentlyUsedEmojis;
- recents = recents.filter((e: any) => e !== value);
- recents.unshift(value);
- this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
- }
- },
-
- setPosition() {
- if (this.x + this.$el.offsetWidth > window.innerWidth) {
- this.$el.style.left = (window.innerWidth - this.$el.offsetWidth) + 'px';
+ } else if (props.type === 'hashtag') {
+ if (!props.q || props.q == '') {
+ hashtags.value = JSON.parse(localStorage.getItem('hashtags') || '[]');
+ fetching.value = false;
+ } else {
+ const cacheKey = `autocomplete:hashtag:${props.q}`;
+ const cache = sessionStorage.getItem(cacheKey);
+ if (cache) {
+ const hashtags = JSON.parse(cache);
+ hashtags.value = hashtags;
+ fetching.value = false;
} else {
- this.$el.style.left = this.x + 'px';
+ os.api('hashtags/search', {
+ query: props.q,
+ limit: 30
+ }).then(searchedHashtags => {
+ hashtags.value = searchedHashtags as any[];
+ fetching.value = false;
+ // キャッシュ
+ sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
+ });
}
+ }
+ } else if (props.type === 'emoji') {
+ if (!props.q || props.q == '') {
+ // 最近使った絵文字をサジェスト
+ emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.find(e => e.emoji == emoji)).filter(x => x) as EmojiDef[];
+ return;
+ }
- if (this.y + this.$el.offsetHeight > window.innerHeight) {
- this.$el.style.top = (this.y - this.$el.offsetHeight) + 'px';
- this.$el.style.marginTop = '0';
- } else {
- this.$el.style.top = this.y + 'px';
- this.$el.style.marginTop = 'calc(1em + 8px)';
- }
- },
+ const matched: EmojiDef[] = [];
+ const max = 30;
- exec() {
- this.select = -1;
- if (this.$refs.suggests) {
- for (const el of Array.from(this.items)) {
- el.removeAttribute('data-selected');
- }
- }
+ emojiDb.some(x => {
+ if (x.name.startsWith(props.q || '') && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+ return matched.length == max;
+ });
- if (this.type === 'user') {
- if (this.q == null) {
- this.users = [];
- this.fetching = false;
- return;
- }
+ if (matched.length < max) {
+ emojiDb.some(x => {
+ if (x.name.startsWith(props.q || '') && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+ return matched.length == max;
+ });
+ }
- const cacheKey = `autocomplete:user:${this.q}`;
- const cache = sessionStorage.getItem(cacheKey);
- if (cache) {
- const users = JSON.parse(cache);
- this.users = users;
- this.fetching = false;
- } else {
- os.api('users/search-by-username-and-host', {
- username: this.q,
- limit: 10,
- detail: false
- }).then(users => {
- this.users = users;
- this.fetching = false;
+ if (matched.length < max) {
+ emojiDb.some(x => {
+ if (x.name.includes(props.q || '') && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
+ return matched.length == max;
+ });
+ }
- // キャッシュ
- sessionStorage.setItem(cacheKey, JSON.stringify(users));
- });
- }
- } else if (this.type === 'hashtag') {
- if (this.q == null || this.q == '') {
- this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]');
- this.fetching = false;
- } else {
- const cacheKey = `autocomplete:hashtag:${this.q}`;
- const cache = sessionStorage.getItem(cacheKey);
- if (cache) {
- const hashtags = JSON.parse(cache);
- this.hashtags = hashtags;
- this.fetching = false;
- } else {
- os.api('hashtags/search', {
- query: this.q,
- limit: 30
- }).then(hashtags => {
- this.hashtags = hashtags;
- this.fetching = false;
+ emojis.value = matched;
+ } else if (props.type === 'mfmTag') {
+ if (!props.q || props.q == '') {
+ mfmTags.value = MFM_TAGS;
+ return;
+ }
- // キャッシュ
- sessionStorage.setItem(cacheKey, JSON.stringify(hashtags));
- });
- }
- }
- } 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);
- return;
- }
+ mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q || ''));
+ }
+}
+
+function onMousedown(e: Event) {
+ if (!contains(rootEl.value, e.target) && (rootEl.value != e.target)) props.close();
+}
- const matched = [];
- const max = 30;
+function onKeydown(e: KeyboardEvent) {
+ const cancel = () => {
+ e.preventDefault();
+ e.stopPropagation();
+ };
- emojiDb.some(x => {
- if (x.name.startsWith(this.q) && !x.aliasOf && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
- return matched.length == max;
- });
- if (matched.length < max) {
- emojiDb.some(x => {
- if (x.name.startsWith(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
- return matched.length == max;
- });
- }
- if (matched.length < max) {
- emojiDb.some(x => {
- if (x.name.includes(this.q) && !matched.some(y => y.emoji == x.emoji)) matched.push(x);
- return matched.length == max;
- });
- }
+ switch (e.key) {
+ case 'Enter':
+ if (select.value !== -1) {
+ cancel();
+ (items.value[select.value] as any).click();
+ } else {
+ props.close();
+ }
+ break;
- this.emojis = matched;
- } else if (this.type === 'mfmTag') {
- if (this.q == null || this.q == '') {
- this.mfmTags = MFM_TAGS;
- return;
- }
+ case 'Escape':
+ cancel();
+ props.close();
+ break;
- this.mfmTags = MFM_TAGS.filter(tag => tag.startsWith(this.q));
+ case 'ArrowUp':
+ if (select.value !== -1) {
+ cancel();
+ selectPrev();
+ } else {
+ props.close();
}
- },
+ break;
+
+ case 'Tab':
+ case 'ArrowDown':
+ cancel();
+ selectNext();
+ break;
- onMousedown(e) {
- if (!contains(this.$el, e.target) && (this.$el != e.target)) this.close();
- },
+ default:
+ e.stopPropagation();
+ props.textarea.focus();
+ }
+}
- onKeydown(e) {
- const cancel = () => {
- e.preventDefault();
- e.stopPropagation();
- };
+function selectNext() {
+ if (++select.value >= items.value.length) select.value = 0;
+ if (items.value.length === 0) select.value = -1;
+ applySelect();
+}
- switch (e.which) {
- case 10: // [ENTER]
- case 13: // [ENTER]
- if (this.select !== -1) {
- cancel();
- (this.items[this.select] as any).click();
- } else {
- this.close();
- }
- break;
+function selectPrev() {
+ if (--select.value < 0) select.value = items.value.length - 1;
+ applySelect();
+}
- case 27: // [ESC]
- cancel();
- this.close();
- break;
+function applySelect() {
+ for (const el of Array.from(items.value)) {
+ el.removeAttribute('data-selected');
+ }
- case 38: // [↑]
- if (this.select !== -1) {
- cancel();
- this.selectPrev();
- } else {
- this.close();
- }
- break;
+ if (select.value !== -1) {
+ items.value[select.value].setAttribute('data-selected', 'true');
+ (items.value[select.value] as any).focus();
+ }
+}
- case 9: // [TAB]
- case 40: // [↓]
- cancel();
- this.selectNext();
- break;
+function chooseUser() {
+ props.close();
+ os.selectUser().then(user => {
+ complete('user', user);
+ props.textarea.focus();
+ });
+}
- default:
- e.stopPropagation();
- this.textarea.focus();
- }
- },
+onUpdated(() => {
+ setPosition();
+ items.value = suggests.value?.children || [];
+});
- selectNext() {
- if (++this.select >= this.items.length) this.select = 0;
- if (this.items.length === 0) this.select = -1;
- this.applySelect();
- },
+onMounted(() => {
+ setPosition();
- selectPrev() {
- if (--this.select < 0) this.select = this.items.length - 1;
- this.applySelect();
- },
+ props.textarea.addEventListener('keydown', onKeydown);
- applySelect() {
- for (const el of Array.from(this.items)) {
- el.removeAttribute('data-selected');
- }
+ for (const el of Array.from(document.querySelectorAll('body *'))) {
+ el.addEventListener('mousedown', onMousedown);
+ }
- if (this.select !== -1) {
- this.items[this.select].setAttribute('data-selected', 'true');
- (this.items[this.select] as any).focus();
- }
- },
+ nextTick(() => {
+ exec();
- chooseUser() {
- this.close();
- os.selectUser().then(user => {
- this.complete('user', user);
- this.textarea.focus();
+ watch(() => props.q, () => {
+ nextTick(() => {
+ exec();
});
- },
+ });
+ });
+});
+
+onBeforeUnmount(() => {
+ props.textarea.removeEventListener('keydown', onKeydown);
- acct
+ for (const el of Array.from(document.querySelectorAll('body *'))) {
+ el.removeEventListener('mousedown', onMousedown);
}
});
</script>
diff --git a/packages/client/src/components/avatars.vue b/packages/client/src/components/avatars.vue
index e843d26daa..958e5db0a1 100644
--- a/packages/client/src/components/avatars.vue
+++ b/packages/client/src/components/avatars.vue
@@ -1,30 +1,24 @@
<template>
<div>
- <div v-for="user in us" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
+ <div v-for="user in users" :key="user.id" style="display:inline-block;width:32px;height:32px;margin-right:8px;">
<MkAvatar :user="user" style="width:32px;height:32px;" :show-indicator="true"/>
</div>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, ref } from 'vue';
import * as os from '@/os';
-export default defineComponent({
- props: {
- userIds: {
- required: true
- },
- },
- data() {
- return {
- us: []
- };
- },
- async created() {
- this.us = await os.api('users/show', {
- userIds: this.userIds
- });
- }
+const props = defineProps<{
+ userIds: string[];
+}>();
+
+const users = ref([]);
+
+onMounted(async () => {
+ users.value = await os.api('users/show', {
+ userIds: props.userIds
+ });
});
</script>
diff --git a/packages/client/src/components/captcha.vue b/packages/client/src/components/captcha.vue
index baa922506e..307fc312bc 100644
--- a/packages/client/src/components/captcha.vue
+++ b/packages/client/src/components/captcha.vue
@@ -1,12 +1,14 @@
<template>
<div>
- <span v-if="!available">{{ $ts.waiting }}<MkEllipsis/></span>
- <div ref="captcha"></div>
+ <span v-if="!available">{{ i18n.locale.waiting }}<MkEllipsis/></span>
+ <div ref="captchaEl"></div>
</div>
</template>
-<script lang="ts">
-import { defineComponent, PropType } from 'vue';
+<script lang="ts" setup>
+import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
type Captcha = {
render(container: string | Node, options: {
@@ -14,7 +16,7 @@ type Captcha = {
}): string;
remove(id: string): void;
execute(id: string): void;
- reset(id: string): void;
+ reset(id?: string): void;
getResponse(id: string): string;
};
@@ -29,95 +31,85 @@ declare global {
}
}
-export default defineComponent({
- props: {
- provider: {
- type: String as PropType<CaptchaProvider>,
- required: true,
- },
- sitekey: {
- type: String,
- required: true,
- },
- modelValue: {
- type: String,
- },
- },
+const props = defineProps<{
+ provider: CaptchaProvider;
+ sitekey: string;
+ modelValue?: string | null;
+}>();
- data() {
- return {
- available: false,
- };
- },
+const emit = defineEmits<{
+ (ev: 'update:modelValue', v: string | null): void;
+}>();
- computed: {
- variable(): string {
- switch (this.provider) {
- case 'hcaptcha': return 'hcaptcha';
- case 'recaptcha': return 'grecaptcha';
- }
- },
- loaded(): boolean {
- return !!window[this.variable];
- },
- src(): string {
- const endpoint = ({
- hcaptcha: 'https://hcaptcha.com/1',
- recaptcha: 'https://www.recaptcha.net/recaptcha',
- } as Record<CaptchaProvider, string>)[this.provider];
+const available = ref(false);
- return `${typeof endpoint === 'string' ? endpoint : 'about:invalid'}/api.js?render=explicit`;
- },
- captcha(): Captcha {
- return window[this.variable] || {} as unknown as Captcha;
- },
- },
+const captchaEl = ref<HTMLDivElement | undefined>();
- created() {
- if (this.loaded) {
- this.available = true;
- } else {
- (document.getElementById(this.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
- async: true,
- id: this.provider,
- src: this.src,
- })))
- .addEventListener('load', () => this.available = true);
- }
- },
+const variable = computed(() => {
+ switch (props.provider) {
+ case 'hcaptcha': return 'hcaptcha';
+ case 'recaptcha': return 'grecaptcha';
+ }
+});
+
+const loaded = computed(() => !!window[variable.value]);
+
+const src = computed(() => {
+ switch (props.provider) {
+ case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off';
+ case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
+ }
+});
+
+const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
+
+if (loaded.value) {
+ available.value = true;
+} else {
+ (document.getElementById(props.provider) || document.head.appendChild(Object.assign(document.createElement('script'), {
+ async: true,
+ id: props.provider,
+ src: src.value,
+ })))
+ .addEventListener('load', () => available.value = true);
+}
+
+function reset() {
+ if (captcha.value?.reset) captcha.value.reset();
+}
+
+function requestRender() {
+ if (captcha.value.render && captchaEl.value instanceof Element) {
+ captcha.value.render(captchaEl.value, {
+ sitekey: props.sitekey,
+ theme: defaultStore.state.darkMode ? 'dark' : 'light',
+ callback: callback,
+ 'expired-callback': callback,
+ 'error-callback': callback,
+ });
+ } else {
+ window.setTimeout(requestRender, 1);
+ }
+}
+
+function callback(response?: string) {
+ emit('update:modelValue', typeof response == 'string' ? response : null);
+}
- mounted() {
- if (this.available) {
- this.requestRender();
- } else {
- this.$watch('available', this.requestRender);
- }
- },
+onMounted(() => {
+ if (available.value) {
+ requestRender();
+ } else {
+ watch(available, requestRender);
+ }
+});
- beforeUnmount() {
- this.reset();
- },
+onBeforeUnmount(() => {
+ reset();
+});
- methods: {
- reset() {
- if (this.captcha?.reset) this.captcha.reset();
- },
- requestRender() {
- if (this.captcha.render && this.$refs.captcha instanceof Element) {
- this.captcha.render(this.$refs.captcha, {
- sitekey: this.sitekey,
- theme: this.$store.state.darkMode ? 'dark' : 'light',
- callback: this.callback,
- 'expired-callback': this.callback,
- 'error-callback': this.callback,
- });
- } else {
- setTimeout(this.requestRender.bind(this), 1);
- }
- },
- callback(response?: string) {
- this.$emit('update:modelValue', typeof response == 'string' ? response : null);
- },
- },
+defineExpose({
+ reset,
});
+
</script>
diff --git a/packages/client/src/components/channel-follow-button.vue b/packages/client/src/components/channel-follow-button.vue
index abde7c8148..0ad5384cd5 100644
--- a/packages/client/src/components/channel-follow-button.vue
+++ b/packages/client/src/components/channel-follow-button.vue
@@ -6,66 +6,54 @@
>
<template v-if="!wait">
<template v-if="isFollowing">
- <span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
+ <span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
</template>
<template v-else>
- <span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
+ <span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
</template>
</template>
<template v-else>
- <span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+ <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
</template>
</button>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
import * as os from '@/os';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- channel: {
- type: Object,
- required: true
- },
- full: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
+const props = withDefaults(defineProps<{
+ channel: Record<string, any>;
+ full?: boolean;
+}>(), {
+ full: false,
+});
- data() {
- return {
- isFollowing: this.channel.isFollowing,
- wait: false,
- };
- },
+const isFollowing = ref<boolean>(props.channel.isFollowing);
+const wait = ref(false);
- methods: {
- async onClick() {
- this.wait = true;
+async function onClick() {
+ wait.value = true;
- try {
- if (this.isFollowing) {
- await os.api('channels/unfollow', {
- channelId: this.channel.id
- });
- this.isFollowing = false;
- } else {
- await os.api('channels/follow', {
- channelId: this.channel.id
- });
- this.isFollowing = true;
- }
- } catch (e) {
- console.error(e);
- } finally {
- this.wait = false;
- }
+ try {
+ if (isFollowing.value) {
+ await os.api('channels/unfollow', {
+ channelId: props.channel.id
+ });
+ isFollowing.value = false;
+ } else {
+ await os.api('channels/follow', {
+ channelId: props.channel.id
+ });
+ isFollowing.value = true;
}
+ } catch (e) {
+ console.error(e);
+ } finally {
+ wait.value = false;
}
-});
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/channel-preview.vue b/packages/client/src/components/channel-preview.vue
index f2b6de97fd..8d135a192f 100644
--- a/packages/client/src/components/channel-preview.vue
+++ b/packages/client/src/components/channel-preview.vue
@@ -6,7 +6,7 @@
<div class="status">
<div>
<i class="fas fa-users fa-fw"></i>
- <I18n :src="$ts._channel.usersCount" tag="span" style="margin-left: 4px;">
+ <I18n :src="i18n.locale._channel.usersCount" tag="span" style="margin-left: 4px;">
<template #n>
<b>{{ channel.usersCount }}</b>
</template>
@@ -14,7 +14,7 @@
</div>
<div>
<i class="fas fa-pencil-alt fa-fw"></i>
- <I18n :src="$ts._channel.notesCount" tag="span" style="margin-left: 4px;">
+ <I18n :src="i18n.locale._channel.notesCount" tag="span" style="margin-left: 4px;">
<template #n>
<b>{{ channel.notesCount }}</b>
</template>
@@ -27,37 +27,26 @@
</article>
<footer>
<span v-if="channel.lastNotedAt">
- {{ $ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
+ {{ i18n.locale.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
</span>
</footer>
</MkA>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- channel: {
- type: Object,
- required: true
- },
- },
+const props = defineProps<{
+ channel: Record<string, any>;
+}>();
- data() {
- return {
- };
- },
-
- computed: {
- bannerStyle() {
- if (this.channel.bannerUrl) {
- return { backgroundImage: `url(${this.channel.bannerUrl})` };
- } else {
- return { backgroundColor: '#4c5e6d' };
- }
- }
- },
+const bannerStyle = computed(() => {
+ if (props.channel.bannerUrl) {
+ return { backgroundImage: `url(${props.channel.bannerUrl})` };
+ } else {
+ return { backgroundColor: '#4c5e6d' };
+ }
});
</script>
diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue
index c4d0eb85dd..1959271f5d 100644
--- a/packages/client/src/components/chart.vue
+++ b/packages/client/src/components/chart.vue
@@ -170,10 +170,10 @@ export default defineComponent({
aspectRatio: props.aspectRatio || 2.5,
layout: {
padding: {
- left: 16,
- right: 16,
- top: 16,
- bottom: 8,
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
},
},
scales: {
diff --git a/packages/client/src/components/code-core.vue b/packages/client/src/components/code-core.vue
index b58484c2ac..45a38afe04 100644
--- a/packages/client/src/components/code-core.vue
+++ b/packages/client/src/components/code-core.vue
@@ -3,33 +3,17 @@
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import 'prismjs';
import 'prismjs/themes/prism-okaidia.css';
-export default defineComponent({
- props: {
- code: {
- type: String,
- required: true
- },
- lang: {
- type: String,
- required: false
- },
- inline: {
- type: Boolean,
- required: false
- }
- },
- computed: {
- prismLang() {
- return Prism.languages[this.lang] ? this.lang : 'js';
- },
- html() {
- return Prism.highlight(this.code, Prism.languages[this.prismLang], this.prismLang);
- }
- }
-});
+const props = defineProps<{
+ code: string;
+ lang?: string;
+ inline?: boolean;
+}>();
+
+const prismLang = computed(() => Prism.languages[props.lang] ? props.lang : 'js');
+const html = computed(() => Prism.highlight(props.code, Prism.languages[prismLang.value], prismLang.value));
</script>
diff --git a/packages/client/src/components/code.vue b/packages/client/src/components/code.vue
index f5d6c5673a..d6478fd2f8 100644
--- a/packages/client/src/components/code.vue
+++ b/packages/client/src/components/code.vue
@@ -2,26 +2,14 @@
<XCode :code="code" :lang="lang" :inline="inline"/>
</template>
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+<script lang="ts" setup>
+import { defineAsyncComponent } from 'vue';
-export default defineComponent({
- components: {
- XCode: defineAsyncComponent(() => import('./code-core.vue'))
- },
- props: {
- code: {
- type: String,
- required: true
- },
- lang: {
- type: String,
- required: false
- },
- inline: {
- type: Boolean,
- required: false
- }
- }
-});
+defineProps<{
+ code: string;
+ lang?: string;
+ inline?: boolean;
+}>();
+
+const XCode = defineAsyncComponent(() => import('./code-core.vue'));
</script>
diff --git a/packages/client/src/components/cw-button.vue b/packages/client/src/components/cw-button.vue
index 4bec7b511e..ccfd11462a 100644
--- a/packages/client/src/components/cw-button.vue
+++ b/packages/client/src/components/cw-button.vue
@@ -1,45 +1,37 @@
<template>
<button class="nrvgflfu _button" @click="toggle">
- <b>{{ modelValue ? $ts._cw.hide : $ts._cw.show }}</b>
+ <b>{{ modelValue ? i18n.locale._cw.hide : i18n.locale._cw.show }}</b>
<span v-if="!modelValue">{{ label }}</span>
</button>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import { length } from 'stringz';
+import * as misskey from 'misskey-js';
import { concat } from '@/scripts/array';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- modelValue: {
- type: Boolean,
- required: true
- },
- note: {
- type: Object,
- required: true
- }
- },
-
- computed: {
- label(): string {
- return concat([
- this.note.text ? [this.$t('_cw.chars', { count: length(this.note.text) })] : [],
- this.note.files && this.note.files.length !== 0 ? [this.$t('_cw.files', { count: this.note.files.length }) ] : [],
- this.note.poll != null ? [this.$ts.poll] : []
- ] as string[][]).join(' / ');
- }
- },
+const props = defineProps<{
+ modelValue: boolean;
+ note: misskey.entities.Note;
+}>();
- methods: {
- length,
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: boolean): void;
+}>();
- toggle() {
- this.$emit('update:modelValue', !this.modelValue);
- }
- }
+const label = computed(() => {
+ return concat([
+ props.note.text ? [i18n.t('_cw.chars', { count: length(props.note.text) })] : [],
+ props.note.files && props.note.files.length !== 0 ? [i18n.t('_cw.files', { count: props.note.files.length }) ] : [],
+ props.note.poll != null ? [i18n.locale.poll] : []
+ ] as string[][]).join(' / ');
});
+
+const toggle = () => {
+ emit('update:modelValue', !props.modelValue);
+};
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/date-separated-list.vue b/packages/client/src/components/date-separated-list.vue
index aa84c6f60d..c85a0a6ffc 100644
--- a/packages/client/src/components/date-separated-list.vue
+++ b/packages/client/src/components/date-separated-list.vue
@@ -1,6 +1,8 @@
<script lang="ts">
import { defineComponent, h, PropType, TransitionGroup } from 'vue';
import MkAd from '@/components/global/ad.vue';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
export default defineComponent({
props: {
@@ -30,29 +32,29 @@ export default defineComponent({
},
},
- methods: {
- getDateText(time: string) {
+ setup(props, { slots, expose }) {
+ function getDateText(time: string) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
- return this.$t('monthAndDay', {
+ return i18n.t('monthAndDay', {
month: month.toString(),
day: date.toString()
});
}
- },
- render() {
- if (this.items.length === 0) return;
+ if (props.items.length === 0) return;
+
+ const renderChildren = () => props.items.map((item, i) => {
+ if (!slots || !slots.default) return;
- const renderChildren = () => this.items.map((item, i) => {
- const el = this.$slots.default({
+ const el = slots.default({
item: item
})[0];
if (el.key == null && item.id) el.key = item.id;
if (
- i != this.items.length - 1 &&
- new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
+ i != props.items.length - 1 &&
+ new Date(item.createdAt).getDate() != new Date(props.items[i + 1].createdAt).getDate()
) {
const separator = h('div', {
class: 'separator',
@@ -64,10 +66,10 @@ export default defineComponent({
h('i', {
class: 'fas fa-angle-up icon',
}),
- this.getDateText(item.createdAt)
+ getDateText(item.createdAt)
]),
h('span', [
- this.getDateText(this.items[i + 1].createdAt),
+ getDateText(props.items[i + 1].createdAt),
h('i', {
class: 'fas fa-angle-down icon',
})
@@ -76,7 +78,7 @@ export default defineComponent({
return [el, separator];
} else {
- if (this.ad && item._shouldInsertAd_) {
+ if (props.ad && item._shouldInsertAd_) {
return [h(MkAd, {
class: 'a', // advertiseの意(ブロッカー対策)
key: item.id + ':ad',
@@ -88,18 +90,19 @@ export default defineComponent({
}
});
- return h(this.$store.state.animation ? TransitionGroup : 'div', this.$store.state.animation ? {
- class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''),
- name: 'list',
- tag: 'div',
- 'data-direction': this.direction,
- 'data-reversed': this.reversed ? 'true' : 'false',
- } : {
- class: 'sqadhkmv' + (this.noGap ? ' noGap' : ''),
- }, {
- default: renderChildren
- });
- },
+ return () => h(
+ defaultStore.state.animation ? TransitionGroup : 'div',
+ defaultStore.state.animation ? {
+ class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
+ name: 'list',
+ tag: 'div',
+ 'data-direction': props.direction,
+ 'data-reversed': props.reversed ? 'true' : 'false',
+ } : {
+ class: 'sqadhkmv' + (props.noGap ? ' noGap' : ''),
+ },
+ { default: renderChildren });
+ }
});
</script>
diff --git a/packages/client/src/components/debobigego/base.vue b/packages/client/src/components/debobigego/base.vue
deleted file mode 100644
index 9ed59abc69..0000000000
--- a/packages/client/src/components/debobigego/base.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-<div v-size="{ max: [400] }" class="rbusrurv" :class="{ wide: forceWide }">
- <slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
- props: {
- forceWide: {
- type: Boolean,
- required: false,
- default: false,
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.rbusrurv {
- // 他のCSSからも参照されるので消さないように
- --debobigegoXPadding: 32px;
- --debobigegoYPadding: 32px;
-
- --debobigegoContentHMargin: 16px;
-
- font-size: 95%;
- line-height: 1.3em;
- background: var(--bg);
- padding: var(--debobigegoYPadding) var(--debobigegoXPadding);
- max-width: 750px;
- margin: 0 auto;
-
- &:not(.wide).max-width_400px {
- --debobigegoXPadding: 0px;
-
- > ::v-deep(*) {
- ._debobigegoPanel {
- border: solid 0.5px var(--divider);
- border-radius: 0;
- border-left: none;
- border-right: none;
- }
-
- ._debobigego_group {
- > *:not(._debobigegoNoConcat) {
- &:not(:last-child):not(._debobigegoNoConcatPrev) {
- &._debobigegoPanel, ._debobigegoPanel {
- border-bottom: solid 0.5px var(--divider);
- }
- }
-
- &:not(:first-child):not(._debobigegoNoConcatNext) {
- &._debobigegoPanel, ._debobigegoPanel {
- border-top: none;
- }
- }
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/button.vue b/packages/client/src/components/debobigego/button.vue
deleted file mode 100644
index b883e817a4..0000000000
--- a/packages/client/src/components/debobigego/button.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<template>
-<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>
- <div class="icon">
- <slot name="suffixIcon"></slot>
- </div>
- </div>
- </button>
- <div class="_debobigegoCaption"><slot name="desc"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import './debobigego.scss';
-
-export default defineComponent({
- props: {
- primary: {
- type: Boolean,
- required: false,
- default: false,
- },
- danger: {
- type: Boolean,
- required: false,
- default: false,
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- center: {
- type: Boolean,
- required: false,
- default: true,
- }
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.yzpgjkxe {
- > .main {
- display: flex;
- width: 100%;
- box-sizing: border-box;
- padding: 14px 16px;
- text-align: left;
- align-items: center;
-
- &.center {
- display: block;
- text-align: center;
- }
-
- &.primary {
- color: var(--accent);
- }
-
- &.danger {
- color: #ff2a2a;
- }
-
- > .suffix {
- display: inline-flex;
- margin-left: auto;
- opacity: 0.7;
-
- > .icon {
- margin-left: 1em;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/debobigego.scss b/packages/client/src/components/debobigego/debobigego.scss
deleted file mode 100644
index 833b656b66..0000000000
--- a/packages/client/src/components/debobigego/debobigego.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-._debobigegoPanel {
- background: var(--panel);
- border-radius: var(--radius);
- transition: background 0.2s ease;
-
- &._debobigegoClickable {
- &:hover {
- //background: var(--panelHighlight);
- }
-
- &:active {
- background: var(--panelHighlight);
- transition: background 0s;
- }
- }
-}
-
-._debobigegoLabel,
-._debobigegoCaption {
- font-size: 80%;
- color: var(--fgTransparentWeak);
-
- &:empty {
- display: none;
- }
-}
-
-._debobigegoLabel {
- position: sticky;
- top: var(--stickyTop, 0px);
- z-index: 2;
- 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_ ._debobigegoLabel {
- transition: none !important;
- background: transparent;
-}
-
-._debobigegoCaption {
- padding: 8px var(--debobigegoContentHMargin) 0 var(--debobigegoContentHMargin);
-}
-
-._debobigegoItem {
- & + ._debobigegoItem {
- margin-top: 24px;
- }
-}
diff --git a/packages/client/src/components/debobigego/group.vue b/packages/client/src/components/debobigego/group.vue
deleted file mode 100644
index 871d3c8dba..0000000000
--- a/packages/client/src/components/debobigego/group.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-<template>
-<div v-size="{ max: [500] }" v-sticky-container class="vrtktovg _debobigegoItem _debobigegoNoConcat">
- <div class="_debobigegoLabel"><slot name="label"></slot></div>
- <div ref="child" class="main _debobigego_group">
- <slot></slot>
- </div>
- <div class="_debobigegoCaption"><slot name="caption"></slot></div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, ref } from 'vue';
-
-export default defineComponent({
- setup(props, context) {
- const child = ref<HTMLElement | null>(null);
-
- const scanChild = () => {
- if (child.value == null) return;
- const els = Array.from(child.value.children);
- for (let i = 0; i < els.length; i++) {
- const el = els[i];
- 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');
- }
- }
- };
-
- onMounted(() => {
- scanChild();
-
- const observer = new MutationObserver(records => {
- scanChild();
- });
-
- observer.observe(child.value, {
- childList: true,
- subtree: false,
- attributes: false,
- characterData: false,
- });
- });
-
- return {
- child
- };
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.vrtktovg {
- > .main {
- > ::v-deep(*):not(._debobigegoNoConcat) {
- &:not(._debobigegoNoConcatNext) {
- margin: 0;
- }
-
- &: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(._debobigegoNoConcatNext) {
- &._debobigegoPanel, ._debobigegoPanel {
- border-top: none;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/info.vue b/packages/client/src/components/debobigego/info.vue
deleted file mode 100644
index 41afb03304..0000000000
--- a/packages/client/src/components/debobigego/info.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<template>
-<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>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
- props: {
- warn: {
- type: Boolean,
- required: false,
- default: false
- },
- },
- data() {
- return {
- };
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.fzenkabp {
- > div {
- padding: 14px 16px;
- font-size: 90%;
- background: var(--infoBg);
- color: var(--infoFg);
-
- &.warn {
- background: var(--infoWarnBg);
- color: var(--infoWarnFg);
- }
-
- > i {
- margin-right: 4px;
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/input.vue b/packages/client/src/components/debobigego/input.vue
deleted file mode 100644
index 6228a33fe4..0000000000
--- a/packages/client/src/components/debobigego/input.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-<template>
-<FormGroup class="_debobigegoItem">
- <template #label><slot></slot></template>
- <div class="ztzhwixg _debobigegoItem" :class="{ inline, disabled }">
- <div ref="icon" class="icon"><slot name="icon"></slot></div>
- <div class="input _debobigegoPanel">
- <div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
- <input ref="inputEl"
- v-model="v"
- :type="type"
- :disabled="disabled"
- :required="required"
- :readonly="readonly"
- :placeholder="placeholder"
- :pattern="pattern"
- :autocomplete="autocomplete"
- :spellcheck="spellcheck"
- :step="step"
- :list="id"
- @focus="focused = true"
- @blur="focused = false"
- @keydown="onKeydown($event)"
- @input="onInput"
- >
- <datalist v-if="datalist" :id="id">
- <option v-for="data in datalist" :value="data"/>
- </datalist>
- <div ref="suffixEl" class="suffix"><slot name="suffix"></slot></div>
- </div>
- </div>
- <template #caption><slot name="desc"></slot></template>
-
- <FormButton v-if="manualSave && changed" primary @click="updated"><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 './debobigego.scss';
-import FormButton from './button.vue';
-import FormGroup from './group.vue';
-
-export default defineComponent({
- components: {
- FormGroup,
- FormButton,
- },
- props: {
- modelValue: {
- required: false
- },
- type: {
- type: String,
- required: false
- },
- required: {
- type: Boolean,
- required: false
- },
- readonly: {
- type: Boolean,
- required: false
- },
- disabled: {
- type: Boolean,
- required: false
- },
- pattern: {
- type: String,
- required: false
- },
- placeholder: {
- type: String,
- required: false
- },
- autofocus: {
- type: Boolean,
- required: false,
- default: false
- },
- autocomplete: {
- required: false
- },
- spellcheck: {
- required: false
- },
- step: {
- required: false
- },
- datalist: {
- type: Array,
- required: false,
- },
- inline: {
- 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);
- const id = Math.random().toString(); // TODO: uuid?
- const focused = ref(false);
- const changed = ref(false);
- const invalid = ref(false);
- const filled = computed(() => v.value !== '' && v.value != null);
- const inputEl = ref(null);
- const prefixEl = ref(null);
- const suffixEl = ref(null);
-
- const focus = () => inputEl.value.focus();
- const onInput = (ev) => {
- changed.value = true;
- context.emit('change', ev);
- };
- const onKeydown = (ev: KeyboardEvent) => {
- context.emit('keydown', ev);
-
- if (ev.code === 'Enter') {
- context.emit('enter');
- }
- };
-
- const updated = () => {
- changed.value = false;
- if (type?.value === 'number') {
- context.emit('update:modelValue', parseFloat(v.value));
- } else {
- context.emit('update:modelValue', v.value);
- }
- };
-
- watch(modelValue.value, 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 {
- id,
- v,
- focused,
- invalid,
- changed,
- filled,
- inputEl,
- prefixEl,
- suffixEl,
- focus,
- onInput,
- onKeydown,
- updated,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.ztzhwixg {
- position: relative;
-
- > .icon {
- position: absolute;
- top: 0;
- left: 0;
- width: 24px;
- text-align: center;
- line-height: 32px;
-
- &:not(:empty) + .input {
- margin-left: 28px;
- }
- }
-
- > .input {
- $height: 48px;
- position: relative;
-
- > input {
- display: block;
- height: $height;
- width: 100%;
- margin: 0;
- padding: 0 16px;
- font: inherit;
- font-weight: normal;
- font-size: 1em;
- line-height: $height;
- color: var(--inputText);
- background: transparent;
- border: none;
- border-radius: 0;
- outline: none;
- box-shadow: none;
- box-sizing: border-box;
-
- &[type='file'] {
- display: none;
- }
- }
-
- > .prefix,
- > .suffix {
- display: block;
- position: absolute;
- z-index: 1;
- top: 0;
- padding: 0 16px;
- font-size: 1em;
- line-height: $height;
- color: var(--inputLabel);
- pointer-events: none;
-
- &:empty {
- display: none;
- }
-
- > * {
- display: inline-block;
- min-width: 16px;
- max-width: 150px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- }
-
- > .prefix {
- left: 0;
- padding-right: 8px;
- }
-
- > .suffix {
- right: 0;
- padding-left: 8px;
- }
- }
-
- &.inline {
- display: inline-block;
- margin: 0;
- }
-
- &.disabled {
- opacity: 0.7;
-
- &, * {
- cursor: not-allowed !important;
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/key-value-view.vue b/packages/client/src/components/debobigego/key-value-view.vue
deleted file mode 100644
index 0e034a2d54..0000000000
--- a/packages/client/src/components/debobigego/key-value-view.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<div class="_debobigegoItem">
- <div class="_debobigegoPanel anocepby">
- <span class="key"><slot name="key"></slot></span>
- <span class="value"><slot name="value"></slot></span>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import './debobigego.scss';
-
-export default defineComponent({
-
-});
-</script>
-
-<style lang="scss" scoped>
-.anocepby {
- display: flex;
- align-items: center;
- padding: 14px var(--debobigegoContentHMargin);
-
- > .key {
- margin-right: 12px;
- white-space: nowrap;
- }
-
- > .value {
- margin-left: auto;
- opacity: 0.7;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/link.vue b/packages/client/src/components/debobigego/link.vue
deleted file mode 100644
index de463465d4..0000000000
--- a/packages/client/src/components/debobigego/link.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<template>
-<div class="qmfkfnzi _debobigegoItem">
- <a v-if="external" class="main _button _debobigegoPanel _debobigegoClickable" :href="to" target="_blank">
- <span class="icon"><slot name="icon"></slot></span>
- <span class="text"><slot></slot></span>
- <span class="right">
- <span class="text"><slot name="suffix"></slot></span>
- <i class="fas fa-external-link-alt icon"></i>
- </span>
- </a>
- <MkA v-else class="main _button _debobigegoPanel _debobigegoClickable" :class="{ active }" :to="to" :behavior="behavior">
- <span class="icon"><slot name="icon"></slot></span>
- <span class="text"><slot></slot></span>
- <span class="right">
- <span class="text"><slot name="suffix"></slot></span>
- <i class="fas fa-chevron-right icon"></i>
- </span>
- </MkA>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import './debobigego.scss';
-
-export default defineComponent({
- props: {
- to: {
- type: String,
- required: true
- },
- active: {
- type: Boolean,
- required: false
- },
- external: {
- type: Boolean,
- required: false
- },
- behavior: {
- type: String,
- required: false,
- },
- },
- data() {
- return {
- };
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.qmfkfnzi {
- > .main {
- display: flex;
- align-items: center;
- width: 100%;
- box-sizing: border-box;
- padding: 14px 16px 14px 14px;
-
- &:hover {
- text-decoration: none;
- }
-
- &.active {
- color: var(--accent);
- background: var(--panelHighlight);
- }
-
- > .icon {
- width: 32px;
- margin-right: 2px;
- flex-shrink: 0;
- text-align: center;
- opacity: 0.8;
-
- &:empty {
- display: none;
-
- & + .text {
- padding-left: 4px;
- }
- }
- }
-
- > .text {
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- padding-right: 12px;
- }
-
- > .right {
- margin-left: auto;
- opacity: 0.7;
-
- > .text:not(:empty) {
- margin-right: 0.75em;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/object-view.vue b/packages/client/src/components/debobigego/object-view.vue
deleted file mode 100644
index 68be08560b..0000000000
--- a/packages/client/src/components/debobigego/object-view.vue
+++ /dev/null
@@ -1,102 +0,0 @@
-<template>
-<FormGroup class="_debobigegoItem">
- <template #label><slot></slot></template>
- <div class="drooglns _debobigegoItem" :class="{ tall }">
- <div class="input _debobigegoPanel">
- <textarea v-model="v"
- class="_monospace"
- readonly
- :spellcheck="false"
- ></textarea>
- </div>
- </div>
- <template #caption><slot name="desc"></slot></template>
-</FormGroup>
-</template>
-
-<script lang="ts">
-import { defineComponent, ref, toRefs, watch } from 'vue';
-import * as JSON5 from 'json5';
-import './debobigego.scss';
-import FormGroup from './group.vue';
-
-export default defineComponent({
- components: {
- FormGroup,
- },
- props: {
- value: {
- 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 { value } = toRefs(props);
- const v = ref('');
-
- watch(() => value, newValue => {
- v.value = JSON5.stringify(newValue.value, null, '\t');
- }, {
- immediate: true
- });
-
- return {
- v,
- };
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.drooglns {
- position: relative;
-
- > .input {
- position: relative;
-
- > textarea {
- display: block;
- width: 100%;
- min-width: 100%;
- max-width: 100%;
- min-height: 130px;
- margin: 0;
- padding: 16px var(--debobigegoContentHMargin);
- 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);
- tab-size: 2;
- white-space: pre;
- }
- }
-
- &.tall {
- > .input {
- > textarea {
- min-height: 200px;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/pagination.vue b/packages/client/src/components/debobigego/pagination.vue
deleted file mode 100644
index 16779caa42..0000000000
--- a/packages/client/src/components/debobigego/pagination.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<template>
-<FormGroup class="uljviswt _debobigegoItem">
- <template #label><slot name="label"></slot></template>
- <slot :items="items"></slot>
- <div v-if="empty" key="_empty_" class="empty">
- <slot name="empty"></slot>
- </div>
- <FormButton v-show="more" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
- </FormButton>
-</FormGroup>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormButton from './button.vue';
-import FormGroup from './group.vue';
-import paging from '@/scripts/paging';
-
-export default defineComponent({
- components: {
- FormButton,
- FormGroup,
- },
-
- mixins: [
- paging({}),
- ],
-
- props: {
- pagination: {
- required: true
- },
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.uljviswt {
-}
-</style>
diff --git a/packages/client/src/components/debobigego/radios.vue b/packages/client/src/components/debobigego/radios.vue
deleted file mode 100644
index b4c5841337..0000000000
--- a/packages/client/src/components/debobigego/radios.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<script lang="ts">
-import { defineComponent, h } from 'vue';
-import MkRadio from '@/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/packages/client/src/components/debobigego/range.vue b/packages/client/src/components/debobigego/range.vue
deleted file mode 100644
index dc71f25d83..0000000000
--- a/packages/client/src/components/debobigego/range.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<template>
-<div class="ifitouly _debobigegoItem" :class="{ focused, disabled }">
- <div class="_debobigegoLabel"><slot name="label"></slot></div>
- <div class="_debobigegoPanel main">
- <input
- ref="input"
- v-model="v"
- type="range"
- :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/packages/client/src/components/debobigego/select.vue b/packages/client/src/components/debobigego/select.vue
deleted file mode 100644
index 081bbfe302..0000000000
--- a/packages/client/src/components/debobigego/select.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-<template>
-<div class="yrtfrpux _debobigegoItem" :class="{ disabled, inline }">
- <div class="_debobigegoLabel"><slot name="label"></slot></div>
- <div ref="icon" class="icon"><slot name="icon"></slot></div>
- <div class="input _debobigegoPanel _debobigegoClickable" @click="focus">
- <div ref="prefix" class="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/packages/client/src/components/debobigego/suspense.vue b/packages/client/src/components/debobigego/suspense.vue
deleted file mode 100644
index acb0b64424..0000000000
--- a/packages/client/src/components/debobigego/suspense.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<template>
-<transition name="fade" mode="out-in">
- <div v-if="pending" class="_debobigegoItem">
- <div class="_debobigegoPanel">
- <MkLoading/>
- </div>
- </div>
- <div v-else-if="resolved" class="_debobigegoItem">
- <slot :result="result"></slot>
- </div>
- <div v-else class="_debobigegoItem">
- <div class="_debobigegoPanel eiurkvay">
- <div><i class="fas fa-exclamation-triangle"></i> {{ $ts.somethingHappened }}</div>
- <MkButton inline class="retry" @click="retry"><i class="fas fa-redo-alt"></i> {{ $ts.retry }}</MkButton>
- </div>
- </div>
-</transition>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType, ref, watch } from 'vue';
-import './debobigego.scss';
-import MkButton from '@/components/ui/button.vue';
-
-export default defineComponent({
- components: {
- MkButton
- },
-
- props: {
- p: {
- type: Function as PropType<() => Promise<any>>,
- required: true,
- }
- },
-
- setup(props, context) {
- const pending = ref(true);
- const resolved = ref(false);
- const rejected = ref(false);
- const result = ref(null);
-
- const process = () => {
- if (props.p == null) {
- return;
- }
- const promise = props.p();
- pending.value = true;
- resolved.value = false;
- rejected.value = false;
- promise.then((_result) => {
- pending.value = false;
- resolved.value = true;
- result.value = _result;
- });
- promise.catch(() => {
- pending.value = false;
- rejected.value = true;
- });
- };
-
- watch(() => props.p, () => {
- process();
- }, {
- immediate: true
- });
-
- const retry = () => {
- process();
- };
-
- return {
- pending,
- resolved,
- rejected,
- result,
- retry,
- };
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
- transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
- opacity: 0;
-}
-
-.eiurkvay {
- padding: 16px;
- text-align: center;
-
- > .retry {
- margin-top: 16px;
- }
-}
-</style>
diff --git a/packages/client/src/components/debobigego/switch.vue b/packages/client/src/components/debobigego/switch.vue
deleted file mode 100644
index 239140f730..0000000000
--- a/packages/client/src/components/debobigego/switch.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<template>
-<div class="ijnpvmgr _debobigegoItem">
- <div class="main _debobigegoPanel _debobigegoClickable"
- :class="{ disabled, checked }"
- :aria-checked="checked"
- :aria-disabled="disabled"
- @click.prevent="toggle"
- >
- <input
- ref="input"
- type="checkbox"
- :disabled="disabled"
- @keydown.enter="toggle"
- >
- <span v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button">
- <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/packages/client/src/components/debobigego/textarea.vue b/packages/client/src/components/debobigego/textarea.vue
deleted file mode 100644
index ca5b35c49e..0000000000
--- a/packages/client/src/components/debobigego/textarea.vue
+++ /dev/null
@@ -1,161 +0,0 @@
-<template>
-<FormGroup class="_debobigegoItem">
- <template #label><slot></slot></template>
- <div class="rivhosbp _debobigegoItem" :class="{ tall, pre }">
- <div class="input _debobigegoPanel">
- <textarea ref="input" v-model="v"
- :class="{ code, _monospace: code }"
- :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" primary @click="updated"><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/packages/client/src/components/debobigego/tuple.vue b/packages/client/src/components/debobigego/tuple.vue
deleted file mode 100644
index 1d2a6cb55e..0000000000
--- a/packages/client/src/components/debobigego/tuple.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<template>
-<div v-size="{ max: [500] }" class="wthhikgt _debobigegoItem">
- <slot></slot>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-});
-</script>
-
-<style lang="scss" scoped>
-.wthhikgt {
- position: relative;
- display: flex;
-
- > ::v-deep(*) {
- flex: 1;
- margin: 0;
-
- &:not(:last-child) {
- margin-right: 16px;
- }
- }
-
- &.max-width_500px {
- display: block;
-
- > ::v-deep(*) {
- margin: inherit;
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/dialog.vue b/packages/client/src/components/dialog.vue
index c2fa1b02b8..b6b649cde9 100644
--- a/packages/client/src/components/dialog.vue
+++ b/packages/client/src/components/dialog.vue
@@ -1,5 +1,5 @@
<template>
-<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="$emit('closed')">
+<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
<div class="mk-dialog">
<div v-if="icon" class="icon">
<i :class="icon"></i>
@@ -14,7 +14,7 @@
</div>
<header v-if="title"><Mfm :text="title"/></header>
<div v-if="text" class="body"><Mfm :text="text"/></div>
- <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder" @keydown="onInputKeydown">
+ <MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="fas fa-lock"></i></template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
@@ -28,8 +28,8 @@
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
- <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? $ts.ok : $ts.gotIt }}</MkButton>
- <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ $ts.cancel }}</MkButton>
+ <MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.locale.ok : i18n.locale.gotIt }}</MkButton>
+ <MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.locale.cancel }}</MkButton>
</div>
<div v-if="actions" class="buttons">
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
@@ -38,118 +38,108 @@
</MkModal>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onBeforeUnmount, onMounted, ref } from 'vue';
import MkModal from '@/components/ui/modal.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkModal,
- MkButton,
- MkInput,
- MkSelect,
- },
+type Input = {
+ type: HTMLInputElement['type'];
+ placeholder?: string | null;
+ default: any | null;
+};
- props: {
- type: {
- type: String,
- required: false,
- default: 'info'
- },
- title: {
- type: String,
- required: false
- },
- text: {
- type: String,
- required: false
- },
- input: {
- required: false
- },
- select: {
- required: false
- },
- icon: {
- required: false
- },
- actions: {
- required: false
- },
- showOkButton: {
- type: Boolean,
- default: true
- },
- showCancelButton: {
- type: Boolean,
- default: false
- },
- cancelableByBgClick: {
- type: Boolean,
- default: true
- },
- },
+type Select = {
+ items: {
+ value: string;
+ text: string;
+ }[];
+ groupedItems: {
+ label: string;
+ items: {
+ value: string;
+ text: string;
+ }[];
+ }[];
+ default: string | null;
+};
- emits: ['done', 'closed'],
+const props = withDefaults(defineProps<{
+ type?: 'success' | 'error' | 'warning' | 'info' | 'question' | 'waiting';
+ title: string;
+ text?: string;
+ input?: Input;
+ select?: Select;
+ icon?: string;
+ actions?: {
+ text: string;
+ primary?: boolean,
+ callback: (...args: any[]) => void;
+ }[];
+ showOkButton?: boolean;
+ showCancelButton?: boolean;
+ cancelableByBgClick?: boolean;
+}>(), {
+ type: 'info',
+ showOkButton: true,
+ showCancelButton: false,
+ cancelableByBgClick: true,
+});
- data() {
- return {
- inputValue: this.input && this.input.default ? this.input.default : null,
- selectedValue: this.select ? this.select.default ? this.select.default : this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
- };
- },
+const emit = defineEmits<{
+ (e: 'done', v: { canceled: boolean; result: any }): void;
+ (e: 'closed'): void;
+}>();
- mounted() {
- document.addEventListener('keydown', this.onKeydown);
- },
+const modal = ref<InstanceType<typeof MkModal>>();
- beforeUnmount() {
- document.removeEventListener('keydown', this.onKeydown);
- },
+const inputValue = ref(props.input?.default || null);
+const selectedValue = ref(props.select?.default || null);
- methods: {
- done(canceled, result?) {
- this.$emit('done', { canceled, result });
- this.$refs.modal.close();
- },
+function done(canceled: boolean, result?) {
+ emit('done', { canceled, result });
+ modal.value?.close();
+}
- async ok() {
- if (!this.showOkButton) return;
+async function ok() {
+ if (!props.showOkButton) return;
- const result =
- this.input ? this.inputValue :
- this.select ? this.selectedValue :
- true;
- this.done(false, result);
- },
+ const result =
+ props.input ? inputValue.value :
+ props.select ? selectedValue.value :
+ true;
+ done(false, result);
+}
- cancel() {
- this.done(true);
- },
+function cancel() {
+ done(true);
+}
+/*
+function onBgClick() {
+ if (props.cancelableByBgClick) cancel();
+}
+*/
+function onKeydown(e: KeyboardEvent) {
+ if (e.key === 'Escape') cancel();
+}
- onBgClick() {
- if (this.cancelableByBgClick) {
- this.cancel();
- }
- },
+function onInputKeydown(e: KeyboardEvent) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ e.stopPropagation();
+ ok();
+ }
+}
- onKeydown(e) {
- if (e.which === 27) { // ESC
- this.cancel();
- }
- },
+onMounted(() => {
+ document.addEventListener('keydown', onKeydown);
+});
- onInputKeydown(e) {
- if (e.which === 13) { // Enter
- e.preventDefault();
- e.stopPropagation();
- this.ok();
- }
- }
- }
+onBeforeUnmount(() => {
+ document.removeEventListener('keydown', onKeydown);
});
</script>
diff --git a/packages/client/src/components/drive-file-thumbnail.vue b/packages/client/src/components/drive-file-thumbnail.vue
index e94b6b8bcb..81b80e7e8e 100644
--- a/packages/client/src/components/drive-file-thumbnail.vue
+++ b/packages/client/src/components/drive-file-thumbnail.vue
@@ -14,71 +14,42 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as Misskey from 'misskey-js';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
-import { ColdDeviceStorage } from '@/store';
-export default defineComponent({
- components: {
- ImgWithBlurhash
- },
- props: {
- file: {
- type: Object,
- required: true
- },
- fit: {
- type: String,
- required: false,
- default: 'cover'
- },
- },
- data() {
- return {
- isContextmenuShowing: false,
- isDragging: false,
+const props = defineProps<{
+ file: Misskey.entities.DriveFile;
+ fit: string;
+}>();
- };
- },
- computed: {
- is(): 'image' | 'video' | 'midi' | 'audio' | 'csv' | 'pdf' | 'textfile' | 'archive' | 'unknown' {
- if (this.file.type.startsWith('image/')) return 'image';
- if (this.file.type.startsWith('video/')) return 'video';
- if (this.file.type === 'audio/midi') return 'midi';
- if (this.file.type.startsWith('audio/')) return 'audio';
- if (this.file.type.endsWith('/csv')) return 'csv';
- if (this.file.type.endsWith('/pdf')) return 'pdf';
- if (this.file.type.startsWith('text/')) return 'textfile';
- if ([
- "application/zip",
- "application/x-cpio",
- "application/x-bzip",
- "application/x-bzip2",
- "application/java-archive",
- "application/x-rar-compressed",
- "application/x-tar",
- "application/gzip",
- "application/x-7z-compressed"
- ].some(e => e === this.file.type)) return 'archive';
- return 'unknown';
- },
- isThumbnailAvailable(): boolean {
- return this.file.thumbnailUrl
- ? (this.is === 'image' || this.is === 'video')
- : false;
- },
- },
- mounted() {
- const audioTag = this.$refs.volumectrl as HTMLAudioElement;
- if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
- },
- methods: {
- volumechange() {
- const audioTag = this.$refs.volumectrl as HTMLAudioElement;
- ColdDeviceStorage.set('mediaVolume', audioTag.volume);
- }
- }
+const is = computed(() => {
+ if (props.file.type.startsWith('image/')) return 'image';
+ if (props.file.type.startsWith('video/')) return 'video';
+ if (props.file.type === 'audio/midi') return 'midi';
+ if (props.file.type.startsWith('audio/')) return 'audio';
+ if (props.file.type.endsWith('/csv')) return 'csv';
+ if (props.file.type.endsWith('/pdf')) return 'pdf';
+ if (props.file.type.startsWith('text/')) return 'textfile';
+ if ([
+ "application/zip",
+ "application/x-cpio",
+ "application/x-bzip",
+ "application/x-bzip2",
+ "application/java-archive",
+ "application/x-rar-compressed",
+ "application/x-tar",
+ "application/gzip",
+ "application/x-7z-compressed"
+ ].some(e => e === props.file.type)) return 'archive';
+ return 'unknown';
+});
+
+const isThumbnailAvailable = computed(() => {
+ return props.file.thumbnailUrl
+ ? (is.value === 'image' as const || is.value === 'video')
+ : false;
});
</script>
diff --git a/packages/client/src/components/drive-select-dialog.vue b/packages/client/src/components/drive-select-dialog.vue
index 75537dfe3e..6d84511277 100644
--- a/packages/client/src/components/drive-select-dialog.vue
+++ b/packages/client/src/components/drive-select-dialog.vue
@@ -7,64 +7,51 @@
@click="cancel()"
@close="cancel()"
@ok="ok()"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header>
- {{ multiple ? ((type === 'file') ? $ts.selectFiles : $ts.selectFolders) : ((type === 'file') ? $ts.selectFile : $ts.selectFolder) }}
+ {{ multiple ? ((type === 'file') ? i18n.locale.selectFiles : i18n.locale.selectFolders) : ((type === 'file') ? i18n.locale.selectFile : i18n.locale.selectFolder) }}
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
</template>
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
</XModalWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
import XDrive from './drive.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import number from '@/filters/number';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XDrive,
- XModalWindow,
- },
-
- props: {
- type: {
- type: String,
- required: false,
- default: 'file'
- },
- multiple: {
- type: Boolean,
- default: false
- }
- },
+withDefaults(defineProps<{
+ type?: 'file' | 'folder';
+ multiple: boolean;
+}>(), {
+ type: 'file',
+});
- emits: ['done', 'closed'],
+const emit = defineEmits<{
+ (e: 'done', r?: Misskey.entities.DriveFile[]): void;
+ (e: 'closed'): void;
+}>();
- data() {
- return {
- selected: []
- };
- },
+const dialog = ref<InstanceType<typeof XModalWindow>>();
- methods: {
- ok() {
- this.$emit('done', this.selected);
- this.$refs.dialog.close();
- },
+const selected = ref<Misskey.entities.DriveFile[]>([]);
- cancel() {
- this.$emit('done');
- this.$refs.dialog.close();
- },
+function ok() {
+ emit('done', selected.value);
+ dialog.value?.close();
+}
- onChangeSelection(xs) {
- this.selected = xs;
- },
+function cancel() {
+ emit('done');
+ dialog.value?.close();
+}
- number
- }
-});
+function onChangeSelection(files: Misskey.entities.DriveFile[]) {
+ selected.value = files;
+}
</script>
diff --git a/packages/client/src/components/drive-window.vue b/packages/client/src/components/drive-window.vue
index 43f07ebe76..8b60bf7794 100644
--- a/packages/client/src/components/drive-window.vue
+++ b/packages/client/src/components/drive-window.vue
@@ -3,42 +3,27 @@
:initial-width="800"
:initial-height="500"
:can-resize="true"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<template #header>
- {{ $ts.drive }}
+ {{ i18n.locale.drive }}
</template>
<XDrive :initial-folder="initialFolder"/>
</XWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as Misskey from 'misskey-js';
import XDrive from './drive.vue';
import XWindow from '@/components/ui/window.vue';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XDrive,
- XWindow,
- },
+defineProps<{
+ initialFolder?: Misskey.entities.DriveFolder;
+}>();
- props: {
- initialFolder: {
- type: Object,
- required: false
- },
- },
-
- emits: ['closed'],
-
- data() {
- return {
- };
- },
-
- methods: {
-
- }
-});
+const emit = defineEmits<{
+ (e: 'closed'): void;
+}>();
</script>
diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue
index 511647229e..fd6a813838 100644
--- a/packages/client/src/components/drive.file.vue
+++ b/packages/client/src/components/drive.file.vue
@@ -8,17 +8,17 @@
@dragstart="onDragstart"
@dragend="onDragend"
>
- <div v-if="$i.avatarId == file.id" class="label">
+ <div v-if="$i?.avatarId == file.id" class="label">
<img src="/client-assets/label.svg"/>
- <p>{{ $ts.avatar }}</p>
+ <p>{{ i18n.locale.avatar }}</p>
</div>
- <div v-if="$i.bannerId == file.id" class="label">
+ <div v-if="$i?.bannerId == file.id" class="label">
<img src="/client-assets/label.svg"/>
- <p>{{ $ts.banner }}</p>
+ <p>{{ i18n.locale.banner }}</p>
</div>
<div v-if="file.isSensitive" class="label red">
<img src="/client-assets/label-red.svg"/>
- <p>{{ $ts.nsfw }}</p>
+ <p>{{ i18n.locale.nsfw }}</p>
</div>
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
@@ -30,179 +30,155 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
-export default defineComponent({
- components: {
- MkDriveFileThumbnail
- },
+const props = withDefaults(defineProps<{
+ file: Misskey.entities.DriveFile;
+ isSelected?: boolean;
+ selectMode?: boolean;
+}>(), {
+ isSelected: false,
+ selectMode: false,
+});
- props: {
- file: {
- type: Object,
- required: true,
- },
- isSelected: {
- type: Boolean,
- required: false,
- default: false,
- },
- selectMode: {
- type: Boolean,
- required: false,
- default: false,
- }
- },
+const emit = defineEmits<{
+ (e: 'chosen', r: Misskey.entities.DriveFile): void;
+ (e: 'dragstart'): void;
+ (e: 'dragend'): void;
+}>();
- emits: ['chosen'],
+const isDragging = ref(false);
- data() {
- return {
- isDragging: false
- };
- },
+const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
- computed: {
- // TODO: parentへの参照を無くす
- browser(): any {
- return this.$parent;
- },
- title(): string {
- return `${this.file.name}\n${this.file.type} ${bytes(this.file.size)}`;
- }
- },
-
- methods: {
- getMenu() {
- return [{
- text: this.$ts.rename,
- icon: 'fas fa-i-cursor',
- action: this.rename
- }, {
- text: this.file.isSensitive ? this.$ts.unmarkAsSensitive : this.$ts.markAsSensitive,
- icon: this.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
- action: this.toggleSensitive
- }, {
- text: this.$ts.describeFile,
- icon: 'fas fa-i-cursor',
- action: this.describe
- }, null, {
- text: this.$ts.copyUrl,
- icon: 'fas fa-link',
- action: this.copyUrl
- }, {
- type: 'a',
- href: this.file.url,
- target: '_blank',
- text: this.$ts.download,
- icon: 'fas fa-download',
- download: this.file.name
- }, null, {
- text: this.$ts.delete,
- icon: 'fas fa-trash-alt',
- danger: true,
- action: this.deleteFile
- }];
- },
+function getMenu() {
+ return [{
+ text: i18n.locale.rename,
+ icon: 'fas fa-i-cursor',
+ action: rename
+ }, {
+ text: props.file.isSensitive ? i18n.locale.unmarkAsSensitive : i18n.locale.markAsSensitive,
+ icon: props.file.isSensitive ? 'fas fa-eye' : 'fas fa-eye-slash',
+ action: toggleSensitive
+ }, {
+ text: i18n.locale.describeFile,
+ icon: 'fas fa-i-cursor',
+ action: describe
+ }, null, {
+ text: i18n.locale.copyUrl,
+ icon: 'fas fa-link',
+ action: copyUrl
+ }, {
+ type: 'a',
+ href: props.file.url,
+ target: '_blank',
+ text: i18n.locale.download,
+ icon: 'fas fa-download',
+ download: props.file.name
+ }, null, {
+ text: i18n.locale.delete,
+ icon: 'fas fa-trash-alt',
+ danger: true,
+ action: deleteFile
+ }];
+}
- onClick(ev) {
- if (this.selectMode) {
- this.$emit('chosen', this.file);
- } else {
- os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
- }
- },
+function onClick(ev: MouseEvent) {
+ if (props.selectMode) {
+ emit('chosen', props.file);
+ } else {
+ os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
+ }
+}
- onContextmenu(e) {
- os.contextMenu(this.getMenu(), e);
- },
+function onContextmenu(e: MouseEvent) {
+ os.contextMenu(getMenu(), e);
+}
- onDragstart(e) {
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(this.file));
- this.isDragging = true;
+function onDragstart(e: DragEvent) {
+ if (e.dataTransfer) {
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
+ }
+ isDragging.value = true;
- // 親ブラウザに対して、ドラッグが開始されたフラグを立てる
- // (=あなたの子供が、ドラッグを開始しましたよ)
- this.browser.isDragSource = true;
- },
+ emit('dragstart');
+}
- onDragend(e) {
- this.isDragging = false;
- this.browser.isDragSource = false;
- },
+function onDragend() {
+ isDragging.value = false;
+ emit('dragend');
+}
- rename() {
- os.inputText({
- title: this.$ts.renameFile,
- placeholder: this.$ts.inputNewFileName,
- default: this.file.name,
- allowEmpty: false
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/files/update', {
- fileId: this.file.id,
- name: name
- });
- });
- },
+function rename() {
+ os.inputText({
+ title: i18n.locale.renameFile,
+ placeholder: i18n.locale.inputNewFileName,
+ default: props.file.name,
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ os.api('drive/files/update', {
+ fileId: props.file.id,
+ name: name
+ });
+ });
+}
- describe() {
- os.popup(import('@/components/media-caption.vue'), {
- title: this.$ts.describeFile,
- input: {
- placeholder: this.$ts.inputNewDescription,
- default: this.file.comment !== null ? this.file.comment : '',
- },
- image: this.file
- }, {
- done: result => {
- if (!result || result.canceled) return;
- let comment = result.result;
- os.api('drive/files/update', {
- fileId: this.file.id,
- comment: comment.length == 0 ? null : comment
- });
- }
- }, 'closed');
+function describe() {
+ os.popup(import('@/components/media-caption.vue'), {
+ title: i18n.locale.describeFile,
+ input: {
+ placeholder: i18n.locale.inputNewDescription,
+ default: props.file.comment !== null ? props.file.comment : '',
},
-
- toggleSensitive() {
+ image: props.file
+ }, {
+ done: result => {
+ if (!result || result.canceled) return;
+ let comment = result.result;
os.api('drive/files/update', {
- fileId: this.file.id,
- isSensitive: !this.file.isSensitive
+ fileId: props.file.id,
+ comment: comment.length == 0 ? null : comment
});
- },
-
- copyUrl() {
- copyToClipboard(this.file.url);
- os.success();
- },
-
- addApp() {
- alert('not implemented yet');
- },
+ }
+ }, 'closed');
+}
- async deleteFile() {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: this.$t('driveFileDeleteConfirm', { name: this.file.name }),
- });
- if (canceled) return;
+function toggleSensitive() {
+ os.api('drive/files/update', {
+ fileId: props.file.id,
+ isSensitive: !props.file.isSensitive
+ });
+}
- os.api('drive/files/delete', {
- fileId: this.file.id
- });
- },
+function copyUrl() {
+ copyToClipboard(props.file.url);
+ os.success();
+}
+/*
+function addApp() {
+ alert('not implemented yet');
+}
+*/
+async function deleteFile() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.t('driveFileDeleteConfirm', { name: props.file.name }),
+ });
- bytes
- }
-});
+ if (canceled) return;
+ os.api('drive/files/delete', {
+ fileId: props.file.id
+ });
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue
index aaba736cf8..20a6343cfe 100644
--- a/packages/client/src/components/drive.folder.vue
+++ b/packages/client/src/components/drive.folder.vue
@@ -19,243 +19,233 @@
<template v-if="!hover"><i class="fas fa-folder fa-fw"></i></template>
{{ folder.name }}
</p>
- <p v-if="$store.state.uploadFolder == folder.id" class="upload">
- {{ $ts.uploadFolder }}
+ <p v-if="defaultStore.state.uploadFolder == folder.id" class="upload">
+ {{ i18n.locale.uploadFolder }}
</p>
<button v-if="selectMode" class="checkbox _button" :class="{ checked: isSelected }" @click.prevent.stop="checkboxClicked"></button>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
-export default defineComponent({
- props: {
- folder: {
- type: Object,
- required: true,
- },
- isSelected: {
- type: Boolean,
- required: false,
- default: false,
- },
- selectMode: {
- type: Boolean,
- required: false,
- default: false,
- }
- },
+const props = withDefaults(defineProps<{
+ folder: Misskey.entities.DriveFolder;
+ isSelected?: boolean;
+ selectMode?: boolean;
+}>(), {
+ isSelected: false,
+ selectMode: false,
+});
- emits: ['chosen'],
+const emit = defineEmits<{
+ (ev: 'chosen', v: Misskey.entities.DriveFolder): void;
+ (ev: 'move', v: Misskey.entities.DriveFolder): void;
+ (ev: 'upload', file: File, folder: Misskey.entities.DriveFolder);
+ (ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
+ (ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
+ (ev: 'dragstart'): void;
+ (ev: 'dragend'): void;
+}>();
- data() {
- return {
- hover: false,
- draghover: false,
- isDragging: false,
- };
- },
+const hover = ref(false);
+const draghover = ref(false);
+const isDragging = ref(false);
- computed: {
- browser(): any {
- return this.$parent;
- },
- title(): string {
- return this.folder.name;
- }
- },
+const title = computed(() => props.folder.name);
- methods: {
- checkboxClicked(e) {
- this.$emit('chosen', this.folder);
- },
+function checkboxClicked() {
+ emit('chosen', props.folder);
+}
- onClick() {
- this.browser.move(this.folder);
- },
+function onClick() {
+ emit('move', props.folder);
+}
- onMouseover() {
- this.hover = true;
- },
+function onMouseover() {
+ hover.value = true;
+}
- onMouseout() {
- this.hover = false
- },
+function onMouseout() {
+ hover.value = false
+}
- onDragover(e) {
- // 自分自身がドラッグされている場合
- if (this.isDragging) {
- // 自分自身にはドロップさせない
- e.dataTransfer.dropEffect = 'none';
- return;
- }
+function onDragover(ev: DragEvent) {
+ if (!ev.dataTransfer) return;
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+ // 自分自身がドラッグされている場合
+ if (isDragging.value) {
+ // 自分自身にはドロップさせない
+ ev.dataTransfer.dropEffect = 'none';
+ return;
+ }
- if (isFile || isDriveFile || isDriveFolder) {
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- } else {
- e.dataTransfer.dropEffect = 'none';
- }
- },
+ const isFile = ev.dataTransfer.items[0].kind == 'file';
+ const isDriveFile = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+ const isDriveFolder = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
- onDragenter() {
- if (!this.isDragging) this.draghover = true;
- },
+ if (isFile || isDriveFile || isDriveFolder) {
+ ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+ } else {
+ ev.dataTransfer.dropEffect = 'none';
+ }
+}
- onDragleave() {
- this.draghover = false;
- },
+function onDragenter() {
+ if (!isDragging.value) draghover.value = true;
+}
- onDrop(e) {
- this.draghover = false;
+function onDragleave() {
+ draghover.value = false;
+}
- // ファイルだったら
- if (e.dataTransfer.files.length > 0) {
- for (const file of Array.from(e.dataTransfer.files)) {
- this.browser.upload(file, this.folder);
- }
- return;
- }
+function onDrop(ev: DragEvent) {
+ draghover.value = false;
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- this.browser.removeFile(file.id);
- os.api('drive/files/update', {
- fileId: file.id,
- folderId: this.folder.id
- });
- }
- //#endregion
+ if (!ev.dataTransfer) return;
+
+ // ファイルだったら
+ if (ev.dataTransfer.files.length > 0) {
+ for (const file of Array.from(ev.dataTransfer.files)) {
+ emit('upload', file, props.folder);
+ }
+ return;
+ }
- //#region ドライブのフォルダ
- const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
- if (driveFolder != null && driveFolder != '') {
- const folder = JSON.parse(driveFolder);
+ //#region ドライブのファイル
+ const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ if (driveFile != null && driveFile != '') {
+ const file = JSON.parse(driveFile);
+ emit('removeFile', file.id);
+ os.api('drive/files/update', {
+ fileId: file.id,
+ folderId: props.folder.id
+ });
+ }
+ //#endregion
- // 移動先が自分自身ならreject
- if (folder.id == this.folder.id) return;
+ //#region ドライブのフォルダ
+ const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+ if (driveFolder != null && driveFolder != '') {
+ const folder = JSON.parse(driveFolder);
- this.browser.removeFolder(folder.id);
- os.api('drive/folders/update', {
- folderId: folder.id,
- parentId: this.folder.id
- }).then(() => {
- // noop
- }).catch(err => {
- switch (err) {
- case 'detected-circular-definition':
- os.alert({
- title: this.$ts.unableToProcess,
- text: this.$ts.circularReferenceFolder
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: this.$ts.somethingHappened
- });
- }
- });
+ // 移動先が自分自身ならreject
+ if (folder.id == props.folder.id) return;
+
+ emit('removeFolder', folder.id);
+ os.api('drive/folders/update', {
+ folderId: folder.id,
+ parentId: props.folder.id
+ }).then(() => {
+ // noop
+ }).catch(err => {
+ switch (err) {
+ case 'detected-circular-definition':
+ os.alert({
+ title: i18n.locale.unableToProcess,
+ text: i18n.locale.circularReferenceFolder
+ });
+ break;
+ default:
+ os.alert({
+ type: 'error',
+ text: i18n.locale.somethingHappened
+ });
}
- //#endregion
- },
+ });
+ }
+ //#endregion
+}
- onDragstart(e) {
- e.dataTransfer.effectAllowed = 'move';
- e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(this.folder));
- this.isDragging = true;
+function onDragstart(ev: DragEvent) {
+ if (!ev.dataTransfer) return;
- // 親ブラウザに対して、ドラッグが開始されたフラグを立てる
- // (=あなたの子供が、ドラッグを開始しましたよ)
- this.browser.isDragSource = true;
- },
+ ev.dataTransfer.effectAllowed = 'move';
+ ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FOLDER_, JSON.stringify(props.folder));
+ isDragging.value = true;
- onDragend() {
- this.isDragging = false;
- this.browser.isDragSource = false;
- },
+ // 親ブラウザに対して、ドラッグが開始されたフラグを立てる
+ // (=あなたの子供が、ドラッグを開始しましたよ)
+ emit('dragstart');
+}
- go() {
- this.browser.move(this.folder.id);
- },
+function onDragend() {
+ isDragging.value = false;
+ emit('dragend');
+}
- newWindow() {
- this.browser.newWindow(this.folder);
- },
+function go() {
+ emit('move', props.folder.id);
+}
- rename() {
- os.inputText({
- title: this.$ts.renameFolder,
- placeholder: this.$ts.inputNewFolderName,
- default: this.folder.name
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/folders/update', {
- folderId: this.folder.id,
- name: name
- });
- });
- },
+function rename() {
+ os.inputText({
+ title: i18n.locale.renameFolder,
+ placeholder: i18n.locale.inputNewFolderName,
+ default: props.folder.name
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ os.api('drive/folders/update', {
+ folderId: props.folder.id,
+ name: name
+ });
+ });
+}
- deleteFolder() {
- os.api('drive/folders/delete', {
- folderId: this.folder.id
- }).then(() => {
- if (this.$store.state.uploadFolder === this.folder.id) {
- this.$store.set('uploadFolder', null);
- }
- }).catch(err => {
- switch(err.id) {
- case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
- os.alert({
- type: 'error',
- title: this.$ts.unableToDelete,
- text: this.$ts.hasChildFilesOrFolders
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: this.$ts.unableToDelete
- });
- }
- });
- },
+function deleteFolder() {
+ os.api('drive/folders/delete', {
+ folderId: props.folder.id
+ }).then(() => {
+ if (defaultStore.state.uploadFolder === props.folder.id) {
+ defaultStore.set('uploadFolder', null);
+ }
+ }).catch(err => {
+ switch(err.id) {
+ case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
+ os.alert({
+ type: 'error',
+ title: i18n.locale.unableToDelete,
+ text: i18n.locale.hasChildFilesOrFolders
+ });
+ break;
+ default:
+ os.alert({
+ type: 'error',
+ text: i18n.locale.unableToDelete
+ });
+ }
+ });
+}
- setAsUploadFolder() {
- this.$store.set('uploadFolder', this.folder.id);
- },
+function setAsUploadFolder() {
+ defaultStore.set('uploadFolder', props.folder.id);
+}
- onContextmenu(e) {
- os.contextMenu([{
- text: this.$ts.openInWindow,
- icon: 'fas fa-window-restore',
- action: () => {
- os.popup(import('./drive-window.vue'), {
- initialFolder: this.folder
- }, {
- }, 'closed');
- }
- }, null, {
- text: this.$ts.rename,
- icon: 'fas fa-i-cursor',
- action: this.rename
- }, null, {
- text: this.$ts.delete,
- icon: 'fas fa-trash-alt',
- danger: true,
- action: this.deleteFolder
- }], e);
- },
- }
-});
+function onContextmenu(ev: MouseEvent) {
+ os.contextMenu([{
+ text: i18n.locale.openInWindow,
+ icon: 'fas fa-window-restore',
+ action: () => {
+ os.popup(import('./drive-window.vue'), {
+ initialFolder: props.folder
+ }, {
+ }, 'closed');
+ }
+ }, null, {
+ text: i18n.locale.rename,
+ icon: 'fas fa-i-cursor',
+ action: rename,
+ }, null, {
+ text: i18n.locale.delete,
+ icon: 'fas fa-trash-alt',
+ danger: true,
+ action: deleteFolder,
+ }], ev);
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.nav-folder.vue b/packages/client/src/components/drive.nav-folder.vue
index 4f0e6ce0e9..7c35c5d3da 100644
--- a/packages/client/src/components/drive.nav-folder.vue
+++ b/packages/client/src/components/drive.nav-folder.vue
@@ -8,114 +8,111 @@
@drop.stop="onDrop"
>
<i v-if="folder == null" class="fas fa-cloud"></i>
- <span>{{ folder == null ? $ts.drive : folder.name }}</span>
+ <span>{{ folder == null ? i18n.locale.drive : folder.name }}</span>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- folder: {
- type: Object,
- required: false,
- }
- },
+const props = defineProps<{
+ folder?: Misskey.entities.DriveFolder;
+ parentFolder: Misskey.entities.DriveFolder | null;
+}>();
- data() {
- return {
- hover: false,
- draghover: false,
- };
- },
+const emit = defineEmits<{
+ (e: 'move', v?: Misskey.entities.DriveFolder): void;
+ (e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
+ (e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
+ (e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
+}>();
- computed: {
- browser(): any {
- return this.$parent;
- }
- },
+const hover = ref(false);
+const draghover = ref(false);
- methods: {
- onClick() {
- this.browser.move(this.folder);
- },
+function onClick() {
+ emit('move', props.folder);
+}
- onMouseover() {
- this.hover = true;
- },
+function onMouseover() {
+ hover.value = true;
+}
- onMouseout() {
- this.hover = false;
- },
+function onMouseout() {
+ hover.value = false;
+}
- onDragover(e) {
- // このフォルダがルートかつカレントディレクトリならドロップ禁止
- if (this.folder == null && this.browser.folder == null) {
- e.dataTransfer.dropEffect = 'none';
- }
+function onDragover(e: DragEvent) {
+ if (!e.dataTransfer) return;
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+ // このフォルダがルートかつカレントディレクトリならドロップ禁止
+ if (props.folder == null && props.parentFolder == null) {
+ e.dataTransfer.dropEffect = 'none';
+ }
- if (isFile || isDriveFile || isDriveFolder) {
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- } else {
- e.dataTransfer.dropEffect = 'none';
- }
+ const isFile = e.dataTransfer.items[0].kind == 'file';
+ const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+ const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
- return false;
- },
+ if (isFile || isDriveFile || isDriveFolder) {
+ e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+ } else {
+ e.dataTransfer.dropEffect = 'none';
+ }
- onDragenter() {
- if (this.folder || this.browser.folder) this.draghover = true;
- },
+ return false;
+}
- onDragleave() {
- if (this.folder || this.browser.folder) this.draghover = false;
- },
+function onDragenter() {
+ if (props.folder || props.parentFolder) draghover.value = true;
+}
- onDrop(e) {
- this.draghover = false;
+function onDragleave() {
+ if (props.folder || props.parentFolder) draghover.value = false;
+}
- // ファイルだったら
- if (e.dataTransfer.files.length > 0) {
- for (const file of Array.from(e.dataTransfer.files)) {
- this.browser.upload(file, this.folder);
- }
- return;
- }
+function onDrop(e: DragEvent) {
+ draghover.value = false;
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- this.browser.removeFile(file.id);
- os.api('drive/files/update', {
- fileId: file.id,
- folderId: this.folder ? this.folder.id : null
- });
- }
- //#endregion
+ if (!e.dataTransfer) return;
- //#region ドライブのフォルダ
- const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
- if (driveFolder != null && driveFolder != '') {
- const folder = JSON.parse(driveFolder);
- // 移動先が自分自身ならreject
- if (this.folder && folder.id == this.folder.id) return;
- this.browser.removeFolder(folder.id);
- os.api('drive/folders/update', {
- folderId: folder.id,
- parentId: this.folder ? this.folder.id : null
- });
- }
- //#endregion
+ // ファイルだったら
+ if (e.dataTransfer.files.length > 0) {
+ for (const file of Array.from(e.dataTransfer.files)) {
+ emit('upload', file, props.folder);
}
+ return;
+ }
+
+ //#region ドライブのファイル
+ const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ if (driveFile != null && driveFile != '') {
+ const file = JSON.parse(driveFile);
+ emit('removeFile', file.id);
+ os.api('drive/files/update', {
+ fileId: file.id,
+ folderId: props.folder ? props.folder.id : null
+ });
}
-});
+ //#endregion
+
+ //#region ドライブのフォルダ
+ const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+ if (driveFolder != null && driveFolder != '') {
+ const folder = JSON.parse(driveFolder);
+ // 移動先が自分自身ならreject
+ if (props.folder && folder.id == props.folder.id) return;
+ emit('removeFolder', folder.id);
+ os.api('drive/folders/update', {
+ folderId: folder.id,
+ parentId: props.folder ? props.folder.id : null
+ });
+ }
+ //#endregion
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue
index 46bcd42558..e27b0a5fbb 100644
--- a/packages/client/src/components/drive.vue
+++ b/packages/client/src/components/drive.vue
@@ -2,10 +2,24 @@
<div class="yfudmmck">
<nav>
<div class="path" @contextmenu.prevent.stop="() => {}">
- <XNavFolder :class="{ current: folder == null }"/>
+ <XNavFolder
+ :class="{ current: folder == null }"
+ :parent-folder="folder"
+ @move="move"
+ @upload="upload"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
+ />
<template v-for="f in hierarchyFolders">
<span class="separator"><i class="fas fa-angle-right"></i></span>
- <XNavFolder :folder="f"/>
+ <XNavFolder
+ :folder="f"
+ :parent-folder="folder"
+ @move="move"
+ @upload="upload"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
+ />
</template>
<span v-if="folder != null" class="separator"><i class="fas fa-angle-right"></i></span>
<span v-if="folder != null" class="folder current">{{ folder.name }}</span>
@@ -22,616 +36,601 @@
>
<div ref="contents" class="contents">
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
- <XFolder v-for="(f, i) in folders" :key="f.id" v-anim="i" class="folder" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder"/>
+ <XFolder
+ v-for="(f, i) in folders"
+ :key="f.id"
+ v-anim="i"
+ class="folder"
+ :folder="f"
+ :select-mode="select === 'folder'"
+ :is-selected="selectedFolders.some(x => x.id === f.id)"
+ @chosen="chooseFolder"
+ @move="move"
+ @upload="upload"
+ @removeFile="removeFile"
+ @removeFolder="removeFolder"
+ @dragstart="isDragSource = true"
+ @dragend="isDragSource = false"
+ />
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
- <MkButton v-if="moreFolders" ref="moreFolders">{{ $ts.loadMore }}</MkButton>
+ <MkButton v-if="moreFolders" ref="moreFolders">{{ i18n.locale.loadMore }}</MkButton>
</div>
<div v-show="files.length > 0" ref="filesContainer" class="files">
- <XFile v-for="(file, i) in files" :key="file.id" v-anim="i" class="file" :file="file" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === file.id)" @chosen="chooseFile"/>
+ <XFile
+ v-for="(file, i) in files"
+ :key="file.id"
+ v-anim="i"
+ class="file"
+ :file="file"
+ :select-mode="select === 'file'"
+ :is-selected="selectedFiles.some(x => x.id === file.id)"
+ @chosen="chooseFile"
+ @dragstart="isDragSource = true"
+ @dragend="isDragSource = false"
+ />
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div v-for="(n, i) in 16" :key="i" class="padding"></div>
- <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ $ts.loadMore }}</MkButton>
+ <MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.locale.loadMore }}</MkButton>
</div>
<div v-if="files.length == 0 && folders.length == 0 && !fetching" class="empty">
- <p v-if="draghover">{{ $t('empty-draghover') }}</p>
- <p v-if="!draghover && folder == null"><strong>{{ $ts.emptyDrive }}</strong><br/>{{ $t('empty-drive-description') }}</p>
- <p v-if="!draghover && folder != null">{{ $ts.emptyFolder }}</p>
+ <p v-if="draghover">{{ i18n.t('empty-draghover') }}</p>
+ <p v-if="!draghover && folder == null"><strong>{{ i18n.locale.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</p>
+ <p v-if="!draghover && folder != null">{{ i18n.locale.emptyFolder }}</p>
</div>
</div>
<MkLoading v-if="fetching"/>
</div>
<div v-if="draghover" class="dropzone"></div>
- <input ref="fileInput" type="file" accept="*/*" multiple="multiple" tabindex="-1" @change="onChangeFileInput"/>
+ <input ref="fileInput" type="file" accept="*/*" multiple tabindex="-1" @change="onChangeFileInput"/>
</div>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
import XNavFolder from './drive.nav-folder.vue';
import XFolder from './drive.folder.vue';
import XFile from './drive.file.vue';
import MkButton from './ui/button.vue';
import * as os from '@/os';
+import { stream } from '@/stream';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XNavFolder,
- XFolder,
- XFile,
- MkButton,
- },
-
- props: {
- initialFolder: {
- type: Object,
- required: false
- },
- type: {
- type: String,
- required: false,
- default: undefined
- },
- multiple: {
- type: Boolean,
- required: false,
- default: false
- },
- select: {
- type: String,
- required: false,
- default: null
- }
- },
-
- emits: ['selected', 'change-selection', 'move-root', 'cd', 'open-folder'],
-
- data() {
- return {
- /**
- * 現在の階層(フォルダ)
- * * null でルートを表す
- */
- folder: null,
+const props = withDefaults(defineProps<{
+ initialFolder?: Misskey.entities.DriveFolder;
+ type?: string;
+ multiple?: boolean;
+ select?: 'file' | 'folder' | null;
+}>(), {
+ multiple: false,
+ select: null,
+});
- files: [],
- folders: [],
- moreFiles: false,
- moreFolders: false,
- hierarchyFolders: [],
- selectedFiles: [],
- selectedFolders: [],
- uploadings: os.uploads,
- connection: null,
+const emit = defineEmits<{
+ (e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
+ (e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
+ (e: 'move-root'): void;
+ (e: 'cd', v: Misskey.entities.DriveFolder | null): void;
+ (e: 'open-folder', v: Misskey.entities.DriveFolder): void;
+}>();
- /**
- * ドロップされようとしているか
- */
- draghover: false,
+const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
+const fileInput = ref<HTMLInputElement>();
- /**
- * 自信の所有するアイテムがドラッグをスタートさせたか
- * (自分自身の階層にドロップできないようにするためのフラグ)
- */
- isDragSource: false,
+const folder = ref<Misskey.entities.DriveFolder | null>(null);
+const files = ref<Misskey.entities.DriveFile[]>([]);
+const folders = ref<Misskey.entities.DriveFolder[]>([]);
+const moreFiles = ref(false);
+const moreFolders = ref(false);
+const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]);
+const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
+const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
+const uploadings = os.uploads;
+const connection = stream.useChannel('drive');
- fetching: true,
+// ドロップされようとしているか
+const draghover = ref(false);
- ilFilesObserver: new IntersectionObserver(
- (entries) => entries.some((entry) => entry.isIntersecting)
- && !this.fetching && this.moreFiles &&
- this.fetchMoreFiles()
- ),
- moreFilesElement: null as Element,
+// 自身の所有するアイテムがドラッグをスタートさせたか
+// (自分自身の階層にドロップできないようにするためのフラグ)
+const isDragSource = ref(false);
- };
- },
+const fetching = ref(true);
- watch: {
- folder() {
- this.$emit('cd', this.folder);
- }
- },
+const ilFilesObserver = new IntersectionObserver(
+ (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles()
+)
- mounted() {
- if (this.$store.state.enableInfiniteScroll && this.$refs.loadMoreFiles) {
- this.$nextTick(() => {
- this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
- });
- }
+watch(folder, () => emit('cd', folder.value));
- this.connection = markRaw(os.stream.useChannel('drive'));
-
- this.connection.on('fileCreated', this.onStreamDriveFileCreated);
- this.connection.on('fileUpdated', this.onStreamDriveFileUpdated);
- this.connection.on('fileDeleted', this.onStreamDriveFileDeleted);
- this.connection.on('folderCreated', this.onStreamDriveFolderCreated);
- this.connection.on('folderUpdated', this.onStreamDriveFolderUpdated);
- this.connection.on('folderDeleted', this.onStreamDriveFolderDeleted);
+function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
+ addFile(file, true);
+}
- if (this.initialFolder) {
- this.move(this.initialFolder);
- } else {
- this.fetch();
- }
- },
+function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
+ const current = folder.value ? folder.value.id : null;
+ if (current != file.folderId) {
+ removeFile(file);
+ } else {
+ addFile(file, true);
+ }
+}
- activated() {
- if (this.$store.state.enableInfiniteScroll) {
- this.$nextTick(() => {
- this.ilFilesObserver.observe((this.$refs.loadMoreFiles as Vue).$el)
- });
- }
- },
+function onStreamDriveFileDeleted(fileId: string) {
+ removeFile(fileId);
+}
- beforeUnmount() {
- this.connection.dispose();
- this.ilFilesObserver.disconnect();
- },
+function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder) {
+ addFolder(createdFolder, true);
+}
- methods: {
- onStreamDriveFileCreated(file) {
- this.addFile(file, true);
- },
+function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
+ const current = folder.value ? folder.value.id : null;
+ if (current != updatedFolder.parentId) {
+ removeFolder(updatedFolder);
+ } else {
+ addFolder(updatedFolder, true);
+ }
+}
- onStreamDriveFileUpdated(file) {
- const current = this.folder ? this.folder.id : null;
- if (current != file.folderId) {
- this.removeFile(file);
- } else {
- this.addFile(file, true);
- }
- },
+function onStreamDriveFolderDeleted(folderId: string) {
+ removeFolder(folderId);
+}
- onStreamDriveFileDeleted(fileId) {
- this.removeFile(fileId);
- },
+function onDragover(e: DragEvent): any {
+ if (!e.dataTransfer) return;
- onStreamDriveFolderCreated(folder) {
- this.addFolder(folder, true);
- },
+ // ドラッグ元が自分自身の所有するアイテムだったら
+ if (isDragSource.value) {
+ // 自分自身にはドロップさせない
+ e.dataTransfer.dropEffect = 'none';
+ return;
+ }
- onStreamDriveFolderUpdated(folder) {
- const current = this.folder ? this.folder.id : null;
- if (current != folder.parentId) {
- this.removeFolder(folder);
- } else {
- this.addFolder(folder, true);
- }
- },
+ const isFile = e.dataTransfer.items[0].kind == 'file';
+ const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+ const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+ if (isFile || isDriveFile || isDriveFolder) {
+ e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+ } else {
+ e.dataTransfer.dropEffect = 'none';
+ }
- onStreamDriveFolderDeleted(folderId) {
- this.removeFolder(folderId);
- },
+ return false;
+}
- onDragover(e): any {
- // ドラッグ元が自分自身の所有するアイテムだったら
- if (this.isDragSource) {
- // 自分自身にはドロップさせない
- e.dataTransfer.dropEffect = 'none';
- return;
- }
+function onDragenter() {
+ if (!isDragSource.value) draghover.value = true;
+}
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
+function onDragleave() {
+ draghover.value = false;
+}
- if (isFile || isDriveFile || isDriveFolder) {
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- } else {
- e.dataTransfer.dropEffect = 'none';
- }
+function onDrop(e: DragEvent): any {
+ draghover.value = false;
- return false;
- },
+ if (!e.dataTransfer) return;
- onDragenter(e) {
- if (!this.isDragSource) this.draghover = true;
- },
+ // ドロップされてきたものがファイルだったら
+ if (e.dataTransfer.files.length > 0) {
+ for (const file of Array.from(e.dataTransfer.files)) {
+ upload(file, folder.value);
+ }
+ return;
+ }
- onDragleave(e) {
- this.draghover = false;
- },
+ //#region ドライブのファイル
+ const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ if (driveFile != null && driveFile != '') {
+ const file = JSON.parse(driveFile);
+ if (files.value.some(f => f.id == file.id)) return;
+ removeFile(file.id);
+ os.api('drive/files/update', {
+ fileId: file.id,
+ folderId: folder.value ? folder.value.id : null
+ });
+ }
+ //#endregion
- onDrop(e): any {
- this.draghover = false;
+ //#region ドライブのフォルダ
+ const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
+ if (driveFolder != null && driveFolder != '') {
+ const droppedFolder = JSON.parse(driveFolder);
- // ドロップされてきたものがファイルだったら
- if (e.dataTransfer.files.length > 0) {
- for (const file of Array.from(e.dataTransfer.files)) {
- this.upload(file, this.folder);
- }
- return;
+ // 移動先が自分自身ならreject
+ if (folder.value && droppedFolder.id == folder.value.id) return false;
+ if (folders.value.some(f => f.id == droppedFolder.id)) return false;
+ removeFolder(droppedFolder.id);
+ os.api('drive/folders/update', {
+ folderId: droppedFolder.id,
+ parentId: folder.value ? folder.value.id : null
+ }).then(() => {
+ // noop
+ }).catch(err => {
+ switch (err) {
+ case 'detected-circular-definition':
+ os.alert({
+ title: i18n.locale.unableToProcess,
+ text: i18n.locale.circularReferenceFolder
+ });
+ break;
+ default:
+ os.alert({
+ type: 'error',
+ text: i18n.locale.somethingHappened
+ });
}
+ });
+ }
+ //#endregion
+}
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- if (this.files.some(f => f.id == file.id)) return;
- this.removeFile(file.id);
- os.api('drive/files/update', {
- fileId: file.id,
- folderId: this.folder ? this.folder.id : null
- });
- }
- //#endregion
+function selectLocalFile() {
+ fileInput.value?.click();
+}
- //#region ドライブのフォルダ
- const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
- if (driveFolder != null && driveFolder != '') {
- const folder = JSON.parse(driveFolder);
+function urlUpload() {
+ os.inputText({
+ title: i18n.locale.uploadFromUrl,
+ type: 'url',
+ placeholder: i18n.locale.uploadFromUrlDescription
+ }).then(({ canceled, result: url }) => {
+ if (canceled || !url) return;
+ os.api('drive/files/upload-from-url', {
+ url: url,
+ folderId: folder.value ? folder.value.id : undefined
+ });
- // 移動先が自分自身ならreject
- if (this.folder && folder.id == this.folder.id) return false;
- if (this.folders.some(f => f.id == folder.id)) return false;
- this.removeFolder(folder.id);
- os.api('drive/folders/update', {
- folderId: folder.id,
- parentId: this.folder ? this.folder.id : null
- }).then(() => {
- // noop
- }).catch(err => {
- switch (err) {
- case 'detected-circular-definition':
- os.alert({
- title: this.$ts.unableToProcess,
- text: this.$ts.circularReferenceFolder
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: this.$ts.somethingHappened
- });
- }
- });
- }
- //#endregion
- },
+ os.alert({
+ title: i18n.locale.uploadFromUrlRequested,
+ text: i18n.locale.uploadFromUrlMayTakeTime
+ });
+ });
+}
- selectLocalFile() {
- (this.$refs.fileInput as any).click();
- },
+function createFolder() {
+ os.inputText({
+ title: i18n.locale.createFolder,
+ placeholder: i18n.locale.folderName
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ os.api('drive/folders/create', {
+ name: name,
+ parentId: folder.value ? folder.value.id : undefined
+ }).then(createdFolder => {
+ addFolder(createdFolder, true);
+ });
+ });
+}
- urlUpload() {
- os.inputText({
- title: this.$ts.uploadFromUrl,
- type: 'url',
- placeholder: this.$ts.uploadFromUrlDescription
- }).then(({ canceled, result: url }) => {
- if (canceled) return;
- os.api('drive/files/upload-from-url', {
- url: url,
- folderId: this.folder ? this.folder.id : undefined
- });
+function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
+ os.inputText({
+ title: i18n.locale.renameFolder,
+ placeholder: i18n.locale.inputNewFolderName,
+ default: folderToRename.name
+ }).then(({ canceled, result: name }) => {
+ if (canceled) return;
+ os.api('drive/folders/update', {
+ folderId: folderToRename.id,
+ name: name
+ }).then(updatedFolder => {
+ // FIXME: 画面を更新するために自分自身に移動
+ move(updatedFolder);
+ });
+ });
+}
+function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
+ os.api('drive/folders/delete', {
+ folderId: folderToDelete.id
+ }).then(() => {
+ // 削除時に親フォルダに移動
+ move(folderToDelete.parentId);
+ }).catch(err => {
+ switch(err.id) {
+ case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
os.alert({
- title: this.$ts.uploadFromUrlRequested,
- text: this.$ts.uploadFromUrlMayTakeTime
+ type: 'error',
+ title: i18n.locale.unableToDelete,
+ text: i18n.locale.hasChildFilesOrFolders
});
- });
- },
-
- createFolder() {
- os.inputText({
- title: this.$ts.createFolder,
- placeholder: this.$ts.folderName
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/folders/create', {
- name: name,
- parentId: this.folder ? this.folder.id : undefined
- }).then(folder => {
- this.addFolder(folder, true);
+ break;
+ default:
+ os.alert({
+ type: 'error',
+ text: i18n.locale.unableToDelete
});
- });
- },
+ }
+ });
+}
- renameFolder(folder) {
- os.inputText({
- title: this.$ts.renameFolder,
- placeholder: this.$ts.inputNewFolderName,
- default: folder.name
- }).then(({ canceled, result: name }) => {
- if (canceled) return;
- os.api('drive/folders/update', {
- folderId: folder.id,
- name: name
- }).then(folder => {
- // FIXME: 画面を更新するために自分自身に移動
- this.move(folder);
- });
- });
- },
+function onChangeFileInput() {
+ if (!fileInput.value?.files) return;
+ for (const file of Array.from(fileInput.value.files)) {
+ upload(file, folder.value);
+ }
+}
- deleteFolder(folder) {
- os.api('drive/folders/delete', {
- folderId: folder.id
- }).then(() => {
- // 削除時に親フォルダに移動
- this.move(folder.parentId);
- }).catch(err => {
- switch(err.id) {
- case 'b0fc8a17-963c-405d-bfbc-859a487295e1':
- os.alert({
- type: 'error',
- title: this.$ts.unableToDelete,
- text: this.$ts.hasChildFilesOrFolders
- });
- break;
- default:
- os.alert({
- type: 'error',
- text: this.$ts.unableToDelete
- });
- }
- });
- },
+function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
+ os.upload(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null).then(res => {
+ addFile(res, true);
+ });
+}
- onChangeFileInput() {
- for (const file of Array.from((this.$refs.fileInput as any).files)) {
- this.upload(file, this.folder);
- }
- },
+function chooseFile(file: Misskey.entities.DriveFile) {
+ const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id);
+ if (props.multiple) {
+ if (isAlreadySelected) {
+ selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id);
+ } else {
+ selectedFiles.value.push(file);
+ }
+ emit('change-selection', selectedFiles.value);
+ } else {
+ if (isAlreadySelected) {
+ emit('selected', file);
+ } else {
+ selectedFiles.value = [file];
+ emit('change-selection', [file]);
+ }
+ }
+}
- upload(file, folder) {
- if (folder && typeof folder == 'object') folder = folder.id;
- os.upload(file, folder).then(res => {
- this.addFile(res, true);
- });
- },
+function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
+ const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id);
+ if (props.multiple) {
+ if (isAlreadySelected) {
+ selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id);
+ } else {
+ selectedFolders.value.push(folderToChoose);
+ }
+ emit('change-selection', selectedFolders.value);
+ } else {
+ if (isAlreadySelected) {
+ emit('selected', folderToChoose);
+ } else {
+ selectedFolders.value = [folderToChoose];
+ emit('change-selection', [folderToChoose]);
+ }
+ }
+}
- chooseFile(file) {
- const isAlreadySelected = this.selectedFiles.some(f => f.id == file.id);
- if (this.multiple) {
- if (isAlreadySelected) {
- this.selectedFiles = this.selectedFiles.filter(f => f.id != file.id);
- } else {
- this.selectedFiles.push(file);
- }
- this.$emit('change-selection', this.selectedFiles);
- } else {
- if (isAlreadySelected) {
- this.$emit('selected', file);
- } else {
- this.selectedFiles = [file];
- this.$emit('change-selection', [file]);
- }
- }
- },
+function move(target?: Misskey.entities.DriveFolder) {
+ if (!target) {
+ goRoot();
+ return;
+ } else if (typeof target == 'object') {
+ target = target.id;
+ }
- chooseFolder(folder) {
- const isAlreadySelected = this.selectedFolders.some(f => f.id == folder.id);
- if (this.multiple) {
- if (isAlreadySelected) {
- this.selectedFolders = this.selectedFolders.filter(f => f.id != folder.id);
- } else {
- this.selectedFolders.push(folder);
- }
- this.$emit('change-selection', this.selectedFolders);
- } else {
- if (isAlreadySelected) {
- this.$emit('selected', folder);
- } else {
- this.selectedFolders = [folder];
- this.$emit('change-selection', [folder]);
- }
- }
- },
+ fetching.value = true;
- move(target) {
- if (target == null) {
- this.goRoot();
- return;
- } else if (typeof target == 'object') {
- target = target.id;
- }
+ os.api('drive/folders/show', {
+ folderId: target
+ }).then(folderToMove => {
+ folder.value = folderToMove;
+ hierarchyFolders.value = [];
- this.fetching = true;
+ const dive = folderToDive => {
+ hierarchyFolders.value.unshift(folderToDive);
+ if (folderToDive.parent) dive(folderToDive.parent);
+ };
+
+ if (folderToMove.parent) dive(folderToMove.parent);
- os.api('drive/folders/show', {
- folderId: target
- }).then(folder => {
- this.folder = folder;
- this.hierarchyFolders = [];
+ emit('open-folder', folderToMove);
+ fetch();
+ });
+}
- const dive = folder => {
- this.hierarchyFolders.unshift(folder);
- if (folder.parent) dive(folder.parent);
- };
+function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
+ const current = folder.value ? folder.value.id : null;
+ if (current != folderToAdd.parentId) return;
- if (folder.parent) dive(folder.parent);
+ if (folders.value.some(f => f.id == folderToAdd.id)) {
+ const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
+ folders.value[exist] = folderToAdd;
+ return;
+ }
- this.$emit('open-folder', folder);
- this.fetch();
- });
- },
+ if (unshift) {
+ folders.value.unshift(folderToAdd);
+ } else {
+ folders.value.push(folderToAdd);
+ }
+}
- addFolder(folder, unshift = false) {
- const current = this.folder ? this.folder.id : null;
- if (current != folder.parentId) return;
+function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
+ const current = folder.value ? folder.value.id : null;
+ if (current != fileToAdd.folderId) return;
- if (this.folders.some(f => f.id == folder.id)) {
- const exist = this.folders.map(f => f.id).indexOf(folder.id);
- this.folders[exist] = folder;
- return;
- }
+ if (files.value.some(f => f.id == fileToAdd.id)) {
+ const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
+ files.value[exist] = fileToAdd;
+ return;
+ }
- if (unshift) {
- this.folders.unshift(folder);
- } else {
- this.folders.push(folder);
- }
- },
+ if (unshift) {
+ files.value.unshift(fileToAdd);
+ } else {
+ files.value.push(fileToAdd);
+ }
+}
- addFile(file, unshift = false) {
- const current = this.folder ? this.folder.id : null;
- if (current != file.folderId) return;
+function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
+ const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
+ folders.value = folders.value.filter(f => f.id != folderIdToRemove);
+}
- if (this.files.some(f => f.id == file.id)) {
- const exist = this.files.map(f => f.id).indexOf(file.id);
- this.files[exist] = file;
- return;
- }
+function removeFile(file: Misskey.entities.DriveFile | string) {
+ const fileId = typeof file === 'object' ? file.id : file;
+ files.value = files.value.filter(f => f.id != fileId);
+}
- if (unshift) {
- this.files.unshift(file);
- } else {
- this.files.push(file);
- }
- },
+function appendFile(file: Misskey.entities.DriveFile) {
+ addFile(file);
+}
- removeFolder(folder) {
- if (typeof folder == 'object') folder = folder.id;
- this.folders = this.folders.filter(f => f.id != folder);
- },
+function appendFolder(folderToAppend: Misskey.entities.DriveFolder) {
+ addFolder(folderToAppend);
+}
+/*
+function prependFile(file: Misskey.entities.DriveFile) {
+ addFile(file, true);
+}
- removeFile(file) {
- if (typeof file == 'object') file = file.id;
- this.files = this.files.filter(f => f.id != file);
- },
+function prependFolder(folderToPrepend: Misskey.entities.DriveFolder) {
+ addFolder(folderToPrepend, true);
+}
+*/
+function goRoot() {
+ // 既にrootにいるなら何もしない
+ if (folder.value == null) return;
- appendFile(file) {
- this.addFile(file);
- },
+ folder.value = null;
+ hierarchyFolders.value = [];
+ emit('move-root');
+ fetch();
+}
- appendFolder(folder) {
- this.addFolder(folder);
- },
+async function fetch() {
+ folders.value = [];
+ files.value = [];
+ moreFolders.value = false;
+ moreFiles.value = false;
+ fetching.value = true;
- prependFile(file) {
- this.addFile(file, true);
- },
+ const foldersMax = 30;
+ const filesMax = 30;
- prependFolder(folder) {
- this.addFolder(folder, true);
- },
+ const foldersPromise = os.api('drive/folders', {
+ folderId: folder.value ? folder.value.id : null,
+ limit: foldersMax + 1
+ }).then(fetchedFolders => {
+ if (fetchedFolders.length == foldersMax + 1) {
+ moreFolders.value = true;
+ fetchedFolders.pop();
+ }
+ return fetchedFolders;
+ });
- goRoot() {
- // 既にrootにいるなら何もしない
- if (this.folder == null) return;
+ const filesPromise = os.api('drive/files', {
+ folderId: folder.value ? folder.value.id : null,
+ type: props.type,
+ limit: filesMax + 1
+ }).then(fetchedFiles => {
+ if (fetchedFiles.length == filesMax + 1) {
+ moreFiles.value = true;
+ fetchedFiles.pop();
+ }
+ return fetchedFiles;
+ });
- this.folder = null;
- this.hierarchyFolders = [];
- this.$emit('move-root');
- this.fetch();
- },
+ const [fetchedFolders, fetchedFiles] = await Promise.all([foldersPromise, filesPromise]);
- fetch() {
- this.folders = [];
- this.files = [];
- this.moreFolders = false;
- this.moreFiles = false;
- this.fetching = true;
+ for (const x of fetchedFolders) appendFolder(x);
+ for (const x of fetchedFiles) appendFile(x);
- let fetchedFolders = null;
- let fetchedFiles = null;
+ fetching.value = false;
+}
- const foldersMax = 30;
- const filesMax = 30;
+function fetchMoreFiles() {
+ fetching.value = true;
- // フォルダ一覧取得
- os.api('drive/folders', {
- folderId: this.folder ? this.folder.id : null,
- limit: foldersMax + 1
- }).then(folders => {
- if (folders.length == foldersMax + 1) {
- this.moreFolders = true;
- folders.pop();
- }
- fetchedFolders = folders;
- complete();
- });
+ const max = 30;
- // ファイル一覧取得
- os.api('drive/files', {
- folderId: this.folder ? this.folder.id : null,
- type: this.type,
- limit: filesMax + 1
- }).then(files => {
- if (files.length == filesMax + 1) {
- this.moreFiles = true;
- files.pop();
- }
- fetchedFiles = files;
- complete();
- });
+ // ファイル一覧取得
+ os.api('drive/files', {
+ folderId: folder.value ? folder.value.id : null,
+ type: props.type,
+ untilId: files.value[files.value.length - 1].id,
+ limit: max + 1
+ }).then(files => {
+ if (files.length == max + 1) {
+ moreFiles.value = true;
+ files.pop();
+ } else {
+ moreFiles.value = false;
+ }
+ for (const x of files) appendFile(x);
+ fetching.value = false;
+ });
+}
- let flag = false;
- const complete = () => {
- if (flag) {
- for (const x of fetchedFolders) this.appendFolder(x);
- for (const x of fetchedFiles) this.appendFile(x);
- this.fetching = false;
- } else {
- flag = true;
- }
- };
- },
+function getMenu() {
+ return [{
+ text: i18n.locale.addFile,
+ type: 'label'
+ }, {
+ text: i18n.locale.upload,
+ icon: 'fas fa-upload',
+ action: () => { selectLocalFile(); }
+ }, {
+ text: i18n.locale.fromUrl,
+ icon: 'fas fa-link',
+ action: () => { urlUpload(); }
+ }, null, {
+ text: folder.value ? folder.value.name : i18n.locale.drive,
+ type: 'label'
+ }, folder.value ? {
+ text: i18n.locale.renameFolder,
+ icon: 'fas fa-i-cursor',
+ action: () => { renameFolder(folder.value); }
+ } : undefined, folder.value ? {
+ text: i18n.locale.deleteFolder,
+ icon: 'fas fa-trash-alt',
+ action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }
+ } : undefined, {
+ text: i18n.locale.createFolder,
+ icon: 'fas fa-folder-plus',
+ action: () => { createFolder(); }
+ }];
+}
- fetchMoreFiles() {
- this.fetching = true;
+function showMenu(ev: MouseEvent) {
+ os.popupMenu(getMenu(), (ev.currentTarget || ev.target || undefined) as HTMLElement | undefined);
+}
- const max = 30;
+function onContextmenu(ev: MouseEvent) {
+ os.contextMenu(getMenu(), ev);
+}
- // ファイル一覧取得
- os.api('drive/files', {
- folderId: this.folder ? this.folder.id : null,
- type: this.type,
- untilId: this.files[this.files.length - 1].id,
- limit: max + 1
- }).then(files => {
- if (files.length == max + 1) {
- this.moreFiles = true;
- files.pop();
- } else {
- this.moreFiles = false;
- }
- for (const x of files) this.appendFile(x);
- this.fetching = false;
- });
- },
+onMounted(() => {
+ if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
+ nextTick(() => {
+ ilFilesObserver.observe(loadMoreFiles.value?.$el)
+ });
+ }
- getMenu() {
- return [{
- text: this.$ts.addFile,
- type: 'label'
- }, {
- text: this.$ts.upload,
- icon: 'fas fa-upload',
- action: () => { this.selectLocalFile(); }
- }, {
- text: this.$ts.fromUrl,
- icon: 'fas fa-link',
- action: () => { this.urlUpload(); }
- }, null, {
- text: this.folder ? this.folder.name : this.$ts.drive,
- type: 'label'
- }, this.folder ? {
- text: this.$ts.renameFolder,
- icon: 'fas fa-i-cursor',
- action: () => { this.renameFolder(this.folder); }
- } : undefined, this.folder ? {
- text: this.$ts.deleteFolder,
- icon: 'fas fa-trash-alt',
- action: () => { this.deleteFolder(this.folder); }
- } : undefined, {
- text: this.$ts.createFolder,
- icon: 'fas fa-folder-plus',
- action: () => { this.createFolder(); }
- }];
- },
+ connection.on('fileCreated', onStreamDriveFileCreated);
+ connection.on('fileUpdated', onStreamDriveFileUpdated);
+ connection.on('fileDeleted', onStreamDriveFileDeleted);
+ connection.on('folderCreated', onStreamDriveFolderCreated);
+ connection.on('folderUpdated', onStreamDriveFolderUpdated);
+ connection.on('folderDeleted', onStreamDriveFolderDeleted);
- showMenu(ev) {
- os.popupMenu(this.getMenu(), ev.currentTarget || ev.target);
- },
+ if (props.initialFolder) {
+ move(props.initialFolder);
+ } else {
+ fetch();
+ }
+});
- onContextmenu(ev) {
- os.contextMenu(this.getMenu(), ev);
- },
+onActivated(() => {
+ if (defaultStore.state.enableInfiniteScroll) {
+ nextTick(() => {
+ ilFilesObserver.observe(loadMoreFiles.value?.$el)
+ });
}
});
+
+onBeforeUnmount(() => {
+ connection.dispose();
+ ilFilesObserver.disconnect();
+});
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker-dialog.vue b/packages/client/src/components/emoji-picker-dialog.vue
index 51c634dd8e..f06a24636c 100644
--- a/packages/client/src/components/emoji-picker-dialog.vue
+++ b/packages/client/src/components/emoji-picker-dialog.vue
@@ -1,58 +1,65 @@
<template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :z-priority="'middle'" :prefer-type="asReactionPicker && $store.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'" :transparent-bg="true" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
- <MkEmojiPicker ref="picker" class="ryghynhb _popup _shadow" :class="{ drawer: type === 'drawer' }" :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" :as-drawer="type === 'drawer'" :max-height="maxHeight" @chosen="chosen"/>
+<MkModal
+ ref="modal"
+ v-slot="{ type, maxHeight }"
+ :z-priority="'middle'"
+ :prefer-type="asReactionPicker && defaultStore.state.reactionPickerUseDrawerForMobile === false ? 'popup' : 'auto'"
+ :transparent-bg="true"
+ :manual-showing="manualShowing"
+ :src="src"
+ @click="modal?.close()"
+ @opening="opening"
+ @close="emit('close')"
+ @closed="emit('closed')"
+>
+ <MkEmojiPicker
+ ref="picker"
+ class="ryghynhb _popup _shadow"
+ :class="{ drawer: type === 'drawer' }"
+ :show-pinned="showPinned"
+ :as-reaction-picker="asReactionPicker"
+ :as-drawer="type === 'drawer'"
+ :max-height="maxHeight"
+ @chosen="chosen"
+ />
</MkModal>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
import MkModal from '@/components/ui/modal.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue';
+import { defaultStore } from '@/store';
-export default defineComponent({
- components: {
- MkModal,
- MkEmojiPicker,
- },
-
- props: {
- manualShowing: {
- type: Boolean,
- required: false,
- default: null,
- },
- src: {
- required: false
- },
- showPinned: {
- required: false,
- default: true
- },
- asReactionPicker: {
- required: false
- },
- },
-
- emits: ['done', 'close', 'closed'],
+withDefaults(defineProps<{
+ manualShowing?: boolean;
+ src?: HTMLElement;
+ showPinned?: boolean;
+ asReactionPicker?: boolean;
+}>(), {
+ manualShowing: false,
+ showPinned: true,
+ asReactionPicker: false,
+});
- data() {
- return {
+const emit = defineEmits<{
+ (e: 'done', v: any): void;
+ (e: 'close'): void;
+ (e: 'closed'): void;
+}>();
- };
- },
+const modal = ref<InstanceType<typeof MkModal>>();
+const picker = ref<InstanceType<typeof MkEmojiPicker>>();
- methods: {
- chosen(emoji: any) {
- this.$emit('done', emoji);
- this.$refs.modal.close();
- },
+function chosen(emoji: any) {
+ emit('done', emoji);
+ modal.value?.close();
+}
- opening() {
- this.$refs.picker.reset();
- this.$refs.picker.focus();
- }
- }
-});
+function opening() {
+ picker.value?.reset();
+ picker.value?.focus();
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker-window.vue b/packages/client/src/components/emoji-picker-window.vue
index 0ffa0c1187..4d27fb48ba 100644
--- a/packages/client/src/components/emoji-picker-window.vue
+++ b/packages/client/src/components/emoji-picker-window.vue
@@ -5,50 +5,33 @@
:can-resize="false"
:mini="true"
:front="true"
- @closed="$emit('closed')"
+ @closed="emit('closed')"
>
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen"/>
</MkWindow>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MkWindow from '@/components/ui/window.vue';
import MkEmojiPicker from '@/components/emoji-picker.vue';
-export default defineComponent({
- components: {
- MkWindow,
- MkEmojiPicker,
- },
-
- props: {
- src: {
- required: false
- },
- showPinned: {
- required: false,
- default: true
- },
- asReactionPicker: {
- required: false
- },
- },
-
- emits: ['chosen', 'closed'],
-
- data() {
- return {
+withDefaults(defineProps<{
+ src?: HTMLElement;
+ showPinned?: boolean;
+ asReactionPicker?: boolean;
+}>(), {
+ showPinned: true,
+});
- };
- },
+const emit = defineEmits<{
+ (e: 'chosen', v: any): void;
+ (e: 'closed'): void;
+}>();
- methods: {
- chosen(emoji: any) {
- this.$emit('chosen', emoji);
- },
- }
-});
+function chosen(emoji: any) {
+ emit('chosen', emoji);
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker.section.vue b/packages/client/src/components/emoji-picker.section.vue
index 08c4f6813d..1026e894d1 100644
--- a/packages/client/src/components/emoji-picker.section.vue
+++ b/packages/client/src/components/emoji-picker.section.vue
@@ -7,7 +7,7 @@
<button v-for="emoji in emojis"
:key="emoji"
class="_button"
- @click="chosen(emoji, $event)"
+ @click="emit('chosen', emoji, $event)"
>
<MkEmoji :emoji="emoji" :normal="true"/>
</button>
@@ -15,35 +15,19 @@
</section>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { getStaticImageUrl } from '@/scripts/get-static-image-url';
+<script lang="ts" setup>
+import { ref } from 'vue';
-export default defineComponent({
- props: {
- emojis: {
- required: true,
- },
- initialShown: {
- required: false
- }
- },
+const props = defineProps<{
+ emojis: string[];
+ initialShown?: boolean;
+}>();
- emits: ['chosen'],
+const emit = defineEmits<{
+ (e: 'chosen', v: string, ev: MouseEvent): void;
+}>();
- data() {
- return {
- getStaticImageUrl,
- shown: this.initialShown,
- };
- },
-
- methods: {
- chosen(emoji: any, ev) {
- this.$parent.chosen(emoji, ev);
- },
- }
-});
+const shown = ref(!!props.initialShown);
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/emoji-picker.vue b/packages/client/src/components/emoji-picker.vue
index a8eed1ca21..96670fa58c 100644
--- a/packages/client/src/components/emoji-picker.vue
+++ b/packages/client/src/components/emoji-picker.vue
@@ -1,18 +1,18 @@
<template>
-<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : null }">
- <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="$ts.search" @paste.stop="paste" @keyup.enter="done()">
+<div class="omfetrab" :class="['w' + width, 'h' + height, { big, asDrawer }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
+ <input ref="search" v-model.trim="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.locale.search" @paste.stop="paste" @keyup.enter="done()">
<div ref="emojis" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0">
<button v-for="emoji in searchResultCustom"
- :key="emoji"
+ :key="emoji.id"
class="_button"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
- <MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>
- <img v-else :src="$store.state.disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
+ <!--<MkEmoji v-if="emoji.char != null" :emoji="emoji.char"/>-->
+ <img :src="disableShowingAnimatedImages ? getStaticImageUrl(emoji.url) : emoji.url"/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0">
@@ -43,9 +43,9 @@
</section>
<section>
- <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ $ts.recentUsed }}</header>
+ <header class="_acrylic"><i class="far fa-clock fa-fw"></i> {{ i18n.locale.recentUsed }}</header>
<div>
- <button v-for="emoji in $store.state.recentlyUsedEmojis"
+ <button v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button"
@click="chosen(emoji, $event)"
@@ -56,12 +56,12 @@
</section>
</div>
<div>
- <header class="_acrylic">{{ $ts.customEmojis }}</header>
- <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')">{{ category || $ts.other }}</XSection>
+ <header class="_acrylic">{{ i18n.locale.customEmojis }}</header>
+ <XSection v-for="category in customEmojiCategories" :key="'custom:' + category" :initial-shown="false" :emojis="customEmojis.filter(e => e.category === category).map(e => ':' + e.name + ':')" @chosen="chosen">{{ category || i18n.locale.other }}</XSection>
</div>
<div>
- <header class="_acrylic">{{ $ts.emoji }}</header>
- <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)">{{ category }}</XSection>
+ <header class="_acrylic">{{ i18n.locale.emoji }}</header>
+ <XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
</div>
</div>
<div class="tabs">
@@ -73,277 +73,272 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import { emojilist } from '@/scripts/emojilist';
+<script lang="ts" setup>
+import { ref, computed, watch, onMounted } from 'vue';
+import * as Misskey from 'misskey-js';
+import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import Ripple from '@/components/ripple.vue';
import * as os from '@/os';
import { isTouchUsing } from '@/scripts/touch';
import { isMobile } from '@/scripts/is-mobile';
-import { emojiCategories } from '@/instance';
+import { emojiCategories, instance } from '@/instance';
import XSection from './emoji-picker.section.vue';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
-export default defineComponent({
- components: {
- XSection
- },
-
- props: {
- showPinned: {
- required: false,
- default: true,
- },
- asReactionPicker: {
- required: false,
- },
- maxHeight: {
- type: Number,
- required: false,
- },
- asDrawer: {
- type: Boolean,
- required: false
- },
- },
+const props = withDefaults(defineProps<{
+ showPinned?: boolean;
+ asReactionPicker?: boolean;
+ maxHeight?: number;
+ asDrawer?: boolean;
+}>(), {
+ showPinned: true,
+});
- emits: ['chosen'],
+const emit = defineEmits<{
+ (e: 'chosen', v: string): void;
+}>();
- data() {
- return {
- emojilist: markRaw(emojilist),
- getStaticImageUrl,
- pinned: this.$store.reactiveState.reactions,
- width: this.asReactionPicker ? this.$store.state.reactionPickerWidth : 3,
- height: this.asReactionPicker ? this.$store.state.reactionPickerHeight : 2,
- big: this.asReactionPicker ? isTouchUsing : false,
- customEmojiCategories: emojiCategories,
- customEmojis: this.$instance.emojis,
- q: null,
- searchResultCustom: [],
- searchResultUnicode: [],
- tab: 'index',
- categories: ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'],
- };
- },
+const search = ref<HTMLInputElement>();
+const emojis = ref<HTMLDivElement>();
- watch: {
- q() {
- this.$refs.emojis.scrollTop = 0;
+const {
+ reactions: pinned,
+ reactionPickerWidth,
+ reactionPickerHeight,
+ disableShowingAnimatedImages,
+ recentlyUsedEmojis,
+} = defaultStore.reactiveState;
- if (this.q == null || this.q === '') {
- this.searchResultCustom = [];
- this.searchResultUnicode = [];
- return;
- }
+const width = computed(() => props.asReactionPicker ? reactionPickerWidth.value : 3);
+const height = computed(() => props.asReactionPicker ? reactionPickerHeight.value : 2);
+const big = props.asReactionPicker ? isTouchUsing : false;
+const customEmojiCategories = emojiCategories;
+const customEmojis = instance.emojis;
+const q = ref<string | null>(null);
+const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
+const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
+const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
- const q = this.q.replace(/:/g, '');
+watch(q, () => {
+ if (emojis.value) emojis.value.scrollTop = 0;
- const searchCustom = () => {
- const max = 8;
- const emojis = this.customEmojis;
- const matches = new Set();
+ if (q.value == null || q.value === '') {
+ searchResultCustom.value = [];
+ searchResultUnicode.value = [];
+ return;
+ }
- const exactMatch = emojis.find(e => e.name === q);
- if (exactMatch) matches.add(exactMatch);
+ const newQ = q.value.replace(/:/g, '');
- if (q.includes(' ')) { // AND検索
- const keywords = q.split(' ');
+ const searchCustom = () => {
+ const max = 8;
+ const emojis = customEmojis;
+ const matches = new Set<Misskey.entities.CustomEmoji>();
- // 名前にキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ const exactMatch = emojis.find(e => e.name === newQ);
+ if (exactMatch) matches.add(exactMatch);
- // 名前またはエイリアスにキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- } else {
- for (const emoji of emojis) {
- if (emoji.name.startsWith(q)) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ if (newQ.includes(' ')) { // AND検索
+ const keywords = newQ.split(' ');
- for (const emoji of emojis) {
- if (emoji.aliases.some(alias => alias.startsWith(q))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ // 名前にキーワードが含まれている
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => emoji.name.includes(keyword))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.name.includes(q)) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ // 名前またはエイリアスにキーワードが含まれている
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ } else {
+ for (const emoji of emojis) {
+ if (emoji.name.startsWith(newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.aliases.some(alias => alias.includes(q))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
+ for (const emoji of emojis) {
+ if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
}
+ }
+ if (matches.size >= max) return matches;
- return matches;
- };
+ for (const emoji of emojis) {
+ if (emoji.name.includes(newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- const searchUnicode = () => {
- const max = 8;
- const emojis = this.emojilist;
- const matches = new Set();
+ for (const emoji of emojis) {
+ if (emoji.aliases.some(alias => alias.includes(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ }
- const exactMatch = emojis.find(e => e.name === q);
- if (exactMatch) matches.add(exactMatch);
+ return matches;
+ };
- if (q.includes(' ')) { // AND検索
- const keywords = q.split(' ');
+ const searchUnicode = () => {
+ const max = 8;
+ const emojis = emojilist;
+ const matches = new Set<UnicodeEmojiDef>();
- // 名前にキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ const exactMatch = emojis.find(e => e.name === newQ);
+ if (exactMatch) matches.add(exactMatch);
- // 名前またはエイリアスにキーワードが含まれている
- for (const emoji of emojis) {
- if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- } else {
- for (const emoji of emojis) {
- if (emoji.name.startsWith(q)) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ if (newQ.includes(' ')) { // AND検索
+ const keywords = newQ.split(' ');
- for (const emoji of emojis) {
- if (emoji.keywords.some(keyword => keyword.startsWith(q))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ // 名前にキーワードが含まれている
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => emoji.name.includes(keyword))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.name.includes(q)) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
- if (matches.size >= max) return matches;
+ // 名前またはエイリアスにキーワードが含まれている
+ for (const emoji of emojis) {
+ if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.keywords.some(alias => alias.includes(keyword)))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ } else {
+ for (const emoji of emojis) {
+ if (emoji.name.startsWith(newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- for (const emoji of emojis) {
- if (emoji.keywords.some(keyword => keyword.includes(q))) {
- matches.add(emoji);
- if (matches.size >= max) break;
- }
- }
+ for (const emoji of emojis) {
+ if (emoji.keywords.some(keyword => keyword.startsWith(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
}
+ }
+ if (matches.size >= max) return matches;
- return matches;
- };
+ for (const emoji of emojis) {
+ if (emoji.name.includes(newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
- this.searchResultCustom = Array.from(searchCustom());
- this.searchResultUnicode = Array.from(searchUnicode());
+ for (const emoji of emojis) {
+ if (emoji.keywords.some(keyword => keyword.includes(newQ))) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
}
- },
- mounted() {
- this.focus();
- },
+ return matches;
+ };
- methods: {
- focus() {
- if (!isMobile && !isTouchUsing) {
- this.$refs.search.focus({
- preventScroll: true
- });
- }
- },
+ searchResultCustom.value = Array.from(searchCustom());
+ searchResultUnicode.value = Array.from(searchUnicode());
+});
- reset() {
- this.$refs.emojis.scrollTop = 0;
- this.q = '';
- },
+function focus() {
+ if (!isMobile && !isTouchUsing) {
+ search.value?.focus({
+ preventScroll: true
+ });
+ }
+}
- getKey(emoji: any) {
- return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
- },
+function reset() {
+ if (emojis.value) emojis.value.scrollTop = 0;
+ q.value = '';
+}
- chosen(emoji: any, ev) {
- if (ev) {
- const el = ev.currentTarget || ev.target;
- const rect = el.getBoundingClientRect();
- const x = rect.left + (el.offsetWidth / 2);
- const y = rect.top + (el.offsetHeight / 2);
- os.popup(Ripple, { x, y }, {}, 'end');
- }
+function getKey(emoji: string | Misskey.entities.CustomEmoji | UnicodeEmojiDef): string {
+ return typeof emoji === 'string' ? emoji : (emoji.char || `:${emoji.name}:`);
+}
- const key = this.getKey(emoji);
- this.$emit('chosen', key);
+function chosen(emoji: any, ev?: MouseEvent) {
+ const el = ev && (ev.currentTarget || ev.target) as HTMLElement | null | undefined;
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+ os.popup(Ripple, { x, y }, {}, 'end');
+ }
- // 最近使った絵文字更新
- if (!this.pinned.includes(key)) {
- let recents = this.$store.state.recentlyUsedEmojis;
- recents = recents.filter((e: any) => e !== key);
- recents.unshift(key);
- this.$store.set('recentlyUsedEmojis', recents.splice(0, 32));
- }
- },
+ const key = getKey(emoji);
+ emit('chosen', key);
- paste(event) {
- const paste = (event.clipboardData || window.clipboardData).getData('text');
- if (this.done(paste)) {
- event.preventDefault();
- }
- },
+ // 最近使った絵文字更新
+ if (!pinned.value.includes(key)) {
+ let recents = defaultStore.state.recentlyUsedEmojis;
+ recents = recents.filter((e: any) => e !== key);
+ recents.unshift(key);
+ defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+ }
+}
- done(query) {
- if (query == null) query = this.q;
- if (query == null) return;
- const q = query.replace(/:/g, '');
- const exactMatchCustom = this.customEmojis.find(e => e.name === q);
- if (exactMatchCustom) {
- this.chosen(exactMatchCustom);
- return true;
- }
- const exactMatchUnicode = this.emojilist.find(e => e.char === q || e.name === q);
- if (exactMatchUnicode) {
- this.chosen(exactMatchUnicode);
- return true;
- }
- if (this.searchResultCustom.length > 0) {
- this.chosen(this.searchResultCustom[0]);
- return true;
- }
- if (this.searchResultUnicode.length > 0) {
- this.chosen(this.searchResultUnicode[0]);
- return true;
- }
- },
+function paste(event: ClipboardEvent) {
+ const paste = (event.clipboardData || window.clipboardData).getData('text');
+ if (done(paste)) {
+ event.preventDefault();
}
+}
+
+function done(query?: any): boolean | void {
+ if (query == null) query = q.value;
+ if (query == null || typeof query !== 'string') return;
+
+ const q2 = query.replace(/:/g, '');
+ const exactMatchCustom = customEmojis.find(e => e.name === q2);
+ if (exactMatchCustom) {
+ chosen(exactMatchCustom);
+ return true;
+ }
+ const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
+ if (exactMatchUnicode) {
+ chosen(exactMatchUnicode);
+ return true;
+ }
+ if (searchResultCustom.value.length > 0) {
+ chosen(searchResultCustom.value[0]);
+ return true;
+ }
+ if (searchResultUnicode.value.length > 0) {
+ chosen(searchResultUnicode.value[0]);
+ return true;
+ }
+}
+
+onMounted(() => {
+ focus();
+});
+
+defineExpose({
+ focus,
+ reset,
});
</script>
diff --git a/packages/client/src/components/featured-photos.vue b/packages/client/src/components/featured-photos.vue
index af5892c98e..e58b5d2849 100644
--- a/packages/client/src/components/featured-photos.vue
+++ b/packages/client/src/components/featured-photos.vue
@@ -2,25 +2,15 @@
<div v-if="meta" class="xfbouadm" :style="{ backgroundImage: `url(${ meta.backgroundImageUrl })` }"></div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os';
-export default defineComponent({
- components: {
- },
+const meta = ref<Misskey.entities.DetailedInstanceMetadata>();
- data() {
- return {
- meta: null,
- };
- },
-
- created() {
- os.api('meta', { detail: true }).then(meta => {
- this.meta = meta;
- });
- },
+os.api('meta', { detail: true }).then(gotMeta => {
+ meta.value = gotMeta;
});
</script>
diff --git a/packages/client/src/components/file-type-icon.vue b/packages/client/src/components/file-type-icon.vue
index be1af5e501..11d28188cc 100644
--- a/packages/client/src/components/file-type-icon.vue
+++ b/packages/client/src/components/file-type-icon.vue
@@ -4,25 +4,12 @@
</span>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { computed } from 'vue';
-export default defineComponent({
- props: {
- type: {
- type: String,
- required: true,
- }
- },
- data() {
- return {
- };
- },
- computed: {
- kind(): string {
- return this.type.split('/')[0];
- }
- }
-});
+const props = defineProps<{
+ type: string;
+}>();
+
+const kind = computed(() => props.type.split('/')[0]);
</script>
diff --git a/packages/client/src/components/follow-button.vue b/packages/client/src/components/follow-button.vue
index 7136261914..345edb6441 100644
--- a/packages/client/src/components/follow-button.vue
+++ b/packages/client/src/components/follow-button.vue
@@ -6,128 +6,110 @@
>
<template v-if="!wait">
<template v-if="hasPendingFollowRequestFromYou && user.isLocked">
- <span v-if="full">{{ $ts.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
+ <span v-if="full">{{ i18n.locale.followRequestPending }}</span><i class="fas fa-hourglass-half"></i>
</template>
<template v-else-if="hasPendingFollowRequestFromYou && !user.isLocked"> <!-- つまりリモートフォローの場合。 -->
- <span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
+ <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse"></i>
</template>
<template v-else-if="isFollowing">
- <span v-if="full">{{ $ts.unfollow }}</span><i class="fas fa-minus"></i>
+ <span v-if="full">{{ i18n.locale.unfollow }}</span><i class="fas fa-minus"></i>
</template>
<template v-else-if="!isFollowing && user.isLocked">
- <span v-if="full">{{ $ts.followRequest }}</span><i class="fas fa-plus"></i>
+ <span v-if="full">{{ i18n.locale.followRequest }}</span><i class="fas fa-plus"></i>
</template>
<template v-else-if="!isFollowing && !user.isLocked">
- <span v-if="full">{{ $ts.follow }}</span><i class="fas fa-plus"></i>
+ <span v-if="full">{{ i18n.locale.follow }}</span><i class="fas fa-plus"></i>
</template>
</template>
<template v-else>
- <span v-if="full">{{ $ts.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
+ <span v-if="full">{{ i18n.locale.processing }}</span><i class="fas fa-spinner fa-pulse fa-fw"></i>
</template>
</button>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { onBeforeUnmount, onMounted, ref } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os';
+import { stream } from '@/stream';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- user: {
- type: Object,
- required: true
- },
- full: {
- type: Boolean,
- required: false,
- default: false,
- },
- large: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- data() {
- return {
- isFollowing: this.user.isFollowing,
- hasPendingFollowRequestFromYou: this.user.hasPendingFollowRequestFromYou,
- wait: false,
- connection: null,
- };
- },
-
- created() {
- // 渡されたユーザー情報が不完全な場合
- if (this.user.isFollowing == null) {
- os.api('users/show', {
- userId: this.user.id
- }).then(u => {
- this.isFollowing = u.isFollowing;
- this.hasPendingFollowRequestFromYou = u.hasPendingFollowRequestFromYou;
- });
- }
- },
-
- mounted() {
- this.connection = markRaw(os.stream.useChannel('main'));
+const props = withDefaults(defineProps<{
+ user: Misskey.entities.UserDetailed,
+ full?: boolean,
+ large?: boolean,
+}>(), {
+ full: false,
+ large: false,
+});
- this.connection.on('follow', this.onFollowChange);
- this.connection.on('unfollow', this.onFollowChange);
- },
+const isFollowing = ref(props.user.isFollowing);
+const hasPendingFollowRequestFromYou = ref(props.user.hasPendingFollowRequestFromYou);
+const wait = ref(false);
+const connection = stream.useChannel('main');
- beforeUnmount() {
- this.connection.dispose();
- },
+if (props.user.isFollowing == null) {
+ os.api('users/show', {
+ userId: props.user.id
+ }).then(u => {
+ isFollowing.value = u.isFollowing;
+ hasPendingFollowRequestFromYou.value = u.hasPendingFollowRequestFromYou;
+ });
+}
- methods: {
- onFollowChange(user) {
- if (user.id == this.user.id) {
- this.isFollowing = user.isFollowing;
- this.hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
- }
- },
+function onFollowChange(user: Misskey.entities.UserDetailed) {
+ if (user.id == props.user.id) {
+ isFollowing.value = user.isFollowing;
+ hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
+ }
+}
- async onClick() {
- this.wait = true;
+async function onClick() {
+ wait.value = true;
- try {
- if (this.isFollowing) {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: this.$t('unfollowConfirm', { name: this.user.name || this.user.username }),
- });
+ try {
+ if (isFollowing.value) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
+ });
- if (canceled) return;
+ if (canceled) return;
- await os.api('following/delete', {
- userId: this.user.id
- });
- } else {
- if (this.hasPendingFollowRequestFromYou) {
- await os.api('following/requests/cancel', {
- userId: this.user.id
- });
- } else if (this.user.isLocked) {
- await os.api('following/create', {
- userId: this.user.id
- });
- this.hasPendingFollowRequestFromYou = true;
- } else {
- await os.api('following/create', {
- userId: this.user.id
- });
- this.hasPendingFollowRequestFromYou = true;
- }
- }
- } catch (e) {
- console.error(e);
- } finally {
- this.wait = false;
+ await os.api('following/delete', {
+ userId: props.user.id
+ });
+ } else {
+ if (hasPendingFollowRequestFromYou.value) {
+ await os.api('following/requests/cancel', {
+ userId: props.user.id
+ });
+ } else if (props.user.isLocked) {
+ await os.api('following/create', {
+ userId: props.user.id
+ });
+ hasPendingFollowRequestFromYou.value = true;
+ } else {
+ await os.api('following/create', {
+ userId: props.user.id
+ });
+ hasPendingFollowRequestFromYou.value = true;
}
}
+ } catch (e) {
+ console.error(e);
+ } finally {
+ wait.value = false;
}
+}
+
+onMounted(() => {
+ connection.on('follow', onFollowChange);
+ connection.on('unfollow', onFollowChange);
+});
+
+onBeforeUnmount(() => {
+ connection.dispose();
});
</script>
diff --git a/packages/client/src/components/forgot-password.vue b/packages/client/src/components/forgot-password.vue
index b03a6133b4..c74e1ac75e 100644
--- a/packages/client/src/components/forgot-password.vue
+++ b/packages/client/src/components/forgot-password.vue
@@ -2,72 +2,64 @@
<XModalWindow ref="dialog"
:width="370"
:height="400"
- @close="$refs.dialog.close()"
- @closed="$emit('closed')"
+ @close="dialog.close()"
+ @closed="emit('closed')"
>
- <template #header>{{ $ts.forgotPassword }}</template>
+ <template #header>{{ i18n.locale.forgotPassword }}</template>
- <form v-if="$instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
+ <form v-if="instance.enableEmail" class="bafeceda" @submit.prevent="onSubmit">
<div class="main _formRoot">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required>
- <template #label>{{ $ts.username }}</template>
+ <template #label>{{ i18n.locale.username }}</template>
<template #prefix>@</template>
</MkInput>
<MkInput v-model="email" class="_formBlock" type="email" spellcheck="false" required>
- <template #label>{{ $ts.emailAddress }}</template>
- <template #caption>{{ $ts._forgotPassword.enterEmail }}</template>
+ <template #label>{{ i18n.locale.emailAddress }}</template>
+ <template #caption>{{ i18n.locale._forgotPassword.enterEmail }}</template>
</MkInput>
- <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ $ts.send }}</MkButton>
+ <MkButton class="_formBlock" type="submit" :disabled="processing" primary style="margin: 0 auto;">{{ i18n.locale.send }}</MkButton>
</div>
<div class="sub">
- <MkA to="/about" class="_link">{{ $ts._forgotPassword.ifNoEmail }}</MkA>
+ <MkA to="/about" class="_link">{{ i18n.locale._forgotPassword.ifNoEmail }}</MkA>
</div>
</form>
- <div v-else>
- {{ $ts._forgotPassword.contactAdmin }}
+ <div v-else class="bafecedb">
+ {{ i18n.locale._forgotPassword.contactAdmin }}
</div>
</XModalWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import * as os from '@/os';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XModalWindow,
- MkButton,
- MkInput,
- },
+const emit = defineEmits<{
+ (e: 'done'): void;
+ (e: 'closed'): void;
+}>();
- emits: ['done', 'closed'],
+let dialog: InstanceType<typeof XModalWindow> = $ref();
- data() {
- return {
- username: '',
- email: '',
- processing: false,
- };
- },
+let username = $ref('');
+let email = $ref('');
+let processing = $ref(false);
- methods: {
- async onSubmit() {
- this.processing = true;
- await os.apiWithDialog('request-reset-password', {
- username: this.username,
- email: this.email,
- });
-
- this.$emit('done');
- this.$refs.dialog.close();
- }
- }
-});
+async function onSubmit() {
+ processing = true;
+ await os.apiWithDialog('request-reset-password', {
+ username,
+ email,
+ });
+ emit('done');
+ dialog.close();
+}
</script>
<style lang="scss" scoped>
@@ -81,4 +73,8 @@ export default defineComponent({
padding: 24px;
}
}
+
+.bafecedb {
+ padding: 24px;
+}
</style>
diff --git a/packages/client/src/components/form/folder.vue b/packages/client/src/components/form/folder.vue
new file mode 100644
index 0000000000..571afe50c0
--- /dev/null
+++ b/packages/client/src/components/form/folder.vue
@@ -0,0 +1,107 @@
+<template>
+<div class="dwzlatin" :class="{ opened }">
+ <div class="header _button" @click="toggle">
+ <span class="icon"><slot name="icon"></slot></span>
+ <span class="text"><slot name="label"></slot></span>
+ <span class="right">
+ <span class="text"><slot name="suffix"></slot></span>
+ <i v-if="opened" class="fas fa-angle-up icon"></i>
+ <i v-else class="fas fa-angle-down icon"></i>
+ </span>
+ </div>
+ <keep-alive>
+ <div v-if="openedAtLeastOnce" v-show="opened" class="body">
+ <MkSpacer :margin-min="14" :margin-max="22">
+ <slot></slot>
+ </MkSpacer>
+ </div>
+ </keep-alive>
+</div>
+</template>
+
+<script lang="ts" setup>
+const props = withDefaults(defineProps<{
+ defaultOpen: boolean;
+}>(), {
+ defaultOpen: false,
+})
+
+let opened = $ref(props.defaultOpen);
+let openedAtLeastOnce = $ref(props.defaultOpen);
+
+const toggle = () => {
+ opened = !opened;
+ if (opened) {
+ openedAtLeastOnce = true;
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+.dwzlatin {
+ display: block;
+
+ > .header {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 12px 14px 12px 14px;
+ background: var(--buttonBg);
+ border-radius: 6px;
+
+ &:hover {
+ text-decoration: none;
+ background: var(--buttonHoverBg);
+ }
+
+ &.active {
+ color: var(--accent);
+ background: var(--buttonHoverBg);
+ }
+
+ > .icon {
+ margin-right: 0.75em;
+ flex-shrink: 0;
+ text-align: center;
+ opacity: 0.8;
+
+ &:empty {
+ display: none;
+
+ & + .text {
+ padding-left: 4px;
+ }
+ }
+ }
+
+ > .text {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ padding-right: 12px;
+ }
+
+ > .right {
+ margin-left: auto;
+ opacity: 0.7;
+ white-space: nowrap;
+
+ > .text:not(:empty) {
+ margin-right: 0.75em;
+ }
+ }
+ }
+
+ > .body {
+ background: var(--panel);
+ border-radius: 0 0 6px 6px;
+ }
+
+ &.opened {
+ > .header {
+ border-radius: 6px 6px 0 0;
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/components/form/group.vue b/packages/client/src/components/form/group.vue
index 2fc203f1b9..1e8376ca44 100644
--- a/packages/client/src/components/form/group.vue
+++ b/packages/client/src/components/form/group.vue
@@ -1,5 +1,5 @@
<template>
-<div v-sticky-container v-panel class="adfeebaf _formBlock">
+<div v-sticky-container class="adfeebaf _formBlock">
<div class="label"><slot name="label"></slot></div>
<div class="main _formRoot">
<slot></slot>
@@ -17,6 +17,7 @@ export default defineComponent({
<style lang="scss" scoped>
.adfeebaf {
padding: 24px 24px;
+ border: solid 1px var(--divider);
border-radius: var(--radius);
> .label {
diff --git a/packages/client/src/components/form/input.vue b/packages/client/src/components/form/input.vue
index 3533f4f27b..7165671af3 100644
--- a/packages/client/src/components/form/input.vue
+++ b/packages/client/src/components/form/input.vue
@@ -167,7 +167,7 @@ export default defineComponent({
// このコンポーネントが作成された時、非表示状態である場合がある
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
- const clock = setInterval(() => {
+ const clock = window.setInterval(() => {
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@@ -181,7 +181,7 @@ export default defineComponent({
}, 100);
onUnmounted(() => {
- clearInterval(clock);
+ window.clearInterval(clock);
});
});
});
diff --git a/packages/client/src/components/form/pagination.vue b/packages/client/src/components/form/pagination.vue
deleted file mode 100644
index 3d3b40a783..0000000000
--- a/packages/client/src/components/form/pagination.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<template>
-<FormSlot>
- <template #label><slot name="label"></slot></template>
- <div class="abcaccfa">
- <slot :items="items"></slot>
- <div v-if="empty" key="_empty_" class="empty">
- <slot name="empty"></slot>
- </div>
- <MkButton v-show="more" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
- </MkButton>
- </div>
-</FormSlot>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@/components/ui/button.vue';
-import FormSlot from './slot.vue';
-import paging from '@/scripts/paging';
-
-export default defineComponent({
- components: {
- MkButton,
- FormSlot,
- },
-
- mixins: [
- paging({}),
- ],
-
- props: {
- pagination: {
- required: true
- },
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.abcaccfa {
-}
-</style>
diff --git a/packages/client/src/components/form/radio.vue b/packages/client/src/components/form/radio.vue
index f0b8c71376..2becbec6f3 100644
--- a/packages/client/src/components/form/radio.vue
+++ b/packages/client/src/components/form/radio.vue
@@ -1,6 +1,6 @@
<template>
<div
- v-panel
+ v-adaptive-border
class="novjtctn"
:class="{ disabled, checked }"
:aria-checked="checked"
@@ -53,7 +53,10 @@ export default defineComponent({
display: inline-block;
text-align: left;
cursor: pointer;
- padding: 11px 14px;
+ padding: 10px 12px;
+ background-color: var(--panel);
+ background-clip: padding-box !important;
+ border: solid 1px var(--panel);
border-radius: 6px;
transition: all 0.3s;
@@ -69,9 +72,13 @@ export default defineComponent({
}
}
+ &:hover {
+ border-color: var(--inputBorderHover) !important;
+ }
+
&.checked {
- background: var(--accentedBg) !important;
- border-color: var(--accent);
+ background-color: var(--accentedBg) !important;
+ border-color: var(--accentedBg) !important;
color: var(--accent);
&, * {
@@ -89,11 +96,6 @@ export default defineComponent({
}
}
- &:hover {
- border-color: var(--inputBorderHover);
- color: var(--accent);
- }
-
> input {
position: absolute;
width: 0;
diff --git a/packages/client/src/components/form/section.vue b/packages/client/src/components/form/section.vue
index bc2ab966b8..c6e34ef1cc 100644
--- a/packages/client/src/components/form/section.vue
+++ b/packages/client/src/components/form/section.vue
@@ -1,5 +1,5 @@
<template>
-<div v-size="{ max: [500] }" v-sticky-container class="vrtktovh _formBlock">
+<div class="vrtktovh _formBlock">
<div class="label"><slot name="label"></slot></div>
<div class="main _formRoot">
<slot></slot>
@@ -7,20 +7,13 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-
-});
+<script lang="ts" setup>
</script>
<style lang="scss" scoped>
.vrtktovh {
- margin: 0;
border-top: solid 0.5px var(--divider);
border-bottom: solid 0.5px var(--divider);
- padding: 24px 0;
& + .vrtktovh {
border-top: none;
@@ -36,7 +29,7 @@ export default defineComponent({
> .label {
font-weight: bold;
- padding: 0 0 16px 0;
+ margin: 1.5em 0 16px 0;
&:empty {
display: none;
@@ -44,6 +37,7 @@ export default defineComponent({
}
> .main {
+ margin: 1.5em 0;
}
}
</style>
diff --git a/packages/client/src/components/form/select.vue b/packages/client/src/components/form/select.vue
index afc53ca9c8..87196027a8 100644
--- a/packages/client/src/components/form/select.vue
+++ b/packages/client/src/components/form/select.vue
@@ -117,7 +117,7 @@ export default defineComponent({
// このコンポーネントが作成された時、非表示状態である場合がある
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
- const clock = setInterval(() => {
+ const clock = window.setInterval(() => {
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@@ -131,7 +131,7 @@ export default defineComponent({
}, 100);
onUnmounted(() => {
- clearInterval(clock);
+ window.clearInterval(clock);
});
});
});
diff --git a/packages/client/src/components/form/split.vue b/packages/client/src/components/form/split.vue
new file mode 100644
index 0000000000..676b293967
--- /dev/null
+++ b/packages/client/src/components/form/split.vue
@@ -0,0 +1,27 @@
+<template>
+<div class="terlnhxf _formBlock">
+ <slot></slot>
+</div>
+</template>
+
+<script lang="ts" setup>
+const props = withDefaults(defineProps<{
+ minWidth: number;
+}>(), {
+ minWidth: 210,
+});
+
+const minWidth = props.minWidth + 'px';
+</script>
+
+<style lang="scss" scoped>
+.terlnhxf {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(v-bind('minWidth'), 1fr));
+ grid-gap: 12px;
+
+ > ::v-deep(*) {
+ margin: 0 !important;
+ }
+}
+</style>
diff --git a/packages/client/src/components/form/suspense.vue b/packages/client/src/components/form/suspense.vue
index 4d5debe604..2ad55dacae 100644
--- a/packages/client/src/components/form/suspense.vue
+++ b/packages/client/src/components/form/suspense.vue
@@ -1,5 +1,5 @@
<template>
-<transition name="fade" mode="out-in">
+<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="pending">
<MkLoading/>
</div>
diff --git a/packages/client/src/components/form/switch.vue b/packages/client/src/components/form/switch.vue
index aa9b09215e..f8a07b4caa 100644
--- a/packages/client/src/components/form/switch.vue
+++ b/packages/client/src/components/form/switch.vue
@@ -13,7 +13,8 @@
<i class="check fas fa-check"></i>
</span>
<span class="label">
- <span @click="toggle"><slot></slot></span>
+ <!-- TODO: 無名slotの方は廃止 -->
+ <span @click="toggle"><slot name="label"></slot><slot></slot></span>
<p class="caption"><slot name="caption"></slot></p>
</span>
</div>
@@ -110,7 +111,7 @@ export default defineComponent({
}
> .label {
- margin-left: 16px;
+ margin-left: 12px;
margin-top: 2px;
display: block;
transition: inherit;
diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue
index 77ee7525a4..cf7385ca22 100644
--- a/packages/client/src/components/global/a.vue
+++ b/packages/client/src/components/global/a.vue
@@ -4,130 +4,114 @@
</a>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { inject } from 'vue';
import * as os from '@/os';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { router } from '@/router';
import { url } from '@/config';
-import { popout } from '@/scripts/popout';
-import { ColdDeviceStorage } from '@/store';
+import { popout as popout_ } from '@/scripts/popout';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
-export default defineComponent({
- inject: {
- navHook: {
- default: null
- },
- sideViewHook: {
- default: null
- }
- },
+const props = withDefaults(defineProps<{
+ to: string;
+ activeClass?: null | string;
+ behavior?: null | 'window' | 'browser' | 'modalWindow';
+}>(), {
+ activeClass: null,
+ behavior: null,
+});
- props: {
- to: {
- type: String,
- required: true,
- },
- activeClass: {
- type: String,
- required: false,
- },
- behavior: {
- type: String,
- required: false,
- },
- },
+const navHook = inject('navHook', null);
+const sideViewHook = inject('sideViewHook', null);
- computed: {
- active() {
- if (this.activeClass == null) return false;
- const resolved = router.resolve(this.to);
- if (resolved.path == this.$route.path) return true;
- if (resolved.name == null) return false;
- if (this.$route.name == null) return false;
- return resolved.name == this.$route.name;
- }
- },
+const active = $computed(() => {
+ if (props.activeClass == null) return false;
+ const resolved = router.resolve(props.to);
+ if (resolved.path === router.currentRoute.value.path) return true;
+ if (resolved.name == null) return false;
+ if (router.currentRoute.value.name == null) return false;
+ return resolved.name === router.currentRoute.value.name;
+});
- methods: {
- onContextmenu(e) {
- if (window.getSelection().toString() !== '') return;
- os.contextMenu([{
- type: 'label',
- text: this.to,
- }, {
- icon: 'fas fa-window-maximize',
- text: this.$ts.openInWindow,
- action: () => {
- os.pageWindow(this.to);
- }
- }, this.sideViewHook ? {
- icon: 'fas fa-columns',
- text: this.$ts.openInSideView,
- action: () => {
- this.sideViewHook(this.to);
- }
- } : undefined, {
- icon: 'fas fa-expand-alt',
- text: this.$ts.showInPage,
- action: () => {
- this.$router.push(this.to);
- }
- }, null, {
- icon: 'fas fa-external-link-alt',
- text: this.$ts.openInNewTab,
- action: () => {
- window.open(this.to, '_blank');
- }
- }, {
- icon: 'fas fa-link',
- text: this.$ts.copyLink,
- action: () => {
- copyToClipboard(`${url}${this.to}`);
- }
- }], e);
- },
+function onContextmenu(ev) {
+ const selection = window.getSelection();
+ if (selection && selection.toString() !== '') return;
+ os.contextMenu([{
+ type: 'label',
+ text: props.to,
+ }, {
+ icon: 'fas fa-window-maximize',
+ text: i18n.locale.openInWindow,
+ action: () => {
+ os.pageWindow(props.to);
+ }
+ }, sideViewHook ? {
+ icon: 'fas fa-columns',
+ text: i18n.locale.openInSideView,
+ action: () => {
+ sideViewHook(props.to);
+ }
+ } : undefined, {
+ icon: 'fas fa-expand-alt',
+ text: i18n.locale.showInPage,
+ action: () => {
+ router.push(props.to);
+ }
+ }, null, {
+ icon: 'fas fa-external-link-alt',
+ text: i18n.locale.openInNewTab,
+ action: () => {
+ window.open(props.to, '_blank');
+ }
+ }, {
+ icon: 'fas fa-link',
+ text: i18n.locale.copyLink,
+ action: () => {
+ copyToClipboard(`${url}${props.to}`);
+ }
+ }], ev);
+}
- window() {
- os.pageWindow(this.to);
- },
+function openWindow() {
+ os.pageWindow(props.to);
+}
- modalWindow() {
- os.modalPageWindow(this.to);
- },
+function modalWindow() {
+ os.modalPageWindow(props.to);
+}
- popout() {
- popout(this.to);
- },
+function popout() {
+ popout_(props.to);
+}
- nav() {
- if (this.behavior === 'browser') {
- location.href = this.to;
- return;
- }
+function nav() {
+ if (props.behavior === 'browser') {
+ location.href = props.to;
+ return;
+ }
- if (this.behavior) {
- if (this.behavior === 'window') {
- return this.window();
- } else if (this.behavior === 'modalWindow') {
- return this.modalWindow();
- }
- }
+ if (props.behavior) {
+ if (props.behavior === 'window') {
+ return openWindow();
+ } else if (props.behavior === 'modalWindow') {
+ return modalWindow();
+ }
+ }
- if (this.navHook) {
- this.navHook(this.to);
- } else {
- if (this.$store.state.defaultSideView && this.sideViewHook && this.to !== '/') {
- return this.sideViewHook(this.to);
- }
+ if (navHook) {
+ navHook(props.to);
+ } else {
+ if (defaultStore.state.defaultSideView && sideViewHook && props.to !== '/') {
+ return sideViewHook(props.to);
+ }
- if (this.$router.currentRoute.value.path === this.to) {
- window.scroll({ top: 0, behavior: 'smooth' });
- } else {
- this.$router.push(this.to);
- }
- }
+ if (router.currentRoute.value.path === props.to) {
+ window.scroll({ top: 0, behavior: 'smooth' });
+ } else {
+ router.push(props.to);
}
}
-});
+}
</script>
diff --git a/packages/client/src/components/global/acct.vue b/packages/client/src/components/global/acct.vue
index 018826153c..c3e806b5fb 100644
--- a/packages/client/src/components/global/acct.vue
+++ b/packages/client/src/components/global/acct.vue
@@ -5,28 +5,17 @@
</span>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import * as misskey from 'misskey-js';
import { toUnicode } from 'punycode/';
-import { host } from '@/config';
+import { host as hostRaw } from '@/config';
-export default defineComponent({
- props: {
- user: {
- type: Object,
- required: true
- },
- detail: {
- type: Boolean,
- default: false
- },
- },
- data() {
- return {
- host: toUnicode(host),
- };
- }
-});
+defineProps<{
+ user: misskey.entities.UserDetailed;
+ detail?: boolean;
+}>();
+
+const host = toUnicode(hostRaw);
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/global/ad.vue b/packages/client/src/components/global/ad.vue
index 49046b00a7..180dabb2a2 100644
--- a/packages/client/src/components/global/ad.vue
+++ b/packages/client/src/components/global/ad.vue
@@ -20,7 +20,7 @@
<script lang="ts">
import { defineComponent, ref } from 'vue';
-import { Instance, instance } from '@/instance';
+import { instance } from '@/instance';
import { host } from '@/config';
import MkButton from '@/components/ui/button.vue';
import { defaultStore } from '@/store';
@@ -48,9 +48,9 @@ export default defineComponent({
showMenu.value = !showMenu.value;
};
- const choseAd = (): Instance['ads'][number] | null => {
+ const choseAd = (): (typeof instance)['ads'][number] | null => {
if (props.specify) {
- return props.specify as Instance['ads'][number];
+ return props.specify as (typeof instance)['ads'][number];
}
const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
diff --git a/packages/client/src/components/global/avatar.vue b/packages/client/src/components/global/avatar.vue
index 300e5e079f..27cfb6e4d4 100644
--- a/packages/client/src/components/global/avatar.vue
+++ b/packages/client/src/components/global/avatar.vue
@@ -1,74 +1,54 @@
<template>
-<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :title="acct(user)" @click="onClick">
+<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :title="acct(user)" @click="onClick">
<img class="inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
</span>
-<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :to="userPage(user)" :title="acct(user)" :target="target">
+<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
<img class="inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
</MkA>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, watch } from 'vue';
+import * as misskey from 'misskey-js';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
import { acct, userPage } from '@/filters/user';
import MkUserOnlineIndicator from '@/components/user-online-indicator.vue';
+import { defaultStore } from '@/store';
-export default defineComponent({
- components: {
- MkUserOnlineIndicator
- },
- props: {
- user: {
- type: Object,
- required: true
- },
- target: {
- required: false,
- default: null
- },
- disableLink: {
- required: false,
- default: false
- },
- disablePreview: {
- required: false,
- default: false
- },
- showIndicator: {
- required: false,
- default: false
- }
- },
- emits: ['click'],
- computed: {
- cat(): boolean {
- return this.user.isCat;
- },
- url(): string {
- return this.$store.state.disableShowingAnimatedImages
- ? getStaticImageUrl(this.user.avatarUrl)
- : this.user.avatarUrl;
- },
- },
- watch: {
- 'user.avatarBlurhash'() {
- if (this.$el == null) return;
- this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
- }
- },
- mounted() {
- this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
- },
- methods: {
- onClick(e) {
- this.$emit('click', e);
- },
- acct,
- userPage
- }
+const props = withDefaults(defineProps<{
+ user: misskey.entities.User;
+ target?: string | null;
+ disableLink?: boolean;
+ disablePreview?: boolean;
+ showIndicator?: boolean;
+}>(), {
+ target: null,
+ disableLink: false,
+ disablePreview: false,
+ showIndicator: false,
+});
+
+const emit = defineEmits<{
+ (e: 'click', ev: MouseEvent): void;
+}>();
+
+const url = $computed(() => defaultStore.state.disableShowingAnimatedImages
+ ? getStaticImageUrl(props.user.avatarUrl)
+ : props.user.avatarUrl);
+
+function onClick(ev: MouseEvent) {
+ emit('click', ev);
+}
+
+let color = $ref();
+
+watch(() => props.user.avatarBlurhash, () => {
+ color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
+}, {
+ immediate: true,
});
</script>
diff --git a/packages/client/src/components/global/error.vue b/packages/client/src/components/global/error.vue
index d759186167..98b96fb414 100644
--- a/packages/client/src/components/global/error.vue
+++ b/packages/client/src/components/global/error.vue
@@ -8,19 +8,8 @@
</transition>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
import MkButton from '@/components/ui/button.vue';
-
-export default defineComponent({
- components: {
- MkButton,
- },
- data() {
- return {
- };
- },
-});
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/global/loading.vue b/packages/client/src/components/global/loading.vue
index 7bde53c12e..43ea1395ed 100644
--- a/packages/client/src/components/global/loading.vue
+++ b/packages/client/src/components/global/loading.vue
@@ -4,27 +4,17 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
-export default defineComponent({
- props: {
- inline: {
- type: Boolean,
- required: false,
- default: false
- },
- colored: {
- type: Boolean,
- required: false,
- default: true
- },
- mini: {
- type: Boolean,
- required: false,
- default: false
- },
- }
+const props = withDefaults(defineProps<{
+ inline?: boolean;
+ colored?: boolean;
+ mini?: boolean;
+}>(), {
+ inline: false,
+ colored: true,
+ mini: false,
});
</script>
diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue
index ab20404909..243d8614ba 100644
--- a/packages/client/src/components/global/misskey-flavored-markdown.vue
+++ b/packages/client/src/components/global/misskey-flavored-markdown.vue
@@ -1,15 +1,23 @@
<template>
-<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
+<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :customEmojis="customEmojis" :isNote="isNote" class="havbbuyv" :class="{ nowrap }"/>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MfmCore from '@/components/mfm';
-export default defineComponent({
- components: {
- MfmCore
- }
+const props = withDefaults(defineProps<{
+ text: string;
+ plain?: boolean;
+ nowrap?: boolean;
+ author?: any;
+ customEmojis?: any;
+ isNote?: boolean;
+}>(), {
+ plain: false,
+ nowrap: false,
+ author: null,
+ isNote: true,
});
</script>
diff --git a/packages/client/src/components/global/spacer.vue b/packages/client/src/components/global/spacer.vue
index e2f1d1aec7..8a1d7a4e8a 100644
--- a/packages/client/src/components/global/spacer.vue
+++ b/packages/client/src/components/global/spacer.vue
@@ -40,7 +40,7 @@ export default defineComponent({
return;
}
- if (rect.width > props.contentMax || rect.width > 500) {
+ if (rect.width > props.contentMax || (rect.width > 360 && window.innerWidth > 400)) {
margin.value = props.marginMax;
} else {
margin.value = props.marginMin;
diff --git a/packages/client/src/components/global/sticky-container.vue b/packages/client/src/components/global/sticky-container.vue
index 859b2c1d73..89d397f082 100644
--- a/packages/client/src/components/global/sticky-container.vue
+++ b/packages/client/src/components/global/sticky-container.vue
@@ -45,7 +45,7 @@ export default defineComponent({
calc();
const observer = new MutationObserver(() => {
- setTimeout(() => {
+ window.setTimeout(() => {
calc();
}, 100);
});
diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue
index 6a330a2307..d2788264c5 100644
--- a/packages/client/src/components/global/time.vue
+++ b/packages/client/src/components/global/time.vue
@@ -1,73 +1,57 @@
<template>
<time :title="absolute">
- <template v-if="mode == 'relative'">{{ relative }}</template>
- <template v-else-if="mode == 'absolute'">{{ absolute }}</template>
- <template v-else-if="mode == 'detail'">{{ absolute }} ({{ relative }})</template>
+ <template v-if="mode === 'relative'">{{ relative }}</template>
+ <template v-else-if="mode === 'absolute'">{{ absolute }}</template>
+ <template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template>
</time>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onUnmounted } from 'vue';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- time: {
- type: [Date, String],
- required: true
- },
- mode: {
- type: String,
- default: 'relative'
- }
- },
- data() {
- return {
- tickId: null,
- now: new Date()
- };
- },
- computed: {
- _time(): Date {
- return typeof this.time == 'string' ? new Date(this.time) : this.time;
- },
- absolute(): string {
- return this._time.toLocaleString();
- },
- relative(): string {
- const time = this._time;
- const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
- return (
- ago >= 31536000 ? this.$t('_ago.yearsAgo', { n: (~~(ago / 31536000)).toString() }) :
- ago >= 2592000 ? this.$t('_ago.monthsAgo', { n: (~~(ago / 2592000)).toString() }) :
- ago >= 604800 ? this.$t('_ago.weeksAgo', { n: (~~(ago / 604800)).toString() }) :
- ago >= 86400 ? this.$t('_ago.daysAgo', { n: (~~(ago / 86400)).toString() }) :
- ago >= 3600 ? this.$t('_ago.hoursAgo', { n: (~~(ago / 3600)).toString() }) :
- ago >= 60 ? this.$t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
- ago >= 10 ? this.$t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
- ago >= -1 ? this.$ts._ago.justNow :
- ago < -1 ? this.$ts._ago.future :
- this.$ts._ago.unknown);
- }
- },
- created() {
- if (this.mode == 'relative' || this.mode == 'detail') {
- this.tickId = window.requestAnimationFrame(this.tick);
- }
- },
- unmounted() {
- if (this.mode === 'relative' || this.mode === 'detail') {
- window.clearTimeout(this.tickId);
- }
- },
- methods: {
- tick() {
- // TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
- this.now = new Date();
+const props = withDefaults(defineProps<{
+ time: Date | string;
+ mode?: 'relative' | 'absolute' | 'detail';
+}>(), {
+ mode: 'relative',
+});
+
+const _time = typeof props.time == 'string' ? new Date(props.time) : props.time;
+const absolute = _time.toLocaleString();
- this.tickId = setTimeout(() => {
- window.requestAnimationFrame(this.tick);
- }, 10000);
- }
- }
+let now = $ref(new Date());
+const relative = $computed(() => {
+ const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
+ return (
+ ago >= 31536000 ? i18n.t('_ago.yearsAgo', { n: (~~(ago / 31536000)).toString() }) :
+ ago >= 2592000 ? i18n.t('_ago.monthsAgo', { n: (~~(ago / 2592000)).toString() }) :
+ ago >= 604800 ? i18n.t('_ago.weeksAgo', { n: (~~(ago / 604800)).toString() }) :
+ ago >= 86400 ? i18n.t('_ago.daysAgo', { n: (~~(ago / 86400)).toString() }) :
+ ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: (~~(ago / 3600)).toString() }) :
+ ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
+ ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
+ ago >= -1 ? i18n.locale._ago.justNow :
+ ago < -1 ? i18n.locale._ago.future :
+ i18n.locale._ago.unknown);
});
+
+function tick() {
+ // TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
+ now = new Date();
+
+ tickId = window.setTimeout(() => {
+ window.requestAnimationFrame(tick);
+ }, 10000);
+}
+
+let tickId: number;
+
+if (props.mode === 'relative' || props.mode === 'detail') {
+ tickId = window.requestAnimationFrame(tick);
+
+ onUnmounted(() => {
+ window.clearTimeout(tickId);
+ });
+}
</script>
diff --git a/packages/client/src/components/global/user-name.vue b/packages/client/src/components/global/user-name.vue
index bc93a8ea30..090de3df30 100644
--- a/packages/client/src/components/global/user-name.vue
+++ b/packages/client/src/components/global/user-name.vue
@@ -2,19 +2,14 @@
<Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
-export default defineComponent({
- props: {
- user: {
- type: Object,
- required: true
- },
- nowrap: {
- type: Boolean,
- default: true
- },
- }
+const props = withDefaults(defineProps<{
+ user: misskey.entities.User;
+ nowrap?: boolean;
+}>(), {
+ nowrap: true,
});
</script>
diff --git a/packages/client/src/components/google.vue b/packages/client/src/components/google.vue
index a39168b80f..210ca72bfe 100644
--- a/packages/client/src/components/google.vue
+++ b/packages/client/src/components/google.vue
@@ -5,31 +5,18 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { ref } from 'vue';
-export default defineComponent({
- props: {
- q: {
- type: String,
- required: true,
- }
- },
- data() {
- return {
- query: null,
- };
- },
- mounted() {
- this.query = this.q;
- },
- methods: {
- search() {
- window.open(`https://www.google.com/search?q=${this.query}`, '_blank');
- }
- }
-});
+const props = defineProps<{
+ q: string;
+}>();
+
+const query = ref(props.q);
+
+const search = () => {
+ window.open(`https://www.google.com/search?q=${query.value}`, '_blank');
+};
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/image-viewer.vue b/packages/client/src/components/image-viewer.vue
index 8584b91a61..c39076df16 100644
--- a/packages/client/src/components/image-viewer.vue
+++ b/packages/client/src/components/image-viewer.vue
@@ -1,8 +1,8 @@
<template>
-<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
+<MkModal ref="modal" :z-priority="'middle'" @click="modal.close()" @closed="emit('closed')">
<div class="xubzgfga">
<header>{{ image.name }}</header>
- <img :src="image.url" :alt="image.comment" :title="image.comment" @click="$refs.modal.close()"/>
+ <img :src="image.url" :alt="image.comment" :title="image.comment" @click="modal.close()"/>
<footer>
<span>{{ image.type }}</span>
<span>{{ bytes(image.size) }}</span>
@@ -12,31 +12,23 @@
</MkModal>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
import MkModal from '@/components/ui/modal.vue';
-export default defineComponent({
- components: {
- MkModal,
- },
-
- props: {
- image: {
- type: Object,
- required: true
- },
- },
+const props = withDefaults(defineProps<{
+ image: misskey.entities.DriveFile;
+}>(), {
+});
- emits: ['closed'],
+const emit = defineEmits<{
+ (e: 'closed'): void;
+}>();
- methods: {
- bytes,
- number,
- }
-});
+const modal = $ref<InstanceType<typeof MkModal>>();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/img-with-blurhash.vue b/packages/client/src/components/img-with-blurhash.vue
index a000c699b6..06ad764403 100644
--- a/packages/client/src/components/img-with-blurhash.vue
+++ b/packages/client/src/components/img-with-blurhash.vue
@@ -5,67 +5,43 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
import { decode } from 'blurhash';
-export default defineComponent({
- props: {
- src: {
- type: String,
- required: false,
- default: null
- },
- hash: {
- type: String,
- required: true
- },
- alt: {
- type: String,
- required: false,
- default: '',
- },
- title: {
- type: String,
- required: false,
- default: null,
- },
- size: {
- type: Number,
- required: false,
- default: 64
- },
- cover: {
- type: Boolean,
- required: false,
- default: true,
- }
- },
+const props = withDefaults(defineProps<{
+ src?: string | null;
+ hash: string;
+ alt?: string;
+ title?: string | null;
+ size?: number;
+ cover?: boolean;
+}>(), {
+ src: null,
+ alt: '',
+ title: null,
+ size: 64,
+ cover: true,
+});
- data() {
- return {
- loaded: false,
- };
- },
+const canvas = $ref<HTMLCanvasElement>();
+let loaded = $ref(false);
- mounted() {
- this.draw();
- },
+function draw() {
+ if (props.hash == null) return;
+ const pixels = decode(props.hash, props.size, props.size);
+ const ctx = canvas.getContext('2d');
+ const imageData = ctx!.createImageData(props.size, props.size);
+ imageData.data.set(pixels);
+ ctx!.putImageData(imageData, 0, 0);
+}
- methods: {
- draw() {
- if (this.hash == null) return;
- const pixels = decode(this.hash, this.size, this.size);
- const ctx = (this.$refs.canvas as HTMLCanvasElement).getContext('2d');
- const imageData = ctx!.createImageData(this.size, this.size);
- imageData.data.set(pixels);
- ctx!.putImageData(imageData, 0, 0);
- },
+function onLoad() {
+ loaded = true;
+}
- onLoad() {
- this.loaded = true;
- }
- }
+onMounted(() => {
+ draw();
});
</script>
diff --git a/packages/client/src/components/instance-stats.vue b/packages/client/src/components/instance-stats.vue
index bc62998a4a..409c3a49ca 100644
--- a/packages/client/src/components/instance-stats.vue
+++ b/packages/client/src/components/instance-stats.vue
@@ -1,5 +1,5 @@
<template>
-<div class="zbcjwnqg" style="margin-top: -8px;">
+<div class="zbcjwnqg">
<div class="selects" style="display: flex;">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
<optgroup :label="$ts.federation">
@@ -29,16 +29,16 @@
<option value="day">{{ $ts.perDay }}</option>
</MkSelect>
</div>
- <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
+ <div class="chart">
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="detailed"></MkChart>
+ </div>
</div>
</template>
<script lang="ts">
-import { defineComponent, onMounted, ref, watch } from 'vue';
+import { defineComponent, ref } from 'vue';
import MkSelect from '@/components/form/select.vue';
import MkChart from '@/components/chart.vue';
-import * as os from '@/os';
-import { defaultStore } from '@/store';
export default defineComponent({
components: {
@@ -74,7 +74,10 @@ export default defineComponent({
<style lang="scss" scoped>
.zbcjwnqg {
> .selects {
- padding: 8px 16px 0 16px;
+ }
+
+ > .chart {
+ padding: 8px 0 0 0;
}
}
</style>
diff --git a/packages/client/src/components/instance-ticker.vue b/packages/client/src/components/instance-ticker.vue
index 1ce5a1c2c1..77fd8bb344 100644
--- a/packages/client/src/components/instance-ticker.vue
+++ b/packages/client/src/components/instance-ticker.vue
@@ -1,41 +1,22 @@
<template>
<div class="hpaizdrt" :style="bg">
- <img v-if="info.faviconUrl" class="icon" :src="info.faviconUrl"/>
- <span class="name">{{ info.name }}</span>
+ <img v-if="instance.faviconUrl" class="icon" :src="instance.faviconUrl"/>
+ <span class="name">{{ instance.name }}</span>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { instanceName } from '@/config';
+<script lang="ts" setup>
+import { } from 'vue';
-export default defineComponent({
- props: {
- instance: {
- type: Object,
- required: false
- },
- },
+const props = defineProps<{
+ instance: any; // TODO
+}>();
- data() {
- return {
- info: this.instance || {
- faviconUrl: '/favicon.ico',
- name: instanceName,
- themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content
- }
- }
- },
+const themeColor = props.instance.themeColor || '#777777';
- computed: {
- bg(): any {
- const themeColor = this.info.themeColor || '#777777';
- return {
- background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
- };
- }
- }
-});
+const bg = {
+ background: `linear-gradient(90deg, ${themeColor}, ${themeColor + '00'})`
+};
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/key-value.vue b/packages/client/src/components/key-value.vue
index 6a9a948ce9..da98abd77c 100644
--- a/packages/client/src/components/key-value.vue
+++ b/packages/client/src/components/key-value.vue
@@ -1,5 +1,5 @@
<template>
-<div class="alqyeyti">
+<div class="alqyeyti" :class="{ oneline }">
<div class="key">
<slot name="key"></slot>
</div>
@@ -22,6 +22,11 @@ export default defineComponent({
required: false,
default: null,
},
+ oneline: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
setup(props) {
@@ -39,10 +44,30 @@ export default defineComponent({
<style lang="scss" scoped>
.alqyeyti {
+ > .key, > .value {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
> .key {
font-size: 0.85em;
padding: 0 0 0.25em 0;
opacity: 0.75;
}
+
+ &.oneline {
+ display: flex;
+
+ > .key {
+ width: 30%;
+ font-size: 1em;
+ padding: 0 8px 0 0;
+ }
+
+ > .value {
+ width: 70%;
+ }
+ }
}
</style>
diff --git a/packages/client/src/components/link.vue b/packages/client/src/components/link.vue
index 8b8cde6510..317c931cec 100644
--- a/packages/client/src/components/link.vue
+++ b/packages/client/src/components/link.vue
@@ -1,82 +1,36 @@
<template>
-<component :is="self ? 'MkA' : 'a'" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
+<component :is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
:title="url"
- @mouseover="onMouseover"
- @mouseleave="onMouseleave"
>
<slot></slot>
<i v-if="target === '_blank'" class="fas fa-external-link-square-alt icon"></i>
</component>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import { url as local } from '@/config';
-import { isTouchUsing } from '@/scripts/touch';
+import { useTooltip } from '@/scripts/use-tooltip';
import * as os from '@/os';
-export default defineComponent({
- props: {
- url: {
- type: String,
- required: true,
- },
- rel: {
- type: String,
- required: false,
- }
- },
- data() {
- const self = this.url.startsWith(local);
- return {
- local,
- self: self,
- attr: self ? 'to' : 'href',
- target: self ? null : '_blank',
- showTimer: null,
- hideTimer: null,
- checkTimer: null,
- close: null,
- };
- },
- methods: {
- async showPreview() {
- if (!document.body.contains(this.$el)) return;
- if (this.close) return;
+const props = withDefaults(defineProps<{
+ url: string;
+ rel?: null | string;
+}>(), {
+});
- const { dispose } = await os.popup(import('@/components/url-preview-popup.vue'), {
- url: this.url,
- source: this.$el
- });
+const self = props.url.startsWith(local);
+const attr = self ? 'to' : 'href';
+const target = self ? null : '_blank';
- this.close = () => {
- dispose();
- };
+const el = $ref();
- this.checkTimer = setInterval(() => {
- if (!document.body.contains(this.$el)) this.closePreview();
- }, 1000);
- },
- closePreview() {
- if (this.close) {
- clearInterval(this.checkTimer);
- this.close();
- this.close = null;
- }
- },
- onMouseover() {
- if (isTouchUsing) return;
- clearTimeout(this.showTimer);
- clearTimeout(this.hideTimer);
- this.showTimer = setTimeout(this.showPreview, 500);
- },
- onMouseleave() {
- if (isTouchUsing) return;
- clearTimeout(this.showTimer);
- clearTimeout(this.hideTimer);
- this.hideTimer = setTimeout(this.closePreview, 500);
- }
- }
+useTooltip($$(el), (showing) => {
+ os.popup(import('@/components/url-preview-popup.vue'), {
+ showing,
+ url: props.url,
+ source: el,
+ }, {}, 'closed');
});
</script>
diff --git a/packages/client/src/components/media-banner.vue b/packages/client/src/components/media-banner.vue
index 9dbfe3d0c6..5093f11e97 100644
--- a/packages/client/src/components/media-banner.vue
+++ b/packages/client/src/components/media-banner.vue
@@ -6,7 +6,7 @@
<span>{{ $ts.clickToShow }}</span>
</div>
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
- <audio ref="audio"
+ <audio ref="audioEl"
class="audio"
:src="media.url"
:title="media.name"
@@ -25,34 +25,26 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+import * as misskey from 'misskey-js';
import { ColdDeviceStorage } from '@/store';
-export default defineComponent({
- props: {
- media: {
- type: Object,
- required: true
- }
- },
- data() {
- return {
- hide: true,
- };
- },
- mounted() {
- const audioTag = this.$refs.audio as HTMLAudioElement;
- if (audioTag) audioTag.volume = ColdDeviceStorage.get('mediaVolume');
- },
- methods: {
- volumechange() {
- const audioTag = this.$refs.audio as HTMLAudioElement;
- ColdDeviceStorage.set('mediaVolume', audioTag.volume);
- },
- },
-})
+const props = withDefaults(defineProps<{
+ media: misskey.entities.DriveFile;
+}>(), {
+});
+
+const audioEl = $ref<HTMLAudioElement | null>();
+let hide = $ref(true);
+
+function volumechange() {
+ if (audioEl) ColdDeviceStorage.set('mediaVolume', audioEl.volume);
+}
+
+onMounted(() => {
+ if (audioEl) audioEl.volume = ColdDeviceStorage.get('mediaVolume');
+});
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue
index 2970d06c97..efcbb12922 100644
--- a/packages/client/src/components/media-list.vue
+++ b/packages/client/src/components/media-list.vue
@@ -3,7 +3,7 @@
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length">
- <template v-for="media in mediaList">
+ <template v-for="media in mediaList.filter(media => previewable(media))">
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
</template>
@@ -22,6 +22,7 @@ import XBanner from './media-banner.vue';
import XImage from './media-image.vue';
import XVideo from './media-video.vue';
import * as os from '@/os';
+import { FILE_TYPE_BROWSERSAFE } from '@/const';
import { defaultStore } from '@/store';
export default defineComponent({
@@ -44,18 +45,23 @@ export default defineComponent({
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
- dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => {
- const item = {
- src: media.url,
- w: media.properties.width,
- h: media.properties.height,
- alt: media.name,
- };
- if (media.properties.orientation != null && media.properties.orientation >= 5) {
- [item.w, item.h] = [item.h, item.w];
- }
- return item;
- }),
+ dataSource: props.mediaList
+ .filter(media => {
+ if (media.type === 'image/svg+xml') return true; // svgのwebpublicはpngなのでtrue
+ return media.type.startsWith('image') && FILE_TYPE_BROWSERSAFE.includes(media.type);
+ })
+ .map(media => {
+ const item = {
+ src: media.url,
+ w: media.properties.width,
+ h: media.properties.height,
+ alt: media.name,
+ };
+ if (media.properties.orientation != null && media.properties.orientation >= 5) {
+ [item.w, item.h] = [item.h, item.w];
+ }
+ return item;
+ }),
gallery: gallery.value,
children: '.image',
thumbSelector: '.image',
@@ -99,7 +105,9 @@ export default defineComponent({
});
const previewable = (file: misskey.entities.DriveFile): boolean => {
- return file.type.startsWith('video') || file.type.startsWith('image');
+ if (file.type === 'image/svg+xml') return true; // svgのwebpublic/thumbnailはpngなのでtrue
+ // FILE_TYPE_BROWSERSAFEに適合しないものはブラウザで表示するのに不適切
+ return (file.type.startsWith('video') || file.type.startsWith('image')) && FILE_TYPE_BROWSERSAFE.includes(file.type);
};
return {
diff --git a/packages/client/src/components/media-video.vue b/packages/client/src/components/media-video.vue
index a0dc57b657..680eb27e64 100644
--- a/packages/client/src/components/media-video.vue
+++ b/packages/client/src/components/media-video.vue
@@ -22,26 +22,16 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import * as misskey from 'misskey-js';
+import { defaultStore } from '@/store';
-export default defineComponent({
- props: {
- video: {
- type: Object,
- required: true
- }
- },
- data() {
- return {
- hide: true,
- };
- },
- created() {
- this.hide = (this.$store.state.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.nsfw !== 'ignore');
- },
-});
+const props = defineProps<{
+ video: misskey.entities.DriveFile;
+}>();
+
+const hide = ref((defaultStore.state.nsfw === 'force') ? true : props.video.isSensitive && (defaultStore.state.nsfw !== 'ignore'));
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/mini-chart.vue b/packages/client/src/components/mini-chart.vue
index 2eb9ae8cbe..8c74eae876 100644
--- a/packages/client/src/components/mini-chart.vue
+++ b/packages/client/src/components/mini-chart.vue
@@ -63,10 +63,10 @@ export default defineComponent({
this.draw();
// Vueが何故かWatchを発動させない場合があるので
- this.clock = setInterval(this.draw, 1000);
+ this.clock = window.setInterval(this.draw, 1000);
},
beforeUnmount() {
- clearInterval(this.clock);
+ window.clearInterval(this.clock);
},
methods: {
draw() {
diff --git a/packages/client/src/components/modal-page-window.vue b/packages/client/src/components/modal-page-window.vue
index 3de1980820..2e17d5d030 100644
--- a/packages/client/src/components/modal-page-window.vue
+++ b/packages/client/src/components/modal-page-window.vue
@@ -153,8 +153,8 @@ export default defineComponent({
this.$refs.window.close();
},
- onContextmenu(e) {
- os.contextMenu(this.contextmenu, e);
+ onContextmenu(ev: MouseEvent) {
+ os.contextMenu(this.contextmenu, ev);
}
},
});
diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue
index 55a02f1e73..a3b30f726e 100644
--- a/packages/client/src/components/note-detailed.vue
+++ b/packages/client/src/components/note-detailed.vue
@@ -4,12 +4,13 @@
v-show="!isDeleted"
v-hotkey="keymap"
v-size="{ max: [500, 450, 350, 300] }"
+ ref="el"
class="lxwezrsl _block"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
>
- <XSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
- <XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
+ <MkNoteSub v-for="note in conversation" :key="note.id" class="reply-to-more" :note="note"/>
+ <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
<div v-if="isRenote" class="renote">
<MkAvatar class="avatar" :user="note.user"/>
<i class="fas fa-retweet"></i>
@@ -107,7 +108,7 @@
</footer>
</div>
</article>
- <XSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
+ <MkNoteSub v-for="note in replies" :key="note.id" :note="note" class="reply" :detail="true"/>
</div>
<div v-else class="_panel muted" @click="muted = false">
<I18n :src="$ts.userSaysSomething" tag="small">
@@ -120,764 +121,171 @@
</div>
</template>
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
import * as mfm from 'mfm-js';
-import { sum } from '@/scripts/array';
-import XSub from './note.sub.vue';
-import XNoteHeader from './note-header.vue';
+import * as misskey from 'misskey-js';
+import MkNoteSub from './MkNoteSub.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';
import XPoll from './poll.vue';
import XRenoteButton from './renote-button.vue';
+import MkUrlPreview from '@/components/url-preview.vue';
+import MkInstanceTicker from '@/components/instance-ticker.vue';
import { pleaseLogin } from '@/scripts/please-login';
-import { focusPrev, focusNext } from '@/scripts/focus';
-import { url } from '@/config';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user';
import { notePage } from '@/filters/note';
import * as os from '@/os';
-import { noteActions, noteViewInterruptors } from '@/store';
+import { defaultStore, noteViewInterruptors } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+import { getNoteMenu } from '@/scripts/get-note-menu';
+import { useNoteCapture } from '@/scripts/use-note-capture';
-// TODO: note.vueとほぼ同じなので共通化したい
-export default defineComponent({
- components: {
- XSub,
- XNoteHeader,
- XNoteSimple,
- XReactionsViewer,
- XMediaList,
- XCwButton,
- XPoll,
- XRenoteButton,
- MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
- MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
- },
+const props = defineProps<{
+ note: misskey.entities.Note;
+ pinned?: boolean;
+}>();
- inject: {
- inChannel: {
- default: null
- },
- },
+const inChannel = inject('inChannel', null);
- props: {
- note: {
- type: Object,
- required: true
- },
- },
+const isRenote = (
+ props.note.renote != null &&
+ props.note.text == null &&
+ props.note.fileIds.length === 0 &&
+ props.note.poll == null
+);
- emits: ['update:note'],
+const el = ref<HTMLElement>();
+const menuButton = ref<HTMLElement>();
+const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
+const renoteTime = ref<HTMLElement>();
+const reactButton = ref<HTMLElement>();
+let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note);
+const isMyRenote = $i && ($i.id === props.note.userId);
+const showContent = ref(false);
+const isDeleted = ref(false);
+const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
+const translation = ref(null);
+const translating = ref(false);
+const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
+const conversation = ref<misskey.entities.Note[]>([]);
+const replies = ref<misskey.entities.Note[]>([]);
- data() {
- return {
- connection: null,
- conversation: [],
- replies: [],
- showContent: false,
- isDeleted: false,
- muted: false,
- translation: null,
- translating: false,
- notePage,
- };
- },
+const keymap = {
+ 'r': () => reply(true),
+ 'e|a|plus': () => react(true),
+ 'q': () => renoteButton.value.renote(true),
+ 'esc': blur,
+ 'm|o': () => menu(true),
+ 's': () => showContent.value != showContent.value,
+};
- computed: {
- rs() {
- return this.$store.state.reactions;
- },
- keymap(): any {
- return {
- 'r': () => this.reply(true),
- 'e|a|plus': () => this.react(true),
- 'q': () => this.$refs.renoteButton.renote(true),
- 'f|b': this.favorite,
- 'delete|ctrl+d': this.del,
- 'ctrl+q': this.renoteDirectly,
- 'up|k|shift+tab': this.focusBefore,
- 'down|j|tab': this.focusAfter,
- 'esc': this.blur,
- 'm|o': () => this.menu(true),
- 's': this.toggleShowContent,
- '1': () => this.reactDirectly(this.rs[0]),
- '2': () => this.reactDirectly(this.rs[1]),
- '3': () => this.reactDirectly(this.rs[2]),
- '4': () => this.reactDirectly(this.rs[3]),
- '5': () => this.reactDirectly(this.rs[4]),
- '6': () => this.reactDirectly(this.rs[5]),
- '7': () => this.reactDirectly(this.rs[6]),
- '8': () => this.reactDirectly(this.rs[7]),
- '9': () => this.reactDirectly(this.rs[8]),
- '0': () => this.reactDirectly(this.rs[9]),
- };
- },
-
- isRenote(): boolean {
- return (this.note.renote &&
- this.note.text == null &&
- this.note.fileIds.length == 0 &&
- this.note.poll == null);
- },
-
- appearNote(): any {
- return this.isRenote ? this.note.renote : this.note;
- },
-
- isMyNote(): boolean {
- return this.$i && (this.$i.id === this.appearNote.userId);
- },
-
- isMyRenote(): boolean {
- return this.$i && (this.$i.id === this.note.userId);
- },
-
- reactionsCount(): number {
- return this.appearNote.reactions
- ? sum(Object.values(this.appearNote.reactions))
- : 0;
- },
-
- urls(): string[] {
- if (this.appearNote.text) {
- return extractUrlFromMfm(mfm.parse(this.appearNote.text));
- } else {
- return null;
- }
- },
-
- showTicker() {
- if (this.$store.state.instanceTicker === 'always') return true;
- if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
- return false;
- }
- },
-
- async created() {
- if (this.$i) {
- this.connection = os.stream;
- }
-
- this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
+useNoteCapture({
+ appearNote: $$(appearNote),
+ rootEl: el,
+});
- // plugin
- if (noteViewInterruptors.length > 0) {
- let result = this.note;
- for (const interruptor of noteViewInterruptors) {
- result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
- }
- this.$emit('update:note', Object.freeze(result));
- }
+function reply(viaKeyboard = false): void {
+ pleaseLogin();
+ os.post({
+ reply: appearNote,
+ animation: !viaKeyboard,
+ }, () => {
+ focus();
+ });
+}
- os.api('notes/children', {
- noteId: this.appearNote.id,
- limit: 30
- }).then(replies => {
- this.replies = replies;
+function react(viaKeyboard = false): void {
+ pleaseLogin();
+ blur();
+ reactionPicker.show(reactButton.value, reaction => {
+ os.api('notes/reactions/create', {
+ noteId: appearNote.id,
+ reaction: reaction
});
+ }, () => {
+ focus();
+ });
+}
- if (this.appearNote.replyId) {
- os.api('notes/conversation', {
- noteId: this.appearNote.replyId
- }).then(conversation => {
- this.conversation = conversation.reverse();
- });
- }
- },
-
- mounted() {
- this.capture(true);
-
- if (this.$i) {
- this.connection.on('_connected_', this.onStreamConnected);
- }
- },
-
- beforeUnmount() {
- this.decapture(true);
+function undoReact(note): void {
+ const oldReaction = note.myReaction;
+ if (!oldReaction) return;
+ os.api('notes/reactions/delete', {
+ noteId: note.id
+ });
+}
- if (this.$i) {
- this.connection.off('_connected_', this.onStreamConnected);
+function onContextmenu(ev: MouseEvent): void {
+ const isLink = (el: HTMLElement) => {
+ if (el.tagName === 'A') return true;
+ if (el.parentElement) {
+ return isLink(el.parentElement);
}
- },
-
- methods: {
- updateAppearNote(v) {
- this.$emit('update:note', Object.freeze(this.isRenote ? {
- ...this.note,
- renote: {
- ...this.note.renote,
- ...v
- }
- } : {
- ...this.note,
- ...v
- }));
- },
-
- readPromo() {
- os.api('promo/read', {
- noteId: this.appearNote.id
- });
- this.isDeleted = true;
- },
-
- capture(withHandler = false) {
- if (this.$i) {
- // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
- this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
- if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
- }
- },
-
- decapture(withHandler = false) {
- if (this.$i) {
- this.connection.send('un', {
- id: this.appearNote.id
- });
- if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
- }
- },
-
- onStreamConnected() {
- this.capture();
- },
-
- onStreamNoteUpdated(data) {
- const { type, id, body } = data;
-
- if (id !== this.appearNote.id) return;
+ };
+ if (isLink(ev.target)) return;
+ if (window.getSelection().toString() !== '') return;
- switch (type) {
- case 'reacted': {
- const reaction = body.reaction;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- if (body.emoji) {
- const emojis = this.appearNote.emojis || [];
- if (!emojis.includes(body.emoji)) {
- n.emojis = [...emojis, body.emoji];
- }
- }
-
- // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
- const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
- // Increment the count
- n.reactions = {
- ...this.appearNote.reactions,
- [reaction]: currentCount + 1
- };
-
- if (body.userId === this.$i.id) {
- n.myReaction = reaction;
- }
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'unreacted': {
- const reaction = body.reaction;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
- const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
- // Decrement the count
- n.reactions = {
- ...this.appearNote.reactions,
- [reaction]: Math.max(0, currentCount - 1)
- };
-
- if (body.userId === this.$i.id) {
- n.myReaction = null;
- }
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'pollVoted': {
- const choice = body.choice;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- const choices = [...this.appearNote.poll.choices];
- choices[choice] = {
- ...choices[choice],
- votes: choices[choice].votes + 1,
- ...(body.userId === this.$i.id ? {
- isVoted: true
- } : {})
- };
-
- n.poll = {
- ...this.appearNote.poll,
- choices: choices
- };
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'deleted': {
- this.isDeleted = true;
- break;
- }
- }
- },
-
- reply(viaKeyboard = false) {
- pleaseLogin();
- os.post({
- reply: this.appearNote,
- animation: !viaKeyboard,
- }, () => {
- this.focus();
- });
- },
-
- renoteDirectly() {
- os.apiWithDialog('notes/create', {
- renoteId: this.appearNote.id
- }, undefined, (res: any) => {
- os.alert({
- type: 'success',
- text: this.$ts.renoted,
- });
- }, (e: Error) => {
- if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
- os.alert({
- type: 'error',
- text: this.$ts.cantRenote,
- });
- } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
- os.alert({
- type: 'error',
- text: this.$ts.cantReRenote,
- });
- }
- });
- },
-
- react(viaKeyboard = false) {
- pleaseLogin();
- this.blur();
- reactionPicker.show(this.$refs.reactButton, reaction => {
- os.api('notes/reactions/create', {
- noteId: this.appearNote.id,
- reaction: reaction
- });
- }, () => {
- this.focus();
- });
- },
-
- reactDirectly(reaction) {
- os.api('notes/reactions/create', {
- noteId: this.appearNote.id,
- reaction: reaction
- });
- },
-
- undoReact(note) {
- const oldReaction = note.myReaction;
- if (!oldReaction) return;
- os.api('notes/reactions/delete', {
- noteId: note.id
- });
- },
-
- favorite() {
- pleaseLogin();
- os.apiWithDialog('notes/favorites/create', {
- noteId: this.appearNote.id
- }, undefined, (res: any) => {
- os.alert({
- type: 'success',
- text: this.$ts.favorited,
- });
- }, (e: Error) => {
- if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
- os.alert({
- type: 'error',
- text: this.$ts.alreadyFavorited,
- });
- } else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
- os.alert({
- type: 'error',
- text: this.$ts.cantFavorite,
- });
- }
- });
- },
-
- del() {
- os.confirm({
- type: 'warning',
- text: this.$ts.noteDeleteConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
-
- os.api('notes/delete', {
- noteId: this.appearNote.id
- });
- });
- },
-
- delEdit() {
- os.confirm({
- type: 'warning',
- text: this.$ts.deleteAndEditConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
-
- os.api('notes/delete', {
- noteId: this.appearNote.id
- });
-
- os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
- });
- },
-
- toggleFavorite(favorite: boolean) {
- os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
- noteId: this.appearNote.id
- });
- },
-
- toggleWatch(watch: boolean) {
- os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
- noteId: this.appearNote.id
- });
- },
-
- toggleThreadMute(mute: boolean) {
- os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
- noteId: this.appearNote.id
- });
- },
-
- getMenu() {
- let menu;
- if (this.$i) {
- const statePromise = os.api('notes/state', {
- noteId: this.appearNote.id
- });
-
- menu = [{
- icon: 'fas fa-copy',
- text: this.$ts.copyContent,
- action: this.copyContent
- }, {
- icon: 'fas fa-link',
- text: this.$ts.copyLink,
- action: this.copyLink
- }, (this.appearNote.url || this.appearNote.uri) ? {
- icon: 'fas fa-external-link-square-alt',
- text: this.$ts.showOnRemote,
- action: () => {
- window.open(this.appearNote.url || this.appearNote.uri, '_blank');
- }
- } : undefined,
- {
- icon: 'fas fa-share-alt',
- text: this.$ts.share,
- action: this.share
- },
- this.$instance.translatorAvailable ? {
- icon: 'fas fa-language',
- text: this.$ts.translate,
- action: this.translate
- } : undefined,
- null,
- statePromise.then(state => state.isFavorited ? {
- icon: 'fas fa-star',
- text: this.$ts.unfavorite,
- action: () => this.toggleFavorite(false)
- } : {
- icon: 'fas fa-star',
- text: this.$ts.favorite,
- action: () => this.toggleFavorite(true)
- }),
- {
- icon: 'fas fa-paperclip',
- text: this.$ts.clip,
- action: () => this.clip()
- },
- (this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
- icon: 'fas fa-eye-slash',
- text: this.$ts.unwatch,
- action: () => this.toggleWatch(false)
- } : {
- icon: 'fas fa-eye',
- text: this.$ts.watch,
- action: () => this.toggleWatch(true)
- }) : undefined,
- statePromise.then(state => state.isMutedThread ? {
- icon: 'fas fa-comment-slash',
- text: this.$ts.unmuteThread,
- action: () => this.toggleThreadMute(false)
- } : {
- icon: 'fas fa-comment-slash',
- text: this.$ts.muteThread,
- action: () => this.toggleThreadMute(true)
- }),
- this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
- icon: 'fas fa-thumbtack',
- text: this.$ts.unpin,
- action: () => this.togglePin(false)
- } : {
- icon: 'fas fa-thumbtack',
- text: this.$ts.pin,
- action: () => this.togglePin(true)
- } : undefined,
- /*...(this.$i.isModerator || this.$i.isAdmin ? [
- null,
- {
- icon: 'fas fa-bullhorn',
- text: this.$ts.promote,
- action: this.promote
- }]
- : []
- ),*/
- ...(this.appearNote.userId != this.$i.id ? [
- null,
- {
- icon: 'fas fa-exclamation-circle',
- text: this.$ts.reportAbuse,
- action: () => {
- const u = `${url}/notes/${this.appearNote.id}`;
- os.popup(import('@/components/abuse-report-window.vue'), {
- user: this.appearNote.user,
- initialComment: `Note: ${u}\n-----\n`
- }, {}, 'closed');
- }
- }]
- : []
- ),
- ...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
- null,
- this.appearNote.userId == this.$i.id ? {
- icon: 'fas fa-edit',
- text: this.$ts.deleteAndEdit,
- action: this.delEdit
- } : undefined,
- {
- icon: 'fas fa-trash-alt',
- text: this.$ts.delete,
- danger: true,
- action: this.del
- }]
- : []
- )]
- .filter(x => x !== undefined);
- } else {
- menu = [{
- icon: 'fas fa-copy',
- text: this.$ts.copyContent,
- action: this.copyContent
- }, {
- icon: 'fas fa-link',
- text: this.$ts.copyLink,
- action: this.copyLink
- }, (this.appearNote.url || this.appearNote.uri) ? {
- icon: 'fas fa-external-link-square-alt',
- text: this.$ts.showOnRemote,
- action: () => {
- window.open(this.appearNote.url || this.appearNote.uri, '_blank');
- }
- } : undefined]
- .filter(x => x !== undefined);
- }
-
- if (noteActions.length > 0) {
- menu = menu.concat([null, ...noteActions.map(action => ({
- icon: 'fas fa-plug',
- text: action.title,
- action: () => {
- action.handler(this.appearNote);
- }
- }))]);
- }
-
- return menu;
- },
-
- onContextmenu(e) {
- const isLink = (el: HTMLElement) => {
- if (el.tagName === 'A') return true;
- if (el.parentElement) {
- return isLink(el.parentElement);
- }
- };
- if (isLink(e.target)) return;
- if (window.getSelection().toString() !== '') return;
-
- if (this.$store.state.useReactionPickerForContextMenu) {
- e.preventDefault();
- this.react();
- } else {
- os.contextMenu(this.getMenu(), e).then(this.focus);
- }
- },
-
- menu(viaKeyboard = false) {
- os.popupMenu(this.getMenu(), this.$refs.menuButton, {
- viaKeyboard
- }).then(this.focus);
- },
-
- showRenoteMenu(viaKeyboard = false) {
- if (!this.isMyRenote) return;
- os.popupMenu([{
- text: this.$ts.unrenote,
- icon: 'fas fa-trash-alt',
- danger: true,
- action: () => {
- os.api('notes/delete', {
- noteId: this.note.id
- });
- this.isDeleted = true;
- }
- }], this.$refs.renoteTime, {
- viaKeyboard: viaKeyboard
- });
- },
-
- toggleShowContent() {
- this.showContent = !this.showContent;
- },
-
- copyContent() {
- copyToClipboard(this.appearNote.text);
- os.success();
- },
-
- copyLink() {
- copyToClipboard(`${url}/notes/${this.appearNote.id}`);
- os.success();
- },
-
- togglePin(pin: boolean) {
- os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
- noteId: this.appearNote.id
- }, undefined, null, e => {
- if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
- os.alert({
- type: 'error',
- text: this.$ts.pinLimitExceeded
- });
- }
- });
- },
-
- async clip() {
- const clips = await os.api('clips/list');
- os.popupMenu([{
- icon: 'fas fa-plus',
- text: this.$ts.createNew,
- action: async () => {
- const { canceled, result } = await os.form(this.$ts.createNewClip, {
- name: {
- type: 'string',
- label: this.$ts.name
- },
- description: {
- type: 'string',
- required: false,
- multiline: true,
- label: this.$ts.description
- },
- isPublic: {
- type: 'boolean',
- label: this.$ts.public,
- default: false
- }
- });
- if (canceled) return;
-
- const clip = await os.apiWithDialog('clips/create', result);
-
- os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
- }
- }, null, ...clips.map(clip => ({
- text: clip.name,
- action: () => {
- os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
- }
- }))], this.$refs.menuButton, {
- }).then(this.focus);
- },
-
- async promote() {
- const { canceled, result: days } = await os.inputNumber({
- title: this.$ts.numberOfDays,
- });
-
- if (canceled) return;
-
- os.apiWithDialog('admin/promo/create', {
- noteId: this.appearNote.id,
- expiresAt: Date.now() + (86400000 * days)
- });
- },
+ if (defaultStore.state.useReactionPickerForContextMenu) {
+ ev.preventDefault();
+ react();
+ } else {
+ os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
+ }
+}
- share() {
- navigator.share({
- title: this.$t('noteOf', { user: this.appearNote.user.name }),
- text: this.appearNote.text,
- url: `${url}/notes/${this.appearNote.id}`
- });
- },
+function menu(viaKeyboard = false): void {
+ os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, {
+ viaKeyboard
+ }).then(focus);
+}
- async translate() {
- if (this.translation != null) return;
- this.translating = true;
- const res = await os.api('notes/translate', {
- noteId: this.appearNote.id,
- targetLang: localStorage.getItem('lang') || navigator.language,
+function showRenoteMenu(viaKeyboard = false): void {
+ if (!isMyRenote) return;
+ os.popupMenu([{
+ text: i18n.locale.unrenote,
+ icon: 'fas fa-trash-alt',
+ danger: true,
+ action: () => {
+ os.api('notes/delete', {
+ noteId: props.note.id
});
- this.translating = false;
- this.translation = res;
- },
-
- focus() {
- this.$el.focus();
- },
-
- blur() {
- this.$el.blur();
- },
+ isDeleted.value = true;
+ }
+ }], renoteTime.value, {
+ viaKeyboard: viaKeyboard
+ });
+}
- focusBefore() {
- focusPrev(this.$el);
- },
+function focus() {
+ el.value.focus();
+}
- focusAfter() {
- focusNext(this.$el);
- },
+function blur() {
+ el.value.blur();
+}
- userPage
- }
+os.api('notes/children', {
+ noteId: appearNote.id,
+ limit: 30
+}).then(res => {
+ replies.value = res;
});
+
+if (appearNote.replyId) {
+ os.api('notes/conversation', {
+ noteId: appearNote.replyId
+ }).then(res => {
+ conversation.value = res.reverse();
+ });
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/note-header.vue b/packages/client/src/components/note-header.vue
index 26e725c6b8..56a3a37e75 100644
--- a/packages/client/src/components/note-header.vue
+++ b/packages/client/src/components/note-header.vue
@@ -19,30 +19,16 @@
</header>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user';
-import * as os from '@/os';
-export default defineComponent({
- props: {
- note: {
- type: Object,
- required: true
- },
- },
-
- data() {
- return {
- };
- },
-
- methods: {
- notePage,
- userPage
- }
-});
+defineProps<{
+ note: misskey.entities.Note;
+ pinned?: boolean;
+}>();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/note-preview.vue b/packages/client/src/components/note-preview.vue
index bdcb8d5eed..a78b499654 100644
--- a/packages/client/src/components/note-preview.vue
+++ b/packages/client/src/components/note-preview.vue
@@ -14,20 +14,12 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
-export default defineComponent({
- components: {
- },
-
- props: {
- text: {
- type: String,
- required: true
- }
- },
-});
+const props = defineProps<{
+ text: string;
+}>();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/note-simple.vue b/packages/client/src/components/note-simple.vue
index 135f06602d..c6907787b5 100644
--- a/packages/client/src/components/note-simple.vue
+++ b/packages/client/src/components/note-simple.vue
@@ -9,40 +9,26 @@
<XCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent" class="content">
- <XSubNote-content class="text" :note="note"/>
+ <MkNoteSubNoteContent class="text" :note="note"/>
</div>
</div>
</div>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
import XNoteHeader from './note-header.vue';
-import XSubNoteContent from './sub-note-content.vue';
+import MkNoteSubNoteContent from './sub-note-content.vue';
import XCwButton from './cw-button.vue';
-import * as os from '@/os';
-export default defineComponent({
- components: {
- XNoteHeader,
- XSubNoteContent,
- XCwButton,
- },
+const props = defineProps<{
+ note: misskey.entities.Note;
+ pinned?: boolean;
+}>();
- props: {
- note: {
- type: Object,
- required: true
- }
- },
-
- data() {
- return {
- showContent: false
- };
- }
-});
+const showContent = $ref(false);
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue
index c4040388a9..fc89c2777b 100644
--- a/packages/client/src/components/note.vue
+++ b/packages/client/src/components/note.vue
@@ -2,20 +2,21 @@
<div
v-if="!muted"
v-show="!isDeleted"
+ ref="el"
v-hotkey="keymap"
v-size="{ max: [500, 450, 350, 300] }"
class="tkcbzcuz"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
>
- <XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
- <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
- <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
- <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
+ <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
+ <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.locale.pinnedNote }}</div>
+ <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.locale.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.locale.hideThisNote }} <i class="fas fa-times"></i></button></div>
+ <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.locale.featured }}</div>
<div v-if="isRenote" class="renote">
<MkAvatar class="avatar" :user="note.user"/>
<i class="fas fa-retweet"></i>
- <I18n :src="$ts.renotedBy" tag="span">
+ <I18n :src="i18n.locale.renotedBy" tag="span">
<template #user>
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
<MkUserName :user="note.user"/>
@@ -47,7 +48,7 @@
</p>
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
<div class="text">
- <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
+ <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.locale.private }})</span>
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
<a v-if="appearNote.renote != null" class="rp">RN:</a>
@@ -66,7 +67,7 @@
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
<button v-if="collapsed" class="fade _button" @click="collapsed = false">
- <span>{{ $ts.showMore }}</span>
+ <span>{{ i18n.locale.showMore }}</span>
</button>
</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>
@@ -93,7 +94,7 @@
</article>
</div>
<div v-else class="muted" @click="muted = false">
- <I18n :src="$ts.userSaysSomething" tag="small">
+ <I18n :src="i18n.locale.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
<MkUserName :user="appearNote.user"/>
@@ -103,11 +104,11 @@
</div>
</template>
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
import * as mfm from 'mfm-js';
-import { sum } from '@/scripts/array';
-import XSub from './note.sub.vue';
+import * as misskey from 'misskey-js';
+import MkNoteSub from './MkNoteSub.vue';
import XNoteHeader from './note-header.vue';
import XNoteSimple from './note-simple.vue';
import XReactionsViewer from './reactions-viewer.vue';
@@ -115,744 +116,164 @@ import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue';
import XPoll from './poll.vue';
import XRenoteButton from './renote-button.vue';
+import MkUrlPreview from '@/components/url-preview.vue';
+import MkInstanceTicker from '@/components/instance-ticker.vue';
import { pleaseLogin } from '@/scripts/please-login';
import { focusPrev, focusNext } from '@/scripts/focus';
-import { url } from '@/config';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
import { checkWordMute } from '@/scripts/check-word-mute';
import { userPage } from '@/filters/user';
import * as os from '@/os';
-import { noteActions, noteViewInterruptors } from '@/store';
+import { defaultStore, noteViewInterruptors } from '@/store';
import { reactionPicker } from '@/scripts/reaction-picker';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+import { getNoteMenu } from '@/scripts/get-note-menu';
+import { useNoteCapture } from '@/scripts/use-note-capture';
-export default defineComponent({
- components: {
- XSub,
- XNoteHeader,
- XNoteSimple,
- XReactionsViewer,
- XMediaList,
- XCwButton,
- XPoll,
- XRenoteButton,
- MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
- MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
- },
+const props = defineProps<{
+ note: misskey.entities.Note;
+ pinned?: boolean;
+}>();
- inject: {
- inChannel: {
- default: null
- },
- },
+const inChannel = inject('inChannel', null);
- props: {
- note: {
- type: Object,
- required: true
- },
- pinned: {
- type: Boolean,
- required: false,
- default: false
- },
- },
+const isRenote = (
+ props.note.renote != null &&
+ props.note.text == null &&
+ props.note.fileIds.length === 0 &&
+ props.note.poll == null
+);
- emits: ['update:note'],
+const el = ref<HTMLElement>();
+const menuButton = ref<HTMLElement>();
+const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
+const renoteTime = ref<HTMLElement>();
+const reactButton = ref<HTMLElement>();
+let appearNote = $ref(isRenote ? props.note.renote as misskey.entities.Note : props.note);
+const isMyRenote = $i && ($i.id === props.note.userId);
+const showContent = ref(false);
+const collapsed = ref(appearNote.cw == null && appearNote.text != null && (
+ (appearNote.text.split('\n').length > 9) ||
+ (appearNote.text.length > 500)
+));
+const isDeleted = ref(false);
+const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
+const translation = ref(null);
+const translating = ref(false);
+const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
+const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
- data() {
- return {
- connection: null,
- replies: [],
- showContent: false,
- collapsed: false,
- isDeleted: false,
- muted: false,
- translation: null,
- translating: false,
- };
- },
+const keymap = {
+ 'r': () => reply(true),
+ 'e|a|plus': () => react(true),
+ 'q': () => renoteButton.value.renote(true),
+ 'up|k|shift+tab': focusBefore,
+ 'down|j|tab': focusAfter,
+ 'esc': blur,
+ 'm|o': () => menu(true),
+ 's': () => showContent.value != showContent.value,
+};
- computed: {
- rs() {
- return this.$store.state.reactions;
- },
- keymap(): any {
- return {
- 'r': () => this.reply(true),
- 'e|a|plus': () => this.react(true),
- 'q': () => this.$refs.renoteButton.renote(true),
- 'f|b': this.favorite,
- 'delete|ctrl+d': this.del,
- 'ctrl+q': this.renoteDirectly,
- 'up|k|shift+tab': this.focusBefore,
- 'down|j|tab': this.focusAfter,
- 'esc': this.blur,
- 'm|o': () => this.menu(true),
- 's': this.toggleShowContent,
- '1': () => this.reactDirectly(this.rs[0]),
- '2': () => this.reactDirectly(this.rs[1]),
- '3': () => this.reactDirectly(this.rs[2]),
- '4': () => this.reactDirectly(this.rs[3]),
- '5': () => this.reactDirectly(this.rs[4]),
- '6': () => this.reactDirectly(this.rs[5]),
- '7': () => this.reactDirectly(this.rs[6]),
- '8': () => this.reactDirectly(this.rs[7]),
- '9': () => this.reactDirectly(this.rs[8]),
- '0': () => this.reactDirectly(this.rs[9]),
- };
- },
-
- isRenote(): boolean {
- return (this.note.renote &&
- this.note.text == null &&
- this.note.fileIds.length == 0 &&
- this.note.poll == null);
- },
-
- appearNote(): any {
- return this.isRenote ? this.note.renote : this.note;
- },
-
- isMyNote(): boolean {
- return this.$i && (this.$i.id === this.appearNote.userId);
- },
-
- isMyRenote(): boolean {
- return this.$i && (this.$i.id === this.note.userId);
- },
-
- reactionsCount(): number {
- return this.appearNote.reactions
- ? sum(Object.values(this.appearNote.reactions))
- : 0;
- },
-
- urls(): string[] {
- if (this.appearNote.text) {
- return extractUrlFromMfm(mfm.parse(this.appearNote.text));
- } else {
- return null;
- }
- },
-
- showTicker() {
- if (this.$store.state.instanceTicker === 'always') return true;
- if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
- return false;
- }
- },
-
- async created() {
- if (this.$i) {
- this.connection = os.stream;
- }
-
- this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
- (this.appearNote.text.split('\n').length > 9) ||
- (this.appearNote.text.length > 500)
- );
- this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
-
- // plugin
- if (noteViewInterruptors.length > 0) {
- let result = this.note;
- for (const interruptor of noteViewInterruptors) {
- result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
- }
- this.$emit('update:note', Object.freeze(result));
- }
- },
+useNoteCapture({
+ appearNote: $$(appearNote),
+ rootEl: el,
+});
- mounted() {
- this.capture(true);
+function reply(viaKeyboard = false): void {
+ pleaseLogin();
+ os.post({
+ reply: appearNote,
+ animation: !viaKeyboard,
+ }, () => {
+ focus();
+ });
+}
- if (this.$i) {
- this.connection.on('_connected_', this.onStreamConnected);
- }
- },
+function react(viaKeyboard = false): void {
+ pleaseLogin();
+ blur();
+ reactionPicker.show(reactButton.value, reaction => {
+ os.api('notes/reactions/create', {
+ noteId: appearNote.id,
+ reaction: reaction
+ });
+ }, () => {
+ focus();
+ });
+}
- beforeUnmount() {
- this.decapture(true);
+function undoReact(note): void {
+ const oldReaction = note.myReaction;
+ if (!oldReaction) return;
+ os.api('notes/reactions/delete', {
+ noteId: note.id
+ });
+}
- if (this.$i) {
- this.connection.off('_connected_', this.onStreamConnected);
+function onContextmenu(ev: MouseEvent): void {
+ const isLink = (el: HTMLElement) => {
+ if (el.tagName === 'A') return true;
+ if (el.parentElement) {
+ return isLink(el.parentElement);
}
- },
-
- methods: {
- updateAppearNote(v) {
- this.$emit('update:note', Object.freeze(this.isRenote ? {
- ...this.note,
- renote: {
- ...this.note.renote,
- ...v
- }
- } : {
- ...this.note,
- ...v
- }));
- },
-
- readPromo() {
- os.api('promo/read', {
- noteId: this.appearNote.id
- });
- this.isDeleted = true;
- },
-
- capture(withHandler = false) {
- if (this.$i) {
- // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
- this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
- if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
- }
- },
+ };
+ if (isLink(ev.target)) return;
+ if (window.getSelection().toString() !== '') return;
- decapture(withHandler = false) {
- if (this.$i) {
- this.connection.send('un', {
- id: this.appearNote.id
- });
- if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
- }
- },
-
- onStreamConnected() {
- this.capture();
- },
-
- onStreamNoteUpdated(data) {
- const { type, id, body } = data;
-
- if (id !== this.appearNote.id) return;
-
- switch (type) {
- case 'reacted': {
- const reaction = body.reaction;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- if (body.emoji) {
- const emojis = this.appearNote.emojis || [];
- if (!emojis.includes(body.emoji)) {
- n.emojis = [...emojis, body.emoji];
- }
- }
-
- // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
- const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
- // Increment the count
- n.reactions = {
- ...this.appearNote.reactions,
- [reaction]: currentCount + 1
- };
-
- if (body.userId === this.$i.id) {
- n.myReaction = reaction;
- }
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'unreacted': {
- const reaction = body.reaction;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
- const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
- // Decrement the count
- n.reactions = {
- ...this.appearNote.reactions,
- [reaction]: Math.max(0, currentCount - 1)
- };
-
- if (body.userId === this.$i.id) {
- n.myReaction = null;
- }
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'pollVoted': {
- const choice = body.choice;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- const choices = [...this.appearNote.poll.choices];
- choices[choice] = {
- ...choices[choice],
- votes: choices[choice].votes + 1,
- ...(body.userId === this.$i.id ? {
- isVoted: true
- } : {})
- };
-
- n.poll = {
- ...this.appearNote.poll,
- choices: choices
- };
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'deleted': {
- this.isDeleted = true;
- break;
- }
- }
- },
-
- reply(viaKeyboard = false) {
- pleaseLogin();
- os.post({
- reply: this.appearNote,
- animation: !viaKeyboard,
- }, () => {
- this.focus();
- });
- },
-
- renoteDirectly() {
- os.apiWithDialog('notes/create', {
- renoteId: this.appearNote.id
- }, undefined, (res: any) => {
- os.alert({
- type: 'success',
- text: this.$ts.renoted,
- });
- }, (e: Error) => {
- if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
- os.alert({
- type: 'error',
- text: this.$ts.cantRenote,
- });
- } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
- os.alert({
- type: 'error',
- text: this.$ts.cantReRenote,
- });
- }
- });
- },
-
- react(viaKeyboard = false) {
- pleaseLogin();
- this.blur();
- reactionPicker.show(this.$refs.reactButton, reaction => {
- os.api('notes/reactions/create', {
- noteId: this.appearNote.id,
- reaction: reaction
- });
- }, () => {
- this.focus();
- });
- },
-
- reactDirectly(reaction) {
- os.api('notes/reactions/create', {
- noteId: this.appearNote.id,
- reaction: reaction
- });
- },
-
- undoReact(note) {
- const oldReaction = note.myReaction;
- if (!oldReaction) return;
- os.api('notes/reactions/delete', {
- noteId: note.id
- });
- },
-
- favorite() {
- pleaseLogin();
- os.apiWithDialog('notes/favorites/create', {
- noteId: this.appearNote.id
- }, undefined, (res: any) => {
- os.alert({
- type: 'success',
- text: this.$ts.favorited,
- });
- }, (e: Error) => {
- if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
- os.alert({
- type: 'error',
- text: this.$ts.alreadyFavorited,
- });
- } else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
- os.alert({
- type: 'error',
- text: this.$ts.cantFavorite,
- });
- }
- });
- },
-
- del() {
- os.confirm({
- type: 'warning',
- text: this.$ts.noteDeleteConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
-
- os.api('notes/delete', {
- noteId: this.appearNote.id
- });
- });
- },
-
- delEdit() {
- os.confirm({
- type: 'warning',
- text: this.$ts.deleteAndEditConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
-
- os.api('notes/delete', {
- noteId: this.appearNote.id
- });
-
- os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
- });
- },
-
- toggleFavorite(favorite: boolean) {
- os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
- noteId: this.appearNote.id
- });
- },
-
- toggleWatch(watch: boolean) {
- os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
- noteId: this.appearNote.id
- });
- },
-
- toggleThreadMute(mute: boolean) {
- os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
- noteId: this.appearNote.id
- });
- },
-
- getMenu() {
- let menu;
- if (this.$i) {
- const statePromise = os.api('notes/state', {
- noteId: this.appearNote.id
- });
-
- menu = [{
- icon: 'fas fa-copy',
- text: this.$ts.copyContent,
- action: this.copyContent
- }, {
- icon: 'fas fa-link',
- text: this.$ts.copyLink,
- action: this.copyLink
- }, (this.appearNote.url || this.appearNote.uri) ? {
- icon: 'fas fa-external-link-square-alt',
- text: this.$ts.showOnRemote,
- action: () => {
- window.open(this.appearNote.url || this.appearNote.uri, '_blank');
- }
- } : undefined,
- {
- icon: 'fas fa-share-alt',
- text: this.$ts.share,
- action: this.share
- },
- this.$instance.translatorAvailable ? {
- icon: 'fas fa-language',
- text: this.$ts.translate,
- action: this.translate
- } : undefined,
- null,
- statePromise.then(state => state.isFavorited ? {
- icon: 'fas fa-star',
- text: this.$ts.unfavorite,
- action: () => this.toggleFavorite(false)
- } : {
- icon: 'fas fa-star',
- text: this.$ts.favorite,
- action: () => this.toggleFavorite(true)
- }),
- {
- icon: 'fas fa-paperclip',
- text: this.$ts.clip,
- action: () => this.clip()
- },
- (this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
- icon: 'fas fa-eye-slash',
- text: this.$ts.unwatch,
- action: () => this.toggleWatch(false)
- } : {
- icon: 'fas fa-eye',
- text: this.$ts.watch,
- action: () => this.toggleWatch(true)
- }) : undefined,
- statePromise.then(state => state.isMutedThread ? {
- icon: 'fas fa-comment-slash',
- text: this.$ts.unmuteThread,
- action: () => this.toggleThreadMute(false)
- } : {
- icon: 'fas fa-comment-slash',
- text: this.$ts.muteThread,
- action: () => this.toggleThreadMute(true)
- }),
- this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
- icon: 'fas fa-thumbtack',
- text: this.$ts.unpin,
- action: () => this.togglePin(false)
- } : {
- icon: 'fas fa-thumbtack',
- text: this.$ts.pin,
- action: () => this.togglePin(true)
- } : undefined,
- /*
- ...(this.$i.isModerator || this.$i.isAdmin ? [
- null,
- {
- icon: 'fas fa-bullhorn',
- text: this.$ts.promote,
- action: this.promote
- }]
- : []
- ),*/
- ...(this.appearNote.userId != this.$i.id ? [
- null,
- {
- icon: 'fas fa-exclamation-circle',
- text: this.$ts.reportAbuse,
- action: () => {
- const u = `${url}/notes/${this.appearNote.id}`;
- os.popup(import('@/components/abuse-report-window.vue'), {
- user: this.appearNote.user,
- initialComment: `Note: ${u}\n-----\n`
- }, {}, 'closed');
- }
- }]
- : []
- ),
- ...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
- null,
- this.appearNote.userId == this.$i.id ? {
- icon: 'fas fa-edit',
- text: this.$ts.deleteAndEdit,
- action: this.delEdit
- } : undefined,
- {
- icon: 'fas fa-trash-alt',
- text: this.$ts.delete,
- danger: true,
- action: this.del
- }]
- : []
- )]
- .filter(x => x !== undefined);
- } else {
- menu = [{
- icon: 'fas fa-copy',
- text: this.$ts.copyContent,
- action: this.copyContent
- }, {
- icon: 'fas fa-link',
- text: this.$ts.copyLink,
- action: this.copyLink
- }, (this.appearNote.url || this.appearNote.uri) ? {
- icon: 'fas fa-external-link-square-alt',
- text: this.$ts.showOnRemote,
- action: () => {
- window.open(this.appearNote.url || this.appearNote.uri, '_blank');
- }
- } : undefined]
- .filter(x => x !== undefined);
- }
-
- if (noteActions.length > 0) {
- menu = menu.concat([null, ...noteActions.map(action => ({
- icon: 'fas fa-plug',
- text: action.title,
- action: () => {
- action.handler(this.appearNote);
- }
- }))]);
- }
-
- return menu;
- },
-
- onContextmenu(e) {
- const isLink = (el: HTMLElement) => {
- if (el.tagName === 'A') return true;
- if (el.parentElement) {
- return isLink(el.parentElement);
- }
- };
- if (isLink(e.target)) return;
- if (window.getSelection().toString() !== '') return;
-
- if (this.$store.state.useReactionPickerForContextMenu) {
- e.preventDefault();
- this.react();
- } else {
- os.contextMenu(this.getMenu(), e).then(this.focus);
- }
- },
-
- menu(viaKeyboard = false) {
- os.popupMenu(this.getMenu(), this.$refs.menuButton, {
- viaKeyboard
- }).then(this.focus);
- },
-
- showRenoteMenu(viaKeyboard = false) {
- if (!this.isMyRenote) return;
- os.popupMenu([{
- text: this.$ts.unrenote,
- icon: 'fas fa-trash-alt',
- danger: true,
- action: () => {
- os.api('notes/delete', {
- noteId: this.note.id
- });
- this.isDeleted = true;
- }
- }], this.$refs.renoteTime, {
- viaKeyboard: viaKeyboard
- });
- },
-
- toggleShowContent() {
- this.showContent = !this.showContent;
- },
-
- copyContent() {
- copyToClipboard(this.appearNote.text);
- os.success();
- },
-
- copyLink() {
- copyToClipboard(`${url}/notes/${this.appearNote.id}`);
- os.success();
- },
-
- togglePin(pin: boolean) {
- os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
- noteId: this.appearNote.id
- }, undefined, null, e => {
- if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
- os.alert({
- type: 'error',
- text: this.$ts.pinLimitExceeded
- });
- }
- });
- },
-
- async clip() {
- const clips = await os.api('clips/list');
- os.popupMenu([{
- icon: 'fas fa-plus',
- text: this.$ts.createNew,
- action: async () => {
- const { canceled, result } = await os.form(this.$ts.createNewClip, {
- name: {
- type: 'string',
- label: this.$ts.name
- },
- description: {
- type: 'string',
- required: false,
- multiline: true,
- label: this.$ts.description
- },
- isPublic: {
- type: 'boolean',
- label: this.$ts.public,
- default: false
- }
- });
- if (canceled) return;
-
- const clip = await os.apiWithDialog('clips/create', result);
-
- os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
- }
- }, null, ...clips.map(clip => ({
- text: clip.name,
- action: () => {
- os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
- }
- }))], this.$refs.menuButton, {
- }).then(this.focus);
- },
-
- async promote() {
- const { canceled, result: days } = await os.inputNumber({
- title: this.$ts.numberOfDays,
- });
-
- if (canceled) return;
-
- os.apiWithDialog('admin/promo/create', {
- noteId: this.appearNote.id,
- expiresAt: Date.now() + (86400000 * days)
- });
- },
+ if (defaultStore.state.useReactionPickerForContextMenu) {
+ ev.preventDefault();
+ react();
+ } else {
+ os.contextMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), ev).then(focus);
+ }
+}
- share() {
- navigator.share({
- title: this.$t('noteOf', { user: this.appearNote.user.name }),
- text: this.appearNote.text,
- url: `${url}/notes/${this.appearNote.id}`
- });
- },
+function menu(viaKeyboard = false): void {
+ os.popupMenu(getNoteMenu({ note: props.note, translating, translation, menuButton }), menuButton.value, {
+ viaKeyboard
+ }).then(focus);
+}
- async translate() {
- if (this.translation != null) return;
- this.translating = true;
- const res = await os.api('notes/translate', {
- noteId: this.appearNote.id,
- targetLang: localStorage.getItem('lang') || navigator.language,
+function showRenoteMenu(viaKeyboard = false): void {
+ if (!isMyRenote) return;
+ os.popupMenu([{
+ text: i18n.locale.unrenote,
+ icon: 'fas fa-trash-alt',
+ danger: true,
+ action: () => {
+ os.api('notes/delete', {
+ noteId: props.note.id
});
- this.translating = false;
- this.translation = res;
- },
+ isDeleted.value = true;
+ }
+ }], renoteTime.value, {
+ viaKeyboard: viaKeyboard
+ });
+}
- focus() {
- this.$el.focus();
- },
+function focus() {
+ el.value.focus();
+}
- blur() {
- this.$el.blur();
- },
+function blur() {
+ el.value.blur();
+}
- focusBefore() {
- focusPrev(this.$el);
- },
+function focusBefore() {
+ focusPrev(el.value);
+}
- focusAfter() {
- focusNext(this.$el);
- },
+function focusAfter() {
+ focusNext(el.value);
+}
- userPage
- }
-});
+function readPromo() {
+ os.api('promo/read', {
+ noteId: appearNote.id
+ });
+ isDeleted.value = true;
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/notes.vue b/packages/client/src/components/notes.vue
index 4136f72b1b..41bec5a579 100644
--- a/packages/client/src/components/notes.vue
+++ b/packages/client/src/components/notes.vue
@@ -1,114 +1,42 @@
<template>
-<transition name="fade" mode="out-in">
- <MkLoading v-if="fetching"/>
-
- <MkError v-else-if="error" @retry="init()"/>
-
- <div v-else-if="empty" class="_fullinfo">
- <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
- <div>{{ $ts.noNotes }}</div>
- </div>
-
- <div v-else class="giivymft" :class="{ noGap }">
- <div v-show="more && reversed" style="margin-bottom: var(--margin);">
- <MkButton style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMoreFeature">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
- </MkButton>
+<MkPagination ref="pagingComponent" :pagination="pagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+ <div>{{ $ts.noNotes }}</div>
</div>
+ </template>
- <XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :no-gap="noGap" :ad="true" class="notes">
- <XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note" @update:note="updated(note, $event)"/>
- </XList>
-
- <div v-show="more && !reversed" style="margin-top: var(--margin);">
- <MkButton v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
- </MkButton>
+ <template #default="{ items: notes }">
+ <div class="giivymft" :class="{ noGap }">
+ <XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" class="notes">
+ <XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/>
+ </XList>
</div>
- </div>
-</transition>
+ </template>
+</MkPagination>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@/scripts/paging';
-import XNote from './note.vue';
-import XList from './date-separated-list.vue';
-import MkButton from '@/components/ui/button.vue';
-
-export default defineComponent({
- components: {
- XNote, XList, MkButton,
- },
-
- mixins: [
- paging({
- before: (self) => {
- self.$emit('before');
- },
-
- after: (self, e) => {
- self.$emit('after', e);
- }
- }),
- ],
-
- props: {
- pagination: {
- required: true
- },
- prop: {
- type: String,
- required: false
- },
- noGap: {
- type: Boolean,
- required: false,
- default: false
- },
- },
+<script lang="ts" setup>
+import { ref } from 'vue';
+import XNote from '@/components/note.vue';
+import XList from '@/components/date-separated-list.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import { Paging } from '@/components/ui/pagination.vue';
- emits: ['before', 'after'],
+const props = defineProps<{
+ pagination: Paging;
+ noGap?: boolean;
+}>();
- computed: {
- notes(): any[] {
- return this.prop ? this.items.map(item => item[this.prop]) : this.items;
- },
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
- reversed(): boolean {
- return this.pagination.reversed;
- }
- },
-
- methods: {
- updated(oldValue, newValue) {
- const i = this.notes.findIndex(n => n === oldValue);
- if (this.prop) {
- this.items[i][this.prop] = newValue;
- } else {
- this.items[i] = newValue;
- }
- },
-
- focus() {
- this.$refs.notes.focus();
- }
- }
+defineExpose({
+ pagingComponent,
});
</script>
<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
- transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
- opacity: 0;
-}
-
.giivymft {
&.noGap {
> .notes {
diff --git a/packages/client/src/components/notification-toast.vue b/packages/client/src/components/notification-toast.vue
index 5449409ccc..b2ab1029ad 100644
--- a/packages/client/src/components/notification-toast.vue
+++ b/packages/client/src/components/notification-toast.vue
@@ -1,6 +1,6 @@
<template>
<div class="mk-notification-toast" :style="{ zIndex }">
- <transition name="notification-toast" appear @after-leave="$emit('closed')">
+ <transition :name="$store.state.animation ? 'notification-toast' : ''" appear @after-leave="$emit('closed')">
<XNotification v-if="showing" :notification="notification" class="notification _acrylic"/>
</transition>
</div>
@@ -29,7 +29,7 @@ export default defineComponent({
};
},
mounted() {
- setTimeout(() => {
+ window.setTimeout(() => {
this.showing = false;
}, 6000);
}
diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue
index 37a88edc64..5659c899be 100644
--- a/packages/client/src/components/notification.vue
+++ b/packages/client/src/components/notification.vue
@@ -74,6 +74,7 @@ import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
import * as os from '@/os';
+import { stream } from '@/stream';
import { useTooltip } from '@/scripts/use-tooltip';
export default defineComponent({
@@ -106,7 +107,7 @@ export default defineComponent({
if (!props.notification.isRead) {
const readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some(entry => entry.isIntersecting)) return;
- os.stream.send('readNotification', {
+ stream.send('readNotification', {
id: props.notification.id
});
observer.disconnect();
@@ -114,7 +115,7 @@ export default defineComponent({
readObserver.observe(elRef.value);
- const connection = os.stream.useChannel('main');
+ const connection = stream.useChannel('main');
connection.on('readAllNotifications', () => readObserver.disconnect());
onUnmounted(() => {
diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue
index f3e5ee32f7..5a77b5487e 100644
--- a/packages/client/src/components/notifications.vue
+++ b/packages/client/src/components/notifications.vue
@@ -1,158 +1,77 @@
<template>
-<transition name="fade" mode="out-in">
- <MkLoading v-if="fetching"/>
+<MkPagination ref="pagingComponent" :pagination="pagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+ <div>{{ $ts.noNotifications }}</div>
+ </div>
+ </template>
- <MkError v-else-if="error" @retry="init()"/>
-
- <p v-else-if="empty" class="mfcuwfyp">{{ $ts.noNotifications }}</p>
-
- <div v-else>
- <XList v-slot="{ item: notification }" class="elsfgstc" :items="items" :no-gap="true">
- <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" @update:note="noteUpdated(notification.note, $event)"/>
+ <template #default="{ items: notifications }">
+ <XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
+ <XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
</XList>
-
- <MkButton v-show="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" primary style="margin: var(--margin) auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
- </MkButton>
- </div>
-</transition>
+ </template>
+</MkPagination>
</template>
-<script lang="ts">
-import { defineComponent, PropType, markRaw } from 'vue';
-import paging from '@/scripts/paging';
-import XNotification from './notification.vue';
-import XList from './date-separated-list.vue';
-import XNote from './note.vue';
+<script lang="ts" setup>
+import { defineComponent, PropType, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
import { notificationTypes } from 'misskey-js';
+import MkPagination from '@/components/ui/pagination.vue';
+import { Paging } from '@/components/ui/pagination.vue';
+import XNotification from '@/components/notification.vue';
+import XList from '@/components/date-separated-list.vue';
+import XNote from '@/components/note.vue';
import * as os from '@/os';
-import MkButton from '@/components/ui/button.vue';
-
-export default defineComponent({
- components: {
- XNotification,
- XList,
- XNote,
- MkButton,
- },
-
- mixins: [
- paging({}),
- ],
-
- props: {
- includeTypes: {
- type: Array as PropType<typeof notificationTypes[number][]>,
- required: false,
- default: null,
- },
- unreadOnly: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- data() {
- return {
- connection: null,
- pagination: {
- endpoint: 'i/notifications',
- limit: 10,
- params: () => ({
- includeTypes: this.allIncludeTypes || undefined,
- unreadOnly: this.unreadOnly,
- })
- },
- };
- },
-
- computed: {
- allIncludeTypes() {
- return this.includeTypes ?? notificationTypes.filter(x => !this.$i.mutingNotificationTypes.includes(x));
- }
- },
+import { stream } from '@/stream';
+import { $i } from '@/account';
- watch: {
- includeTypes: {
- handler() {
- this.reload();
- },
- deep: true
- },
- unreadOnly: {
- handler() {
- this.reload();
- },
- },
- // TODO: vue/vuexのバグか仕様かは不明なものの、プロフィール更新するなどして $i が更新されると、
- // mutingNotificationTypes に変化が無くてもこのハンドラーが呼び出され無駄なリロードが発生するのを直す
- '$i.mutingNotificationTypes': {
- handler() {
- if (this.includeTypes === null) {
- this.reload();
- }
- },
- deep: true
- }
- },
+const props = defineProps<{
+ includeTypes?: PropType<typeof notificationTypes[number][]>;
+ unreadOnly?: boolean;
+}>();
- mounted() {
- this.connection = markRaw(os.stream.useChannel('main'));
- this.connection.on('notification', this.onNotification);
- },
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
- beforeUnmount() {
- this.connection.dispose();
- },
+const allIncludeTypes = computed(() => props.includeTypes ?? notificationTypes.filter(x => !$i.mutingNotificationTypes.includes(x)));
- methods: {
- onNotification(notification) {
- const isMuted = !this.allIncludeTypes.includes(notification.type);
- if (isMuted || document.visibilityState === 'visible') {
- os.stream.send('readNotification', {
- id: notification.id
- });
- }
+const pagination: Paging = {
+ endpoint: 'i/notifications' as const,
+ limit: 10,
+ params: computed(() => ({
+ includeTypes: allIncludeTypes.value || undefined,
+ unreadOnly: props.unreadOnly,
+ })),
+};
- if (!isMuted) {
- this.prepend({
- ...notification,
- isRead: document.visibilityState === 'visible'
- });
- }
- },
+const onNotification = (notification) => {
+ const isMuted = !allIncludeTypes.value.includes(notification.type);
+ if (isMuted || document.visibilityState === 'visible') {
+ stream.send('readNotification', {
+ id: notification.id
+ });
+ }
- noteUpdated(oldValue, newValue) {
- const i = this.items.findIndex(n => n.note === oldValue);
- this.items[i] = {
- ...this.items[i],
- note: newValue
- };
- },
+ if (!isMuted) {
+ pagingComponent.value.prepend({
+ ...notification,
+ isRead: document.visibilityState === 'visible'
+ });
}
+};
+
+onMounted(() => {
+ const connection = stream.useChannel('main');
+ connection.on('notification', onNotification);
+ onUnmounted(() => {
+ connection.dispose();
+ });
});
</script>
<style lang="scss" scoped>
-.fade-enter-active,
-.fade-leave-active {
- transition: opacity 0.125s ease;
-}
-.fade-enter-from,
-.fade-leave-to {
- opacity: 0;
-}
-
-.mfcuwfyp {
- margin: 0;
- padding: 16px;
- text-align: center;
- color: var(--fg);
-}
-
.elsfgstc {
background: var(--panel);
}
diff --git a/packages/client/src/components/object-view.value.vue b/packages/client/src/components/object-view.value.vue
new file mode 100644
index 0000000000..6f388636dd
--- /dev/null
+++ b/packages/client/src/components/object-view.value.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="igpposuu _monospace">
+ <div v-if="value === null" class="null">null</div>
+ <div v-else-if="typeof value === 'boolean'" class="boolean">{{ value ? 'true' : 'false' }}</div>
+ <div v-else-if="typeof value === 'string'" class="string">"{{ value }}"</div>
+ <div v-else-if="typeof value === 'number'" class="number">{{ number(value) }}</div>
+ <div v-else-if="Array.isArray(value)" class="array">
+ <button @click="collapsed_ = !collapsed_">[ {{ collapsed_ ? '+' : '-' }} ]</button>
+ <template v-if="!collapsed_">
+ <div v-for="i in value.length" class="element">
+ {{ i }}: <XValue :value="value[i - 1]" collapsed/>
+ </div>
+ </template>
+ </div>
+ <div v-else-if="typeof value === 'object'" class="object">
+ <button @click="collapsed_ = !collapsed_">{ {{ collapsed_ ? '+' : '-' }} }</button>
+ <template v-if="!collapsed_">
+ <div v-for="k in Object.keys(value)" class="kv">
+ <div class="k">{{ k }}:</div>
+ <div class="v"><XValue :value="value[k]" collapsed/></div>
+ </div>
+ </template>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, ref } from 'vue';
+import number from '@/filters/number';
+
+export default defineComponent({
+ name: 'XValue',
+
+ props: {
+ value: {
+ type: Object,
+ required: true,
+ },
+ collapsed: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ setup(props) {
+ const collapsed_ = ref(props.collapsed);
+
+ return {
+ number,
+ collapsed_,
+ };
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.igpposuu {
+ display: inline;
+
+ > .null {
+ display: inline;
+ opacity: 0.7;
+ }
+
+ > .boolean {
+ display: inline;
+ color: var(--codeBoolean);
+ }
+
+ > .string {
+ display: inline;
+ color: var(--codeString);
+ }
+
+ > .number {
+ display: inline;
+ color: var(--codeNumber);
+ }
+
+ > .array {
+ display: inline;
+
+ > .element {
+ display: block;
+ padding-left: 16px;
+ }
+ }
+
+ > .object {
+ display: inline;
+
+ > .kv {
+ display: block;
+ padding-left: 16px;
+
+ > .k {
+ display: inline;
+ margin-right: 8px;
+ }
+
+ > .v {
+ display: inline;
+ }
+ }
+ }
+}
+</style>
diff --git a/packages/client/src/components/object-view.vue b/packages/client/src/components/object-view.vue
new file mode 100644
index 0000000000..e9db96de8c
--- /dev/null
+++ b/packages/client/src/components/object-view.vue
@@ -0,0 +1,33 @@
+<template>
+<div class="zhyxdalp">
+ <XValue :value="value" :collapsed="false"/>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import XValue from './object-view.value.vue';
+
+export default defineComponent({
+ components: {
+ XValue
+ },
+
+ props: {
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ setup(props) {
+
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.zhyxdalp {
+
+}
+</style>
diff --git a/packages/client/src/components/poll-editor.vue b/packages/client/src/components/poll-editor.vue
index fad0cf1593..6f3f23a2d3 100644
--- a/packages/client/src/components/poll-editor.vue
+++ b/packages/client/src/components/poll-editor.vue
@@ -3,7 +3,7 @@
<p v-if="choices.length < 2" class="caution">
<i class="fas fa-exclamation-triangle"></i>{{ $ts._poll.noOnlyOneChoice }}
</p>
- <ul ref="choices">
+ <ul>
<li v-for="(choice, i) in choices" :key="i">
<MkInput class="input" :model-value="choice" :placeholder="$t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
</MkInput>
@@ -14,8 +14,8 @@
</ul>
<MkButton v-if="choices.length < 10" class="add" @click="add">{{ $ts.add }}</MkButton>
<MkButton v-else class="add" disabled>{{ $ts._poll.noMore }}</MkButton>
+ <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
<section>
- <MkSwitch v-model="multiple">{{ $ts._poll.canMultipleVote }}</MkSwitch>
<div>
<MkSelect v-model="expiration">
<template #label>{{ $ts._poll.expiration }}</template>
@@ -31,7 +31,7 @@
<template #label>{{ $ts._poll.deadlineTime }}</template>
</MkInput>
</section>
- <section v-if="expiration === 'after'">
+ <section v-else-if="expiration === 'after'">
<MkInput v-model="after" type="number" class="input">
<template #label>{{ $ts._poll.duration }}</template>
</MkInput>
@@ -47,8 +47,8 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
import { addTime } from '@/scripts/time';
import { formatDateTimeString } from '@/scripts/format-time-string';
import MkInput from './form/input.vue';
@@ -56,131 +56,91 @@ import MkSelect from './form/select.vue';
import MkSwitch from './form/switch.vue';
import MkButton from './ui/button.vue';
-export default defineComponent({
- components: {
- MkInput,
- MkSelect,
- MkSwitch,
- MkButton,
- },
+const props = defineProps<{
+ modelValue: {
+ expiresAt: string;
+ expiredAfter: number;
+ choices: string[];
+ multiple: boolean;
+ };
+}>();
+const emit = defineEmits<{
+ (ev: 'update:modelValue', v: {
+ expiresAt: string;
+ expiredAfter: number;
+ choices: string[];
+ multiple: boolean;
+ }): void;
+}>();
- props: {
- poll: {
- type: Object,
- required: true
- }
- },
-
- emits: ['updated'],
-
- data() {
- return {
- choices: this.poll.choices,
- multiple: this.poll.multiple,
- expiration: 'infinite',
- atDate: formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'),
- atTime: '00:00',
- after: 0,
- unit: 'second',
- };
- },
+const choices = ref(props.modelValue.choices);
+const multiple = ref(props.modelValue.multiple);
+const expiration = ref('infinite');
+const atDate = ref(formatDateTimeString(addTime(new Date(), 1, 'day'), 'yyyy-MM-dd'));
+const atTime = ref('00:00');
+const after = ref(0);
+const unit = ref('second');
- watch: {
- choices: {
- handler() {
- this.$emit('updated', this.get());
- },
- deep: true
- },
- multiple: {
- handler() {
- this.$emit('updated', this.get());
- },
- },
- expiration: {
- handler() {
- this.$emit('updated', this.get());
- },
- },
- atDate: {
- handler() {
- this.$emit('updated', this.get());
- },
- },
- after: {
- handler() {
- this.$emit('updated', this.get());
- },
- },
- unit: {
- handler() {
- this.$emit('updated', this.get());
- },
- },
- },
+if (props.modelValue.expiresAt) {
+ expiration.value = 'at';
+ atDate.value = atTime.value = props.modelValue.expiresAt;
+} else if (typeof props.modelValue.expiredAfter === 'number') {
+ expiration.value = 'after';
+ after.value = props.modelValue.expiredAfter / 1000;
+} else {
+ expiration.value = 'infinite';
+}
- created() {
- const poll = this.poll;
- if (poll.expiresAt) {
- this.expiration = 'at';
- this.atDate = this.atTime = poll.expiresAt;
- } else if (typeof poll.expiredAfter === 'number') {
- this.expiration = 'after';
- this.after = poll.expiredAfter / 1000;
- } else {
- this.expiration = 'infinite';
- }
- },
+function onInput(i, value) {
+ choices.value[i] = value;
+}
- methods: {
- onInput(i, e) {
- this.choices[i] = e;
- },
+function add() {
+ choices.value.push('');
+ // TODO
+ // nextTick(() => {
+ // (this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
+ // });
+}
- add() {
- this.choices.push('');
- this.$nextTick(() => {
- // TODO
- //(this.$refs.choices as any).childNodes[this.choices.length - 1].childNodes[0].focus();
- });
- },
+function remove(i) {
+ choices.value = choices.value.filter((_, _i) => _i != i);
+}
- remove(i) {
- this.choices = this.choices.filter((_, _i) => _i != i);
- },
+function get() {
+ const calcAt = () => {
+ return new Date(`${atDate.value} ${atTime.value}`).getTime();
+ };
- get() {
- const at = () => {
- return new Date(`${this.atDate} ${this.atTime}`).getTime();
- };
+ const calcAfter = () => {
+ let base = parseInt(after.value);
+ switch (unit.value) {
+ case 'day': base *= 24;
+ case 'hour': base *= 60;
+ case 'minute': base *= 60;
+ case 'second': return base *= 1000;
+ default: return null;
+ }
+ };
- const after = () => {
- let base = parseInt(this.after);
- switch (this.unit) {
- case 'day': base *= 24;
- case 'hour': base *= 60;
- case 'minute': base *= 60;
- case 'second': return base *= 1000;
- default: return null;
- }
- };
+ return {
+ choices: choices.value,
+ multiple: multiple.value,
+ ...(
+ expiration.value === 'at' ? { expiresAt: calcAt() } :
+ expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
+ )
+ };
+}
- return {
- choices: this.choices,
- multiple: this.multiple,
- ...(
- this.expiration === 'at' ? { expiresAt: at() } :
- this.expiration === 'after' ? { expiredAfter: after() } : {}
- )
- };
- },
- }
+watch([choices, multiple, expiration, atDate, atTime, after, unit], () => emit('update:modelValue', get()), {
+ deep: true,
});
</script>
<style lang="scss" scoped>
.zmdxowus {
- padding: 8px;
+ padding: 8px 16px;
> .caution {
margin: 0 0 8px 0;
@@ -216,7 +176,7 @@ export default defineComponent({
}
> .add {
- margin: 8px 0 0 0;
+ margin: 8px 0;
z-index: 1;
}
@@ -225,21 +185,27 @@ export default defineComponent({
> div {
margin: 0 8px;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ gap: 12px;
&:last-child {
flex: 1 0 auto;
+ > div {
+ flex-grow: 1;
+ }
+
> section {
- align-items: center;
+ // MAGIC: Prevent div above from growing unless wrapped to its own line
+ flex-grow: 9999;
+ align-items: end;
display: flex;
- margin: -32px 0 0;
-
- > &:first-child {
- margin-right: 16px;
- }
+ gap: 4px;
> .input {
- flex: 1 0 auto;
+ flex: 1 1 auto;
}
}
}
diff --git a/packages/client/src/components/post-form-attaches.vue b/packages/client/src/components/post-form-attaches.vue
index 0782ce22e5..0c8181b481 100644
--- a/packages/client/src/components/post-form-attaches.vue
+++ b/packages/client/src/components/post-form-attaches.vue
@@ -10,7 +10,7 @@
</div>
</template>
</XDraggable>
- <p class="remain">{{ 4 - files.length }}/4</p>
+ <p class="remain">{{ 16 - files.length }}/16</p>
</div>
</template>
@@ -41,7 +41,6 @@ export default defineComponent({
data() {
return {
menu: null as Promise<null> | null,
-
};
},
@@ -99,10 +98,12 @@ export default defineComponent({
}, {
done: result => {
if (!result || result.canceled) return;
- let comment = result.result;
+ let comment = result.result.length == 0 ? null : result.result;
os.api('drive/files/update', {
fileId: file.id,
- comment: comment.length == 0 ? null : comment
+ comment: comment,
+ }).then(() => {
+ file.comment = comment;
});
}
}, 'closed');
diff --git a/packages/client/src/components/post-form.vue b/packages/client/src/components/post-form.vue
index 4265c575e2..ed78c5a3fb 100644
--- a/packages/client/src/components/post-form.vue
+++ b/packages/client/src/components/post-form.vue
@@ -8,25 +8,28 @@
>
<header>
<button v-if="!fixed" class="cancel _button" @click="cancel"><i class="fas fa-times"></i></button>
+ <button v-click-anime v-tooltip="i18n.locale.switchAccount" class="account _button" @click="openAccountMenu">
+ <MkAvatar :user="postAccount ?? $i" class="avatar"/>
+ </button>
<div>
- <span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
+ <span class="text-count" :class="{ over: textLength > maxTextLength }">{{ maxTextLength - textLength }}</span>
<span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
- <button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
+ <button ref="visibilityButton" v-tooltip="i18n.locale.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
<span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
<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 v-tooltip="$ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
+ <button v-tooltip="i18n.locale.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="fas fa-file-code"></i></button>
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ 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 }">
<XNoteSimple v-if="reply" class="preview" :note="reply"/>
<XNoteSimple v-if="renote" class="preview" :note="renote"/>
- <div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ $ts.quoteAttached }}<button @click="quoteId = null"><i class="fas fa-times"></i></button></div>
+ <div v-if="quoteId" class="with-quote"><i class="fas fa-quote-left"></i> {{ i18n.locale.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>
+ <span style="margin-right: 8px;">{{ i18n.locale.recipient }}</span>
<div class="visibleUsers">
<span v-for="u in visibleUsers" :key="u.id">
<MkAcct :user="u"/>
@@ -35,21 +38,21 @@
<button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
</div>
</div>
- <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ $ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ $ts.add }}</button></MkInfo>
- <input v-show="useCw" ref="cw" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
- <textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
- <input v-show="withHashtags" ref="hashtags" v-model="hashtags" class="hashtags" :placeholder="$ts.hashtags" list="hashtags">
+ <MkInfo v-if="hasNotSpecifiedMentions" warn class="hasNotSpecifiedMentions">{{ i18n.locale.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.locale.add }}</button></MkInfo>
+ <input v-show="useCw" ref="cwInputEl" v-model="cw" class="cw" :placeholder="i18n.locale.annotation" @keydown="onKeydown">
+ <textarea ref="textareaEl" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
+ <input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" class="hashtags" :placeholder="i18n.locale.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"/>
+ <XPollEditor v-if="poll" v-model="poll" @destroyed="poll = null"/>
<XNotePreview v-if="showPreview" class="preview" :text="text"/>
<footer>
- <button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
- <button v-tooltip="$ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
- <button v-tooltip="$ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
- <button v-tooltip="$ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
- <button v-tooltip="$ts.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
- <button v-tooltip="$ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
- <button v-if="postFormActions.length > 0" v-tooltip="$ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
+ <button v-tooltip="i18n.locale.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
+ <button v-tooltip="i18n.locale.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
+ <button v-tooltip="i18n.locale.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
+ <button v-tooltip="i18n.locale.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
+ <button v-tooltip="i18n.locale.hashtags" class="_button" :class="{ active: withHashtags }" @click="withHashtags = !withHashtags"><i class="fas fa-hashtag"></i></button>
+ <button v-tooltip="i18n.locale.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
+ <button v-if="postFormActions.length > 0" v-tooltip="i18n.locale.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
</footer>
<datalist id="hashtags">
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
@@ -58,667 +61,623 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+<script lang="ts" setup>
+import { inject, watch, nextTick, onMounted } from 'vue';
+import * as mfm from 'mfm-js';
+import * as misskey from 'misskey-js';
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 XPostFormAttaches from './post-form-attaches.vue';
+import XPollEditor from './poll-editor.vue';
import { host, url } from '@/config';
import { erase, unique } from '@/scripts/array';
import { extractMentions } from '@/scripts/extract-mentions';
import * as Acct from 'misskey-js/built/acct';
import { formatTimeString } from '@/scripts/format-time-string';
import { Autocomplete } from '@/scripts/autocomplete';
-import { noteVisibilities } from 'misskey-js';
import * as os from '@/os';
+import { stream } from '@/stream';
import { selectFiles } from '@/scripts/select-file';
import { defaultStore, notePostInterruptors, postFormActions } from '@/store';
import { throttle } from 'throttle-debounce';
import MkInfo from '@/components/ui/info.vue';
-import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import { $i, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account';
-export default defineComponent({
- components: {
- XNoteSimple,
- XNotePreview,
- XPostFormAttaches: defineAsyncComponent(() => import('./post-form-attaches.vue')),
- XPollEditor: defineAsyncComponent(() => import('./poll-editor.vue')),
- MkInfo,
- },
+const modal = inject('modal');
- inject: ['modal'],
+const props = withDefaults(defineProps<{
+ reply?: misskey.entities.Note;
+ renote?: misskey.entities.Note;
+ channel?: any; // TODO
+ mention?: misskey.entities.User;
+ specified?: misskey.entities.User;
+ initialText?: string;
+ initialVisibility?: typeof misskey.noteVisibilities;
+ initialFiles?: misskey.entities.DriveFile[];
+ initialLocalOnly?: boolean;
+ initialVisibleUsers?: misskey.entities.User[];
+ initialNote?: misskey.entities.Note;
+ share?: boolean;
+ fixed?: boolean;
+ autofocus?: boolean;
+}>(), {
+ initialVisibleUsers: [],
+ autofocus: true,
+});
- props: {
- reply: {
- type: Object,
- required: false
- },
- renote: {
- type: Object,
- required: false
- },
- channel: {
- type: Object,
- required: false
- },
- mention: {
- type: Object,
- required: false
- },
- specified: {
- type: Object,
- required: false
- },
- initialText: {
- type: String,
- required: false
- },
- initialVisibility: {
- type: String,
- required: false
- },
- initialFiles: {
- type: Array,
- required: false
- },
- initialLocalOnly: {
- type: Boolean,
- required: false
- },
- initialVisibleUsers: {
- type: Array,
- required: false,
- default: () => []
- },
- initialNote: {
- type: Object,
- required: false
- },
- share: {
- type: Boolean,
- required: false,
- default: false
- },
- fixed: {
- type: Boolean,
- required: false,
- default: false
- },
- autofocus: {
- type: Boolean,
- required: false,
- default: true
- },
- },
+const emit = defineEmits<{
+ (ev: 'posted'): void;
+ (ev: 'cancel'): void;
+ (ev: 'esc'): void;
+}>();
- emits: ['posted', 'cancel', 'esc'],
+const textareaEl = $ref<HTMLTextAreaElement | null>(null);
+const cwInputEl = $ref<HTMLInputElement | null>(null);
+const hashtagsInputEl = $ref<HTMLInputElement | null>(null);
+const visibilityButton = $ref<HTMLElement | null>(null);
- data() {
- return {
- posting: false,
- text: '',
- 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) as typeof noteVisibilities[number],
- visibleUsers: [],
- autocomplete: null,
- draghover: false,
- quoteId: null,
- hasNotSpecifiedMentions: false,
- recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
- imeText: '',
- typing: throttle(3000, () => {
- if (this.channel) {
- os.stream.send('typingOnChannel', { channel: this.channel.id });
- }
- }),
- postFormActions,
- };
- },
+let posting = $ref(false);
+let text = $ref(props.initialText ?? '');
+let files = $ref(props.initialFiles ?? []);
+let poll = $ref<{
+ choices: string[];
+ multiple: boolean;
+ expiresAt: string | null;
+ expiredAfter: string | null;
+} | null>(null);
+let useCw = $ref(false);
+let showPreview = $ref(false);
+let cw = $ref<string | null>(null);
+let localOnly = $ref<boolean>(props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly);
+let visibility = $ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility) as typeof misskey.noteVisibilities[number]);
+let visibleUsers = $ref(props.initialVisibleUsers ?? []);
+let autocomplete = $ref(null);
+let draghover = $ref(false);
+let quoteId = $ref(null);
+let hasNotSpecifiedMentions = $ref(false);
+let recentHashtags = $ref(JSON.parse(localStorage.getItem('hashtags') || '[]'));
+let imeText = $ref('');
- computed: {
- draftKey(): string {
- let key = this.channel ? `channel:${this.channel.id}` : '';
+const typing = throttle(3000, () => {
+ if (props.channel) {
+ stream.send('typingOnChannel', { channel: props.channel.id });
+ }
+});
- if (this.renote) {
- key += `renote:${this.renote.id}`;
- } else if (this.reply) {
- key += `reply:${this.reply.id}`;
- } else {
- key += 'note';
- }
+const draftKey = $computed((): string => {
+ let key = props.channel ? `channel:${props.channel.id}` : '';
- return key;
- },
+ if (props.renote) {
+ key += `renote:${props.renote.id}`;
+ } else if (props.reply) {
+ key += `reply:${props.reply.id}`;
+ } else {
+ key += 'note';
+ }
- placeholder(): string {
- if (this.renote) {
- return this.$ts._postForm.quotePlaceholder;
- } else if (this.reply) {
- return this.$ts._postForm.replyPlaceholder;
- } else if (this.channel) {
- return this.$ts._postForm.channelPlaceholder;
- } else {
- const xs = [
- this.$ts._postForm._placeholders.a,
- this.$ts._postForm._placeholders.b,
- this.$ts._postForm._placeholders.c,
- this.$ts._postForm._placeholders.d,
- this.$ts._postForm._placeholders.e,
- this.$ts._postForm._placeholders.f
- ];
- return xs[Math.floor(Math.random() * xs.length)];
- }
- },
+ return key;
+});
- submitText(): string {
- return this.renote
- ? this.$ts.quote
- : this.reply
- ? this.$ts.reply
- : this.$ts.note;
- },
+const placeholder = $computed((): string => {
+ if (props.renote) {
+ return i18n.locale._postForm.quotePlaceholder;
+ } else if (props.reply) {
+ return i18n.locale._postForm.replyPlaceholder;
+ } else if (props.channel) {
+ return i18n.locale._postForm.channelPlaceholder;
+ } else {
+ const xs = [
+ i18n.locale._postForm._placeholders.a,
+ i18n.locale._postForm._placeholders.b,
+ i18n.locale._postForm._placeholders.c,
+ i18n.locale._postForm._placeholders.d,
+ i18n.locale._postForm._placeholders.e,
+ i18n.locale._postForm._placeholders.f
+ ];
+ return xs[Math.floor(Math.random() * xs.length)];
+ }
+});
- textLength(): number {
- return length((this.text + this.imeText).trim());
- },
+const submitText = $computed((): string => {
+ return props.renote
+ ? i18n.locale.quote
+ : props.reply
+ ? i18n.locale.reply
+ : i18n.locale.note;
+});
- canPost(): boolean {
- return !this.posting &&
- (1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
- (this.textLength <= this.max) &&
- (!this.poll || this.poll.choices.length >= 2);
- },
+const textLength = $computed((): number => {
+ return length((text + imeText).trim());
+});
- max(): number {
- return this.$instance ? this.$instance.maxNoteTextLength : 1000;
- },
+const maxTextLength = $computed((): number => {
+ return instance ? instance.maxNoteTextLength : 1000;
+});
- withHashtags: defaultStore.makeGetterSetter('postFormWithHashtags'),
- hashtags: defaultStore.makeGetterSetter('postFormHashtags'),
- },
+const canPost = $computed((): boolean => {
+ return !posting &&
+ (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) &&
+ (textLength <= maxTextLength) &&
+ (!poll || poll.choices.length >= 2);
+});
- watch: {
- text() {
- this.checkMissingMention();
- },
- visibleUsers: {
- handler() {
- this.checkMissingMention();
- },
- deep: true
- }
- },
+const withHashtags = $computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
+const hashtags = $computed(defaultStore.makeGetterSetter('postFormHashtags'));
- mounted() {
- if (this.initialText) {
- this.text = this.initialText;
- }
+watch($$(text), () => {
+ checkMissingMention();
+});
- if (this.initialVisibility) {
- this.visibility = this.initialVisibility;
- }
+watch($$(visibleUsers), () => {
+ checkMissingMention();
+}, {
+ deep: true,
+});
- if (this.initialFiles) {
- this.files = this.initialFiles;
- }
+if (props.mention) {
+ text = props.mention.host ? `@${props.mention.username}@${toASCII(props.mention.host)}` : `@${props.mention.username}`;
+ text += ' ';
+}
- if (typeof this.initialLocalOnly === 'boolean') {
- this.localOnly = this.initialLocalOnly;
- }
+if (props.reply && (props.reply.user.username != $i.username || (props.reply.user.host != null && props.reply.user.host != host))) {
+ text = `@${props.reply.user.username}${props.reply.user.host != null ? '@' + toASCII(props.reply.user.host) : ''} `;
+}
- if (this.initialVisibleUsers) {
- this.visibleUsers = this.initialVisibleUsers;
- }
+if (props.reply && props.reply.text != null) {
+ const ast = mfm.parse(props.reply.text);
+ const otherHost = props.reply.user.host;
- if (this.mention) {
- this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
- this.text += ' ';
- }
+ for (const x of extractMentions(ast)) {
+ const mention = x.host ?
+ `@${x.username}@${toASCII(x.host)}` :
+ (otherHost == null || otherHost == host) ?
+ `@${x.username}` :
+ `@${x.username}@${toASCII(otherHost)}`;
- if (this.reply && (this.reply.user.username != this.$i.username || (this.reply.user.host != null && this.reply.user.host != host))) {
- this.text = `@${this.reply.user.username}${this.reply.user.host != null ? '@' + toASCII(this.reply.user.host) : ''} `;
- }
+ // 自分は除外
+ if ($i.username == x.username && x.host == null) continue;
+ if ($i.username == x.username && x.host == host) continue;
- if (this.reply && this.reply.text != null) {
- const ast = mfm.parse(this.reply.text);
- const otherHost = this.reply.user.host;
+ // 重複は除外
+ if (text.indexOf(`${mention} `) != -1) continue;
- for (const x of extractMentions(ast)) {
- const mention = x.host ?
- `@${x.username}@${toASCII(x.host)}` :
- (otherHost == null || otherHost == host) ?
- `@${x.username}` :
- `@${x.username}@${toASCII(otherHost)}`;
+ text += `${mention} `;
+ }
+}
- // 自分は除外
- if (this.$i.username == x.username && x.host == null) continue;
- if (this.$i.username == x.username && x.host == host) continue;
+if (props.channel) {
+ visibility = 'public';
+ localOnly = true; // TODO: チャンネルが連合するようになった折には消す
+}
- // 重複は除外
- if (this.text.indexOf(`${mention} `) != -1) continue;
+// 公開以外へのリプライ時は元の公開範囲を引き継ぐ
+if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visibility)) {
+ visibility = props.reply.visibility;
+ if (props.reply.visibility === 'specified') {
+ os.api('users/show', {
+ userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId)
+ }).then(users => {
+ visibleUsers.push(...users);
+ });
- this.text += `${mention} `;
- }
+ if (props.reply.userId !== $i.id) {
+ os.api('users/show', { userId: props.reply.userId }).then(user => {
+ visibleUsers.push(user);
+ });
}
+ }
+}
- if (this.channel) {
- this.visibility = 'public';
- this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
- }
+if (props.specified) {
+ visibility = 'specified';
+ visibleUsers.push(props.specified);
+}
- // 公開以外へのリプライ時は元の公開範囲を引き継ぐ
- if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
- this.visibility = this.reply.visibility;
- if (this.reply.visibility === 'specified') {
- os.api('users/show', {
- userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
- }).then(users => {
- this.visibleUsers.push(...users);
- });
+// keep cw when reply
+if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
+ useCw = true;
+ cw = props.reply.cw;
+}
- if (this.reply.userId !== this.$i.id) {
- os.api('users/show', { userId: this.reply.userId }).then(user => {
- this.visibleUsers.push(user);
- });
- }
- }
- }
+function watchForDraft() {
+ watch($$(text), () => saveDraft());
+ watch($$(useCw), () => saveDraft());
+ watch($$(cw), () => saveDraft());
+ watch($$(poll), () => saveDraft());
+ watch($$(files), () => saveDraft(), { deep: true });
+ watch($$(visibility), () => saveDraft());
+ watch($$(localOnly), () => saveDraft());
+}
- if (this.specified) {
- this.visibility = 'specified';
- this.visibleUsers.push(this.specified);
- }
+function checkMissingMention() {
+ if (visibility === 'specified') {
+ const ast = mfm.parse(text);
- // keep cw when reply
- if (this.$store.state.keepCw && this.reply && this.reply.cw) {
- this.useCw = true;
- this.cw = this.reply.cw;
+ for (const x of extractMentions(ast)) {
+ if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
+ hasNotSpecifiedMentions = true;
+ return;
+ }
}
+ hasNotSpecifiedMentions = false;
+ }
+}
- if (this.autofocus) {
- this.focus();
+function addMissingMention() {
+ const ast = mfm.parse(text);
- this.$nextTick(() => {
- this.focus();
+ for (const x of extractMentions(ast)) {
+ if (!visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
+ os.api('users/show', { username: x.username, host: x.host }).then(user => {
+ visibleUsers.push(user);
});
}
+ }
+}
- // TODO: detach when unmount
- new Autocomplete(this.$refs.text, this, { model: 'text' });
- new Autocomplete(this.$refs.cw, this, { model: 'cw' });
- new Autocomplete(this.$refs.hashtags, this, { model: 'hashtags' });
+function togglePoll() {
+ if (poll) {
+ poll = null;
+ } else {
+ poll = {
+ choices: ['', ''],
+ multiple: false,
+ expiresAt: null,
+ expiredAfter: null,
+ };
+ }
+}
- this.$nextTick(() => {
- // 書きかけの投稿を復元
- if (!this.share && !this.mention && !this.specified) {
- const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
- if (draft) {
- this.text = draft.data.text;
- this.useCw = draft.data.useCw;
- this.cw = draft.data.cw;
- this.visibility = draft.data.visibility;
- this.localOnly = draft.data.localOnly;
- this.files = (draft.data.files || []).filter(e => e);
- if (draft.data.poll) {
- this.poll = draft.data.poll;
- }
- }
- }
+function addTag(tag: string) {
+ insertTextAtCursor(textareaEl, ` #${tag} `);
+}
- // 削除して編集
- if (this.initialNote) {
- const init = this.initialNote;
- this.text = init.text ? init.text : '';
- this.files = init.files;
- this.cw = init.cw;
- this.useCw = init.cw != null;
- if (init.poll) {
- this.poll = {
- choices: init.poll.choices.map(x => x.text),
- multiple: init.poll.multiple,
- expiresAt: init.poll.expiresAt,
- expiredAfter: init.poll.expiredAfter,
- };
- }
- this.visibility = init.visibility;
- this.localOnly = init.localOnly;
- this.quoteId = init.renote ? init.renote.id : null;
- }
+function focus() {
+ textareaEl.focus();
+}
- this.$nextTick(() => this.watch());
- });
- },
+function chooseFileFrom(ev) {
+ selectFiles(ev.currentTarget || ev.target, i18n.locale.attachFile).then(files_ => {
+ for (const file of files_) {
+ files.push(file);
+ }
+ });
+}
- methods: {
- watch() {
- this.$watch('text', () => this.saveDraft());
- this.$watch('useCw', () => this.saveDraft());
- this.$watch('cw', () => this.saveDraft());
- this.$watch('poll', () => this.saveDraft());
- this.$watch('files', () => this.saveDraft(), { deep: true });
- this.$watch('visibility', () => this.saveDraft());
- this.$watch('localOnly', () => this.saveDraft());
- },
+function detachFile(id) {
+ files = files.filter(x => x.id != id);
+}
- checkMissingMention() {
- if (this.visibility === 'specified') {
- const ast = mfm.parse(this.text);
+function updateFiles(_files) {
+ files = _files;
+}
- for (const x of extractMentions(ast)) {
- if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
- this.hasNotSpecifiedMentions = true;
- return;
- }
- }
- this.hasNotSpecifiedMentions = false;
- }
- },
+function updateFileSensitive(file, sensitive) {
+ files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
+}
- addMissingMention() {
- const ast = mfm.parse(this.text);
+function updateFileName(file, name) {
+ files[files.findIndex(x => x.id === file.id)].name = name;
+}
- for (const x of extractMentions(ast)) {
- if (!this.visibleUsers.some(u => (u.username === x.username) && (u.host == x.host))) {
- os.api('users/show', { username: x.username, host: x.host }).then(user => {
- this.visibleUsers.push(user);
- });
- }
- }
- },
+function upload(file: File, name?: string) {
+ os.upload(file, defaultStore.state.uploadFolder, name).then(res => {
+ files.push(res);
+ });
+}
- togglePoll() {
- if (this.poll) {
- this.poll = null;
- } else {
- this.poll = {
- choices: ['', ''],
- multiple: false,
- expiresAt: null,
- expiredAfter: null,
- };
+function setVisibility() {
+ if (props.channel) {
+ // TODO: information dialog
+ return;
+ }
+
+ os.popup(import('./visibility-picker.vue'), {
+ currentVisibility: visibility,
+ currentLocalOnly: localOnly,
+ src: visibilityButton,
+ }, {
+ changeVisibility: v => {
+ visibility = v;
+ if (defaultStore.state.rememberNoteVisibility) {
+ defaultStore.set('visibility', visibility);
}
},
+ changeLocalOnly: v => {
+ localOnly = v;
+ if (defaultStore.state.rememberNoteVisibility) {
+ defaultStore.set('localOnly', localOnly);
+ }
+ }
+ }, 'closed');
+}
- addTag(tag: string) {
- insertTextAtCursor(this.$refs.text, ` #${tag} `);
- },
+function addVisibleUser() {
+ os.selectUser().then(user => {
+ visibleUsers.push(user);
+ });
+}
- focus() {
- (this.$refs.text as any).focus();
- },
+function removeVisibleUser(user) {
+ visibleUsers = erase(user, visibleUsers);
+}
- chooseFileFrom(ev) {
- selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
- for (const file of files) {
- this.files.push(file);
- }
- });
- },
+function clear() {
+ text = '';
+ files = [];
+ poll = null;
+ quoteId = null;
+}
- detachFile(id) {
- this.files = this.files.filter(x => x.id != id);
- },
+function onKeydown(e: KeyboardEvent) {
+ if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && canPost) post();
+ if (e.which === 27) emit('esc');
+ typing();
+}
- updateFiles(files) {
- this.files = files;
- },
+function onCompositionUpdate(e: CompositionEvent) {
+ imeText = e.data;
+ typing();
+}
- updateFileSensitive(file, sensitive) {
- this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
- },
+function onCompositionEnd(e: CompositionEvent) {
+ imeText = '';
+}
- updateFileName(file, name) {
- this.files[this.files.findIndex(x => x.id === file.id)].name = name;
- },
+async function onPaste(e: ClipboardEvent) {
+ for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
+ if (item.kind == 'file') {
+ const file = item.getAsFile();
+ const lio = file.name.lastIndexOf('.');
+ const ext = lio >= 0 ? file.name.slice(lio) : '';
+ const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+ upload(file, formatted);
+ }
+ }
- upload(file: File, name?: string) {
- os.upload(file, this.$store.state.uploadFolder, name).then(res => {
- this.files.push(res);
- });
- },
+ const paste = e.clipboardData.getData('text');
- onPollUpdate(poll) {
- this.poll = poll;
- this.saveDraft();
- },
+ if (!props.renote && !quoteId && paste.startsWith(url + '/notes/')) {
+ e.preventDefault();
- setVisibility() {
- if (this.channel) {
- // TODO: information dialog
+ os.confirm({
+ type: 'info',
+ text: i18n.locale.quoteQuestion,
+ }).then(({ canceled }) => {
+ if (canceled) {
+ insertTextAtCursor(textareaEl, paste);
return;
}
- os.popup(import('./visibility-picker.vue'), {
- currentVisibility: this.visibility,
- currentLocalOnly: this.localOnly,
- src: this.$refs.visibilityButton
- }, {
- changeVisibility: visibility => {
- this.visibility = visibility;
- if (this.$store.state.rememberNoteVisibility) {
- this.$store.set('visibility', visibility);
- }
- },
- changeLocalOnly: localOnly => {
- this.localOnly = localOnly;
- if (this.$store.state.rememberNoteVisibility) {
- this.$store.set('localOnly', localOnly);
- }
- }
- }, 'closed');
- },
-
- addVisibleUser() {
- os.selectUser().then(user => {
- this.visibleUsers.push(user);
- });
- },
-
- removeVisibleUser(user) {
- this.visibleUsers = erase(user, this.visibleUsers);
- },
+ quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+ });
+ }
+}
- clear() {
- this.text = '';
- this.files = [];
- this.poll = null;
- this.quoteId = null;
- },
+function onDragover(e) {
+ if (!e.dataTransfer.items[0]) return;
+ const isFile = e.dataTransfer.items[0].kind == 'file';
+ const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
+ if (isFile || isDriveFile) {
+ e.preventDefault();
+ draghover = true;
+ e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
+ }
+}
- onKeydown(e: KeyboardEvent) {
- if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
- if (e.which === 27) this.$emit('esc');
- this.typing();
- },
+function onDragenter(e) {
+ draghover = true;
+}
- onCompositionUpdate(e: CompositionEvent) {
- this.imeText = e.data;
- this.typing();
- },
+function onDragleave(e) {
+ draghover = false;
+}
- onCompositionEnd(e: CompositionEvent) {
- this.imeText = '';
- },
+function onDrop(e): void {
+ draghover = false;
- async onPaste(e: ClipboardEvent) {
- for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
- if (item.kind == 'file') {
- const file = item.getAsFile();
- const lio = file.name.lastIndexOf('.');
- const ext = lio >= 0 ? file.name.slice(lio) : '';
- const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
- this.upload(file, formatted);
- }
- }
+ // ファイルだったら
+ if (e.dataTransfer.files.length > 0) {
+ e.preventDefault();
+ for (const x of Array.from(e.dataTransfer.files)) upload(x);
+ return;
+ }
- const paste = e.clipboardData.getData('text');
+ //#region ドライブのファイル
+ const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ if (driveFile != null && driveFile != '') {
+ const file = JSON.parse(driveFile);
+ files.push(file);
+ e.preventDefault();
+ }
+ //#endregion
+}
- if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
- e.preventDefault();
+function saveDraft() {
+ const data = JSON.parse(localStorage.getItem('drafts') || '{}');
- os.confirm({
- type: 'info',
- text: this.$ts.quoteQuestion,
- }).then(({ canceled }) => {
- if (canceled) {
- insertTextAtCursor(this.$refs.text, paste);
- return;
- }
+ data[draftKey] = {
+ updatedAt: new Date(),
+ data: {
+ text: text,
+ useCw: useCw,
+ cw: cw,
+ visibility: visibility,
+ localOnly: localOnly,
+ files: files,
+ poll: poll
+ }
+ };
- this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
- });
- }
- },
+ localStorage.setItem('drafts', JSON.stringify(data));
+}
- onDragover(e) {
- if (!e.dataTransfer.items[0]) return;
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- if (isFile || isDriveFile) {
- e.preventDefault();
- this.draghover = true;
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- }
- },
+function deleteDraft() {
+ const data = JSON.parse(localStorage.getItem('drafts') || '{}');
- onDragenter(e) {
- this.draghover = true;
- },
+ delete data[draftKey];
- onDragleave(e) {
- this.draghover = false;
- },
+ localStorage.setItem('drafts', JSON.stringify(data));
+}
- onDrop(e): void {
- this.draghover = false;
+async function post() {
+ let data = {
+ text: text == '' ? undefined : text,
+ fileIds: files.length > 0 ? files.map(f => f.id) : undefined,
+ replyId: props.reply ? props.reply.id : undefined,
+ renoteId: props.renote ? props.renote.id : quoteId ? quoteId : undefined,
+ channelId: props.channel ? props.channel.id : undefined,
+ poll: poll,
+ cw: useCw ? cw || '' : undefined,
+ localOnly: localOnly,
+ visibility: visibility,
+ visibleUserIds: visibility == 'specified' ? visibleUsers.map(u => u.id) : undefined,
+ };
- // ファイルだったら
- if (e.dataTransfer.files.length > 0) {
- e.preventDefault();
- for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
- return;
- }
+ if (withHashtags && hashtags && hashtags.trim() !== '') {
+ const hashtags = hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
+ data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
+ }
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- this.files.push(file);
- e.preventDefault();
- }
- //#endregion
- },
+ // plugin
+ if (notePostInterruptors.length > 0) {
+ for (const interruptor of notePostInterruptors) {
+ data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
+ }
+ }
- saveDraft() {
- const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+ let token = undefined;
- data[this.draftKey] = {
- updatedAt: new Date(),
- data: {
- text: this.text,
- useCw: this.useCw,
- cw: this.cw,
- visibility: this.visibility,
- localOnly: this.localOnly,
- files: this.files,
- poll: this.poll
- }
- };
+ if (postAccount) {
+ const storedAccounts = await getAccounts();
+ token = storedAccounts.find(x => x.id === postAccount.id)?.token;
+ }
- localStorage.setItem('drafts', JSON.stringify(data));
- },
+ posting = true;
+ os.api('notes/create', data, token).then(() => {
+ clear();
+ nextTick(() => {
+ deleteDraft();
+ emit('posted');
+ if (data.text && data.text != '') {
+ const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
+ const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
+ localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
+ }
+ posting = false;
+ postAccount = null;
+ });
+ }).catch(err => {
+ posting = false;
+ os.alert({
+ type: 'error',
+ text: err.message + '\n' + (err as any).id,
+ });
+ });
+}
- deleteDraft() {
- const data = JSON.parse(localStorage.getItem('drafts') || '{}');
+function cancel() {
+ emit('cancel');
+}
- delete data[this.draftKey];
+function insertMention() {
+ os.selectUser().then(user => {
+ insertTextAtCursor(textareaEl, '@' + Acct.toString(user) + ' ');
+ });
+}
- localStorage.setItem('drafts', JSON.stringify(data));
- },
+async function insertEmoji(ev: MouseEvent) {
+ os.openEmojiPicker(ev.currentTarget || ev.target, {}, textareaEl);
+}
- async post() {
- let data = {
- text: this.text == '' ? undefined : this.text,
- fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
- replyId: this.reply ? this.reply.id : undefined,
- renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
- channelId: this.channel ? this.channel.id : undefined,
- poll: this.poll,
- cw: this.useCw ? this.cw || '' : undefined,
- localOnly: this.localOnly,
- visibility: this.visibility,
- visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
- };
+function showActions(ev) {
+ os.popupMenu(postFormActions.map(action => ({
+ text: action.title,
+ action: () => {
+ action.handler({
+ text: text
+ }, (key, value) => {
+ if (key === 'text') { text = value; }
+ });
+ }
+ })), ev.currentTarget || ev.target);
+}
- if (this.withHashtags && this.hashtags && this.hashtags.trim() !== '') {
- const hashtags = this.hashtags.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
- data.text = data.text ? `${data.text} ${hashtags}` : hashtags;
- }
+let postAccount = $ref<misskey.entities.UserDetailed | null>(null);
- // plugin
- if (notePostInterruptors.length > 0) {
- for (const interruptor of notePostInterruptors) {
- data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
- }
+function openAccountMenu(ev: MouseEvent) {
+ openAccountMenu_({
+ withExtraOperation: false,
+ includeCurrentAccount: true,
+ active: postAccount != null ? postAccount.id : $i.id,
+ onChoose: (account) => {
+ if (account.id === $i.id) {
+ postAccount = null;
+ } else {
+ postAccount = account;
}
-
- this.posting = true;
- os.api('notes/create', data).then(() => {
- this.clear();
- this.$nextTick(() => {
- this.deleteDraft();
- this.$emit('posted');
- if (data.text && data.text != '') {
- const hashtags = mfm.parse(data.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
- const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
- }
- this.posting = false;
- });
- }).catch(err => {
- this.posting = false;
- os.alert({
- type: 'error',
- text: err.message + '\n' + (err as any).id,
- });
- });
},
+ }, ev);
+}
- cancel() {
- this.$emit('cancel');
- },
+onMounted(() => {
+ if (props.autofocus) {
+ focus();
- insertMention() {
- os.selectUser().then(user => {
- insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' ');
- });
- },
+ nextTick(() => {
+ focus();
+ });
+ }
- async insertEmoji(ev) {
- os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
- },
+ // TODO: detach when unmount
+ new Autocomplete(textareaEl, $$(text));
+ new Autocomplete(cwInputEl, $$(cw));
+ new Autocomplete(hashtagsInputEl, $$(hashtags));
- showActions(ev) {
- os.popupMenu(postFormActions.map(action => ({
- text: action.title,
- action: () => {
- action.handler({
- text: this.text
- }, (key, value) => {
- if (key === 'text') { this.text = value; }
- });
+ nextTick(() => {
+ // 書きかけの投稿を復元
+ if (!props.share && !props.mention && !props.specified) {
+ const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[draftKey];
+ if (draft) {
+ text = draft.data.text;
+ useCw = draft.data.useCw;
+ cw = draft.data.cw;
+ visibility = draft.data.visibility;
+ localOnly = draft.data.localOnly;
+ files = (draft.data.files || []).filter(e => e);
+ if (draft.data.poll) {
+ poll = draft.data.poll;
}
- })), ev.currentTarget || ev.target);
+ }
}
- }
+
+ // 削除して編集
+ if (props.initialNote) {
+ const init = props.initialNote;
+ text = init.text ? init.text : '';
+ files = init.files;
+ cw = init.cw;
+ useCw = init.cw != null;
+ if (init.poll) {
+ poll = {
+ choices: init.poll.choices.map(x => x.text),
+ multiple: init.poll.multiple,
+ expiresAt: init.poll.expiresAt,
+ expiredAfter: init.poll.expiredAfter,
+ };
+ }
+ visibility = init.visibility;
+ localOnly = init.localOnly;
+ quoteId = init.renote ? init.renote.id : null;
+ }
+
+ nextTick(() => watchForDraft());
+ });
});
</script>
@@ -742,6 +701,19 @@ export default defineComponent({
line-height: 66px;
}
+ > .account {
+ height: 100%;
+ aspect-ratio: 1/1;
+ display: inline-flex;
+ vertical-align: bottom;
+
+ > .avatar {
+ width: 28px;
+ height: 28px;
+ margin: auto;
+ }
+ }
+
> div {
position: absolute;
top: 0;
diff --git a/packages/client/src/components/reaction-icon.vue b/packages/client/src/components/reaction-icon.vue
index c0ec955e32..5638c9a816 100644
--- a/packages/client/src/components/reaction-icon.vue
+++ b/packages/client/src/components/reaction-icon.vue
@@ -1,25 +1,13 @@
<template>
-<MkEmoji :emoji="reaction" :custom-emojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/>
+<MkEmoji :emoji="reaction" :custom-emojis="customEmojis || []" :is-reaction="true" :normal="true" :no-style="noStyle"/>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
-export default defineComponent({
- props: {
- reaction: {
- type: String,
- required: true
- },
- customEmojis: {
- required: false,
- default: () => []
- },
- noStyle: {
- type: Boolean,
- required: false,
- default: false
- },
- },
-});
+const props = defineProps<{
+ reaction: string;
+ customEmojis?: any[]; // TODO
+ noStyle?: boolean;
+}>();
</script>
diff --git a/packages/client/src/components/reaction-tooltip.vue b/packages/client/src/components/reaction-tooltip.vue
index dda8e7c6d7..1b2a024e21 100644
--- a/packages/client/src/components/reaction-tooltip.vue
+++ b/packages/client/src/components/reaction-tooltip.vue
@@ -1,5 +1,5 @@
<template>
-<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="$emit('closed')">
+<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
<div class="beeadbfb">
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
<div class="name">{{ reaction.replace('@.', '') }}</div>
@@ -7,31 +7,20 @@
</MkTooltip>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MkTooltip from './ui/tooltip.vue';
import XReactionIcon from './reaction-icon.vue';
-export default defineComponent({
- components: {
- MkTooltip,
- XReactionIcon,
- },
- props: {
- reaction: {
- type: String,
- required: true,
- },
- emojis: {
- type: Array,
- required: true,
- },
- source: {
- required: true,
- }
- },
- emits: ['closed'],
-})
+const props = defineProps<{
+ reaction: string;
+ emojis: any[]; // TODO
+ source: any; // TODO
+}>();
+
+const emit = defineEmits<{
+ (e: 'closed'): void;
+}>();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/reactions-viewer.details.vue b/packages/client/src/components/reactions-viewer.details.vue
index d6374517a2..8cec8dfa2f 100644
--- a/packages/client/src/components/reactions-viewer.details.vue
+++ b/packages/client/src/components/reactions-viewer.details.vue
@@ -1,5 +1,5 @@
<template>
-<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="$emit('closed')">
+<MkTooltip ref="tooltip" :source="source" :max-width="340" @closed="emit('closed')">
<div class="bqxuuuey">
<div class="reaction">
<XReactionIcon :reaction="reaction" :custom-emojis="emojis" class="icon" :no-style="true"/>
@@ -16,39 +16,22 @@
</MkTooltip>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MkTooltip from './ui/tooltip.vue';
import XReactionIcon from './reaction-icon.vue';
-export default defineComponent({
- components: {
- MkTooltip,
- XReactionIcon
- },
- props: {
- reaction: {
- type: String,
- required: true,
- },
- users: {
- type: Array,
- required: true,
- },
- count: {
- type: Number,
- required: true,
- },
- emojis: {
- type: Array,
- required: true,
- },
- source: {
- required: true,
- }
- },
- emits: ['closed'],
-})
+const props = defineProps<{
+ reaction: string;
+ users: any[]; // TODO
+ count: number;
+ emojis: any[]; // TODO
+ source: any; // TODO
+}>();
+
+const emit = defineEmits<{
+ (e: 'closed'): void;
+}>();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/reactions-viewer.vue b/packages/client/src/components/reactions-viewer.vue
index 59fcbb7129..a9bf51f65f 100644
--- a/packages/client/src/components/reactions-viewer.vue
+++ b/packages/client/src/components/reactions-viewer.vue
@@ -4,31 +4,19 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
+import { $i } from '@/account';
import XReaction from './reactions-viewer.reaction.vue';
-export default defineComponent({
- components: {
- XReaction
- },
- props: {
- note: {
- type: Object,
- required: true
- },
- },
- data() {
- return {
- initialReactions: new Set(Object.keys(this.note.reactions))
- };
- },
- computed: {
- isMe(): boolean {
- return this.$i && this.$i.id === this.note.userId;
- },
- },
-});
+const props = defineProps<{
+ note: misskey.entities.Note;
+}>();
+
+const initialReactions = new Set(Object.keys(props.note.reactions));
+
+const isMe = computed(() => $i && $i.id === props.note.userId);
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/remote-caution.vue b/packages/client/src/components/remote-caution.vue
index c496ea8f48..aa623f0fb0 100644
--- a/packages/client/src/components/remote-caution.vue
+++ b/packages/client/src/components/remote-caution.vue
@@ -2,22 +2,10 @@
<div class="jmgmzlwq _block"><i class="fas fa-exclamation-triangle" style="margin-right: 8px;"></i>{{ $ts.remoteUserCaution }}<a :href="href" rel="nofollow noopener" target="_blank">{{ $ts.showOnRemote }}</a></div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
-
-export default defineComponent({
- props: {
- href: {
- type: String,
- required: true
- },
- },
- data() {
- return {
- };
- }
-});
+<script lang="ts" setup>
+defineProps<{
+ href: string;
+}>();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/renote.details.vue b/packages/client/src/components/renote.details.vue
index e3ef15c753..cdbc71bdce 100644
--- a/packages/client/src/components/renote.details.vue
+++ b/packages/client/src/components/renote.details.vue
@@ -1,5 +1,5 @@
<template>
-<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="$emit('closed')">
+<MkTooltip ref="tooltip" :source="source" :max-width="250" @closed="emit('closed')">
<div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u"/>
@@ -10,29 +10,19 @@
</MkTooltip>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MkTooltip from './ui/tooltip.vue';
-export default defineComponent({
- components: {
- MkTooltip,
- },
- props: {
- users: {
- type: Array,
- required: true,
- },
- count: {
- type: Number,
- required: true,
- },
- source: {
- required: true,
- }
- },
- emits: ['closed'],
-})
+const props = defineProps<{
+ users: any[]; // TODO
+ count: number;
+ source: any; // TODO
+}>();
+
+const emit = defineEmits<{
+ (e: 'closed'): void;
+}>();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/ripple.vue b/packages/client/src/components/ripple.vue
index 272eacbc6e..401e78e304 100644
--- a/packages/client/src/components/ripple.vue
+++ b/packages/client/src/components/ripple.vue
@@ -94,7 +94,7 @@ export default defineComponent({
}
onMounted(() => {
- setTimeout(() => {
+ window.setTimeout(() => {
context.emit('end');
}, 1100);
});
diff --git a/packages/client/src/components/signin-dialog.vue b/packages/client/src/components/signin-dialog.vue
index 2edd10f539..5c2048e7b0 100644
--- a/packages/client/src/components/signin-dialog.vue
+++ b/packages/client/src/components/signin-dialog.vue
@@ -2,8 +2,8 @@
<XModalWindow ref="dialog"
:width="370"
:height="400"
- @close="$refs.dialog.close()"
- @closed="$emit('closed')"
+ @close="dialog.close()"
+ @closed="emit('closed')"
>
<template #header>{{ $ts.login }}</template>
@@ -11,32 +11,26 @@
</XModalWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkSignin from './signin.vue';
-export default defineComponent({
- components: {
- MkSignin,
- XModalWindow,
- },
+const props = withDefaults(defineProps<{
+ autoSet?: boolean;
+}>(), {
+ autoSet: false,
+});
- props: {
- autoSet: {
- type: Boolean,
- required: false,
- default: false,
- }
- },
+const emit = defineEmits<{
+ (e: 'done'): void;
+ (e: 'closed'): void;
+}>();
- emits: ['done', 'closed'],
+const dialog = $ref<InstanceType<typeof XModalWindow>>();
- methods: {
- onLogin(res) {
- this.$emit('done', res);
- this.$refs.dialog.close();
- }
- }
-});
+function onLogin(res) {
+ emit('done', res);
+ dialog.close();
+}
</script>
diff --git a/packages/client/src/components/signup-dialog.vue b/packages/client/src/components/signup-dialog.vue
index 30fe3bf7d3..bda2495ba7 100644
--- a/packages/client/src/components/signup-dialog.vue
+++ b/packages/client/src/components/signup-dialog.vue
@@ -2,7 +2,7 @@
<XModalWindow ref="dialog"
:width="366"
:height="500"
- @close="$refs.dialog.close()"
+ @close="dialog.close()"
@closed="$emit('closed')"
>
<template #header>{{ $ts.signup }}</template>
@@ -15,36 +15,30 @@
</XModalWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import XSignup from './signup.vue';
-export default defineComponent({
- components: {
- XSignup,
- XModalWindow,
- },
+const props = withDefaults(defineProps<{
+ autoSet?: boolean;
+}>(), {
+ autoSet: false,
+});
- props: {
- autoSet: {
- type: Boolean,
- required: false,
- default: false,
- }
- },
+const emit = defineEmits<{
+ (e: 'done'): void;
+ (e: 'closed'): void;
+}>();
- emits: ['done', 'closed'],
+const dialog = $ref<InstanceType<typeof XModalWindow>>();
- methods: {
- onSignup(res) {
- this.$emit('done', res);
- this.$refs.dialog.close();
- },
+function onSignup(res) {
+ emit('done', res);
+ dialog.close();
+}
- onSignupEmailPending() {
- this.$refs.dialog.close();
- }
- }
-});
+function onSignupEmailPending() {
+ dialog.close();
+}
</script>
diff --git a/packages/client/src/components/sub-note-content.vue b/packages/client/src/components/sub-note-content.vue
index efa202ce2f..d6a37d07be 100644
--- a/packages/client/src/components/sub-note-content.vue
+++ b/packages/client/src/components/sub-note-content.vue
@@ -21,35 +21,21 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import XPoll from './poll.vue';
import XMediaList from './media-list.vue';
-import * as os from '@/os';
+import * as misskey from 'misskey-js';
-export default defineComponent({
- components: {
- XPoll,
- XMediaList,
- },
- props: {
- note: {
- type: Object,
- required: true
- }
- },
- data() {
- return {
- collapsed: false,
- };
- },
- created() {
- this.collapsed = this.note.cw == null && this.note.text && (
- (this.note.text.split('\n').length > 9) ||
- (this.note.text.length > 500)
- );
- }
-});
+const props = defineProps<{
+ note: misskey.entities.Note;
+}>();
+
+const collapsed = $ref(
+ props.note.cw == null && props.note.text != null && (
+ (props.note.text.split('\n').length > 9) ||
+ (props.note.text.length > 500)
+ ));
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/taskmanager.api-window.vue b/packages/client/src/components/taskmanager.api-window.vue
deleted file mode 100644
index 6ec4da3a59..0000000000
--- a/packages/client/src/components/taskmanager.api-window.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<template>
-<XWindow ref="window"
- :initial-width="370"
- :initial-height="450"
- :can-resize="true"
- @close="$refs.window.close()"
- @closed="$emit('closed')"
->
- <template #header>Req Viewer</template>
-
- <div class="rlkneywz">
- <MkTab v-model="tab" style="border-bottom: solid 0.5px var(--divider);">
- <option value="req">Request</option>
- <option value="res">Response</option>
- </MkTab>
-
- <code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code>
- <code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code>
- </div>
-</XWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import XWindow from '@/components/ui/window.vue';
-import MkTab from '@/components/tab.vue';
-
-export default defineComponent({
- components: {
- XWindow,
- MkTab,
- },
-
- props: {
- req: {
- required: true,
- }
- },
-
- emits: ['closed'],
-
- data() {
- return {
- tab: 'req',
- reqStr: JSON5.stringify(this.req.req, null, '\t'),
- resStr: JSON5.stringify(this.req.res, null, '\t'),
- }
- },
-
- methods: {
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.rlkneywz {
- display: flex;
- flex-direction: column;
- height: 100%;
-
- > code {
- display: block;
- flex: 1;
- padding: 8px;
- overflow: auto;
- font-size: 0.9em;
- tab-size: 2;
- white-space: pre;
- }
-}
-</style>
diff --git a/packages/client/src/components/taskmanager.vue b/packages/client/src/components/taskmanager.vue
deleted file mode 100644
index 6901d88c2c..0000000000
--- a/packages/client/src/components/taskmanager.vue
+++ /dev/null
@@ -1,233 +0,0 @@
-<template>
-<XWindow ref="window" :initial-width="650" :initial-height="420" :can-resize="true" @closed="$emit('closed')">
- <template #header>
- <i class="fas fa-terminal" style="margin-right: 0.5em;"></i>Task Manager
- </template>
- <div class="qljqmnzj _monospace">
- <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>
- <option value="api">API</option>
- </MkTab>
-
- <div class="content">
- <div v-if="tab === 'windows'" v-follow class="windows">
- <div class="header">
- <div>#ID</div>
- <div>Component</div>
- <div>Action</div>
- </div>
- <div v-for="p in popups">
- <div>#{{ p.id }}</div>
- <div>{{ p.component.name ? p.component.name : '<anonymous>' }}</div>
- <div><button class="_textButton" @click="killPopup(p)">Kill</button></div>
- </div>
- </div>
- <div v-if="tab === 'stream'" v-follow class="stream">
- <div class="header">
- <div>#ID</div>
- <div>Ch</div>
- <div>Handle</div>
- <div>In</div>
- <div>Out</div>
- </div>
- <div v-for="c in connections">
- <div>#{{ c.id }}</div>
- <div>{{ c.channel }}</div>
- <div v-if="c.users !== null">(shared)<span v-if="c.name">{{ ' ' + c.name }}</span></div>
- <div v-else>{{ c.name ? c.name : '<anonymous>' }}</div>
- <div>{{ c.in }}</div>
- <div>{{ c.out }}</div>
- </div>
- </div>
- <div v-if="tab === 'streamPool'" v-follow class="streamPool">
- <div class="header">
- <div>#ID</div>
- <div>Ch</div>
- <div>Users</div>
- </div>
- <div v-for="p in pools">
- <div>#{{ p.id }}</div>
- <div>{{ p.channel }}</div>
- <div>{{ p.users }}</div>
- </div>
- </div>
- <div v-if="tab === 'api'" v-follow class="api">
- <div class="header">
- <div>#ID</div>
- <div>Endpoint</div>
- <div>State</div>
- </div>
- <div v-for="req in apiRequests" @click="showReq(req)">
- <div>#{{ req.id }}</div>
- <div>{{ req.endpoint }}</div>
- <div class="state" :class="req.state">{{ req.state }}</div>
- </div>
- </div>
- </div>
-
- <footer>
- <div><span class="label">Windows</span>{{ popups.length }}</div>
- <div><span class="label">Stream</span>{{ connections.length }}</div>
- <div><span class="label">Stream (Pool)</span>{{ pools.length }}</div>
- </footer>
- </div>
-</XWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw, onBeforeUnmount, ref, shallowRef } from 'vue';
-import XWindow from '@/components/ui/window.vue';
-import MkTab from '@/components/tab.vue';
-import MkButton from '@/components/ui/button.vue';
-import follow from '@/directives/follow-append';
-import * as os from '@/os';
-
-export default defineComponent({
- components: {
- XWindow,
- MkTab,
- MkButton,
- },
-
- directives: {
- follow
- },
-
- props: {
- },
-
- emits: ['closed'],
-
- setup() {
- const connections = shallowRef([]);
- const pools = shallowRef([]);
- const refreshStreamInfo = () => {
- console.log(os.stream.sharedConnectionPools, os.stream.sharedConnections, os.stream.nonSharedConnections);
- const conn = os.stream.sharedConnections.map(c => ({
- id: c.id, name: c.name, channel: c.channel, users: c.pool.users, in: c.inCount, out: c.outCount,
- })).concat(os.stream.nonSharedConnections.map(c => ({
- id: c.id, name: c.name, channel: c.channel, users: null, in: c.inCount, out: c.outCount,
- })));
- conn.sort((a, b) => (a.id > b.id) ? 1 : -1);
- connections.value = conn;
- pools.value = os.stream.sharedConnectionPools;
- };
- const interval = setInterval(refreshStreamInfo, 1000);
- onBeforeUnmount(() => {
- clearInterval(interval);
- });
-
- const killPopup = p => {
- os.popups.value = os.popups.value.filter(x => x !== p);
- };
-
- const showReq = req => {
- os.popup(import('./taskmanager.api-window.vue'), {
- req: req
- }, {
- }, 'closed');
- };
-
- return {
- tab: ref('stream'),
- popups: os.popups,
- apiRequests: os.apiRequests,
- connections,
- pools,
- killPopup,
- showReq,
- };
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.qljqmnzj {
- display: flex;
- flex-direction: column;
- height: 100%;
-
- > .content {
- flex: 1;
- overflow: auto;
-
- > div {
- display: table;
- width: 100%;
- padding: 16px;
- box-sizing: border-box;
-
- > div {
- display: table-row;
-
- &:nth-child(even) {
- //background: rgba(0, 0, 0, 0.1);
- }
-
- &.header {
- opacity: 0.7;
- }
-
- > div {
- display: table-cell;
- white-space: nowrap;
-
- &:not(:last-child) {
- padding-right: 8px;
- }
- }
- }
-
- &.api {
- > div {
- &:not(.header) {
- cursor: pointer;
-
- &:hover {
- color: var(--accent);
- }
- }
-
- > .state {
- &.pending {
- color: var(--warn);
- }
-
- &.success {
- color: var(--success);
- }
-
- &.failed {
- color: var(--error);
- }
- }
- }
- }
- }
- }
-
- > footer {
- display: flex;
- width: 100%;
- padding: 8px 16px;
- box-sizing: border-box;
- border-top: solid 0.5px var(--divider);
- font-size: 0.9em;
-
- > div {
- flex: 1;
-
- > .label {
- opacity: 0.7;
- margin-right: 0.5em;
-
- &:after {
- content: ":";
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/components/timeline.vue b/packages/client/src/components/timeline.vue
index f8a800872f..59956b9526 100644
--- a/packages/client/src/components/timeline.vue
+++ b/packages/client/src/components/timeline.vue
@@ -1,183 +1,143 @@
<template>
-<XNotes ref="tl" :no-gap="!$store.state.showGapBetweenNotesInTimeline" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)" @queue="$emit('queue', $event)"/>
+<XNotes ref="tlComponent" :no-gap="!$store.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
+<script lang="ts" setup>
+import { ref, computed, provide, onUnmounted } from 'vue';
import XNotes from './notes.vue';
import * as os from '@/os';
+import { stream } from '@/stream';
import * as sound from '@/scripts/sound';
+import { $i } from '@/account';
-export default defineComponent({
- components: {
- XNotes
- },
+const props = defineProps<{
+ src: string;
+ list?: string;
+ antenna?: string;
+ channel?: string;
+ sound?: boolean;
+}>();
- provide() {
- return {
- inChannel: this.src === 'channel'
- };
- },
+const emit = defineEmits<{
+ (e: 'note'): void;
+ (e: 'queue', count: number): void;
+}>();
- props: {
- src: {
- type: String,
- required: true
- },
- list: {
- type: String,
- required: false
- },
- antenna: {
- type: String,
- required: false
- },
- channel: {
- type: String,
- required: false
- },
- sound: {
- type: Boolean,
- required: false,
- default: false,
- }
- },
-
- emits: ['note', 'queue', 'before', 'after'],
+provide('inChannel', computed(() => props.src === 'channel'));
- data() {
- return {
- connection: null,
- connection2: null,
- pagination: null,
- baseQuery: {
- includeMyRenotes: this.$store.state.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.showLocalRenotes
- },
- query: {},
- date: null
- };
- },
+const tlComponent: InstanceType<typeof XNotes> = $ref();
- created() {
- const prepend = note => {
- (this.$refs.tl as any).prepend(note);
+const prepend = note => {
+ tlComponent.pagingComponent?.prepend(note);
- this.$emit('note');
+ emit('note');
- if (this.sound) {
- sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
- }
- };
+ if (props.sound) {
+ sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note');
+ }
+};
- const onUserAdded = () => {
- (this.$refs.tl as any).reload();
- };
+const onUserAdded = () => {
+ tlComponent.pagingComponent?.reload();
+};
- const onUserRemoved = () => {
- (this.$refs.tl as any).reload();
- };
+const onUserRemoved = () => {
+ tlComponent.pagingComponent?.reload();
+};
- const onChangeFollowing = () => {
- if (!this.$refs.tl.backed) {
- this.$refs.tl.reload();
- }
- };
+const onChangeFollowing = () => {
+ if (!tlComponent.pagingComponent?.backed) {
+ tlComponent.pagingComponent?.reload();
+ }
+};
- let endpoint;
+let endpoint;
+let query;
+let connection;
+let connection2;
- if (this.src == 'antenna') {
- endpoint = 'antennas/notes';
- this.query = {
- antennaId: this.antenna
- };
- this.connection = markRaw(os.stream.useChannel('antenna', {
- antennaId: this.antenna
- }));
- this.connection.on('note', prepend);
- } else if (this.src == 'home') {
- endpoint = 'notes/timeline';
- this.connection = markRaw(os.stream.useChannel('homeTimeline'));
- this.connection.on('note', prepend);
+if (props.src === 'antenna') {
+ endpoint = 'antennas/notes';
+ query = {
+ antennaId: props.antenna
+ };
+ connection = stream.useChannel('antenna', {
+ antennaId: props.antenna
+ });
+ connection.on('note', prepend);
+} else if (props.src === 'home') {
+ endpoint = 'notes/timeline';
+ connection = stream.useChannel('homeTimeline');
+ connection.on('note', prepend);
- this.connection2 = markRaw(os.stream.useChannel('main'));
- this.connection2.on('follow', onChangeFollowing);
- this.connection2.on('unfollow', onChangeFollowing);
- } else if (this.src == 'local') {
- endpoint = 'notes/local-timeline';
- this.connection = markRaw(os.stream.useChannel('localTimeline'));
- this.connection.on('note', prepend);
- } else if (this.src == 'social') {
- endpoint = 'notes/hybrid-timeline';
- this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
- this.connection.on('note', prepend);
- } else if (this.src == 'global') {
- endpoint = 'notes/global-timeline';
- this.connection = markRaw(os.stream.useChannel('globalTimeline'));
- this.connection.on('note', prepend);
- } else if (this.src == 'mentions') {
- endpoint = 'notes/mentions';
- this.connection = markRaw(os.stream.useChannel('main'));
- this.connection.on('mention', prepend);
- } else if (this.src == 'directs') {
- endpoint = 'notes/mentions';
- this.query = {
- visibility: 'specified'
- };
- const onNote = note => {
- if (note.visibility == 'specified') {
- prepend(note);
- }
- };
- this.connection = markRaw(os.stream.useChannel('main'));
- this.connection.on('mention', onNote);
- } else if (this.src == 'list') {
- endpoint = 'notes/user-list-timeline';
- this.query = {
- listId: this.list
- };
- this.connection = markRaw(os.stream.useChannel('userList', {
- listId: this.list
- }));
- this.connection.on('note', prepend);
- this.connection.on('userAdded', onUserAdded);
- this.connection.on('userRemoved', onUserRemoved);
- } else if (this.src == 'channel') {
- endpoint = 'channels/timeline';
- this.query = {
- channelId: this.channel
- };
- this.connection = markRaw(os.stream.useChannel('channel', {
- channelId: this.channel
- }));
- this.connection.on('note', prepend);
+ connection2 = stream.useChannel('main');
+ connection2.on('follow', onChangeFollowing);
+ connection2.on('unfollow', onChangeFollowing);
+} else if (props.src === 'local') {
+ endpoint = 'notes/local-timeline';
+ connection = stream.useChannel('localTimeline');
+ connection.on('note', prepend);
+} else if (props.src === 'social') {
+ endpoint = 'notes/hybrid-timeline';
+ connection = stream.useChannel('hybridTimeline');
+ connection.on('note', prepend);
+} else if (props.src === 'global') {
+ endpoint = 'notes/global-timeline';
+ connection = stream.useChannel('globalTimeline');
+ connection.on('note', prepend);
+} else if (props.src === 'mentions') {
+ endpoint = 'notes/mentions';
+ connection = stream.useChannel('main');
+ connection.on('mention', prepend);
+} else if (props.src === 'directs') {
+ endpoint = 'notes/mentions';
+ query = {
+ visibility: 'specified'
+ };
+ const onNote = note => {
+ if (note.visibility == 'specified') {
+ prepend(note);
}
+ };
+ connection = stream.useChannel('main');
+ connection.on('mention', onNote);
+} else if (props.src === 'list') {
+ endpoint = 'notes/user-list-timeline';
+ query = {
+ listId: props.list
+ };
+ connection = stream.useChannel('userList', {
+ listId: props.list
+ });
+ connection.on('note', prepend);
+ connection.on('userAdded', onUserAdded);
+ connection.on('userRemoved', onUserRemoved);
+} else if (props.src === 'channel') {
+ endpoint = 'channels/timeline';
+ query = {
+ channelId: props.channel
+ };
+ connection = stream.useChannel('channel', {
+ channelId: props.channel
+ });
+ connection.on('note', prepend);
+}
- this.pagination = {
- endpoint: endpoint,
- limit: 10,
- params: init => ({
- untilDate: this.date?.getTime(),
- ...this.baseQuery, ...this.query
- })
- };
- },
-
- beforeUnmount() {
- this.connection.dispose();
- if (this.connection2) this.connection2.dispose();
- },
+const pagination = {
+ endpoint: endpoint,
+ limit: 10,
+ params: query,
+};
- methods: {
- focus() {
- this.$refs.tl.focus();
- },
-
- timetravel(date?: Date) {
- this.date = date;
- this.$refs.tl.reload();
- }
- }
+onUnmounted(() => {
+ connection.dispose();
+ if (connection2) connection2.dispose();
});
+
+/* TODO
+const timetravel = (date?: Date) => {
+ this.date = date;
+ this.$refs.tl.reload();
+};
+*/
</script>
diff --git a/packages/client/src/components/toast.vue b/packages/client/src/components/toast.vue
index 914704c527..c114379716 100644
--- a/packages/client/src/components/toast.vue
+++ b/packages/client/src/components/toast.vue
@@ -1,6 +1,6 @@
<template>
<div class="mk-toast">
- <transition name="toast" appear @after-leave="$emit('closed')">
+ <transition :name="$store.state.animation ? 'toast' : ''" appear @after-leave="emit('closed')">
<div v-if="showing" class="body _acrylic" :style="{ zIndex }">
<div class="message">
{{ message }}
@@ -10,29 +10,25 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, ref } from 'vue';
import * as os from '@/os';
-export default defineComponent({
- props: {
- message: {
- type: String,
- required: true,
- },
- },
- emits: ['closed'],
- data() {
- return {
- showing: true,
- zIndex: os.claimZIndex('high'),
- };
- },
- mounted() {
- setTimeout(() => {
- this.showing = false;
- }, 4000);
- }
+defineProps<{
+ message: string;
+}>();
+
+const emit = defineEmits<{
+ (e: 'closed'): void;
+}>();
+
+const showing = ref(true);
+const zIndex = os.claimZIndex('high');
+
+onMounted(() => {
+ window.setTimeout(() => {
+ showing.value = false;
+ }, 4000);
});
</script>
diff --git a/packages/client/src/components/ui/button.vue b/packages/client/src/components/ui/button.vue
index 804a2e2720..c7b6c8ba96 100644
--- a/packages/client/src/components/ui/button.vue
+++ b/packages/client/src/components/ui/button.vue
@@ -117,14 +117,14 @@ export default defineComponent({
const scale = calcCircleScale(e.target.clientWidth, e.target.clientHeight, circleCenterX, circleCenterY);
- setTimeout(() => {
+ window.setTimeout(() => {
ripple.style.transform = 'scale(' + (scale / 2) + ')';
}, 1);
- setTimeout(() => {
+ window.setTimeout(() => {
ripple.style.transition = 'all 1s ease';
ripple.style.opacity = '0';
}, 1000);
- setTimeout(() => {
+ window.setTimeout(() => {
if (this.$refs.ripples) this.$refs.ripples.removeChild(ripple);
}, 2000);
}
diff --git a/packages/client/src/components/ui/container.vue b/packages/client/src/components/ui/container.vue
index fcd9f32290..7c595d8116 100644
--- a/packages/client/src/components/ui/container.vue
+++ b/packages/client/src/components/ui/container.vue
@@ -10,7 +10,7 @@
</button>
</div>
</header>
- <transition name="container-toggle"
+ <transition :name="$store.state.animation ? 'container-toggle' : ''"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
diff --git a/packages/client/src/components/ui/folder.vue b/packages/client/src/components/ui/folder.vue
index 9795b1d81a..fe1602b2bb 100644
--- a/packages/client/src/components/ui/folder.vue
+++ b/packages/client/src/components/ui/folder.vue
@@ -8,7 +8,7 @@
<template v-else><i class="fas fa-angle-down"></i></template>
</button>
</header>
- <transition name="folder-toggle"
+ <transition :name="$store.state.animation ? 'folder-toggle' : ''"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
diff --git a/packages/client/src/components/ui/menu.vue b/packages/client/src/components/ui/menu.vue
index 6f3f277b11..41165c8d33 100644
--- a/packages/client/src/components/ui/menu.vue
+++ b/packages/client/src/components/ui/menu.vue
@@ -24,7 +24,7 @@
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="fas fa-circle"></i></span>
</a>
- <button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" @click="clicked(item.action, $event)">
+ <button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)">
<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>
diff --git a/packages/client/src/components/ui/modal.vue b/packages/client/src/components/ui/modal.vue
index 3e2e59b27c..c691c8c6d0 100644
--- a/packages/client/src/components/ui/modal.vue
+++ b/packages/client/src/components/ui/modal.vue
@@ -211,7 +211,7 @@ export default defineComponent({
contentClicking = true;
window.addEventListener('mouseup', e => {
// click イベントより先に mouseup イベントが発生するかもしれないのでちょっと待つ
- setTimeout(() => {
+ window.setTimeout(() => {
contentClicking = false;
}, 100);
}, { passive: true, once: true });
diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue
index 64af4a54f7..13f3215671 100644
--- a/packages/client/src/components/ui/pagination.vue
+++ b/packages/client/src/components/ui/pagination.vue
@@ -1,5 +1,5 @@
<template>
-<transition name="fade" mode="out-in">
+<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<MkLoading v-if="fetching"/>
<MkError v-else-if="error" @retry="init()"/>
@@ -13,43 +13,269 @@
</slot>
</div>
- <div v-else class="cxiknjgy">
+ <div v-else ref="rootEl">
<slot :items="items"></slot>
- <div v-show="more" key="_more_" class="more _gap">
- <MkButton v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
+ <div v-show="more" key="_more_" class="cxiknjgy _gap">
+ <MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
+ {{ $ts.loadMore }}
</MkButton>
+ <MkLoading v-else class="loading"/>
</div>
</div>
</transition>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from './button.vue';
-import paging from '@/scripts/paging';
+<script lang="ts" setup>
+import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
+import * as misskey from 'misskey-js';
+import * as os from '@/os';
+import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
+import MkButton from '@/components/ui/button.vue';
-export default defineComponent({
- components: {
- MkButton
- },
+const SECOND_FETCH_LIMIT = 30;
- mixins: [
- paging({}),
- ],
+export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints> = {
+ endpoint: E;
+ limit: number;
+ params?: misskey.Endpoints[E]['req'] | ComputedRef<misskey.Endpoints[E]['req']>;
- props: {
- pagination: {
- required: true
- },
+ /**
+ * 検索APIのような、ページング不可なエンドポイントを利用する場合
+ * (そのようなAPIをこの関数で使うのは若干矛盾してるけど)
+ */
+ noPaging?: boolean;
- disableAutoLoad: {
- type: Boolean,
- required: false,
- default: false,
+ /**
+ * items 配列の中身を逆順にする(新しい方が最後)
+ */
+ reversed?: boolean;
+
+ offsetMode?: boolean;
+};
+
+const props = withDefaults(defineProps<{
+ pagination: Paging;
+ disableAutoLoad?: boolean;
+ displayLimit?: number;
+}>(), {
+ displayLimit: 30,
+});
+
+const emit = defineEmits<{
+ (e: 'queue', count: number): void;
+}>();
+
+type Item = { id: string; [another: string]: unknown; };
+
+const rootEl = ref<HTMLElement>();
+const items = ref<Item[]>([]);
+const queue = ref<Item[]>([]);
+const offset = ref(0);
+const fetching = ref(true);
+const moreFetching = ref(false);
+const more = ref(false);
+const backed = ref(false); // 遡り中か否か
+const isBackTop = ref(false);
+const empty = computed(() => items.value.length === 0);
+const error = ref(false);
+
+const init = async (): Promise<void> => {
+ queue.value = [];
+ fetching.value = true;
+ const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+ await os.api(props.pagination.endpoint, {
+ ...params,
+ limit: props.pagination.noPaging ? (props.pagination.limit || 10) : (props.pagination.limit || 10) + 1,
+ }).then(res => {
+ for (let i = 0; i < res.length; i++) {
+ const item = res[i];
+ if (props.pagination.reversed) {
+ if (i === res.length - 2) item._shouldInsertAd_ = true;
+ } else {
+ if (i === 3) item._shouldInsertAd_ = true;
+ }
+ }
+ if (!props.pagination.noPaging && (res.length > (props.pagination.limit || 10))) {
+ res.pop();
+ items.value = props.pagination.reversed ? [...res].reverse() : res;
+ more.value = true;
+ } else {
+ items.value = props.pagination.reversed ? [...res].reverse() : res;
+ more.value = false;
+ }
+ offset.value = res.length;
+ error.value = false;
+ fetching.value = false;
+ }, e => {
+ error.value = true;
+ fetching.value = false;
+ });
+};
+
+const reload = (): void => {
+ items.value = [];
+ init();
+};
+
+const fetchMore = async (): Promise<void> => {
+ if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
+ moreFetching.value = true;
+ backed.value = true;
+ const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+ await os.api(props.pagination.endpoint, {
+ ...params,
+ limit: SECOND_FETCH_LIMIT + 1,
+ ...(props.pagination.offsetMode ? {
+ offset: offset.value,
+ } : {
+ untilId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
+ }),
+ }).then(res => {
+ for (let i = 0; i < res.length; i++) {
+ const item = res[i];
+ if (props.pagination.reversed) {
+ if (i === res.length - 9) item._shouldInsertAd_ = true;
+ } else {
+ if (i === 10) item._shouldInsertAd_ = true;
+ }
+ }
+ if (res.length > SECOND_FETCH_LIMIT) {
+ res.pop();
+ items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ more.value = true;
+ } else {
+ items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ more.value = false;
+ }
+ offset.value += res.length;
+ moreFetching.value = false;
+ }, e => {
+ moreFetching.value = false;
+ });
+};
+
+const fetchMoreAhead = async (): Promise<void> => {
+ if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
+ moreFetching.value = true;
+ const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
+ await os.api(props.pagination.endpoint, {
+ ...params,
+ limit: SECOND_FETCH_LIMIT + 1,
+ ...(props.pagination.offsetMode ? {
+ offset: offset.value,
+ } : {
+ sinceId: props.pagination.reversed ? items.value[0].id : items.value[items.value.length - 1].id,
+ }),
+ }).then(res => {
+ if (res.length > SECOND_FETCH_LIMIT) {
+ res.pop();
+ items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ more.value = true;
+ } else {
+ items.value = props.pagination.reversed ? [...res].reverse().concat(items.value) : items.value.concat(res);
+ more.value = false;
+ }
+ offset.value += res.length;
+ moreFetching.value = false;
+ }, e => {
+ moreFetching.value = false;
+ });
+};
+
+const prepend = (item: Item): void => {
+ if (props.pagination.reversed) {
+ if (rootEl.value) {
+ const container = getScrollContainer(rootEl.value);
+ if (container == null) return; // TODO?
+
+ const pos = getScrollPosition(rootEl.value);
+ const viewHeight = container.clientHeight;
+ const height = container.scrollHeight;
+ const isBottom = (pos + viewHeight > height - 32);
+ if (isBottom) {
+ // オーバーフローしたら古いアイテムは捨てる
+ if (items.value.length >= props.displayLimit) {
+ // このやり方だとVue 3.2以降アニメーションが動かなくなる
+ //items.value = items.value.slice(-props.displayLimit);
+ while (items.value.length >= props.displayLimit) {
+ items.value.shift();
+ }
+ more.value = true;
+ }
+ }
}
- },
+ items.value.push(item);
+ // TODO
+ } else {
+ // 初回表示時はunshiftだけでOK
+ if (!rootEl.value) {
+ items.value.unshift(item);
+ return;
+ }
+
+ const isTop = isBackTop.value || (document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
+
+ if (isTop) {
+ // Prepend the item
+ items.value.unshift(item);
+
+ // オーバーフローしたら古いアイテムは捨てる
+ if (items.value.length >= props.displayLimit) {
+ // このやり方だとVue 3.2以降アニメーションが動かなくなる
+ //this.items = items.value.slice(0, props.displayLimit);
+ while (items.value.length >= props.displayLimit) {
+ items.value.pop();
+ }
+ more.value = true;
+ }
+ } else {
+ queue.value.push(item);
+ onScrollTop(rootEl.value, () => {
+ for (const item of queue.value) {
+ prepend(item);
+ }
+ queue.value = [];
+ });
+ }
+ }
+};
+
+const append = (item: Item): void => {
+ items.value.push(item);
+};
+
+const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
+ const i = items.value.findIndex(item => item.id === id);
+ items.value[i] = replacer(items.value[i]);
+};
+
+if (props.pagination.params && isRef(props.pagination.params)) {
+ watch(props.pagination.params, init, { deep: true });
+}
+
+watch(queue, (a, b) => {
+ if (a.length === 0 && b.length === 0) return;
+ emit('queue', queue.value.length);
+}, { deep: true });
+
+init();
+
+onActivated(() => {
+ isBackTop.value = false;
+});
+
+onDeactivated(() => {
+ isBackTop.value = window.scrollY === 0;
+});
+
+defineExpose({
+ items,
+ backed,
+ reload,
+ fetchMoreAhead,
+ prepend,
+ append,
+ updateItem,
});
</script>
@@ -64,11 +290,9 @@ export default defineComponent({
}
.cxiknjgy {
- > .more > .button {
+ > .button {
margin-left: auto;
margin-right: auto;
- height: 48px;
- min-width: 150px;
}
}
</style>
diff --git a/packages/client/src/components/ui/tooltip.vue b/packages/client/src/components/ui/tooltip.vue
index 2e48ab623e..394b068352 100644
--- a/packages/client/src/components/ui/tooltip.vue
+++ b/packages/client/src/components/ui/tooltip.vue
@@ -1,5 +1,5 @@
<template>
-<transition name="tooltip" appear @after-leave="$emit('closed')">
+<transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="$emit('closed')">
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>{{ text }}</slot>
</div>
diff --git a/packages/client/src/components/ui/window.vue b/packages/client/src/components/ui/window.vue
index bd33289ccc..fa32ecfdef 100644
--- a/packages/client/src/components/ui/window.vue
+++ b/packages/client/src/components/ui/window.vue
@@ -147,9 +147,9 @@ export default defineComponent({
}
},
- onContextmenu(e) {
+ onContextmenu(ev: MouseEvent) {
if (this.contextmenu) {
- os.contextMenu(this.contextmenu, e);
+ os.contextMenu(this.contextmenu, ev);
}
},
diff --git a/packages/client/src/components/url-preview-popup.vue b/packages/client/src/components/url-preview-popup.vue
index c345bafcf9..5f3717ab91 100644
--- a/packages/client/src/components/url-preview-popup.vue
+++ b/packages/client/src/components/url-preview-popup.vue
@@ -1,6 +1,6 @@
<template>
<div class="fgmtyycl" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
- <transition name="zoom" @after-leave="$emit('closed')">
+ <transition :name="$store.state.animation ? 'zoom' : ''" @after-leave="$emit('closed')">
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/>
</transition>
</div>
diff --git a/packages/client/src/components/url-preview.vue b/packages/client/src/components/url-preview.vue
index fe88985a62..6c57957617 100644
--- a/packages/client/src/components/url-preview.vue
+++ b/packages/client/src/components/url-preview.vue
@@ -4,10 +4,10 @@
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen />
</div>
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter">
- <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', left: `${tweetLeft}px`, width: `${tweetLeft < 0 ? 'auto' : '100%'}`, height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
+ <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
</div>
<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
- <transition name="zoom" mode="out-in">
+ <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
<button v-if="!playerEnabled && player.url" class="_button" :title="$ts.enablePlayer" @click.prevent="playerEnabled = true"><i class="fas fa-play-circle"></i></button>
@@ -32,110 +32,80 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted } from 'vue';
import { url as local, lang } from '@/config';
-import * as os from '@/os';
-export default defineComponent({
- props: {
- url: {
- type: String,
- require: true
- },
-
- detail: {
- type: Boolean,
- required: false,
- default: false
- },
-
- compact: {
- type: Boolean,
- required: false,
- default: false
- },
- },
-
- data() {
- const self = this.url.startsWith(local);
- return {
- local,
- fetching: true,
- title: null,
- description: null,
- thumbnail: null,
- icon: null,
- sitename: null,
- player: {
- url: null,
- width: null,
- height: null
- },
- tweetId: null,
- tweetExpanded: this.detail,
- embedId: `embed${Math.random().toString().replace(/\D/,'')}`,
- tweetHeight: 150,
- tweetLeft: 0,
- playerEnabled: false,
- self: self,
- attr: self ? 'to' : 'href',
- target: self ? null : '_blank',
- };
- },
+const props = withDefaults(defineProps<{
+ url: string;
+ detail?: boolean;
+ compact?: boolean;
+}>(), {
+ detail: false,
+ compact: false,
+});
- created() {
- const requestUrl = new URL(this.url);
+const self = props.url.startsWith(local);
+const attr = self ? 'to' : 'href';
+const target = self ? null : '_blank';
+let fetching = $ref(true);
+let title = $ref<string | null>(null);
+let description = $ref<string | null>(null);
+let thumbnail = $ref<string | null>(null);
+let icon = $ref<string | null>(null);
+let sitename = $ref<string | null>(null);
+let player = $ref({
+ url: null,
+ width: null,
+ height: null
+});
+let playerEnabled = $ref(false);
+let tweetId = $ref<string | null>(null);
+let tweetExpanded = $ref(props.detail);
+const embedId = `embed${Math.random().toString().replace(/\D/,'')}`;
+let tweetHeight = $ref(150);
- if (requestUrl.hostname == 'twitter.com') {
- const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
- if (m) this.tweetId = m[1];
- }
+const requestUrl = new URL(props.url);
- if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
- requestUrl.hostname = 'www.youtube.com';
- }
+if (requestUrl.hostname == 'twitter.com') {
+ const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
+ if (m) tweetId = m[1];
+}
- const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
+if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/(?:watch|channel)')) {
+ requestUrl.hostname = 'www.youtube.com';
+}
- requestUrl.hash = '';
+const requestLang = (lang || 'ja-JP').replace('ja-KS', 'ja-JP');
- fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
- res.json().then(info => {
- if (info.url == null) return;
- this.title = info.title;
- this.description = info.description;
- this.thumbnail = info.thumbnail;
- this.icon = info.icon;
- this.sitename = info.sitename;
- this.fetching = false;
- this.player = info.player;
- })
- });
+requestUrl.hash = '';
- (window as any).addEventListener('message', this.adjustTweetHeight);
- },
+fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${requestLang}`).then(res => {
+ res.json().then(info => {
+ if (info.url == null) return;
+ title = info.title;
+ description = info.description;
+ thumbnail = info.thumbnail;
+ icon = info.icon;
+ sitename = info.sitename;
+ fetching = false;
+ player = info.player;
+ })
+});
- mounted() {
- // 300pxないと絶対右にはみ出るので左に移動してしまう
- const areaWidth = (this.$el as any)?.clientWidth;
- if (areaWidth && areaWidth < 300) this.tweetLeft = areaWidth - 241;
- },
+function adjustTweetHeight(message: any) {
+ if (message.origin !== 'https://platform.twitter.com') return;
+ const embed = message.data?.['twttr.embed'];
+ if (embed?.method !== 'twttr.private.resize') return;
+ if (embed?.id !== embedId) return;
+ const height = embed?.params[0]?.height;
+ if (height) tweetHeight = height;
+}
- beforeUnmount() {
- (window as any).removeEventListener('message', this.adjustTweetHeight);
- },
+(window as any).addEventListener('message', adjustTweetHeight);
- methods: {
- adjustTweetHeight(message: any) {
- if (message.origin !== 'https://platform.twitter.com') return;
- const embed = message.data?.['twttr.embed'];
- if (embed?.method !== 'twttr.private.resize') return;
- if (embed?.id !== this.embedId) return;
- const height = embed?.params[0]?.height;
- if (height) this.tweetHeight = height;
- },
- },
+onUnmounted(() => {
+ (window as any).removeEventListener('message', adjustTweetHeight);
});
</script>
diff --git a/packages/client/src/components/user-info.vue b/packages/client/src/components/user-info.vue
index 779a71358d..6a25d412fc 100644
--- a/packages/client/src/components/user-info.vue
+++ b/packages/client/src/components/user-info.vue
@@ -27,32 +27,14 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import * as misskey from 'misskey-js';
import MkFollowButton from './follow-button.vue';
import { userPage } from '@/filters/user';
-export default defineComponent({
- components: {
- MkFollowButton
- },
-
- props: {
- user: {
- type: Object,
- required: true
- },
- },
-
- data() {
- return {
- };
- },
-
- methods: {
- userPage,
- }
-});
+defineProps<{
+ user: misskey.entities.UserDetailed;
+}>();
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/user-list.vue b/packages/client/src/components/user-list.vue
index 2148dab608..3e273721c7 100644
--- a/packages/client/src/components/user-list.vue
+++ b/packages/client/src/components/user-list.vue
@@ -1,91 +1,39 @@
<template>
-<MkError v-if="error" @retry="init()"/>
+<MkPagination ref="pagingComponent" :pagination="pagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+ <div>{{ $ts.noUsers }}</div>
+ </div>
+ </template>
-<div v-else class="efvhhmdq _isolated">
- <div v-if="empty" class="no-users">
- <p>{{ $ts.noUsers }}</p>
- </div>
- <div class="users">
- <MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
- </div>
- <button v-show="more" v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" class="more" :class="{ fetching: moreFetching }" :disabled="moreFetching" @click="fetchMore">
- <template v-if="moreFetching"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>{{ moreFetching ? $ts.loading : $ts.loadMore }}
- </button>
-</div>
+ <template #default="{ items: users }">
+ <div class="efvhhmdq">
+ <MkUserInfo v-for="user in users" :key="user.id" class="user" :user="user"/>
+ </div>
+ </template>
+</MkPagination>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@/scripts/paging';
-import MkUserInfo from './user-info.vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import MkUserInfo from '@/components/user-info.vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import { Paging } from '@/components/ui/pagination.vue';
import { userPage } from '@/filters/user';
-export default defineComponent({
- components: {
- MkUserInfo,
- },
+const props = defineProps<{
+ pagination: Paging;
+ noGap?: boolean;
+}>();
- mixins: [
- paging({}),
- ],
-
- props: {
- pagination: {
- required: true
- },
- extract: {
- required: false
- },
- expanded: {
- type: Boolean,
- default: true
- },
- },
-
- computed: {
- users() {
- return this.extract ? this.extract(this.items) : this.items;
- }
- },
-
- methods: {
- userPage
- }
-});
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
</script>
<style lang="scss" scoped>
.efvhhmdq {
- > .no-users {
- text-align: center;
- }
-
- > .users {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- grid-gap: var(--margin);
- }
-
- > .more {
- display: block;
- width: 100%;
- padding: 16px;
-
- &:hover {
- background: rgba(#000, 0.025);
- }
-
- &:active {
- background: rgba(#000, 0.05);
- }
-
- &.fetching {
- cursor: wait;
- }
-
- > i {
- margin-right: 4px;
- }
- }
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+ grid-gap: var(--margin);
}
</style>
diff --git a/packages/client/src/components/user-online-indicator.vue b/packages/client/src/components/user-online-indicator.vue
index 93e9dea57b..a87b0aeff5 100644
--- a/packages/client/src/components/user-online-indicator.vue
+++ b/packages/client/src/components/user-online-indicator.vue
@@ -2,26 +2,21 @@
<div v-tooltip="text" class="fzgwjkgc" :class="user.onlineStatus"></div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- user: {
- type: Object,
- required: true
- },
- },
+const props = defineProps<{
+ user: misskey.entities.User;
+}>();
- computed: {
- text(): string {
- switch (this.user.onlineStatus) {
- case 'online': return this.$ts.online;
- case 'active': return this.$ts.active;
- case 'offline': return this.$ts.offline;
- case 'unknown': return this.$ts.unknown;
- }
- }
+const text = $computed(() => {
+ switch (props.user.onlineStatus) {
+ case 'online': return i18n.locale.online;
+ case 'active': return i18n.locale.active;
+ case 'offline': return i18n.locale.offline;
+ case 'unknown': return i18n.locale.unknown;
}
});
</script>
diff --git a/packages/client/src/components/user-preview.vue b/packages/client/src/components/user-preview.vue
index f85a32fbe7..51c5330564 100644
--- a/packages/client/src/components/user-preview.vue
+++ b/packages/client/src/components/user-preview.vue
@@ -1,5 +1,5 @@
<template>
-<transition name="popup" appear @after-leave="$emit('closed')">
+<transition :name="$store.state.animation ? 'popup' : ''" appear @after-leave="$emit('closed')">
<div v-if="showing" class="fxxzrfni _popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { $emit('mouseover'); }" @mouseleave="() => { $emit('mouseleave'); }">
<div v-if="fetched" class="info">
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
diff --git a/packages/client/src/components/user-select-dialog.vue b/packages/client/src/components/user-select-dialog.vue
index ba2975478b..dbef34d547 100644
--- a/packages/client/src/components/user-select-dialog.vue
+++ b/packages/client/src/components/user-select-dialog.vue
@@ -1,5 +1,5 @@
<template>
-<XModalWindow ref="dialog"
+<XModalWindow ref="dialogEl"
:with-ok-button="true"
:ok-button-disabled="selected == null"
@click="cancel()"
@@ -8,20 +8,20 @@
@closed="$emit('closed')"
>
<template #header>{{ $ts.selectUser }}</template>
- <div class="tbhwbxda _monolithic_">
- <div class="_section">
- <div class="_inputSplit">
- <MkInput ref="username" v-model="username" class="input" @update:modelValue="search">
+ <div class="tbhwbxda">
+ <div class="form">
+ <FormSplit :min-width="170">
+ <MkInput ref="usernameEl" v-model="username" @update:modelValue="search">
<template #label>{{ $ts.username }}</template>
<template #prefix>@</template>
</MkInput>
- <MkInput v-model="host" class="input" @update:modelValue="search">
+ <MkInput v-model="host" @update:modelValue="search">
<template #label>{{ $ts.host }}</template>
<template #prefix>@</template>
</MkInput>
- </div>
+ </FormSplit>
</div>
- <div v-if="username != '' || host != ''" class="_section result" :class="{ hit: users.length > 0 }">
+ <div v-if="username != '' || host != ''" class="result" :class="{ hit: users.length > 0 }">
<div v-if="users.length > 0" class="users">
<div v-for="user in users" :key="user.id" class="user" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
@@ -35,7 +35,7 @@
<span>{{ $ts.noUsers }}</span>
</div>
</div>
- <div v-if="username == '' && host == ''" class="_section recent">
+ <div v-if="username == '' && host == ''" class="recent">
<div class="users">
<div v-for="user in recentUsers" :key="user.id" class="user" :class="{ selected: selected && selected.id === user.id }" @click="selected = user" @dblclick="ok()">
<MkAvatar :user="user" class="avatar" :show-indicator="true"/>
@@ -50,87 +50,89 @@
</XModalWindow>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkInput from './form/input.vue';
+<script lang="ts" setup>
+import { nextTick, onMounted } from 'vue';
+import * as misskey from 'misskey-js';
+import MkInput from '@/components/form/input.vue';
+import FormSplit from '@/components/form/split.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import * as os from '@/os';
+import { defaultStore } from '@/store';
-export default defineComponent({
- components: {
- MkInput,
- XModalWindow,
- },
-
- props: {
- },
+const emit = defineEmits<{
+ (e: 'ok', selected: misskey.entities.UserDetailed): void;
+ (e: 'cancel'): void;
+ (e: 'closed'): void;
+}>();
- emits: ['ok', 'cancel', 'closed'],
+let username = $ref('');
+let host = $ref('');
+let users: misskey.entities.UserDetailed[] = $ref([]);
+let recentUsers: misskey.entities.UserDetailed[] = $ref([]);
+let selected: misskey.entities.UserDetailed | null = $ref(null);
+let usernameEl: HTMLElement = $ref();
+let dialogEl = $ref();
- data() {
- return {
- username: '',
- host: '',
- recentUsers: [],
- users: [],
- selected: null,
- };
- },
+const focus = () => {
+ if (usernameEl) {
+ usernameEl.focus();
+ }
+};
- async mounted() {
- this.focus();
+const search = () => {
+ if (username === '' && host === '') {
+ users = [];
+ return;
+ }
+ os.api('users/search-by-username-and-host', {
+ username: username,
+ host: host,
+ limit: 10,
+ detail: false
+ }).then(_users => {
+ users = _users;
+ });
+};
- this.$nextTick(() => {
- this.focus();
- });
+const ok = () => {
+ if (selected == null) return;
+ emit('ok', selected);
+ dialogEl.close();
- this.recentUsers = await os.api('users/show', {
- userIds: this.$store.state.recentlyUsedUsers
- });
- },
+ // 最近使ったユーザー更新
+ let recents = defaultStore.state.recentlyUsedUsers;
+ recents = recents.filter(x => x !== selected.id);
+ recents.unshift(selected.id);
+ defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
+};
- methods: {
- search() {
- if (this.username == '' && this.host == '') {
- this.users = [];
- return;
- }
- os.api('users/search-by-username-and-host', {
- username: this.username,
- host: this.host,
- limit: 10,
- detail: false
- }).then(users => {
- this.users = users;
- });
- },
+const cancel = () => {
+ emit('cancel');
+ dialogEl.close();
+};
- focus() {
- this.$refs.username.focus();
- },
+onMounted(() => {
+ focus();
- ok() {
- this.$emit('ok', this.selected);
- this.$refs.dialog.close();
+ nextTick(() => {
+ focus();
+ });
- // 最近使ったユーザー更新
- let recents = this.$store.state.recentlyUsedUsers;
- recents = recents.filter(x => x !== this.selected.id);
- recents.unshift(this.selected.id);
- this.$store.set('recentlyUsedUsers', recents.splice(0, 16));
- },
-
- cancel() {
- this.$emit('cancel');
- this.$refs.dialog.close();
- },
- }
+ os.api('users/show', {
+ userIds: defaultStore.state.recentlyUsedUsers,
+ }).then(users => {
+ recentUsers = users;
+ });
});
</script>
<style lang="scss" scoped>
.tbhwbxda {
- > ._section {
+ > .form {
+ padding: 0 var(--root-margin);
+ }
+
+ > .result, > .recent {
display: flex;
flex-direction: column;
overflow: auto;
diff --git a/packages/client/src/components/visibility-picker.vue b/packages/client/src/components/visibility-picker.vue
index 4200f4354e..4b20063a51 100644
--- a/packages/client/src/components/visibility-picker.vue
+++ b/packages/client/src/components/visibility-picker.vue
@@ -1,28 +1,28 @@
<template>
-<MkModal ref="modal" :z-priority="'high'" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
+<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
<div class="gqyayizv _popup">
- <button key="public" class="_button" :class="{ active: v == 'public' }" data-index="1" @click="choose('public')">
+ <button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')">
<div><i class="fas fa-globe"></i></div>
<div>
<span>{{ $ts._visibility.public }}</span>
<span>{{ $ts._visibility.publicDescription }}</span>
</div>
</button>
- <button key="home" class="_button" :class="{ active: v == 'home' }" data-index="2" @click="choose('home')">
+ <button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')">
<div><i class="fas fa-home"></i></div>
<div>
<span>{{ $ts._visibility.home }}</span>
<span>{{ $ts._visibility.homeDescription }}</span>
</div>
</button>
- <button key="followers" class="_button" :class="{ active: v == 'followers' }" data-index="3" @click="choose('followers')">
+ <button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
<div><i class="fas fa-unlock"></i></div>
<div>
<span>{{ $ts._visibility.followers }}</span>
<span>{{ $ts._visibility.followersDescription }}</span>
</div>
</button>
- <button key="specified" :disabled="localOnly" class="_button" :class="{ active: v == 'specified' }" data-index="4" @click="choose('specified')">
+ <button key="specified" :disabled="localOnly" class="_button" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')">
<div><i class="fas fa-envelope"></i></div>
<div>
<span>{{ $ts._visibility.specified }}</span>
@@ -42,49 +42,40 @@
</MkModal>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { nextTick, watch } from 'vue';
+import * as misskey from 'misskey-js';
import MkModal from '@/components/ui/modal.vue';
-export default defineComponent({
- components: {
- MkModal,
- },
- props: {
- currentVisibility: {
- type: String,
- required: true
- },
- currentLocalOnly: {
- type: Boolean,
- required: true
- },
- src: {
- required: false
- },
- },
- emits: ['change-visibility', 'change-local-only', 'closed'],
- data() {
- return {
- v: this.currentVisibility,
- localOnly: this.currentLocalOnly,
- }
- },
- watch: {
- localOnly() {
- this.$emit('change-local-only', this.localOnly);
- }
- },
- methods: {
- choose(visibility) {
- this.v = visibility;
- this.$emit('change-visibility', visibility);
- this.$nextTick(() => {
- this.$refs.modal.close();
- });
- },
- }
+const modal = $ref<InstanceType<typeof MkModal>>();
+
+const props = withDefaults(defineProps<{
+ currentVisibility: typeof misskey.noteVisibilities[number];
+ currentLocalOnly: boolean;
+ src?: HTMLElement;
+}>(), {
+});
+
+const emit = defineEmits<{
+ (e: 'changeVisibility', v: typeof misskey.noteVisibilities[number]): void;
+ (e: 'changeLocalOnly', v: boolean): void;
+ (e: 'closed'): void;
+}>();
+
+let v = $ref(props.currentVisibility);
+let localOnly = $ref(props.currentLocalOnly);
+
+watch($$(localOnly), () => {
+ emit('changeLocalOnly', localOnly);
});
+
+function choose(visibility: typeof misskey.noteVisibilities[number]): void {
+ v = visibility;
+ emit('changeVisibility', visibility);
+ nextTick(() => {
+ modal.close();
+ });
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/components/waiting-dialog.vue b/packages/client/src/components/waiting-dialog.vue
index 10aedbd8f6..7dfcc55695 100644
--- a/packages/client/src/components/waiting-dialog.vue
+++ b/packages/client/src/components/waiting-dialog.vue
@@ -1,5 +1,5 @@
<template>
-<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="$emit('closed')">
+<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
<div class="iuyakobc" :class="{ iconOnly: (text == null) || success }">
<i v-if="success" class="fas fa-check icon success"></i>
<i v-else class="fas fa-spinner fa-pulse icon waiting"></i>
@@ -8,49 +8,30 @@
</MkModal>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { watch, ref } from 'vue';
import MkModal from '@/components/ui/modal.vue';
-export default defineComponent({
- components: {
- MkModal,
- },
+const modal = ref<InstanceType<typeof MkModal>>();
- props: {
- success: {
- type: Boolean,
- required: true,
- },
- showing: {
- type: Boolean,
- required: true,
- },
- text: {
- type: String,
- required: false,
- },
- },
+const props = defineProps<{
+ success: boolean;
+ showing: boolean;
+ text?: string;
+}>();
- emits: ['done', 'closed'],
+const emit = defineEmits<{
+ (e: 'done');
+ (e: 'closed');
+}>();
- data() {
- return {
- };
- },
-
- watch: {
- showing() {
- if (!this.showing) this.done();
- }
- },
+function done() {
+ emit('done');
+ modal.value.close();
+}
- methods: {
- done() {
- this.$emit('done');
- this.$refs.modal.close();
- },
- }
+watch(() => props.showing, () => {
+ if (!props.showing) done();
});
</script>
diff --git a/packages/client/src/components/widgets.vue b/packages/client/src/components/widgets.vue
index 12f7129253..ccde5fbe55 100644
--- a/packages/client/src/components/widgets.vue
+++ b/packages/client/src/components/widgets.vue
@@ -10,7 +10,7 @@
<MkButton inline @click="$emit('exit')">{{ $ts.close }}</MkButton>
</header>
<XDraggable
- v-model="_widgets"
+ v-model="widgets_"
item-key="id"
animation="150"
>
@@ -18,7 +18,7 @@
<div class="customize-container">
<button class="config _button" @click.prevent.stop="configWidget(element.id)"><i class="fas fa-cog"></i></button>
<button class="remove _button" @click.prevent.stop="removeWidget(element)"><i class="fas fa-times"></i></button>
- <component :is="`mkw-${element.name}`" :widget="element" :setting-callback="setting => settings[element.id] = setting" @updateProps="updateWidget(element.id, $event)"/>
+ <component :ref="el => widgetRefs[element.id] = el" :is="`mkw-${element.name}`" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
</div>
</template>
</XDraggable>
@@ -28,7 +28,7 @@
</template>
<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
+import { defineComponent, defineAsyncComponent, reactive, ref, computed } from 'vue';
import { v4 as uuid } from 'uuid';
import MkSelect from '@/components/form/select.vue';
import MkButton from '@/components/ui/button.vue';
@@ -54,50 +54,47 @@ export default defineComponent({
emits: ['updateWidgets', 'addWidget', 'removeWidget', 'updateWidget', 'exit'],
- data() {
- return {
- widgetAdderSelected: null,
- widgetDefs,
- settings: {},
+ setup(props, context) {
+ const widgetRefs = reactive({});
+ const configWidget = (id: string) => {
+ widgetRefs[id].configure();
};
- },
-
- computed: {
- _widgets: {
- get() {
- return this.widgets;
- },
- set(value) {
- this.$emit('updateWidgets', value);
- }
- }
- },
-
- methods: {
- configWidget(id) {
- this.settings[id]();
- },
+ const widgetAdderSelected = ref(null);
+ const addWidget = () => {
+ if (widgetAdderSelected.value == null) return;
- addWidget() {
- if (this.widgetAdderSelected == null) return;
-
- this.$emit('addWidget', {
- name: this.widgetAdderSelected,
+ context.emit('addWidget', {
+ name: widgetAdderSelected.value,
id: uuid(),
- data: {}
+ data: {},
});
- this.widgetAdderSelected = null;
- },
-
- removeWidget(widget) {
- this.$emit('removeWidget', widget);
- },
+ widgetAdderSelected.value = null;
+ };
+ const removeWidget = (widget) => {
+ context.emit('removeWidget', widget);
+ };
+ const updateWidget = (id, data) => {
+ context.emit('updateWidget', { id, data });
+ };
+ const widgets_ = computed({
+ get: () => props.widgets,
+ set: (value) => {
+ context.emit('updateWidgets', value);
+ },
+ });
- updateWidget(id, data) {
- this.$emit('updateWidget', { id, data });
- },
- }
+ return {
+ widgetRefs,
+ configWidget,
+ widgetAdderSelected,
+ widgetDefs,
+ addWidget,
+ removeWidget,
+ updateWidget,
+ widgets_,
+ };
+ },
});
</script>
diff --git a/packages/client/src/const.ts b/packages/client/src/const.ts
new file mode 100644
index 0000000000..505cf2748e
--- /dev/null
+++ b/packages/client/src/const.ts
@@ -0,0 +1,44 @@
+// ブラウザで直接表示することを許可するファイルの種類のリスト
+// ここに含まれないものは application/octet-stream としてレスポンスされる
+// SVGはXSSを生むので許可しない
+export const FILE_TYPE_BROWSERSAFE = [
+ // Images
+ 'image/png',
+ 'image/gif',
+ 'image/jpeg',
+ 'image/webp',
+ 'image/apng',
+ 'image/bmp',
+ 'image/tiff',
+ 'image/x-icon',
+
+ // OggS
+ 'audio/opus',
+ 'video/ogg',
+ 'audio/ogg',
+ 'application/ogg',
+
+ // ISO/IEC base media file format
+ 'video/quicktime',
+ 'video/mp4',
+ 'audio/mp4',
+ 'video/x-m4v',
+ 'audio/x-m4a',
+ 'video/3gpp',
+ 'video/3gpp2',
+
+ 'video/mpeg',
+ 'audio/mpeg',
+
+ 'video/webm',
+ 'audio/webm',
+
+ 'audio/aac',
+ 'audio/x-flac',
+ 'audio/vnd.wave',
+];
+/*
+https://github.com/sindresorhus/file-type/blob/main/supported.js
+https://github.com/sindresorhus/file-type/blob/main/core.js
+https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
+*/
diff --git a/packages/client/src/directives/anim.ts b/packages/client/src/directives/anim.ts
index 1ceef984d8..04e1c6a404 100644
--- a/packages/client/src/directives/anim.ts
+++ b/packages/client/src/directives/anim.ts
@@ -10,7 +10,7 @@ export default {
},
mounted(src, binding, vn) {
- setTimeout(() => {
+ window.setTimeout(() => {
src.style.opacity = '1';
src.style.transform = 'none';
}, 1);
diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts
index e14ee81dff..fffde14874 100644
--- a/packages/client/src/directives/tooltip.ts
+++ b/packages/client/src/directives/tooltip.ts
@@ -21,7 +21,7 @@ export default {
self.close = () => {
if (self._close) {
- clearInterval(self.checkTimer);
+ window.clearInterval(self.checkTimer);
self._close();
self._close = null;
}
@@ -61,19 +61,19 @@ export default {
});
el.addEventListener(start, () => {
- clearTimeout(self.showTimer);
- clearTimeout(self.hideTimer);
- self.showTimer = setTimeout(self.show, delay);
+ window.clearTimeout(self.showTimer);
+ window.clearTimeout(self.hideTimer);
+ self.showTimer = window.setTimeout(self.show, delay);
}, { passive: true });
el.addEventListener(end, () => {
- clearTimeout(self.showTimer);
- clearTimeout(self.hideTimer);
- self.hideTimer = setTimeout(self.close, delay);
+ window.clearTimeout(self.showTimer);
+ window.clearTimeout(self.hideTimer);
+ self.hideTimer = window.setTimeout(self.close, delay);
}, { passive: true });
el.addEventListener('click', () => {
- clearTimeout(self.showTimer);
+ window.clearTimeout(self.showTimer);
self.close();
});
},
@@ -85,6 +85,6 @@ export default {
unmounted(el, binding, vn) {
const self = el._tooltipDirective_;
- clearInterval(self.checkTimer);
+ window.clearInterval(self.checkTimer);
},
} as Directive;
diff --git a/packages/client/src/directives/user-preview.ts b/packages/client/src/directives/user-preview.ts
index 68d9e2816c..cdd2afa194 100644
--- a/packages/client/src/directives/user-preview.ts
+++ b/packages/client/src/directives/user-preview.ts
@@ -30,11 +30,11 @@ export class UserPreview {
source: this.el
}, {
mouseover: () => {
- clearTimeout(this.hideTimer);
+ window.clearTimeout(this.hideTimer);
},
mouseleave: () => {
- clearTimeout(this.showTimer);
- this.hideTimer = setTimeout(this.close, 500);
+ window.clearTimeout(this.showTimer);
+ this.hideTimer = window.setTimeout(this.close, 500);
},
}, 'closed');
@@ -44,10 +44,10 @@ export class UserPreview {
}
};
- this.checkTimer = setInterval(() => {
+ this.checkTimer = window.setInterval(() => {
if (!document.body.contains(this.el)) {
- clearTimeout(this.showTimer);
- clearTimeout(this.hideTimer);
+ window.clearTimeout(this.showTimer);
+ window.clearTimeout(this.hideTimer);
this.close();
}
}, 1000);
@@ -56,7 +56,7 @@ export class UserPreview {
@autobind
private close() {
if (this.promise) {
- clearInterval(this.checkTimer);
+ window.clearInterval(this.checkTimer);
this.promise.cancel();
this.promise = null;
}
@@ -64,21 +64,21 @@ export class UserPreview {
@autobind
private onMouseover() {
- clearTimeout(this.showTimer);
- clearTimeout(this.hideTimer);
- this.showTimer = setTimeout(this.show, 500);
+ window.clearTimeout(this.showTimer);
+ window.clearTimeout(this.hideTimer);
+ this.showTimer = window.setTimeout(this.show, 500);
}
@autobind
private onMouseleave() {
- clearTimeout(this.showTimer);
- clearTimeout(this.hideTimer);
- this.hideTimer = setTimeout(this.close, 500);
+ window.clearTimeout(this.showTimer);
+ window.clearTimeout(this.hideTimer);
+ this.hideTimer = window.setTimeout(this.close, 500);
}
@autobind
private onClick() {
- clearTimeout(this.showTimer);
+ window.clearTimeout(this.showTimer);
this.close();
}
@@ -94,7 +94,7 @@ export class UserPreview {
this.el.removeEventListener('mouseover', this.onMouseover);
this.el.removeEventListener('mouseleave', this.onMouseleave);
this.el.removeEventListener('click', this.onClick);
- clearInterval(this.checkTimer);
+ window.clearInterval(this.checkTimer);
}
}
diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts
index 82a1e169ce..af70aec70a 100644
--- a/packages/client/src/init.ts
+++ b/packages/client/src/init.ts
@@ -13,10 +13,8 @@ if (localStorage.getItem('accounts') != null) {
}
//#endregion
-import * as Sentry from '@sentry/browser';
-import { Integrations } from '@sentry/tracing';
import { computed, createApp, watch, markRaw, version as vueVersion } from 'vue';
-import compareVersions from 'compare-versions';
+import * as compareVersions from 'compare-versions';
import widgets from '@/widgets';
import directives from '@/directives';
@@ -26,7 +24,8 @@ import { router } from '@/router';
import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n } from '@/i18n';
-import { stream, confirm, alert, post, popup, toast } from '@/os';
+import { confirm, alert, post, popup, toast } from '@/os';
+import { stream } from '@/stream';
import * as sound from '@/scripts/sound';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
import { defaultStore, ColdDeviceStorage } from '@/store';
@@ -73,18 +72,6 @@ if (_DEV_) {
});
}
-if (defaultStore.state.reportError && !_DEV_) {
- Sentry.init({
- dsn: 'https://fd273254a07a4b61857607a9ea05d629@o501808.ingest.sentry.io/5583438',
- tracesSampleRate: 1.0,
- });
-
- Sentry.setTag('misskey_version', version);
- Sentry.setTag('ui', ui);
- Sentry.setTag('lang', lang);
- Sentry.setTag('host', host);
-}
-
// タッチデバイスでCSSの:hoverを機能させる
document.addEventListener('touchend', () => {}, { passive: true });
@@ -185,7 +172,6 @@ const app = createApp(await (
!$i ? import('@/ui/visitor.vue') :
ui === 'deck' ? import('@/ui/deck.vue') :
ui === 'desktop' ? import('@/ui/desktop.vue') :
- ui === 'chat' ? import('@/ui/chat/index.vue') :
ui === 'classic' ? import('@/ui/classic.vue') :
import('@/ui/universal.vue')
).then(x => x.default));
diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts
index bd155ba16d..184779f21f 100644
--- a/packages/client/src/menu.ts
+++ b/packages/client/src/menu.ts
@@ -163,22 +163,11 @@ export const menuDef = reactive({
icon: 'fas fa-laugh',
to: '/emojis',
},
- games: {
- title: 'games',
- icon: 'fas fa-gamepad',
- to: '/games/reversi',
- },
scratchpad: {
title: 'scratchpad',
icon: 'fas fa-terminal',
to: '/scratchpad',
},
- rooms: {
- title: 'rooms',
- icon: 'fas fa-door-closed',
- show: computed(() => $i != null),
- to: computed(() => `/@${$i.username}/room`),
- },
ui: {
title: 'switchUi',
icon: 'fas fa-columns',
@@ -204,13 +193,6 @@ export const menuDef = reactive({
localStorage.setItem('ui', 'classic');
unisonReload();
}
- }, {
- text: 'Chat (β)',
- active: ui === 'chat',
- action: () => {
- localStorage.setItem('ui', 'chat');
- unisonReload();
- }
}, /*{
text: i18n.locale.desktop + ' (β)',
active: ui === 'desktop',
diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts
index 4ed69e0ec0..c16ea717ad 100644
--- a/packages/client/src/os.ts
+++ b/packages/client/src/os.ts
@@ -4,19 +4,13 @@ import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vu
import { EventEmitter } from 'eventemitter3';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as Misskey from 'misskey-js';
-import * as Sentry from '@sentry/browser';
-import { apiUrl, debug, url } from '@/config';
+import { apiUrl, url } from '@/config';
import MkPostFormDialog from '@/components/post-form-dialog.vue';
import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { resolve } from '@/router';
import { $i } from '@/account';
-import { defaultStore } from '@/store';
-
-export const stream = markRaw(new Misskey.Stream(url, $i));
export const pendingApiRequestsCount = ref(0);
-let apiRequestsCount = 0; // for debug
-export const apiRequests = ref([]); // for debug
const apiClient = new Misskey.api.APIClient({
origin: url,
@@ -29,18 +23,6 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
pendingApiRequestsCount.value--;
};
- const log = debug ? reactive({
- id: ++apiRequestsCount,
- endpoint,
- req: markRaw(data),
- res: null,
- state: 'pending',
- }) : null;
- if (debug) {
- apiRequests.value.push(log);
- if (apiRequests.value.length > 128) apiRequests.value.shift();
- }
-
const promise = new Promise((resolve, reject) => {
// Append a credential
if ($i) (data as any).i = $i.token;
@@ -57,34 +39,10 @@ export const api = ((endpoint: string, data: Record<string, any> = {}, token?: s
if (res.status === 200) {
resolve(body);
- if (debug) {
- log!.res = markRaw(JSON.parse(JSON.stringify(body)));
- log!.state = 'success';
- }
} else if (res.status === 204) {
resolve();
- if (debug) {
- log!.state = 'success';
- }
} else {
reject(body.error);
- if (debug) {
- log!.res = markRaw(body.error);
- log!.state = 'failed';
- }
-
- if (defaultStore.state.reportError && !_DEV_) {
- Sentry.withScope((scope) => {
- scope.setTag('api_endpoint', endpoint);
- scope.setContext('api params', data);
- scope.setContext('api error info', body.info);
- scope.setTag('api_error_id', body.id);
- scope.setTag('api_error_code', body.code);
- scope.setTag('api_error_kind', body.kind);
- scope.setLevel(Sentry.Severity.Error);
- Sentry.captureMessage('API error');
- });
- }
}
}).catch(reject);
});
@@ -125,7 +83,7 @@ export function promiseDialog<T extends Promise<any>>(
onSuccess(res);
} else {
success.value = true;
- setTimeout(() => {
+ window.setTimeout(() => {
showing.value = false;
}, 1000);
}
@@ -181,7 +139,7 @@ export async function popup(component: Component | typeof import('*.vue') | Prom
const id = ++popupIdCount;
const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
- setTimeout(() => {
+ window.setTimeout(() => {
popups.value = popups.value.filter(popup => popup.id !== id);
}, 0);
};
@@ -371,7 +329,7 @@ export function select(props: {
export function success() {
return new Promise((resolve, reject) => {
const showing = ref(true);
- setTimeout(() => {
+ window.setTimeout(() => {
showing.value = false;
}, 1000);
popup(import('@/components/waiting-dialog.vue'), {
@@ -583,7 +541,7 @@ export const uploads = ref<{
img: string;
}[]>([]);
-export function upload(file: File, folder?: any, name?: string) {
+export function upload(file: File, folder?: any, name?: string): Promise<Misskey.entities.DriveFile> {
if (folder && typeof folder == 'object') folder = folder.id;
return new Promise((resolve, reject) => {
@@ -612,7 +570,7 @@ export function upload(file: File, folder?: any, name?: string) {
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.onload = (ev) => {
- if (ev.target == null || ev.target.response == null) {
+ if (xhr.status !== 200 || ev.target == null || ev.target.response == null) {
// TODO: 消すのではなくて再送できるようにしたい
uploads.value = uploads.value.filter(x => x.id != id);
diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue
index 2f8f08b5cf..7540995707 100644
--- a/packages/client/src/pages/_error_.vue
+++ b/packages/client/src/pages/_error_.vue
@@ -1,68 +1,61 @@
<template>
-<MkLoading v-if="!loaded" />
+<MkLoading v-if="!loaded"/>
<transition :name="$store.state.animation ? 'zoom' : ''" appear>
<div v-show="loaded" class="mjndxjch">
<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 v-if="version === meta.version">{{ $ts.pageLoadErrorDescription }}</p>
- <p v-else-if="serverIsDead">{{ $ts.serverIsDead }}</p>
+ <p><b><i class="fas fa-exclamation-triangle"></i> {{ i18n.locale.pageLoadError }}</b></p>
+ <p v-if="meta && (version === meta.version)">{{ i18n.locale.pageLoadErrorDescription }}</p>
+ <p v-else-if="serverIsDead">{{ i18n.locale.serverIsDead }}</p>
<template v-else>
- <p>{{ $ts.newVersionOfClientAvailable }}</p>
- <p>{{ $ts.youShouldUpgradeClient }}</p>
- <MkButton class="button primary" @click="reload">{{ $ts.reload }}</MkButton>
+ <p>{{ i18n.locale.newVersionOfClientAvailable }}</p>
+ <p>{{ i18n.locale.youShouldUpgradeClient }}</p>
+ <MkButton class="button primary" @click="reload">{{ i18n.locale.reload }}</MkButton>
</template>
- <p><MkA to="/docs/general/troubleshooting" class="_link">{{ $ts.troubleshooting }}</MkA></p>
+ <p><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.locale.troubleshooting }}</MkA></p>
<p v-if="error" class="error">ERROR: {{ error }}</p>
</div>
</transition>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
import MkButton from '@/components/ui/button.vue';
import * as symbols from '@/symbols';
import { version } from '@/config';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkButton,
- },
- props: {
- error: {
- required: false,
- }
- },
- data() {
- return {
- [symbols.PAGE_INFO]: {
- 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();
- },
+const props = withDefaults(defineProps<{
+ error?: Error;
+}>(), {
+});
+
+let loaded = $ref(false);
+let serverIsDead = $ref(false);
+let meta = $ref<misskey.entities.LiteInstanceMetadata | null>(null);
+
+os.api('meta', {
+ detail: false,
+}).then(res => {
+ loaded = true;
+ serverIsDead = false;
+ meta = res;
+ localStorage.setItem('v', res.version);
+}, () => {
+ loaded = true;
+ serverIsDead = true;
+});
+
+function reload() {
+ unisonReload();
+}
+
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.error,
+ icon: 'fas fa-exclamation-triangle',
},
});
</script>
diff --git a/packages/client/src/pages/_loading_.vue b/packages/client/src/pages/_loading_.vue
index 05c6af1cd7..1dd2e46e10 100644
--- a/packages/client/src/pages/_loading_.vue
+++ b/packages/client/src/pages/_loading_.vue
@@ -2,9 +2,5 @@
<MkLoading/>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
-
-export default defineComponent({});
+<script lang="ts" setup>
</script>
diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue
index 855a21e493..8119f33051 100644
--- a/packages/client/src/pages/about-misskey.vue
+++ b/packages/client/src/pages/about-misskey.vue
@@ -3,36 +3,39 @@
<MkSpacer :content-max="600" :margin-min="20">
<div class="_formRoot znqjceqz">
<div id="debug"></div>
- <div ref="about" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
+ <div ref="containerEl" v-panel class="_formBlock about" :class="{ playing: easterEggEngine != null }">
<img src="/client-assets/about-icon.png" alt="" class="icon" draggable="false" @load="iconLoaded" @click="gravity"/>
<div class="misskey">Misskey</div>
<div class="version">v{{ version }}</div>
<span v-for="emoji in easterEggEmojis" :key="emoji.id" class="emoji" :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>
<div class="_formBlock" style="text-align: center;">
- {{ $ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ $ts.learnMore }}</a>
+ {{ i18n.locale._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.locale.learnMore }}</a>
+ </div>
+ <div class="_formBlock" style="text-align: center;">
+ <MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
</div>
<FormSection>
<div class="_formLinks">
<FormLink to="https://github.com/misskey-dev/misskey" external>
<template #icon><i class="fas fa-code"></i></template>
- {{ $ts._aboutMisskey.source }}
+ {{ i18n.locale._aboutMisskey.source }}
<template #suffix>GitHub</template>
</FormLink>
<FormLink to="https://crowdin.com/project/misskey" external>
<template #icon><i class="fas fa-language"></i></template>
- {{ $ts._aboutMisskey.translation }}
+ {{ i18n.locale._aboutMisskey.translation }}
<template #suffix>Crowdin</template>
</FormLink>
<FormLink to="https://www.patreon.com/syuilo" external>
<template #icon><i class="fas fa-hand-holding-medical"></i></template>
- {{ $ts._aboutMisskey.donate }}
+ {{ i18n.locale._aboutMisskey.donate }}
<template #suffix>Patreon</template>
</FormLink>
</div>
</FormSection>
<FormSection>
- <template #label>{{ $ts._aboutMisskey.contributors }}</template>
+ <template #label>{{ i18n.locale._aboutMisskey.contributors }}</template>
<div class="_formLinks">
<FormLink to="https://github.com/syuilo" external>@syuilo</FormLink>
<FormLink to="https://github.com/AyaMorisawa" external>@AyaMorisawa</FormLink>
@@ -44,27 +47,30 @@
<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
</div>
- <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template>
+ <template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.locale._aboutMisskey.allContributors }}</MkLink></template>
</FormSection>
<FormSection>
- <template #label><Mfm text="$[jelly ❤]"/> {{ $ts._aboutMisskey.patrons }}</template>
+ <template #label><Mfm text="$[jelly ❤]"/> {{ i18n.locale._aboutMisskey.patrons }}</template>
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
- <template #caption>{{ $ts._aboutMisskey.morePatrons }}</template>
+ <template #caption>{{ i18n.locale._aboutMisskey.morePatrons }}</template>
</FormSection>
</div>
</MkSpacer>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { nextTick, onBeforeUnmount } from 'vue';
import { version } from '@/config';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
-import MkKeyValue from '@/components/key-value.vue';
+import MkButton from '@/components/ui/button.vue';
import MkLink from '@/components/link.vue';
import { physics } from '@/scripts/physics';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
+import * as os from '@/os';
const patrons = [
'まっちゃとーにゅ',
@@ -145,59 +151,53 @@ const patrons = [
'蝉暮せせせ',
];
-export default defineComponent({
- components: {
- FormSection,
- FormLink,
- MkKeyValue,
- MkLink,
- },
+let easterEggReady = false;
+let easterEggEmojis = $ref([]);
+let easterEggEngine = $ref(null);
+const containerEl = $ref<HTMLElement>();
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.aboutMisskey,
- icon: null
- },
- version,
- patrons,
- easterEggReady: false,
- easterEggEmojis: [],
- easterEggEngine: null,
- }
- },
+function iconLoaded() {
+ const emojis = defaultStore.state.reactions;
+ const containerWidth = containerEl.offsetWidth;
+ for (let i = 0; i < 32; i++) {
+ easterEggEmojis.push({
+ id: i.toString(),
+ top: -(128 + (Math.random() * 256)),
+ left: (Math.random() * containerWidth),
+ emoji: emojis[Math.floor(Math.random() * emojis.length)],
+ });
+ }
- beforeUnmount() {
- if (this.easterEggEngine) {
- this.easterEggEngine.stop();
- }
- },
+ nextTick(() => {
+ easterEggReady = true;
+ });
+}
- methods: {
- iconLoaded() {
- const emojis = this.$store.state.reactions;
- const containerWidth = this.$refs.about.offsetWidth;
- for (let i = 0; i < 32; i++) {
- this.easterEggEmojis.push({
- id: i.toString(),
- top: -(128 + (Math.random() * 256)),
- left: (Math.random() * containerWidth),
- emoji: emojis[Math.floor(Math.random() * emojis.length)],
- });
- }
+function gravity() {
+ if (!easterEggReady) return;
+ easterEggReady = false;
+ easterEggEngine = physics(containerEl);
+}
- this.$nextTick(() => {
- this.easterEggReady = true;
- });
- },
+function iLoveMisskey() {
+ os.post({
+ initialText: 'I $[jelly ❤] #Misskey',
+ });
+}
- gravity() {
- if (!this.easterEggReady) return;
- this.easterEggReady = false;
- this.easterEggEngine = physics(this.$refs.about);
- }
+onBeforeUnmount(() => {
+ if (easterEggEngine) {
+ easterEggEngine.stop();
}
});
+
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.aboutMisskey,
+ icon: null,
+ bg: 'var(--bg)',
+ },
+});
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue
index 04f68b7201..a5984c548d 100644
--- a/packages/client/src/pages/about.vue
+++ b/packages/client/src/pages/about.vue
@@ -24,7 +24,7 @@
</FormSection>
<FormSection>
- <div class="_inputSplit _formBlock">
+ <FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.administrator }}</template>
<template #value>{{ $instance.maintainerName }}</template>
@@ -33,14 +33,14 @@
<template #key>{{ $ts.contact }}</template>
<template #value>{{ $instance.maintainerEmail }}</template>
</MkKeyValue>
- </div>
+ </FormSplit>
<FormLink v-if="$instance.tosUrl" :to="$instance.tosUrl" class="_formBlock" external>{{ $ts.tos }}</FormLink>
</FormSection>
<FormSuspense :p="initStats">
<FormSection>
<template #label>{{ $ts.statistics }}</template>
- <div class="_inputSplit">
+ <FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.users }}</template>
<template #value>{{ number(stats.originalUsersCount) }}</template>
@@ -49,7 +49,7 @@
<template #key>{{ $ts.notes }}</template>
<template #value>{{ number(stats.originalNotesCount) }}</template>
</MkKeyValue>
- </div>
+ </FormSplit>
</FormSection>
</FormSuspense>
@@ -67,46 +67,33 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref } from 'vue';
import { version, instanceName } from '@/config';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.vue';
+import FormSplit from '@/components/form/split.vue';
import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import number from '@/filters/number';
import * as symbols from '@/symbols';
import { host } from '@/config';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkKeyValue,
- FormSection,
- FormLink,
- FormSuspense,
- },
+const stats = ref(null);
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.instanceInfo,
- icon: 'fas fa-info-circle'
- },
- host,
- version,
- instanceName,
- stats: null,
- initStats: () => os.api('stats', {
- }).then((stats) => {
- this.stats = stats;
- })
- }
- },
+const initStats = () => os.api('stats', {
+}).then((res) => {
+ stats.value = res;
+});
- methods: {
- number
- }
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.instanceInfo,
+ icon: 'fas fa-info-circle',
+ bg: 'var(--bg)',
+ },
});
</script>
diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue
index 8df20097b3..92f93797ce 100644
--- a/packages/client/src/pages/admin/abuses.vue
+++ b/packages/client/src/pages/admin/abuses.vue
@@ -34,27 +34,7 @@
-->
<MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);">
- <div v-for="report in items" :key="report.id" class="bcekxzvu _card _gap">
- <div class="_content target">
- <MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
- <div class="info">
- <MkUserName class="name" :user="report.targetUser"/>
- <div class="acct">@{{ acct(report.targetUser) }}</div>
- </div>
- </div>
- <div class="_content">
- <div>
- <Mfm :text="report.comment"/>
- </div>
- <hr>
- <div>Reporter: <MkAcct :user="report.reporter"/></div>
- <div><MkTime :time="report.createdAt"/></div>
- </div>
- <div class="_footer">
- <div v-if="report.assignee">Assignee: <MkAcct :user="report.assignee"/></div>
- <MkButton v-if="!report.resolved" primary @click="resolve(report)">{{ $ts.abuseMarkAsResolved }}</MkButton>
- </div>
- </div>
+ <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
</MkPagination>
</div>
</div>
@@ -62,22 +42,21 @@
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
-import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
-import { acct } from '@/filters/user';
+import XAbuseReport from '@/components/abuse-report.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
- MkButton,
MkInput,
MkSelect,
MkPagination,
+ XAbuseReport,
},
emits: ['info'],
@@ -95,44 +74,20 @@ export default defineComponent({
reporterOrigin: 'combined',
targetUserOrigin: 'combined',
pagination: {
- endpoint: 'admin/abuse-user-reports',
+ endpoint: 'admin/abuse-user-reports' as const,
limit: 10,
- params: () => ({
+ params: computed(() => ({
state: this.state,
reporterOrigin: this.reporterOrigin,
targetUserOrigin: this.targetUserOrigin,
- }),
+ })),
},
}
},
- watch: {
- state() {
- this.$refs.reports.reload();
- },
-
- reporterOrigin() {
- this.$refs.reports.reload();
- },
-
- targetUserOrigin() {
- this.$refs.reports.reload();
- },
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
- acct,
-
- resolve(report) {
- os.apiWithDialog('admin/resolve-abuse-user-report', {
- reportId: report.id,
- }).then(() => {
- this.$refs.reports.removeItem(item => item.id === report.id);
- });
+ resolved(reportId) {
+ this.$refs.reports.removeItem(item => item.id === reportId);
},
}
});
@@ -142,29 +97,4 @@ export default defineComponent({
.lcixvhis {
margin: var(--margin);
}
-
-.bcekxzvu {
- > .target {
- display: flex;
- width: 100%;
- box-sizing: border-box;
- text-align: left;
- align-items: center;
-
- > .avatar {
- width: 42px;
- height: 42px;
- }
-
- > .info {
- margin-left: 0.3em;
- padding: 0 8px;
- flex: 1;
-
- > .name {
- font-weight: bold;
- }
- }
- }
-}
</style>
diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue
index d12ed8563e..8f164caa99 100644
--- a/packages/client/src/pages/admin/ads.vue
+++ b/packages/client/src/pages/admin/ads.vue
@@ -23,14 +23,14 @@
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
</div>
-->
- <div class="_inputSplit">
+ <FormSplit>
<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>
- </div>
+ </FormSplit>
<MkTextarea v-model="ad.memo" class="_formBlock">
<template #label>{{ $ts.memo }}</template>
</MkTextarea>
@@ -49,6 +49,7 @@ import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
+import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
@@ -58,6 +59,7 @@ export default defineComponent({
MkInput,
MkTextarea,
FormRadios,
+ FormSplit,
},
emits: ['info'],
@@ -85,10 +87,6 @@ export default defineComponent({
});
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
add() {
this.ads.unshift({
diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue
index 3614cb1441..a0d720bb29 100644
--- a/packages/client/src/pages/admin/announcements.vue
+++ b/packages/client/src/pages/admin/announcements.vue
@@ -61,10 +61,6 @@ export default defineComponent({
});
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
add() {
this.announcements.unshift({
diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue
index 5a97083841..82ab155317 100644
--- a/packages/client/src/pages/admin/bot-protection.vue
+++ b/packages/client/src/pages/admin/bot-protection.vue
@@ -1,70 +1,55 @@
<template>
-<FormBase>
+<div>
<FormSuspense :p="init">
- <FormRadios v-model="provider">
- <template #desc><i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}</template>
- <option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
- <option value="hcaptcha">hCaptcha</option>
- <option value="recaptcha">reCAPTCHA</option>
- </FormRadios>
+ <div class="_formRoot">
+ <FormRadios v-model="provider" class="_formBlock">
+ <option :value="null">{{ $ts.none }} ({{ $ts.notRecommended }})</option>
+ <option value="hcaptcha">hCaptcha</option>
+ <option value="recaptcha">reCAPTCHA</option>
+ </FormRadios>
- <template v-if="provider === 'hcaptcha'">
- <div v-sticky-container class="_debobigegoItem _debobigegoNoConcat">
- <div class="_debobigegoLabel">hCaptcha</div>
- <div class="main">
- <FormInput v-model="hcaptchaSiteKey">
- <template #prefix><i class="fas fa-key"></i></template>
- <span>{{ $ts.hcaptchaSiteKey }}</span>
- </FormInput>
- <FormInput v-model="hcaptchaSecretKey">
- <template #prefix><i class="fas fa-key"></i></template>
- <span>{{ $ts.hcaptchaSecretKey }}</span>
- </FormInput>
- </div>
- </div>
- <div v-sticky-container class="_debobigegoItem _debobigegoNoConcat">
- <div class="_debobigegoLabel">{{ $ts.preview }}</div>
- <div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
+ <template v-if="provider === 'hcaptcha'">
+ <FormInput v-model="hcaptchaSiteKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>{{ $ts.hcaptchaSiteKey }}</template>
+ </FormInput>
+ <FormInput v-model="hcaptchaSecretKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>{{ $ts.hcaptchaSecretKey }}</template>
+ </FormInput>
+ <FormSlot class="_formBlock">
+ <template #label>{{ $ts.preview }}</template>
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
- </div>
- </div>
- </template>
- <template v-else-if="provider === 'recaptcha'">
- <div v-sticky-container class="_debobigegoItem _debobigegoNoConcat">
- <div class="_debobigegoLabel">reCAPTCHA</div>
- <div class="main">
- <FormInput v-model="recaptchaSiteKey">
- <template #prefix><i class="fas fa-key"></i></template>
- <span>{{ $ts.recaptchaSiteKey }}</span>
- </FormInput>
- <FormInput v-model="recaptchaSecretKey">
- <template #prefix><i class="fas fa-key"></i></template>
- <span>{{ $ts.recaptchaSecretKey }}</span>
- </FormInput>
- </div>
- </div>
- <div v-if="recaptchaSiteKey" v-sticky-container class="_debobigegoItem _debobigegoNoConcat">
- <div class="_debobigegoLabel">{{ $ts.preview }}</div>
- <div class="_debobigegoPanel" style="padding: var(--debobigegoContentHMargin);">
+ </FormSlot>
+ </template>
+ <template v-else-if="provider === 'recaptcha'">
+ <FormInput v-model="recaptchaSiteKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>{{ $ts.recaptchaSiteKey }}</template>
+ </FormInput>
+ <FormInput v-model="recaptchaSecretKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>{{ $ts.recaptchaSecretKey }}</template>
+ </FormInput>
+ <FormSlot v-if="recaptchaSiteKey" class="_formBlock">
+ <template #label>{{ $ts.preview }}</template>
<MkCaptcha provider="recaptcha" :sitekey="recaptchaSiteKey"/>
- </div>
- </div>
- </template>
+ </FormSlot>
+ </template>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </div>
</FormSuspense>
-</FormBase>
+</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import FormRadios from '@/components/debobigego/radios.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormInput from '@/components/form/input.vue';
+import FormButton from '@/components/ui/button.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
@@ -73,11 +58,9 @@ export default defineComponent({
components: {
FormRadios,
FormInput,
- FormBase,
- FormGroup,
FormButton,
- FormInfo,
FormSuspense,
+ FormSlot,
MkCaptcha: defineAsyncComponent(() => import('@/components/captcha.vue')),
},
@@ -99,10 +82,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue
index b09f1ad867..3a835eeafa 100644
--- a/packages/client/src/pages/admin/database.vue
+++ b/packages/client/src/pages/admin/database.vue
@@ -1,28 +1,18 @@
<template>
-<FormBase>
+<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
- <FormGroup v-for="table in database" :key="table[0]">
- <template #label>{{ table[0] }}</template>
- <FormKeyValueView>
- <template #key>Size</template>
- <template #value>{{ bytes(table[1].size) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>Records</template>
- <template #value>{{ number(table[1].count) }}</template>
- </FormKeyValueView>
- </FormGroup>
+ <MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
+ <template #key>{{ table[0] }}</template>
+ <template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template>
+ </MkKeyValue>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import bytes from '@/filters/bytes';
@@ -31,10 +21,7 @@ import number from '@/filters/number';
export default defineComponent({
components: {
FormSuspense,
- FormKeyValueView,
- FormBase,
- FormGroup,
- FormLink,
+ MkKeyValue,
},
emits: ['info'],
@@ -50,10 +37,6 @@ export default defineComponent({
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
bytes, number,
}
diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue
index 873a853918..6491a453ab 100644
--- a/packages/client/src/pages/admin/email-settings.vue
+++ b/packages/client/src/pages/admin/email-settings.vue
@@ -1,50 +1,55 @@
<template>
-<FormBase>
+<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <FormSwitch v-model="enableEmail">{{ $ts.enableEmail }}<template #desc>{{ $ts.emailConfigInfo }}</template></FormSwitch>
+ <div class="_formRoot">
+ <FormSwitch v-model="enableEmail" class="_formBlock">
+ <template #label>{{ $ts.enableEmail }}</template>
+ <template #caption>{{ $ts.emailConfigInfo }}</template>
+ </FormSwitch>
- <template v-if="enableEmail">
- <FormInput v-model="email" type="email">
- <span>{{ $ts.emailAddress }}</span>
- </FormInput>
+ <template v-if="enableEmail">
+ <FormInput v-model="email" type="email" class="_formBlock">
+ <template #label>{{ $ts.emailAddress }}</template>
+ </FormInput>
- <div v-sticky-container class="_debobigegoItem _debobigegoNoConcat">
- <div class="_debobigegoLabel">{{ $ts.smtpConfig }}</div>
- <div class="main">
- <FormInput v-model="smtpHost">
- <span>{{ $ts.smtpHost }}</span>
- </FormInput>
- <FormInput v-model="smtpPort" type="number">
- <span>{{ $ts.smtpPort }}</span>
- </FormInput>
- <FormInput v-model="smtpUser">
- <span>{{ $ts.smtpUser }}</span>
- </FormInput>
- <FormInput v-model="smtpPass" type="password">
- <span>{{ $ts.smtpPass }}</span>
- </FormInput>
- <FormInfo>{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
- <FormSwitch v-model="smtpSecure">{{ $ts.smtpSecure }}<template #desc>{{ $ts.smtpSecureInfo }}</template></FormSwitch>
- </div>
- </div>
-
- <FormButton @click="testEmail">{{ $ts.testEmail }}</FormButton>
- </template>
-
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ <FormSection>
+ <template #label>{{ $ts.smtpConfig }}</template>
+ <FormSplit :min-width="280">
+ <FormInput v-model="smtpHost" class="_formBlock">
+ <template #label>{{ $ts.smtpHost }}</template>
+ </FormInput>
+ <FormInput v-model="smtpPort" type="number" class="_formBlock">
+ <template #label>{{ $ts.smtpPort }}</template>
+ </FormInput>
+ </FormSplit>
+ <FormSplit :min-width="280">
+ <FormInput v-model="smtpUser" class="_formBlock">
+ <template #label>{{ $ts.smtpUser }}</template>
+ </FormInput>
+ <FormInput v-model="smtpPass" type="password" class="_formBlock">
+ <template #label>{{ $ts.smtpPass }}</template>
+ </FormInput>
+ </FormSplit>
+ <FormInfo class="_formBlock">{{ $ts.emptyToDisableSmtpAuth }}</FormInfo>
+ <FormSwitch v-model="smtpSecure" class="_formBlock">
+ <template #label>{{ $ts.smtpSecure }}</template>
+ <template #caption>{{ $ts.smtpSecureInfo }}</template>
+ </FormSwitch>
+ </FormSection>
+ </template>
+ </div>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormInput from '@/components/form/input.vue';
+import FormInfo from '@/components/ui/info.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import FormSplit from '@/components/form/split.vue';
+import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
@@ -53,9 +58,8 @@ export default defineComponent({
components: {
FormSwitch,
FormInput,
- FormBase,
- FormGroup,
- FormButton,
+ FormSplit,
+ FormSection,
FormInfo,
FormSuspense,
},
@@ -68,6 +72,16 @@ export default defineComponent({
title: this.$ts.emailServer,
icon: 'fas fa-envelope',
bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ text: this.$ts.testEmail,
+ handler: this.testEmail,
+ }, {
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: this.$ts.save,
+ handler: this.save,
+ }],
},
enableEmail: false,
email: null,
@@ -79,10 +93,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/emoji-edit-dialog.vue b/packages/client/src/pages/admin/emoji-edit-dialog.vue
index a45d92fa16..2e3903426e 100644
--- a/packages/client/src/pages/admin/emoji-edit-dialog.vue
+++ b/packages/client/src/pages/admin/emoji-edit-dialog.vue
@@ -95,7 +95,7 @@ export default defineComponent({
});
if (canceled) return;
- os.api('admin/emoji/remove', {
+ os.api('admin/emoji/delete', {
id: this.emoji.id
}).then(() => {
this.$emit('done', {
diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue
index 49277325a0..5b1dfe565a 100644
--- a/packages/client/src/pages/admin/emojis.vue
+++ b/packages/client/src/pages/admin/emojis.vue
@@ -6,11 +6,22 @@
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
</MkInput>
- <MkPagination ref="emojis" :pagination="pagination">
+ <MkSwitch v-model="selectMode" style="margin: 8px 0;">
+ <template #label>Select mode</template>
+ </MkSwitch>
+ <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <MkButton inline @click="selectAll">Select all</MkButton>
+ <MkButton inline @click="setCategoryBulk">Set category</MkButton>
+ <MkButton inline @click="addTagBulk">Add tag</MkButton>
+ <MkButton inline @click="removeTagBulk">Remove tag</MkButton>
+ <MkButton inline @click="setTagBulk">Set tag</MkButton>
+ <MkButton inline danger @click="delBulk">Delete</MkButton>
+ </div>
+ <MkPagination ref="emojisPaginationComponent" :pagination="pagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}">
<div class="ldhfsamy">
- <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="edit(emoji)">
+ <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)">
<img :src="emoji.url" class="img" :alt="emoji.name"/>
<div class="body">
<div class="name _monospace">{{ emoji.name }}</div>
@@ -23,7 +34,7 @@
</div>
<div v-else-if="tab === 'remote'" class="remote">
- <div class="_inputSplit">
+ <FormSplit>
<MkInput v-model="queryRemote" :debounce="true" type="search">
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.search }}</template>
@@ -31,8 +42,8 @@
<MkInput v-model="host" :debounce="true">
<template #label>{{ $ts.host }}</template>
</MkInput>
- </div>
- <MkPagination ref="remoteEmojis" :pagination="remotePagination">
+ </FormSplit>
+ <MkPagination :pagination="remotePagination">
<template #empty><span>{{ $ts.noCustomEmojis }}</span></template>
<template v-slot="{items}">
<div class="ldhfsamy">
@@ -51,146 +62,233 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { computed, defineComponent, toRef } from 'vue';
+<script lang="ts" setup>
+import { computed, defineComponent, ref, toRef } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkTab from '@/components/tab.vue';
-import { selectFiles } from '@/scripts/select-file';
+import MkSwitch from '@/components/form/switch.vue';
+import FormSplit from '@/components/form/split.vue';
+import { selectFile, selectFiles } from '@/scripts/select-file';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkTab,
- MkButton,
- MkInput,
- MkPagination,
- },
+const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>();
- emits: ['info'],
+const tab = ref('local');
+const query = ref(null);
+const queryRemote = ref(null);
+const host = ref(null);
+const selectMode = ref(false);
+const selectedEmojis = ref<string[]>([]);
- data() {
- return {
- [symbols.PAGE_INFO]: 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,
- }, {
- icon: 'fas fa-ellipsis-h',
- handler: this.menu,
- }],
- 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,
- host: '',
- pagination: {
- endpoint: 'admin/emoji/list',
- limit: 30,
- params: computed(() => ({
- query: (this.query && this.query !== '') ? this.query : null
- }))
- },
- remotePagination: {
- endpoint: 'admin/emoji/list-remote',
- limit: 30,
- params: computed(() => ({
- query: (this.queryRemote && this.queryRemote !== '') ? this.queryRemote : null,
- host: (this.host && this.host !== '') ? this.host : null
- }))
- },
- }
- },
+const pagination = {
+ endpoint: 'admin/emoji/list' as const,
+ limit: 30,
+ params: computed(() => ({
+ query: (query.value && query.value !== '') ? query.value : null,
+ })),
+};
- async mounted() {
- this.$emit('info', toRef(this, symbols.PAGE_INFO));
- },
+const remotePagination = {
+ endpoint: 'admin/emoji/list-remote' as const,
+ limit: 30,
+ params: computed(() => ({
+ query: (queryRemote.value && queryRemote.value !== '') ? queryRemote.value : null,
+ host: (host.value && host.value !== '') ? host.value : null,
+ })),
+};
- methods: {
- async add(e) {
- const files = await selectFiles(e.currentTarget || e.target, null);
+const selectAll = () => {
+ if (selectedEmojis.value.length > 0) {
+ selectedEmojis.value = [];
+ } else {
+ selectedEmojis.value = emojisPaginationComponent.value.items.map(item => item.id);
+ }
+};
- const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
- fileId: file.id,
- })));
- promise.then(() => {
- this.$refs.emojis.reload();
- });
- os.promiseDialog(promise);
- },
+const toggleSelect = (emoji) => {
+ if (selectedEmojis.value.includes(emoji.id)) {
+ selectedEmojis.value = selectedEmojis.value.filter(x => x !== emoji.id);
+ } else {
+ selectedEmojis.value.push(emoji.id);
+ }
+};
- edit(emoji) {
- os.popup(import('./emoji-edit-dialog.vue'), {
- emoji: emoji
- }, {
- done: result => {
- if (result.updated) {
- this.$refs.emojis.replaceItem(item => item.id === emoji.id, {
- ...emoji,
- ...result.updated
- });
- } else if (result.deleted) {
- this.$refs.emojis.removeItem(item => item.id === emoji.id);
- }
- },
- }, 'closed');
- },
+const add = async (ev: MouseEvent) => {
+ const files = await selectFiles(ev.currentTarget || ev.target, null);
- im(emoji) {
- os.apiWithDialog('admin/emoji/copy', {
- emojiId: emoji.id,
- });
- },
+ const promise = Promise.all(files.map(file => os.api('admin/emoji/add', {
+ fileId: file.id,
+ })));
+ promise.then(() => {
+ emojisPaginationComponent.value.reload();
+ });
+ os.promiseDialog(promise);
+};
- remoteMenu(emoji, ev) {
- os.popupMenu([{
- type: 'label',
- text: ':' + emoji.name + ':',
- }, {
- text: this.$ts.import,
- icon: 'fas fa-plus',
- action: () => { this.im(emoji) }
- }], ev.currentTarget || ev.target);
+const edit = (emoji) => {
+ os.popup(import('./emoji-edit-dialog.vue'), {
+ emoji: emoji
+ }, {
+ done: result => {
+ if (result.updated) {
+ emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, {
+ ...emoji,
+ ...result.updated
+ });
+ } else if (result.deleted) {
+ emojisPaginationComponent.value.removeItem(item => item.id === emoji.id);
+ }
},
+ }, 'closed');
+};
- menu(ev) {
- os.popupMenu([{
- icon: 'fas fa-download',
- text: this.$ts.export,
- action: async () => {
- os.api('export-custom-emojis', {
- })
- .then(() => {
- os.alert({
- type: 'info',
- text: this.$ts.exportRequested,
- });
- }).catch((e) => {
- os.alert({
- type: 'error',
- text: e.message,
- });
- });
- }
- }], ev.currentTarget || ev.target);
+const im = (emoji) => {
+ os.apiWithDialog('admin/emoji/copy', {
+ emojiId: emoji.id,
+ });
+};
+
+const remoteMenu = (emoji, ev: MouseEvent) => {
+ os.popupMenu([{
+ type: 'label',
+ text: ':' + emoji.name + ':',
+ }, {
+ text: i18n.locale.import,
+ icon: 'fas fa-plus',
+ action: () => { im(emoji) }
+ }], ev.currentTarget || ev.target);
+};
+
+const menu = (ev: MouseEvent) => {
+ os.popupMenu([{
+ icon: 'fas fa-download',
+ text: i18n.locale.export,
+ action: async () => {
+ os.api('export-custom-emojis', {
+ })
+ .then(() => {
+ os.alert({
+ type: 'info',
+ text: i18n.locale.exportRequested,
+ });
+ }).catch((e) => {
+ os.alert({
+ type: 'error',
+ text: e.message,
+ });
+ });
}
- }
+ }, {
+ icon: 'fas fa-upload',
+ text: i18n.locale.import,
+ action: async () => {
+ const file = await selectFile(ev.currentTarget || ev.target);
+ os.api('admin/emoji/import-zip', {
+ fileId: file.id,
+ })
+ .then(() => {
+ os.alert({
+ type: 'info',
+ text: i18n.locale.importRequested,
+ });
+ }).catch((e) => {
+ os.alert({
+ type: 'error',
+ text: e.message,
+ });
+ });
+ }
+ }], ev.currentTarget || ev.target);
+};
+
+const setCategoryBulk = async () => {
+ const { canceled, result } = await os.inputText({
+ title: 'Category',
+ });
+ if (canceled) return;
+ await os.apiWithDialog('admin/emoji/set-category-bulk', {
+ ids: selectedEmojis.value,
+ category: result,
+ });
+ emojisPaginationComponent.value.reload();
+};
+
+const addTagBulk = async () => {
+ const { canceled, result } = await os.inputText({
+ title: 'Tag',
+ });
+ if (canceled) return;
+ await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
+ ids: selectedEmojis.value,
+ aliases: result.split(' '),
+ });
+ emojisPaginationComponent.value.reload();
+};
+
+const removeTagBulk = async () => {
+ const { canceled, result } = await os.inputText({
+ title: 'Tag',
+ });
+ if (canceled) return;
+ await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
+ ids: selectedEmojis.value,
+ aliases: result.split(' '),
+ });
+ emojisPaginationComponent.value.reload();
+};
+
+const setTagBulk = async () => {
+ const { canceled, result } = await os.inputText({
+ title: 'Tag',
+ });
+ if (canceled) return;
+ await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
+ ids: selectedEmojis.value,
+ aliases: result.split(' '),
+ });
+ emojisPaginationComponent.value.reload();
+};
+
+const delBulk = async () => {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.locale.deleteConfirm,
+ });
+ if (canceled) return;
+ await os.apiWithDialog('admin/emoji/delete-bulk', {
+ ids: selectedEmojis.value,
+ });
+ emojisPaginationComponent.value.reload();
+};
+
+defineExpose({
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: i18n.locale.customEmojis,
+ icon: 'fas fa-laugh',
+ bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: i18n.locale.addEmoji,
+ handler: add,
+ }, {
+ icon: 'fas fa-ellipsis-h',
+ handler: menu,
+ }],
+ tabs: [{
+ active: tab.value === 'local',
+ title: i18n.locale.local,
+ onClick: () => { tab.value = 'local'; },
+ }, {
+ active: tab.value === 'remote',
+ title: i18n.locale.remote,
+ onClick: () => { tab.value = 'remote'; },
+ },]
+ })),
});
</script>
@@ -210,11 +308,16 @@ export default defineComponent({
> .emoji {
display: flex;
align-items: center;
- padding: 12px;
+ padding: 11px;
text-align: left;
+ border: solid 1px var(--panel);
&:hover {
- color: var(--accent);
+ border-color: var(--inputBorderHover);
+ }
+
+ &.selected {
+ border-color: var(--accent);
}
> .img {
diff --git a/packages/client/src/pages/admin/files-settings.vue b/packages/client/src/pages/admin/files-settings.vue
deleted file mode 100644
index df25bd0fb2..0000000000
--- a/packages/client/src/pages/admin/files-settings.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<template>
-<FormBase>
- <FormSuspense :p="init">
- <FormSwitch v-model="cacheRemoteFiles">
- {{ $ts.cacheRemoteFiles }}
- <template #desc>{{ $ts.cacheRemoteFilesDescription }}</template>
- </FormSwitch>
-
- <FormSwitch v-model="proxyRemoteFiles">
- {{ $ts.proxyRemoteFiles }}
- <template #desc>{{ $ts.proxyRemoteFilesDescription }}</template>
- </FormSwitch>
-
- <FormInput v-model="localDriveCapacityMb" type="number">
- <span>{{ $ts.driveCapacityPerLocalAccount }}</span>
- <template #suffix>MB</template>
- <template #desc>{{ $ts.inMb }}</template>
- </FormInput>
-
- <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
- <span>{{ $ts.driveCapacityPerRemoteAccount }}</span>
- <template #suffix>MB</template>
- <template #desc>{{ $ts.inMb }}</template>
- </FormInput>
-
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
- </FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
-import * as os from '@/os';
-import * as symbols from '@/symbols';
-import { fetchInstance } from '@/instance';
-
-export default defineComponent({
- components: {
- FormSwitch,
- FormInput,
- FormBase,
- FormGroup,
- FormButton,
- FormSuspense,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.files,
- icon: 'fas fa-cloud',
- bg: 'var(--bg)',
- },
- cacheRemoteFiles: false,
- proxyRemoteFiles: false,
- localDriveCapacityMb: 0,
- remoteDriveCapacityMb: 0,
- }
- },
-
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- async init() {
- const meta = await os.api('meta', { detail: true });
- this.cacheRemoteFiles = meta.cacheRemoteFiles;
- this.proxyRemoteFiles = meta.proxyRemoteFiles;
- this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
- this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
- },
- save() {
- os.apiWithDialog('admin/update-meta', {
- cacheRemoteFiles: this.cacheRemoteFiles,
- proxyRemoteFiles: this.proxyRemoteFiles,
- localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
- remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
- }).then(() => {
- fetchInstance();
- });
- }
- }
-});
-</script>
diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue
index 032e394a66..87dd12f489 100644
--- a/packages/client/src/pages/admin/files.vue
+++ b/packages/client/src/pages/admin/files.vue
@@ -19,7 +19,7 @@
<option value="local">{{ $ts.local }}</option>
<option value="remote">{{ $ts.remote }}</option>
</MkSelect>
- <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params().origin === 'local'">
+ <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'">
<template #label>{{ $ts.host }}</template>
</MkInput>
</div>
@@ -55,7 +55,7 @@
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
@@ -95,33 +95,17 @@ export default defineComponent({
type: null,
searchHost: '',
pagination: {
- endpoint: 'admin/drive/files',
+ endpoint: 'admin/drive/files' as const,
limit: 10,
- params: () => ({
+ params: computed(() => ({
type: (this.type && this.type !== '') ? this.type : null,
origin: this.origin,
- hostname: (this.hostname && this.hostname !== '') ? this.hostname : null,
- }),
+ hostname: (this.searchHost && this.searchHost !== '') ? this.searchHost : null,
+ })),
},
}
},
- watch: {
- type() {
- this.$refs.files.reload();
- },
- origin() {
- this.$refs.files.reload();
- },
- searchHost() {
- this.$refs.files.reload();
- },
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
clear() {
os.confirm({
diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue
index e363d1bd03..350e7defc6 100644
--- a/packages/client/src/pages/admin/index.vue
+++ b/packages/client/src/pages/admin/index.vue
@@ -3,14 +3,14 @@
<div v-if="!narrow || page == null" class="nav">
<MkHeader :info="header"></MkHeader>
- <MkSpacer :content-max="700">
+ <MkSpacer :content-max="700" :margin-min="16">
<div class="lxpfedzu">
<div class="banner">
<img :src="$instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
</div>
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
- <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/bot-protection" class="_link">{{ $ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
</div>
@@ -19,7 +19,7 @@
<div class="main">
<MkStickyContainer>
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
- <component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
+ <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
</MkStickyContainer>
</div>
</div>
@@ -29,9 +29,6 @@
import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { i18n } from '@/i18n';
import MkSuperMenu from '@/components/ui/super-menu.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormButton from '@/components/debobigego/button.vue';
import MkInfo from '@/components/ui/info.vue';
import { scroll } from '@/scripts/scroll';
import { instance } from '@/instance';
@@ -41,10 +38,7 @@ import { lookupUser } from '@/scripts/lookup-user';
export default defineComponent({
components: {
- FormBase,
MkSuperMenu,
- FormGroup,
- FormButton,
MkInfo,
},
@@ -72,7 +66,9 @@ export default defineComponent({
const narrow = ref(false);
const view = ref(null);
const el = ref(null);
- const onInfo = (viewInfo) => {
+ const pageChanged = (page) => {
+ if (page == null) return;
+ const viewInfo = page[symbols.PAGE_INFO];
if (isRef(viewInfo)) {
watch(viewInfo, () => {
childInfo.value = viewInfo.value;
@@ -163,11 +159,6 @@ export default defineComponent({
to: '/admin/settings',
active: page.value === 'settings',
}, {
- icon: 'fas fa-cloud',
- text: i18n.locale.files,
- to: '/admin/files-settings',
- active: page.value === 'files-settings',
- }, {
icon: 'fas fa-envelope',
text: i18n.locale.emailServer,
to: '/admin/email-settings',
@@ -183,11 +174,6 @@ export default defineComponent({
to: '/admin/security',
active: page.value === 'security',
}, {
- icon: 'fas fa-bolt',
- text: 'ServiceWorker',
- to: '/admin/service-worker',
- active: page.value === 'service-worker',
- }, {
icon: 'fas fa-globe',
text: i18n.locale.relays,
to: '/admin/relays',
@@ -236,17 +222,11 @@ export default defineComponent({
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
- case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
- case 'bot-protection': return defineAsyncComponent(() => import('./bot-protection.vue'));
- case 'service-worker': return defineAsyncComponent(() => import('./service-worker.vue'));
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
- case 'integrations/twitter': return defineAsyncComponent(() => import('./integrations-twitter.vue'));
- case 'integrations/github': return defineAsyncComponent(() => import('./integrations-github.vue'));
- case 'integrations/discord': return defineAsyncComponent(() => import('./integrations-discord.vue'));
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
@@ -333,7 +313,7 @@ export default defineComponent({
narrow,
view,
el,
- onInfo,
+ pageChanged,
childInfo,
pageProps,
component,
diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue
index 2e899de687..6cadc7df39 100644
--- a/packages/client/src/pages/admin/instance-block.vue
+++ b/packages/client/src/pages/admin/instance-block.vue
@@ -1,39 +1,29 @@
<template>
-<FormBase>
+<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <FormTextarea v-model="blockedHosts">
+ <FormTextarea v-model="blockedHosts" class="_formBlock">
<span>{{ $ts.blockedInstances }}</span>
- <template #desc>{{ $ts.blockedInstancesDescription }}</template>
+ <template #caption>{{ $ts.blockedInstancesDescription }}</template>
</FormTextarea>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormButton from '@/components/ui/button.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
- FormSwitch,
- FormInput,
- FormBase,
- FormGroup,
FormButton,
FormTextarea,
- FormInfo,
FormSuspense,
},
@@ -50,10 +40,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/instance.vue b/packages/client/src/pages/admin/instance.vue
deleted file mode 100644
index 51fcb8675a..0000000000
--- a/packages/client/src/pages/admin/instance.vue
+++ /dev/null
@@ -1,291 +0,0 @@
-<template>
-<XModalWindow ref="dialog"
- :width="520"
- :height="500"
- @close="$refs.dialog.close()"
- @closed="$emit('closed')"
->
- <template #header>{{ instance.host }}</template>
- <div class="mk-instance-info">
- <div class="_table section">
- <div class="_row">
- <div class="_cell">
- <div class="_label">{{ $ts.software }}</div>
- <div class="_data">{{ instance.softwareName || '?' }}</div>
- </div>
- <div class="_cell">
- <div class="_label">{{ $ts.version }}</div>
- <div class="_data">{{ instance.softwareVersion || '?' }}</div>
- </div>
- </div>
- </div>
- <div class="_table data section">
- <div class="_row">
- <div class="_cell">
- <div class="_label">{{ $ts.registeredAt }}</div>
- <div class="_data">{{ new Date(instance.caughtAt).toLocaleString() }} (<MkTime :time="instance.caughtAt"/>)</div>
- </div>
- </div>
- <div class="_row">
- <div class="_cell">
- <div class="_label">{{ $ts.following }}</div>
- <button class="_data _textButton" @click="showFollowing()">{{ number(instance.followingCount) }}</button>
- </div>
- <div class="_cell">
- <div class="_label">{{ $ts.followers }}</div>
- <button class="_data _textButton" @click="showFollowers()">{{ number(instance.followersCount) }}</button>
- </div>
- </div>
- <div class="_row">
- <div class="_cell">
- <div class="_label">{{ $ts.users }}</div>
- <button class="_data _textButton" @click="showUsers()">{{ number(instance.usersCount) }}</button>
- </div>
- <div class="_cell">
- <div class="_label">{{ $ts.notes }}</div>
- <div class="_data">{{ number(instance.notesCount) }}</div>
- </div>
- </div>
- <div class="_row">
- <div class="_cell">
- <div class="_label">{{ $ts.files }}</div>
- <div class="_data">{{ number(instance.driveFiles) }}</div>
- </div>
- <div class="_cell">
- <div class="_label">{{ $ts.storageUsage }}</div>
- <div class="_data">{{ bytes(instance.driveUsage) }}</div>
- </div>
- </div>
- <div class="_row">
- <div class="_cell">
- <div class="_label">{{ $ts.latestRequestSentAt }}</div>
- <div class="_data"><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></div>
- </div>
- <div class="_cell">
- <div class="_label">{{ $ts.latestStatus }}</div>
- <div class="_data">{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</div>
- </div>
- </div>
- <div class="_row">
- <div class="_cell">
- <div class="_label">{{ $ts.latestRequestReceivedAt }}</div>
- <div class="_data"><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></div>
- </div>
- </div>
- </div>
- <div class="chart">
- <div class="header">
- <span class="label">{{ $ts.charts }}</span>
- <div class="selects">
- <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
- <option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
- <option value="instance-users">{{ $ts._instanceCharts.users }}</option>
- <option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
- <option value="instance-notes">{{ $ts._instanceCharts.notes }}</option>
- <option value="instance-notes-total">{{ $ts._instanceCharts.notesTotal }}</option>
- <option value="instance-ff">{{ $ts._instanceCharts.ff }}</option>
- <option value="instance-ff-total">{{ $ts._instanceCharts.ffTotal }}</option>
- <option value="instance-drive-usage">{{ $ts._instanceCharts.cacheSize }}</option>
- <option value="instance-drive-usage-total">{{ $ts._instanceCharts.cacheSizeTotal }}</option>
- <option value="instance-drive-files">{{ $ts._instanceCharts.files }}</option>
- <option value="instance-drive-files-total">{{ $ts._instanceCharts.filesTotal }}</option>
- </MkSelect>
- <MkSelect v-model="chartSpan" style="margin: 0;">
- <option value="hour">{{ $ts.perHour }}</option>
- <option value="day">{{ $ts.perDay }}</option>
- </MkSelect>
- </div>
- </div>
- <div class="chart">
- <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
- </div>
- </div>
- <div class="operations section">
- <span class="label">{{ $ts.operations }}</span>
- <MkSwitch v-model="isSuspended" class="switch">{{ $ts.stopActivityDelivery }}</MkSwitch>
- <MkSwitch :model-value="isBlocked" class="switch" @update:modelValue="changeBlock">{{ $ts.blockThisInstance }}</MkSwitch>
- <details>
- <summary>{{ $ts.deleteAllFiles }}</summary>
- <MkButton style="margin: 0.5em 0 0.5em 0;" @click="deleteAllFiles()"><i class="fas fa-trash-alt"></i> {{ $ts.deleteAllFiles }}</MkButton>
- </details>
- <details>
- <summary>{{ $ts.removeAllFollowing }}</summary>
- <MkButton style="margin: 0.5em 0 0.5em 0;" @click="removeAllFollowing()"><i class="fas fa-minus-circle"></i> {{ $ts.removeAllFollowing }}</MkButton>
- <MkInfo warn>{{ $t('removeAllFollowingDescription', { host: instance.host }) }}</MkInfo>
- </details>
- </div>
- <details class="metadata section">
- <summary class="label">{{ $ts.metadata }}</summary>
- <pre><code>{{ JSON.stringify(instance, null, 2) }}</code></pre>
- </details>
- </div>
-</XModalWindow>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import XModalWindow from '@/components/ui/modal-window.vue';
-import MkSelect from '@/components/form/select.vue';
-import MkButton from '@/components/ui/button.vue';
-import MkSwitch from '@/components/form/switch.vue';
-import MkInfo from '@/components/ui/info.vue';
-import MkChart from '@/components/chart.vue';
-import bytes from '@/filters/bytes';
-import number from '@/filters/number';
-import * as os from '@/os';
-
-export default defineComponent({
- components: {
- XModalWindow,
- MkSelect,
- MkButton,
- MkSwitch,
- MkInfo,
- MkChart,
- },
-
- props: {
- instance: {
- type: Object,
- required: true
- }
- },
-
- emits: ['closed'],
-
- data() {
- return {
- isSuspended: this.instance.isSuspended,
- chartSrc: 'requests',
- chartSpan: 'hour',
- };
- },
-
- computed: {
- meta() {
- return this.$instance;
- },
-
- isBlocked() {
- return this.meta && this.meta.blockedHosts && this.meta.blockedHosts.includes(this.instance.host);
- }
- },
-
- watch: {
- isSuspended() {
- os.api('admin/federation/update-instance', {
- host: this.instance.host,
- isSuspended: this.isSuspended
- });
- },
- },
-
- methods: {
- changeBlock(e) {
- os.api('admin/update-meta', {
- blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
- });
- },
-
- removeAllFollowing() {
- os.apiWithDialog('admin/federation/remove-all-following', {
- host: this.instance.host
- });
- },
-
- deleteAllFiles() {
- os.apiWithDialog('admin/federation/delete-all-files', {
- host: this.instance.host
- });
- },
-
- showFollowing() {
- // TODO: ページ遷移
- },
-
- showFollowers() {
- // TODO: ページ遷移
- },
-
- showUsers() {
- // TODO: ページ遷移
- },
-
- bytes,
-
- number
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-instance-info {
- overflow: auto;
-
- > .section {
- padding: 16px 32px;
-
- @media (max-width: 500px) {
- padding: 8px 16px;
- }
-
- &:not(:first-child) {
- border-top: solid 0.5px var(--divider);
- }
- }
-
- > .chart {
- border-top: solid 0.5px var(--divider);
- padding: 16px 0 12px 0;
-
- > .header {
- padding: 0 32px;
-
- @media (max-width: 500px) {
- padding: 0 16px;
- }
-
- > .label {
- font-size: 80%;
- opacity: 0.7;
- }
-
- > .selects {
- display: flex;
- }
- }
-
- > .chart {
- padding: 0 16px;
-
- @media (max-width: 500px) {
- padding: 0;
- }
- }
- }
-
- > .operations {
- > .label {
- font-size: 80%;
- opacity: 0.7;
- }
-
- > .switch {
- margin: 16px 0;
- }
- }
-
- > .metadata {
- > .label {
- font-size: 80%;
- opacity: 0.7;
- }
-
- > pre > code {
- display: block;
- max-height: 200px;
- overflow: auto;
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/admin/integrations-discord.vue b/packages/client/src/pages/admin/integrations.discord.vue
index 383031f3d1..8fc340150a 100644
--- a/packages/client/src/pages/admin/integrations-discord.vue
+++ b/packages/client/src/pages/admin/integrations.discord.vue
@@ -1,37 +1,36 @@
<template>
-<FormBase>
- <FormSuspense :p="init">
- <FormSwitch v-model="enableDiscordIntegration">
- {{ $ts.enable }}
+<FormSuspense :p="init">
+ <div class="_formRoot">
+ <FormSwitch v-model="enableDiscordIntegration" class="_formBlock">
+ <template #label>{{ $ts.enable }}</template>
</FormSwitch>
<template v-if="enableDiscordIntegration">
- <FormInfo>Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
+ <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/dc/cb` }}</FormInfo>
- <FormInput v-model="discordClientId">
+ <FormInput v-model="discordClientId" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
- Client ID
+ <template #label>Client ID</template>
</FormInput>
- <FormInput v-model="discordClientSecret">
+ <FormInput v-model="discordClientSecret" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
- Client Secret
+ <template #label>Client Secret</template>
</FormInput>
</template>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
- </FormSuspense>
-</FormBase>
+ <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </div>
+</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormInput from '@/components/form/input.vue';
+import FormButton from '@/components/ui/button.vue';
+import FormInfo from '@/components/ui/info.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
@@ -40,7 +39,6 @@ export default defineComponent({
components: {
FormSwitch,
FormInput,
- FormBase,
FormInfo,
FormButton,
FormSuspense,
@@ -60,10 +58,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/integrations-github.vue b/packages/client/src/pages/admin/integrations.github.vue
index ecb2fd67fa..d9db9c00f1 100644
--- a/packages/client/src/pages/admin/integrations-github.vue
+++ b/packages/client/src/pages/admin/integrations.github.vue
@@ -1,37 +1,36 @@
<template>
-<FormBase>
- <FormSuspense :p="init">
- <FormSwitch v-model="enableGithubIntegration">
- {{ $ts.enable }}
+<FormSuspense :p="init">
+ <div class="_formRoot">
+ <FormSwitch v-model="enableGithubIntegration" class="_formBlock">
+ <template #label>{{ $ts.enable }}</template>
</FormSwitch>
<template v-if="enableGithubIntegration">
- <FormInfo>Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
+ <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/gh/cb` }}</FormInfo>
- <FormInput v-model="githubClientId">
+ <FormInput v-model="githubClientId" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
- Client ID
+ <template #label>Client ID</template>
</FormInput>
- <FormInput v-model="githubClientSecret">
+ <FormInput v-model="githubClientSecret" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
- Client Secret
+ <template #label>Client Secret</template>
</FormInput>
</template>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
- </FormSuspense>
-</FormBase>
+ <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </div>
+</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormInput from '@/components/form/input.vue';
+import FormButton from '@/components/ui/button.vue';
+import FormInfo from '@/components/ui/info.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
@@ -40,7 +39,6 @@ export default defineComponent({
components: {
FormSwitch,
FormInput,
- FormBase,
FormInfo,
FormButton,
FormSuspense,
@@ -60,10 +58,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/integrations-twitter.vue b/packages/client/src/pages/admin/integrations.twitter.vue
index 1404102c57..1f8074535a 100644
--- a/packages/client/src/pages/admin/integrations-twitter.vue
+++ b/packages/client/src/pages/admin/integrations.twitter.vue
@@ -1,37 +1,36 @@
<template>
-<FormBase>
- <FormSuspense :p="init">
- <FormSwitch v-model="enableTwitterIntegration">
- {{ $ts.enable }}
+<FormSuspense :p="init">
+ <div class="_formRoot">
+ <FormSwitch v-model="enableTwitterIntegration" class="_formBlock">
+ <template #label>{{ $ts.enable }}</template>
</FormSwitch>
<template v-if="enableTwitterIntegration">
- <FormInfo>Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
+ <FormInfo class="_formBlock">Callback URL: {{ `${uri}/api/tw/cb` }}</FormInfo>
- <FormInput v-model="twitterConsumerKey">
+ <FormInput v-model="twitterConsumerKey" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
- Consumer Key
+ <template #label>Consumer Key</template>
</FormInput>
- <FormInput v-model="twitterConsumerSecret">
+ <FormInput v-model="twitterConsumerSecret" class="_formBlock">
<template #prefix><i class="fas fa-key"></i></template>
- Consumer Secret
+ <template #label>Consumer Secret</template>
</FormInput>
</template>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
- </FormSuspense>
-</FormBase>
+ <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </div>
+</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormInput from '@/components/form/input.vue';
+import FormButton from '@/components/ui/button.vue';
+import FormInfo from '@/components/ui/info.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
@@ -40,7 +39,6 @@ export default defineComponent({
components: {
FormSwitch,
FormInput,
- FormBase,
FormInfo,
FormButton,
FormSuspense,
@@ -60,10 +58,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue
index c21eebc1c6..91d03fef31 100644
--- a/packages/client/src/pages/admin/integrations.vue
+++ b/packages/client/src/pages/admin/integrations.vue
@@ -1,46 +1,48 @@
<template>
-<FormBase>
+<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <FormLink to="/admin/integrations/twitter">
- <i class="fab fa-twitter"></i> Twitter
+ <FormFolder class="_formBlock">
+ <template #icon><i class="fab fa-twitter"></i></template>
+ <template #label>Twitter</template>
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
- </FormLink>
- <FormLink to="/admin/integrations/github">
- <i class="fab fa-github"></i> GitHub
+ <XTwitter/>
+ </FormFolder>
+ <FormFolder to="/admin/integrations/github" class="_formBlock">
+ <template #icon><i class="fab fa-github"></i></template>
+ <template #label>GitHub</template>
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
- </FormLink>
- <FormLink to="/admin/integrations/discord">
- <i class="fab fa-discord"></i> Discord
+ <XGithub/>
+ </FormFolder>
+ <FormFolder to="/admin/integrations/discord" class="_formBlock">
+ <template #icon><i class="fab fa-discord"></i></template>
+ <template #label>Discord</template>
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
- </FormLink>
+ <XDiscord/>
+ </FormFolder>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormFolder from '@/components/form/folder.vue';
+import FormSecion from '@/components/form/section.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import XTwitter from './integrations.twitter.vue';
+import XGithub from './integrations.github.vue';
+import XDiscord from './integrations.discord.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
- FormLink,
- FormInput,
- FormBase,
- FormGroup,
- FormButton,
- FormTextarea,
- FormInfo,
+ FormFolder,
+ FormSecion,
FormSuspense,
+ XTwitter,
+ XGithub,
+ XDiscord,
},
emits: ['info'],
@@ -58,10 +60,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/metrics.vue b/packages/client/src/pages/admin/metrics.vue
index 05b64b235c..1de297fd93 100644
--- a/packages/client/src/pages/admin/metrics.vue
+++ b/packages/client/src/pages/admin/metrics.vue
@@ -76,7 +76,6 @@ import MkwFederation from '../../widgets/federation.vue';
import { version, url } from '@/config';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
-import MkInstanceInfo from './instance.vue';
Chart.register(
ArcElement,
@@ -101,6 +100,7 @@ const alpha = (hex, a) => {
return `rgba(${r}, ${g}, ${b}, ${a})`;
};
import * as os from '@/os';
+import { stream } from '@/stream';
export default defineComponent({
components: {
@@ -119,7 +119,7 @@ export default defineComponent({
stats: null,
serverInfo: null,
connection: null,
- queueConnection: markRaw(os.stream.useChannel('queueStats')),
+ queueConnection: markRaw(stream.useChannel('queueStats')),
memUsage: 0,
chartCpuMem: null,
chartNet: null,
@@ -150,7 +150,7 @@ export default defineComponent({
os.api('admin/server-info', {}).then(res => {
this.serverInfo = res;
- this.connection = markRaw(os.stream.useChannel('serverStats'));
+ this.connection = markRaw(stream.useChannel('serverStats'));
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send('requestLog', {
diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue
index 8984686b5e..6c5be220f8 100644
--- a/packages/client/src/pages/admin/object-storage.vue
+++ b/packages/client/src/pages/admin/object-storage.vue
@@ -1,76 +1,78 @@
<template>
-<FormBase>
+<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <FormSwitch v-model="useObjectStorage">{{ $ts.useObjectStorage }}</FormSwitch>
+ <div class="_formRoot">
+ <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ $ts.useObjectStorage }}</FormSwitch>
- <template v-if="useObjectStorage">
- <FormInput v-model="objectStorageBaseUrl">
- <span>{{ $ts.objectStorageBaseUrl }}</span>
- <template #desc>{{ $ts.objectStorageBaseUrlDesc }}</template>
- </FormInput>
+ <template v-if="useObjectStorage">
+ <FormInput v-model="objectStorageBaseUrl" class="_formBlock">
+ <template #label>{{ $ts.objectStorageBaseUrl }}</template>
+ <template #caption>{{ $ts.objectStorageBaseUrlDesc }}</template>
+ </FormInput>
- <FormInput v-model="objectStorageBucket">
- <span>{{ $ts.objectStorageBucket }}</span>
- <template #desc>{{ $ts.objectStorageBucketDesc }}</template>
- </FormInput>
+ <FormInput v-model="objectStorageBucket" class="_formBlock">
+ <template #label>{{ $ts.objectStorageBucket }}</template>
+ <template #caption>{{ $ts.objectStorageBucketDesc }}</template>
+ </FormInput>
- <FormInput v-model="objectStoragePrefix">
- <span>{{ $ts.objectStoragePrefix }}</span>
- <template #desc>{{ $ts.objectStoragePrefixDesc }}</template>
- </FormInput>
+ <FormInput v-model="objectStoragePrefix" class="_formBlock">
+ <template #label>{{ $ts.objectStoragePrefix }}</template>
+ <template #caption>{{ $ts.objectStoragePrefixDesc }}</template>
+ </FormInput>
- <FormInput v-model="objectStorageEndpoint">
- <span>{{ $ts.objectStorageEndpoint }}</span>
- <template #desc>{{ $ts.objectStorageEndpointDesc }}</template>
- </FormInput>
+ <FormInput v-model="objectStorageEndpoint" class="_formBlock">
+ <template #label>{{ $ts.objectStorageEndpoint }}</template>
+ <template #caption>{{ $ts.objectStorageEndpointDesc }}</template>
+ </FormInput>
- <FormInput v-model="objectStorageRegion">
- <span>{{ $ts.objectStorageRegion }}</span>
- <template #desc>{{ $ts.objectStorageRegionDesc }}</template>
- </FormInput>
+ <FormInput v-model="objectStorageRegion" class="_formBlock">
+ <template #label>{{ $ts.objectStorageRegion }}</template>
+ <template #caption>{{ $ts.objectStorageRegionDesc }}</template>
+ </FormInput>
- <FormInput v-model="objectStorageAccessKey">
- <template #prefix><i class="fas fa-key"></i></template>
- <span>Access key</span>
- </FormInput>
+ <FormSplit :min-width="280">
+ <FormInput v-model="objectStorageAccessKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>Access key</template>
+ </FormInput>
- <FormInput v-model="objectStorageSecretKey">
- <template #prefix><i class="fas fa-key"></i></template>
- <span>Secret key</span>
- </FormInput>
+ <FormInput v-model="objectStorageSecretKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>Secret key</template>
+ </FormInput>
+ </FormSplit>
- <FormSwitch v-model="objectStorageUseSSL">
- {{ $ts.objectStorageUseSSL }}
- <template #desc>{{ $ts.objectStorageUseSSLDesc }}</template>
- </FormSwitch>
+ <FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
+ <template #label>{{ $ts.objectStorageUseSSL }}</template>
+ <template #caption>{{ $ts.objectStorageUseSSLDesc }}</template>
+ </FormSwitch>
- <FormSwitch v-model="objectStorageUseProxy">
- {{ $ts.objectStorageUseProxy }}
- <template #desc>{{ $ts.objectStorageUseProxyDesc }}</template>
- </FormSwitch>
+ <FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
+ <template #label>{{ $ts.objectStorageUseProxy }}</template>
+ <template #caption>{{ $ts.objectStorageUseProxyDesc }}</template>
+ </FormSwitch>
- <FormSwitch v-model="objectStorageSetPublicRead">
- {{ $ts.objectStorageSetPublicRead }}
- </FormSwitch>
+ <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
+ <template #label>{{ $ts.objectStorageSetPublicRead }}</template>
+ </FormSwitch>
- <FormSwitch v-model="objectStorageS3ForcePathStyle">
- s3ForcePathStyle
- </FormSwitch>
- </template>
-
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
+ <template #label>s3ForcePathStyle</template>
+ </FormSwitch>
+ </template>
+ </div>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormInput from '@/components/form/input.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import FormSplit from '@/components/form/split.vue';
+import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
@@ -79,10 +81,10 @@ export default defineComponent({
components: {
FormSwitch,
FormInput,
- FormBase,
FormGroup,
- FormButton,
FormSuspense,
+ FormSplit,
+ FormSection,
},
emits: ['info'],
@@ -93,6 +95,12 @@ export default defineComponent({
title: this.$ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: this.$ts.save,
+ handler: this.save,
+ }],
},
useObjectStorage: false,
objectStorageBaseUrl: null,
@@ -110,10 +118,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue
index eb214a21c8..6b588e88aa 100644
--- a/packages/client/src/pages/admin/other-settings.vue
+++ b/packages/client/src/pages/admin/other-settings.vue
@@ -1,34 +1,17 @@
<template>
-<FormBase>
+<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <FormGroup>
- <FormInput v-model="summalyProxy">
- <template #prefix><i class="fas fa-link"></i></template>
- Summaly Proxy URL
- </FormInput>
- </FormGroup>
- <FormGroup>
- <FormInput v-model="deeplAuthKey">
- <template #prefix><i class="fas fa-key"></i></template>
- DeepL Auth Key
- </FormInput>
- <FormSwitch v-model="deeplIsPro">
- Pro account
- </FormSwitch>
- </FormGroup>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ none
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormInput from '@/components/form/input.vue';
+import FormSection from '@/components/form/section.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
@@ -37,9 +20,7 @@ export default defineComponent({
components: {
FormSwitch,
FormInput,
- FormBase,
- FormGroup,
- FormButton,
+ FormSection,
FormSuspense,
},
@@ -51,29 +32,22 @@ export default defineComponent({
title: this.$ts.other,
icon: 'fas fa-cogs',
bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: this.$ts.save,
+ handler: this.save,
+ }],
},
- summalyProxy: '',
- deeplAuthKey: '',
- deeplIsPro: false,
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
- this.summalyProxy = meta.summalyProxy;
- this.deeplAuthKey = meta.deeplAuthKey;
- this.deeplIsPro = meta.deeplIsPro;
},
save() {
os.apiWithDialog('admin/update-meta', {
- summalyProxy: this.summalyProxy,
- deeplAuthKey: this.deeplAuthKey,
- deeplIsPro: this.deeplIsPro,
}).then(() => {
fetchInstance();
});
diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue
index da5fc0ba6d..b8ae8ad9e1 100644
--- a/packages/client/src/pages/admin/overview.vue
+++ b/packages/client/src/pages/admin/overview.vue
@@ -19,7 +19,7 @@
<MkContainer :foldable="true" class="charts">
<template #header><i class="fas fa-chart-bar"></i>{{ $ts.charts }}</template>
- <div style="padding-top: 12px;">
+ <div style="padding: 12px;">
<MkInstanceStats :chart-limit="500" :detailed="true"/>
</div>
</MkContainer>
@@ -67,7 +67,6 @@
<script lang="ts">
import { computed, defineComponent, markRaw, version as vueVersion } from 'vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
import MkInstanceStats from '@/components/instance-stats.vue';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/form/select.vue';
@@ -78,15 +77,14 @@ import MkQueueChart from '@/components/queue-chart.vue';
import { version, url } from '@/config';
import bytes from '@/filters/bytes';
import number from '@/filters/number';
-import MkInstanceInfo from './instance.vue';
import XMetrics from './metrics.vue';
import * as os from '@/os';
+import { stream } from '@/stream';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
MkNumberDiff,
- FormKeyValueView,
MkInstanceStats,
MkContainer,
MkFolder,
@@ -113,13 +111,11 @@ export default defineComponent({
notesComparedToThePrevDay: null,
fetchJobs: () => os.api('admin/queue/deliver-delayed', {}),
fetchModLogs: () => os.api('admin/show-moderation-logs', {}),
- queueStatsConnection: markRaw(os.stream.useChannel('queueStats')),
+ queueStatsConnection: markRaw(stream.useChannel('queueStats')),
}
},
async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
-
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
@@ -160,9 +156,7 @@ export default defineComponent({
host: q
});
}
- os.popup(MkInstanceInfo, {
- instance: instance
- }, {}, 'closed');
+ // TODO
},
bytes,
diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue
index 14ef92a747..5c4fbffa0c 100644
--- a/packages/client/src/pages/admin/proxy-account.vue
+++ b/packages/client/src/pages/admin/proxy-account.vue
@@ -1,42 +1,32 @@
<template>
-<FormBase>
+<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <FormGroup>
- <FormKeyValueView>
- <template #key>{{ $ts.proxyAccount }}</template>
- <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
- </FormKeyValueView>
- <template #caption>{{ $ts.proxyAccountDescription }}</template>
- </FormGroup>
+ <MkInfo class="_formBlock">{{ $ts.proxyAccountDescription }}</MkInfo>
+ <MkKeyValue class="_formBlock">
+ <template #key>{{ $ts.proxyAccount }}</template>
+ <template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
+ </MkKeyValue>
- <FormButton primary @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton>
+ <FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import MkKeyValue from '@/components/key-value.vue';
+import FormButton from '@/components/ui/button.vue';
+import MkInfo from '@/components/ui/info.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
- FormKeyValueView,
- FormInput,
- FormBase,
- FormGroup,
+ MkKeyValue,
FormButton,
- FormTextarea,
- FormInfo,
+ MkInfo,
FormSuspense,
},
@@ -54,10 +44,6 @@ export default defineComponent({
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue
index 37a87089cb..522210d933 100644
--- a/packages/client/src/pages/admin/queue.vue
+++ b/packages/client/src/pages/admin/queue.vue
@@ -1,28 +1,25 @@
<template>
-<FormBase>
+<MkSpacer :content-max="800">
<XQueue :connection="connection" domain="inbox">
<template #title>In</template>
</XQueue>
<XQueue :connection="connection" domain="deliver">
<template #title>Out</template>
</XQueue>
- <FormButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</FormButton>
-</FormBase>
+ <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
import MkButton from '@/components/ui/button.vue';
import XQueue from './queue.chart.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormButton from '@/components/debobigego/button.vue';
import * as os from '@/os';
+import { stream } from '@/stream';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
- FormButton,
MkButton,
XQueue,
},
@@ -36,13 +33,11 @@ export default defineComponent({
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
},
- connection: markRaw(os.stream.useChannel('queueStats')),
+ connection: markRaw(stream.useChannel('queueStats')),
}
},
mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
-
this.$nextTick(() => {
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue
index 3e2f1c6f26..bb840db0a2 100644
--- a/packages/client/src/pages/admin/relays.vue
+++ b/packages/client/src/pages/admin/relays.vue
@@ -1,32 +1,27 @@
<template>
-<FormBase class="relaycxt">
- <FormButton primary @click="addRelay"><i class="fas fa-plus"></i> {{ $ts.addRelay }}</FormButton>
-
- <div v-for="relay in relays" :key="relay.inbox" class="_debobigegoItem">
- <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>
+<MkSpacer :content-max="800">
+ <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;">
+ <div>{{ relay.inbox }}</div>
+ <div class="status">
+ <i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i>
+ <i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i>
+ <i v-else class="fas fa-clock icon requesting"></i>
+ <span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
</div>
+ <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
</div>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
-import MkInput from '@/components/form/input.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormButton from '@/components/debobigego/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
- FormButton,
MkButton,
- MkInput,
},
emits: ['info'],
@@ -37,6 +32,12 @@ export default defineComponent({
title: this.$ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-plus',
+ text: this.$ts.addRelay,
+ handler: this.addRelay,
+ }],
},
relays: [],
inbox: '',
@@ -47,10 +48,6 @@ export default defineComponent({
this.refresh();
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async addRelay() {
const { canceled, result: inbox } = await os.inputText({
@@ -94,5 +91,22 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
+.relaycxt {
+ > .status {
+ margin: 8px 0;
+
+ > .icon {
+ width: 1em;
+ margin-right: 0.75em;
+ &.accepted {
+ color: var(--success);
+ }
+
+ &.rejected {
+ color: var(--error);
+ }
+ }
+ }
+}
</style>
diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue
index adfb2e786c..d069891647 100644
--- a/packages/client/src/pages/admin/security.vue
+++ b/packages/client/src/pages/admin/security.vue
@@ -1,44 +1,58 @@
<template>
-<FormBase>
+<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <FormLink to="/admin/bot-protection">
- <i class="fas fa-shield-alt"></i> {{ $ts.botProtection }}
- <template v-if="enableHcaptcha" #suffix>hCaptcha</template>
- <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
- <template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
- </FormLink>
+ <div class="_formRoot">
+ <FormFolder class="_formBlock">
+ <template #icon><i class="fas fa-shield-alt"></i></template>
+ <template #label>{{ $ts.botProtection }}</template>
+ <template v-if="enableHcaptcha" #suffix>hCaptcha</template>
+ <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
+ <template v-else #suffix>{{ $ts.none }} ({{ $ts.notRecommended }})</template>
- <FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
+ <XBotProtection/>
+ </FormFolder>
- <FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch>
+ <FormFolder class="_formBlock">
+ <template #label>Summaly Proxy</template>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ <div class="_formRoot">
+ <FormInput v-model="summalyProxy" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>Summaly Proxy URL</template>
+ </FormInput>
+
+ <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ </div>
+ </FormFolder>
+ </div>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormFolder from '@/components/form/folder.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormInfo from '@/components/ui/info.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import FormSection from '@/components/form/section.vue';
+import FormInput from '@/components/form/input.vue';
+import FormButton from '@/components/ui/button.vue';
+import XBotProtection from './bot-protection.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
- FormLink,
+ FormFolder,
FormSwitch,
- FormBase,
- FormGroup,
- FormButton,
FormInfo,
+ FormSection,
FormSuspense,
+ FormButton,
+ FormInput,
+ XBotProtection,
},
emits: ['info'],
@@ -50,30 +64,23 @@ export default defineComponent({
icon: 'fas fa-lock',
bg: 'var(--bg)',
},
+ summalyProxy: '',
enableHcaptcha: false,
enableRecaptcha: false,
- enableRegistration: false,
- emailRequiredForSignup: false,
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
+ this.summalyProxy = meta.summalyProxy;
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,
+ summalyProxy: this.summalyProxy,
}).then(() => {
fetchInstance();
});
diff --git a/packages/client/src/pages/admin/service-worker.vue b/packages/client/src/pages/admin/service-worker.vue
deleted file mode 100644
index f34cb03e4e..0000000000
--- a/packages/client/src/pages/admin/service-worker.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<template>
-<FormBase>
- <FormSuspense :p="init">
- <FormSwitch v-model="enableServiceWorker">
- {{ $ts.enableServiceworker }}
- <template #desc>{{ $ts.serviceworkerInfo }}</template>
- </FormSwitch>
-
- <template v-if="enableServiceWorker">
- <FormInput v-model="swPublicKey">
- <template #prefix><i class="fas fa-key"></i></template>
- Public key
- </FormInput>
-
- <FormInput v-model="swPrivateKey">
- <template #prefix><i class="fas fa-key"></i></template>
- Private key
- </FormInput>
- </template>
-
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
- </FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
-import * as os from '@/os';
-import * as symbols from '@/symbols';
-import { fetchInstance } from '@/instance';
-
-export default defineComponent({
- components: {
- FormSwitch,
- FormInput,
- FormBase,
- FormGroup,
- FormButton,
- FormSuspense,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: 'ServiceWorker',
- icon: 'fas fa-bolt',
- bg: 'var(--bg)',
- },
- enableServiceWorker: false,
- swPublicKey: null,
- swPrivateKey: null,
- }
- },
-
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- async init() {
- const meta = await os.api('meta', { detail: true });
- this.enableServiceWorker = meta.enableServiceWorker;
- this.swPublicKey = meta.swPublickey;
- this.swPrivateKey = meta.swPrivateKey;
- },
- save() {
- os.apiWithDialog('admin/update-meta', {
- enableServiceWorker: this.enableServiceWorker,
- swPublicKey: this.swPublicKey,
- swPrivateKey: this.swPrivateKey,
- }).then(() => {
- fetchInstance();
- });
- }
- }
-});
-</script>
diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue
index d88445abdb..a4bac93834 100644
--- a/packages/client/src/pages/admin/settings.vue
+++ b/packages/client/src/pages/admin/settings.vue
@@ -1,72 +1,146 @@
<template>
-<FormBase>
+<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <FormInput v-model="name">
- <span>{{ $ts.instanceName }}</span>
- </FormInput>
+ <div class="_formRoot">
+ <FormInput v-model="name" class="_formBlock">
+ <template #label>{{ $ts.instanceName }}</template>
+ </FormInput>
- <FormTextarea v-model="description">
- <span>{{ $ts.instanceDescription }}</span>
- </FormTextarea>
+ <FormTextarea v-model="description" class="_formBlock">
+ <template #label>{{ $ts.instanceDescription }}</template>
+ </FormTextarea>
- <FormInput v-model="iconUrl">
- <template #prefix><i class="fas fa-link"></i></template>
- <span>{{ $ts.iconUrl }}</span>
- </FormInput>
+ <FormInput v-model="iconUrl" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>{{ $ts.iconUrl }}</template>
+ </FormInput>
- <FormInput v-model="bannerUrl">
- <template #prefix><i class="fas fa-link"></i></template>
- <span>{{ $ts.bannerUrl }}</span>
- </FormInput>
+ <FormInput v-model="bannerUrl" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>{{ $ts.bannerUrl }}</template>
+ </FormInput>
- <FormInput v-model="backgroundImageUrl">
- <template #prefix><i class="fas fa-link"></i></template>
- <span>{{ $ts.backgroundImageUrl }}</span>
- </FormInput>
+ <FormInput v-model="backgroundImageUrl" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>{{ $ts.backgroundImageUrl }}</template>
+ </FormInput>
- <FormInput v-model="tosUrl">
- <template #prefix><i class="fas fa-link"></i></template>
- <span>{{ $ts.tosUrl }}</span>
- </FormInput>
+ <FormInput v-model="tosUrl" class="_formBlock">
+ <template #prefix><i class="fas fa-link"></i></template>
+ <template #label>{{ $ts.tosUrl }}</template>
+ </FormInput>
- <FormInput v-model="maintainerName">
- <span>{{ $ts.maintainerName }}</span>
- </FormInput>
+ <FormSplit :min-width="300">
+ <FormInput v-model="maintainerName" class="_formBlock">
+ <template #label>{{ $ts.maintainerName }}</template>
+ </FormInput>
- <FormInput v-model="maintainerEmail" type="email">
- <template #prefix><i class="fas fa-envelope"></i></template>
- <span>{{ $ts.maintainerEmail }}</span>
- </FormInput>
+ <FormInput v-model="maintainerEmail" type="email" class="_formBlock">
+ <template #prefix><i class="fas fa-envelope"></i></template>
+ <template #label>{{ $ts.maintainerEmail }}</template>
+ </FormInput>
+ </FormSplit>
- <FormTextarea v-model="pinnedUsers">
- <span>{{ $ts.pinnedUsers }}</span>
- <template #desc>{{ $ts.pinnedUsersDescription }}</template>
- </FormTextarea>
+ <FormTextarea v-model="pinnedUsers" class="_formBlock">
+ <template #label>{{ $ts.pinnedUsers }}</template>
+ <template #caption>{{ $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>
+ <FormInput v-model="maxNoteTextLength" type="number" class="_formBlock">
+ <template #prefix><i class="fas fa-pencil-alt"></i></template>
+ <template #label>{{ $ts.maxNoteTextLength }}</template>
+ </FormInput>
- <FormSwitch v-model="enableLocalTimeline">{{ $ts.enableLocalTimeline }}</FormSwitch>
- <FormSwitch v-model="enableGlobalTimeline">{{ $ts.enableGlobalTimeline }}</FormSwitch>
- <FormInfo>{{ $ts.disablingTimelinesInfo }}</FormInfo>
+ <FormSection>
+ <FormSwitch v-model="enableRegistration" class="_formBlock">
+ <template #label>{{ $ts.enableRegistration }}</template>
+ </FormSwitch>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+ <FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
+ <template #label>{{ $ts.emailRequiredForSignup }}</template>
+ </FormSwitch>
+ </FormSection>
+
+ <FormSection>
+ <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ $ts.enableLocalTimeline }}</FormSwitch>
+ <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ $ts.enableGlobalTimeline }}</FormSwitch>
+ <FormInfo class="_formBlock">{{ $ts.disablingTimelinesInfo }}</FormInfo>
+ </FormSection>
+
+ <FormSection>
+ <template #label>{{ $ts.files }}</template>
+
+ <FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
+ <template #label>{{ $ts.cacheRemoteFiles }}</template>
+ <template #caption>{{ $ts.cacheRemoteFilesDescription }}</template>
+ </FormSwitch>
+
+ <FormSwitch v-model="proxyRemoteFiles" class="_formBlock">
+ <template #label>{{ $ts.proxyRemoteFiles }}</template>
+ <template #caption>{{ $ts.proxyRemoteFilesDescription }}</template>
+ </FormSwitch>
+
+ <FormSplit :min-width="280">
+ <FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
+ <template #label>{{ $ts.driveCapacityPerLocalAccount }}</template>
+ <template #suffix>MB</template>
+ <template #caption>{{ $ts.inMb }}</template>
+ </FormInput>
+
+ <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
+ <template #label>{{ $ts.driveCapacityPerRemoteAccount }}</template>
+ <template #suffix>MB</template>
+ <template #caption>{{ $ts.inMb }}</template>
+ </FormInput>
+ </FormSplit>
+ </FormSection>
+
+ <FormSection>
+ <template #label>ServiceWorker</template>
+
+ <FormSwitch v-model="enableServiceWorker" class="_formBlock">
+ <template #label>{{ $ts.enableServiceworker }}</template>
+ <template #caption>{{ $ts.serviceworkerInfo }}</template>
+ </FormSwitch>
+
+ <template v-if="enableServiceWorker">
+ <FormInput v-model="swPublicKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>Public key</template>
+ </FormInput>
+
+ <FormInput v-model="swPrivateKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>Private key</template>
+ </FormInput>
+ </template>
+ </FormSection>
+
+ <FormSection>
+ <template #label>DeepL Translation</template>
+
+ <FormInput v-model="deeplAuthKey" class="_formBlock">
+ <template #prefix><i class="fas fa-key"></i></template>
+ <template #label>DeepL Auth Key</template>
+ </FormInput>
+ <FormSwitch v-model="deeplIsPro" class="_formBlock">
+ <template #label>Pro account</template>
+ </FormSwitch>
+ </FormSection>
+ </div>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormInput from '@/components/form/input.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormInfo from '@/components/ui/info.vue';
+import FormSection from '@/components/form/section.vue';
+import FormSplit from '@/components/form/split.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
@@ -75,12 +149,11 @@ export default defineComponent({
components: {
FormSwitch,
FormInput,
- FormBase,
- FormGroup,
- FormButton,
+ FormSuspense,
FormTextarea,
FormInfo,
- FormSuspense,
+ FormSection,
+ FormSplit,
},
emits: ['info'],
@@ -91,6 +164,12 @@ export default defineComponent({
title: this.$ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: this.$ts.save,
+ handler: this.save,
+ }],
},
name: null,
description: null,
@@ -104,13 +183,20 @@ export default defineComponent({
enableLocalTimeline: false,
enableGlobalTimeline: false,
pinnedUsers: '',
+ cacheRemoteFiles: false,
+ proxyRemoteFiles: false,
+ localDriveCapacityMb: 0,
+ remoteDriveCapacityMb: 0,
+ enableRegistration: false,
+ emailRequiredForSignup: false,
+ enableServiceWorker: false,
+ swPublicKey: null,
+ swPrivateKey: null,
+ deeplAuthKey: '',
+ deeplIsPro: false,
}
},
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
@@ -126,6 +212,17 @@ export default defineComponent({
this.enableLocalTimeline = !meta.disableLocalTimeline;
this.enableGlobalTimeline = !meta.disableGlobalTimeline;
this.pinnedUsers = meta.pinnedUsers.join('\n');
+ this.cacheRemoteFiles = meta.cacheRemoteFiles;
+ this.proxyRemoteFiles = meta.proxyRemoteFiles;
+ this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
+ this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
+ this.enableRegistration = !meta.disableRegistration;
+ this.emailRequiredForSignup = meta.emailRequiredForSignup;
+ this.enableServiceWorker = meta.enableServiceWorker;
+ this.swPublicKey = meta.swPublickey;
+ this.swPrivateKey = meta.swPrivateKey;
+ this.deeplAuthKey = meta.deeplAuthKey;
+ this.deeplIsPro = meta.deeplIsPro;
},
save() {
@@ -142,6 +239,17 @@ export default defineComponent({
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
pinnedUsers: this.pinnedUsers.split('\n'),
+ cacheRemoteFiles: this.cacheRemoteFiles,
+ proxyRemoteFiles: this.proxyRemoteFiles,
+ localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
+ remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
+ disableRegistration: !this.enableRegistration,
+ emailRequiredForSignup: this.emailRequiredForSignup,
+ enableServiceWorker: this.enableServiceWorker,
+ swPublicKey: this.swPublicKey,
+ swPrivateKey: this.swPrivateKey,
+ deeplAuthKey: this.deeplAuthKey,
+ deeplIsPro: this.deeplIsPro,
}).then(() => {
fetchInstance();
});
diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue
index e7a3437167..03e155ddcf 100644
--- a/packages/client/src/pages/admin/users.vue
+++ b/packages/client/src/pages/admin/users.vue
@@ -30,7 +30,7 @@
<template #prefix>@</template>
<template #label>{{ $ts.username }}</template>
</MkInput>
- <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params().origin === 'local'" @update:modelValue="$refs.users.reload()">
+ <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()">
<template #prefix>@</template>
<template #label>{{ $ts.host }}</template>
</MkInput>
@@ -62,7 +62,7 @@
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
@@ -110,36 +110,20 @@ export default defineComponent({
searchUsername: '',
searchHost: '',
pagination: {
- endpoint: 'admin/show-users',
+ endpoint: 'admin/show-users' as const,
limit: 10,
- params: () => ({
+ params: computed(() => ({
sort: this.sort,
state: this.state,
origin: this.origin,
username: this.searchUsername,
hostname: this.searchHost,
- }),
+ })),
offsetMode: true
},
}
},
- watch: {
- sort() {
- this.$refs.users.reload();
- },
- state() {
- this.$refs.users.reload();
- },
- origin() {
- this.$refs.users.reload();
- },
- },
-
- async mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
lookupUser,
diff --git a/packages/client/src/pages/advanced-theme-editor.vue b/packages/client/src/pages/advanced-theme-editor.vue
deleted file mode 100644
index 9c2423131c..0000000000
--- a/packages/client/src/pages/advanced-theme-editor.vue
+++ /dev/null
@@ -1,349 +0,0 @@
-<template>
-<div class="t9makv94">
- <section class="_section">
- <div class="_content">
- <details>
- <summary>{{ $ts.import }}</summary>
- <MkTextarea v-model="themeToImport">
- {{ $ts._theme.importInfo }}
- </MkTextarea>
- <MkButton :disabled="!themeToImport.trim()" @click="importTheme">{{ $ts.import }}</MkButton>
- </details>
- </div>
- </section>
- <section class="_section">
- <div class="_content _card _gap">
- <div class="_content">
- <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>
- <MkRadio v-model="baseTheme" value="dark">{{ $ts.dark }}</MkRadio>
- </div>
- </div>
- </div>
- <div class="_content _card _gap">
- <div class="list-view _content">
- <div v-for="([ k, v ], i) in theme" :key="k" class="item">
- <div class="_inputs">
- <div>
- {{ k.startsWith('$') ? `${k} (${$ts._theme.constant})` : $t('_theme.keys.' + k) }}
- <button v-if="k.startsWith('$')" class="_button _link" @click="del(i)" v-text="$ts.delete" />
- </div>
- <div>
- <div class="type" @click="chooseType($event, i)">
- {{ getTypeOf(v) }} <i class="fas fa-chevron-down"></i>
- </div>
- <!-- default -->
- <div v-if="v === null" class="default-value" v-text="baseProps[k]" />
- <!-- 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:modelValue="colorChanged($event, i)"/>
- </div>
- <!-- ref const -->
- <MkInput v-else-if="v.type === 'refConst'" v-model="v.key">
- <template #prefix>$</template>
- <span>{{ $ts.name }}</span>
- </MkInput>
- <!-- ref props -->
- <MkSelect v-else-if="v.type === 'refProp'" v-model="v.key" class="select">
- <option v-for="key in themeProps" :key="key" :value="key">{{ $t('_theme.keys.' + key) }}</option>
- </MkSelect>
- <!-- func -->
- <template v-else-if="v.type === 'func'">
- <MkSelect v-model="v.name" class="select">
- <template #label>{{ $ts._theme.funcKind }}</template>
- <option v-for="n in ['alpha', 'darken', 'lighten']" :key="n" :value="n">{{ $t('_theme.' + n) }}</option>
- </MkSelect>
- <MkInput v-model="v.arg" type="number"><span>{{ $ts._theme.argument }}</span></MkInput>
- <MkSelect v-model="v.value" class="select">
- <template #label>{{ $ts._theme.basedProp }}</template>
- <option v-for="key in themeProps" :key="key" :value="key">{{ $t('_theme.keys.' + key) }}</option>
- </MkSelect>
- </template>
- <!-- CSS -->
- <MkInput v-else-if="v.type === 'css'" v-model="v.value">
- <span>CSS</span>
- </MkInput>
- </div>
- </div>
- </div>
- <MkButton primary @click="addConst">{{ $ts._theme.addConstant }}</MkButton>
- </div>
- </div>
- </section>
- <section class="_section">
- <details class="_content">
- <summary>{{ $ts.sample }}</summary>
- <MkSample/>
- </details>
- </section>
- <section class="_section">
- <div class="_content">
- <MkButton inline @click="preview">{{ $ts.preview }}</MkButton>
- <MkButton inline primary :disabled="!name || !author" @click="save">{{ $ts.save }}</MkButton>
- </div>
- </section>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import { toUnicode } from 'punycode/';
-
-import MkRadio from '@/components/form/radio.vue';
-import MkButton from '@/components/ui/button.vue';
-import MkInput from '@/components/form/input.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import MkSelect from '@/components/form/select.vue';
-import MkSample from '@/components/sample.vue';
-
-import { convertToMisskeyTheme, ThemeValue, convertToViewModel, ThemeViewModel } from '@/scripts/theme-editor';
-import { Theme, applyTheme, lightTheme, darkTheme, themeProps, validateTheme } from '@/scripts/theme';
-import { host } from '@/config';
-import * as os from '@/os';
-import { ColdDeviceStorage } from '@/store';
-import { addTheme } from '@/theme-store';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- MkRadio,
- MkButton,
- MkInput,
- MkTextarea,
- MkSelect,
- MkSample,
- },
-
- async beforeRouteLeave(to, from, next) {
- if (this.changed && !(await this.confirm())) {
- next(false);
- } else {
- next();
- }
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.themeEditor,
- icon: 'fas fa-palette',
- },
- theme: [] as ThemeViewModel,
- name: '',
- description: '',
- baseTheme: 'light' as 'dark' | 'light',
- author: `@${this.$i.username}@${toUnicode(host)}`,
- themeToImport: '',
- changed: false,
- lightTheme, darkTheme, themeProps,
- }
- },
-
- computed: {
- baseProps() {
- return this.baseTheme === 'light' ? this.lightTheme.props : this.darkTheme.props;
- },
- },
-
- beforeUnmount() {
- window.removeEventListener('beforeunload', this.beforeunload);
- },
-
- mounted() {
- this.init();
- window.addEventListener('beforeunload', this.beforeunload);
- const changed = () => this.changed = true;
- this.$watch('name', changed);
- this.$watch('description', changed);
- this.$watch('baseTheme', changed);
- this.$watch('author', changed);
- this.$watch('theme', changed);
- },
-
- methods: {
- beforeunload(e: BeforeUnloadEvent) {
- if (this.changed) {
- e.preventDefault();
- e.returnValue = '';
- }
- },
-
- async confirm(): Promise<boolean> {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: this.$ts.leaveConfirm,
- });
- return !canceled;
- },
-
- init() {
- const t: ThemeViewModel = [];
- for (const key of themeProps) {
- t.push([ key, null ]);
- }
- this.theme = t;
- },
-
- async del(i: number) {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: this.$t('_theme.deleteConstantConfirm', { const: this.theme[i][0] }),
- });
- if (canceled) return;
- Vue.delete(this.theme, i);
- },
-
- async addConst() {
- const { canceled, result } = await os.inputText({
- title: this.$ts._theme.inputConstantName,
- });
- if (canceled) return;
- this.theme.push([ '$' + result, '#000000']);
- },
-
- save() {
- const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
- addTheme(theme);
- os.alert({
- type: 'success',
- text: this.$t('_theme.installed', { name: theme.name })
- });
- this.changed = false;
- },
-
- preview() {
- const theme = convertToMisskeyTheme(this.theme, this.name, this.description, this.author, this.baseTheme);
- try {
- applyTheme(theme, false);
- } catch (e) {
- os.alert({
- type: 'error',
- text: e.message
- });
- }
- },
-
- async importTheme() {
- if (this.changed && (!await this.confirm())) return;
-
- try {
- const theme = JSON5.parse(this.themeToImport) as Theme;
- if (!validateTheme(theme)) throw new Error(this.$ts._theme.invalid);
-
- this.name = theme.name;
- this.description = theme.desc || '';
- this.author = theme.author;
- this.baseTheme = theme.base || 'light';
- this.theme = convertToViewModel(theme);
- this.themeToImport = '';
- } catch (e) {
- os.alert({
- type: 'error',
- text: e.message
- });
- }
- },
-
- colorChanged(color: string, i: number) {
- this.theme[i] = [this.theme[i][0], color];
- },
-
- getTypeOf(v: ThemeValue) {
- return v === null
- ? this.$ts._theme.defaultValue
- : typeof v === 'string'
- ? this.$ts._theme.color
- : this.$t('_theme.' + v.type);
- },
-
- async chooseType(e: MouseEvent, i: number) {
- const newValue = await this.showTypeMenu(e);
- this.theme[i] = [ this.theme[i][0], newValue ];
- },
-
- showTypeMenu(e: MouseEvent) {
- return new Promise<ThemeValue>((resolve) => {
- os.popupMenu([{
- text: this.$ts._theme.defaultValue,
- action: () => resolve(null),
- }, {
- text: this.$ts._theme.color,
- action: () => resolve('#000000'),
- }, {
- text: this.$ts._theme.func,
- action: () => resolve({
- type: 'func', name: 'alpha', arg: 1, value: 'accent'
- }),
- }, {
- text: this.$ts._theme.refProp,
- action: () => resolve({
- type: 'refProp', key: 'accent',
- }),
- }, {
- text: this.$ts._theme.refConst,
- action: () => resolve({
- type: 'refConst', key: '',
- }),
- }, {
- text: 'CSS',
- action: () => resolve({
- type: 'css', value: '',
- }),
- }], e.currentTarget || e.target);
- });
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.t9makv94 {
- > ._section {
- > ._content {
- > .list-view {
- > .item {
- min-height: 48px;
- word-break: break-all;
-
- &:not(:last-child) {
- margin-bottom: 8px;
- }
-
- .select {
- margin: 24px 0;
- }
-
- .type {
- cursor: pointer;
- }
-
- .default-value {
- opacity: 0.6;
- pointer-events: none;
- user-select: none;
- }
-
- .color {
- > input {
- display: inline-block;
- width: 1.5em;
- height: 1.5em;
- }
-
- > div {
- margin-left: 8px;
- display: inline-block;
- }
- }
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue
index ca94640dda..53727823a4 100644
--- a/packages/client/src/pages/announcements.vue
+++ b/packages/client/src/pages/announcements.vue
@@ -36,7 +36,7 @@ export default defineComponent({
bg: 'var(--bg)',
},
pagination: {
- endpoint: 'announcements',
+ endpoint: 'announcements' as const,
limit: 10,
},
};
diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue
index 67ab2d8981..c9a8f36844 100644
--- a/packages/client/src/pages/channel.vue
+++ b/packages/client/src/pages/channel.vue
@@ -67,11 +67,11 @@ export default defineComponent({
channel: null,
showBanner: true,
pagination: {
- endpoint: 'channels/timeline',
+ endpoint: 'channels/timeline' as const,
limit: 10,
- params: () => ({
+ params: computed(() => ({
channelId: this.channelId,
- })
+ }))
},
};
},
diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue
index 48877ab3ec..4e538a6da3 100644
--- a/packages/client/src/pages/channels.vue
+++ b/packages/client/src/pages/channels.vue
@@ -60,15 +60,15 @@ export default defineComponent({
})),
tab: 'featured',
featuredPagination: {
- endpoint: 'channels/featured',
+ endpoint: 'channels/featured' as const,
noPaging: true,
},
followingPagination: {
- endpoint: 'channels/followed',
+ endpoint: 'channels/followed' as const,
limit: 5,
},
ownedPagination: {
- endpoint: 'channels/owned',
+ endpoint: 'channels/owned' as const,
limit: 5,
},
};
diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue
index 077a6ac8b5..6b49221d32 100644
--- a/packages/client/src/pages/clip.vue
+++ b/packages/client/src/pages/clip.vue
@@ -50,11 +50,11 @@ export default defineComponent({
} : null),
clip: null,
pagination: {
- endpoint: 'clips/notes',
+ endpoint: 'clips/notes' as const,
limit: 10,
- params: () => ({
+ params: computed(() => ({
clipId: this.clipId,
- })
+ }))
},
};
},
diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue
index f30000367f..1e17bea0cc 100644
--- a/packages/client/src/pages/drive.vue
+++ b/packages/client/src/pages/drive.vue
@@ -4,27 +4,21 @@
</div>
</template>
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import XDrive from '@/components/drive.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XDrive
- },
+let folder = $ref(null);
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: computed(() => this.folder ? this.folder.name : this.$ts.drive),
- icon: 'fas fa-cloud',
- bg: 'var(--bg)',
- hideHeader: true,
- },
- folder: null,
- };
- },
+defineExpose({
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: folder ? folder.name : i18n.locale.drive,
+ icon: 'fas fa-cloud',
+ bg: 'var(--bg)',
+ hideHeader: true,
+ })),
});
</script>
diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue
index 5dab72daea..83539ce7a3 100644
--- a/packages/client/src/pages/emojis.emoji.vue
+++ b/packages/client/src/pages/emojis.emoji.vue
@@ -8,35 +8,29 @@
</button>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import * as os from '@/os';
import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { i18n } from '@/i18n';
-export default defineComponent({
- props: {
- emoji: {
- type: Object,
- required: true,
- }
- },
+const props = defineProps<{
+ emoji: Record<string, unknown>; // TODO
+}>();
- methods: {
- menu(ev) {
- os.popupMenu([{
- type: 'label',
- text: ':' + this.emoji.name + ':',
- }, {
- text: this.$ts.copy,
- icon: 'fas fa-copy',
- action: () => {
- copyToClipboard(`:${this.emoji.name}:`);
- os.success();
- }
- }], ev.currentTarget || ev.target);
+function menu(ev) {
+ os.popupMenu([{
+ type: 'label',
+ text: ':' + props.emoji.name + ':',
+ }, {
+ text: i18n.locale.copy,
+ icon: 'fas fa-copy',
+ action: () => {
+ copyToClipboard(`:${props.emoji.name}:`);
+ os.success();
}
- }
-});
+ }], ev.currentTarget || ev.target);
+}
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/emojis.vue b/packages/client/src/pages/emojis.vue
index 2adb5345e2..6577f5abd9 100644
--- a/packages/client/src/pages/emojis.vue
+++ b/packages/client/src/pages/emojis.vue
@@ -4,55 +4,47 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent, computed } from 'vue';
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import XCategory from './emojis.category.vue';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XCategory,
- },
+const tab = ref('category');
- data() {
- return {
- [symbols.PAGE_INFO]: computed(() => ({
- title: this.$ts.customEmojis,
- icon: 'fas fa-laugh',
- bg: 'var(--bg)',
- actions: [{
- icon: 'fas fa-ellipsis-h',
- handler: this.menu
- }],
- })),
- tab: 'category',
+function menu(ev) {
+ os.popupMenu([{
+ icon: 'fas fa-download',
+ text: i18n.locale.export,
+ action: async () => {
+ os.api('export-custom-emojis', {
+ })
+ .then(() => {
+ os.alert({
+ type: 'info',
+ text: i18n.locale.exportRequested,
+ });
+ }).catch((e) => {
+ os.alert({
+ type: 'error',
+ text: e.message,
+ });
+ });
}
- },
+ }], ev.currentTarget || ev.target);
+}
- methods: {
- menu(ev) {
- os.popupMenu([{
- icon: 'fas fa-download',
- text: this.$ts.export,
- action: async () => {
- os.api('export-custom-emojis', {
- })
- .then(() => {
- os.alert({
- type: 'info',
- text: this.$ts.exportRequested,
- });
- }).catch((e) => {
- os.alert({
- type: 'error',
- text: e.message,
- });
- });
- }
- }], ev.currentTarget || ev.target);
- }
- }
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.customEmojis,
+ icon: 'fas fa-laugh',
+ bg: 'var(--bg)',
+ actions: [{
+ icon: 'fas fa-ellipsis-h',
+ handler: menu,
+ }],
+ },
});
</script>
diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue
index a3c3b771f2..04cc3662a7 100644
--- a/packages/client/src/pages/explore.vue
+++ b/packages/client/src/pages/explore.vue
@@ -156,7 +156,7 @@ export default defineComponent({
sort: '+createdAt',
} },
searchPagination: {
- endpoint: 'users/search',
+ endpoint: 'users/search' as const,
limit: 10,
params: computed(() => (this.searchQuery && this.searchQuery !== '') ? {
query: this.searchQuery,
@@ -178,7 +178,7 @@ export default defineComponent({
},
tagUsers(): any {
return {
- endpoint: 'hashtags/users',
+ endpoint: 'hashtags/users' as const,
limit: 30,
params: {
tag: this.tag,
diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue
index faab864744..8965b30d60 100644
--- a/packages/client/src/pages/favorites.vue
+++ b/packages/client/src/pages/favorites.vue
@@ -1,49 +1,49 @@
<template>
-<div class="jmelgwjh">
- <div class="body">
- <XNotes class="notes" :pagination="pagination" :detail="true" :prop="'note'"/>
- </div>
-</div>
+<MkSpacer :content-max="800">
+ <MkPagination ref="pagingComponent" :pagination="pagination">
+ <template #empty>
+ <div class="_fullinfo">
+ <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+ <div>{{ $ts.noNotes }}</div>
+ </div>
+ </template>
+
+ <template #default="{ items }">
+ <XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
+ <XNote :key="item.id" :note="item.note" :class="$style.note"/>
+ </XList>
+ </template>
+ </MkPagination>
+</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XNotes from '@/components/notes.vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { ref } from 'vue';
+import MkPagination from '@/components/ui/pagination.vue';
+import XNote from '@/components/note.vue';
+import XList from '@/components/date-separated-list.vue';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XNotes
- },
+const pagination = {
+ endpoint: 'i/favorites' as const,
+ limit: 10,
+};
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.favorites,
- icon: 'fas fa-star',
- bg: 'var(--bg)',
- },
- pagination: {
- endpoint: 'i/favorites',
- limit: 10,
- params: () => ({
- })
- },
- };
+const pagingComponent = ref<InstanceType<typeof MkPagination>>();
+
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.favorites,
+ icon: 'fas fa-star',
+ bg: 'var(--bg)',
},
});
</script>
-<style lang="scss" scoped>
-.jmelgwjh {
- background: var(--bg);
-
- > .body {
- box-sizing: border-box;
- max-width: 800px;
- margin: 0 auto;
- padding: 16px;
- }
+<style lang="scss" module>
+.note {
+ background: var(--panel);
+ border-radius: var(--radius);
}
</style>
diff --git a/packages/client/src/pages/featured.vue b/packages/client/src/pages/featured.vue
index 0844c0952f..725c70f0f7 100644
--- a/packages/client/src/pages/featured.vue
+++ b/packages/client/src/pages/featured.vue
@@ -4,29 +4,22 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XNotes
- },
+const pagination = {
+ endpoint: 'notes/featured' as const,
+ limit: 10,
+ offsetMode: true,
+};
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.featured,
- icon: 'fas fa-fire-alt',
- bg: 'var(--bg)',
- },
- pagination: {
- endpoint: 'notes/featured',
- limit: 10,
- offsetMode: true,
- },
- };
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.featured,
+ icon: 'fas fa-fire-alt',
+ bg: 'var(--bg)',
},
});
</script>
diff --git a/packages/client/src/pages/federation.vue b/packages/client/src/pages/federation.vue
index 4e5f428ff9..6a4a28b6b4 100644
--- a/packages/client/src/pages/federation.vue
+++ b/packages/client/src/pages/federation.vue
@@ -6,7 +6,7 @@
<template #prefix><i class="fas fa-search"></i></template>
<template #label>{{ $ts.host }}</template>
</MkInput>
- <div class="_inputSplit" style="margin-top: var(--margin);">
+ <FormSplit style="margin-top: var(--margin);">
<MkSelect v-model="state">
<template #label>{{ $ts.state }}</template>
<option value="all">{{ $ts.all }}</option>
@@ -38,7 +38,7 @@
<option value="+driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.descendingOrder }})</option>
<option value="-driveFiles">{{ $ts.driveFilesCount }} ({{ $ts.ascendingOrder }})</option>
</MkSelect>
- </div>
+ </FormSplit>
</div>
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
@@ -95,75 +95,50 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import MkSelect from '@/components/form/select.vue';
import MkPagination from '@/components/ui/pagination.vue';
+import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkButton,
- MkInput,
- MkSelect,
- MkPagination,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.federation,
- icon: 'fas fa-globe',
- bg: 'var(--bg)',
- },
- host: '',
- state: 'federating',
- sort: '+pubSub',
- pagination: {
- endpoint: 'federation/instances',
- limit: 10,
- offsetMode: true,
- params: () => ({
- sort: this.sort,
- host: this.host != '' ? this.host : null,
- ...(
- this.state === 'federating' ? { federating: true } :
- this.state === 'subscribing' ? { subscribing: true } :
- this.state === 'publishing' ? { publishing: true } :
- this.state === 'suspended' ? { suspended: true } :
- this.state === 'blocked' ? { blocked: true } :
- this.state === 'notResponding' ? { notResponding: true } :
- {})
- })
- },
- }
- },
+let host = $ref('');
+let state = $ref('federating');
+let sort = $ref('+pubSub');
+const pagination = {
+ endpoint: 'federation/instances' as const,
+ limit: 10,
+ offsetMode: true,
+ params: computed(() => ({
+ sort: sort,
+ host: host != '' ? host : null,
+ ...(
+ state === 'federating' ? { federating: true } :
+ state === 'subscribing' ? { subscribing: true } :
+ state === 'publishing' ? { publishing: true } :
+ state === 'suspended' ? { suspended: true } :
+ state === 'blocked' ? { blocked: true } :
+ state === 'notResponding' ? { notResponding: true } :
+ {})
+ }))
+};
- watch: {
- host() {
- this.$refs.instances.reload();
- },
- state() {
- this.$refs.instances.reload();
- }
- },
+function getStatus(instance) {
+ if (instance.isSuspended) return 'suspended';
+ if (instance.isNotResponding) return 'error';
+ return 'alive';
+};
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.federation,
+ icon: 'fas fa-globe',
+ bg: 'var(--bg)',
},
-
- methods: {
- getStatus(instance) {
- if (instance.isSuspended) return 'suspended';
- if (instance.isNotResponding) return 'error';
- return 'alive';
- },
- }
});
</script>
diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue
index 54d695091d..764daa0d3e 100644
--- a/packages/client/src/pages/follow-requests.vue
+++ b/packages/client/src/pages/follow-requests.vue
@@ -1,6 +1,6 @@
<template>
<div>
- <MkPagination ref="list" :pagination="pagination" class="mk-follow-requests">
+ <MkPagination ref="paginationComponent" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
@@ -8,19 +8,21 @@
</div>
</template>
<template v-slot="{items}">
- <div v-for="req in items" :key="req.id" class="user _panel">
- <MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
- <div class="body">
- <div class="name">
- <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
- <p class="acct">@{{ acct(req.follower) }}</p>
- </div>
- <div v-if="req.follower.description" class="description" :title="req.follower.description">
- <Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
- </div>
- <div class="actions">
- <button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
- <button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
+ <div class="mk-follow-requests">
+ <div v-for="req in items" :key="req.id" class="user _panel">
+ <MkAvatar class="avatar" :user="req.follower" :show-indicator="true"/>
+ <div class="body">
+ <div class="name">
+ <MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
+ <p class="acct">@{{ acct(req.follower) }}</p>
+ </div>
+ <div v-if="req.follower.description" class="description" :title="req.follower.description">
+ <Mfm :text="req.follower.description" :is-note="false" :author="req.follower" :i="$i" :custom-emojis="req.follower.emojis" :plain="true" :nowrap="true"/>
+ </div>
+ <div class="actions">
+ <button class="_button" @click="accept(req.follower)"><i class="fas fa-check"></i></button>
+ <button class="_button" @click="reject(req.follower)"><i class="fas fa-times"></i></button>
+ </div>
</div>
</div>
</div>
@@ -29,45 +31,39 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
import MkPagination from '@/components/ui/pagination.vue';
import { userPage, acct } from '@/filters/user';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkPagination
- },
+const paginationComponent = ref<InstanceType<typeof MkPagination>>();
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.followRequests,
- icon: 'fas fa-user-clock',
- },
- pagination: {
- endpoint: 'following/requests/list',
- limit: 10,
- },
- };
- },
+const pagination = {
+ endpoint: 'following/requests/list' as const,
+ limit: 10,
+};
- methods: {
- accept(user) {
- os.api('following/requests/accept', { userId: user.id }).then(() => {
- this.$refs.list.reload();
- });
- },
- reject(user) {
- os.api('following/requests/reject', { userId: user.id }).then(() => {
- this.$refs.list.reload();
- });
- },
- userPage,
- acct
- }
+function accept(user) {
+ os.api('following/requests/accept', { userId: user.id }).then(() => {
+ paginationComponent.value.reload();
+ });
+}
+
+function reject(user) {
+ os.api('following/requests/reject', { userId: user.id }).then(() => {
+ paginationComponent.value.reload();
+ });
+}
+
+defineExpose({
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: i18n.locale.followRequests,
+ icon: 'fas fa-user-clock',
+ bg: 'var(--bg)',
+ })),
});
</script>
diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue
index caca6aed4b..e3fa1a0fcd 100644
--- a/packages/client/src/pages/gallery/edit.vue
+++ b/packages/client/src/pages/gallery/edit.vue
@@ -1,16 +1,16 @@
<template>
-<FormBase>
+<div>
<FormSuspense :p="init">
<FormInput v-model="title">
- <span>{{ $ts.title }}</span>
+ <template #label>{{ $ts.title }}</template>
</FormInput>
<FormTextarea v-model="description" :max="500">
- <span>{{ $ts.description }}</span>
+ <template #label>{{ $ts.description }}</template>
</FormTextarea>
<FormGroup>
- <div v-for="file in files" :key="file.id" class="_debobigegoItem _debobigegoPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
+ <div v-for="file in files" :key="file.id" class="_formGroup wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
<div class="name">{{ file.name }}</div>
<button v-tooltip="$ts.remove" class="remove _button" @click="remove(file)"><i class="fas fa-times"></i></button>
</div>
@@ -24,19 +24,17 @@
<FormButton v-if="postId" danger @click="del"><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
</FormSuspense>
-</FormBase>
+</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormTuple from '@/components/debobigego/tuple.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import FormButton from '@/components/ui/button.vue';
+import FormInput from '@/components/form/input.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import { selectFiles } from '@/scripts/select-file';
import * as os from '@/os';
import * as symbols from '@/symbols';
@@ -47,7 +45,6 @@ export default defineComponent({
FormInput,
FormTextarea,
FormSwitch,
- FormBase,
FormGroup,
FormSuspense,
},
diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue
index cd0d2a40e4..a19d69d5c2 100644
--- a/packages/client/src/pages/gallery/index.vue
+++ b/packages/client/src/pages/gallery/index.vue
@@ -81,19 +81,19 @@ export default defineComponent({
},
tab: 'explore',
recentPostsPagination: {
- endpoint: 'gallery/posts',
+ endpoint: 'gallery/posts' as const,
limit: 6,
},
popularPostsPagination: {
- endpoint: 'gallery/featured',
+ endpoint: 'gallery/featured' as const,
limit: 5,
},
myPostsPagination: {
- endpoint: 'i/gallery/posts',
+ endpoint: 'i/gallery/posts' as const,
limit: 5,
},
likedPostsPagination: {
- endpoint: 'i/gallery/likes',
+ endpoint: 'i/gallery/likes' as const,
limit: 5,
},
tags: [],
@@ -106,7 +106,7 @@ export default defineComponent({
},
tagUsers(): any {
return {
- endpoint: 'hashtags/users',
+ endpoint: 'hashtags/users' as const,
limit: 30,
params: {
tag: this.tag,
diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue
index 096947e6f8..1755c23286 100644
--- a/packages/client/src/pages/gallery/post.vue
+++ b/packages/client/src/pages/gallery/post.vue
@@ -1,6 +1,6 @@
<template>
<div class="_root">
- <transition name="fade" mode="out-in">
+ <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="post" class="rkxwuolj">
<div class="files">
<div v-for="file in post.files" :key="file.id" class="file">
@@ -93,11 +93,11 @@ export default defineComponent({
}]
} : null),
otherPostsPagination: {
- endpoint: 'users/gallery/posts',
+ endpoint: 'users/gallery/posts' as const,
limit: 6,
- params: () => ({
+ params: computed(() => ({
userId: this.post.user.id
- })
+ })),
},
post: null,
error: null,
diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue
index 85096d991a..fa36db0659 100644
--- a/packages/client/src/pages/instance-info.vue
+++ b/packages/client/src/pages/instance-info.vue
@@ -1,70 +1,71 @@
<template>
-<FormBase>
- <FormGroup v-if="instance">
- <template #label>{{ instance.host }}</template>
- <FormGroup>
- <div class="_debobigegoItem">
- <div class="_debobigegoPanel fnfelxur">
- <img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
- </div>
- </div>
- <FormKeyValueView>
- <template #key>Name</template>
- <template #value><span class="_monospace">{{ instance.name || `(${$ts.unknown})` }}</span></template>
- </FormKeyValueView>
- </FormGroup>
-
- <FormButton v-if="$i.isAdmin || $i.isModerator" primary @click="info">{{ $ts.settings }}</FormButton>
+<MkSpacer :content-max="600" :margin-min="16" :margin-max="32">
+ <div v-if="instance" class="_formRoot">
+ <div class="fnfelxur">
+ <img :src="instance.iconUrl || instance.faviconUrl" alt="" class="icon"/>
+ </div>
+ <MkKeyValue :copy="host" oneline style="margin: 1em 0;">
+ <template #key>Host</template>
+ <template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>Name</template>
+ <template #value>{{ instance.name || `(${$ts.unknown})` }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ $ts.description }}</template>
+ <template #value>{{ instance.description }}</template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>{{ $ts.software }}</template>
+ <template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }} / {{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>{{ $ts.administrator }}</template>
+ <template #value>{{ instance.maintainerName || `(${$ts.unknown})` }} ({{ instance.maintainerEmail || `(${$ts.unknown})` }})</template>
+ </MkKeyValue>
- <FormTextarea readonly :value="instance.description">
- <span>{{ $ts.description }}</span>
- </FormTextarea>
+ <FormSection v-if="iAmModerator">
+ <template #label>Moderation</template>
+ <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.stopActivityDelivery }}</FormSwitch>
+ <FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ $ts.blockThisInstance }}</FormSwitch>
+ </FormSection>
- <FormGroup>
- <FormKeyValueView>
- <template #key>{{ $ts.software }}</template>
- <template #value><span class="_monospace">{{ instance.softwareName || `(${$ts.unknown})` }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>{{ $ts.version }}</template>
- <template #value><span class="_monospace">{{ instance.softwareVersion || `(${$ts.unknown})` }}</span></template>
- </FormKeyValueView>
- </FormGroup>
- <FormGroup>
- <FormKeyValueView>
- <template #key>{{ $ts.administrator }}</template>
- <template #value><span class="_monospace">{{ instance.maintainerName || `(${$ts.unknown})` }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>{{ $ts.contact }}</template>
- <template #value><span class="_monospace">{{ instance.maintainerEmail || `(${$ts.unknown})` }}</span></template>
- </FormKeyValueView>
- </FormGroup>
- <FormGroup>
- <FormKeyValueView>
+ <FormSection>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>{{ $ts.registeredAt }}</template>
+ <template #value><MkTime mode="detail" :time="instance.caughtAt"/></template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>{{ $ts.updatedAt }}</template>
+ <template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.latestRequestSentAt }}</template>
<template #value><MkTime v-if="instance.latestRequestSentAt" :time="instance.latestRequestSentAt"/><span v-else>N/A</span></template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.latestStatus }}</template>
<template #value>{{ instance.latestStatus ? instance.latestStatus : 'N/A' }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.latestRequestReceivedAt }}</template>
<template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
- </FormKeyValueView>
- </FormGroup>
- <FormGroup>
- <FormKeyValueView>
+ </MkKeyValue>
+ </FormSection>
+
+ <FormSection>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>Open Registrations</template>
<template #value>{{ instance.openRegistrations ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- </FormGroup>
- <div class="_debobigegoItem">
- <div class="_debobigegoLabel">{{ $ts.statistics }}</div>
- <div class="_debobigegoPanel cmhjzshl">
+ </MkKeyValue>
+ </FormSection>
+
+ <FormSection>
+ <template #label>{{ $ts.statistics }}</template>
+ <div class="cmhjzshl">
<div class="selects">
- <MkSelect v-model="chartSrc" style="margin: 0; flex: 1;">
+ <MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
<option value="instance-requests">{{ $ts._instanceCharts.requests }}</option>
<option value="instance-users">{{ $ts._instanceCharts.users }}</option>
<option value="instance-users-total">{{ $ts._instanceCharts.usersTotal }}</option>
@@ -83,147 +84,100 @@
</MkSelect>
</div>
<div class="chart">
- <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :detailed="true"></MkChart>
+ <MkChart :src="chartSrc" :span="chartSpan" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
</div>
</div>
- </div>
- <FormGroup>
- <FormKeyValueView>
- <template #key>{{ $ts.registeredAt }}</template>
- <template #value><MkTime mode="detail" :time="instance.caughtAt"/></template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>{{ $ts.updatedAt }}</template>
- <template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
- </FormKeyValueView>
- </FormGroup>
- <FormObjectView tall :value="instance">
- <span>Raw</span>
- </FormObjectView>
- <FormGroup>
+ </FormSection>
+
+ <MkObjectView tall :value="instance">
+ </MkObjectView>
+
+ <FormSection>
<template #label>Well-known resources</template>
- <FormLink :to="`https://${host}/.well-known/host-meta`" external>host-meta</FormLink>
- <FormLink :to="`https://${host}/.well-known/host-meta.json`" external>host-meta.json</FormLink>
- <FormLink :to="`https://${host}/.well-known/nodeinfo`" external>nodeinfo</FormLink>
- <FormLink :to="`https://${host}/robots.txt`" external>robots.txt</FormLink>
- <FormLink :to="`https://${host}/manifest.json`" external>manifest.json</FormLink>
- </FormGroup>
- <FormSuspense v-slot="{ result: dns }" :p="dnsPromiseFactory">
- <FormGroup>
- <template #label>DNS</template>
- <FormKeyValueView v-for="record in dns.a" :key="record">
- <template #key>A</template>
- <template #value><span class="_monospace">{{ record }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView v-for="record in dns.aaaa" :key="record">
- <template #key>AAAA</template>
- <template #value><span class="_monospace">{{ record }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView v-for="record in dns.cname" :key="record">
- <template #key>CNAME</template>
- <template #value><span class="_monospace">{{ record }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView v-for="record in dns.txt">
- <template #key>TXT</template>
- <template #value><span class="_monospace">{{ record[0] }}</span></template>
- </FormKeyValueView>
- </FormGroup>
- </FormSuspense>
- </FormGroup>
-</FormBase>
+ <FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
+ <FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink>
+ <FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink>
+ <FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink>
+ <FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
+ </FormSection>
+ </div>
+</MkSpacer>
</template>
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
import MkChart from '@/components/chart.vue';
-import FormObjectView from '@/components/debobigego/object-view.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import MkObjectView from '@/components/object-view.vue';
+import FormLink from '@/components/form/link.vue';
+import MkLink from '@/components/link.vue';
+import FormSection from '@/components/form/section.vue';
+import MkKeyValue from '@/components/key-value.vue';
import MkSelect from '@/components/form/select.vue';
+import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
-import MkInstanceInfo from '@/pages/admin/instance.vue';
+import { iAmModerator } from '@/account';
-export default defineComponent({
- components: {
- FormBase,
- FormTextarea,
- FormObjectView,
- FormButton,
- FormLink,
- FormGroup,
- FormKeyValueView,
- FormSuspense,
- MkSelect,
- MkChart,
- },
+const props = defineProps<{
+ host: string;
+}>();
- props: {
- host: {
- type: String,
- required: true
- }
- },
+let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null);
+let instance = $ref<misskey.entities.Instance | null>(null);
+let suspended = $ref(false);
+let isBlocked = $ref(false);
+let chartSrc = $ref('instance-requests');
+let chartSpan = $ref('hour');
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.instanceInfo,
- icon: 'fas fa-info-circle',
- actions: [{
- text: `https://${this.host}`,
- icon: 'fas fa-external-link-alt',
- handler: () => {
- window.open(`https://${this.host}`, '_blank');
- }
- }],
- },
- instance: null,
- dnsPromiseFactory: () => os.api('federation/dns', {
- host: this.host
- }),
- chartSrc: 'instance-requests',
- chartSpan: 'hour',
- }
- },
+async function fetch() {
+ meta = await os.api('meta', { detail: true });
+ instance = await os.api('federation/show-instance', {
+ host: props.host,
+ });
+ suspended = instance.isSuspended;
+ isBlocked = meta.blockedHosts.includes(instance.host);
+}
- mounted() {
- this.fetch();
- },
+async function toggleBlock(ev) {
+ if (meta == null) return;
+ await os.api('admin/update-meta', {
+ blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host)
+ });
+}
- methods: {
- number,
- bytes,
+async function toggleSuspend(v) {
+ await os.api('admin/federation/update-instance', {
+ host: instance.host,
+ isSuspended: suspended,
+ });
+}
- async fetch() {
- this.instance = await os.api('federation/show-instance', {
- host: this.host
- });
- },
+fetch();
- info() {
- os.popup(MkInstanceInfo, {
- instance: this.instance
- }, {}, 'closed');
- }
- }
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: props.host,
+ icon: 'fas fa-info-circle',
+ bg: 'var(--bg)',
+ actions: [{
+ text: `https://${props.host}`,
+ icon: 'fas fa-external-link-alt',
+ handler: () => {
+ window.open(`https://${props.host}`, '_blank');
+ }
+ }],
+ },
});
</script>
<style lang="scss" scoped>
.fnfelxur {
- padding: 16px;
-
> .icon {
display: block;
- margin: auto;
+ margin: 0;
height: 64px;
border-radius: 8px;
}
@@ -232,7 +186,7 @@ export default defineComponent({
.cmhjzshl {
> .selects {
display: flex;
- padding: 16px;
+ margin: 0 0 16px 0;
}
}
</style>
diff --git a/packages/client/src/pages/mentions.vue b/packages/client/src/pages/mentions.vue
index 691d3bd9aa..bda56fc729 100644
--- a/packages/client/src/pages/mentions.vue
+++ b/packages/client/src/pages/mentions.vue
@@ -4,28 +4,21 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XNotes
- },
+const pagination = {
+ endpoint: 'notes/mentions' as const,
+ limit: 10,
+};
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.mentions,
- icon: 'fas fa-at',
- bg: 'var(--bg)',
- },
- pagination: {
- endpoint: 'notes/mentions',
- limit: 10,
- },
- };
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.mentions,
+ icon: 'fas fa-at',
+ bg: 'var(--bg)',
},
});
</script>
diff --git a/packages/client/src/pages/messages.vue b/packages/client/src/pages/messages.vue
index 9085af9489..8efdc55586 100644
--- a/packages/client/src/pages/messages.vue
+++ b/packages/client/src/pages/messages.vue
@@ -4,31 +4,24 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XNotes
- },
+const pagination = {
+ endpoint: 'notes/mentions' as const,
+ limit: 10,
+ params: () => ({
+ visibility: 'specified'
+ }),
+};
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.directNotes,
- icon: 'fas fa-envelope',
- bg: 'var(--bg)',
- },
- pagination: {
- endpoint: 'notes/mentions',
- limit: 10,
- params: () => ({
- visibility: 'specified'
- })
- },
- };
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.directNotes,
+ icon: 'fas fa-envelope',
+ bg: 'var(--bg)',
},
});
</script>
diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue
index 01f9d4518f..554ebc4b6b 100644
--- a/packages/client/src/pages/messaging/index.vue
+++ b/packages/client/src/pages/messaging/index.vue
@@ -44,6 +44,7 @@ import * as Acct from 'misskey-js/built/acct';
import MkButton from '@/components/ui/button.vue';
import { acct } from '@/filters/user';
import * as os from '@/os';
+import { stream } from '@/stream';
import * as symbols from '@/symbols';
export default defineComponent({
@@ -66,7 +67,7 @@ export default defineComponent({
},
mounted() {
- this.connection = markRaw(os.stream.useChannel('messagingIndex'));
+ this.connection = markRaw(stream.useChannel('messagingIndex'));
this.connection.on('message', this.onMessage);
this.connection.on('read', this.onRead);
diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue
index 8d92c430f1..0fc7c8a5df 100644
--- a/packages/client/src/pages/messaging/messaging-room.form.vue
+++ b/packages/client/src/pages/messaging/messaging-room.form.vue
@@ -7,7 +7,7 @@
ref="text"
v-model="text"
:placeholder="$ts.inputMessageHere"
- @keypress="onKeypress"
+ @keydown="onKeydown"
@compositionupdate="onCompositionUpdate"
@paste="onPaste"
></textarea>
@@ -28,6 +28,7 @@ import * as autosize from 'autosize';
import { formatTimeString } from '@/scripts/format-time-string';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
+import { stream } from '@/stream';
import { Autocomplete } from '@/scripts/autocomplete';
import { throttle } from 'throttle-debounce';
@@ -48,7 +49,7 @@ export default defineComponent({
file: null,
sending: false,
typing: throttle(3000, () => {
- os.stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id });
+ stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id });
}),
};
},
@@ -140,7 +141,7 @@ export default defineComponent({
//#endregion
},
- onKeypress(e) {
+ onKeydown(e) {
this.typing();
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
this.send();
diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue
index ffc7f7bc0d..a715dad6de 100644
--- a/packages/client/src/pages/messaging/messaging-room.vue
+++ b/packages/client/src/pages/messaging/messaging-room.vue
@@ -24,7 +24,7 @@
</I18n>
<MkEllipsis/>
</div>
- <transition name="fade">
+ <transition :name="$store.state.animation ? 'fade' : ''">
<div v-show="showIndicator" class="new-message">
<button class="_buttonPrimary" @click="onIndicatorClick"><i class="fas fa-arrow-circle-down"></i>{{ $ts.newMessageExists }}</button>
</div>
@@ -43,6 +43,7 @@ import XForm from './messaging-room.form.vue';
import * as Acct from 'misskey-js/built/acct';
import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
import * as os from '@/os';
+import { stream } from '@/stream';
import { popout } from '@/scripts/popout';
import * as sound from '@/scripts/sound';
import * as symbols from '@/symbols';
@@ -141,7 +142,7 @@ const Component = defineComponent({
this.group = group;
}
- this.connection = markRaw(os.stream.useChannel('messaging', {
+ this.connection = markRaw(stream.useChannel('messaging', {
otherparty: this.user ? this.user.id : undefined,
group: this.group ? this.group.id : undefined,
}));
@@ -161,7 +162,7 @@ const Component = defineComponent({
// もっと見るの交差検知を発火させないためにfetchは
// スクロールが終わるまでfalseにしておく
// scrollendのようなイベントはないのでsetTimeoutで
- setTimeout(() => this.fetching = false, 300);
+ window.setTimeout(() => this.fetching = false, 300);
});
},
@@ -299,9 +300,9 @@ const Component = defineComponent({
this.showIndicator = false;
});
- if (this.timer) clearTimeout(this.timer);
+ if (this.timer) window.clearTimeout(this.timer);
- this.timer = setTimeout(() => {
+ this.timer = window.setTimeout(() => {
this.showIndicator = false;
}, 4000);
},
diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue
index 173807475a..427c9935c3 100644
--- a/packages/client/src/pages/my-antennas/create.vue
+++ b/packages/client/src/pages/my-antennas/create.vue
@@ -4,45 +4,37 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@/components/ui/button.vue';
+<script lang="ts" setup>
+import { } from 'vue';
import XAntenna from './editor.vue';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { router } from '@/router';
-export default defineComponent({
- components: {
- MkButton,
- XAntenna,
- },
+let draft = $ref({
+ name: '',
+ src: 'all',
+ userListId: null,
+ userGroupId: null,
+ users: [],
+ keywords: [],
+ excludeKeywords: [],
+ withReplies: false,
+ caseSensitive: false,
+ withFile: false,
+ notify: false
+});
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.manageAntennas,
- icon: 'fas fa-satellite',
- },
- draft: {
- name: '',
- src: 'all',
- userListId: null,
- userGroupId: null,
- users: [],
- keywords: [],
- excludeKeywords: [],
- withReplies: false,
- caseSensitive: false,
- withFile: false,
- notify: false
- },
- };
- },
+function onAntennaCreated() {
+ router.push('/my/antennas');
+}
- methods: {
- onAntennaCreated() {
- this.$router.push('/my/antennas');
- },
- }
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.manageAntennas,
+ icon: 'fas fa-satellite',
+ bg: 'var(--bg)',
+ },
});
</script>
diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue
index d185e796c3..7138d269a9 100644
--- a/packages/client/src/pages/my-antennas/index.vue
+++ b/packages/client/src/pages/my-antennas/index.vue
@@ -38,7 +38,7 @@ export default defineComponent({
}
},
pagination: {
- endpoint: 'antennas/list',
+ endpoint: 'antennas/list' as const,
limit: 10,
},
};
diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue
index a5bbc3fd2d..97b563f6f8 100644
--- a/packages/client/src/pages/my-clips/index.vue
+++ b/packages/client/src/pages/my-clips/index.vue
@@ -3,7 +3,7 @@
<div class="qtcaoidl">
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
- <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="list">
+ <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="list">
<MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _gap">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>
@@ -13,71 +13,64 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import i18n from '@/components/global/i18n';
-export default defineComponent({
- components: {
- MkPagination,
- MkButton,
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.clip,
- icon: 'fas fa-paperclip',
- bg: 'var(--bg)',
- action: {
- icon: 'fas fa-plus',
- handler: this.create
- }
- },
- pagination: {
- endpoint: 'clips/list',
- limit: 10,
- },
- draft: null,
- };
- },
+const pagination = {
+ endpoint: 'clips/list' as const,
+ limit: 10,
+};
- methods: {
- async create() {
- const { canceled, result } = await os.form(this.$ts.createNewClip, {
- name: {
- type: 'string',
- label: this.$ts.name
- },
- description: {
- type: 'string',
- required: false,
- multiline: true,
- label: this.$ts.description
- },
- isPublic: {
- type: 'boolean',
- label: this.$ts.public,
- default: false
- }
- });
- if (canceled) return;
+const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
- os.apiWithDialog('clips/create', result);
+async function create() {
+ const { canceled, result } = await os.form(i18n.locale.createNewClip, {
+ name: {
+ type: 'string',
+ label: i18n.locale.name,
},
-
- onClipCreated() {
- this.$refs.list.reload();
- this.draft = null;
+ description: {
+ type: 'string',
+ required: false,
+ multiline: true,
+ label: i18n.locale.description,
},
+ isPublic: {
+ type: 'boolean',
+ label: i18n.locale.public,
+ default: false,
+ },
+ });
+ if (canceled) return;
+
+ os.apiWithDialog('clips/create', result);
+
+ pagingComponent.reload();
+}
+
+function onClipCreated() {
+ pagingComponent.reload();
+}
- onClipDeleted() {
- this.$refs.list.reload();
+function onClipDeleted() {
+ pagingComponent.reload();
+}
+
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.clip,
+ icon: 'fas fa-paperclip',
+ bg: 'var(--bg)',
+ action: {
+ icon: 'fas fa-plus',
+ handler: create
},
- }
+ },
});
</script>
diff --git a/packages/client/src/pages/my-groups/group.vue b/packages/client/src/pages/my-groups/group.vue
index c307f037a6..92c0483af9 100644
--- a/packages/client/src/pages/my-groups/group.vue
+++ b/packages/client/src/pages/my-groups/group.vue
@@ -1,6 +1,6 @@
<template>
<div class="mk-group-page">
- <transition name="zoom" mode="out-in">
+ <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<div v-if="group" class="_section">
<div class="_content" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
<MkButton inline @click="invite()">{{ $ts.invite }}</MkButton>
@@ -11,7 +11,7 @@
</div>
</transition>
- <transition name="zoom" mode="out-in">
+ <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<div v-if="group" class="_section members _gap">
<div class="_title">{{ $ts.members }}</div>
<div class="_content">
diff --git a/packages/client/src/pages/my-groups/index.vue b/packages/client/src/pages/my-groups/index.vue
index db5ccde466..4b2b2963a8 100644
--- a/packages/client/src/pages/my-groups/index.vue
+++ b/packages/client/src/pages/my-groups/index.vue
@@ -87,15 +87,15 @@ export default defineComponent({
})),
tab: 'owned',
ownedPagination: {
- endpoint: 'users/groups/owned',
+ endpoint: 'users/groups/owned' as const,
limit: 10,
},
joinedPagination: {
- endpoint: 'users/groups/joined',
+ endpoint: 'users/groups/joined' as const,
limit: 10,
},
invitationPagination: {
- endpoint: 'i/user-group-invites',
+ endpoint: 'i/user-group-invites' as const,
limit: 10,
},
};
diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue
index 94a869b9ff..e6fcba1b34 100644
--- a/packages/client/src/pages/my-lists/index.vue
+++ b/packages/client/src/pages/my-lists/index.vue
@@ -3,7 +3,7 @@
<div class="qkcjvfiv">
<MkButton primary class="add" @click="create"><i class="fas fa-plus"></i> {{ $ts.createList }}</MkButton>
- <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="lists _content">
+ <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists _content">
<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"/>
@@ -13,50 +13,41 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import MkAvatars from '@/components/avatars.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkPagination,
- MkButton,
- MkAvatars,
- },
+const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.manageLists,
- icon: 'fas fa-list-ul',
- bg: 'var(--bg)',
- action: {
- icon: 'fas fa-plus',
- handler: this.create
- },
- },
- pagination: {
- endpoint: 'users/lists/list',
- limit: 10,
- },
- };
- },
+const pagination = {
+ endpoint: 'users/lists/list' as const,
+ limit: 10,
+};
+
+async function create() {
+ const { canceled, result: name } = await os.inputText({
+ title: i18n.locale.enterListName,
+ });
+ if (canceled) return;
+ await os.apiWithDialog('users/lists/create', { name: name });
+ pagingComponent.reload();
+}
- methods: {
- async create() {
- const { canceled, result: name } = await os.inputText({
- title: this.$ts.enterListName,
- });
- if (canceled) return;
- await os.api('users/lists/create', { name: name });
- this.$refs.list.reload();
- os.success();
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.manageLists,
+ icon: 'fas fa-list-ul',
+ bg: 'var(--bg)',
+ action: {
+ icon: 'fas fa-plus',
+ handler: create,
},
- }
+ },
});
</script>
diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue
index a25522f933..bc24f58431 100644
--- a/packages/client/src/pages/my-lists/list.vue
+++ b/packages/client/src/pages/my-lists/list.vue
@@ -1,7 +1,7 @@
<template>
<MkSpacer :content-max="700">
<div class="mk-list-page">
- <transition name="zoom" mode="out-in">
+ <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<div v-if="list" class="_section">
<div class="_content">
<MkButton inline @click="addUser()">{{ $ts.addUser }}</MkButton>
@@ -11,7 +11,7 @@
</div>
</transition>
- <transition name="zoom" mode="out-in">
+ <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<div v-if="list" class="_section members _gap">
<div class="_title">{{ $ts.members }}</div>
<div class="_content">
diff --git a/packages/client/src/pages/not-found.vue b/packages/client/src/pages/not-found.vue
index 92d3f399f7..914fdb9297 100644
--- a/packages/client/src/pages/not-found.vue
+++ b/packages/client/src/pages/not-found.vue
@@ -7,19 +7,15 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.notFound,
- icon: 'fas fa-exclamation-triangle'
- },
- }
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.notFound,
+ icon: 'fas fa-exclamation-triangle',
+ bg: 'var(--bg)',
},
});
</script>
diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue
index d40082381c..efeea345dc 100644
--- a/packages/client/src/pages/note.vue
+++ b/packages/client/src/pages/note.vue
@@ -1,7 +1,7 @@
<template>
<MkSpacer :content-max="800">
<div class="fcuexfpr">
- <transition name="fade" mode="out-in">
+ <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="note" class="note">
<div v-if="showNext" class="_gap">
<XNotes class="_content" :pagination="next" :no-gap="true"/>
@@ -82,21 +82,21 @@ export default defineComponent({
showNext: false,
error: null,
prev: {
- endpoint: 'users/notes',
+ endpoint: 'users/notes' as const,
limit: 10,
- params: init => ({
+ params: computed(() => ({
userId: this.note.userId,
untilId: this.note.id,
- })
+ })),
},
next: {
reversed: true,
- endpoint: 'users/notes',
+ endpoint: 'users/notes' as const,
limit: 10,
- params: init => ({
+ params: computed(() => ({
userId: this.note.userId,
sinceId: this.note.id,
- })
+ })),
},
};
},
diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue
index 695c54a535..090e80f99a 100644
--- a/packages/client/src/pages/notifications.vue
+++ b/packages/client/src/pages/notifications.vue
@@ -6,70 +6,62 @@
</MkSpacer>
</template>
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import XNotifications from '@/components/notifications.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { notificationTypes } from 'misskey-js';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XNotifications
- },
+let tab = $ref('all');
+let includeTypes = $ref<string[] | null>(null);
- data() {
- return {
- [symbols.PAGE_INFO]: 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'; },
- },]
- })),
- tab: 'all',
- includeTypes: null,
- };
- },
-
- methods: {
- 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);
+function setFilter(ev) {
+ const typeItems = notificationTypes.map(t => ({
+ text: i18n.t(`_notification._types.${t}`),
+ active: includeTypes && includeTypes.includes(t),
+ action: () => {
+ includeTypes = [t];
+ }
+ }));
+ const items = includeTypes != null ? [{
+ icon: 'fas fa-times',
+ text: i18n.locale.clear,
+ action: () => {
+ includeTypes = null;
}
- }
+ }, null, ...typeItems] : typeItems;
+ os.popupMenu(items, ev.currentTarget || ev.target);
+}
+
+defineExpose({
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: i18n.locale.notifications,
+ icon: 'fas fa-bell',
+ bg: 'var(--bg)',
+ actions: [{
+ text: i18n.locale.filter,
+ icon: 'fas fa-filter',
+ highlighted: includeTypes != null,
+ handler: setFilter,
+ }, {
+ text: i18n.locale.markAllAsRead,
+ icon: 'fas fa-check',
+ handler: () => {
+ os.apiWithDialog('notifications/mark-all-as-read');
+ },
+ }],
+ tabs: [{
+ active: tab === 'all',
+ title: i18n.locale.all,
+ onClick: () => { tab = 'all'; },
+ }, {
+ active: tab === 'unread',
+ title: i18n.locale.unread,
+ onClick: () => { tab = 'unread'; },
+ },]
+ })),
});
</script>
diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue
index 3a4803c3a3..b2c039a269 100644
--- a/packages/client/src/pages/page.vue
+++ b/packages/client/src/pages/page.vue
@@ -1,6 +1,6 @@
<template>
<MkSpacer :content-max="700">
- <transition name="fade" mode="out-in">
+ <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="page" :key="page.id" v-size="{ max: [450] }" class="xcukqgmh">
<div class="_block main">
<!--
@@ -106,11 +106,11 @@ export default defineComponent({
page: null,
error: null,
otherPostsPagination: {
- endpoint: 'users/pages',
+ endpoint: 'users/pages' as const,
limit: 6,
- params: () => ({
+ params: computed(() => ({
userId: this.page.user.id
- })
+ })),
},
};
},
diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue
index f1dd64f119..dcccf7f7c4 100644
--- a/packages/client/src/pages/pages.vue
+++ b/packages/client/src/pages/pages.vue
@@ -62,15 +62,15 @@ export default defineComponent({
})),
tab: 'featured',
featuredPagesPagination: {
- endpoint: 'pages/featured',
+ endpoint: 'pages/featured' as const,
noPaging: true,
},
myPagesPagination: {
- endpoint: 'i/pages',
+ endpoint: 'i/pages' as const,
limit: 5,
},
likedPagesPagination: {
- endpoint: 'i/page-likes',
+ endpoint: 'i/page-likes' as const,
limit: 5,
},
};
diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue
index 9d1ebb74ed..8eb4549516 100644
--- a/packages/client/src/pages/preview.vue
+++ b/packages/client/src/pages/preview.vue
@@ -4,24 +4,18 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import MkSample from '@/components/sample.vue';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkSample,
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.preview,
- icon: 'fas fa-eye',
- },
- }
- },
+defineExpose({
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: i18n.locale.preview,
+ icon: 'fas fa-eye',
+ bg: 'var(--bg)',
+ })),
});
</script>
diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue
index f9a2500840..8ef73858f6 100644
--- a/packages/client/src/pages/reset-password.vue
+++ b/packages/client/src/pages/reset-password.vue
@@ -1,67 +1,53 @@
<template>
-<FormBase v-if="token">
- <FormInput v-model="password" type="password">
- <template #prefix><i class="fas fa-lock"></i></template>
- <span>{{ $ts.newPassword }}</span>
- </FormInput>
-
- <FormButton primary @click="save">{{ $ts.save }}</FormButton>
-</FormBase>
+<MkSpacer v-if="token" :content-max="700" :margin-min="16" :margin-max="32">
+ <div class="_formRoot">
+ <FormInput v-model="password" type="password" class="_formBlock">
+ <template #prefix><i class="fas fa-lock"></i></template>
+ <template #label>{{ i18n.locale.newPassword }}</template>
+ </FormInput>
+
+ <FormButton primary class="_formBlock" @click="save">{{ i18n.locale.save }}</FormButton>
+ </div>
+</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormButton from '@/components/debobigego/button.vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
+import FormInput from '@/components/form/input.vue';
+import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { router } from '@/router';
-export default defineComponent({
- components: {
- FormBase,
- FormGroup,
- FormLink,
- FormInput,
- FormButton,
- },
-
- props: {
- token: {
- type: String,
- required: false
- }
- },
+const props = defineProps<{
+ token?: string;
+}>();
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.resetPassword,
- icon: 'fas fa-lock'
- },
- password: '',
- }
- },
+let password = $ref('');
- mounted() {
- if (this.token == null) {
- os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed');
- this.$router.push('/');
- }
- },
+async function save() {
+ await os.apiWithDialog('reset-password', {
+ token: props.token,
+ password: password,
+ });
+ router.push('/');
+}
- methods: {
- async save() {
- await os.apiWithDialog('reset-password', {
- token: this.token,
- password: this.password,
- });
- this.$router.push('/');
- }
+onMounted(() => {
+ if (props.token == null) {
+ os.popup(import('@/components/forgot-password.vue'), {}, {}, 'closed');
+ router.push('/');
}
});
+
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.resetPassword,
+ icon: 'fas fa-lock',
+ bg: 'var(--bg)',
+ },
+});
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/reversi/game.board.vue b/packages/client/src/pages/reversi/game.board.vue
deleted file mode 100644
index eb6fef2799..0000000000
--- a/packages/client/src/pages/reversi/game.board.vue
+++ /dev/null
@@ -1,528 +0,0 @@
-<template>
-<div class="xqnhankfuuilcwvhgsopeqncafzsquya">
- <header><b><MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA></b>({{ $ts._reversi.black }}) vs <b><MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA></b>({{ $ts._reversi.white }})</header>
-
- <div style="overflow: hidden; line-height: 28px;">
- <p v-if="!iAmPlayer && !game.isEnded" class="turn">
- <Mfm :key="'turn:' + turnUser().name" :text="$t('_reversi.turnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
- <MkEllipsis/>
- </p>
- <p v-if="logPos != logs.length" class="turn">
- <Mfm :key="'past-turn-of:' + turnUser().name" :text="$t('_reversi.pastTurnOf', { name: turnUser().name })" :plain="true" :custom-emojis="turnUser().emojis"/>
- </p>
- <p v-if="iAmPlayer && !game.isEnded && !isMyTurn()" class="turn1">{{ $ts._reversi.opponentTurn }}<MkEllipsis/></p>
- <p v-if="iAmPlayer && !game.isEnded && isMyTurn()" class="turn2" style="animation: tada 1s linear infinite both;">{{ $ts._reversi.myTurn }}</p>
- <p v-if="game.isEnded && logPos == logs.length" class="result">
- <template v-if="game.winner">
- <Mfm :key="'won'" :text="$t('_reversi.won', { name: game.winner.name })" :plain="true" :custom-emojis="game.winner.emojis"/>
- <span v-if="game.surrendered != null"> ({{ $ts._reversi.surrendered }})</span>
- </template>
- <template v-else>{{ $ts._reversi.drawn }}</template>
- </p>
- </div>
-
- <div class="board">
- <div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-x">
- <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
- </div>
- <div class="flex">
- <div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-y">
- <div v-for="i in game.map.length">{{ i }}</div>
- </div>
- <div class="cells" :style="cellsStyle">
- <div v-for="(stone, i) in o.board"
- :class="{ empty: stone == null, none: o.map[i] == 'null', isEnded: game.isEnded, myTurn: !game.isEnded && isMyTurn(), can: turnUser() ? o.canPut(turnUser().id == blackUser.id, i) : null, prev: o.prevPos == i }"
- :title="`${String.fromCharCode(65 + o.transformPosToXy(i)[0])}${o.transformPosToXy(i)[1] + 1}`"
- @click="set(i)"
- >
- <template v-if="$store.state.gamesReversiUseAvatarStones || true">
- <img v-if="stone === true" :src="blackUser.avatarUrl" alt="black">
- <img v-if="stone === false" :src="whiteUser.avatarUrl" alt="white">
- </template>
- <template v-else>
- <i v-if="stone === true" class="fas fa-circle"></i>
- <i v-if="stone === false" class="far fa-circle"></i>
- </template>
- </div>
- </div>
- <div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-y">
- <div v-for="i in game.map.length">{{ i }}</div>
- </div>
- </div>
- <div v-if="$store.state.gamesReversiShowBoardLabels" class="labels-x">
- <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span>
- </div>
- </div>
-
- <p class="status"><b>{{ $t('_reversi.turnCount', { count: logPos }) }}</b> {{ $ts._reversi.black }}:{{ o.blackCount }} {{ $ts._reversi.white }}:{{ o.whiteCount }} {{ $ts._reversi.total }}:{{ o.blackCount + o.whiteCount }}</p>
-
- <div v-if="!game.isEnded && iAmPlayer" class="actions">
- <MkButton inline @click="surrender">{{ $ts._reversi.surrender }}</MkButton>
- </div>
-
- <div v-if="game.isEnded" class="player">
- <span>{{ logPos }} / {{ logs.length }}</span>
- <div v-if="!autoplaying" class="buttons">
- <MkButton inline :disabled="logPos == 0" @click="logPos = 0"><i class="fas fa-angle-double-left"></i></MkButton>
- <MkButton inline :disabled="logPos == 0" @click="logPos--"><i class="fas fa-angle-left"></i></MkButton>
- <MkButton inline :disabled="logPos == logs.length" @click="logPos++"><i class="fas fa-angle-right"></i></MkButton>
- <MkButton inline :disabled="logPos == logs.length" @click="logPos = logs.length"><i class="fas fa-angle-double-right"></i></MkButton>
- </div>
- <MkButton :disabled="autoplaying" style="margin: var(--margin) auto 0 auto;" @click="autoplay()"><i class="fas fa-play"></i></MkButton>
- </div>
-
- <div class="info">
- <p v-if="game.isLlotheo">{{ $ts._reversi.isLlotheo }}</p>
- <p v-if="game.loopedBoard">{{ $ts._reversi.loopedMap }}</p>
- <p v-if="game.canPutEverywhere">{{ $ts._reversi.canPutEverywhere }}</p>
- </div>
-
- <div class="watchers">
- <MkAvatar v-for="user in watchers" :key="user.id" :user="user" class="avatar"/>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as CRC32 from 'crc-32';
-import Reversi, { Color } from '@/scripts/games/reversi/core';
-import { url } from '@/config';
-import MkButton from '@/components/ui/button.vue';
-import { userPage } from '@/filters/user';
-import * as os from '@/os';
-import * as sound from '@/scripts/sound';
-
-export default defineComponent({
- components: {
- MkButton
- },
-
- props: {
- initGame: {
- type: Object,
- require: true
- },
- connection: {
- type: Object,
- require: true
- },
- },
-
- data() {
- return {
- game: JSON.parse(JSON.stringify(this.initGame)),
- o: null as Reversi,
- logs: [],
- logPos: 0,
- watchers: [],
- pollingClock: null,
- };
- },
-
- computed: {
- iAmPlayer(): boolean {
- if (!this.$i) return false;
- return this.game.user1Id == this.$i.id || this.game.user2Id == this.$i.id;
- },
-
- myColor(): Color {
- if (!this.iAmPlayer) return null;
- if (this.game.user1Id == this.$i.id && this.game.black == 1) return true;
- if (this.game.user2Id == this.$i.id && this.game.black == 2) return true;
- return false;
- },
-
- opColor(): Color {
- if (!this.iAmPlayer) return null;
- return this.myColor === true ? false : true;
- },
-
- blackUser(): any {
- return this.game.black == 1 ? this.game.user1 : this.game.user2;
- },
-
- whiteUser(): any {
- return this.game.black == 1 ? this.game.user2 : this.game.user1;
- },
-
- cellsStyle(): any {
- return {
- 'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`,
- 'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)`
- };
- }
- },
-
- watch: {
- logPos(v) {
- if (!this.game.isEnded) return;
- const o = new Reversi(this.game.map, {
- isLlotheo: this.game.isLlotheo,
- canPutEverywhere: this.game.canPutEverywhere,
- loopedBoard: this.game.loopedBoard
- });
- for (const log of this.logs.slice(0, v)) {
- o.put(log.color, log.pos);
- }
- this.o = o;
- //this.$forceUpdate();
- }
- },
-
- created() {
- this.o = new Reversi(this.game.map, {
- isLlotheo: this.game.isLlotheo,
- canPutEverywhere: this.game.canPutEverywhere,
- loopedBoard: this.game.loopedBoard
- });
-
- for (const log of this.game.logs) {
- this.o.put(log.color, log.pos);
- }
-
- this.logs = this.game.logs;
- this.logPos = this.logs.length;
-
- // 通信を取りこぼしてもいいように定期的にポーリングさせる
- if (this.game.isStarted && !this.game.isEnded) {
- this.pollingClock = setInterval(() => {
- if (this.game.isEnded) return;
- const crc32 = CRC32.str(this.logs.map(x => x.pos.toString()).join(''));
- this.connection.send('check', {
- crc32: crc32
- });
- }, 3000);
- }
- },
-
- mounted() {
- this.connection.on('set', this.onSet);
- this.connection.on('rescue', this.onRescue);
- this.connection.on('ended', this.onEnded);
- this.connection.on('watchers', this.onWatchers);
- },
-
- beforeUnmount() {
- this.connection.off('set', this.onSet);
- this.connection.off('rescue', this.onRescue);
- this.connection.off('ended', this.onEnded);
- this.connection.off('watchers', this.onWatchers);
-
- clearInterval(this.pollingClock);
- },
-
- methods: {
- userPage,
-
- // this.o がリアクティブになった折にはcomputedにできる
- turnUser(): any {
- if (this.o.turn === true) {
- return this.game.black == 1 ? this.game.user1 : this.game.user2;
- } else if (this.o.turn === false) {
- return this.game.black == 1 ? this.game.user2 : this.game.user1;
- } else {
- return null;
- }
- },
-
- // this.o がリアクティブになった折にはcomputedにできる
- isMyTurn(): boolean {
- if (!this.iAmPlayer) return false;
- if (this.turnUser() == null) return false;
- return this.turnUser().id == this.$i.id;
- },
-
- set(pos) {
- if (this.game.isEnded) return;
- if (!this.iAmPlayer) return;
- if (!this.isMyTurn()) return;
- if (!this.o.canPut(this.myColor, pos)) return;
-
- this.o.put(this.myColor, pos);
-
- // サウンドを再生する
- sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite');
-
- this.connection.send('set', {
- pos: pos
- });
-
- this.checkEnd();
-
- this.$forceUpdate();
- },
-
- onSet(x) {
- this.logs.push(x);
- this.logPos++;
- this.o.put(x.color, x.pos);
- this.checkEnd();
- this.$forceUpdate();
-
- // サウンドを再生する
- if (x.color !== this.myColor) {
- sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
- }
- },
-
- onEnded(x) {
- this.game = JSON.parse(JSON.stringify(x.game));
- },
-
- checkEnd() {
- this.game.isEnded = this.o.isEnded;
- if (this.game.isEnded) {
- if (this.o.winner === true) {
- this.game.winnerId = this.game.black == 1 ? this.game.user1Id : this.game.user2Id;
- this.game.winner = this.game.black == 1 ? this.game.user1 : this.game.user2;
- } else if (this.o.winner === false) {
- this.game.winnerId = this.game.black == 1 ? this.game.user2Id : this.game.user1Id;
- this.game.winner = this.game.black == 1 ? this.game.user2 : this.game.user1;
- } else {
- this.game.winnerId = null;
- this.game.winner = null;
- }
- }
- },
-
- // 正しいゲーム情報が送られてきたとき
- onRescue(game) {
- this.game = JSON.parse(JSON.stringify(game));
-
- this.o = new Reversi(this.game.map, {
- isLlotheo: this.game.isLlotheo,
- canPutEverywhere: this.game.canPutEverywhere,
- loopedBoard: this.game.loopedBoard
- });
-
- for (const log of this.game.logs) {
- this.o.put(log.color, log.pos, true);
- }
-
- this.logs = this.game.logs;
- this.logPos = this.logs.length;
-
- this.checkEnd();
- this.$forceUpdate();
- },
-
- onWatchers(users) {
- this.watchers = users;
- },
-
- surrender() {
- os.api('games/reversi/games/surrender', {
- gameId: this.game.id
- });
- },
-
- autoplay() {
- this.autoplaying = true;
- this.logPos = 0;
-
- setTimeout(() => {
- this.logPos = 1;
-
- let i = 1;
- let previousLog = this.game.logs[0];
- const tick = () => {
- const log = this.game.logs[i];
- const time = new Date(log.at).getTime() - new Date(previousLog.at).getTime()
- setTimeout(() => {
- i++;
- this.logPos++;
- previousLog = log;
-
- if (i < this.game.logs.length) {
- tick();
- } else {
- this.autoplaying = false;
- }
- }, time);
- };
-
- tick();
- }, 1000);
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-
-@use "sass:math";
-
-.xqnhankfuuilcwvhgsopeqncafzsquya {
- text-align: center;
-
- > .go-index {
- position: absolute;
- top: 0;
- left: 0;
- z-index: 1;
- width: 42px;
- height :42px;
- }
-
- > header {
- padding: 8px;
- border-bottom: dashed 1px var(--divider);
- }
-
- > .board {
- width: calc(100% - 16px);
- max-width: 500px;
- margin: 0 auto;
-
- $label-size: 16px;
- $gap: 4px;
-
- > .labels-x {
- height: $label-size;
- padding: 0 $label-size;
- display: flex;
-
- > * {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 0.8em;
-
- &:first-child {
- margin-left: -(math.div($gap, 2));
- }
-
- &:last-child {
- margin-right: -(math.div($gap, 2));
- }
- }
- }
-
- > .flex {
- display: flex;
-
- > .labels-y {
- width: $label-size;
- display: flex;
- flex-direction: column;
-
- > * {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
-
- &:first-child {
- margin-top: -(math.div($gap, 2));
- }
-
- &:last-child {
- margin-bottom: -(math.div($gap, 2));
- }
- }
- }
-
- > .cells {
- flex: 1;
- display: grid;
- grid-gap: $gap;
-
- > div {
- background: transparent;
- border-radius: 6px;
- overflow: hidden;
-
- * {
- pointer-events: none;
- user-select: none;
- }
-
- &.empty {
- border: solid 2px var(--divider);
- }
-
- &.empty.can {
- border-color: var(--accent);
- }
-
- &.empty.myTurn {
- border-color: var(--divider);
-
- &.can {
- border-color: var(--accent);
- cursor: pointer;
-
- &:hover {
- background: var(--accent);
- }
- }
- }
-
- &.prev {
- box-shadow: 0 0 0 4px var(--accent);
- }
-
- &.isEnded {
- border-color: var(--divider);
- }
-
- &.none {
- border-color: transparent !important;
- }
-
- > svg, > img {
- display: block;
- width: 100%;
- height: 100%;
- }
- }
- }
- }
- }
-
- > .status {
- margin: 0;
- padding: 16px 0;
- }
-
- > .actions {
- padding-bottom: 16px;
- }
-
- > .player {
- padding: 0 16px 32px 16px;
- margin: 0 auto;
- max-width: 500px;
-
- > span {
- display: inline-block;
- margin: 0 8px;
- min-width: 70px;
- }
-
- > .buttons {
- display: flex;
-
- > * {
- flex: 1;
- }
- }
- }
-
- > .watchers {
- padding: 0 0 16px 0;
-
- &:empty {
- display: none;
- }
-
- > .avatar {
- width: 32px;
- height: 32px;
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/reversi/game.setting.vue b/packages/client/src/pages/reversi/game.setting.vue
deleted file mode 100644
index 28bc598cfd..0000000000
--- a/packages/client/src/pages/reversi/game.setting.vue
+++ /dev/null
@@ -1,390 +0,0 @@
-<template>
-<div class="urbixznjwwuukfsckrwzwsqzsxornqij">
- <header><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></header>
-
- <div>
- <p>{{ $ts._reversi.gameSettings }}</p>
-
- <div class="card map _panel">
- <header>
- <select v-model="mapName" :placeholder="$ts._reversi.chooseBoard" @change="onMapChange">
- <option v-if="mapName == '-Custom-'" label="-Custom-" :value="mapName"/>
- <option :label="$ts.random" :value="null"/>
- <optgroup v-for="c in mapCategories" :key="c" :label="c">
- <option v-for="m in Object.values(maps).filter(m => m.category == c)" :key="m.name" :label="m.name" :value="m.name">{{ m.name }}</option>
- </optgroup>
- </select>
- </header>
-
- <div>
- <div v-if="game.map == null" class="random"><i class="fas fa-dice"></i></div>
- <div v-else class="board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
- <div v-for="(x, i) in game.map.join('')" :class="{ none: x == ' ' }" @click="onPixelClick(i, x)">
- <i v-if="x === 'b'" class="fas fa-circle"></i>
- <i v-if="x === 'w'" class="far fa-circle"></i>
- </div>
- </div>
- </div>
- </div>
-
- <div class="card _panel">
- <header>
- <span>{{ $ts._reversi.blackOrWhite }}</span>
- </header>
-
- <div>
- <MkRadio v-model="game.bw" value="random" @update:modelValue="updateSettings('bw')">{{ $ts.random }}</MkRadio>
- <MkRadio v-model="game.bw" :value="'1'" @update:modelValue="updateSettings('bw')">
- <I18n :src="$ts._reversi.blackIs" tag="span">
- <template #name>
- <b><MkUserName :user="game.user1"/></b>
- </template>
- </I18n>
- </MkRadio>
- <MkRadio v-model="game.bw" :value="'2'" @update:modelValue="updateSettings('bw')">
- <I18n :src="$ts._reversi.blackIs" tag="span">
- <template #name>
- <b><MkUserName :user="game.user2"/></b>
- </template>
- </I18n>
- </MkRadio>
- </div>
- </div>
-
- <div class="card _panel">
- <header>
- <span>{{ $ts._reversi.rules }}</span>
- </header>
-
- <div>
- <MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ $ts._reversi.isLlotheo }}</MkSwitch>
- <MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ $ts._reversi.loopedMap }}</MkSwitch>
- <MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ $ts._reversi.canPutEverywhere }}</MkSwitch>
- </div>
- </div>
-
- <div v-if="form" class="card form _panel">
- <header>
- <span>{{ $ts._reversi.botSettings }}</span>
- </header>
-
- <div>
- <template v-for="item in form">
- <MkSwitch v-if="item.type == 'switch'" :key="item.id" v-model="item.value" @change="onChangeForm(item)">{{ item.label || item.desc || '' }}</MkSwitch>
-
- <div v-if="item.type == 'radio'" :key="item.id" class="card">
- <header>
- <span>{{ item.label }}</span>
- </header>
-
- <div>
- <MkRadio v-for="(r, i) in item.items" :key="item.id + ':' + i" v-model="item.value" :value="r.value" @update:modelValue="onChangeForm(item)">{{ r.label }}</MkRadio>
- </div>
- </div>
-
- <div v-if="item.type == 'slider'" :key="item.id" class="card">
- <header>
- <span>{{ item.label }}</span>
- </header>
-
- <div>
- <input v-model="item.value" type="range" :min="item.min" :max="item.max" :step="item.step || 1" @change="onChangeForm(item)"/>
- </div>
- </div>
-
- <div v-if="item.type == 'textbox'" :key="item.id" class="card">
- <header>
- <span>{{ item.label }}</span>
- </header>
-
- <div>
- <input v-model="item.value" @change="onChangeForm(item)"/>
- </div>
- </div>
- </template>
- </div>
- </div>
- </div>
-
- <footer class="_acrylic">
- <p class="status">
- <template v-if="isAccepted && isOpAccepted">{{ $ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
- <template v-if="isAccepted && !isOpAccepted">{{ $ts._reversi.waitingForOther }}<MkEllipsis/></template>
- <template v-if="!isAccepted && isOpAccepted">{{ $ts._reversi.waitingForMe }}</template>
- <template v-if="!isAccepted && !isOpAccepted">{{ $ts._reversi.waitingBoth }}<MkEllipsis/></template>
- </p>
-
- <div class="actions">
- <MkButton inline @click="exit">{{ $ts.cancel }}</MkButton>
- <MkButton v-if="!isAccepted" inline primary @click="accept">{{ $ts._reversi.ready }}</MkButton>
- <MkButton v-if="isAccepted" inline primary @click="cancel">{{ $ts._reversi.cancelReady }}</MkButton>
- </div>
- </footer>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as maps from '@/scripts/games/reversi/maps';
-import MkButton from '@/components/ui/button.vue';
-import MkSwitch from '@/components/form/switch.vue';
-import MkRadio from '@/components/form/radio.vue';
-
-export default defineComponent({
- components: {
- MkButton,
- MkSwitch,
- MkRadio,
- },
-
- props: {
- initGame: {
- type: Object,
- require: true
- },
- connection: {
- type: Object,
- require: true
- },
- },
-
- data() {
- return {
- game: this.initGame,
- o: null,
- isLlotheo: false,
- mapName: maps.eighteight.name,
- maps: maps,
- form: null,
- messages: [],
- };
- },
-
- computed: {
- mapCategories(): string[] {
- const categories = Object.values(maps).map(x => x.category);
- return categories.filter((item, pos) => categories.indexOf(item) == pos);
- },
- isAccepted(): boolean {
- if (this.game.user1Id == this.$i.id && this.game.user1Accepted) return true;
- if (this.game.user2Id == this.$i.id && this.game.user2Accepted) return true;
- return false;
- },
- isOpAccepted(): boolean {
- if (this.game.user1Id != this.$i.id && this.game.user1Accepted) return true;
- if (this.game.user2Id != this.$i.id && this.game.user2Accepted) return true;
- return false;
- }
- },
-
- created() {
- this.connection.on('changeAccepts', this.onChangeAccepts);
- this.connection.on('updateSettings', this.onUpdateSettings);
- this.connection.on('initForm', this.onInitForm);
- this.connection.on('message', this.onMessage);
-
- if (this.game.user1Id != this.$i.id && this.game.form1) this.form = this.game.form1;
- if (this.game.user2Id != this.$i.id && this.game.form2) this.form = this.game.form2;
- },
-
- beforeUnmount() {
- this.connection.off('changeAccepts', this.onChangeAccepts);
- this.connection.off('updateSettings', this.onUpdateSettings);
- this.connection.off('initForm', this.onInitForm);
- this.connection.off('message', this.onMessage);
- },
-
- methods: {
- exit() {
-
- },
-
- accept() {
- this.connection.send('accept', {});
- },
-
- cancel() {
- this.connection.send('cancelAccept', {});
- },
-
- onChangeAccepts(accepts) {
- this.game.user1Accepted = accepts.user1;
- this.game.user2Accepted = accepts.user2;
- },
-
- updateSettings(key: string) {
- this.connection.send('updateSettings', {
- key: key,
- value: this.game[key]
- });
- },
-
- onUpdateSettings({ key, value }) {
- this.game[key] = value;
- if (this.game.map == null) {
- this.mapName = null;
- } else {
- const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join(''));
- this.mapName = found ? found.name : '-Custom-';
- }
- },
-
- onInitForm(x) {
- if (x.userId == this.$i.id) return;
- this.form = x.form;
- },
-
- onMessage(x) {
- if (x.userId == this.$i.id) return;
- this.messages.unshift(x.message);
- },
-
- onChangeForm(item) {
- this.connection.send('updateForm', {
- id: item.id,
- value: item.value
- });
- },
-
- onMapChange() {
- if (this.mapName == null) {
- this.game.map = null;
- } else {
- this.game.map = Object.values(maps).find(x => x.name == this.mapName).data;
- }
- this.updateSettings('map');
- },
-
- onPixelClick(pos, pixel) {
- const x = pos % this.game.map[0].length;
- const y = Math.floor(pos / this.game.map[0].length);
- const newPixel =
- pixel == ' ' ? '-' :
- pixel == '-' ? 'b' :
- pixel == 'b' ? 'w' :
- ' ';
- const line = this.game.map[y].split('');
- line[x] = newPixel;
- this.game.map[y] = line.join('');
- this.updateSettings('map');
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.urbixznjwwuukfsckrwzwsqzsxornqij {
- text-align: center;
- background: var(--bg);
-
- > header {
- padding: 8px;
- border-bottom: dashed 1px #c4cdd4;
- }
-
- > div {
- padding: 0 16px;
-
- > .card {
- margin: 0 auto 16px auto;
-
- &.map {
- > header {
- > select {
- width: 100%;
- padding: 12px 14px;
- background: var(--face);
- border: 1px solid var(--inputBorder);
- border-radius: 4px;
- color: var(--fg);
- cursor: pointer;
- transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-
- &:focus-visible,
- &:active {
- border-color: var(--accent);
- }
- }
- }
-
- > div {
- > .random {
- padding: 32px 0;
- font-size: 64px;
- color: var(--fg);
- opacity: 0.7;
- }
-
- > .board {
- display: grid;
- grid-gap: 4px;
- width: 300px;
- height: 300px;
- margin: 0 auto;
- color: var(--fg);
-
- > div {
- background: transparent;
- border: solid 2px var(--divider);
- border-radius: 6px;
- overflow: hidden;
- cursor: pointer;
-
- * {
- pointer-events: none;
- user-select: none;
- width: 100%;
- height: 100%;
- }
-
- &.none {
- border-color: transparent;
- }
- }
- }
- }
- }
-
- &.form {
- > div {
- > .card + .card {
- margin-top: 16px;
- }
-
- input[type='range'] {
- width: 100%;
- }
- }
- }
- }
-
- .card {
- max-width: 400px;
-
- > header {
- padding: 18px 20px;
- border-bottom: 1px solid var(--divider);
- }
-
- > div {
- padding: 20px;
- color: var(--fg);
- }
- }
- }
-
- > footer {
- position: sticky;
- bottom: 0;
- padding: 16px;
- border-top: solid 1px var(--divider);
-
- > .status {
- margin: 0 0 16px 0;
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/reversi/game.vue b/packages/client/src/pages/reversi/game.vue
deleted file mode 100644
index b1ed632904..0000000000
--- a/packages/client/src/pages/reversi/game.vue
+++ /dev/null
@@ -1,76 +0,0 @@
-<template>
-<div v-if="game == null"><MkLoading/></div>
-<GameSetting v-else-if="!game.isStarted" :init-game="game" :connection="connection"/>
-<GameBoard v-else :init-game="game" :connection="connection"/>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import GameSetting from './game.setting.vue';
-import GameBoard from './game.board.vue';
-import * as os from '@/os';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- GameSetting,
- GameBoard,
- },
-
- props: {
- gameId: {
- type: String,
- required: true
- },
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts._reversi.reversi,
- icon: 'fas fa-gamepad'
- },
- game: null,
- connection: null,
- };
- },
-
- watch: {
- gameId() {
- this.fetch();
- }
- },
-
- mounted() {
- this.fetch();
- },
-
- beforeUnmount() {
- if (this.connection) {
- this.connection.dispose();
- }
- },
-
- methods: {
- fetch() {
- os.api('games/reversi/games/show', {
- gameId: this.gameId
- }).then(game => {
- this.game = game;
-
- if (this.connection) {
- this.connection.dispose();
- }
- this.connection = markRaw(os.stream.useChannel('gamesReversiGame', {
- gameId: this.game.id
- }));
- this.connection.on('started', this.onStarted);
- });
- },
-
- onStarted(game) {
- Object.assign(this.game, game);
- },
- }
-});
-</script>
diff --git a/packages/client/src/pages/reversi/index.vue b/packages/client/src/pages/reversi/index.vue
deleted file mode 100644
index 0b118531fc..0000000000
--- a/packages/client/src/pages/reversi/index.vue
+++ /dev/null
@@ -1,279 +0,0 @@
-<template>
-<div v-if="!matching" class="bgvwxkhb">
- <h1>Misskey {{ $ts._reversi.reversi }}</h1>
-
- <div class="play">
- <MkButton primary round style="margin: var(--margin) auto 0 auto;" @click="match">{{ $ts.invite }}</MkButton>
- </div>
-
- <div class="_section">
- <MkFolder v-if="invitations.length > 0">
- <template #header>{{ $ts.invitations }}</template>
- <div class="nfcacttm">
- <button v-for="invitation in invitations" class="invitation _panel _button" tabindex="-1" @click="accept(invitation)">
- <MkAvatar class="avatar" :user="invitation.parent" :show-indicator="true"/>
- <span class="name"><b><MkUserName :user="invitation.parent"/></b></span>
- <span class="username">@{{ invitation.parent.username }}</span>
- <MkTime :time="invitation.createdAt" class="time"/>
- </button>
- </div>
- </MkFolder>
-
- <MkFolder v-if="myGames.length > 0">
- <template #header>{{ $ts._reversi.myGames }}</template>
- <div class="knextgwz">
- <MkA v-for="g in myGames" :key="g.id" class="game _panel" tabindex="-1" :to="`/games/reversi/${g.id}`">
- <div class="players">
- <MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
- </div>
- <footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
- </MkA>
- </div>
- </MkFolder>
-
- <MkFolder v-if="games.length > 0">
- <template #header>{{ $ts._reversi.allGames }}</template>
- <div class="knextgwz">
- <MkA v-for="g in games" :key="g.id" class="game _panel" tabindex="-1" :to="`/games/reversi/${g.id}`">
- <div class="players">
- <MkAvatar class="avatar" :user="g.user1"/><b><MkUserName :user="g.user1"/></b> vs <b><MkUserName :user="g.user2"/></b><MkAvatar class="avatar" :user="g.user2"/>
- </div>
- <footer><span class="state" :class="{ playing: !g.isEnded }">{{ g.isEnded ? $ts._reversi.ended : $ts._reversi.playing }}</span><MkTime class="time" :time="g.createdAt"/></footer>
- </MkA>
- </div>
- </MkFolder>
- </div>
-</div>
-<div v-else class="sazhgisb">
- <h1>
- <I18n :src="$ts.waitingFor" tag="span">
- <template #x>
- <b><MkUserName :user="matching"/></b>
- </template>
- </I18n>
- <MkEllipsis/>
- </h1>
- <div class="cancel">
- <MkButton inline round @click="cancel">{{ $ts.cancel }}</MkButton>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import * as os from '@/os';
-import MkButton from '@/components/ui/button.vue';
-import MkFolder from '@/components/ui/folder.vue';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- MkButton, MkFolder,
- },
-
- inject: ['navHook'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts._reversi.reversi,
- icon: 'fas fa-gamepad'
- },
- games: [],
- gamesFetching: true,
- gamesMoreFetching: false,
- myGames: [],
- matching: null,
- invitations: [],
- connection: null,
- pingClock: null,
- };
- },
-
- mounted() {
- if (this.$i) {
- this.connection = markRaw(os.stream.useChannel('gamesReversi'));
-
- this.connection.on('invited', this.onInvited);
-
- this.connection.on('matched', this.onMatched);
-
- this.pingClock = setInterval(() => {
- if (this.matching) {
- this.connection.send('ping', {
- id: this.matching.id
- });
- }
- }, 3000);
-
- os.api('games/reversi/games', {
- my: true
- }).then(games => {
- this.myGames = games;
- });
-
- os.api('games/reversi/invitations').then(invitations => {
- this.invitations = this.invitations.concat(invitations);
- });
- }
-
- os.api('games/reversi/games').then(games => {
- this.games = games;
- this.gamesFetching = false;
- });
- },
-
- beforeUnmount() {
- if (this.connection) {
- this.connection.dispose();
- clearInterval(this.pingClock);
- }
- },
-
- methods: {
- go(game) {
- const url = '/games/reversi/' + game.id;
- if (this.navHook) {
- this.navHook(url);
- } else {
- this.$router.push(url);
- }
- },
-
- async match() {
- const user = await os.selectUser({ local: true });
- if (user == null) return;
- os.api('games/reversi/match', {
- userId: user.id
- }).then(res => {
- if (res == null) {
- this.matching = user;
- } else {
- this.go(res);
- }
- });
- },
-
- cancel() {
- this.matching = null;
- os.api('games/reversi/match/cancel');
- },
-
- accept(invitation) {
- os.api('games/reversi/match', {
- userId: invitation.parent.id
- }).then(game => {
- if (game) {
- this.go(game);
- }
- });
- },
-
- onMatched(game) {
- this.go(game);
- },
-
- onInvited(invite) {
- this.invitations.unshift(invite);
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.bgvwxkhb {
- > h1 {
- margin: 0;
- padding: 24px;
- text-align: center;
- font-size: 1.5em;
- background: linear-gradient(0deg, #43c583, #438881);
- color: #fff;
- }
-
- > .play {
- text-align: center;
- }
-}
-
-.sazhgisb {
- text-align: center;
-}
-
-.nfcacttm {
- > .invitation {
- display: flex;
- box-sizing: border-box;
- width: 100%;
- padding: 16px;
- line-height: 32px;
- text-align: left;
-
- > .avatar {
- width: 32px;
- height: 32px;
- margin-right: 8px;
- }
-
- > .name {
- margin-right: 8px;
- }
-
- > .username {
- margin-right: 8px;
- opacity: 0.7;
- }
-
- > .time {
- margin-left: auto;
- opacity: 0.7;
- }
- }
-}
-
-.knextgwz {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- grid-gap: var(--margin);
-
- > .game {
- > .players {
- text-align: center;
- padding: 16px;
- line-height: 32px;
-
- > .avatar {
- width: 32px;
- height: 32px;
-
- &:first-child {
- margin-right: 8px;
- }
-
- &:last-child {
- margin-left: 8px;
- }
- }
- }
-
- > footer {
- display: flex;
- align-items: baseline;
- border-top: solid 0.5px var(--divider);
- padding: 6px 8px;
- font-size: 0.9em;
-
- > .state {
- &.playing {
- color: var(--accent);
- }
- }
-
- > .time {
- margin-left: auto;
- opacity: 0.7;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/pages/room/preview.vue b/packages/client/src/pages/room/preview.vue
deleted file mode 100644
index b0e600d4fb..0000000000
--- a/packages/client/src/pages/room/preview.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<template>
-<canvas width="224" height="128"></canvas>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as THREE from 'three';
-import * as os from '@/os';
-
-export default defineComponent({
- data() {
- return {
- selected: null,
- objectHeight: 0,
- orbitRadius: 5
- };
- },
-
- mounted() {
- const canvas = this.$el;
-
- const width = canvas.width;
- const height = canvas.height;
-
- const scene = new THREE.Scene();
-
- const renderer = new THREE.WebGLRenderer({
- canvas: canvas,
- antialias: true,
- alpha: false
- });
- renderer.setPixelRatio(window.devicePixelRatio);
- renderer.setSize(width, height);
- renderer.setClearColor(0x000000);
- renderer.autoClear = false;
- renderer.shadowMap.enabled = true;
- renderer.shadowMap.cullFace = THREE.CullFaceBack;
-
- const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
- camera.zoom = 10;
- camera.position.x = 0;
- camera.position.y = 2;
- camera.position.z = 0;
- camera.updateProjectionMatrix();
- scene.add(camera);
-
- const ambientLight = new THREE.AmbientLight(0xffffff, 1);
- ambientLight.castShadow = false;
- scene.add(ambientLight);
-
- const light = new THREE.PointLight(0xffffff, 1, 100);
- light.position.set(3, 3, 3);
- scene.add(light);
-
- const grid = new THREE.GridHelper(5, 16, 0x444444, 0x222222);
- scene.add(grid);
-
- const render = () => {
- const timer = Date.now() * 0.0004;
- requestAnimationFrame(render);
-
- camera.position.y = Math.sin(Math.PI / 6) * this.orbitRadius; // Math.PI / 6 => 30deg
- camera.position.z = Math.cos(timer) * this.orbitRadius;
- camera.position.x = Math.sin(timer) * this.orbitRadius;
- camera.lookAt(new THREE.Vector3(0, this.objectHeight / 2, 0));
- renderer.render(scene, camera);
- };
-
- this.selected = selected => {
- const obj = selected.clone();
-
- // Remove current object
- const current = scene.getObjectByName('obj');
- if (current != null) {
- scene.remove(current);
- }
-
- // Add new object
- obj.name = 'obj';
- obj.position.x = 0;
- obj.position.y = 0;
- obj.position.z = 0;
- obj.rotation.x = 0;
- obj.rotation.y = 0;
- obj.rotation.z = 0;
- obj.traverse(child => {
- if (child instanceof THREE.Mesh) {
- child.material = child.material.clone();
- return child.material.emissive.setHex(0x000000);
- }
- });
- const objectBoundingBox = new THREE.Box3().setFromObject(obj);
- this.objectHeight = objectBoundingBox.max.y - objectBoundingBox.min.y;
-
- const objectWidth = objectBoundingBox.max.x - objectBoundingBox.min.x;
- const objectDepth = objectBoundingBox.max.z - objectBoundingBox.min.z;
-
- const horizontal = Math.hypot(objectWidth, objectDepth) / camera.aspect;
- this.orbitRadius = Math.max(horizontal, this.objectHeight) * camera.zoom * 0.625 / Math.tan(camera.fov * 0.5 * (Math.PI / 180));
-
- scene.add(obj);
- };
-
- render();
- },
-});
-</script>
diff --git a/packages/client/src/pages/room/room.vue b/packages/client/src/pages/room/room.vue
deleted file mode 100644
index eb85d39dc4..0000000000
--- a/packages/client/src/pages/room/room.vue
+++ /dev/null
@@ -1,279 +0,0 @@
-<template>
-<div class="hveuntkp">
- <div v-if="objectSelected" class="controller _section">
- <div class="_content">
- <p class="name">{{ selectedFurnitureName }}</p>
- <XPreview ref="preview"/>
- <template v-if="selectedFurnitureInfo.props">
- <div v-for="k in Object.keys(selectedFurnitureInfo.props)" :key="k">
- <p>{{ k }}</p>
- <template v-if="selectedFurnitureInfo.props[k] === 'image'">
- <MkButton @click="chooseImage(k, $event)">{{ $ts._rooms.chooseImage }}</MkButton>
- </template>
- <template v-else-if="selectedFurnitureInfo.props[k] === 'color'">
- <input type="color" :value="selectedFurnitureProps ? selectedFurnitureProps[k] : null" @change="updateColor(k, $event)"/>
- </template>
- </div>
- </template>
- </div>
- <div class="_content">
- <MkButton inline :primary="isTranslateMode" @click="translate()"><i class="fas fa-arrows-alt"></i> {{ $ts._rooms.translate }}</MkButton>
- <MkButton inline :primary="isRotateMode" @click="rotate()"><i class="fas fa-undo"></i> {{ $ts._rooms.rotate }}</MkButton>
- <MkButton v-if="isTranslateMode || isRotateMode" inline @click="exit()"><i class="fas fa-ban"></i> {{ $ts._rooms.exit }}</MkButton>
- </div>
- <div class="_content">
- <MkButton @click="remove()"><i class="fas fa-trash-alt"></i> {{ $ts._rooms.remove }}</MkButton>
- </div>
- </div>
-
- <div v-if="isMyRoom" class="menu _section">
- <div class="_content">
- <MkButton @click="add()"><i class="fas fa-box-open"></i> {{ $ts._rooms.addFurniture }}</MkButton>
- </div>
- <div class="_content">
- <MkSelect :model-value="roomType" @update:modelValue="updateRoomType($event)">
- <template #label>{{ $ts._rooms.roomType }}</template>
- <option value="default">{{ $ts._rooms._roomType.default }}</option>
- <option value="washitsu">{{ $ts._rooms._roomType.washitsu }}</option>
- </MkSelect>
- <label v-if="roomType === 'default'">
- <span>{{ $ts._rooms.carpetColor }}</span>
- <input type="color" :value="carpetColor" @change="updateCarpetColor($event)"/>
- </label>
- </div>
- <div class="_content">
- <MkButton inline :disabled="!changed" primary @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton>
- <MkButton inline @click="clear()"><i class="fas fa-broom"></i> {{ $ts._rooms.clear }}</MkButton>
- </div>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import { Room } from '@/scripts/room/room';
-import * as Acct from 'misskey-js/built/acct';
-import XPreview from './preview.vue';
-const storeItems = require('@/scripts/room/furnitures.json5');
-import { query as urlQuery } from '@/scripts/url';
-import MkButton from '@/components/ui/button.vue';
-import MkSelect from '@/components/form/select.vue';
-import { selectFile } from '@/scripts/select-file';
-import * as os from '@/os';
-import { ColdDeviceStorage } from '@/store';
-import * as symbols from '@/symbols';
-
-let room: Room;
-
-export default defineComponent({
- components: {
- XPreview,
- MkButton,
- MkSelect,
- },
-
- beforeRouteLeave(to, from, next) {
- if (this.changed) {
- os.confirm({
- type: 'warning',
- text: this.$ts.leaveConfirm,
- }).then(({ canceled }) => {
- if (canceled) {
- next(false);
- } else {
- next();
- }
- });
- } else {
- next();
- }
- },
-
- props: {
- acct: {
- type: String,
- required: true
- },
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: computed(() => this.user ? {
- title: this.$ts.room,
- avatar: this.user,
- } : null),
- user: null,
- objectSelected: false,
- selectedFurnitureName: null,
- selectedFurnitureInfo: null,
- selectedFurnitureProps: null,
- roomType: null,
- carpetColor: null,
- isTranslateMode: false,
- isRotateMode: false,
- isMyRoom: false,
- changed: false,
- };
- },
-
- async mounted() {
- window.addEventListener('beforeunload', this.beforeunload);
-
- this.user = await os.api('users/show', {
- ...Acct.parse(this.acct)
- });
-
- this.isMyRoom = this.$i && (this.$i.id === this.user.id);
-
- const roomInfo = await os.api('room/show', {
- userId: this.user.id
- });
-
- this.roomType = roomInfo.roomType;
- this.carpetColor = roomInfo.carpetColor;
-
- room = new Room(this.user, this.isMyRoom, roomInfo, this.$el, {
- graphicsQuality: ColdDeviceStorage.get('roomGraphicsQuality'),
- onChangeSelect: obj => {
- this.objectSelected = obj != null;
- if (obj) {
- const f = room.findFurnitureById(obj.name);
- this.selectedFurnitureName = this.$t('_rooms._furnitures.' + f.type);
- this.selectedFurnitureInfo = storeItems.find(x => x.id === f.type);
- this.selectedFurnitureProps = f.props
- ? JSON.parse(JSON.stringify(f.props)) // Disable reactivity
- : null;
- this.$nextTick(() => {
- this.$refs.preview.selected(obj);
- });
- }
- },
- useOrthographicCamera: ColdDeviceStorage.get('roomUseOrthographicCamera'),
- });
- },
-
- beforeUnmount() {
- room.destroy();
- window.removeEventListener('beforeunload', this.beforeunload);
- },
-
- methods: {
- beforeunload(e: BeforeUnloadEvent) {
- if (this.changed) {
- e.preventDefault();
- e.returnValue = '';
- }
- },
-
- async add() {
- const { canceled, result: id } = await os.select({
- title: this.$ts._rooms.addFurniture,
- items: storeItems.map(item => ({
- value: item.id, text: this.$t('_rooms._furnitures.' + item.id)
- }))
- });
- if (canceled) return;
- room.addFurniture(id);
- this.changed = true;
- },
-
- remove() {
- this.isTranslateMode = false;
- this.isRotateMode = false;
- room.removeFurniture();
- this.changed = true;
- },
-
- save() {
- os.api('room/update', {
- room: room.getRoomInfo()
- }).then(() => {
- this.changed = false;
- os.success();
- }).catch((e: any) => {
- os.alert({
- type: 'error',
- text: e.message
- });
- });
- },
-
- clear() {
- os.confirm({
- type: 'warning',
- text: this.$ts._rooms.clearConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
- room.removeAllFurnitures();
- this.changed = true;
- });
- },
-
- chooseImage(key, e) {
- selectFile(e.currentTarget || e.target, null).then(file => {
- room.updateProp(key, `/proxy/?${urlQuery({ url: file.thumbnailUrl })}`);
- this.$refs.preview.selected(room.getSelectedObject());
- this.changed = true;
- });
- },
-
- updateColor(key, ev) {
- room.updateProp(key, ev.target.value);
- this.$refs.preview.selected(room.getSelectedObject());
- this.changed = true;
- },
-
- updateCarpetColor(ev) {
- room.updateCarpetColor(ev.target.value);
- this.carpetColor = ev.target.value;
- this.changed = true;
- },
-
- updateRoomType(type) {
- room.changeRoomType(type);
- this.roomType = type;
- this.changed = true;
- },
-
- translate() {
- if (this.isTranslateMode) {
- this.exit();
- } else {
- this.isRotateMode = false;
- this.isTranslateMode = true;
- room.enterTransformMode('translate');
- }
- this.changed = true;
- },
-
- rotate() {
- if (this.isRotateMode) {
- this.exit();
- } else {
- this.isTranslateMode = false;
- this.isRotateMode = true;
- room.enterTransformMode('rotate');
- }
- this.changed = true;
- },
-
- exit() {
- this.isTranslateMode = false;
- this.isRotateMode = false;
- room.exitTransformMode();
- this.changed = true;
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.hveuntkp {
- position: relative;
- min-height: 500px;
-
- > ::v-deep(canvas) {
- display: block;
- }
-}
-</style>
diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue
index 85d19bb255..ce2b7035da 100644
--- a/packages/client/src/pages/search.vue
+++ b/packages/client/src/pages/search.vue
@@ -6,37 +6,31 @@
</div>
</template>
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- XNotes
- },
+const props = defineProps<{
+ query: string;
+ channel?: string;
+}>();
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: computed(() => this.$t('searchWith', { q: this.$route.query.q })),
- icon: 'fas fa-search',
- },
- pagination: {
- endpoint: 'notes/search',
- limit: 10,
- params: () => ({
- query: this.$route.query.q,
- channelId: this.$route.query.channel,
- })
- },
- };
- },
+const pagination = {
+ endpoint: 'notes/search' as const,
+ limit: 10,
+ params: computed(() => ({
+ query: props.query,
+ channelId: props.channel,
+ }))
+};
- watch: {
- $route() {
- (this.$refs.notes as any).reload();
- }
- },
+defineExpose({
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: i18n.t('searchWith', { q: props.query }),
+ icon: 'fas fa-search',
+ bg: 'var(--bg)',
+ })),
});
</script>
diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue
index cffd10a0ee..10599d99ff 100644
--- a/packages/client/src/pages/settings/2fa.vue
+++ b/packages/client/src/pages/settings/2fa.vue
@@ -71,9 +71,6 @@ import MkButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
import MkInput from '@/components/form/input.vue';
import MkSwitch from '@/components/form/switch.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
diff --git a/packages/client/src/pages/settings/account-info.vue b/packages/client/src/pages/settings/account-info.vue
index f3d5e2f2c3..c98ad056f6 100644
--- a/packages/client/src/pages/settings/account-info.vue
+++ b/packages/client/src/pages/settings/account-info.vue
@@ -1,144 +1,135 @@
<template>
-<FormBase>
- <FormKeyValueView>
+<div class="_formRoot">
+ <MkKeyValue>
<template #key>ID</template>
<template #value><span class="_monospace">{{ $i.id }}</span></template>
- </FormKeyValueView>
+ </MkKeyValue>
- <FormGroup>
- <FormKeyValueView>
+ <FormSection>
+ <MkKeyValue>
<template #key>{{ $ts.registeredDate }}</template>
<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
- </FormKeyValueView>
- </FormGroup>
+ </MkKeyValue>
+ </FormSection>
- <FormGroup v-if="stats">
+ <FormSection v-if="stats">
<template #label>{{ $ts.statistics }}</template>
- <FormKeyValueView>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.notesCount }}</template>
<template #value>{{ number(stats.notesCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.repliesCount }}</template>
<template #value>{{ number(stats.repliesCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.renotesCount }}</template>
<template #value>{{ number(stats.renotesCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.repliedCount }}</template>
<template #value>{{ number(stats.repliedCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.renotedCount }}</template>
<template #value>{{ number(stats.renotedCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.pollVotesCount }}</template>
<template #value>{{ number(stats.pollVotesCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.pollVotedCount }}</template>
<template #value>{{ number(stats.pollVotedCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.sentReactionsCount }}</template>
<template #value>{{ number(stats.sentReactionsCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.receivedReactionsCount }}</template>
<template #value>{{ number(stats.receivedReactionsCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.noteFavoritesCount }}</template>
<template #value>{{ number(stats.noteFavoritesCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followingCount }}</template>
<template #value>{{ number(stats.followingCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followingCount }} ({{ $ts.local }})</template>
<template #value>{{ number(stats.localFollowingCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followingCount }} ({{ $ts.remote }})</template>
<template #value>{{ number(stats.remoteFollowingCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followersCount }}</template>
<template #value>{{ number(stats.followersCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followersCount }} ({{ $ts.local }})</template>
<template #value>{{ number(stats.localFollowersCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.followersCount }} ({{ $ts.remote }})</template>
<template #value>{{ number(stats.remoteFollowersCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.pageLikesCount }}</template>
<template #value>{{ number(stats.pageLikesCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.pageLikedCount }}</template>
<template #value>{{ number(stats.pageLikedCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.driveFilesCount }}</template>
<template #value>{{ number(stats.driveFilesCount) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ $ts.driveUsage }}</template>
<template #value>{{ bytes(stats.driveUsage) }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>{{ $ts.reversiCount }}</template>
- <template #value>{{ number(stats.reversiCount) }}</template>
- </FormKeyValueView>
- </FormGroup>
+ </MkKeyValue>
+ </FormSection>
- <FormGroup>
+ <FormSection>
<template #label>{{ $ts.other }}</template>
- <FormKeyValueView>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>emailVerified</template>
<template #value>{{ $i.emailVerified ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>twoFactorEnabled</template>
<template #value>{{ $i.twoFactorEnabled ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>securityKeys</template>
<template #value>{{ $i.securityKeys ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>usePasswordLessLogin</template>
<template #value>{{ $i.usePasswordLessLogin ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>isModerator</template>
<template #value>{{ $i.isModerator ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
<template #key>isAdmin</template>
<template #value>{{ $i.isAdmin ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- </FormGroup>
-</FormBase>
+ </MkKeyValue>
+ </FormSection>
+</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
+import FormSection from '@/components/form/section.vue';
+import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
@@ -146,13 +137,8 @@ import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
- FormSelect,
- FormSwitch,
- FormButton,
- FormLink,
- FormGroup,
- FormKeyValueView,
+ FormSection,
+ MkKeyValue,
},
emits: ['info'],
@@ -168,8 +154,6 @@ export default defineComponent({
},
mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
-
os.api('users/stats', {
userId: this.$i.id
}).then(stats => {
diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue
index 2d1e0eff4e..c795ede8ac 100644
--- a/packages/client/src/pages/settings/accounts.vue
+++ b/packages/client/src/pages/settings/accounts.vue
@@ -1,41 +1,35 @@
<template>
-<FormBase>
+<div class="_formRoot">
<FormSuspense :p="init">
<FormButton primary @click="addAccount"><i class="fas fa-plus"></i> {{ $ts.addAccount }}</FormButton>
- <div v-for="account in accounts" :key="account.id" class="_debobigegoItem _button" @click="menu(account, $event)">
- <div class="_debobigegoPanel lcjjdxlm">
- <div class="avatar">
- <MkAvatar :user="account" class="avatar"/>
+ <div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
+ <div class="avatar">
+ <MkAvatar :user="account" class="avatar"/>
+ </div>
+ <div class="body">
+ <div class="name">
+ <MkUserName :user="account"/>
</div>
- <div class="body">
- <div class="name">
- <MkUserName :user="account"/>
- </div>
- <div class="acct">
- <MkAcct :user="account"/>
- </div>
+ <div class="acct">
+ <MkAcct :user="account"/>
</div>
</div>
</div>
</FormSuspense>
-</FormBase>
+</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
+import FormSuspense from '@/components/form/suspense.vue';
+import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { getAccounts, addAccount, login } from '@/account';
export default defineComponent({
components: {
- FormBase,
FormSuspense,
FormButton,
},
@@ -59,10 +53,6 @@ export default defineComponent({
};
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
menu(account, ev) {
os.popupMenu([{
diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue
index 30a4902a15..20ff2a8d96 100644
--- a/packages/client/src/pages/settings/api.vue
+++ b/packages/client/src/pages/settings/api.vue
@@ -1,25 +1,20 @@
<template>
-<FormBase>
- <FormButton primary @click="generateToken">{{ $ts.generateAccessToken }}</FormButton>
- <FormLink to="/settings/apps">{{ $ts.manageAccessTokens }}</FormLink>
- <FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink>
-</FormBase>
+<div class="_formRoot">
+ <FormButton primary class="_formBlock" @click="generateToken">{{ $ts.generateAccessToken }}</FormButton>
+ <FormLink to="/settings/apps" class="_formBlock">{{ $ts.manageAccessTokens }}</FormLink>
+ <FormLink to="/api-console" :behavior="isDesktop ? 'window' : null" class="_formBlock">API console</FormLink>
+</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
+import FormLink from '@/components/form/link.vue';
+import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
FormButton,
FormLink,
},
@@ -37,10 +32,6 @@ export default defineComponent({
};
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
generateToken() {
os.popup(import('@/components/token-generate-window.vue'), {}, {
diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue
index b5fe4e0aed..9c0fa8a54d 100644
--- a/packages/client/src/pages/settings/apps.vue
+++ b/packages/client/src/pages/settings/apps.vue
@@ -1,5 +1,5 @@
<template>
-<FormBase>
+<div class="_formRoot">
<FormPagination ref="list" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
@@ -8,7 +8,7 @@
</div>
</template>
<template v-slot="{items}">
- <div v-for="token in items" :key="token.id" class="_debobigegoPanel bfomjevm">
+ <div v-for="token in items" :key="token.id" class="_panel bfomjevm">
<img v-if="token.iconUrl" class="icon" :src="token.iconUrl" alt=""/>
<div class="body">
<div class="name">{{ token.name }}</div>
@@ -34,23 +34,17 @@
</div>
</template>
</FormPagination>
-</FormBase>
+</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormPagination from '@/components/debobigego/pagination.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
+import FormPagination from '@/components/ui/pagination.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
FormPagination,
},
@@ -64,7 +58,7 @@ export default defineComponent({
bg: 'var(--bg)',
},
pagination: {
- endpoint: 'i/apps',
+ endpoint: 'i/apps' as const,
limit: 100,
params: {
sort: '+lastUsedAt'
@@ -73,10 +67,6 @@ export default defineComponent({
};
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
revoke(token) {
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue
index 155956923c..556ee30c1d 100644
--- a/packages/client/src/pages/settings/custom-css.vue
+++ b/packages/client/src/pages/settings/custom-css.vue
@@ -1,25 +1,18 @@
<template>
-<FormBase>
- <FormInfo warn>{{ $ts.customCssWarn }}</FormInfo>
+<div class="_formRoot">
+ <FormInfo warn class="_formBlock">{{ $ts.customCssWarn }}</FormInfo>
- <FormTextarea v-model="localCustomCss" manual-save tall class="_monospace" style="tab-size: 2;">
- <span>{{ $ts.local }}</span>
+ <FormTextarea v-model="localCustomCss" manual-save tall class="_monospace _formBlock" style="tab-size: 2;">
+ <template #label>CSS</template>
</FormTextarea>
-</FormBase>
+</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormRadios from '@/components/form/radios.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormInfo from '@/components/debobigego/info.vue';
+import FormInfo from '@/components/ui/info.vue';
import * as os from '@/os';
-import { ColdDeviceStorage } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
import * as symbols from '@/symbols';
import { defaultStore } from '@/store';
@@ -27,12 +20,6 @@ import { defaultStore } from '@/store';
export default defineComponent({
components: {
FormTextarea,
- FormSelect,
- FormRadios,
- FormBase,
- FormGroup,
- FormLink,
- FormButton,
FormInfo,
},
@@ -50,8 +37,6 @@ export default defineComponent({
},
mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
-
this.$watch('localCustomCss', this.apply);
},
diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue
index bc82b0ca84..46b90d3d1a 100644
--- a/packages/client/src/pages/settings/deck.vue
+++ b/packages/client/src/pages/settings/deck.vue
@@ -1,42 +1,41 @@
<template>
-<FormBase>
+<div class="_formRoot">
<FormGroup>
<template #label>{{ $ts.defaultNavigationBehaviour }}</template>
<FormSwitch v-model="navWindow">{{ $ts.openInWindow }}</FormSwitch>
</FormGroup>
- <FormSwitch v-model="alwaysShowMainColumn">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
+ <FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ $ts._deck.alwaysShowMainColumn }}</FormSwitch>
- <FormRadios v-model="columnAlign">
- <template #desc>{{ $ts._deck.columnAlign }}</template>
+ <FormRadios v-model="columnAlign" class="_formBlock">
+ <template #label>{{ $ts._deck.columnAlign }}</template>
<option value="left">{{ $ts.left }}</option>
<option value="center">{{ $ts.center }}</option>
</FormRadios>
- <FormRadios v-model="columnHeaderHeight">
- <template #desc>{{ $ts._deck.columnHeaderHeight }}</template>
+ <FormRadios v-model="columnHeaderHeight" class="_formBlock">
+ <template #label>{{ $ts._deck.columnHeaderHeight }}</template>
<option :value="42">{{ $ts.narrow }}</option>
<option :value="45">{{ $ts.medium }}</option>
<option :value="48">{{ $ts.wide }}</option>
</FormRadios>
- <FormInput v-model="columnMargin" type="number">
- <span>{{ $ts._deck.columnMargin }}</span>
+ <FormInput v-model="columnMargin" type="number" class="_formBlock">
+ <template #label>{{ $ts._deck.columnMargin }}</template>
<template #suffix>px</template>
</FormInput>
- <FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
-</FormBase>
+ <FormLink class="_formBlock" @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
+</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormRadios from '@/components/debobigego/radios.vue';
-import FormInput from '@/components/debobigego/input.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormLink from '@/components/form/link.vue';
+import FormRadios from '@/components/form/radios.vue';
+import FormInput from '@/components/form/input.vue';
+import FormGroup from '@/components/form/group.vue';
import { deckStore } from '@/ui/deck/deck-store';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
@@ -48,7 +47,6 @@ export default defineComponent({
FormLink,
FormInput,
FormRadios,
- FormBase,
FormGroup,
},
@@ -85,10 +83,6 @@ export default defineComponent({
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async setProfile() {
const { canceled, result: name } = await os.inputText({
diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue
index 6ce8d6509c..7edc81a309 100644
--- a/packages/client/src/pages/settings/delete-account.vue
+++ b/packages/client/src/pages/settings/delete-account.vue
@@ -1,28 +1,23 @@
<template>
-<FormBase>
- <FormInfo warn>{{ $ts._accountDelete.mayTakeTime }}</FormInfo>
- <FormInfo>{{ $ts._accountDelete.sendEmail }}</FormInfo>
- <FormButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ $ts._accountDelete.requestAccountDelete }}</FormButton>
+<div class="_formRoot">
+ <FormInfo warn class="_formBlock">{{ $ts._accountDelete.mayTakeTime }}</FormInfo>
+ <FormInfo class="_formBlock">{{ $ts._accountDelete.sendEmail }}</FormInfo>
+ <FormButton v-if="!$i.isDeleted" danger class="_formBlock" @click="deleteAccount">{{ $ts._accountDelete.requestAccountDelete }}</FormButton>
<FormButton v-else disabled>{{ $ts._accountDelete.inProgress }}</FormButton>
-</FormBase>
+</div>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
+import FormInfo from '@/components/ui/info.vue';
+import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
-import { debug } from '@/config';
import { signout } from '@/account';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
FormButton,
- FormGroup,
FormInfo,
},
@@ -35,14 +30,9 @@ export default defineComponent({
icon: 'fas fa-exclamation-triangle',
bg: 'var(--bg)',
},
- debug,
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async deleteAccount() {
{
diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue
index 9ab99c6efe..f1016ebd84 100644
--- a/packages/client/src/pages/settings/drive.vue
+++ b/packages/client/src/pages/settings/drive.vue
@@ -5,7 +5,7 @@
<div class="_formBlock uawsfosz">
<div class="meter"><div :style="meterStyle"></div></div>
</div>
- <div class="_inputSplit _formBlock">
+ <FormSplit>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.capacity }}</template>
<template #value>{{ bytes(capacity, 1) }}</template>
@@ -14,7 +14,7 @@
<template #key>{{ $ts.inUse }}</template>
<template #value>{{ bytes(usage, 1) }}</template>
</MkKeyValue>
- </div>
+ </FormSplit>
</FormSection>
<FormSection>
@@ -38,6 +38,7 @@ import * as tinycolor from 'tinycolor2';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/key-value.vue';
+import FormSplit from '@/components/form/split.vue';
import * as os from '@/os';
import bytes from '@/filters/bytes';
import * as symbols from '@/symbols';
@@ -49,6 +50,7 @@ export default defineComponent({
FormLink,
FormSection,
MkKeyValue,
+ FormSplit,
},
emits: ['info'],
@@ -97,10 +99,6 @@ export default defineComponent({
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
chooseUploadFolder() {
os.selectDriveFolder(false).then(async folder => {
diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue
index b04295cce0..54557f8773 100644
--- a/packages/client/src/pages/settings/email.vue
+++ b/packages/client/src/pages/settings/email.vue
@@ -41,8 +41,6 @@
<script lang="ts">
import { defineComponent, onMounted, ref, watch } from 'vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormLink from '@/components/debobigego/link.vue';
import FormSection from '@/components/form/section.vue';
import FormInput from '@/components/form/input.vue';
import FormSwitch from '@/components/form/switch.vue';
@@ -54,8 +52,6 @@ import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSection,
- FormLink,
- FormButton,
FormSwitch,
FormInput,
},
@@ -115,8 +111,6 @@ export default defineComponent({
});
onMounted(() => {
- context.emit('info', INFO);
-
watch(emailAddress, () => {
saveEmailAddress();
});
diff --git a/packages/client/src/pages/settings/experimental-features.vue b/packages/client/src/pages/settings/experimental-features.vue
deleted file mode 100644
index 5a7bcb3b41..0000000000
--- a/packages/client/src/pages/settings/experimental-features.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<template>
-<FormBase>
- <FormButton @click="error()">error test</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import * as os from '@/os';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- FormBase,
- FormSelect,
- FormSwitch,
- FormButton,
- FormLink,
- FormGroup,
- FormKeyValueView,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.experimentalFeatures,
- icon: 'fas fa-flask'
- },
- stats: null
- }
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- error() {
- throw new Error('Test error');
- }
- }
-});
-</script>
diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue
index 734bc78442..2e159e56a9 100644
--- a/packages/client/src/pages/settings/general.vue
+++ b/packages/client/src/pages/settings/general.vue
@@ -195,10 +195,6 @@ export default defineComponent({
},
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async reloadAsk() {
const { canceled } = await os.confirm({
diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue
index a1dd6a1539..21031c559e 100644
--- a/packages/client/src/pages/settings/import-export.vue
+++ b/packages/client/src/pages/settings/import-export.vue
@@ -133,10 +133,6 @@ export default defineComponent({
os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
- onMounted(() => {
- context.emit('info', INFO);
- });
-
return {
[symbols.PAGE_INFO]: INFO,
excludeMutingUsers,
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 8ffff86705..66c8b147bb 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -14,7 +14,7 @@
</div>
<div class="main">
<div class="bkzroven">
- <component :is="component" :key="page" v-bind="pageProps" @info="onInfo"/>
+ <component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
</div>
</div>
</div>
@@ -215,19 +215,9 @@ export default defineComponent({
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
- case 'plugin/manage': return defineAsyncComponent(() => import('./plugin.manage.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
- case 'update': return defineAsyncComponent(() => import('./update.vue'));
- case 'registry': return defineAsyncComponent(() => import('./registry.vue'));
case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue'));
- case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
- }
- if (page.value.startsWith('registry/keys/system/')) {
- return defineAsyncComponent(() => import('./registry.keys.vue'));
- }
- if (page.value.startsWith('registry/value/system/')) {
- return defineAsyncComponent(() => import('./registry.value.vue'));
}
return null;
});
@@ -235,17 +225,6 @@ export default defineComponent({
watch(component, () => {
pageProps.value = {};
- if (page.value) {
- if (page.value.startsWith('registry/keys/system/')) {
- pageProps.value.scope = page.value.replace('registry/keys/system/', '').split('/');
- }
- if (page.value.startsWith('registry/value/system/')) {
- const path = page.value.replace('registry/value/system/', '').split('/');
- pageProps.value.xKey = path.pop();
- pageProps.value.scope = path;
- }
- }
-
nextTick(() => {
scroll(el.value, { top: 0 });
});
@@ -271,8 +250,9 @@ export default defineComponent({
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
- const onInfo = (info) => {
- childInfo.value = info;
+ const pageChanged = (page) => {
+ if (page == null) return;
+ childInfo.value = page[symbols.PAGE_INFO];
};
return {
@@ -285,7 +265,7 @@ export default defineComponent({
pageProps,
component,
emailNotConfigured,
- onInfo,
+ pageChanged,
childInfo,
};
},
diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue
index 584a21e4bd..f84a209b60 100644
--- a/packages/client/src/pages/settings/instance-mute.vue
+++ b/packages/client/src/pages/settings/instance-mute.vue
@@ -47,11 +47,6 @@ export default defineComponent({
},
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
-
async created() {
this.instanceMutes = this.$i.mutedInstances.join('\n');
},
diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue
index 3d8aaf8a6f..ca36c91665 100644
--- a/packages/client/src/pages/settings/integration.vue
+++ b/packages/client/src/pages/settings/integration.vue
@@ -1,45 +1,39 @@
<template>
-<FormBase>
- <div v-if="enableTwitterIntegration" class="_debobigegoItem">
- <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" danger @click="disconnectTwitter">{{ $ts.disconnectService }}</MkButton>
- <MkButton v-else primary @click="connectTwitter">{{ $ts.connectService }}</MkButton>
- </div>
- </div>
+<div class="_formRoot">
+ <FormSection v-if="enableTwitterIntegration">
+ <template #label><i class="fab fa-twitter"></i> Twitter</template>
+ <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" danger @click="disconnectTwitter">{{ $ts.disconnectService }}</MkButton>
+ <MkButton v-else primary @click="connectTwitter">{{ $ts.connectService }}</MkButton>
+ </FormSection>
- <div v-if="enableDiscordIntegration" class="_debobigegoItem">
- <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" danger @click="disconnectDiscord">{{ $ts.disconnectService }}</MkButton>
- <MkButton v-else primary @click="connectDiscord">{{ $ts.connectService }}</MkButton>
- </div>
- </div>
+ <FormSection v-if="enableDiscordIntegration">
+ <template #label><i class="fab fa-discord"></i> Discord</template>
+ <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" danger @click="disconnectDiscord">{{ $ts.disconnectService }}</MkButton>
+ <MkButton v-else primary @click="connectDiscord">{{ $ts.connectService }}</MkButton>
+ </FormSection>
- <div v-if="enableGithubIntegration" class="_debobigegoItem">
- <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" danger @click="disconnectGithub">{{ $ts.disconnectService }}</MkButton>
- <MkButton v-else primary @click="connectGithub">{{ $ts.connectService }}</MkButton>
- </div>
- </div>
-</FormBase>
+ <FormSection v-if="enableGithubIntegration">
+ <template #label><i class="fab fa-github"></i> GitHub</template>
+ <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" danger @click="disconnectGithub">{{ $ts.disconnectService }}</MkButton>
+ <MkButton v-else primary @click="connectGithub">{{ $ts.connectService }}</MkButton>
+ </FormSection>
+</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { apiUrl } from '@/config';
-import FormBase from '@/components/debobigego/base.vue';
+import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
+ FormSection,
MkButton
},
@@ -79,8 +73,6 @@ export default defineComponent({
},
mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
-
document.cookie = `igi=${this.$i.token}; path=/;` +
` max-age=31536000;` +
(document.location.protocol.startsWith('https') ? ' secure' : '');
diff --git a/packages/client/src/pages/settings/menu.vue b/packages/client/src/pages/settings/menu.vue
index 19d26be89a..6e38cd5dfe 100644
--- a/packages/client/src/pages/settings/menu.vue
+++ b/packages/client/src/pages/settings/menu.vue
@@ -21,7 +21,6 @@
import { defineComponent } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
-import FormBase from '@/components/debobigego/base.vue';
import FormButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { menuDef } from '@/menu';
@@ -31,7 +30,6 @@ import { unisonReload } from '@/scripts/unison-reload';
export default defineComponent({
components: {
- FormBase,
FormButton,
FormTextarea,
FormRadios,
@@ -69,10 +67,6 @@ export default defineComponent({
},
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async addItem() {
const menu = Object.keys(this.menuDef).filter(k => !this.$store.state.menu.includes(k));
diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue
index 4f42d5e429..f4f9ebf8dd 100644
--- a/packages/client/src/pages/settings/mute-block.vue
+++ b/packages/client/src/pages/settings/mute-block.vue
@@ -1,5 +1,5 @@
<template>
-<FormBase>
+<div class="_formRoot">
<MkTab v-model="tab" style="margin-bottom: var(--margin);">
<option value="mute">{{ $ts.mutedUsers }}</option>
<option value="block">{{ $ts.blockedUsers }}</option>
@@ -8,11 +8,9 @@
<MkPagination :pagination="mutingPagination" class="muting">
<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
<template v-slot="{items}">
- <FormGroup>
- <FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
- <MkAcct :user="mute.mutee"/>
- </FormLink>
- </FormGroup>
+ <FormLink v-for="mute in items" :key="mute.id" :to="userPage(mute.mutee)">
+ <MkAcct :user="mute.mutee"/>
+ </FormLink>
</template>
</MkPagination>
</div>
@@ -20,66 +18,43 @@
<MkPagination :pagination="blockingPagination" class="blocking">
<template #empty><FormInfo>{{ $ts.noUsers }}</FormInfo></template>
<template v-slot="{items}">
- <FormGroup>
- <FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
- <MkAcct :user="block.blockee"/>
- </FormLink>
- </FormGroup>
+ <FormLink v-for="block in items" :key="block.id" :to="userPage(block.blockee)">
+ <MkAcct :user="block.blockee"/>
+ </FormLink>
</template>
</MkPagination>
</div>
-</FormBase>
+</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkTab from '@/components/tab.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
+import FormInfo from '@/components/ui/info.vue';
+import FormLink from '@/components/form/link.vue';
import { userPage } from '@/filters/user';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- MkPagination,
- MkTab,
- FormInfo,
- FormBase,
- FormGroup,
- FormLink,
- },
+let tab = $ref('mute');
- emits: ['info'],
+const mutingPagination = {
+ endpoint: 'mute/list' as const,
+ limit: 10,
+};
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.muteAndBlock,
- icon: 'fas fa-ban',
- bg: 'var(--bg)',
- },
- tab: 'mute',
- mutingPagination: {
- endpoint: 'mute/list',
- limit: 10,
- },
- blockingPagination: {
- endpoint: 'blocking/list',
- limit: 10,
- },
- }
- },
+const blockingPagination = {
+ endpoint: 'blocking/list' as const,
+ limit: 10,
+};
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.muteAndBlock,
+ icon: 'fas fa-ban',
+ bg: 'var(--bg)',
},
-
- methods: {
- userPage
- }
});
</script>
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
index d3ada0d7ef..12171530bb 100644
--- a/packages/client/src/pages/settings/notifications.vue
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -13,7 +13,6 @@
import { defineComponent } from 'vue';
import FormButton from '@/components/ui/button.vue';
import FormLink from '@/components/form/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
import FormSection from '@/components/form/section.vue';
import { notificationTypes } from 'misskey-js';
import * as os from '@/os';
@@ -21,7 +20,6 @@ import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
FormLink,
FormButton,
FormSection,
@@ -39,10 +37,6 @@ export default defineComponent({
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
readAllUnreadNotes() {
os.api('i/read-all-unread-notes');
diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue
index 0d9e60e21d..6e48cb58a6 100644
--- a/packages/client/src/pages/settings/other.vue
+++ b/packages/client/src/pages/settings/other.vue
@@ -1,30 +1,12 @@
<template>
<div class="_formRoot">
- <FormLink to="/settings/update" class="_formBlock">Misskey Update</FormLink>
-
- <FormSwitch :value="$i.injectFeaturedNote" @update:modelValue="onChangeInjectFeaturedNote" class="_formBlock">
+ <FormSwitch :value="$i.injectFeaturedNote" class="_formBlock" @update:modelValue="onChangeInjectFeaturedNote">
{{ $ts.showFeaturedNotesInTimeline }}
</FormSwitch>
- <FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #desc>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
+ <FormSwitch v-model="reportError" class="_formBlock">{{ $ts.sendErrorReports }}<template #caption>{{ $ts.sendErrorReportsDescription }}</template></FormSwitch>
<FormLink to="/settings/account-info" class="_formBlock">{{ $ts.accountInfo }}</FormLink>
- <FormLink to="/settings/experimental-features" class="_formBlock">{{ $ts.experimentalFeatures }}</FormLink>
-
- <FormSection>
- <template #label>{{ $ts.developer }}</template>
- <FormSwitch v-model="debug" @update:modelValue="changeDebug" class="_formBlock">
- DEBUG MODE
- </FormSwitch>
- <template v-if="debug">
- <FormButton @click="taskmanager">Task Manager</FormButton>
- </template>
- </FormSection>
-
- <FormLink to="/settings/registry" class="_formBlock"><template #icon><i class="fas fa-cogs"></i></template>{{ $ts.registry }}</FormLink>
-
- <FormLink to="/bios" behavior="browser" class="_formBlock"><template #icon><i class="fas fa-door-open"></i></template>BIOS</FormLink>
- <FormLink to="/cli" behavior="browser" class="_formBlock"><template #icon><i class="fas fa-door-open"></i></template>CLI</FormLink>
<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ $ts.closeAccount }}</FormLink>
</div>
@@ -33,10 +15,8 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
import FormSection from '@/components/form/section.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormButton from '@/components/debobigego/button.vue';
+import FormLink from '@/components/form/link.vue';
import * as os from '@/os';
import { debug } from '@/config';
import { defaultStore } from '@/store';
@@ -45,10 +25,8 @@ import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormSelect,
FormSection,
FormSwitch,
- FormButton,
FormLink,
},
@@ -69,10 +47,6 @@ export default defineComponent({
reportError: defaultStore.makeGetterSetter('reportError'),
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
changeDebug(v) {
console.log(v);
@@ -85,11 +59,6 @@ export default defineComponent({
injectFeaturedNote: v
});
},
-
- taskmanager() {
- os.popup(import('@/components/taskmanager.vue'), {
- }, {}, 'closed');
- },
}
});
</script>
diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue
index af93ef2930..d35d20d17a 100644
--- a/packages/client/src/pages/settings/plugin.install.vue
+++ b/packages/client/src/pages/settings/plugin.install.vue
@@ -1,15 +1,15 @@
<template>
-<FormBase>
- <FormInfo warn>{{ $ts._plugin.installWarn }}</FormInfo>
+<div class="_formRoot">
+ <FormInfo warn class="_formBlock">{{ $ts._plugin.installWarn }}</FormInfo>
- <FormGroup>
- <FormTextarea v-model="code" tall>
- <span>{{ $ts.code }}</span>
- </FormTextarea>
- </FormGroup>
+ <FormTextarea v-model="code" tall class="_formBlock">
+ <template #label>{{ $ts.code }}</template>
+ </FormTextarea>
- <FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
-</FormBase>
+ <div class="_formBlock">
+ <FormButton :disabled="code == null" primary inline @click="install"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
+ </div>
+</div>
</template>
<script lang="ts">
@@ -18,13 +18,8 @@ import { AiScript, parse } from '@syuilo/aiscript';
import { serialize } from '@syuilo/aiscript/built/serializer';
import { v4 as uuid } from 'uuid';
import FormTextarea from '@/components/form/textarea.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormRadios from '@/components/form/radios.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormInfo from '@/components/debobigego/info.vue';
+import FormButton from '@/components/ui/button.vue';
+import FormInfo from '@/components/ui/info.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
import { unisonReload } from '@/scripts/unison-reload';
@@ -33,11 +28,6 @@ import * as symbols from '@/symbols';
export default defineComponent({
components: {
FormTextarea,
- FormSelect,
- FormRadios,
- FormBase,
- FormGroup,
- FormLink,
FormButton,
FormInfo,
},
@@ -55,10 +45,6 @@ export default defineComponent({
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
installPlugin({ id, meta, ast, token }) {
ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
diff --git a/packages/client/src/pages/settings/plugin.manage.vue b/packages/client/src/pages/settings/plugin.manage.vue
deleted file mode 100644
index 8b9021dc3d..0000000000
--- a/packages/client/src/pages/settings/plugin.manage.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<template>
-<FormBase>
- <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 :modelValue="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>
- </div>
- <div class="_keyValue">
- <div>{{ $ts.description }}:</div>
- <div>{{ plugin.description }}</div>
- </div>
- <div class="_keyValue">
- <div>{{ $ts.permission }}:</div>
- <div>{{ plugin.permissions }}</div>
- </div>
- </div>
- </div>
- <div class="_debobigegoItem">
- <div class="_debobigegoPanel" style="padding: 16px;">
- <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton>
- <MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton>
- </div>
- </div>
- </FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkButton from '@/components/ui/button.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import MkSelect from '@/components/form/select.vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import * as os from '@/os';
-import { ColdDeviceStorage } from '@/store';
-import * as symbols from '@/symbols';
-import { unisonReload } from '@/scripts/unison-reload';
-
-export default defineComponent({
- components: {
- MkButton,
- MkTextarea,
- MkSelect,
- FormSwitch,
- FormBase,
- FormGroup,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts._plugin.manage,
- icon: 'fas fa-plug',
- bg: 'var(--bg)',
- },
- plugins: ColdDeviceStorage.get('plugins'),
- }
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- uninstall(plugin) {
- ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
- os.success();
- this.$nextTick(() => {
- unisonReload();
- });
- },
-
- // TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
- async config(plugin) {
- const config = plugin.config;
- for (const key in plugin.configData) {
- config[key].default = plugin.configData[key];
- }
-
- const { canceled, result } = await os.form(plugin.name, config);
- if (canceled) return;
-
- const plugins = ColdDeviceStorage.get('plugins');
- plugins.find(p => p.id === plugin.id).configData = result;
- ColdDeviceStorage.set('plugins', plugins);
-
- this.$nextTick(() => {
- location.reload();
- });
- },
-
- changeActive(plugin, active) {
- const plugins = ColdDeviceStorage.get('plugins');
- plugins.find(p => p.id === plugin.id).active = active;
- ColdDeviceStorage.set('plugins', plugins);
-
- this.$nextTick(() => {
- location.reload();
- });
- }
- },
-});
-</script>
-
-<style lang="scss" scoped>
-
-</style>
diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue
index 50e53f459f..7a3ab9d152 100644
--- a/packages/client/src/pages/settings/plugin.vue
+++ b/packages/client/src/pages/settings/plugin.vue
@@ -1,23 +1,54 @@
<template>
-<FormBase>
+<div class="_formRoot">
<FormLink to="/settings/plugin/install"><template #icon><i class="fas fa-download"></i></template>{{ $ts._plugin.install }}</FormLink>
- <FormLink to="/settings/plugin/manage"><template #icon><i class="fas fa-folder-open"></i></template>{{ $ts._plugin.manage }}<template #suffix>{{ plugins }}</template></FormLink>
-</FormBase>
+
+ <FormSection>
+ <template #label>{{ $ts.manage }}</template>
+ <div v-for="plugin in plugins" :key="plugin.id" class="_formBlock _panel" style="padding: 20px;">
+ <span style="display: flex;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
+
+ <FormSwitch class="_formBlock" :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ $ts.makeActive }}</FormSwitch>
+
+ <MkKeyValue class="_formBlock">
+ <template #key>{{ $ts.author }}</template>
+ <template #value>{{ plugin.author }}</template>
+ </MkKeyValue>
+ <MkKeyValue class="_formBlock">
+ <template #key>{{ $ts.description }}</template>
+ <template #value>{{ plugin.description }}</template>
+ </MkKeyValue>
+ <MkKeyValue class="_formBlock">
+ <template #key>{{ $ts.permission }}</template>
+ <template #value>{{ plugin.permission }}</template>
+ </MkKeyValue>
+
+ <div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="fas fa-cog"></i> {{ $ts.settings }}</MkButton>
+ <MkButton inline danger @click="uninstall(plugin)"><i class="fas fa-trash-alt"></i> {{ $ts.uninstall }}</MkButton>
+ </div>
+ </div>
+ </FormSection>
+</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormLink from '@/components/debobigego/link.vue';
+import FormLink from '@/components/form/link.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSection from '@/components/form/section.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkKeyValue from '@/components/key-value.vue';
import * as os from '@/os';
import { ColdDeviceStorage } from '@/store';
import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
FormLink,
+ FormSwitch,
+ FormSection,
+ MkButton,
+ MkKeyValue,
},
emits: ['info'],
@@ -29,12 +60,47 @@ export default defineComponent({
icon: 'fas fa-plug',
bg: 'var(--bg)',
},
- plugins: ColdDeviceStorage.get('plugins').length,
+ plugins: ColdDeviceStorage.get('plugins'),
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
+ methods: {
+ uninstall(plugin) {
+ ColdDeviceStorage.set('plugins', this.plugins.filter(x => x.id !== plugin.id));
+ os.success();
+ this.$nextTick(() => {
+ unisonReload();
+ });
+ },
+
+ // TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
+ async config(plugin) {
+ const config = plugin.config;
+ for (const key in plugin.configData) {
+ config[key].default = plugin.configData[key];
+ }
+
+ const { canceled, result } = await os.form(plugin.name, config);
+ if (canceled) return;
+
+ const plugins = ColdDeviceStorage.get('plugins');
+ plugins.find(p => p.id === plugin.id).configData = result;
+ ColdDeviceStorage.set('plugins', plugins);
+
+ this.$nextTick(() => {
+ location.reload();
+ });
+ },
+
+ changeActive(plugin, active) {
+ const plugins = ColdDeviceStorage.get('plugins');
+ plugins.find(p => p.id === plugin.id).active = active;
+ ColdDeviceStorage.set('plugins', plugins);
+
+ this.$nextTick(() => {
+ location.reload();
+ });
+ }
},
});
</script>
diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue
index 78a0ea8b8d..dd13ba4bd0 100644
--- a/packages/client/src/pages/settings/privacy.vue
+++ b/packages/client/src/pages/settings/privacy.vue
@@ -47,8 +47,8 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormSection from '@/components/form/section.vue';
@@ -56,67 +56,39 @@ import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
import { defaultStore } from '@/store';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
-export default defineComponent({
- components: {
- FormSelect,
- FormSection,
- FormGroup,
- FormSwitch,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.privacy,
- icon: 'fas fa-lock-open',
- bg: 'var(--bg)',
- },
- isLocked: false,
- autoAcceptFollowed: false,
- noCrawle: false,
- isExplorable: false,
- hideOnlineStatus: false,
- publicReactions: false,
- ffVisibility: 'public',
- }
- },
+let isLocked = $ref($i.isLocked);
+let autoAcceptFollowed = $ref($i.autoAcceptFollowed);
+let noCrawle = $ref($i.noCrawle);
+let isExplorable = $ref($i.isExplorable);
+let hideOnlineStatus = $ref($i.hideOnlineStatus);
+let publicReactions = $ref($i.publicReactions);
+let ffVisibility = $ref($i.ffVisibility);
- computed: {
- defaultNoteVisibility: defaultStore.makeGetterSetter('defaultNoteVisibility'),
- defaultNoteLocalOnly: defaultStore.makeGetterSetter('defaultNoteLocalOnly'),
- rememberNoteVisibility: defaultStore.makeGetterSetter('rememberNoteVisibility'),
- keepCw: defaultStore.makeGetterSetter('keepCw'),
- },
+let defaultNoteVisibility = $computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
+let defaultNoteLocalOnly = $computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
+let rememberNoteVisibility = $computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
+let keepCw = $computed(defaultStore.makeGetterSetter('keepCw'));
- created() {
- this.isLocked = this.$i.isLocked;
- this.autoAcceptFollowed = this.$i.autoAcceptFollowed;
- this.noCrawle = this.$i.noCrawle;
- this.isExplorable = this.$i.isExplorable;
- this.hideOnlineStatus = this.$i.hideOnlineStatus;
- this.publicReactions = this.$i.publicReactions;
- this.ffVisibility = this.$i.ffVisibility;
- },
+function save() {
+ os.api('i/update', {
+ isLocked: !!isLocked,
+ autoAcceptFollowed: !!autoAcceptFollowed,
+ noCrawle: !!noCrawle,
+ isExplorable: !!isExplorable,
+ hideOnlineStatus: !!hideOnlineStatus,
+ publicReactions: !!publicReactions,
+ ffVisibility: ffVisibility,
+ });
+}
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.privacy,
+ icon: 'fas fa-lock-open',
+ bg: 'var(--bg)',
},
-
- methods: {
- save() {
- os.api('i/update', {
- isLocked: !!this.isLocked,
- autoAcceptFollowed: !!this.autoAcceptFollowed,
- noCrawle: !!this.noCrawle,
- isExplorable: !!this.isExplorable,
- hideOnlineStatus: !!this.hideOnlineStatus,
- publicReactions: !!this.publicReactions,
- ffVisibility: this.ffVisibility,
- });
- }
- }
});
</script>
diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue
index 2eaf9a9f83..f875146a2c 100644
--- a/packages/client/src/pages/settings/profile.vue
+++ b/packages/client/src/pages/settings/profile.vue
@@ -3,50 +3,50 @@
<div class="llvierxe" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div class="avatar _acrylic">
<MkAvatar class="avatar" :user="$i" :disable-link="true" @click="changeAvatar"/>
- <MkButton primary class="avatarEdit" @click="changeAvatar">{{ $ts._profile.changeAvatar }}</MkButton>
+ <MkButton primary class="avatarEdit" @click="changeAvatar">{{ i18n.locale._profile.changeAvatar }}</MkButton>
</div>
- <MkButton primary class="bannerEdit" @click="changeBanner">{{ $ts._profile.changeBanner }}</MkButton>
+ <MkButton primary class="bannerEdit" @click="changeBanner">{{ i18n.locale._profile.changeBanner }}</MkButton>
</div>
- <FormInput v-model="name" :max="30" manual-save class="_formBlock">
- <template #label>{{ $ts._profile.name }}</template>
+ <FormInput v-model="profile.name" :max="30" manual-save class="_formBlock">
+ <template #label>{{ i18n.locale._profile.name }}</template>
</FormInput>
- <FormTextarea v-model="description" :max="500" tall manual-save class="_formBlock">
- <template #label>{{ $ts._profile.description }}</template>
- <template #caption>{{ $ts._profile.youCanIncludeHashtags }}</template>
+ <FormTextarea v-model="profile.description" :max="500" tall manual-save class="_formBlock">
+ <template #label>{{ i18n.locale._profile.description }}</template>
+ <template #caption>{{ i18n.locale._profile.youCanIncludeHashtags }}</template>
</FormTextarea>
- <FormInput v-model="location" manual-save class="_formBlock">
- <template #label>{{ $ts.location }}</template>
+ <FormInput v-model="profile.location" manual-save class="_formBlock">
+ <template #label>{{ i18n.locale.location }}</template>
<template #prefix><i class="fas fa-map-marker-alt"></i></template>
</FormInput>
- <FormInput v-model="birthday" type="date" manual-save class="_formBlock">
- <template #label>{{ $ts.birthday }}</template>
+ <FormInput v-model="profile.birthday" type="date" manual-save class="_formBlock">
+ <template #label>{{ i18n.locale.birthday }}</template>
<template #prefix><i class="fas fa-birthday-cake"></i></template>
</FormInput>
- <FormSelect v-model="lang" class="_formBlock">
- <template #label>{{ $ts.language }}</template>
+ <FormSelect v-model="profile.lang" class="_formBlock">
+ <template #label>{{ i18n.locale.language }}</template>
<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
</FormSelect>
<FormSlot>
- <MkButton @click="editMetadata">{{ $ts._profile.metadataEdit }}</MkButton>
- <template #caption>{{ $ts._profile.metadataDescription }}</template>
+ <MkButton @click="editMetadata">{{ i18n.locale._profile.metadataEdit }}</MkButton>
+ <template #caption>{{ i18n.locale._profile.metadataDescription }}</template>
</FormSlot>
- <FormSwitch v-model="isCat" class="_formBlock">{{ $ts.flagAsCat }}<template #caption>{{ $ts.flagAsCatDescription }}</template></FormSwitch>
+ <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.locale.flagAsCat }}<template #caption>{{ i18n.locale.flagAsCatDescription }}</template></FormSwitch>
- <FormSwitch v-model="isBot" class="_formBlock">{{ $ts.flagAsBot }}<template #caption>{{ $ts.flagAsBotDescription }}</template></FormSwitch>
+ <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.locale.flagAsBot }}<template #caption>{{ i18n.locale.flagAsBotDescription }}</template></FormSwitch>
- <FormSwitch v-model="alwaysMarkNsfw" class="_formBlock">{{ $ts.alwaysMarkSensitive }}</FormSwitch>
+ <FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.locale.alwaysMarkSensitive }}</FormSwitch>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { defineComponent, reactive, watch } from 'vue';
import MkButton from '@/components/ui/button.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
@@ -57,198 +57,149 @@ import { host, langs } from '@/config';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
-export default defineComponent({
- components: {
- MkButton,
- FormInput,
- FormTextarea,
- FormSwitch,
- FormSelect,
- FormSlot,
- },
-
- emits: ['info'],
+const profile = reactive({
+ name: $i.name,
+ description: $i.description,
+ location: $i.location,
+ birthday: $i.birthday,
+ lang: $i.lang,
+ isBot: $i.isBot,
+ isCat: $i.isCat,
+ alwaysMarkNsfw: $i.alwaysMarkNsfw,
+});
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.profile,
- icon: 'fas fa-user',
- bg: 'var(--bg)',
- },
- host,
- langs,
- name: null,
- description: null,
- birthday: null,
- lang: null,
- location: null,
- fieldName0: null,
- fieldValue0: null,
- fieldName1: null,
- fieldValue1: null,
- fieldName2: null,
- fieldValue2: null,
- fieldName3: null,
- fieldValue3: null,
- avatarId: null,
- bannerId: null,
- isBot: false,
- isCat: false,
- alwaysMarkNsfw: false,
- saving: false,
- }
- },
+const additionalFields = reactive({
+ fieldName0: $i.fields[0] ? $i.fields[0].name : null,
+ fieldValue0: $i.fields[0] ? $i.fields[0].value : null,
+ fieldName1: $i.fields[1] ? $i.fields[1].name : null,
+ fieldValue1: $i.fields[1] ? $i.fields[1].value : null,
+ fieldName2: $i.fields[2] ? $i.fields[2].name : null,
+ fieldValue2: $i.fields[2] ? $i.fields[2].value : null,
+ fieldName3: $i.fields[3] ? $i.fields[3].name : null,
+ fieldValue3: $i.fields[3] ? $i.fields[3].value : null,
+});
- created() {
- this.name = this.$i.name;
- this.description = this.$i.description;
- this.location = this.$i.location;
- this.birthday = this.$i.birthday;
- this.lang = this.$i.lang;
- this.avatarId = this.$i.avatarId;
- this.bannerId = this.$i.bannerId;
- this.isBot = this.$i.isBot;
- this.isCat = this.$i.isCat;
- this.alwaysMarkNsfw = this.$i.alwaysMarkNsfw;
+watch(() => profile, () => {
+ save();
+}, {
+ deep: true,
+});
- this.fieldName0 = this.$i.fields[0] ? this.$i.fields[0].name : null;
- this.fieldValue0 = this.$i.fields[0] ? this.$i.fields[0].value : null;
- this.fieldName1 = this.$i.fields[1] ? this.$i.fields[1].name : null;
- this.fieldValue1 = this.$i.fields[1] ? this.$i.fields[1].value : null;
- this.fieldName2 = this.$i.fields[2] ? this.$i.fields[2].name : null;
- this.fieldValue2 = this.$i.fields[2] ? this.$i.fields[2].value : null;
- this.fieldName3 = this.$i.fields[3] ? this.$i.fields[3].name : null;
- this.fieldValue3 = this.$i.fields[3] ? this.$i.fields[3].value : null;
+function save() {
+ os.apiWithDialog('i/update', {
+ name: profile.name || null,
+ description: profile.description || null,
+ location: profile.location || null,
+ birthday: profile.birthday || null,
+ lang: profile.lang || null,
+ isBot: !!profile.isBot,
+ isCat: !!profile.isCat,
+ alwaysMarkNsfw: !!profile.alwaysMarkNsfw,
+ });
+}
- this.$watch('name', this.save);
- this.$watch('description', this.save);
- this.$watch('location', this.save);
- this.$watch('birthday', this.save);
- this.$watch('lang', this.save);
- this.$watch('isBot', this.save);
- this.$watch('isCat', this.save);
- this.$watch('alwaysMarkNsfw', this.save);
- },
+function changeAvatar(ev) {
+ selectFile(ev.currentTarget || ev.target, i18n.locale.avatar).then(async (file) => {
+ const i = await os.apiWithDialog('i/update', {
+ avatarId: file.id,
+ });
+ $i.avatarId = i.avatarId;
+ $i.avatarUrl = i.avatarUrl;
+ });
+}
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
+function changeBanner(ev) {
+ selectFile(ev.currentTarget || ev.target, i18n.locale.banner).then(async (file) => {
+ const i = await os.apiWithDialog('i/update', {
+ bannerId: file.id,
+ });
+ $i.bannerId = i.bannerId;
+ $i.bannerUrl = i.bannerUrl;
+ });
+}
- methods: {
- changeAvatar(e) {
- selectFile(e.currentTarget || e.target, this.$ts.avatar).then(file => {
- os.api('i/update', {
- avatarId: file.id,
- });
- });
+async function editMetadata() {
+ const { canceled, result } = await os.form(i18n.locale._profile.metadata, {
+ fieldName0: {
+ type: 'string',
+ label: i18n.locale._profile.metadataLabel + ' 1',
+ default: additionalFields.fieldName0,
},
-
- changeBanner(e) {
- selectFile(e.currentTarget || e.target, this.$ts.banner).then(file => {
- os.api('i/update', {
- bannerId: file.id,
- });
- });
+ fieldValue0: {
+ type: 'string',
+ label: i18n.locale._profile.metadataContent + ' 1',
+ default: additionalFields.fieldValue0,
},
+ fieldName1: {
+ type: 'string',
+ label: i18n.locale._profile.metadataLabel + ' 2',
+ default: additionalFields.fieldName1,
+ },
+ fieldValue1: {
+ type: 'string',
+ label: i18n.locale._profile.metadataContent + ' 2',
+ default: additionalFields.fieldValue1,
+ },
+ fieldName2: {
+ type: 'string',
+ label: i18n.locale._profile.metadataLabel + ' 3',
+ default: additionalFields.fieldName2,
+ },
+ fieldValue2: {
+ type: 'string',
+ label: i18n.locale._profile.metadataContent + ' 3',
+ default: additionalFields.fieldValue2,
+ },
+ fieldName3: {
+ type: 'string',
+ label: i18n.locale._profile.metadataLabel + ' 4',
+ default: additionalFields.fieldName3,
+ },
+ fieldValue3: {
+ type: 'string',
+ label: i18n.locale._profile.metadataContent + ' 4',
+ default: additionalFields.fieldValue3,
+ },
+ });
+ if (canceled) return;
- async editMetadata() {
- const { canceled, result } = await os.form(this.$ts._profile.metadata, {
- fieldName0: {
- type: 'string',
- label: this.$ts._profile.metadataLabel + ' 1',
- default: this.fieldName0,
- },
- fieldValue0: {
- type: 'string',
- label: this.$ts._profile.metadataContent + ' 1',
- default: this.fieldValue0,
- },
- fieldName1: {
- type: 'string',
- label: this.$ts._profile.metadataLabel + ' 2',
- default: this.fieldName1,
- },
- fieldValue1: {
- type: 'string',
- label: this.$ts._profile.metadataContent + ' 2',
- default: this.fieldValue1,
- },
- fieldName2: {
- type: 'string',
- label: this.$ts._profile.metadataLabel + ' 3',
- default: this.fieldName2,
- },
- fieldValue2: {
- type: 'string',
- label: this.$ts._profile.metadataContent + ' 3',
- default: this.fieldValue2,
- },
- fieldName3: {
- type: 'string',
- label: this.$ts._profile.metadataLabel + ' 4',
- default: this.fieldName3,
- },
- fieldValue3: {
- type: 'string',
- label: this.$ts._profile.metadataContent + ' 4',
- default: this.fieldValue3,
- },
- });
- if (canceled) return;
-
- this.fieldName0 = result.fieldName0;
- this.fieldValue0 = result.fieldValue0;
- this.fieldName1 = result.fieldName1;
- this.fieldValue1 = result.fieldValue1;
- this.fieldName2 = result.fieldName2;
- this.fieldValue2 = result.fieldValue2;
- this.fieldName3 = result.fieldName3;
- this.fieldValue3 = result.fieldValue3;
-
- const fields = [
- { name: this.fieldName0, value: this.fieldValue0 },
- { name: this.fieldName1, value: this.fieldValue1 },
- { name: this.fieldName2, value: this.fieldValue2 },
- { name: this.fieldName3, value: this.fieldValue3 },
- ];
+ additionalFields.fieldName0 = result.fieldName0;
+ additionalFields.fieldValue0 = result.fieldValue0;
+ additionalFields.fieldName1 = result.fieldName1;
+ additionalFields.fieldValue1 = result.fieldValue1;
+ additionalFields.fieldName2 = result.fieldName2;
+ additionalFields.fieldValue2 = result.fieldValue2;
+ additionalFields.fieldName3 = result.fieldName3;
+ additionalFields.fieldValue3 = result.fieldValue3;
- os.api('i/update', {
- fields,
- }).then(i => {
- os.success();
- }).catch(err => {
- os.alert({
- type: 'error',
- text: err.id
- });
- });
- },
+ const fields = [
+ { name: additionalFields.fieldName0, value: additionalFields.fieldValue0 },
+ { name: additionalFields.fieldName1, value: additionalFields.fieldValue1 },
+ { name: additionalFields.fieldName2, value: additionalFields.fieldValue2 },
+ { name: additionalFields.fieldName3, value: additionalFields.fieldValue3 },
+ ];
- save() {
- this.saving = true;
+ os.api('i/update', {
+ fields,
+ }).then(i => {
+ os.success();
+ }).catch(err => {
+ os.alert({
+ type: 'error',
+ text: err.id
+ });
+ });
+}
- os.apiWithDialog('i/update', {
- name: this.name || null,
- description: this.description || null,
- location: this.location || null,
- birthday: this.birthday || null,
- lang: this.lang || null,
- isBot: !!this.isBot,
- isCat: !!this.isCat,
- alwaysMarkNsfw: !!this.alwaysMarkNsfw,
- }).then(i => {
- this.saving = false;
- this.$i.avatarId = i.avatarId;
- this.$i.avatarUrl = i.avatarUrl;
- this.$i.bannerId = i.bannerId;
- this.$i.bannerUrl = i.bannerUrl;
- }).catch(err => {
- this.saving = false;
- });
- },
- }
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.profile,
+ icon: 'fas fa-user',
+ bg: 'var(--bg)',
+ },
});
</script>
diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue
index 0d4db46936..e5b1189947 100644
--- a/packages/client/src/pages/settings/reaction.vue
+++ b/packages/client/src/pages/settings/reaction.vue
@@ -100,10 +100,6 @@ export default defineComponent({
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
save() {
this.$store.set('reactions', this.reactions);
diff --git a/packages/client/src/pages/settings/registry.keys.vue b/packages/client/src/pages/settings/registry.keys.vue
deleted file mode 100644
index 89953ebea1..0000000000
--- a/packages/client/src/pages/settings/registry.keys.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<template>
-<FormBase>
- <FormGroup>
- <FormKeyValueView>
- <template #key>{{ $ts._registry.domain }}</template>
- <template #value>{{ $ts.system }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>{{ $ts._registry.scope }}</template>
- <template #value>{{ scope.join('/') }}</template>
- </FormKeyValueView>
- </FormGroup>
-
- <FormGroup v-if="keys">
- <template #label>{{ $ts._registry.keys }}</template>
- <FormLink v-for="key in keys" :to="`/settings/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
- </FormGroup>
-
- <FormButton primary @click="createKey">{{ $ts._registry.createKey }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import * as os from '@/os';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- FormBase,
- FormSelect,
- FormSwitch,
- FormButton,
- FormLink,
- FormGroup,
- FormKeyValueView,
- },
-
- props: {
- scope: {
- required: true
- }
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.registry,
- icon: 'fas fa-cogs',
- bg: 'var(--bg)',
- },
- keys: null,
- }
- },
-
- watch: {
- scope() {
- this.fetch();
- }
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- this.fetch();
- },
-
- methods: {
- fetch() {
- os.api('i/registry/keys-with-type', {
- scope: this.scope
- }).then(keys => {
- this.keys = Object.entries(keys).sort((a, b) => a[0].localeCompare(b[0]));
- });
- },
-
- async createKey() {
- const { canceled, result } = await os.form(this.$ts._registry.createKey, {
- key: {
- type: 'string',
- label: this.$ts._registry.key,
- },
- value: {
- type: 'string',
- multiline: true,
- label: this.$ts.value,
- },
- scope: {
- type: 'string',
- label: this.$ts._registry.scope,
- default: this.scope.join('/')
- }
- });
- if (canceled) return;
- os.apiWithDialog('i/registry/set', {
- scope: result.scope.split('/'),
- key: result.key,
- value: JSON5.parse(result.value),
- }).then(() => {
- this.fetch();
- });
- }
- }
-});
-</script>
diff --git a/packages/client/src/pages/settings/registry.value.vue b/packages/client/src/pages/settings/registry.value.vue
deleted file mode 100644
index 6acd3f6048..0000000000
--- a/packages/client/src/pages/settings/registry.value.vue
+++ /dev/null
@@ -1,147 +0,0 @@
-<template>
-<FormBase>
- <FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo>
-
- <template v-if="value">
- <FormGroup>
- <FormKeyValueView>
- <template #key>{{ $ts._registry.domain }}</template>
- <template #value>{{ $ts.system }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>{{ $ts._registry.scope }}</template>
- <template #value>{{ scope.join('/') }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>{{ $ts._registry.key }}</template>
- <template #value>{{ xKey }}</template>
- </FormKeyValueView>
- </FormGroup>
-
- <FormGroup>
- <FormTextarea v-model="valueForEditor" tall class="_monospace" style="tab-size: 2;">
- <span>{{ $ts.value }} (JSON)</span>
- </FormTextarea>
- <FormButton primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
- </FormGroup>
-
- <FormKeyValueView>
- <template #key>{{ $ts.updatedAt }}</template>
- <template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
- </FormKeyValueView>
-
- <FormButton danger @click="del"><i class="fas fa-trash"></i> {{ $ts.delete }}</FormButton>
- </template>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import FormInfo from '@/components/debobigego/info.vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormTextarea from '@/components/form/textarea.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import * as os from '@/os';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- FormInfo,
- FormBase,
- FormSelect,
- FormSwitch,
- FormButton,
- FormTextarea,
- FormGroup,
- FormKeyValueView,
- },
-
- props: {
- scope: {
- required: true
- },
- xKey: {
- required: true
- },
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.registry,
- icon: 'fas fa-cogs',
- bg: 'var(--bg)',
- },
- value: null,
- valueForEditor: null,
- }
- },
-
- watch: {
- key() {
- this.fetch();
- },
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- this.fetch();
- },
-
- methods: {
- fetch() {
- os.api('i/registry/get-detail', {
- scope: this.scope,
- key: this.xKey
- }).then(value => {
- this.value = value;
- this.valueForEditor = JSON5.stringify(this.value.value, null, '\t');
- });
- },
-
- save() {
- try {
- JSON5.parse(this.valueForEditor);
- } catch (e) {
- os.alert({
- type: 'error',
- text: this.$ts.invalidValue
- });
- return;
- }
-
- os.confirm({
- type: 'warning',
- text: this.$ts.saveConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
- os.apiWithDialog('i/registry/set', {
- scope: this.scope,
- key: this.xKey,
- value: JSON5.parse(this.valueForEditor)
- });
- });
- },
-
- del() {
- os.confirm({
- type: 'warning',
- text: this.$ts.deleteConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
- os.apiWithDialog('i/registry/remove', {
- scope: this.scope,
- key: this.xKey
- });
- });
- }
- }
-});
-</script>
diff --git a/packages/client/src/pages/settings/registry.vue b/packages/client/src/pages/settings/registry.vue
deleted file mode 100644
index 6faff5d2a4..0000000000
--- a/packages/client/src/pages/settings/registry.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<template>
-<FormBase>
- <FormGroup v-if="scopes">
- <template #label>{{ $ts.system }}</template>
- <FormLink v-for="scope in scopes" :to="`/settings/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
- </FormGroup>
- <FormButton primary @click="createKey">{{ $ts._registry.createKey }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import * as JSON5 from 'json5';
-import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import * as os from '@/os';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- FormBase,
- FormSelect,
- FormSwitch,
- FormButton,
- FormLink,
- FormGroup,
- FormKeyValueView,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.registry,
- icon: 'fas fa-cogs',
- bg: 'var(--bg)',
- },
- scopes: null,
- }
- },
-
- created() {
- this.fetch();
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
- methods: {
- fetch() {
- os.api('i/registry/scopes').then(scopes => {
- this.scopes = scopes.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
- });
- },
-
- async createKey() {
- const { canceled, result } = await os.form(this.$ts._registry.createKey, {
- key: {
- type: 'string',
- label: this.$ts._registry.key,
- },
- value: {
- type: 'string',
- multiline: true,
- label: this.$ts.value,
- },
- scope: {
- type: 'string',
- label: this.$ts._registry.scope,
- }
- });
- if (canceled) return;
- os.apiWithDialog('i/registry/set', {
- scope: result.scope.split('/'),
- key: result.key,
- value: JSON5.parse(result.value),
- }).then(() => {
- this.fetch();
- });
- }
- }
-});
-</script>
diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue
index 069f9d964d..6fb3f1c413 100644
--- a/packages/client/src/pages/settings/security.vue
+++ b/packages/client/src/pages/settings/security.vue
@@ -12,7 +12,7 @@
<FormSection>
<template #label>{{ $ts.signinHistory }}</template>
- <FormPagination :pagination="pagination">
+ <MkPagination :pagination="pagination">
<template v-slot="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
@@ -25,7 +25,7 @@
</div>
</div>
</template>
- </FormPagination>
+ </MkPagination>
</FormSection>
<FormSection>
@@ -40,10 +40,9 @@
<script lang="ts">
import { defineComponent } from 'vue';
import FormSection from '@/components/form/section.vue';
-import FormLink from '@/components/debobigego/link.vue';
import FormSlot from '@/components/form/slot.vue';
import FormButton from '@/components/ui/button.vue';
-import FormPagination from '@/components/form/pagination.vue';
+import MkPagination from '@/components/ui/pagination.vue';
import X2fa from './2fa.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
@@ -51,9 +50,8 @@ import * as symbols from '@/symbols';
export default defineComponent({
components: {
FormSection,
- FormLink,
FormButton,
- FormPagination,
+ MkPagination,
FormSlot,
X2fa,
},
@@ -68,16 +66,12 @@ export default defineComponent({
bg: 'var(--bg)',
},
pagination: {
- endpoint: 'i/signin-history',
+ endpoint: 'i/signin-history' as const,
limit: 5,
},
}
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async change() {
const { canceled: canceled1, result: currentPassword } = await os.inputText({
diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue
index 0977dd8322..490a1b5514 100644
--- a/packages/client/src/pages/settings/sounds.vue
+++ b/packages/client/src/pages/settings/sounds.vue
@@ -94,12 +94,6 @@ export default defineComponent({
this.sounds.chatBg = ColdDeviceStorage.get('sound_chatBg');
this.sounds.antenna = ColdDeviceStorage.get('sound_antenna');
this.sounds.channel = ColdDeviceStorage.get('sound_channel');
- this.sounds.reversiPutBlack = ColdDeviceStorage.get('sound_reversiPutBlack');
- this.sounds.reversiPutWhite = ColdDeviceStorage.get('sound_reversiPutWhite');
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
},
methods: {
diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue
index c3e531afb2..e2a3f042b9 100644
--- a/packages/client/src/pages/settings/theme.install.vue
+++ b/packages/client/src/pages/settings/theme.install.vue
@@ -1,105 +1,79 @@
<template>
-<FormBase>
- <FormGroup>
- <FormTextarea v-model="installThemeCode">
- <span>{{ $ts._theme.code }}</span>
- </FormTextarea>
- <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
- </FormGroup>
+<div class="_formRoot">
+ <FormTextarea v-model="installThemeCode" class="_formBlock">
+ <template #label>{{ i18n.locale._theme.code }}</template>
+ </FormTextarea>
- <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ $ts.install }}</FormButton>
-</FormBase>
+ <div class="_formBlock" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <FormButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="fas fa-eye"></i> {{ i18n.locale.preview }}</FormButton>
+ <FormButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="fas fa-check"></i> {{ i18n.locale.install }}</FormButton>
+ </div>
+</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import * as JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormRadios from '@/components/form/radios.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormButton from '@/components/debobigego/button.vue';
+import FormButton from '@/components/ui/button.vue';
import { applyTheme, validateTheme } from '@/scripts/theme';
import * as os from '@/os';
-import { ColdDeviceStorage } from '@/store';
import { addTheme, getThemes } from '@/theme-store';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
- FormTextarea,
- FormSelect,
- FormRadios,
- FormBase,
- FormGroup,
- FormLink,
- FormButton,
- },
-
- emits: ['info'],
+let installThemeCode = $ref(null);
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts._theme.install,
- icon: 'fas fa-download',
- bg: 'var(--bg)',
- },
- installThemeCode: null,
- }
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
+function parseThemeCode(code: string) {
+ let theme;
- methods: {
- parseThemeCode(code) {
- let theme;
+ try {
+ theme = JSON5.parse(code);
+ } catch (e) {
+ os.alert({
+ type: 'error',
+ text: i18n.locale._theme.invalid
+ });
+ return false;
+ }
+ if (!validateTheme(theme)) {
+ os.alert({
+ type: 'error',
+ text: i18n.locale._theme.invalid
+ });
+ return false;
+ }
+ if (getThemes().some(t => t.id === theme.id)) {
+ os.alert({
+ type: 'info',
+ text: i18n.locale._theme.alreadyInstalled
+ });
+ return false;
+ }
- try {
- theme = JSON5.parse(code);
- } catch (e) {
- os.alert({
- type: 'error',
- text: this.$ts._theme.invalid
- });
- return false;
- }
- if (!validateTheme(theme)) {
- os.alert({
- type: 'error',
- text: this.$ts._theme.invalid
- });
- return false;
- }
- if (getThemes().some(t => t.id === theme.id)) {
- os.alert({
- type: 'info',
- text: this.$ts._theme.alreadyInstalled
- });
- return false;
- }
+ return theme;
+}
- return theme;
- },
+function preview(code: string): void {
+ const theme = parseThemeCode(code);
+ if (theme) applyTheme(theme, false);
+}
- preview(code) {
- const theme = this.parseThemeCode(code);
- if (theme) applyTheme(theme, false);
- },
+async function install(code: string): Promise<void> {
+ const theme = parseThemeCode(code);
+ if (!theme) return;
+ await addTheme(theme);
+ os.alert({
+ type: 'success',
+ text: i18n.t('_theme.installed', { name: theme.name })
+ });
+}
- async install(code) {
- const theme = this.parseThemeCode(code);
- if (!theme) return;
- await addTheme(theme);
- os.alert({
- type: 'success',
- text: this.$t('_theme.installed', { name: theme.name })
- });
- },
- }
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale._theme.install,
+ icon: 'fas fa-download',
+ bg: 'var(--bg)',
+ },
});
</script>
diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue
index c605b1eb64..a1e849b540 100644
--- a/packages/client/src/pages/settings/theme.manage.vue
+++ b/packages/client/src/pages/settings/theme.manage.vue
@@ -30,9 +30,6 @@ import { defineComponent } from 'vue';
import * as JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
-import FormRadios from '@/components/form/radios.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import { Theme, builtinThemes } from '@/scripts/theme';
@@ -46,9 +43,6 @@ export default defineComponent({
components: {
FormTextarea,
FormSelect,
- FormRadios,
- FormBase,
- FormGroup,
FormInput,
FormButton,
},
@@ -84,10 +78,6 @@ export default defineComponent({
},
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
copyThemeCode() {
copyToClipboard(this.selectedThemeCode);
diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue
index 6c88b65699..658e36ec05 100644
--- a/packages/client/src/pages/settings/theme.vue
+++ b/packages/client/src/pages/settings/theme.vue
@@ -163,10 +163,6 @@ export default defineComponent({
location.reload();
});
- onMounted(() => {
- emit('info', INFO);
- });
-
onActivated(() => {
fetchThemes().then(() => {
installedThemes.value = getThemes();
diff --git a/packages/client/src/pages/settings/update.vue b/packages/client/src/pages/settings/update.vue
deleted file mode 100644
index e0d8f6d15c..0000000000
--- a/packages/client/src/pages/settings/update.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<template>
-<FormBase>
- <template v-if="meta">
- <FormInfo v-if="version === meta.version">{{ $ts.youAreRunningUpToDateClient }}</FormInfo>
- <FormInfo v-else warn>{{ $ts.newVersionOfClientAvailable }}</FormInfo>
- </template>
- <FormGroup>
- <template #label>{{ instanceName }}</template>
- <FormKeyValueView>
- <template #key>{{ $ts.currentVersion }}</template>
- <template #value>{{ version }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>{{ $ts.latestVersion }}</template>
- <template v-if="meta" #value>{{ meta.version }}</template>
- <template v-else #value><MkEllipsis/></template>
- </FormKeyValueView>
- </FormGroup>
- <FormGroup>
- <template #label>Misskey</template>
- <FormKeyValueView>
- <template #key>{{ $ts.latestVersion }}</template>
- <template v-if="releases" #value>{{ releases[0].tag_name }}</template>
- <template v-else #value><MkEllipsis/></template>
- </FormKeyValueView>
- <template v-if="releases" #caption><MkTime :time="releases[0].published_at" mode="detail"/></template>
- </FormGroup>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import FormInfo from '@/components/debobigego/info.vue';
-import * as os from '@/os';
-import { version, instanceName } from '@/config';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- FormBase,
- FormSelect,
- FormSwitch,
- FormButton,
- FormLink,
- FormGroup,
- FormKeyValueView,
- FormInfo,
- },
-
- emits: ['info'],
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: 'Misskey Update',
- icon: 'fas fa-sync-alt',
- bg: 'var(--bg)',
- },
- version,
- instanceName,
- releases: null,
- meta: null
- }
- },
-
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
-
- os.api('meta', {
- detail: false
- }).then(meta => {
- this.meta = meta;
- localStorage.setItem('v', meta.version);
- });
-
- fetch('https://api.github.com/repos/misskey-dev/misskey/releases', {
- method: 'GET',
- })
- .then(res => res.json())
- .then(res => {
- this.releases = res;
- });
- },
-
- methods: {
- }
-});
-</script>
diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue
index 068f88740a..19980dea14 100644
--- a/packages/client/src/pages/settings/word-mute.vue
+++ b/packages/client/src/pages/settings/word-mute.vue
@@ -31,7 +31,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import FormTextarea from '@/components/form/textarea.vue';
-import FormBase from '@/components/debobigego/base.vue';
import MkKeyValue from '@/components/key-value.vue';
import MkButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
@@ -42,7 +41,6 @@ import * as symbols from '@/symbols';
export default defineComponent({
components: {
- FormBase,
MkButton,
FormTextarea,
MkKeyValue,
@@ -89,10 +87,6 @@ export default defineComponent({
this.hardWordMutedNotesCount = (await os.api('i/get-word-muted-notes-count', {})).count;
},
- mounted() {
- this.$emit('info', this[symbols.PAGE_INFO]);
- },
-
methods: {
async save() {
this.$store.set('mutedWords', this.softMutedWords.trim().split('\n').map(x => x.trim().split(' ')));
diff --git a/packages/client/src/pages/share.vue b/packages/client/src/pages/share.vue
index bdd8500ee4..5df6256fb2 100644
--- a/packages/client/src/pages/share.vue
+++ b/packages/client/src/pages/share.vue
@@ -169,7 +169,7 @@ export default defineComponent({
window.close();
// 閉じなければ100ms後タイムラインに
- setTimeout(() => {
+ window.setTimeout(() => {
this.$router.push('/');
}, 100);
}
diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue
index 89375e05d2..a10af1a4cc 100644
--- a/packages/client/src/pages/signup-complete.vue
+++ b/packages/client/src/pages/signup-complete.vue
@@ -1,50 +1,36 @@
<template>
<div>
- {{ $ts.processing }}
+ {{ i18n.locale.processing }}
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted } from 'vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { login } from '@/account';
+import { i18n } from '@/i18n';
-export default defineComponent({
- components: {
+const props = defineProps<{
+ code: string;
+}>();
- },
-
- props: {
- code: {
- type: String,
- required: true
- }
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.signup,
- icon: 'fas fa-user'
- },
- }
- },
+onMounted(async () => {
+ await os.alert({
+ type: 'info',
+ text: i18n.t('clickToFinishEmailVerification', { ok: i18n.locale.gotIt }),
+ });
+ const res = await os.apiWithDialog('signup-pending', {
+ code: props.code,
+ });
+ login(res.i, '/');
+});
- async mounted() {
- await os.alert({
- type: 'info',
- text: this.$t('clickToFinishEmailVerification', { ok: this.$ts.gotIt }),
- });
- const res = await os.apiWithDialog('signup-pending', {
- code: this.code,
- });
- login(res.i, '/');
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.signup,
+ icon: 'fas fa-user',
},
-
- methods: {
-
- }
});
</script>
diff --git a/packages/client/src/pages/tag.vue b/packages/client/src/pages/tag.vue
index a0c8367849..045f1ef259 100644
--- a/packages/client/src/pages/tag.vue
+++ b/packages/client/src/pages/tag.vue
@@ -1,46 +1,31 @@
<template>
<div class="_section">
- <XNotes ref="notes" class="_content" :pagination="pagination"/>
+ <XNotes class="_content" :pagination="pagination"/>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import XNotes from '@/components/notes.vue';
import * as symbols from '@/symbols';
-export default defineComponent({
- components: {
- XNotes
- },
+const props = defineProps<{
+ tag: string;
+}>();
- props: {
- tag: {
- type: String,
- required: true
- }
- },
+const pagination = {
+ endpoint: 'notes/search-by-tag' as const,
+ limit: 10,
+ params: computed(() => ({
+ tag: props.tag,
+ })),
+};
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.tag,
- icon: 'fas fa-hashtag'
- },
- pagination: {
- endpoint: 'notes/search-by-tag',
- limit: 10,
- params: () => ({
- tag: this.tag,
- })
- },
- };
- },
-
- watch: {
- tag() {
- (this.$refs.notes as any).reload();
- }
- },
+defineExpose({
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: props.tag,
+ icon: 'fas fa-hashtag',
+ bg: 'var(--bg)',
+ })),
});
</script>
diff --git a/packages/client/src/pages/test.vue b/packages/client/src/pages/test.vue
deleted file mode 100644
index d05e00d374..0000000000
--- a/packages/client/src/pages/test.vue
+++ /dev/null
@@ -1,260 +0,0 @@
-<template>
-<div class="_section">
- <div class="_content">
- <div class="_card _gap">
- <div class="_title">Dialog</div>
- <div class="_content">
- <MkInput v-model="dialogTitle">
- <template #label>Title</template>
- </MkInput>
- <MkInput v-model="dialogBody">
- <template #label>Body</template>
- </MkInput>
- <MkRadio v-model="dialogType" value="info">Info</MkRadio>
- <MkRadio v-model="dialogType" value="success">Success</MkRadio>
- <MkRadio v-model="dialogType" value="warning">Warn</MkRadio>
- <MkRadio v-model="dialogType" value="error">Error</MkRadio>
- <MkSwitch v-model="dialogCancel">
- <span>With cancel button</span>
- </MkSwitch>
- <MkSwitch v-model="dialogCancelByBgClick">
- <span>Can cancel by modal bg click</span>
- </MkSwitch>
- <MkSwitch v-model="dialogInput">
- <span>With input field</span>
- </MkSwitch>
- <MkButton @click="showDialog()">Show</MkButton>
- </div>
- <div class="_content">
- <code>Result: {{ dialogResult }}</code>
- </div>
- </div>
-
- <div class="_card _gap">
- <div class="_title">Form</div>
- <div class="_content">
- <MkInput v-model="formTitle">
- <template #label>Title</template>
- </MkInput>
- <MkTextarea v-model="formForm">
- <template #label>Form</template>
- </MkTextarea>
- <MkButton @click="form()">Show</MkButton>
- </div>
- <div class="_content">
- <code>Result: {{ formResult }}</code>
- </div>
- </div>
-
- <div class="_card _gap">
- <div class="_title">MFM</div>
- <div class="_content">
- <MkTextarea v-model="mfm">
- <template #label>MFM</template>
- </MkTextarea>
- </div>
- <div class="_content">
- <Mfm :text="mfm"/>
- </div>
- </div>
-
- <div class="_card _gap">
- <div class="_title">selectDriveFile</div>
- <div class="_content">
- <MkSwitch v-model="selectDriveFileMultiple">
- <span>Multiple</span>
- </MkSwitch>
- <MkButton @click="selectDriveFile()">selectDriveFile</MkButton>
- </div>
- <div class="_content">
- <code>Result: {{ JSON.stringify(selectDriveFileResult) }}</code>
- </div>
- </div>
-
- <div class="_card _gap">
- <div class="_title">selectDriveFolder</div>
- <div class="_content">
- <MkSwitch v-model="selectDriveFolderMultiple">
- <span>Multiple</span>
- </MkSwitch>
- <MkButton @click="selectDriveFolder()">selectDriveFolder</MkButton>
- </div>
- <div class="_content">
- <code>Result: {{ JSON.stringify(selectDriveFolderResult) }}</code>
- </div>
- </div>
-
- <div class="_card _gap">
- <div class="_title">selectUser</div>
- <div class="_content">
- <MkButton @click="selectUser()">selectUser</MkButton>
- </div>
- <div class="_content">
- <code>Result: {{ user }}</code>
- </div>
- </div>
-
- <div class="_card _gap">
- <div class="_title">Notification</div>
- <div class="_content">
- <MkInput v-model="notificationIconUrl">
- <template #label>Icon URL</template>
- </MkInput>
- <MkInput v-model="notificationHeader">
- <template #label>Header</template>
- </MkInput>
- <MkTextarea v-model="notificationBody">
- <template #label>Body</template>
- </MkTextarea>
- <MkButton @click="createNotification()">createNotification</MkButton>
- </div>
- </div>
-
- <div class="_card _gap">
- <div class="_title">Waiting dialog</div>
- <div class="_content">
- <MkButton inline @click="openWaitingDialog()">icon only</MkButton>
- <MkButton inline @click="openWaitingDialog('Doing')">with text</MkButton>
- </div>
- </div>
-
- <div class="_card _gap">
- <div class="_title">Messaging window</div>
- <div class="_content">
- <MkButton @click="messagingWindowOpen()">open</MkButton>
- </div>
- </div>
-
- <MkButton @click="resetTutorial()">Reset tutorial</MkButton>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import MkButton from '@/components/ui/button.vue';
-import MkInput from '@/components/form/input.vue';
-import MkSwitch from '@/components/form/switch.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import MkRadio from '@/components/form/radio.vue';
-import * as os from '@/os';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- MkButton,
- MkInput,
- MkSwitch,
- MkTextarea,
- MkRadio,
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: 'TEST',
- icon: 'fas fa-exclamation-triangle'
- },
- dialogTitle: 'Hello',
- dialogBody: 'World!',
- dialogType: 'info',
- dialogCancel: false,
- dialogCancelByBgClick: true,
- dialogInput: false,
- dialogResult: null,
- formTitle: 'Test form',
- formForm: JSON.stringify({
- foo: {
- type: 'boolean',
- default: true,
- label: 'This is a boolean property'
- },
- bar: {
- type: 'number',
- default: 300,
- label: 'This is a number property'
- },
- baz: {
- type: 'string',
- default: 'Misskey makes you happy.',
- label: 'This is a string property'
- },
- qux: {
- type: 'string',
- multiline: true,
- default: 'Misskey makes\nyou happy.',
- label: 'Multiline string'
- },
- }, null, '\t'),
- formResult: null,
- mfm: '',
- selectDriveFileMultiple: false,
- selectDriveFolderMultiple: false,
- selectDriveFileResult: null,
- selectDriveFolderResult: null,
- user: null,
- notificationIconUrl: null,
- notificationHeader: '',
- notificationBody: '',
- }
- },
-
- methods: {
- async showDialog() {
- this.dialogResult = null;
- /*
- this.dialogResult = await os.dialog({
- type: this.dialogType,
- title: this.dialogTitle,
- text: this.dialogBody,
- showCancelButton: this.dialogCancel,
- cancelableByBgClick: this.dialogCancelByBgClick,
- input: this.dialogInput ? {} : null
- });*/
- },
-
- async form() {
- this.formResult = null;
- this.formResult = await os.form(this.formTitle, JSON.parse(this.formForm));
- },
-
- async selectDriveFile() {
- this.selectDriveFileResult = null;
- this.selectDriveFileResult = await os.selectDriveFile(this.selectDriveFileMultiple);
- },
-
- async selectDriveFolder() {
- this.selectDriveFolderResult = null;
- this.selectDriveFolderResult = await os.selectDriveFolder(this.selectDriveFolderMultiple);
- },
-
- async selectUser() {
- this.user = null;
- this.user = await os.selectUser();
- },
-
- async createNotification() {
- os.api('notifications/create', {
- header: this.notificationHeader,
- body: this.notificationBody,
- icon: this.notificationIconUrl,
- });
- },
-
- messagingWindowOpen() {
- os.pageWindow('/my/messaging');
- },
-
- openWaitingDialog(text?) {
- const promise = new Promise((resolve, reject) => {
- setTimeout(resolve, 2000);
- });
- os.promiseDialog(promise, null, null, text);
- },
-
- resetTutorial() {
- this.$store.set('tutorial', 0);
- },
- }
-});
-</script>
diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue
index f023653425..80b8c7806c 100644
--- a/packages/client/src/pages/theme-editor.vue
+++ b/packages/client/src/pages/theme-editor.vue
@@ -1,300 +1,274 @@
<template>
-<FormBase class="cwepdizn">
- <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" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
- <div class="preview" :style="{ background: color.forPreview }"></div>
- </button>
+<MkSpacer :content-max="800" :margin-min="16" :margin-max="32">
+ <div class="cwepdizn _formRoot">
+ <FormFolder :default-open="true" class="_formBlock">
+ <template #label>{{ i18n.locale.backgroundColor }}</template>
+ <div class="cwepdizn-colors">
+ <div class="row">
+ <button v-for="color in bgColors.filter(x => x.kind === 'light')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
+ <div class="preview" :style="{ background: color.forPreview }"></div>
+ </button>
+ </div>
+ <div class="row">
+ <button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
+ <div class="preview" :style="{ background: color.forPreview }"></div>
+ </button>
+ </div>
</div>
- <div class="row">
- <button v-for="color in bgColors.filter(x => x.kind === 'dark')" :key="color.color" class="color _button" :class="{ active: theme.props.bg === color.color }" @click="setBgColor(color)">
- <div class="preview" :style="{ background: color.forPreview }"></div>
- </button>
- </div>
- </div>
- </div>
- <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" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)">
- <div class="preview" :style="{ background: color }"></div>
- </button>
+ </FormFolder>
+
+ <FormFolder :default-open="true" class="_formBlock">
+ <template #label>{{ i18n.locale.accentColor }}</template>
+ <div class="cwepdizn-colors">
+ <div class="row">
+ <button v-for="color in accentColors" :key="color" class="color rounded _button" :class="{ active: theme.props.accent === color }" @click="setAccentColor(color)">
+ <div class="preview" :style="{ background: color }"></div>
+ </button>
+ </div>
</div>
- </div>
- </div>
- <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" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)">
- <div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
- </button>
+ </FormFolder>
+
+ <FormFolder :default-open="true" class="_formBlock">
+ <template #label>{{ i18n.locale.textColor }}</template>
+ <div class="cwepdizn-colors">
+ <div class="row">
+ <button v-for="color in fgColors" :key="color" class="color char _button" :class="{ active: (theme.props.fg === color.forLight) || (theme.props.fg === color.forDark) }" @click="setFgColor(color)">
+ <div class="preview" :style="{ color: color.forPreview ? color.forPreview : theme.base === 'light' ? '#5f5f5f' : '#dadada' }">A</div>
+ </button>
+ </div>
</div>
- </div>
- </div>
+ </FormFolder>
- <FormGroup v-if="codeEnabled">
- <FormTextarea v-model="themeCode" tall>
- <span>{{ $ts._theme.code }}</span>
- </FormTextarea>
- <FormButton primary @click="applyThemeCode">{{ $ts.apply }}</FormButton>
- </FormGroup>
- <FormButton v-else @click="codeEnabled = true"><i class="fas fa-code"></i> {{ $ts.editCode }}</FormButton>
+ <FormFolder :default-open="false" class="_formBlock">
+ <template #icon><i class="fas fa-code"></i></template>
+ <template #label>{{ i18n.locale.editCode }}</template>
- <FormGroup v-if="descriptionEnabled">
- <FormTextarea v-model="description">
- <span>{{ $ts._theme.description }}</span>
- </FormTextarea>
- </FormGroup>
- <FormButton v-else @click="descriptionEnabled = true">{{ $ts.addDescription }}</FormButton>
+ <div class="_formRoot">
+ <FormTextarea v-model="themeCode" tall class="_formBlock">
+ <template #label>{{ i18n.locale._theme.code }}</template>
+ </FormTextarea>
+ <FormButton primary class="_formBlock" @click="applyThemeCode">{{ i18n.locale.apply }}</FormButton>
+ </div>
+ </FormFolder>
- <FormGroup>
- <FormButton @click="showPreview"><i class="fas fa-eye"></i> {{ $ts.preview }}</FormButton>
- <FormButton primary @click="saveAs"><i class="fas fa-save"></i> {{ $ts.saveAs }}</FormButton>
- </FormGroup>
-</FormBase>
+ <FormFolder :default-open="false" class="_formBlock">
+ <template #label>{{ i18n.locale.addDescription }}</template>
+
+ <div class="_formRoot">
+ <FormTextarea v-model="description">
+ <template #label>{{ i18n.locale._theme.description }}</template>
+ </FormTextarea>
+ </div>
+ </FormFolder>
+ </div>
+</MkSpacer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { watch } from 'vue';
import { toUnicode } from 'punycode/';
import * as tinycolor from 'tinycolor2';
import { v4 as uuid} from 'uuid';
import * as JSON5 from 'json5';
-import FormBase from '@/components/debobigego/base.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormGroup from '@/components/debobigego/group.vue';
+import FormButton from '@/components/ui/button.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormFolder from '@/components/form/folder.vue';
-import { Theme, applyTheme, validateTheme, darkTheme, lightTheme } from '@/scripts/theme';
+import { Theme, applyTheme, darkTheme, lightTheme } from '@/scripts/theme';
import { host } from '@/config';
import * as os from '@/os';
-import { ColdDeviceStorage } from '@/store';
+import { ColdDeviceStorage, defaultStore } from '@/store';
import { addTheme } from '@/theme-store';
import * as symbols from '@/symbols';
+import { i18n } from '@/i18n';
+import { useLeaveGuard } from '@/scripts/use-leave-guard';
-export default defineComponent({
- components: {
- FormBase,
- FormButton,
- FormTextarea,
- FormGroup,
- },
+const bgColors = [
+ { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
+ { color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
+ { color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' },
+ { color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' },
+ { color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' },
+ { color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' },
+ { color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' },
+ { color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' },
+ { color: '#2b2b2b', kind: 'dark', forPreview: '#444444' },
+ { color: '#362e29', kind: 'dark', forPreview: '#735c4d' },
+ { color: '#303629', kind: 'dark', forPreview: '#506d2f' },
+ { color: '#293436', kind: 'dark', forPreview: '#258192' },
+ { color: '#2e2936', kind: 'dark', forPreview: '#504069' },
+ { color: '#252722', kind: 'dark', forPreview: '#3c462f' },
+ { color: '#212525', kind: 'dark', forPreview: '#303e3e' },
+ { color: '#191919', kind: 'dark', forPreview: '#272727' },
+] as const;
+const accentColors = ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'];
+const fgColors = [
+ { color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
+ { color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
+ { color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' },
+ { color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' },
+ { color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' },
+ { color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
+ { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
+];
- async beforeRouteLeave(to, from) {
- if (this.changed && !(await this.leaveConfirm())) {
- return false;
- }
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.themeEditor,
- icon: 'fas fa-palette',
- },
- theme: {
- base: 'light',
- props: lightTheme.props
- } as Theme,
- codeEnabled: false,
- descriptionEnabled: false,
- description: null,
- themeCode: null,
- bgColors: [
- { color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
- { color: '#f0eee9', kind: 'light', forPreview: '#f3e2b9' },
- { color: '#e9eff0', kind: 'light', forPreview: '#bfe3e8' },
- { color: '#f0e9ee', kind: 'light', forPreview: '#f1d1e8' },
- { color: '#dce2e0', kind: 'light', forPreview: '#a4dccc' },
- { color: '#e2e0dc', kind: 'light', forPreview: '#d8c7a5' },
- { color: '#d5dbe0', kind: 'light', forPreview: '#b0cae0' },
- { color: '#dad5d5', kind: 'light', forPreview: '#d6afaf' },
- { color: '#2b2b2b', kind: 'dark', forPreview: '#444444' },
- { color: '#362e29', kind: 'dark', forPreview: '#735c4d' },
- { color: '#303629', kind: 'dark', forPreview: '#506d2f' },
- { color: '#293436', kind: 'dark', forPreview: '#258192' },
- { color: '#2e2936', kind: 'dark', forPreview: '#504069' },
- { color: '#252722', kind: 'dark', forPreview: '#3c462f' },
- { color: '#212525', kind: 'dark', forPreview: '#303e3e' },
- { color: '#191919', kind: 'dark', forPreview: '#272727' },
- ],
- accentColors: ['#e36749', '#f29924', '#98c934', '#34c9a9', '#34a1c9', '#606df7', '#8d34c9', '#e84d83'],
- fgColors: [
- { color: 'none', forLight: '#5f5f5f', forDark: '#dadada', forPreview: null },
- { color: 'red', forLight: '#7f6666', forDark: '#e4d1d1', forPreview: '#ca4343' },
- { color: 'yellow', forLight: '#736955', forDark: '#e0d5c0', forPreview: '#d49923' },
- { color: 'green', forLight: '#586d5b', forDark: '#d1e4d4', forPreview: '#4cbd5c' },
- { color: 'cyan', forLight: '#5d7475', forDark: '#d1e3e4', forPreview: '#2abdc3' },
- { color: 'blue', forLight: '#676880', forDark: '#d1d2e4', forPreview: '#7275d8' },
- { color: 'pink', forLight: '#84667d', forDark: '#e4d1e0', forPreview: '#b12390' },
- ],
- changed: false,
- }
- },
-
- created() {
- this.$watch('theme', this.apply, { deep: true });
- window.addEventListener('beforeunload', this.beforeunload);
- },
+const theme = $ref<Partial<Theme>>({
+ base: 'light',
+ props: lightTheme.props,
+});
+let description = $ref<string | null>(null);
+let themeCode = $ref<string | null>(null);
+let changed = $ref(false);
- beforeUnmount() {
- window.removeEventListener('beforeunload', this.beforeunload);
- },
+useLeaveGuard($$(changed));
- methods: {
- beforeunload(e: BeforeUnloadEvent) {
- if (this.changed) {
- e.preventDefault();
- e.returnValue = '';
- }
- },
+function showPreview() {
+ os.pageWindow('preview');
+}
- async leaveConfirm(): Promise<boolean> {
- const { canceled } = await os.confirm({
- type: 'warning',
- text: this.$ts.leaveConfirm,
- });
- return !canceled;
- },
+function setBgColor(color: typeof bgColors[number]) {
+ if (theme.base != color.kind) {
+ const base = color.kind === 'dark' ? darkTheme : lightTheme;
+ for (const prop of Object.keys(base.props)) {
+ if (prop === 'accent') continue;
+ if (prop === 'fg') continue;
+ theme.props[prop] = base.props[prop];
+ }
+ }
+ theme.base = color.kind;
+ theme.props.bg = color.color;
- showPreview() {
- os.pageWindow('preview');
- },
+ if (theme.props.fg) {
+ const matchedFgColor = fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(theme.props.fg).toRgbString()));
+ if (matchedFgColor) setFgColor(matchedFgColor);
+ }
+}
- setBgColor(color) {
- if (this.theme.base != color.kind) {
- const base = color.kind === 'dark' ? darkTheme : lightTheme;
- for (const prop of Object.keys(base.props)) {
- if (prop === 'accent') continue;
- if (prop === 'fg') continue;
- this.theme.props[prop] = base.props[prop];
- }
- }
- this.theme.base = color.kind;
- this.theme.props.bg = color.color;
+function setAccentColor(color) {
+ theme.props.accent = color;
+}
- if (this.theme.props.fg) {
- const matchedFgColor = this.fgColors.find(x => [tinycolor(x.forLight).toRgbString(), tinycolor(x.forDark).toRgbString()].includes(tinycolor(this.theme.props.fg).toRgbString()));
- if (matchedFgColor) this.setFgColor(matchedFgColor);
- }
- },
+function setFgColor(color) {
+ theme.props.fg = theme.base === 'light' ? color.forLight : color.forDark;
+}
- setAccentColor(color) {
- this.theme.props.accent = color;
- },
+function apply() {
+ themeCode = JSON5.stringify(theme, null, '\t');
+ applyTheme(theme, false);
+ changed = true;
+}
- setFgColor(color) {
- this.theme.props.fg = this.theme.base === 'light' ? color.forLight : color.forDark;
- },
+function applyThemeCode() {
+ let parsed;
- apply() {
- this.themeCode = JSON5.stringify(this.theme, null, '\t');
- applyTheme(this.theme, false);
- this.changed = true;
- },
+ try {
+ parsed = JSON5.parse(themeCode);
+ } catch (err) {
+ os.alert({
+ type: 'error',
+ text: i18n.locale._theme.invalid,
+ });
+ return;
+ }
- applyThemeCode() {
- let parsed;
+ theme = parsed;
+}
- try {
- parsed = JSON5.parse(this.themeCode);
- } catch (e) {
- os.alert({
- type: 'error',
- text: this.$ts._theme.invalid
- });
- return;
- }
+async function saveAs() {
+ const { canceled, result: name } = await os.inputText({
+ title: i18n.locale.name,
+ allowEmpty: false,
+ });
+ if (canceled) return;
- this.theme = parsed;
- },
+ theme.id = uuid();
+ theme.name = name;
+ theme.author = `@${$i.username}@${toUnicode(host)}`;
+ if (description) theme.desc = description;
+ addTheme(theme);
+ applyTheme(theme);
+ if (defaultStore.state.darkMode) {
+ ColdDeviceStorage.set('darkTheme', theme);
+ } else {
+ ColdDeviceStorage.set('lightTheme', theme);
+ }
+ changed = false;
+ os.alert({
+ type: 'success',
+ text: i18n.t('_theme.installed', { name: theme.name }),
+ });
+}
- async saveAs() {
- const { canceled, result: name } = await os.inputText({
- title: this.$ts.name,
- allowEmpty: false
- });
- if (canceled) return;
+watch($$(theme), apply, { deep: true });
- this.theme.id = uuid();
- this.theme.name = name;
- this.theme.author = `@${this.$i.username}@${toUnicode(host)}`;
- if (this.description) this.theme.desc = this.description;
- addTheme(this.theme);
- applyTheme(this.theme);
- if (this.$store.state.darkMode) {
- ColdDeviceStorage.set('darkTheme', this.theme);
- } else {
- ColdDeviceStorage.set('lightTheme', this.theme);
- }
- this.changed = false;
- os.alert({
- type: 'success',
- text: this.$t('_theme.installed', { name: this.theme.name })
- });
- }
- }
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: i18n.locale.themeEditor,
+ icon: 'fas fa-palette',
+ bg: 'var(--bg)',
+ actions: [{
+ asFullButton: true,
+ icon: 'fas fa-eye',
+ text: i18n.locale.preview,
+ handler: showPreview,
+ }, {
+ asFullButton: true,
+ icon: 'fas fa-check',
+ text: i18n.locale.saveAs,
+ handler: saveAs,
+ }],
+ },
});
</script>
<style lang="scss" scoped>
.cwepdizn {
- max-width: 800px;
- margin: 0 auto;
+ ::v-deep(.cwepdizn-colors) {
+ text-align: center;
- > .colorPicker {
- > .colors {
- padding: 32px;
- text-align: center;
+ > .row {
+ > .color {
+ display: inline-block;
+ position: relative;
+ width: 64px;
+ height: 64px;
+ border-radius: 8px;
- > .row {
- > .color {
- display: inline-block;
- position: relative;
- width: 64px;
- height: 64px;
- border-radius: 8px;
+ > .preview {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ margin: auto;
+ width: 42px;
+ height: 42px;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgb(0 0 0 / 30%);
+ transition: transform 0.15s ease;
+ }
+ &:hover {
> .preview {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- margin: auto;
- width: 42px;
- height: 42px;
- border-radius: 4px;
- box-shadow: 0 2px 4px rgb(0 0 0 / 30%);
- transition: transform 0.15s ease;
+ transform: scale(1.1);
}
+ }
- &:hover {
- > .preview {
- transform: scale(1.1);
- }
- }
+ &.active {
+ box-shadow: 0 0 0 2px var(--divider) inset;
+ }
- &.active {
- box-shadow: 0 0 0 2px var(--divider) inset;
- }
+ &.rounded {
+ border-radius: 999px;
- &.rounded {
+ > .preview {
border-radius: 999px;
-
- > .preview {
- border-radius: 999px;
- }
}
+ }
- &.char {
- line-height: 42px;
- }
+ &.char {
+ line-height: 42px;
}
}
}
diff --git a/packages/client/src/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue
index 3775796940..432d28c60b 100644
--- a/packages/client/src/pages/timeline.tutorial.vue
+++ b/packages/client/src/pages/timeline.tutorial.vue
@@ -65,26 +65,14 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import MkButton from '@/components/ui/button.vue';
+import { defaultStore } from '@/store';
-export default defineComponent({
- components: {
- MkButton,
- },
-
- data() {
- return {
- }
- },
-
- computed: {
- tutorial: {
- get() { return this.$store.reactiveState.tutorial.value || 0; },
- set(value) { this.$store.set('tutorial', value); }
- },
- },
+const tutorial = computed({
+ get() { return defaultStore.reactiveState.tutorial.value || 0; },
+ set(value) { defaultStore.set('tutorial', value); }
});
</script>
diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue
index 216b3c34ea..aabb953aec 100644
--- a/packages/client/src/pages/timeline.vue
+++ b/packages/client/src/pages/timeline.vue
@@ -1,6 +1,6 @@
<template>
<MkSpacer :content-max="800">
- <div v-hotkey.global="keymap" class="cmuxhskf">
+ <div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf">
<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _block"/>
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _block" fixed/>
@@ -18,162 +18,144 @@
</template>
<script lang="ts">
-import { defineComponent, defineAsyncComponent, computed } from 'vue';
+export default {
+ name: 'MkTimelinePage',
+}
+</script>
+
+<script lang="ts" setup>
+import { defineAsyncComponent, computed, watch } from 'vue';
import XTimeline from '@/components/timeline.vue';
import XPostForm from '@/components/post-form.vue';
import { scroll } from '@/scripts/scroll';
import * as os from '@/os';
import * as symbols from '@/symbols';
+import { defaultStore } from '@/store';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import { $i } from '@/account';
-export default defineComponent({
- name: 'timeline',
-
- components: {
- XTimeline,
- XTutorial: defineAsyncComponent(() => import('./timeline.tutorial.vue')),
- XPostForm,
- },
+const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
- data() {
- return {
- src: 'home',
- queue: 0,
- [symbols.PAGE_INFO]: 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,
- handler: this.chooseList
- }, {
- icon: 'fas fa-satellite',
- text: this.$ts.antennas,
- handler: this.chooseAntenna
- }, {
- icon: 'fas fa-satellite-dish',
- text: this.$ts.channel,
- handler: this.chooseChannel
- }, {
- icon: 'fas fa-calendar-alt',
- text: this.$ts.jumpToSpecifiedDate,
- handler: this.timetravel
- }],
- tabs: [{
- active: this.src === 'home',
- title: this.$ts._timelines.home,
- icon: 'fas fa-home',
- iconOnly: true,
- onClick: () => { this.src = 'home'; this.saveSrc(); },
- }, ...(this.isLocalTimelineAvailable ? [{
- active: this.src === 'local',
- title: this.$ts._timelines.local,
- icon: 'fas fa-comments',
- iconOnly: true,
- onClick: () => { this.src = 'local'; this.saveSrc(); },
- }, {
- active: this.src === 'social',
- title: this.$ts._timelines.social,
- icon: 'fas fa-share-alt',
- iconOnly: true,
- onClick: () => { this.src = 'social'; this.saveSrc(); },
- }] : []), ...(this.isGlobalTimelineAvailable ? [{
- active: this.src === 'global',
- title: this.$ts._timelines.global,
- icon: 'fas fa-globe',
- iconOnly: true,
- onClick: () => { this.src = 'global'; this.saveSrc(); },
- }] : [])],
- })),
- };
- },
+const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
+const isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
+const keymap = {
+ 't': focus,
+};
- computed: {
- keymap(): any {
- return {
- 't': this.focus
- };
- },
+const tlComponent = $ref<InstanceType<typeof XTimeline>>();
+const rootEl = $ref<HTMLElement>();
- isLocalTimelineAvailable(): boolean {
- return !this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin;
- },
+let src = $ref<'home' | 'local' | 'social' | 'global'>(defaultStore.state.tl.src);
+let queue = $ref(0);
- isGlobalTimelineAvailable(): boolean {
- return !this.$instance.disableGlobalTimeline || this.$i.isModerator || this.$i.isAdmin;
- },
- },
-
- watch: {
- src() {
- this.showNav = false;
- },
- },
-
- created() {
- this.src = this.$store.state.tl.src;
- },
+function queueUpdated(q: number): void {
+ queue = q;
+}
- methods: {
- queueUpdated(q) {
- this.queue = q;
- },
+function top(): void {
+ scroll(rootEl, { top: 0 });
+}
- top() {
- scroll(this.$el, { top: 0 });
- },
+async function chooseList(ev: MouseEvent): Promise<void> {
+ const lists = await os.api('users/lists/list');
+ const items = lists.map(list => ({
+ type: 'link',
+ text: list.name,
+ to: `/timeline/list/${list.id}`,
+ }));
+ os.popupMenu(items, ev.currentTarget || ev.target);
+}
- async chooseList(ev) {
- const lists = await os.api('users/lists/list');
- const items = lists.map(list => ({
- type: 'link',
- text: list.name,
- to: `/timeline/list/${list.id}`
- }));
- os.popupMenu(items, ev.currentTarget || ev.target);
- },
+async function chooseAntenna(ev: MouseEvent): Promise<void> {
+ const antennas = await os.api('antennas/list');
+ const items = antennas.map(antenna => ({
+ type: 'link',
+ text: antenna.name,
+ indicate: antenna.hasUnreadNote,
+ to: `/timeline/antenna/${antenna.id}`,
+ }));
+ os.popupMenu(items, ev.currentTarget || ev.target);
+}
- async chooseAntenna(ev) {
- const antennas = await os.api('antennas/list');
- const items = antennas.map(antenna => ({
- type: 'link',
- text: antenna.name,
- indicate: antenna.hasUnreadNote,
- to: `/timeline/antenna/${antenna.id}`
- }));
- os.popupMenu(items, ev.currentTarget || ev.target);
- },
+async function chooseChannel(ev: MouseEvent): Promise<void> {
+ const channels = await os.api('channels/followed');
+ const items = channels.map(channel => ({
+ type: 'link',
+ text: channel.name,
+ indicate: channel.hasUnreadNote,
+ to: `/channels/${channel.id}`,
+ }));
+ os.popupMenu(items, ev.currentTarget || ev.target);
+}
- async chooseChannel(ev) {
- const channels = await os.api('channels/followed');
- const items = channels.map(channel => ({
- type: 'link',
- text: channel.name,
- indicate: channel.hasUnreadNote,
- to: `/channels/${channel.id}`
- }));
- os.popupMenu(items, ev.currentTarget || ev.target);
- },
+function saveSrc(): void {
+ defaultStore.set('tl', {
+ src: src,
+ });
+}
- saveSrc() {
- this.$store.set('tl', {
- src: this.src,
- });
- },
+async function timetravel(): Promise<void> {
+ const { canceled, result: date } = await os.inputDate({
+ title: i18n.locale.date,
+ });
+ if (canceled) return;
- async timetravel() {
- const { canceled, result: date } = await os.inputDate({
- title: this.$ts.date,
- });
- if (canceled) return;
+ tlComponent.timetravel(date);
+}
- this.$refs.tl.timetravel(date);
- },
+function focus(): void {
+ tlComponent.focus();
+}
- focus() {
- (this.$refs.tl as any).focus();
- }
- }
+defineExpose({
+ [symbols.PAGE_INFO]: computed(() => ({
+ title: i18n.locale.timeline,
+ icon: src === 'local' ? 'fas fa-comments' : src === 'social' ? 'fas fa-share-alt' : src === 'global' ? 'fas fa-globe' : 'fas fa-home',
+ bg: 'var(--bg)',
+ actions: [{
+ icon: 'fas fa-list-ul',
+ text: i18n.locale.lists,
+ handler: chooseList,
+ }, {
+ icon: 'fas fa-satellite',
+ text: i18n.locale.antennas,
+ handler: chooseAntenna,
+ }, {
+ icon: 'fas fa-satellite-dish',
+ text: i18n.locale.channel,
+ handler: chooseChannel,
+ }, {
+ icon: 'fas fa-calendar-alt',
+ text: i18n.locale.jumpToSpecifiedDate,
+ handler: timetravel,
+ }],
+ tabs: [{
+ active: src === 'home',
+ title: i18n.locale._timelines.home,
+ icon: 'fas fa-home',
+ iconOnly: true,
+ onClick: () => { src = 'home'; saveSrc(); },
+ }, ...(isLocalTimelineAvailable ? [{
+ active: src === 'local',
+ title: i18n.locale._timelines.local,
+ icon: 'fas fa-comments',
+ iconOnly: true,
+ onClick: () => { src = 'local'; saveSrc(); },
+ }, {
+ active: src === 'social',
+ title: i18n.locale._timelines.social,
+ icon: 'fas fa-share-alt',
+ iconOnly: true,
+ onClick: () => { src = 'social'; saveSrc(); },
+ }] : []), ...(isGlobalTimelineAvailable ? [{
+ active: src === 'global',
+ title: i18n.locale._timelines.global,
+ icon: 'fas fa-globe',
+ iconOnly: true,
+ onClick: () => { src = 'global'; saveSrc(); },
+ }] : [])],
+ })),
});
</script>
diff --git a/packages/client/src/pages/user-ap-info.vue b/packages/client/src/pages/user-ap-info.vue
deleted file mode 100644
index 0027381f53..0000000000
--- a/packages/client/src/pages/user-ap-info.vue
+++ /dev/null
@@ -1,124 +0,0 @@
-<template>
-<FormBase>
- <FormSuspense v-slot="{ result: ap }" :p="apPromiseFactory">
- <FormGroup>
- <template #label>ActivityPub</template>
- <FormKeyValueView>
- <template #key>Type</template>
- <template #value><span class="_monospace">{{ ap.type }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>URI</template>
- <template #value><span class="_monospace">{{ ap.id }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>URL</template>
- <template #value><span class="_monospace">{{ ap.url }}</span></template>
- </FormKeyValueView>
- <FormGroup>
- <FormKeyValueView>
- <template #key>Inbox</template>
- <template #value><span class="_monospace">{{ ap.inbox }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>Shared Inbox</template>
- <template #value><span class="_monospace">{{ ap.sharedInbox || ap.endpoints.sharedInbox }}</span></template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>Outbox</template>
- <template #value><span class="_monospace">{{ ap.outbox }}</span></template>
- </FormKeyValueView>
- </FormGroup>
- <FormTextarea readonly tall code pre :value="ap.publicKey.publicKeyPem">
- <span>Public Key</span>
- </FormTextarea>
- <FormKeyValueView>
- <template #key>Discoverable</template>
- <template #value>{{ ap.discoverable ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- <FormKeyValueView>
- <template #key>ManuallyApprovesFollowers</template>
- <template #value>{{ ap.manuallyApprovesFollowers ? $ts.yes : $ts.no }}</template>
- </FormKeyValueView>
- <FormObjectView tall :value="ap">
- <span>Raw</span>
- </FormObjectView>
- <FormGroup>
- <FormLink :to="`https://${user.host}/.well-known/webfinger?resource=acct:${user.username}`" external>WebFinger</FormLink>
- </FormGroup>
- <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
- <FormKeyValueView v-else>
- <template #key>{{ $ts.instanceInfo }}</template>
- <template #value>(Local user)</template>
- </FormKeyValueView>
- </FormGroup>
- </FormSuspense>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
-import FormObjectView from '@/components/debobigego/object-view.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
-import * as os from '@/os';
-import number from '@/filters/number';
-import bytes from '@/filters/bytes';
-import * as symbols from '@/symbols';
-import { url } from '@/config';
-
-export default defineComponent({
- components: {
- FormBase,
- FormTextarea,
- FormObjectView,
- FormButton,
- FormLink,
- FormGroup,
- FormKeyValueView,
- FormSuspense,
- },
-
- props: {
- userId: {
- type: String,
- required: true
- }
- },
-
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: this.$ts.userInfo,
- icon: 'fas fa-info-circle'
- },
- user: null,
- apPromiseFactory: null,
- }
- },
-
- mounted() {
- this.fetch();
- },
-
- methods: {
- number,
- bytes,
-
- async fetch() {
- this.user = await os.api('users/show', {
- userId: this.userId
- });
-
- this.apPromiseFactory = () => os.api('ap/get', {
- uri: this.user.uri || `${url}/users/${this.user.id}`
- });
- }
- }
-});
-</script>
diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue
index 0fd208a64a..4bdc82f601 100644
--- a/packages/client/src/pages/user-info.vue
+++ b/packages/client/src/pages/user-info.vue
@@ -1,70 +1,75 @@
<template>
-<FormBase>
+<MkSpacer :content-max="500" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
- <div class="_debobigegoItem aeakzknw">
- <MkAvatar class="avatar" :user="user" :show-indicator="true"/>
- </div>
-
- <FormLink :to="userPage(user)">Profile</FormLink>
+ <div class="_formRoot">
+ <div class="_formBlock aeakzknw">
+ <MkAvatar class="avatar" :user="user" :show-indicator="true"/>
+ </div>
- <FormGroup>
- <FormKeyValueView>
- <template #key>Acct</template>
- <template #value><span class="_monospace">{{ acct(user) }}</span></template>
- </FormKeyValueView>
+ <FormLink :to="userPage(user)">Profile</FormLink>
- <FormKeyValueView>
- <template #key>ID</template>
- <template #value><span class="_monospace">{{ user.id }}</span></template>
- </FormKeyValueView>
- </FormGroup>
+ <div class="_formBlock">
+ <MkKeyValue :copy="acct(user)" oneline style="margin: 1em 0;">
+ <template #key>Acct</template>
+ <template #value><span class="_monospace">{{ acct(user) }}</span></template>
+ </MkKeyValue>
- <FormGroup v-if="iAmModerator">
- <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch>
- <FormSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
- <FormSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch>
- </FormGroup>
+ <MkKeyValue :copy="user.id" oneline style="margin: 1em 0;">
+ <template #key>ID</template>
+ <template #value><span class="_monospace">{{ user.id }}</span></template>
+ </MkKeyValue>
+ </div>
- <FormGroup>
- <FormButton v-if="user.host != null" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
- <FormButton v-if="user.host == null && iAmModerator" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
- </FormGroup>
+ <FormSection v-if="iAmModerator">
+ <template #label>Moderation</template>
+ <FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch>
+ <FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
+ <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch>
+ <FormButton v-if="user.host == null && iAmModerator" class="_formBlock" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
+ </FormSection>
- <FormGroup>
- <FormLink :to="`/user-ap-info/${user.id}`">ActivityPub</FormLink>
+ <FormSection>
+ <template #label>ActivityPub</template>
- <FormLink v-if="user.host" :to="`/instance-info/${user.host}`">{{ $ts.instanceInfo }}<template #suffix>{{ user.host }}</template></FormLink>
- <FormKeyValueView v-else>
- <template #key>{{ $ts.instanceInfo }}</template>
- <template #value>(Local user)</template>
- </FormKeyValueView>
- </FormGroup>
+ <div class="_formBlock">
+ <MkKeyValue v-if="user.host" oneline style="margin: 1em 0;">
+ <template #key>{{ $ts.instanceInfo }}</template>
+ <template #value><MkA :to="`/instance-info/${user.host}`" class="_link">{{ user.host }} <i class="fas fa-angle-right"></i></MkA></template>
+ </MkKeyValue>
+ <MkKeyValue v-else oneline style="margin: 1em 0;">
+ <template #key>{{ $ts.instanceInfo }}</template>
+ <template #value>(Local user)</template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>{{ $ts.updatedAt }}</template>
+ <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
+ </MkKeyValue>
+ <MkKeyValue v-if="ap" oneline style="margin: 1em 0;">
+ <template #key>Type</template>
+ <template #value><span class="_monospace">{{ ap.type }}</span></template>
+ </MkKeyValue>
+ </div>
- <FormGroup>
- <FormKeyValueView>
- <template #key>{{ $ts.updatedAt }}</template>
- <template #value><MkTime v-if="user.lastFetchedAt" mode="detail" :time="user.lastFetchedAt"/><span v-else>N/A</span></template>
- </FormKeyValueView>
- </FormGroup>
+ <FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
+ </FormSection>
- <FormObjectView tall :value="user">
- <span>Raw</span>
- </FormObjectView>
+ <MkObjectView tall :value="user">
+ </MkObjectView>
+ </div>
</FormSuspense>
-</FormBase>
+</MkSpacer>
</template>
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent } from 'vue';
-import FormObjectView from '@/components/debobigego/object-view.vue';
-import FormTextarea from '@/components/debobigego/textarea.vue';
-import FormSwitch from '@/components/debobigego/switch.vue';
-import FormLink from '@/components/debobigego/link.vue';
-import FormBase from '@/components/debobigego/base.vue';
-import FormGroup from '@/components/debobigego/group.vue';
-import FormButton from '@/components/debobigego/button.vue';
-import FormKeyValueView from '@/components/debobigego/key-value-view.vue';
-import FormSuspense from '@/components/debobigego/suspense.vue';
+import MkObjectView from '@/components/object-view.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormLink from '@/components/form/link.vue';
+import FormSection from '@/components/form/section.vue';
+import FormButton from '@/components/ui/button.vue';
+import MkKeyValue from '@/components/key-value.vue';
+import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
@@ -74,14 +79,13 @@ import { userPage, acct } from '@/filters/user';
export default defineComponent({
components: {
- FormBase,
+ FormSection,
FormTextarea,
FormSwitch,
- FormObjectView,
+ MkObjectView,
FormButton,
FormLink,
- FormGroup,
- FormKeyValueView,
+ MkKeyValue,
FormSuspense,
},
@@ -97,6 +101,7 @@ export default defineComponent({
[symbols.PAGE_INFO]: computed(() => ({
title: this.user ? acct(this.user) : this.$ts.userInfo,
icon: 'fas fa-info-circle',
+ bg: 'var(--bg)',
actions: this.user ? [this.user.url ? {
text: this.user.url,
icon: 'fas fa-external-link-alt',
@@ -108,6 +113,7 @@ export default defineComponent({
init: null,
user: null,
info: null,
+ ap: null,
moderator: false,
silenced: false,
suspended: false,
@@ -126,6 +132,13 @@ export default defineComponent({
this.init = this.createFetcher();
},
immediate: true
+ },
+ user() {
+ os.api('ap/get', {
+ uri: this.user.uri || `${url}/users/${this.user.id}`
+ }).then(res => {
+ this.ap = res;
+ });
}
},
@@ -234,7 +247,6 @@ export default defineComponent({
.aeakzknw {
> .avatar {
display: block;
- margin: 0 auto;
width: 64px;
height: 64px;
}
diff --git a/packages/client/src/pages/user/clips.vue b/packages/client/src/pages/user/clips.vue
index aad5317ce0..870e6f7174 100644
--- a/packages/client/src/pages/user/clips.vue
+++ b/packages/client/src/pages/user/clips.vue
@@ -28,7 +28,7 @@ export default defineComponent({
data() {
return {
pagination: {
- endpoint: 'users/clips',
+ endpoint: 'users/clips' as const,
limit: 20,
params: {
userId: this.user.id,
diff --git a/packages/client/src/pages/user/follow-list.vue b/packages/client/src/pages/user/follow-list.vue
index 9fb8943fb8..98a1fc0f86 100644
--- a/packages/client/src/pages/user/follow-list.vue
+++ b/packages/client/src/pages/user/follow-list.vue
@@ -1,6 +1,6 @@
<template>
<div>
- <MkPagination v-slot="{items}" ref="list" :pagination="pagination" class="mk-following-or-followers">
+ <MkPagination v-slot="{items}" ref="list" :pagination="type === 'following' ? followingPagination : followersPagination" class="mk-following-or-followers">
<div class="users _isolated">
<MkUserInfo v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :key="user.id" class="user" :user="user"/>
</div>
@@ -8,50 +8,32 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
import MkUserInfo from '@/components/user-info.vue';
import MkPagination from '@/components/ui/pagination.vue';
-export default defineComponent({
- components: {
- MkPagination,
- MkUserInfo,
- },
+const props = defineProps<{
+ user: misskey.entities.User;
+ type: 'following' | 'followers';
+}>();
- props: {
- user: {
- type: Object,
- required: true
- },
- type: {
- type: String,
- required: true
- },
- },
+const followingPagination = {
+ endpoint: 'users/following' as const,
+ limit: 20,
+ params: computed(() => ({
+ userId: props.user.id,
+ })),
+};
- data() {
- return {
- pagination: {
- endpoint: () => this.type === 'following' ? 'users/following' : 'users/followers',
- limit: 20,
- params: {
- userId: this.user.id,
- }
- },
- };
- },
-
- watch: {
- type() {
- this.$refs.list.reload();
- },
-
- user() {
- this.$refs.list.reload();
- }
- }
-});
+const followersPagination = {
+ endpoint: 'users/followers' as const,
+ limit: 20,
+ params: computed(() => ({
+ userId: props.user.id,
+ })),
+};
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue
index 860aa9f44f..07dda4a292 100644
--- a/packages/client/src/pages/user/gallery.vue
+++ b/packages/client/src/pages/user/gallery.vue
@@ -9,7 +9,7 @@
</template>
<script lang="ts">
-import { defineComponent } from 'vue';
+import { computed, defineComponent } from 'vue';
import MkGalleryPostPreview from '@/components/gallery-post-preview.vue';
import MkPagination from '@/components/ui/pagination.vue';
@@ -29,20 +29,14 @@ export default defineComponent({
data() {
return {
pagination: {
- endpoint: 'users/gallery/posts',
+ endpoint: 'users/gallery/posts' as const,
limit: 6,
- params: () => ({
+ params: computed(() => ({
userId: this.user.id
- })
+ })),
},
};
},
-
- watch: {
- user() {
- this.$refs.list.reload();
- }
- }
});
</script>
diff --git a/packages/client/src/pages/user/index.activity.vue b/packages/client/src/pages/user/index.activity.vue
index e51d6c6090..43a4f476f1 100644
--- a/packages/client/src/pages/user/index.activity.vue
+++ b/packages/client/src/pages/user/index.activity.vue
@@ -8,27 +8,16 @@
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
import MkContainer from '@/components/ui/container.vue';
import MkChart from '@/components/chart.vue';
-export default defineComponent({
- components: {
- MkContainer,
- MkChart,
- },
- props: {
- user: {
- type: Object,
- required: true
- },
- limit: {
- type: Number,
- required: false,
- default: 40
- }
- },
+const props = withDefaults(defineProps<{
+ user: misskey.entities.User;
+ limit?: number;
+}>(), {
+ limit: 40,
});
</script>
diff --git a/packages/client/src/pages/user/index.timeline.vue b/packages/client/src/pages/user/index.timeline.vue
index 2ffa496979..a1329a7411 100644
--- a/packages/client/src/pages/user/index.timeline.vue
+++ b/packages/client/src/pages/user/index.timeline.vue
@@ -1,60 +1,36 @@
<template>
<div v-sticky-container class="yrzkoczt">
- <MkTab v-model="with_" class="tab">
+ <MkTab v-model="include" class="tab">
<option :value="null">{{ $ts.notes }}</option>
<option value="replies">{{ $ts.notesAndReplies }}</option>
<option value="files">{{ $ts.withFiles }}</option>
</MkTab>
- <XNotes ref="timeline" :no-gap="true" :pagination="pagination" @before="$emit('before')" @after="e => $emit('after', e)"/>
+ <XNotes :no-gap="true" :pagination="pagination"/>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { ref, computed } from 'vue';
+import * as misskey from 'misskey-js';
import XNotes from '@/components/notes.vue';
import MkTab from '@/components/tab.vue';
import * as os from '@/os';
-export default defineComponent({
- components: {
- XNotes,
- MkTab,
- },
+const props = defineProps<{
+ user: misskey.entities.UserDetailed;
+}>();
- props: {
- user: {
- type: Object,
- required: true,
- },
- },
+const include = ref<string | null>(null);
- data() {
- return {
- date: null,
- with_: null,
- pagination: {
- endpoint: 'users/notes',
- limit: 10,
- params: init => ({
- userId: this.user.id,
- includeReplies: this.with_ === 'replies',
- withFiles: this.with_ === 'files',
- untilDate: init ? undefined : (this.date ? this.date.getTime() : undefined),
- })
- }
- };
- },
-
- watch: {
- user() {
- this.$refs.timeline.reload();
- },
-
- with_() {
- this.$refs.timeline.reload();
- },
- },
-});
+const pagination = {
+ endpoint: 'users/notes' as const,
+ limit: 10,
+ params: computed(() => ({
+ userId: props.user.id,
+ includeReplies: include.value === 'replies',
+ withFiles: include.value === 'files',
+ })),
+};
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue
index 0b96368587..599e24d81c 100644
--- a/packages/client/src/pages/user/index.vue
+++ b/packages/client/src/pages/user/index.vue
@@ -1,196 +1,125 @@
<template>
<div>
-<transition name="fade" mode="out-in">
- <div v-if="user && narrow === false" class="ftskorzw wide">
- <MkRemoteCaution v-if="user.host != null" :href="user.url"/>
+ <transition name="fade" mode="out-in">
+ <MkSpacer v-if="user" :content-max="narrow ? 800 : 1100">
+ <div v-size="{ max: [500] }" class="ftskorzw" :class="{ wide: !narrow }">
+ <div class="main">
+ <!-- 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="banner-container" :style="style">
- <div ref="banner" class="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 v-if="$i && $i.id != user.id && user.isFollowed" class="followed"><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 v-if="user.location" class="field">
- <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
- <dd class="value">{{ user.location }}</dd>
- </dl>
- <dl v-if="user.birthday" class="field">
- <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 v-if="user.fields.length > 0" class="fields">
- <dl v-for="(field, i) in user.fields" :key="i" class="field">
- <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 :key="user.id" :user="user" class="_gap"/>
- <XPhotos :key="user.id" :user="user" class="_gap"/>
- </div>
- <div class="main">
- <div class="actions">
- <button class="menu _button" @click="menu"><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" :key="note.id" class="note _gap" :note="note" :pinned="true" @update:note="pinnedNoteUpdated(note, $event)"/>
- </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>
- <MkSpacer v-else-if="user && narrow === true" :content-max="800">
- <div v-size="{ max: [500] }" class="ftskorzw narrow">
- <!-- 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 :key="user.id" class="_block main">
- <div class="banner-container" :style="style">
- <div ref="banner" class="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 :key="user.id" class="_block main">
+ <div class="banner-container" :style="style">
+ <div ref="banner" class="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 v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
+ <div v-if="$i" class="actions">
+ <button class="menu _button" @click="menu"><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 :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>
+ </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 v-if="user.location" class="field">
+ <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
+ <dd class="value">{{ user.location }}</dd>
+ </dl>
+ <dl v-if="user.birthday" class="field">
+ <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 v-if="user.fields.length > 0" class="fields">
+ <dl v-for="(field, i) in user.fields" :key="i" class="field">
+ <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 v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
+ <b>{{ number(user.notesCount) }}</b>
+ <span>{{ $ts.notes }}</span>
+ </MkA>
+ <MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
+ <b>{{ number(user.followingCount) }}</b>
+ <span>{{ $ts.following }}</span>
+ </MkA>
+ <MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
+ <b>{{ number(user.followersCount) }}</b>
+ <span>{{ $ts.followers }}</span>
+ </MkA>
</div>
- </div>
- <span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
- <div v-if="$i" class="actions">
- <button class="menu _button" @click="menu"><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 :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>
</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 v-if="user.location" class="field">
- <dt class="name"><i class="fas fa-map-marker fa-fw"></i> {{ $ts.location }}</dt>
- <dd class="value">{{ user.location }}</dd>
- </dl>
- <dl v-if="user.birthday" class="field">
- <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 v-if="user.fields.length > 0" class="fields">
- <dl v-for="(field, i) in user.fields" :key="i" class="field">
- <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 v-click-anime :to="userPage(user)" :class="{ active: page === 'index' }">
- <b>{{ number(user.notesCount) }}</b>
- <span>{{ $ts.notes }}</span>
- </MkA>
- <MkA v-click-anime :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
- <b>{{ number(user.followingCount) }}</b>
- <span>{{ $ts.following }}</span>
- </MkA>
- <MkA v-click-anime :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
- <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" :key="note.id" class="note _block" :note="note" :pinned="true" @update:note="pinnedNoteUpdated(note, $event)"/>
- </div>
- <MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
- <XPhotos :key="user.id" :user="user"/>
- <XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
- </div>
- <div>
- <XUserTimeline :user="user"/>
+ <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" :key="note.id" class="note _block" :note="note" :pinned="true"/>
+ </div>
+ <MkInfo v-else-if="$i && $i.id === user.id">{{ $ts.userPagePinTip }}</MkInfo>
+ <template v-if="narrow">
+ <XPhotos :key="user.id" :user="user"/>
+ <XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+ </template>
+ </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"/>
+ <XReactions v-else-if="page === 'reactions'" :user="user" class="_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>
- </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"/>
- <XReactions v-else-if="page === 'reactions'" :user="user" class="_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 v-if="!narrow" class="sub">
+ <XPhotos :key="user.id" :user="user"/>
+ <XActivity :key="user.id" :user="user" style="margin-top: var(--margin);"/>
+ </div>
</div>
- </div>
- </MkSpacer>
- <MkError v-else-if="error" @retry="fetch()"/>
- <MkLoading v-else/>
-</transition>
+ </MkSpacer>
+ <MkError v-else-if="error" @retry="fetch()"/>
+ <MkLoading v-else/>
+ </transition>
</div>
</template>
@@ -314,7 +243,7 @@ export default defineComponent({
mounted() {
window.requestAnimationFrame(this.parallaxLoop);
- this.narrow = true//this.$el.clientWidth < 1000;
+ this.narrow = this.$el.clientWidth < 1000;
},
beforeUnmount() {
@@ -356,11 +285,6 @@ export default defineComponent({
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
},
- pinnedNoteUpdated(oldValue, newValue) {
- const i = this.user.pinnedNotes.findIndex(n => n === oldValue);
- this.user.pinnedNotes[i] = newValue;
- },
-
number,
userPage
@@ -378,447 +302,289 @@ export default defineComponent({
opacity: 0;
}
-.ftskorzw.wide {
+.ftskorzw {
- > .banner-container {
- position: relative;
- height: 300px;
- overflow: hidden;
- background-size: cover;
- background-position: center;
+ > .main {
- > .banner {
- height: 100%;
- background-color: #4c5e6d;
- background-size: cover;
- background-position: center;
- box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
- will-change: background-position;
+ > .punished {
+ font-size: 0.8em;
+ padding: 16px;
}
- }
-
- > .contents {
- display: flex;
- padding: 16px;
-
- > .side {
- width: 360px;
- > .avatar {
- display: block;
- width: 180px;
- height: 180px;
- margin: -130px auto 0 auto;
- }
-
- > .name {
- padding: 16px 0px 20px 0;
- text-align: center;
-
- > .name {
- display: block;
- font-size: 1.75em;
- font-weight: bold;
- }
- }
-
- > .followed {
- text-align: center;
-
- > span {
- display: inline-block;
- font-size: 80%;
- padding: 8px 12px;
- margin-bottom: 20px;
- border: solid 0.5px var(--divider);
- border-radius: 999px;
- }
- }
+ > .profile {
- > .status {
- display: flex;
- padding: 20px 16px;
- border-top: solid 0.5px var(--divider);
- font-size: 90%;
+ > .main {
+ position: relative;
+ overflow: hidden;
- > a {
- flex: 1;
- text-align: center;
+ > .banner-container {
+ position: relative;
+ height: 250px;
+ overflow: hidden;
+ background-size: cover;
+ background-position: center;
- &.active {
- color: var(--accent);
+ > .banner {
+ height: 100%;
+ background-color: #4c5e6d;
+ background-size: cover;
+ background-position: center;
+ box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
+ will-change: background-position;
}
- &:hover {
- text-decoration: none;
+ > .fade {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 78px;
+ background: linear-gradient(transparent, rgba(#000, 0.7));
}
- > b {
- display: block;
- line-height: 16px;
+ > .followed {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ padding: 4px 8px;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.7);
+ font-size: 0.7em;
+ border-radius: 6px;
}
- > span {
- font-size: 75%;
- }
- }
- }
+ > .actions {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ -webkit-backdrop-filter: var(--blur, blur(8px));
+ backdrop-filter: var(--blur, blur(8px));
+ background: rgba(0, 0, 0, 0.2);
+ padding: 8px;
+ border-radius: 24px;
- > .description {
- padding: 20px 16px;
- border-top: solid 0.5px var(--divider);
- font-size: 90%;
- }
+ > .menu {
+ vertical-align: bottom;
+ height: 31px;
+ width: 31px;
+ color: #fff;
+ text-shadow: 0 0 8px #000;
+ font-size: 16px;
+ }
- > .fields {
- padding: 20px 16px;
- border-top: solid 0.5px var(--divider);
- font-size: 90%;
+ > .koudoku {
+ margin-left: 4px;
+ vertical-align: bottom;
+ }
+ }
- > .field {
- display: flex;
- padding: 0;
- margin: 0;
- align-items: center;
+ > .title {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ padding: 0 0 8px 154px;
+ box-sizing: border-box;
+ color: #fff;
- &:not(:last-child) {
- margin-bottom: 8px;
- }
+ > .name {
+ display: block;
+ margin: 0;
+ line-height: 32px;
+ font-weight: bold;
+ font-size: 1.8em;
+ text-shadow: 0 0 8px #000;
+ }
- > .name {
- width: 30%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- font-weight: bold;
- }
+ > .bottom {
+ > * {
+ display: inline-block;
+ margin-right: 16px;
+ line-height: 20px;
+ opacity: 0.8;
- > .value {
- width: 70%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- margin: 0;
+ &.username {
+ font-weight: bold;
+ }
+ }
+ }
}
}
- }
- }
- > .main {
- flex: 1;
- margin-left: var(--margin);
- min-width: 0;
-
- > .nav {
- display: flex;
- align-items: center;
- margin-top: var(--margin);
- //font-size: 120%;
- font-weight: bold;
-
- > .link {
- display: inline-block;
- padding: 15px 24px 12px 24px;
+ > .title {
+ display: none;
text-align: center;
- border-bottom: solid 3px transparent;
-
- &:hover {
- text-decoration: none;
- }
-
- &.active {
- color: var(--accent);
- border-bottom-color: var(--accent);
- }
+ padding: 50px 8px 16px 8px;
+ font-weight: bold;
+ border-bottom: solid 0.5px var(--divider);
- &:not(.active):hover {
- color: var(--fgHighlighted);
+ > .bottom {
+ > * {
+ display: inline-block;
+ margin-right: 8px;
+ opacity: 0.8;
+ }
}
+ }
- > .icon {
- margin-right: 6px;
- }
+ > .avatar {
+ display: block;
+ position: absolute;
+ top: 170px;
+ left: 16px;
+ z-index: 2;
+ width: 120px;
+ height: 120px;
+ box-shadow: 1px 1px 3px rgba(#000, 0.2);
}
- > .actions {
- display: flex;
- align-items: center;
- margin-left: auto;
+ > .description {
+ padding: 24px 24px 24px 154px;
+ font-size: 0.95em;
- > .menu {
- padding: 12px 16px;
+ > .empty {
+ margin: 0;
+ opacity: 0.5;
}
}
- }
- }
- }
-}
-
-.ftskorzw.narrow {
- box-sizing: border-box;
- overflow: clip;
- background: var(--bg);
-
- > .punished {
- font-size: 0.8em;
- padding: 16px;
- }
-
- > .profile {
-
- > .main {
- position: relative;
- overflow: hidden;
- > .banner-container {
- position: relative;
- height: 250px;
- overflow: hidden;
- background-size: cover;
- background-position: center;
-
- > .banner {
- height: 100%;
- background-color: #4c5e6d;
- background-size: cover;
- background-position: center;
- box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
- will-change: background-position;
- }
+ > .fields {
+ padding: 24px;
+ font-size: 0.9em;
+ border-top: solid 0.5px var(--divider);
- > .fade {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 78px;
- background: linear-gradient(transparent, rgba(#000, 0.7));
- }
+ > .field {
+ display: flex;
+ padding: 0;
+ margin: 0;
+ align-items: center;
- > .followed {
- position: absolute;
- top: 12px;
- left: 12px;
- padding: 4px 8px;
- color: #fff;
- background: rgba(0, 0, 0, 0.7);
- font-size: 0.7em;
- border-radius: 6px;
- }
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
- > .actions {
- position: absolute;
- top: 12px;
- right: 12px;
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
- background: rgba(0, 0, 0, 0.2);
- padding: 8px;
- border-radius: 24px;
+ > .name {
+ width: 30%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-weight: bold;
+ text-align: center;
+ }
- > .menu {
- vertical-align: bottom;
- height: 31px;
- width: 31px;
- color: #fff;
- text-shadow: 0 0 8px #000;
- font-size: 16px;
+ > .value {
+ width: 70%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ margin: 0;
+ }
}
- > .koudoku {
- margin-left: 4px;
- vertical-align: bottom;
+ &.system > .field > .name {
}
}
- > .title {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- padding: 0 0 8px 154px;
- box-sizing: border-box;
- color: #fff;
+ > .status {
+ display: flex;
+ padding: 24px;
+ border-top: solid 0.5px var(--divider);
- > .name {
- display: block;
- margin: 0;
- line-height: 32px;
- font-weight: bold;
- font-size: 1.8em;
- text-shadow: 0 0 8px #000;
- }
+ > a {
+ flex: 1;
+ text-align: center;
- > .bottom {
- > * {
- display: inline-block;
- margin-right: 16px;
- line-height: 20px;
- opacity: 0.8;
+ &.active {
+ color: var(--accent);
+ }
- &.username {
- font-weight: bold;
- }
+ &:hover {
+ text-decoration: none;
}
- }
- }
- }
- > .title {
- display: none;
- text-align: center;
- padding: 50px 8px 16px 8px;
- font-weight: bold;
- border-bottom: solid 0.5px var(--divider);
+ > b {
+ display: block;
+ line-height: 16px;
+ }
- > .bottom {
- > * {
- display: inline-block;
- margin-right: 8px;
- opacity: 0.8;
+ > span {
+ font-size: 70%;
+ }
}
}
}
+ }
- > .avatar {
- display: block;
- position: absolute;
- top: 170px;
- left: 16px;
- z-index: 2;
- width: 120px;
- height: 120px;
- box-shadow: 1px 1px 3px rgba(#000, 0.2);
- }
-
- > .description {
- padding: 24px 24px 24px 154px;
- font-size: 0.95em;
-
- > .empty {
- margin: 0;
- opacity: 0.5;
- }
+ > .contents {
+ > .content {
+ margin-bottom: var(--margin);
}
+ }
+ }
- > .fields {
- padding: 24px;
- font-size: 0.9em;
- border-top: solid 0.5px var(--divider);
-
- > .field {
- display: flex;
- padding: 0;
- margin: 0;
- align-items: center;
+ &.max-width_500px {
+ > .main {
+ > .profile > .main {
+ > .banner-container {
+ height: 140px;
- &:not(:last-child) {
- margin-bottom: 8px;
+ > .fade {
+ display: none;
}
- > .name {
- width: 30%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- font-weight: bold;
- text-align: center;
- }
-
- > .value {
- width: 70%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- margin: 0;
+ > .title {
+ display: none;
}
}
- &.system > .field > .name {
+ > .title {
+ display: block;
}
- }
- > .status {
- display: flex;
- padding: 24px;
- border-top: solid 0.5px var(--divider);
+ > .avatar {
+ top: 90px;
+ left: 0;
+ right: 0;
+ width: 92px;
+ height: 92px;
+ margin: auto;
+ }
- > a {
- flex: 1;
+ > .description {
+ padding: 16px;
text-align: center;
-
- &.active {
- color: var(--accent);
- }
-
- &:hover {
- text-decoration: none;
- }
-
- > b {
- display: block;
- line-height: 16px;
- }
-
- > span {
- font-size: 70%;
- }
}
- }
- }
- }
- > .contents {
- > .content {
- margin-bottom: var(--margin);
- }
- }
-
- &.max-width_500px {
- > .profile > .main {
- > .banner-container {
- height: 140px;
-
- > .fade {
- display: none;
+ > .fields {
+ padding: 16px;
}
- > .title {
- display: none;
+ > .status {
+ padding: 16px;
}
}
- > .title {
- display: block;
- }
-
- > .avatar {
- top: 90px;
- left: 0;
- right: 0;
- width: 92px;
- height: 92px;
- margin: auto;
- }
-
- > .description {
- padding: 16px;
- text-align: center;
+ > .contents {
+ > .nav {
+ font-size: 80%;
+ }
}
+ }
+ }
- > .fields {
- padding: 16px;
- }
+ &.wide {
+ display: flex;
+ width: 100%;
- > .status {
- padding: 16px;
- }
+ > .main {
+ width: 100%;
+ min-width: 0;
}
- > .contents {
- > .nav {
- font-size: 80%;
- }
+ > .sub {
+ max-width: 350px;
+ min-width: 350px;
+ margin-left: var(--margin);
}
}
}
diff --git a/packages/client/src/pages/user/pages.vue b/packages/client/src/pages/user/pages.vue
index 40d1fe3842..ad101158e0 100644
--- a/packages/client/src/pages/user/pages.vue
+++ b/packages/client/src/pages/user/pages.vue
@@ -6,42 +6,23 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
import MkPagePreview from '@/components/page-preview.vue';
import MkPagination from '@/components/ui/pagination.vue';
-export default defineComponent({
- components: {
- MkPagination,
- MkPagePreview,
- },
+const props = defineProps<{
+ user: misskey.entities.User;
+}>();
- props: {
- user: {
- type: Object,
- required: true
- },
- },
-
- data() {
- return {
- pagination: {
- endpoint: 'users/pages',
- limit: 20,
- params: {
- userId: this.user.id,
- }
- },
- };
- },
-
- watch: {
- user() {
- this.$refs.list.reload();
- }
- }
-});
+const pagination = {
+ endpoint: 'users/pages' as const,
+ limit: 20,
+ params: computed(() => ({
+ userId: props.user.id,
+ })),
+};
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/user/reactions.vue b/packages/client/src/pages/user/reactions.vue
index 69c27de55b..d2c1f92ebb 100644
--- a/packages/client/src/pages/user/reactions.vue
+++ b/packages/client/src/pages/user/reactions.vue
@@ -7,50 +7,30 @@
<MkReactionIcon class="reaction" :reaction="item.type" :custom-emojis="item.note.emojis" :no-style="true"/>
<MkTime :time="item.createdAt" class="createdAt"/>
</div>
- <MkNote :key="item.id" :note="item.note" @update:note="updated(note, $event)"/>
+ <MkNote :key="item.id" :note="item.note"/>
</div>
</MkPagination>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
+import * as misskey from 'misskey-js';
import MkPagination from '@/components/ui/pagination.vue';
import MkNote from '@/components/note.vue';
import MkReactionIcon from '@/components/reaction-icon.vue';
-export default defineComponent({
- components: {
- MkPagination,
- MkNote,
- MkReactionIcon,
- },
+const props = defineProps<{
+ user: misskey.entities.User;
+}>();
- props: {
- user: {
- type: Object,
- required: true
- },
- },
-
- data() {
- return {
- pagination: {
- endpoint: 'users/reactions',
- limit: 20,
- params: {
- userId: this.user.id,
- }
- },
- };
- },
-
- watch: {
- user() {
- this.$refs.list.reload();
- }
- },
-});
+const pagination = {
+ endpoint: 'users/reactions' as const,
+ limit: 20,
+ params: computed(() => ({
+ userId: props.user.id,
+ })),
+};
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/pages/v.vue b/packages/client/src/pages/v.vue
deleted file mode 100644
index 3b1bb20861..0000000000
--- a/packages/client/src/pages/v.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<template>
-<div>
- <section class="_section">
- <div class="_content" style="text-align: center;">
- <img src="/static-assets/icons/512.png" alt="" style="display: block; width: 100px; margin: 0 auto; border-radius: 16px;"/>
- <div style="margin-top: 0.75em;">Misskey</div>
- <div style="opacity: 0.5;">v{{ version }}</div>
- </div>
- </section>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { version } from '@/config';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- data() {
- return {
- [symbols.PAGE_INFO]: {
- title: 'Misskey',
- icon: null
- },
- version,
- }
- },
-});
-</script>
diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts
index 396abc2418..fa35df5511 100644
--- a/packages/client/src/pizzax.ts
+++ b/packages/client/src/pizzax.ts
@@ -1,6 +1,7 @@
import { onUnmounted, Ref, ref, watch } from 'vue';
import { $i } from './account';
import { api } from './os';
+import { stream } from './stream';
type StateDef = Record<string, {
where: 'account' | 'device' | 'deviceAccount';
@@ -9,6 +10,8 @@ type StateDef = Record<string, {
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
+const connection = $i && stream.useChannel('main');
+
export class Storage<T extends StateDef> {
public readonly key: string;
public readonly keyForLocalStorage: string;
@@ -51,7 +54,7 @@ export class Storage<T extends StateDef> {
if ($i) {
// なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
- setTimeout(() => {
+ window.setTimeout(() => {
api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => {
const cache = {};
for (const [k, v] of Object.entries(def)) {
@@ -69,8 +72,19 @@ export class Storage<T extends StateDef> {
localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
});
}, 1);
+ // streamingのuser storage updateイベントを監視して更新
+ connection?.on('registryUpdated', ({ scope, key, value }: { scope: string[], key: keyof T, value: T[typeof key]['default'] }) => {
+ if (scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
+
+ this.state[key] = value;
+ this.reactiveState[key].value = value;
- // TODO: streamingのuser storage updateイベントを監視して更新
+ const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
+ if (cache[key] !== value) {
+ cache[key] = value;
+ localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
+ }
+ });
}
}
diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts
index 9b4dd162f3..ec48b76fdf 100644
--- a/packages/client/src/router.ts
+++ b/packages/client/src/router.ts
@@ -20,7 +20,6 @@ const defaultRoutes = [
{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
- { path: '/@:acct/room', props: true, component: page('room/room') },
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ 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 }) },
@@ -34,7 +33,7 @@ const defaultRoutes = [
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
{ path: '/federation', component: page('federation') },
{ path: '/emojis', component: page('emojis') },
- { path: '/search', component: page('search') },
+ { path: '/search', component: page('search'), props: route => ({ query: route.query.q, channel: route.query.channel }) },
{ path: '/pages', name: 'pages', component: page('pages') },
{ path: '/pages/new', component: page('page-editor/page-editor') },
{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
@@ -73,10 +72,7 @@ const defaultRoutes = [
{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
- { path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) },
{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
- { path: '/games/reversi', component: page('reversi/index') },
- { path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
{ path: '/api-console', component: page('api-console') },
{ path: '/preview', component: page('preview') },
@@ -119,11 +115,11 @@ export const router = createRouter({
window._scroll = () => { // さらにHacky
if (to.name === 'index') {
window.scroll({ top: indexScrollPos, behavior: 'instant' });
- const i = setInterval(() => {
+ const i = window.setInterval(() => {
window.scroll({ top: indexScrollPos, behavior: 'instant' });
}, 10);
- setTimeout(() => {
- clearInterval(i);
+ window.setTimeout(() => {
+ window.clearInterval(i);
}, 500);
} else {
window.scroll({ top: 0, behavior: 'instant' });
diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts
index f2d5806484..f4a3a4c0fc 100644
--- a/packages/client/src/scripts/autocomplete.ts
+++ b/packages/client/src/scripts/autocomplete.ts
@@ -1,4 +1,4 @@
-import { Ref, ref } from 'vue';
+import { nextTick, Ref, ref } from 'vue';
import * as getCaretCoordinates from 'textarea-caret';
import { toASCII } from 'punycode/';
import { popup } from '@/os';
@@ -10,26 +10,23 @@ export class Autocomplete {
q: Ref<string | null>;
close: Function;
} | null;
- private textarea: any;
- private vm: any;
+ private textarea: HTMLInputElement | HTMLTextAreaElement;
private currentType: string;
- private opts: {
- model: string;
- };
+ private textRef: Ref<string>;
private opening: boolean;
private get text(): string {
- return this.vm[this.opts.model];
+ return this.textRef.value;
}
private set text(text: string) {
- this.vm[this.opts.model] = text;
+ this.textRef.value = text;
}
/**
* 対象のテキストエリアを与えてインスタンスを初期化します。
*/
- constructor(textarea, vm, opts) {
+ constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
//#region BIND
this.onInput = this.onInput.bind(this);
this.complete = this.complete.bind(this);
@@ -38,8 +35,7 @@ export class Autocomplete {
this.suggestion = null;
this.textarea = textarea;
- this.vm = vm;
- this.opts = opts;
+ this.textRef = textRef;
this.opening = false;
this.attach();
@@ -218,7 +214,7 @@ export class Autocomplete {
this.text = `${trimmedBefore}@${acct} ${after}`;
// キャレットを戻す
- this.vm.$nextTick(() => {
+ nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (acct.length + 2);
this.textarea.setSelectionRange(pos, pos);
@@ -234,7 +230,7 @@ export class Autocomplete {
this.text = `${trimmedBefore}#${value} ${after}`;
// キャレットを戻す
- this.vm.$nextTick(() => {
+ nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 2);
this.textarea.setSelectionRange(pos, pos);
@@ -250,7 +246,7 @@ export class Autocomplete {
this.text = trimmedBefore + value + after;
// キャレットを戻す
- this.vm.$nextTick(() => {
+ nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + value.length;
this.textarea.setSelectionRange(pos, pos);
@@ -266,7 +262,7 @@ export class Autocomplete {
this.text = `${trimmedBefore}$[${value} ]${after}`;
// キャレットを戻す
- this.vm.$nextTick(() => {
+ nextTick(() => {
this.textarea.focus();
const pos = trimmedBefore.length + (value.length + 3);
this.textarea.setSelectionRange(pos, pos);
diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts
index 3b1fa75b1e..55637bb3b3 100644
--- a/packages/client/src/scripts/check-word-mute.ts
+++ b/packages/client/src/scripts/check-word-mute.ts
@@ -1,4 +1,4 @@
-export async function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): Promise<boolean> {
+export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: string[][]): boolean {
// 自分自身
if (me && (note.userId === me.id)) return false;
diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts
index de7591f5a0..bd8689e4f8 100644
--- a/packages/client/src/scripts/emojilist.ts
+++ b/packages/client/src/scripts/emojilist.ts
@@ -1,7 +1,11 @@
-// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
-export const emojilist = require('../emojilist.json') as {
+export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const;
+
+export type UnicodeEmojiDef = {
name: string;
keywords: string[];
char: string;
- category: 'people' | 'animals_and_nature' | 'food_and_drink' | 'activity' | 'travel_and_places' | 'objects' | 'symbols' | 'flags';
-}[];
+ category: typeof unicodeEmojiCategories[number];
+}
+
+// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
+export const emojilist = require('../emojilist.json') as UnicodeEmojiDef[];
diff --git a/packages/client/src/scripts/form.ts b/packages/client/src/scripts/form.ts
index 7bf6cec452..7f321cc0ae 100644
--- a/packages/client/src/scripts/form.ts
+++ b/packages/client/src/scripts/form.ts
@@ -23,9 +23,37 @@ export type FormItem = {
enum: string[];
} | {
label?: string;
+ type: 'radio';
+ default: unknown | null;
+ hidden?: boolean;
+ options: {
+ label: string;
+ value: unknown;
+ }[];
+} | {
+ label?: string;
+ type: 'object';
+ default: Record<string, unknown> | null;
+ hidden: true;
+} | {
+ label?: string;
type: 'array';
default: unknown[] | null;
- hidden?: boolean;
+ hidden: true;
};
export type Form = Record<string, FormItem>;
+
+type GetItemType<Item extends FormItem> =
+ Item['type'] extends 'string' ? string :
+ Item['type'] extends 'number' ? number :
+ Item['type'] extends 'boolean' ? boolean :
+ Item['type'] extends 'radio' ? unknown :
+ Item['type'] extends 'enum' ? string :
+ Item['type'] extends 'array' ? unknown[] :
+ Item['type'] extends 'object' ? Record<string, unknown>
+ : never;
+
+export type GetFormResultType<F extends Form> = {
+ [P in keyof F]: GetItemType<F[P]>;
+};
diff --git a/packages/client/src/scripts/games/reversi/core.ts b/packages/client/src/scripts/games/reversi/core.ts
deleted file mode 100644
index 0cb8922e19..0000000000
--- a/packages/client/src/scripts/games/reversi/core.ts
+++ /dev/null
@@ -1,263 +0,0 @@
-import { count, concat } from '@/scripts/array';
-
-// MISSKEY REVERSI ENGINE
-
-/**
- * true ... 黒
- * false ... 白
- */
-export type Color = boolean;
-const BLACK = true;
-const WHITE = false;
-
-export type MapPixel = 'null' | 'empty';
-
-export type Options = {
- isLlotheo: boolean;
- canPutEverywhere: boolean;
- loopedBoard: boolean;
-};
-
-export type Undo = {
- /**
- * 色
- */
- color: Color;
-
- /**
- * どこに打ったか
- */
- pos: number;
-
- /**
- * 反転した石の位置の配列
- */
- effects: number[];
-
- /**
- * ターン
- */
- turn: Color | null;
-};
-
-/**
- * リバーシエンジン
- */
-export default class Reversi {
- public map: MapPixel[];
- public mapWidth: number;
- public mapHeight: number;
- public board: (Color | null | undefined)[];
- public turn: Color | null = BLACK;
- public opts: Options;
-
- public prevPos = -1;
- public prevColor: Color | null = null;
-
- private logs: Undo[] = [];
-
- /**
- * ゲームを初期化します
- */
- constructor(map: string[], opts: Options) {
- //#region binds
- this.put = this.put.bind(this);
- //#endregion
-
- //#region Options
- this.opts = opts;
- if (this.opts.isLlotheo == null) this.opts.isLlotheo = false;
- if (this.opts.canPutEverywhere == null) this.opts.canPutEverywhere = false;
- if (this.opts.loopedBoard == null) this.opts.loopedBoard = false;
- //#endregion
-
- //#region Parse map data
- this.mapWidth = map[0].length;
- this.mapHeight = map.length;
- const mapData = map.join('');
-
- this.board = mapData.split('').map(d => d === '-' ? null : d === 'b' ? BLACK : d === 'w' ? WHITE : undefined);
-
- this.map = mapData.split('').map(d => d === '-' || d === 'b' || d === 'w' ? 'empty' : 'null');
- //#endregion
-
- // ゲームが始まった時点で片方の色の石しかないか、始まった時点で勝敗が決定するようなマップの場合がある
- if (!this.canPutSomewhere(BLACK))
- this.turn = this.canPutSomewhere(WHITE) ? WHITE : null;
- }
-
- /**
- * 黒石の数
- */
- public get blackCount() {
- return count(BLACK, this.board);
- }
-
- /**
- * 白石の数
- */
- public get whiteCount() {
- return count(WHITE, this.board);
- }
-
- public transformPosToXy(pos: number): number[] {
- const x = pos % this.mapWidth;
- const y = Math.floor(pos / this.mapWidth);
- return [x, y];
- }
-
- public transformXyToPos(x: number, y: number): number {
- return x + (y * this.mapWidth);
- }
-
- /**
- * 指定のマスに石を打ちます
- * @param color 石の色
- * @param pos 位置
- */
- public put(color: Color, pos: number) {
- this.prevPos = pos;
- this.prevColor = color;
-
- this.board[pos] = color;
-
- // 反転させられる石を取得
- const effects = this.effects(color, pos);
-
- // 反転させる
- for (const pos of effects) {
- this.board[pos] = color;
- }
-
- const turn = this.turn;
-
- this.logs.push({
- color,
- pos,
- effects,
- turn
- });
-
- this.calcTurn();
- }
-
- private calcTurn() {
- // ターン計算
- this.turn =
- this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
- this.canPutSomewhere(this.prevColor!) ? this.prevColor :
- null;
- }
-
- public undo() {
- const undo = this.logs.pop()!;
- this.prevColor = undo.color;
- this.prevPos = undo.pos;
- this.board[undo.pos] = null;
- for (const pos of undo.effects) {
- const color = this.board[pos];
- this.board[pos] = !color;
- }
- this.turn = undo.turn;
- }
-
- /**
- * 指定した位置のマップデータのマスを取得します
- * @param pos 位置
- */
- public mapDataGet(pos: number): MapPixel {
- const [x, y] = this.transformPosToXy(pos);
- return x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight ? 'null' : this.map[pos];
- }
-
- /**
- * 打つことができる場所を取得します
- */
- public puttablePlaces(color: Color): number[] {
- return Array.from(this.board.keys()).filter(i => this.canPut(color, i));
- }
-
- /**
- * 打つことができる場所があるかどうかを取得します
- */
- public canPutSomewhere(color: Color): boolean {
- return this.puttablePlaces(color).length > 0;
- }
-
- /**
- * 指定のマスに石を打つことができるかどうかを取得します
- * @param color 自分の色
- * @param pos 位置
- */
- public canPut(color: Color, pos: number): boolean {
- return (
- this.board[pos] !== null ? false : // 既に石が置いてある場所には打てない
- this.opts.canPutEverywhere ? this.mapDataGet(pos) == 'empty' : // 挟んでなくても置けるモード
- this.effects(color, pos).length !== 0); // 相手の石を1つでも反転させられるか
- }
-
- /**
- * 指定のマスに石を置いた時の、反転させられる石を取得します
- * @param color 自分の色
- * @param initPos 位置
- */
- public effects(color: Color, initPos: number): number[] {
- const enemyColor = !color;
-
- const diffVectors: [number, number][] = [
- [ 0, -1], // 上
- [ +1, -1], // 右上
- [ +1, 0], // 右
- [ +1, +1], // 右下
- [ 0, +1], // 下
- [ -1, +1], // 左下
- [ -1, 0], // 左
- [ -1, -1] // 左上
- ];
-
- const effectsInLine = ([dx, dy]: [number, number]): number[] => {
- const nextPos = (x: number, y: number): [number, number] => [x + dx, y + dy];
-
- const found: number[] = []; // 挟めるかもしれない相手の石を入れておく配列
- let [x, y] = this.transformPosToXy(initPos);
- while (true) {
- [x, y] = nextPos(x, y);
-
- // 座標が指し示す位置がボード外に出たとき
- if (this.opts.loopedBoard && this.transformXyToPos(
- (x = ((x % this.mapWidth) + this.mapWidth) % this.mapWidth),
- (y = ((y % this.mapHeight) + this.mapHeight) % this.mapHeight)) === initPos)
- // 盤面の境界でループし、自分が石を置く位置に戻ってきたとき、挟めるようにしている (ref: Test4のマップ)
- return found;
- else if (x === -1 || y === -1 || x === this.mapWidth || y === this.mapHeight)
- return []; // 挟めないことが確定 (盤面外に到達)
-
- const pos = this.transformXyToPos(x, y);
- if (this.mapDataGet(pos) === 'null') return []; // 挟めないことが確定 (配置不可能なマスに到達)
- const stone = this.board[pos];
- if (stone === null) return []; // 挟めないことが確定 (石が置かれていないマスに到達)
- if (stone === enemyColor) found.push(pos); // 挟めるかもしれない (相手の石を発見)
- if (stone === color) return found; // 挟めることが確定 (対となる自分の石を発見)
- }
- };
-
- return concat(diffVectors.map(effectsInLine));
- }
-
- /**
- * ゲームが終了したか否か
- */
- public get isEnded(): boolean {
- return this.turn === null;
- }
-
- /**
- * ゲームの勝者 (null = 引き分け)
- */
- public get winner(): Color | null {
- return this.isEnded ?
- this.blackCount == this.whiteCount ? null :
- this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
- undefined as never;
- }
-}
diff --git a/packages/client/src/scripts/games/reversi/maps.ts b/packages/client/src/scripts/games/reversi/maps.ts
deleted file mode 100644
index dc0d1bf9d0..0000000000
--- a/packages/client/src/scripts/games/reversi/maps.ts
+++ /dev/null
@@ -1,896 +0,0 @@
-/**
- * 組み込みマップ定義
- *
- * データ値:
- * (スペース) ... マス無し
- * - ... マス
- * b ... 初期配置される黒石
- * w ... 初期配置される白石
- */
-
-export type Map = {
- name?: string;
- category?: string;
- author?: string;
- data: string[];
-};
-
-export const fourfour: Map = {
- name: '4x4',
- category: '4x4',
- data: [
- '----',
- '-wb-',
- '-bw-',
- '----'
- ]
-};
-
-export const sixsix: Map = {
- name: '6x6',
- category: '6x6',
- data: [
- '------',
- '------',
- '--wb--',
- '--bw--',
- '------',
- '------'
- ]
-};
-
-export const roundedSixsix: Map = {
- name: '6x6 rounded',
- category: '6x6',
- author: 'syuilo',
- data: [
- ' ---- ',
- '------',
- '--wb--',
- '--bw--',
- '------',
- ' ---- '
- ]
-};
-
-export const roundedSixsix2: Map = {
- name: '6x6 rounded 2',
- category: '6x6',
- author: 'syuilo',
- data: [
- ' -- ',
- ' ---- ',
- '--wb--',
- '--bw--',
- ' ---- ',
- ' -- '
- ]
-};
-
-export const eighteight: Map = {
- name: '8x8',
- category: '8x8',
- data: [
- '--------',
- '--------',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- '--------',
- '--------'
- ]
-};
-
-export const eighteightH1: Map = {
- name: '8x8 handicap 1',
- category: '8x8',
- data: [
- 'b-------',
- '--------',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- '--------',
- '--------'
- ]
-};
-
-export const eighteightH2: Map = {
- name: '8x8 handicap 2',
- category: '8x8',
- data: [
- 'b-------',
- '--------',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- '--------',
- '-------b'
- ]
-};
-
-export const eighteightH3: Map = {
- name: '8x8 handicap 3',
- category: '8x8',
- data: [
- 'b------b',
- '--------',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- '--------',
- '-------b'
- ]
-};
-
-export const eighteightH4: Map = {
- name: '8x8 handicap 4',
- category: '8x8',
- data: [
- 'b------b',
- '--------',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- '--------',
- 'b------b'
- ]
-};
-
-export const eighteightH28: Map = {
- name: '8x8 handicap 28',
- category: '8x8',
- data: [
- 'bbbbbbbb',
- 'b------b',
- 'b------b',
- 'b--wb--b',
- 'b--bw--b',
- 'b------b',
- 'b------b',
- 'bbbbbbbb'
- ]
-};
-
-export const roundedEighteight: Map = {
- name: '8x8 rounded',
- category: '8x8',
- author: 'syuilo',
- data: [
- ' ------ ',
- '--------',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- '--------',
- ' ------ '
- ]
-};
-
-export const roundedEighteight2: Map = {
- name: '8x8 rounded 2',
- category: '8x8',
- author: 'syuilo',
- data: [
- ' ---- ',
- ' ------ ',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- ' ------ ',
- ' ---- '
- ]
-};
-
-export const roundedEighteight3: Map = {
- name: '8x8 rounded 3',
- category: '8x8',
- author: 'syuilo',
- data: [
- ' -- ',
- ' ---- ',
- ' ------ ',
- '---wb---',
- '---bw---',
- ' ------ ',
- ' ---- ',
- ' -- '
- ]
-};
-
-export const eighteightWithNotch: Map = {
- name: '8x8 with notch',
- category: '8x8',
- author: 'syuilo',
- data: [
- '--- ---',
- '--------',
- '--------',
- ' --wb-- ',
- ' --bw-- ',
- '--------',
- '--------',
- '--- ---'
- ]
-};
-
-export const eighteightWithSomeHoles: Map = {
- name: '8x8 with some holes',
- category: '8x8',
- author: 'syuilo',
- data: [
- '--- ----',
- '----- --',
- '-- -----',
- '---wb---',
- '---bw- -',
- ' -------',
- '--- ----',
- '--------'
- ]
-};
-
-export const circle: Map = {
- name: 'Circle',
- category: '8x8',
- author: 'syuilo',
- data: [
- ' -- ',
- ' ------ ',
- ' ------ ',
- '---wb---',
- '---bw---',
- ' ------ ',
- ' ------ ',
- ' -- '
- ]
-};
-
-export const smile: Map = {
- name: 'Smile',
- category: '8x8',
- author: 'syuilo',
- data: [
- ' ------ ',
- '--------',
- '-- -- --',
- '---wb---',
- '-- bw --',
- '--- ---',
- '--------',
- ' ------ '
- ]
-};
-
-export const window: Map = {
- name: 'Window',
- category: '8x8',
- author: 'syuilo',
- data: [
- '--------',
- '- -- -',
- '- -- -',
- '---wb---',
- '---bw---',
- '- -- -',
- '- -- -',
- '--------'
- ]
-};
-
-export const reserved: Map = {
- name: 'Reserved',
- category: '8x8',
- author: 'Aya',
- data: [
- 'w------b',
- '--------',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- '--------',
- 'b------w'
- ]
-};
-
-export const x: Map = {
- name: 'X',
- category: '8x8',
- author: 'Aya',
- data: [
- 'w------b',
- '-w----b-',
- '--w--b--',
- '---wb---',
- '---bw---',
- '--b--w--',
- '-b----w-',
- 'b------w'
- ]
-};
-
-export const parallel: Map = {
- name: 'Parallel',
- category: '8x8',
- author: 'Aya',
- data: [
- '--------',
- '--------',
- '--------',
- '---bb---',
- '---ww---',
- '--------',
- '--------',
- '--------'
- ]
-};
-
-export const lackOfBlack: Map = {
- name: 'Lack of Black',
- category: '8x8',
- data: [
- '--------',
- '--------',
- '--------',
- '---w----',
- '---bw---',
- '--------',
- '--------',
- '--------'
- ]
-};
-
-export const squareParty: Map = {
- name: 'Square Party',
- category: '8x8',
- author: 'syuilo',
- data: [
- '--------',
- '-wwwbbb-',
- '-w-wb-b-',
- '-wwwbbb-',
- '-bbbwww-',
- '-b-bw-w-',
- '-bbbwww-',
- '--------'
- ]
-};
-
-export const minesweeper: Map = {
- name: 'Minesweeper',
- category: '8x8',
- author: 'syuilo',
- data: [
- 'b-b--w-w',
- '-w-wb-b-',
- 'w-b--w-b',
- '-b-wb-w-',
- '-w-bw-b-',
- 'b-w--b-w',
- '-b-bw-w-',
- 'w-w--b-b'
- ]
-};
-
-export const tenthtenth: Map = {
- name: '10x10',
- category: '10x10',
- data: [
- '----------',
- '----------',
- '----------',
- '----------',
- '----wb----',
- '----bw----',
- '----------',
- '----------',
- '----------',
- '----------'
- ]
-};
-
-export const hole: Map = {
- name: 'The Hole',
- category: '10x10',
- author: 'syuilo',
- data: [
- '----------',
- '----------',
- '--wb--wb--',
- '--bw--bw--',
- '---- ----',
- '---- ----',
- '--wb--wb--',
- '--bw--bw--',
- '----------',
- '----------'
- ]
-};
-
-export const grid: Map = {
- name: 'Grid',
- category: '10x10',
- author: 'syuilo',
- data: [
- '----------',
- '- - -- - -',
- '----------',
- '- - -- - -',
- '----wb----',
- '----bw----',
- '- - -- - -',
- '----------',
- '- - -- - -',
- '----------'
- ]
-};
-
-export const cross: Map = {
- name: 'Cross',
- category: '10x10',
- author: 'Aya',
- data: [
- ' ---- ',
- ' ---- ',
- ' ---- ',
- '----------',
- '----wb----',
- '----bw----',
- '----------',
- ' ---- ',
- ' ---- ',
- ' ---- '
- ]
-};
-
-export const charX: Map = {
- name: 'Char X',
- category: '10x10',
- author: 'syuilo',
- data: [
- '--- ---',
- '---- ----',
- '----------',
- ' -------- ',
- ' --wb-- ',
- ' --bw-- ',
- ' -------- ',
- '----------',
- '---- ----',
- '--- ---'
- ]
-};
-
-export const charY: Map = {
- name: 'Char Y',
- category: '10x10',
- author: 'syuilo',
- data: [
- '--- ---',
- '---- ----',
- '----------',
- ' -------- ',
- ' --wb-- ',
- ' --bw-- ',
- ' ------ ',
- ' ------ ',
- ' ------ ',
- ' ------ '
- ]
-};
-
-export const walls: Map = {
- name: 'Walls',
- category: '10x10',
- author: 'Aya',
- data: [
- ' bbbbbbbb ',
- 'w--------w',
- 'w--------w',
- 'w--------w',
- 'w---wb---w',
- 'w---bw---w',
- 'w--------w',
- 'w--------w',
- 'w--------w',
- ' bbbbbbbb '
- ]
-};
-
-export const cpu: Map = {
- name: 'CPU',
- category: '10x10',
- author: 'syuilo',
- data: [
- ' b b b b ',
- 'w--------w',
- ' -------- ',
- 'w--------w',
- ' ---wb--- ',
- ' ---bw--- ',
- 'w--------w',
- ' -------- ',
- 'w--------w',
- ' b b b b '
- ]
-};
-
-export const checker: Map = {
- name: 'Checker',
- category: '10x10',
- author: 'Aya',
- data: [
- '----------',
- '----------',
- '----------',
- '---wbwb---',
- '---bwbw---',
- '---wbwb---',
- '---bwbw---',
- '----------',
- '----------',
- '----------'
- ]
-};
-
-export const japaneseCurry: Map = {
- name: 'Japanese curry',
- category: '10x10',
- author: 'syuilo',
- data: [
- 'w-b-b-b-b-',
- '-w-b-b-b-b',
- 'w-w-b-b-b-',
- '-w-w-b-b-b',
- 'w-w-wwb-b-',
- '-w-wbb-b-b',
- 'w-w-w-b-b-',
- '-w-w-w-b-b',
- 'w-w-w-w-b-',
- '-w-w-w-w-b'
- ]
-};
-
-export const mosaic: Map = {
- name: 'Mosaic',
- category: '10x10',
- author: 'syuilo',
- data: [
- '- - - - - ',
- ' - - - - -',
- '- - - - - ',
- ' - w w - -',
- '- - b b - ',
- ' - w w - -',
- '- - b b - ',
- ' - - - - -',
- '- - - - - ',
- ' - - - - -',
- ]
-};
-
-export const arena: Map = {
- name: 'Arena',
- category: '10x10',
- author: 'syuilo',
- data: [
- '- - -- - -',
- ' - - - - ',
- '- ------ -',
- ' -------- ',
- '- --wb-- -',
- '- --bw-- -',
- ' -------- ',
- '- ------ -',
- ' - - - - ',
- '- - -- - -'
- ]
-};
-
-export const reactor: Map = {
- name: 'Reactor',
- category: '10x10',
- author: 'syuilo',
- data: [
- '-w------b-',
- 'b- - - -w',
- '- --wb-- -',
- '---b w---',
- '- b wb w -',
- '- w bw b -',
- '---w b---',
- '- --bw-- -',
- 'w- - - -b',
- '-b------w-'
- ]
-};
-
-export const sixeight: Map = {
- name: '6x8',
- category: 'Special',
- data: [
- '------',
- '------',
- '------',
- '--wb--',
- '--bw--',
- '------',
- '------',
- '------'
- ]
-};
-
-export const spark: Map = {
- name: 'Spark',
- category: 'Special',
- author: 'syuilo',
- data: [
- ' - - ',
- '----------',
- ' -------- ',
- ' -------- ',
- ' ---wb--- ',
- ' ---bw--- ',
- ' -------- ',
- ' -------- ',
- '----------',
- ' - - '
- ]
-};
-
-export const islands: Map = {
- name: 'Islands',
- category: 'Special',
- author: 'syuilo',
- data: [
- '-------- ',
- '---wb--- ',
- '---bw--- ',
- '-------- ',
- ' - - ',
- ' - - ',
- ' --------',
- ' --------',
- ' --------',
- ' --------'
- ]
-};
-
-export const galaxy: Map = {
- name: 'Galaxy',
- category: 'Special',
- author: 'syuilo',
- data: [
- ' ------ ',
- ' --www--- ',
- ' ------w--- ',
- '---bbb--w---',
- '--b---b-w-b-',
- '-b--wwb-w-b-',
- '-b-w-bww--b-',
- '-b-w-b---b--',
- '---w--bbb---',
- ' ---w------ ',
- ' ---www-- ',
- ' ------ '
- ]
-};
-
-export const triangle: Map = {
- name: 'Triangle',
- category: 'Special',
- author: 'syuilo',
- data: [
- ' -- ',
- ' -- ',
- ' ---- ',
- ' ---- ',
- ' --wb-- ',
- ' --bw-- ',
- ' -------- ',
- ' -------- ',
- '----------',
- '----------'
- ]
-};
-
-export const iphonex: Map = {
- name: 'iPhone X',
- category: 'Special',
- author: 'syuilo',
- data: [
- ' -- -- ',
- '--------',
- '--------',
- '--------',
- '--------',
- '---wb---',
- '---bw---',
- '--------',
- '--------',
- '--------',
- '--------',
- ' ------ '
- ]
-};
-
-export const dealWithIt: Map = {
- name: 'Deal with it!',
- category: 'Special',
- author: 'syuilo',
- data: [
- '------------',
- '--w-b-------',
- ' --b-w------',
- ' --w-b---- ',
- ' ------- '
- ]
-};
-
-export const experiment: Map = {
- name: 'Let\'s experiment',
- category: 'Special',
- author: 'syuilo',
- data: [
- ' ------------ ',
- '------wb------',
- '------bw------',
- '--------------',
- ' - - ',
- '------ ------',
- 'bbbbbb wwwwww',
- 'bbbbbb wwwwww',
- 'bbbbbb wwwwww',
- 'bbbbbb wwwwww',
- 'wwwwww bbbbbb'
- ]
-};
-
-export const bigBoard: Map = {
- name: 'Big board',
- category: 'Special',
- data: [
- '----------------',
- '----------------',
- '----------------',
- '----------------',
- '----------------',
- '----------------',
- '----------------',
- '-------wb-------',
- '-------bw-------',
- '----------------',
- '----------------',
- '----------------',
- '----------------',
- '----------------',
- '----------------',
- '----------------'
- ]
-};
-
-export const twoBoard: Map = {
- name: 'Two board',
- category: 'Special',
- author: 'Aya',
- data: [
- '-------- --------',
- '-------- --------',
- '-------- --------',
- '---wb--- ---wb---',
- '---bw--- ---bw---',
- '-------- --------',
- '-------- --------',
- '-------- --------'
- ]
-};
-
-export const test1: Map = {
- name: 'Test1',
- category: 'Test',
- data: [
- '--------',
- '---wb---',
- '---bw---',
- '--------'
- ]
-};
-
-export const test2: Map = {
- name: 'Test2',
- category: 'Test',
- data: [
- '------',
- '------',
- '-b--w-',
- '-w--b-',
- '-w--b-'
- ]
-};
-
-export const test3: Map = {
- name: 'Test3',
- category: 'Test',
- data: [
- '-w-',
- '--w',
- 'w--',
- '-w-',
- '--w',
- 'w--',
- '-w-',
- '--w',
- 'w--',
- '-w-',
- '---',
- 'b--',
- ]
-};
-
-export const test4: Map = {
- name: 'Test4',
- category: 'Test',
- data: [
- '-w--b-',
- '-w--b-',
- '------',
- '-w--b-',
- '-w--b-'
- ]
-};
-
-// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)A1に打ってしまう
-export const test6: Map = {
- name: 'Test6',
- category: 'Test',
- data: [
- '--wwwww-',
- 'wwwwwwww',
- 'wbbbwbwb',
- 'wbbbbwbb',
- 'wbwbbwbb',
- 'wwbwbbbb',
- '--wbbbbb',
- '-wwwww--',
- ]
-};
-
-// 検証用: この盤面で藍(lv3)が黒で始めると何故か(?)G7に打ってしまう
-export const test7: Map = {
- name: 'Test7',
- category: 'Test',
- data: [
- 'b--w----',
- 'b-wwww--',
- 'bwbwwwbb',
- 'wbwwwwb-',
- 'wwwwwww-',
- '-wwbbwwb',
- '--wwww--',
- '--wwww--',
- ]
-};
-
-// 検証用: この盤面で藍(lv5)が黒で始めると何故か(?)A1に打ってしまう
-export const test8: Map = {
- name: 'Test8',
- category: 'Test',
- data: [
- '--------',
- '-----w--',
- 'w--www--',
- 'wwwwww--',
- 'bbbbwww-',
- 'wwwwww--',
- '--www---',
- '--ww----',
- ]
-};
diff --git a/packages/client/src/scripts/games/reversi/package.json b/packages/client/src/scripts/games/reversi/package.json
deleted file mode 100644
index a4415ad141..0000000000
--- a/packages/client/src/scripts/games/reversi/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "misskey-reversi",
- "version": "0.0.5",
- "description": "Misskey reversi engine",
- "keywords": [
- "misskey"
- ],
- "author": "syuilo <i@syuilo.com>",
- "license": "MIT",
- "repository": "https://github.com/misskey-dev/misskey.git",
- "bugs": "https://github.com/misskey-dev/misskey/issues",
- "main": "./built/core.js",
- "types": "./built/core.d.ts",
- "scripts": {
- "build": "tsc"
- },
- "dependencies": {}
-}
diff --git a/packages/client/src/scripts/games/reversi/tsconfig.json b/packages/client/src/scripts/games/reversi/tsconfig.json
deleted file mode 100644
index 851fb6b7e4..0000000000
--- a/packages/client/src/scripts/games/reversi/tsconfig.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "compilerOptions": {
- "noEmitOnError": false,
- "noImplicitAny": false,
- "noImplicitReturns": true,
- "noFallthroughCasesInSwitch": true,
- "experimentalDecorators": true,
- "declaration": true,
- "sourceMap": false,
- "target": "es2017",
- "module": "commonjs",
- "removeComments": false,
- "noLib": false,
- "outDir": "./built",
- "rootDir": "./"
- },
- "compileOnSave": false,
- "include": [
- "./core.ts"
- ]
-}
diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts
new file mode 100644
index 0000000000..3634f39632
--- /dev/null
+++ b/packages/client/src/scripts/get-note-menu.ts
@@ -0,0 +1,310 @@
+import { Ref } from 'vue';
+import * as misskey from 'misskey-js';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import * as os from '@/os';
+import copyToClipboard from '@/scripts/copy-to-clipboard';
+import { url } from '@/config';
+import { noteActions } from '@/store';
+import { pleaseLogin } from './please-login';
+
+export function getNoteMenu(props: {
+ note: misskey.entities.Note;
+ menuButton: Ref<HTMLElement>;
+ translation: Ref<any>;
+ translating: Ref<boolean>;
+}) {
+ const isRenote = (
+ props.note.renote != null &&
+ props.note.text == null &&
+ props.note.fileIds.length === 0 &&
+ props.note.poll == null
+ );
+
+ let appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note;
+
+ function del(): void {
+ os.confirm({
+ type: 'warning',
+ text: i18n.locale.noteDeleteConfirm,
+ }).then(({ canceled }) => {
+ if (canceled) return;
+
+ os.api('notes/delete', {
+ noteId: appearNote.id
+ });
+ });
+ }
+
+ function delEdit(): void {
+ os.confirm({
+ type: 'warning',
+ text: i18n.locale.deleteAndEditConfirm,
+ }).then(({ canceled }) => {
+ if (canceled) return;
+
+ os.api('notes/delete', {
+ noteId: appearNote.id
+ });
+
+ os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
+ });
+ }
+
+ function toggleFavorite(favorite: boolean): void {
+ os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
+ noteId: appearNote.id
+ });
+ }
+
+ function toggleWatch(watch: boolean): void {
+ os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
+ noteId: appearNote.id
+ });
+ }
+
+ function toggleThreadMute(mute: boolean): void {
+ os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', {
+ noteId: appearNote.id
+ });
+ }
+
+ function copyContent(): void {
+ copyToClipboard(appearNote.text);
+ os.success();
+ }
+
+ function copyLink(): void {
+ copyToClipboard(`${url}/notes/${appearNote.id}`);
+ os.success();
+ }
+
+ function togglePin(pin: boolean): void {
+ os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
+ noteId: appearNote.id
+ }, undefined, null, e => {
+ if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
+ os.alert({
+ type: 'error',
+ text: i18n.locale.pinLimitExceeded
+ });
+ }
+ });
+ }
+
+ async function clip(): Promise<void> {
+ const clips = await os.api('clips/list');
+ os.popupMenu([{
+ icon: 'fas fa-plus',
+ text: i18n.locale.createNew,
+ action: async () => {
+ const { canceled, result } = await os.form(i18n.locale.createNewClip, {
+ name: {
+ type: 'string',
+ label: i18n.locale.name
+ },
+ description: {
+ type: 'string',
+ required: false,
+ multiline: true,
+ label: i18n.locale.description
+ },
+ isPublic: {
+ type: 'boolean',
+ label: i18n.locale.public,
+ default: false
+ }
+ });
+ if (canceled) return;
+
+ const clip = await os.apiWithDialog('clips/create', result);
+
+ os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
+ }
+ }, null, ...clips.map(clip => ({
+ text: clip.name,
+ action: () => {
+ os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
+ }
+ }))], props.menuButton.value, {
+ }).then(focus);
+ }
+
+ async function promote(): Promise<void> {
+ const { canceled, result: days } = await os.inputNumber({
+ title: i18n.locale.numberOfDays,
+ });
+
+ if (canceled) return;
+
+ os.apiWithDialog('admin/promo/create', {
+ noteId: appearNote.id,
+ expiresAt: Date.now() + (86400000 * days),
+ });
+ }
+
+ function share(): void {
+ navigator.share({
+ title: i18n.t('noteOf', { user: appearNote.user.name }),
+ text: appearNote.text,
+ url: `${url}/notes/${appearNote.id}`,
+ });
+ }
+
+ async function translate(): Promise<void> {
+ if (props.translation.value != null) return;
+ props.translating.value = true;
+ const res = await os.api('notes/translate', {
+ noteId: appearNote.id,
+ targetLang: localStorage.getItem('lang') || navigator.language,
+ });
+ props.translating.value = false;
+ props.translation.value = res;
+ }
+
+ let menu;
+ if ($i) {
+ const statePromise = os.api('notes/state', {
+ noteId: appearNote.id
+ });
+
+ menu = [{
+ icon: 'fas fa-copy',
+ text: i18n.locale.copyContent,
+ action: copyContent
+ }, {
+ icon: 'fas fa-link',
+ text: i18n.locale.copyLink,
+ action: copyLink
+ }, (appearNote.url || appearNote.uri) ? {
+ icon: 'fas fa-external-link-square-alt',
+ text: i18n.locale.showOnRemote,
+ action: () => {
+ window.open(appearNote.url || appearNote.uri, '_blank');
+ }
+ } : undefined,
+ {
+ icon: 'fas fa-share-alt',
+ text: i18n.locale.share,
+ action: share
+ },
+ instance.translatorAvailable ? {
+ icon: 'fas fa-language',
+ text: i18n.locale.translate,
+ action: translate
+ } : undefined,
+ null,
+ statePromise.then(state => state.isFavorited ? {
+ icon: 'fas fa-star',
+ text: i18n.locale.unfavorite,
+ action: () => toggleFavorite(false)
+ } : {
+ icon: 'fas fa-star',
+ text: i18n.locale.favorite,
+ action: () => toggleFavorite(true)
+ }),
+ {
+ icon: 'fas fa-paperclip',
+ text: i18n.locale.clip,
+ action: () => clip()
+ },
+ (appearNote.userId != $i.id) ? statePromise.then(state => state.isWatching ? {
+ icon: 'fas fa-eye-slash',
+ text: i18n.locale.unwatch,
+ action: () => toggleWatch(false)
+ } : {
+ icon: 'fas fa-eye',
+ text: i18n.locale.watch,
+ action: () => toggleWatch(true)
+ }) : undefined,
+ statePromise.then(state => state.isMutedThread ? {
+ icon: 'fas fa-comment-slash',
+ text: i18n.locale.unmuteThread,
+ action: () => toggleThreadMute(false)
+ } : {
+ icon: 'fas fa-comment-slash',
+ text: i18n.locale.muteThread,
+ action: () => toggleThreadMute(true)
+ }),
+ appearNote.userId == $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
+ icon: 'fas fa-thumbtack',
+ text: i18n.locale.unpin,
+ action: () => togglePin(false)
+ } : {
+ icon: 'fas fa-thumbtack',
+ text: i18n.locale.pin,
+ action: () => togglePin(true)
+ } : undefined,
+ /*
+ ...($i.isModerator || $i.isAdmin ? [
+ null,
+ {
+ icon: 'fas fa-bullhorn',
+ text: i18n.locale.promote,
+ action: promote
+ }]
+ : []
+ ),*/
+ ...(appearNote.userId != $i.id ? [
+ null,
+ {
+ icon: 'fas fa-exclamation-circle',
+ text: i18n.locale.reportAbuse,
+ action: () => {
+ const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
+ os.popup(import('@/components/abuse-report-window.vue'), {
+ user: appearNote.user,
+ initialComment: `Note: ${u}\n-----\n`
+ }, {}, 'closed');
+ }
+ }]
+ : []
+ ),
+ ...(appearNote.userId == $i.id || $i.isModerator || $i.isAdmin ? [
+ null,
+ appearNote.userId == $i.id ? {
+ icon: 'fas fa-edit',
+ text: i18n.locale.deleteAndEdit,
+ action: delEdit
+ } : undefined,
+ {
+ icon: 'fas fa-trash-alt',
+ text: i18n.locale.delete,
+ danger: true,
+ action: del
+ }]
+ : []
+ )]
+ .filter(x => x !== undefined);
+ } else {
+ menu = [{
+ icon: 'fas fa-copy',
+ text: i18n.locale.copyContent,
+ action: copyContent
+ }, {
+ icon: 'fas fa-link',
+ text: i18n.locale.copyLink,
+ action: copyLink
+ }, (appearNote.url || appearNote.uri) ? {
+ icon: 'fas fa-external-link-square-alt',
+ text: i18n.locale.showOnRemote,
+ action: () => {
+ window.open(appearNote.url || appearNote.uri, '_blank');
+ }
+ } : undefined]
+ .filter(x => x !== undefined);
+ }
+
+ if (noteActions.length > 0) {
+ menu = menu.concat([null, ...noteActions.map(action => ({
+ icon: 'fas fa-plug',
+ text: action.title,
+ action: () => {
+ action.handler(appearNote);
+ }
+ }))]);
+ }
+
+ return menu;
+}
diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts
index ebe101bc0f..7b910a0083 100644
--- a/packages/client/src/scripts/get-user-menu.ts
+++ b/packages/client/src/scripts/get-user-menu.ts
@@ -5,7 +5,7 @@ import * as Acct from 'misskey-js/built/acct';
import * as os from '@/os';
import { userActions } from '@/store';
import { router } from '@/router';
-import { $i } from '@/account';
+import { $i, iAmModerator } from '@/account';
export function getUserMenu(user) {
const meId = $i ? $i.id : null;
@@ -175,7 +175,7 @@ export function getUserMenu(user) {
action: reportAbuse
}]);
- if ($i && ($i.isAdmin || $i.isModerator)) {
+ if (iAmModerator) {
menu = menu.concat([null, {
icon: 'fas fa-microphone-slash',
text: user.isSilenced ? i18n.locale.unsilence : i18n.locale.silence,
diff --git a/packages/client/src/scripts/paging.ts b/packages/client/src/scripts/paging.ts
deleted file mode 100644
index ef63ecc450..0000000000
--- a/packages/client/src/scripts/paging.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-import { markRaw } from 'vue';
-import * as os from '@/os';
-import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from './scroll';
-
-const SECOND_FETCH_LIMIT = 30;
-
-// reversed: items 配列の中身を逆順にする(新しい方が最後)
-
-export default (opts) => ({
- emits: ['queue'],
-
- data() {
- return {
- items: [],
- queue: [],
- offset: 0,
- fetching: true,
- moreFetching: false,
- inited: false,
- more: false,
- backed: false, // 遡り中か否か
- isBackTop: false,
- };
- },
-
- computed: {
- empty(): boolean {
- return this.items.length === 0 && !this.fetching && this.inited;
- },
-
- error(): boolean {
- return !this.fetching && !this.inited;
- },
- },
-
- watch: {
- pagination: {
- handler() {
- this.init();
- },
- deep: true
- },
-
- queue: {
- handler(a, b) {
- if (a.length === 0 && b.length === 0) return;
- this.$emit('queue', this.queue.length);
- },
- deep: true
- }
- },
-
- created() {
- opts.displayLimit = opts.displayLimit || 30;
- this.init();
- },
-
- activated() {
- this.isBackTop = false;
- },
-
- deactivated() {
- this.isBackTop = window.scrollY === 0;
- },
-
- methods: {
- reload() {
- this.items = [];
- this.init();
- },
-
- replaceItem(finder, data) {
- const i = this.items.findIndex(finder);
- this.items[i] = data;
- },
-
- removeItem(finder) {
- const i = this.items.findIndex(finder);
- this.items.splice(i, 1);
- },
-
- async init() {
- this.queue = [];
- this.fetching = true;
- if (opts.before) opts.before(this);
- let params = typeof this.pagination.params === 'function' ? this.pagination.params(true) : this.pagination.params;
- if (params && params.then) params = await params;
- if (params === null) return;
- const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
- await os.api(endpoint, {
- ...params,
- limit: this.pagination.noPaging ? (this.pagination.limit || 10) : (this.pagination.limit || 10) + 1,
- }).then(items => {
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- markRaw(item);
- if (this.pagination.reversed) {
- if (i === items.length - 2) item._shouldInsertAd_ = true;
- } else {
- if (i === 3) item._shouldInsertAd_ = true;
- }
- }
- if (!this.pagination.noPaging && (items.length > (this.pagination.limit || 10))) {
- items.pop();
- this.items = this.pagination.reversed ? [...items].reverse() : items;
- this.more = true;
- } else {
- this.items = this.pagination.reversed ? [...items].reverse() : items;
- this.more = false;
- }
- this.offset = items.length;
- this.inited = true;
- this.fetching = false;
- if (opts.after) opts.after(this, null);
- }, e => {
- this.fetching = false;
- if (opts.after) opts.after(this, e);
- });
- },
-
- async fetchMore() {
- if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
- this.moreFetching = true;
- this.backed = true;
- let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
- if (params && params.then) params = await params;
- const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
- await os.api(endpoint, {
- ...params,
- limit: SECOND_FETCH_LIMIT + 1,
- ...(this.pagination.offsetMode ? {
- offset: this.offset,
- } : {
- untilId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
- }),
- }).then(items => {
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- markRaw(item);
- if (this.pagination.reversed) {
- if (i === items.length - 9) item._shouldInsertAd_ = true;
- } else {
- if (i === 10) item._shouldInsertAd_ = true;
- }
- }
- if (items.length > SECOND_FETCH_LIMIT) {
- items.pop();
- this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
- this.more = true;
- } else {
- this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
- this.more = false;
- }
- this.offset += items.length;
- this.moreFetching = false;
- }, e => {
- this.moreFetching = false;
- });
- },
-
- async fetchMoreFeature() {
- if (!this.more || this.fetching || this.moreFetching || this.items.length === 0) return;
- this.moreFetching = true;
- let params = typeof this.pagination.params === 'function' ? this.pagination.params(false) : this.pagination.params;
- if (params && params.then) params = await params;
- const endpoint = typeof this.pagination.endpoint === 'function' ? this.pagination.endpoint() : this.pagination.endpoint;
- await os.api(endpoint, {
- ...params,
- limit: SECOND_FETCH_LIMIT + 1,
- ...(this.pagination.offsetMode ? {
- offset: this.offset,
- } : {
- sinceId: this.pagination.reversed ? this.items[0].id : this.items[this.items.length - 1].id,
- }),
- }).then(items => {
- for (const item of items) {
- markRaw(item);
- }
- if (items.length > SECOND_FETCH_LIMIT) {
- items.pop();
- this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
- this.more = true;
- } else {
- this.items = this.pagination.reversed ? [...items].reverse().concat(this.items) : this.items.concat(items);
- this.more = false;
- }
- this.offset += items.length;
- this.moreFetching = false;
- }, e => {
- this.moreFetching = false;
- });
- },
-
- prepend(item) {
- if (this.pagination.reversed) {
- const container = getScrollContainer(this.$el);
- const pos = getScrollPosition(this.$el);
- const viewHeight = container.clientHeight;
- const height = container.scrollHeight;
- const isBottom = (pos + viewHeight > height - 32);
- if (isBottom) {
- // オーバーフローしたら古いアイテムは捨てる
- if (this.items.length >= opts.displayLimit) {
- // このやり方だとVue 3.2以降アニメーションが動かなくなる
- //this.items = this.items.slice(-opts.displayLimit);
- while (this.items.length >= opts.displayLimit) {
- this.items.shift();
- }
- this.more = true;
- }
- }
- this.items.push(item);
- // TODO
- } else {
- const isTop = this.isBackTop || (document.body.contains(this.$el) && isTopVisible(this.$el));
-
- if (isTop) {
- // Prepend the item
- this.items.unshift(item);
-
- // オーバーフローしたら古いアイテムは捨てる
- if (this.items.length >= opts.displayLimit) {
- // このやり方だとVue 3.2以降アニメーションが動かなくなる
- //this.items = this.items.slice(0, opts.displayLimit);
- while (this.items.length >= opts.displayLimit) {
- this.items.pop();
- }
- this.more = true;
- }
- } else {
- this.queue.push(item);
- onScrollTop(this.$el, () => {
- for (const item of this.queue) {
- this.prepend(item);
- }
- this.queue = [];
- });
- }
- }
- },
-
- append(item) {
- this.items.push(item);
- },
- }
-});
diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts
index 445b6296eb..36e476b6f9 100644
--- a/packages/client/src/scripts/physics.ts
+++ b/packages/client/src/scripts/physics.ts
@@ -136,7 +136,7 @@ export function physics(container: HTMLElement) {
}
// 奈落に落ちたオブジェクトは消す
- const intervalId = setInterval(() => {
+ const intervalId = window.setInterval(() => {
for (const obj of objs) {
if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj);
}
@@ -146,7 +146,7 @@ export function physics(container: HTMLElement) {
stop: () => {
stop = true;
Matter.Runner.stop(runner);
- clearInterval(intervalId);
+ window.clearInterval(intervalId);
}
};
}
diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts
index 51b8d72868..b8286a2a76 100644
--- a/packages/client/src/scripts/popout.ts
+++ b/packages/client/src/scripts/popout.ts
@@ -1,8 +1,8 @@
import * as config from '@/config';
export function popout(path: string, w?: HTMLElement) {
- let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path;
- url += '?zen'; // TODO: ちゃんとURLパースしてクエリ付ける
+ let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + "/" + path;
+ url += '?zen';
if (w) {
const position = w.getBoundingClientRect();
const width = parseInt(getComputedStyle(w, '').width, 10);
diff --git a/packages/client/src/scripts/room/furniture.ts b/packages/client/src/scripts/room/furniture.ts
deleted file mode 100644
index 7734e32668..0000000000
--- a/packages/client/src/scripts/room/furniture.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-export type RoomInfo = {
- roomType: string;
- carpetColor: string;
- furnitures: Furniture[];
-};
-
-export type Furniture = {
- id: string; // 同じ家具が複数ある場合にそれぞれを識別するためのIDであり、家具IDではない
- type: string; // こっちが家具ID(chairとか)
- position: {
- x: number;
- y: number;
- z: number;
- };
- rotation: {
- x: number;
- y: number;
- z: number;
- };
- props?: Record<string, any>;
-};
diff --git a/packages/client/src/scripts/room/furnitures.json5 b/packages/client/src/scripts/room/furnitures.json5
deleted file mode 100644
index 4a40994107..0000000000
--- a/packages/client/src/scripts/room/furnitures.json5
+++ /dev/null
@@ -1,407 +0,0 @@
-// 家具メタデータ
-
-// 家具IDはglbファイル及びそのディレクトリ名と一致する必要があります
-
-// 家具にはユーザーが設定できるプロパティを設定可能です:
-//
-// props: {
-// <propname>: <proptype>
-// }
-//
-// proptype一覧:
-// * image ... 画像選択ダイアログを出し、その画像のURLが格納されます
-// * color ... 色選択コントロールを出し、選択された色が格納されます
-
-// 家具にカスタムテクスチャを適用できるようにするには、textureプロパティに以下の追加の情報を含めます:
-// 便宜上そのUVのどの部分にカスタムテクスチャを貼り合わせるかのエリアをテクスチャエリアと呼びます。
-// UVは1024*1024だと仮定します。
-//
-// <key>: {
-// prop: <プロパティ名>,
-// uv: {
-// x: <テクスチャエリアX座標>,
-// y: <テクスチャエリアY座標>,
-// width: <テクスチャエリアの幅>,
-// height: <テクスチャエリアの高さ>,
-// },
-// }
-//
-// <key>には、カスタムテクスチャを適用したいメッシュ名を指定します
-// <プロパティ名>には、カスタムテクスチャとして使用する画像を格納するプロパティ(前述)名を指定します
-
-// 家具にカスタムカラーを適用できるようにするには、colorプロパティに以下の追加の情報を含めます:
-//
-// <key>: <プロパティ名>
-//
-// <key>には、カスタムカラーを適用したいマテリアル名を指定します
-// <プロパティ名>には、カスタムカラーとして使用する色を格納するプロパティ(前述)名を指定します
-
-[
- {
- id: "milk",
- place: "floor"
- },
- {
- id: "bed",
- place: "floor"
- },
- {
- id: "low-table",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Table: 'color'
- }
- },
- {
- id: "desk",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Board: 'color'
- }
- },
- {
- id: "chair",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Chair: 'color'
- }
- },
- {
- id: "chair2",
- place: "floor",
- props: {
- color1: 'color',
- color2: 'color'
- },
- color: {
- Cushion: 'color1',
- Leg: 'color2'
- }
- },
- {
- id: "fan",
- place: "wall"
- },
- {
- id: "pc",
- place: "floor"
- },
- {
- id: "plant",
- place: "floor"
- },
- {
- id: "plant2",
- place: "floor"
- },
- {
- id: "eraser",
- place: "floor"
- },
- {
- id: "pencil",
- place: "floor"
- },
- {
- id: "pudding",
- place: "floor"
- },
- {
- id: "cardboard-box",
- place: "floor"
- },
- {
- id: "cardboard-box2",
- place: "floor"
- },
- {
- id: "cardboard-box3",
- place: "floor"
- },
- {
- id: "book",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Cover: 'color'
- }
- },
- {
- id: "book2",
- place: "floor"
- },
- {
- id: "piano",
- place: "floor"
- },
- {
- id: "facial-tissue",
- place: "floor"
- },
- {
- id: "server",
- place: "floor"
- },
- {
- id: "moon",
- place: "floor"
- },
- {
- id: "corkboard",
- place: "wall"
- },
- {
- id: "mousepad",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Pad: 'color'
- }
- },
- {
- id: "monitor",
- place: "floor",
- props: {
- screen: 'image'
- },
- texture: {
- Screen: {
- prop: 'screen',
- uv: {
- x: 0,
- y: 434,
- width: 1024,
- height: 588,
- },
- },
- },
- },
- {
- id: "tv",
- place: "floor",
- props: {
- screen: 'image'
- },
- texture: {
- Screen: {
- prop: 'screen',
- uv: {
- x: 0,
- y: 434,
- width: 1024,
- height: 588,
- },
- },
- },
- },
- {
- id: "keyboard",
- place: "floor"
- },
- {
- id: "carpet-stripe",
- place: "floor",
- props: {
- color1: 'color',
- color2: 'color'
- },
- color: {
- CarpetAreaA: 'color1',
- CarpetAreaB: 'color2'
- },
- },
- {
- id: "mat",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Mat: 'color'
- }
- },
- {
- id: "color-box",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- main: 'color'
- }
- },
- {
- id: "wall-clock",
- place: "wall"
- },
- {
- id: "cube",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Cube: 'color'
- }
- },
- {
- id: "photoframe",
- place: "wall",
- props: {
- photo: 'image',
- color: 'color'
- },
- texture: {
- Photo: {
- prop: 'photo',
- uv: {
- x: 0,
- y: 342,
- width: 1024,
- height: 683,
- },
- },
- },
- color: {
- Frame: 'color'
- }
- },
- {
- id: "pinguin",
- place: "floor",
- props: {
- body: 'color',
- belly: 'color'
- },
- color: {
- Body: 'body',
- Belly: 'belly',
- }
- },
- {
- id: "rubik-cube",
- place: "floor",
- },
- {
- id: "poster-h",
- place: "wall",
- props: {
- picture: 'image'
- },
- texture: {
- Poster: {
- prop: 'picture',
- uv: {
- x: 0,
- y: 277,
- width: 1024,
- height: 745,
- },
- },
- },
- },
- {
- id: "poster-v",
- place: "wall",
- props: {
- picture: 'image'
- },
- texture: {
- Poster: {
- prop: 'picture',
- uv: {
- x: 0,
- y: 0,
- width: 745,
- height: 1024,
- },
- },
- },
- },
- {
- id: "sofa",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Sofa: 'color'
- }
- },
- {
- id: "spiral",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Step: 'color'
- }
- },
- {
- id: "bin",
- place: "floor",
- props: {
- color: 'color'
- },
- color: {
- Bin: 'color'
- }
- },
- {
- id: "cup-noodle",
- place: "floor"
- },
- {
- id: "holo-display",
- place: "floor",
- props: {
- image: 'image'
- },
- texture: {
- Image_Front: {
- prop: 'image',
- uv: {
- x: 0,
- y: 0,
- width: 1024,
- height: 1024,
- },
- },
- Image_Back: {
- prop: 'image',
- uv: {
- x: 0,
- y: 0,
- width: 1024,
- height: 1024,
- },
- },
- },
- },
- {
- id: 'energy-drink',
- place: "floor",
- },
- {
- id: 'doll-ai',
- place: "floor",
- },
- {
- id: 'banknote',
- place: "floor",
- },
-]
diff --git a/packages/client/src/scripts/room/room.ts b/packages/client/src/scripts/room/room.ts
deleted file mode 100644
index 7e04bec646..0000000000
--- a/packages/client/src/scripts/room/room.ts
+++ /dev/null
@@ -1,775 +0,0 @@
-import autobind from 'autobind-decorator';
-import { v4 as uuid } from 'uuid';
-import * as THREE from 'three';
-import { GLTFLoader, GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
-import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
-import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
-import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
-import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
-import { BloomPass } from 'three/examples/jsm/postprocessing/BloomPass.js';
-import { FXAAShader } from 'three/examples/jsm/shaders/FXAAShader.js';
-import { TransformControls } from 'three/examples/jsm/controls/TransformControls.js';
-import { Furniture, RoomInfo } from './furniture';
-import { query as urlQuery } from '@/scripts/url';
-const furnitureDefs = require('./furnitures.json5');
-
-THREE.ImageUtils.crossOrigin = '';
-
-type Options = {
- graphicsQuality: Room['graphicsQuality'];
- onChangeSelect: Room['onChangeSelect'];
- useOrthographicCamera: boolean;
-};
-
-/**
- * MisskeyRoom Core Engine
- */
-export class Room {
- private clock: THREE.Clock;
- private scene: THREE.Scene;
- private renderer: THREE.WebGLRenderer;
- private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera;
- private controls: OrbitControls;
- private composer: EffectComposer;
- private mixers: THREE.AnimationMixer[] = [];
- private furnitureControl: TransformControls;
- private roomInfo: RoomInfo;
- private graphicsQuality: 'cheep' | 'low' | 'medium' | 'high' | 'ultra';
- private roomObj: THREE.Object3D;
- private objects: THREE.Object3D[] = [];
- private selectedObject: THREE.Object3D = null;
- private onChangeSelect: Function;
- private isTransformMode = false;
- private renderFrameRequestId: number;
-
- private get canvas(): HTMLCanvasElement {
- return this.renderer.domElement;
- }
-
- private get furnitures(): Furniture[] {
- return this.roomInfo.furnitures;
- }
-
- private set furnitures(furnitures: Furniture[]) {
- this.roomInfo.furnitures = furnitures;
- }
-
- private get enableShadow() {
- return this.graphicsQuality != 'cheep';
- }
-
- private get usePostFXs() {
- return this.graphicsQuality !== 'cheep' && this.graphicsQuality !== 'low';
- }
-
- private get shadowQuality() {
- return (
- this.graphicsQuality === 'ultra' ? 16384 :
- this.graphicsQuality === 'high' ? 8192 :
- this.graphicsQuality === 'medium' ? 4096 :
- this.graphicsQuality === 'low' ? 1024 :
- 0); // cheep
- }
-
- constructor(user, isMyRoom, roomInfo: RoomInfo, container: Element, options: Options) {
- this.roomInfo = roomInfo;
- this.graphicsQuality = options.graphicsQuality;
- this.onChangeSelect = options.onChangeSelect;
-
- this.clock = new THREE.Clock(true);
-
- //#region Init a scene
- this.scene = new THREE.Scene();
-
- const width = container.clientWidth;
- const height = container.clientHeight;
-
- //#region Init a renderer
- this.renderer = new THREE.WebGLRenderer({
- antialias: false,
- stencil: false,
- alpha: false,
- powerPreference:
- this.graphicsQuality === 'ultra' ? 'high-performance' :
- this.graphicsQuality === 'high' ? 'high-performance' :
- this.graphicsQuality === 'medium' ? 'default' :
- this.graphicsQuality === 'low' ? 'low-power' :
- 'low-power' // cheep
- });
-
- this.renderer.setPixelRatio(window.devicePixelRatio);
- this.renderer.setSize(width, height);
- this.renderer.autoClear = false;
- this.renderer.setClearColor(new THREE.Color(0x051f2d));
- this.renderer.shadowMap.enabled = this.enableShadow;
- this.renderer.shadowMap.type =
- this.graphicsQuality === 'ultra' ? THREE.PCFSoftShadowMap :
- this.graphicsQuality === 'high' ? THREE.PCFSoftShadowMap :
- this.graphicsQuality === 'medium' ? THREE.PCFShadowMap :
- this.graphicsQuality === 'low' ? THREE.BasicShadowMap :
- THREE.BasicShadowMap; // cheep
-
- container.insertBefore(this.canvas, container.firstChild);
- //#endregion
-
- //#region Init a camera
- this.camera = options.useOrthographicCamera
- ? new THREE.OrthographicCamera(
- width / - 2, width / 2, height / 2, height / - 2, -10, 10)
- : new THREE.PerspectiveCamera(45, width / height);
-
- if (options.useOrthographicCamera) {
- this.camera.position.x = 2;
- this.camera.position.y = 2;
- this.camera.position.z = 2;
- this.camera.zoom = 100;
- this.camera.updateProjectionMatrix();
- } else {
- this.camera.position.x = 5;
- this.camera.position.y = 2;
- this.camera.position.z = 5;
- }
-
- this.scene.add(this.camera);
- //#endregion
-
- //#region AmbientLight
- const ambientLight = new THREE.AmbientLight(0xffffff, 1);
- this.scene.add(ambientLight);
- //#endregion
-
- if (this.graphicsQuality !== 'cheep') {
- //#region Room light
- const roomLight = new THREE.SpotLight(0xffffff, 0.1);
-
- roomLight.position.set(0, 8, 0);
- roomLight.castShadow = this.enableShadow;
- roomLight.shadow.bias = -0.0001;
- roomLight.shadow.mapSize.width = this.shadowQuality;
- roomLight.shadow.mapSize.height = this.shadowQuality;
- roomLight.shadow.camera.near = 0.1;
- roomLight.shadow.camera.far = 9;
- roomLight.shadow.camera.fov = 45;
-
- this.scene.add(roomLight);
- //#endregion
- }
-
- //#region Out light
- const outLight1 = new THREE.SpotLight(0xffffff, 0.4);
- outLight1.position.set(9, 3, -2);
- outLight1.castShadow = this.enableShadow;
- outLight1.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
- outLight1.shadow.mapSize.width = this.shadowQuality;
- outLight1.shadow.mapSize.height = this.shadowQuality;
- outLight1.shadow.camera.near = 6;
- outLight1.shadow.camera.far = 15;
- outLight1.shadow.camera.fov = 45;
- this.scene.add(outLight1);
-
- const outLight2 = new THREE.SpotLight(0xffffff, 0.2);
- outLight2.position.set(-2, 3, 9);
- outLight2.castShadow = false;
- outLight2.shadow.bias = -0.001; // アクネ、アーチファクト対策 その代わりピーターパンが発生する可能性がある
- outLight2.shadow.camera.near = 6;
- outLight2.shadow.camera.far = 15;
- outLight2.shadow.camera.fov = 45;
- this.scene.add(outLight2);
- //#endregion
-
- //#region Init a controller
- this.controls = new OrbitControls(this.camera, this.canvas);
-
- this.controls.target.set(0, 1, 0);
- this.controls.enableZoom = true;
- this.controls.enablePan = isMyRoom;
- this.controls.minPolarAngle = 0;
- this.controls.maxPolarAngle = Math.PI / 2;
- this.controls.minAzimuthAngle = 0;
- this.controls.maxAzimuthAngle = Math.PI / 2;
- this.controls.enableDamping = true;
- this.controls.dampingFactor = 0.2;
- //#endregion
-
- //#region POST FXs
- if (!this.usePostFXs) {
- this.composer = null;
- } else {
- const renderTarget = new THREE.WebGLRenderTarget(width, height, {
- minFilter: THREE.LinearFilter,
- magFilter: THREE.LinearFilter,
- format: THREE.RGBFormat,
- stencilBuffer: false,
- });
-
- const fxaa = new ShaderPass(FXAAShader);
- fxaa.uniforms['resolution'].value = new THREE.Vector2(1 / width, 1 / height);
- fxaa.renderToScreen = true;
-
- this.composer = new EffectComposer(this.renderer, renderTarget);
- this.composer.addPass(new RenderPass(this.scene, this.camera));
- if (this.graphicsQuality === 'ultra') {
- this.composer.addPass(new BloomPass(0.25, 30, 128.0, 512));
- }
- this.composer.addPass(fxaa);
- }
- //#endregion
- //#endregion
-
- //#region Label
- //#region Avatar
- const avatarUrl = `/proxy/?${urlQuery({ url: user.avatarUrl })}`;
-
- const textureLoader = new THREE.TextureLoader();
- textureLoader.crossOrigin = 'anonymous';
-
- const iconTexture = textureLoader.load(avatarUrl);
- iconTexture.wrapS = THREE.RepeatWrapping;
- iconTexture.wrapT = THREE.RepeatWrapping;
- iconTexture.anisotropy = 16;
-
- const avatarMaterial = new THREE.MeshBasicMaterial({
- map: iconTexture,
- side: THREE.DoubleSide,
- alphaTest: 0.5
- });
-
- const iconGeometry = new THREE.PlaneGeometry(1, 1);
-
- const avatarObject = new THREE.Mesh(iconGeometry, avatarMaterial);
- avatarObject.position.set(-3, 2.5, 2);
- avatarObject.rotation.y = Math.PI / 2;
- avatarObject.castShadow = false;
-
- this.scene.add(avatarObject);
- //#endregion
-
- //#region Username
- const name = user.username;
-
- new THREE.FontLoader().load('/assets/fonts/helvetiker_regular.typeface.json', font => {
- const nameGeometry = new THREE.TextGeometry(name, {
- size: 0.5,
- height: 0,
- curveSegments: 8,
- font: font,
- bevelThickness: 0,
- bevelSize: 0,
- bevelEnabled: false
- });
-
- const nameMaterial = new THREE.MeshLambertMaterial({
- color: 0xffffff
- });
-
- const nameObject = new THREE.Mesh(nameGeometry, nameMaterial);
- nameObject.position.set(-3, 2.25, 1.25);
- nameObject.rotation.y = Math.PI / 2;
- nameObject.castShadow = false;
-
- this.scene.add(nameObject);
- });
- //#endregion
- //#endregion
-
- //#region Interaction
- if (isMyRoom) {
- this.furnitureControl = new TransformControls(this.camera, this.canvas);
- this.scene.add(this.furnitureControl);
-
- // Hover highlight
- this.canvas.onmousemove = this.onmousemove;
-
- // Click
- this.canvas.onmousedown = this.onmousedown;
- }
- //#endregion
-
- //#region Init room
- this.loadRoom();
- //#endregion
-
- //#region Load furnitures
- for (const furniture of this.furnitures) {
- this.loadFurniture(furniture).then(obj => {
- this.scene.add(obj.scene);
- this.objects.push(obj.scene);
- });
- }
- //#endregion
-
- // Start render
- if (this.usePostFXs) {
- this.renderWithPostFXs();
- } else {
- this.renderWithoutPostFXs();
- }
- }
-
- @autobind
- private renderWithoutPostFXs() {
- this.renderFrameRequestId =
- window.requestAnimationFrame(this.renderWithoutPostFXs);
-
- // Update animations
- const clock = this.clock.getDelta();
- for (const mixer of this.mixers) {
- mixer.update(clock);
- }
-
- this.controls.update();
- this.renderer.render(this.scene, this.camera);
- }
-
- @autobind
- private renderWithPostFXs() {
- this.renderFrameRequestId =
- window.requestAnimationFrame(this.renderWithPostFXs);
-
- // Update animations
- const clock = this.clock.getDelta();
- for (const mixer of this.mixers) {
- mixer.update(clock);
- }
-
- this.controls.update();
- this.renderer.clear();
- this.composer.render();
- }
-
- @autobind
- private loadRoom() {
- const type = this.roomInfo.roomType;
- new GLTFLoader().load(`/client-assets/room/rooms/${type}/${type}.glb`, gltf => {
- gltf.scene.traverse(child => {
- if (!(child instanceof THREE.Mesh)) return;
-
- child.receiveShadow = this.enableShadow;
-
- child.material = new THREE.MeshLambertMaterial({
- color: (child.material as THREE.MeshStandardMaterial).color,
- map: (child.material as THREE.MeshStandardMaterial).map,
- name: (child.material as THREE.MeshStandardMaterial).name,
- });
-
- // 異方性フィルタリング
- if ((child.material as THREE.MeshLambertMaterial).map && this.graphicsQuality !== 'cheep') {
- (child.material as THREE.MeshLambertMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
- (child.material as THREE.MeshLambertMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
- (child.material as THREE.MeshLambertMaterial).map.anisotropy = 8;
- }
- });
-
- gltf.scene.position.set(0, 0, 0);
-
- this.scene.add(gltf.scene);
- this.roomObj = gltf.scene;
- if (this.roomInfo.roomType === 'default') {
- this.applyCarpetColor();
- }
- });
- }
-
- @autobind
- private loadFurniture(furniture: Furniture) {
- const def = furnitureDefs.find(d => d.id === furniture.type);
- return new Promise<GLTF>((res, rej) => {
- const loader = new GLTFLoader();
- loader.load(`/client-assets/room/furnitures/${furniture.type}/${furniture.type}.glb`, gltf => {
- const model = gltf.scene;
-
- // Load animation
- if (gltf.animations.length > 0) {
- const mixer = new THREE.AnimationMixer(model);
- this.mixers.push(mixer);
- for (const clip of gltf.animations) {
- mixer.clipAction(clip).play();
- }
- }
-
- model.name = furniture.id;
- model.position.x = furniture.position.x;
- model.position.y = furniture.position.y;
- model.position.z = furniture.position.z;
- model.rotation.x = furniture.rotation.x;
- model.rotation.y = furniture.rotation.y;
- model.rotation.z = furniture.rotation.z;
-
- model.traverse(child => {
- if (!(child instanceof THREE.Mesh)) return;
- child.castShadow = this.enableShadow;
- child.receiveShadow = this.enableShadow;
- (child.material as THREE.MeshStandardMaterial).metalness = 0;
-
- // 異方性フィルタリング
- if ((child.material as THREE.MeshStandardMaterial).map && this.graphicsQuality !== 'cheep') {
- (child.material as THREE.MeshStandardMaterial).map.minFilter = THREE.LinearMipMapLinearFilter;
- (child.material as THREE.MeshStandardMaterial).map.magFilter = THREE.LinearMipMapLinearFilter;
- (child.material as THREE.MeshStandardMaterial).map.anisotropy = 8;
- }
- });
-
- if (def.color) { // カスタムカラー
- this.applyCustomColor(model);
- }
-
- if (def.texture) { // カスタムテクスチャ
- this.applyCustomTexture(model);
- }
-
- res(gltf);
- }, null, rej);
- });
- }
-
- @autobind
- private applyCarpetColor() {
- this.roomObj.traverse(child => {
- if (!(child instanceof THREE.Mesh)) return;
- if (child.material &&
- (child.material as THREE.MeshStandardMaterial).name &&
- (child.material as THREE.MeshStandardMaterial).name === 'Carpet'
- ) {
- const colorHex = parseInt(this.roomInfo.carpetColor.substr(1), 16);
- (child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
- }
- });
- }
-
- @autobind
- private applyCustomColor(model: THREE.Object3D) {
- const furniture = this.furnitures.find(furniture => furniture.id === model.name);
- const def = furnitureDefs.find(d => d.id === furniture.type);
- if (def.color == null) return;
- model.traverse(child => {
- if (!(child instanceof THREE.Mesh)) return;
- for (const t of Object.keys(def.color)) {
- if (!child.material ||
- !(child.material as THREE.MeshStandardMaterial).name ||
- (child.material as THREE.MeshStandardMaterial).name !== t
- ) continue;
-
- const prop = def.color[t];
- const val = furniture.props ? furniture.props[prop] : undefined;
-
- if (val == null) continue;
-
- const colorHex = parseInt(val.substr(1), 16);
- (child.material as THREE.MeshStandardMaterial).color.setHex(colorHex);
- }
- });
- }
-
- @autobind
- private applyCustomTexture(model: THREE.Object3D) {
- const furniture = this.furnitures.find(furniture => furniture.id === model.name);
- const def = furnitureDefs.find(d => d.id === furniture.type);
- if (def.texture == null) return;
-
- model.traverse(child => {
- if (!(child instanceof THREE.Mesh)) return;
- for (const t of Object.keys(def.texture)) {
- if (child.name !== t) continue;
-
- const prop = def.texture[t].prop;
- const val = furniture.props ? furniture.props[prop] : undefined;
-
- if (val == null) continue;
-
- const canvas = document.createElement('canvas');
- canvas.height = 1024;
- canvas.width = 1024;
-
- child.material = new THREE.MeshLambertMaterial({
- emissive: 0x111111,
- side: THREE.DoubleSide,
- alphaTest: 0.5,
- });
-
- const img = new Image();
- img.crossOrigin = 'anonymous';
- img.onload = () => {
- const uvInfo = def.texture[t].uv;
-
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img,
- 0, 0, img.width, img.height,
- uvInfo.x, uvInfo.y, uvInfo.width, uvInfo.height);
-
- const texture = new THREE.Texture(canvas);
- texture.wrapS = THREE.RepeatWrapping;
- texture.wrapT = THREE.RepeatWrapping;
- texture.anisotropy = 16;
- texture.flipY = false;
-
- (child.material as THREE.MeshLambertMaterial).map = texture;
- (child.material as THREE.MeshLambertMaterial).needsUpdate = true;
- (child.material as THREE.MeshLambertMaterial).map.needsUpdate = true;
- };
- img.src = val;
- }
- });
- }
-
- @autobind
- private onmousemove(ev: MouseEvent) {
- if (this.isTransformMode) return;
-
- const rect = (ev.target as HTMLElement).getBoundingClientRect();
- const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
- const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
- const pos = new THREE.Vector2(x, y);
-
- this.camera.updateMatrixWorld();
-
- const raycaster = new THREE.Raycaster();
- raycaster.setFromCamera(pos, this.camera);
-
- const intersects = raycaster.intersectObjects(this.objects, true);
-
- for (const object of this.objects) {
- if (this.isSelectedObject(object)) continue;
- object.traverse(child => {
- if (child instanceof THREE.Mesh) {
- (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
- }
- });
- }
-
- if (intersects.length > 0) {
- const intersected = this.getRoot(intersects[0].object);
- if (this.isSelectedObject(intersected)) return;
- intersected.traverse(child => {
- if (child instanceof THREE.Mesh) {
- (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x191919);
- }
- });
- }
- }
-
- @autobind
- private onmousedown(ev: MouseEvent) {
- if (this.isTransformMode) return;
- if (ev.target !== this.canvas || ev.button !== 0) return;
-
- const rect = (ev.target as HTMLElement).getBoundingClientRect();
- const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
- const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
- const pos = new THREE.Vector2(x, y);
-
- this.camera.updateMatrixWorld();
-
- const raycaster = new THREE.Raycaster();
- raycaster.setFromCamera(pos, this.camera);
-
- const intersects = raycaster.intersectObjects(this.objects, true);
-
- for (const object of this.objects) {
- object.traverse(child => {
- if (child instanceof THREE.Mesh) {
- (child.material as THREE.MeshStandardMaterial).emissive.setHex(0x000000);
- }
- });
- }
-
- if (intersects.length > 0) {
- const selectedObj = this.getRoot(intersects[0].object);
- this.selectFurniture(selectedObj);
- } else {
- this.selectedObject = null;
- this.onChangeSelect(null);
- }
- }
-
- @autobind
- private getRoot(obj: THREE.Object3D): THREE.Object3D {
- let found = false;
- let x = obj.parent;
- while (!found) {
- if (x.parent.parent == null) {
- found = true;
- } else {
- x = x.parent;
- }
- }
- return x;
- }
-
- @autobind
- private isSelectedObject(obj: THREE.Object3D): boolean {
- if (this.selectedObject == null) {
- return false;
- } else {
- return obj.name === this.selectedObject.name;
- }
- }
-
- @autobind
- private selectFurniture(obj: THREE.Object3D) {
- this.selectedObject = obj;
- this.onChangeSelect(obj);
- obj.traverse(child => {
- if (child instanceof THREE.Mesh) {
- (child.material as THREE.MeshStandardMaterial).emissive.setHex(0xff0000);
- }
- });
- }
-
- /**
- * 家具の移動/回転モードにします
- * @param type 移動か回転か
- */
- @autobind
- public enterTransformMode(type: 'translate' | 'rotate') {
- this.isTransformMode = true;
- this.furnitureControl.setMode(type);
- this.furnitureControl.attach(this.selectedObject);
- this.controls.enableRotate = false;
- }
-
- /**
- * 家具の移動/回転モードを終了します
- */
- @autobind
- public exitTransformMode() {
- this.isTransformMode = false;
- this.furnitureControl.detach();
- this.controls.enableRotate = true;
- }
-
- /**
- * 家具プロパティを更新します
- * @param key プロパティ名
- * @param value 値
- */
- @autobind
- public updateProp(key: string, value: any) {
- const furniture = this.furnitures.find(furniture => furniture.id === this.selectedObject.name);
- if (furniture.props == null) furniture.props = {};
- furniture.props[key] = value;
- this.applyCustomColor(this.selectedObject);
- this.applyCustomTexture(this.selectedObject);
- }
-
- /**
- * 部屋に家具を追加します
- * @param type 家具の種類
- */
- @autobind
- public addFurniture(type: string) {
- const furniture = {
- id: uuid(),
- type: type,
- position: {
- x: 0,
- y: 0,
- z: 0,
- },
- rotation: {
- x: 0,
- y: 0,
- z: 0,
- },
- };
-
- this.furnitures.push(furniture);
-
- this.loadFurniture(furniture).then(obj => {
- this.scene.add(obj.scene);
- this.objects.push(obj.scene);
- });
- }
-
- /**
- * 現在選択されている家具を部屋から削除します
- */
- @autobind
- public removeFurniture() {
- this.exitTransformMode();
- const obj = this.selectedObject;
- this.scene.remove(obj);
- this.objects = this.objects.filter(object => object.name !== obj.name);
- this.furnitures = this.furnitures.filter(furniture => furniture.id !== obj.name);
- this.selectedObject = null;
- this.onChangeSelect(null);
- }
-
- /**
- * 全ての家具を部屋から削除します
- */
- @autobind
- public removeAllFurnitures() {
- this.exitTransformMode();
- for (const obj of this.objects) {
- this.scene.remove(obj);
- }
- this.objects = [];
- this.furnitures = [];
- this.selectedObject = null;
- this.onChangeSelect(null);
- }
-
- /**
- * 部屋の床の色を変更します
- * @param color 色
- */
- @autobind
- public updateCarpetColor(color: string) {
- this.roomInfo.carpetColor = color;
- this.applyCarpetColor();
- }
-
- /**
- * 部屋の種類を変更します
- * @param type 種類
- */
- @autobind
- public changeRoomType(type: string) {
- this.roomInfo.roomType = type;
- this.scene.remove(this.roomObj);
- this.loadRoom();
- }
-
- /**
- * 部屋データを取得します
- */
- @autobind
- public getRoomInfo() {
- for (const obj of this.objects) {
- const furniture = this.furnitures.find(f => f.id === obj.name);
- furniture.position.x = obj.position.x;
- furniture.position.y = obj.position.y;
- furniture.position.z = obj.position.z;
- furniture.rotation.x = obj.rotation.x;
- furniture.rotation.y = obj.rotation.y;
- furniture.rotation.z = obj.rotation.z;
- }
-
- return this.roomInfo;
- }
-
- /**
- * 選択されている家具を取得します
- */
- @autobind
- public getSelectedObject() {
- return this.selectedObject;
- }
-
- @autobind
- public findFurnitureById(id: string) {
- return this.furnitures.find(furniture => furniture.id === id);
- }
-
- /**
- * レンダリングを終了します
- */
- @autobind
- public destroy() {
- // Stop render loop
- window.cancelAnimationFrame(this.renderFrameRequestId);
-
- this.controls.dispose();
- this.scene.dispose();
- }
-}
diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts
index 6019890444..6bb3f8bf8a 100644
--- a/packages/client/src/scripts/select-file.ts
+++ b/packages/client/src/scripts/select-file.ts
@@ -1,4 +1,5 @@
import * as os from '@/os';
+import { stream } from '@/stream';
import { i18n } from '@/i18n';
import { defaultStore } from '@/store';
import { DriveFile } from 'misskey-js/built/entities';
@@ -48,7 +49,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv
const marker = Math.random().toString(); // TODO: UUIDとか使う
- const connection = os.stream.useChannel('main');
+ const connection = stream.useChannel('main');
connection.on('urlUploadFinished', data => {
if (data.marker === marker) {
res(multiple ? [data.file] : data.file);
diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts
index 3b7f003d0f..85c087331b 100644
--- a/packages/client/src/scripts/theme.ts
+++ b/packages/client/src/scripts/theme.ts
@@ -34,11 +34,11 @@ export const builtinThemes = [
let timeout = null;
export function applyTheme(theme: Theme, persist = true) {
- if (timeout) clearTimeout(timeout);
+ if (timeout) window.clearTimeout(timeout);
document.documentElement.classList.add('_themeChanging_');
- timeout = setTimeout(() => {
+ timeout = window.setTimeout(() => {
document.documentElement.classList.remove('_themeChanging_');
}, 1000);
diff --git a/packages/client/src/scripts/touch.ts b/packages/client/src/scripts/touch.ts
index 06b4f8b2ed..5251bc2e27 100644
--- a/packages/client/src/scripts/touch.ts
+++ b/packages/client/src/scripts/touch.ts
@@ -14,6 +14,10 @@ if (isTouchSupported) {
}, { passive: true });
window.addEventListener('touchend', () => {
+ // 子要素のtouchstartイベントでstopPropagation()が呼ばれると親要素に伝搬されずタッチされたと判定されないため、
+ // touchendイベントでもtouchstartイベントと同様にtrueにする
+ isTouchUsing = true;
+
isScreenTouching = false;
}, { passive: true });
}
diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts
new file mode 100644
index 0000000000..3984256251
--- /dev/null
+++ b/packages/client/src/scripts/use-leave-guard.ts
@@ -0,0 +1,46 @@
+import { inject, onUnmounted, Ref } from 'vue';
+import { onBeforeRouteLeave } from 'vue-router';
+import { i18n } from '@/i18n';
+import * as os from '@/os';
+
+export function useLeaveGuard(enabled: Ref<boolean>) {
+ const setLeaveGuard = inject('setLeaveGuard');
+
+ if (setLeaveGuard) {
+ setLeaveGuard(async () => {
+ if (!enabled.value) return false;
+
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.locale.leaveConfirm,
+ });
+
+ return canceled;
+ });
+ } else {
+ onBeforeRouteLeave(async (to, from) => {
+ if (!enabled.value) return true;
+
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.locale.leaveConfirm,
+ });
+
+ return !canceled;
+ });
+ }
+
+ /*
+ function onBeforeLeave(ev: BeforeUnloadEvent) {
+ if (enabled.value) {
+ ev.preventDefault();
+ ev.returnValue = '';
+ }
+ }
+
+ window.addEventListener('beforeunload', onBeforeLeave);
+ onUnmounted(() => {
+ window.removeEventListener('beforeunload', onBeforeLeave);
+ });
+ */
+}
diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts
new file mode 100644
index 0000000000..bb00e464e3
--- /dev/null
+++ b/packages/client/src/scripts/use-note-capture.ts
@@ -0,0 +1,123 @@
+import { onUnmounted, Ref } from 'vue';
+import * as misskey from 'misskey-js';
+import { stream } from '@/stream';
+import { $i } from '@/account';
+
+export function useNoteCapture(props: {
+ rootEl: Ref<HTMLElement>;
+ appearNote: Ref<misskey.entities.Note>;
+}) {
+ const appearNote = props.appearNote;
+ const connection = $i ? stream : null;
+
+ function onStreamNoteUpdated(data): void {
+ const { type, id, body } = data;
+
+ if (id !== appearNote.value.id) return;
+
+ switch (type) {
+ case 'reacted': {
+ const reaction = body.reaction;
+
+ const updated = JSON.parse(JSON.stringify(appearNote.value));
+
+ if (body.emoji) {
+ const emojis = appearNote.value.emojis || [];
+ if (!emojis.includes(body.emoji)) {
+ updated.emojis = [...emojis, body.emoji];
+ }
+ }
+
+ // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
+ const currentCount = (appearNote.value.reactions || {})[reaction] || 0;
+
+ updated.reactions[reaction] = currentCount + 1;
+
+ if ($i && (body.userId === $i.id)) {
+ updated.myReaction = reaction;
+ }
+
+ appearNote.value = updated;
+ break;
+ }
+
+ case 'unreacted': {
+ const reaction = body.reaction;
+
+ const updated = JSON.parse(JSON.stringify(appearNote.value));
+
+ // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
+ const currentCount = (appearNote.value.reactions || {})[reaction] || 0;
+
+ updated.reactions[reaction] = Math.max(0, currentCount - 1);
+
+ if ($i && (body.userId === $i.id)) {
+ updated.myReaction = null;
+ }
+
+ appearNote.value = updated;
+ break;
+ }
+
+ case 'pollVoted': {
+ const choice = body.choice;
+
+ const updated = JSON.parse(JSON.stringify(appearNote.value));
+
+ const choices = [...appearNote.value.poll.choices];
+ choices[choice] = {
+ ...choices[choice],
+ votes: choices[choice].votes + 1,
+ ...($i && (body.userId === $i.id) ? {
+ isVoted: true
+ } : {})
+ };
+
+ updated.poll.choices = choices;
+
+ appearNote.value = updated;
+ break;
+ }
+
+ case 'deleted': {
+ const updated = JSON.parse(JSON.stringify(appearNote.value));
+ updated.value = true;
+ appearNote.value = updated;
+ break;
+ }
+ }
+ }
+
+ function capture(withHandler = false): void {
+ if (connection) {
+ // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+ connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: appearNote.value.id });
+ if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
+ }
+ }
+
+ function decapture(withHandler = false): void {
+ if (connection) {
+ connection.send('un', {
+ id: appearNote.value.id,
+ });
+ if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated);
+ }
+ }
+
+ function onStreamConnected() {
+ capture(false);
+ }
+
+ capture(true);
+ if (connection) {
+ connection.on('_connected_', onStreamConnected);
+ }
+
+ onUnmounted(() => {
+ decapture(true);
+ if (connection) {
+ connection.off('_connected_', onStreamConnected);
+ }
+ });
+}
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index dc9c3b7b9e..cd358d29d0 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -97,7 +97,7 @@ export const defaultStore = markRaw(new Storage('base', {
tl: {
where: 'deviceAccount',
default: {
- src: 'home',
+ src: 'home' as 'home' | 'local' | 'social' | 'global',
arg: null
}
},
@@ -160,7 +160,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
useReactionPickerForContextMenu: {
where: 'device',
- default: true
+ default: false
},
showGapBetweenNotesInTimeline: {
where: 'device',
@@ -255,10 +255,6 @@ export class ColdDeviceStorage {
sound_chatBg: { type: 'syuilo/waon', volume: 1 },
sound_antenna: { type: 'syuilo/triple', volume: 1 },
sound_channel: { type: 'syuilo/square-pico', volume: 1 },
- sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
- sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
- roomGraphicsQuality: 'medium' as 'cheep' | 'low' | 'medium' | 'high' | 'ultra',
- roomUseOrthographicCamera: true,
};
public static watchers = [];
diff --git a/packages/client/src/stream.ts b/packages/client/src/stream.ts
new file mode 100644
index 0000000000..dea3459b86
--- /dev/null
+++ b/packages/client/src/stream.ts
@@ -0,0 +1,8 @@
+import * as Misskey from 'misskey-js';
+import { markRaw } from 'vue';
+import { $i } from '@/account';
+import { url } from '@/config';
+
+export const stream = markRaw(new Misskey.Stream(url, $i ? {
+ token: $i.token,
+} : null));
diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss
index 181521b4f5..c1d47ffd08 100644
--- a/packages/client/src/style.scss
+++ b/packages/client/src/style.scss
@@ -26,6 +26,7 @@ html {
background-size: cover;
background-position: center;
color: var(--fg);
+ accent-color: var(--accent);
overflow: auto;
overflow-wrap: break-word;
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
@@ -386,16 +387,6 @@ hr {
backdrop-filter: var(--blur, blur(15px));
}
-._inputSplit {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
- grid-gap: 12px;
-
- > * {
- margin: 0 !important;
- }
-}
-
._formBlock {
margin: 1.5em 0;
}
diff --git a/packages/client/src/themes/_dark.json5 b/packages/client/src/themes/_dark.json5
index d8be16f60a..1d87788794 100644
--- a/packages/client/src/themes/_dark.json5
+++ b/packages/client/src/themes/_dark.json5
@@ -69,6 +69,9 @@
success: '#86b300',
error: '#ec4137',
warn: '#ecb637',
+ codeString: '#ffb675',
+ codeNumber: '#cfff9e',
+ codeBoolean: '#c59eff',
htmlThemeColor: '@bg',
X2: ':darken<2<@panel',
X3: 'rgba(255, 255, 255, 0.05)',
diff --git a/packages/client/src/themes/_light.json5 b/packages/client/src/themes/_light.json5
index 251aa36c7a..359b560688 100644
--- a/packages/client/src/themes/_light.json5
+++ b/packages/client/src/themes/_light.json5
@@ -69,6 +69,9 @@
success: '#86b300',
error: '#ec4137',
warn: '#ecb637',
+ codeString: '#b98710',
+ codeNumber: '#0fbbbb',
+ codeBoolean: '#62b70c',
htmlThemeColor: '@bg',
X2: ':darken<2<@panel',
X3: 'rgba(0, 0, 0, 0.05)',
diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue
index 956ec556c1..98069258d9 100644
--- a/packages/client/src/ui/_common_/common.vue
+++ b/packages/client/src/ui/_common_/common.vue
@@ -15,9 +15,10 @@
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import { stream, popup, popups, uploads, pendingApiRequestsCount } from '@/os';
+import { popup, popups, uploads, pendingApiRequestsCount } from '@/os';
import * as sound from '@/scripts/sound';
import { $i } from '@/account';
+import { stream } from '@/stream';
export default defineComponent({
components: {
diff --git a/packages/client/src/ui/_common_/sidebar-for-mobile.vue b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
index 5babdb98a8..afcc50725b 100644
--- a/packages/client/src/ui/_common_/sidebar-for-mobile.vue
+++ b/packages/client/src/ui/_common_/sidebar-for-mobile.vue
@@ -61,7 +61,11 @@ export default defineComponent({
otherMenuItemIndicated,
post: os.post,
search,
- openAccountMenu,
+ openAccountMenu:(ev) => {
+ openAccountMenu({
+ withExtraOperation: true,
+ }, ev);
+ },
more: () => {
os.popup(import('@/components/launch-pad.vue'), {}, {
}, 'closed');
diff --git a/packages/client/src/ui/_common_/sidebar.vue b/packages/client/src/ui/_common_/sidebar.vue
index fa712ba45d..94baacbee9 100644
--- a/packages/client/src/ui/_common_/sidebar.vue
+++ b/packages/client/src/ui/_common_/sidebar.vue
@@ -76,7 +76,11 @@ export default defineComponent({
iconOnly,
post: os.post,
search,
- openAccountMenu,
+ openAccountMenu:(ev) => {
+ openAccountMenu({
+ withExtraOperation: true,
+ }, ev);
+ },
more: () => {
os.popup(import('@/components/launch-pad.vue'), {}, {
}, 'closed');
diff --git a/packages/client/src/ui/_common_/stream-indicator.vue b/packages/client/src/ui/_common_/stream-indicator.vue
index 3f86f94549..5e811e1b88 100644
--- a/packages/client/src/ui/_common_/stream-indicator.vue
+++ b/packages/client/src/ui/_common_/stream-indicator.vue
@@ -8,38 +8,28 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onUnmounted } from 'vue';
+import { stream } from '@/stream';
-export default defineComponent({
- data() {
- return {
- hasDisconnected: false,
- }
- },
- computed: {
- stream() {
- return os.stream;
- },
- },
- created() {
- os.stream.on('_disconnected_', this.onDisconnected);
- },
- beforeUnmount() {
- os.stream.off('_disconnected_', this.onDisconnected);
- },
- methods: {
- onDisconnected() {
- this.hasDisconnected = true;
- },
- resetDisconnected() {
- this.hasDisconnected = false;
- },
- reload() {
- location.reload();
- },
- }
+let hasDisconnected = $ref(false);
+
+function onDisconnected() {
+ hasDisconnected = true;
+}
+
+function resetDisconnected() {
+ hasDisconnected = false;
+}
+
+function reload() {
+ location.reload();
+}
+
+stream.on('_disconnected_', onDisconnected);
+
+onUnmounted(() => {
+ stream.off('_disconnected_', onDisconnected);
});
</script>
diff --git a/packages/client/src/ui/_common_/upload.vue b/packages/client/src/ui/_common_/upload.vue
index a1c5dcdecc..ab7678a505 100644
--- a/packages/client/src/ui/_common_/upload.vue
+++ b/packages/client/src/ui/_common_/upload.vue
@@ -17,18 +17,12 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import * as os from '@/os';
-export default defineComponent({
- data() {
- return {
- uploads: os.uploads,
- zIndex: os.claimZIndex('high'),
- };
- },
-});
+const uploads = os.uploads;
+const zIndex = os.claimZIndex('high');
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/ui/chat/date-separated-list.vue b/packages/client/src/ui/chat/date-separated-list.vue
deleted file mode 100644
index 1a36aca6dd..0000000000
--- a/packages/client/src/ui/chat/date-separated-list.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<script lang="ts">
-import { defineComponent, h, PropType, TransitionGroup } from 'vue';
-import MkAd from '@/components/global/ad.vue';
-
-export default defineComponent({
- props: {
- items: {
- type: Array as PropType<{ id: string; createdAt: string; _shouldInsertAd_: boolean; }[]>,
- required: true,
- },
- reversed: {
- type: Boolean,
- required: false,
- default: false
- },
- ad: {
- type: Boolean,
- required: false,
- default: false
- },
- },
-
- render() {
- const getDateText = (time: string) => {
- const date = new Date(time).getDate();
- const month = new Date(time).getMonth() + 1;
- return this.$t('monthAndDay', {
- month: month.toString(),
- day: date.toString()
- });
- }
-
- return h(this.reversed ? 'div' : TransitionGroup, {
- class: 'hmjzthxl',
- name: this.reversed ? 'list-reversed' : 'list',
- tag: 'div',
- }, this.items.map((item, i) => {
- const el = this.$slots.default({
- item: item
- })[0];
- if (el.key == null && item.id) el.key = item.id;
-
- if (
- i != this.items.length - 1 &&
- new Date(item.createdAt).getDate() != new Date(this.items[i + 1].createdAt).getDate()
- ) {
- const separator = h('div', {
- class: 'separator',
- key: item.id + ':separator',
- }, h('p', {
- class: 'date'
- }, [
- h('span', [
- h('i', {
- class: 'fas fa-angle-up icon',
- }),
- getDateText(item.createdAt)
- ]),
- h('span', [
- getDateText(this.items[i + 1].createdAt),
- h('i', {
- class: 'fas fa-angle-down icon',
- })
- ])
- ]));
-
- return [el, separator];
- } else {
- if (this.ad && item._shouldInsertAd_) {
- return [h(MkAd, {
- class: 'a', // advertiseの意(ブロッカー対策)
- key: item.id + ':ad',
- prefer: ['horizontal', 'horizontal-big'],
- }), el];
- } else {
- return el;
- }
- }
- }));
- },
-});
-</script>
-
-<style lang="scss">
-.hmjzthxl {
- > .list-move {
- transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
- }
- > .list-enter-active {
- transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
- }
- > .list-enter-from {
- opacity: 0;
- transform: translateY(-64px);
- }
-
- > .list-reversed-enter-active, > .list-reversed-leave-active {
- transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
- }
- > .list-reversed-enter-from {
- opacity: 0;
- transform: translateY(64px);
- }
-}
-</style>
-
-<style lang="scss">
-.hmjzthxl {
- > .separator {
- text-align: center;
- position: relative;
-
- &:before {
- content: "";
- display: block;
- position: absolute;
- top: 50%;
- left: 0;
- right: 0;
- margin: auto;
- width: calc(100% - 32px);
- height: 1px;
- background: var(--divider);
- }
-
- > .date {
- display: inline-block;
- position: relative;
- margin: 0;
- padding: 0 16px;
- line-height: 32px;
- text-align: center;
- font-size: 12px;
- color: var(--dateLabelFg);
- background: var(--panel);
-
- > span {
- &:first-child {
- margin-right: 8px;
-
- > .icon {
- margin-right: 8px;
- }
- }
-
- &:last-child {
- margin-left: 8px;
-
- > .icon {
- margin-left: 8px;
- }
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/header-clock.vue b/packages/client/src/ui/chat/header-clock.vue
deleted file mode 100644
index 3488289c21..0000000000
--- a/packages/client/src/ui/chat/header-clock.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="acemodlh _monospace">
- <div>
- <span v-text="y"></span>/<span v-text="m"></span>/<span v-text="d"></span>
- </div>
- <div>
- <span v-text="hh"></span>
- <span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
- <span v-text="mm"></span>
- <span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
- <span v-text="ss"></span>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
-
-export default defineComponent({
- data() {
- return {
- clock: null,
- y: null,
- m: null,
- d: null,
- hh: null,
- mm: null,
- ss: null,
- showColon: true,
- };
- },
- created() {
- this.tick();
- this.clock = setInterval(this.tick, 1000);
- },
- beforeUnmount() {
- clearInterval(this.clock);
- },
- methods: {
- tick() {
- const now = new Date();
- this.y = now.getFullYear().toString();
- this.m = (now.getMonth() + 1).toString().padStart(2, '0');
- this.d = now.getDate().toString().padStart(2, '0');
- this.hh = now.getHours().toString().padStart(2, '0');
- this.mm = now.getMinutes().toString().padStart(2, '0');
- this.ss = now.getSeconds().toString().padStart(2, '0');
- this.showColon = now.getSeconds() % 2 === 0;
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.acemodlh {
- opacity: 0.7;
- font-size: 0.85em;
- line-height: 1em;
- text-align: center;
-}
-</style>
diff --git a/packages/client/src/ui/chat/index.vue b/packages/client/src/ui/chat/index.vue
deleted file mode 100644
index f66ab4dcee..0000000000
--- a/packages/client/src/ui/chat/index.vue
+++ /dev/null
@@ -1,463 +0,0 @@
-<template>
-<div class="mk-app" @contextmenu.self.prevent="onContextmenu">
- <XSidebar ref="menu" class="menu" :default-hidden="true"/>
-
- <div class="nav">
- <header class="header">
- <div class="left">
- <button class="_button account" @click="openAccountMenu">
- <MkAvatar :user="$i" class="avatar"/><!--<MkAcct class="text" :user="$i"/>-->
- </button>
- </div>
- <div class="right">
- <MkA v-tooltip="$ts.messaging" class="item" to="/my/messaging"><i class="fas fa-comments icon"></i><span v-if="$i.hasUnreadMessagingMessage" class="indicator"><i class="fas fa-circle"></i></span></MkA>
- <MkA v-tooltip="$ts.directNotes" class="item" to="/my/messages"><i class="fas fa-envelope icon"></i><span v-if="$i.hasUnreadSpecifiedNotes" class="indicator"><i class="fas fa-circle"></i></span></MkA>
- <MkA v-tooltip="$ts.mentions" class="item" to="/my/mentions"><i class="fas fa-at icon"></i><span v-if="$i.hasUnreadMentions" class="indicator"><i class="fas fa-circle"></i></span></MkA>
- <MkA v-tooltip="$ts.notifications" class="item" to="/my/notifications"><i class="fas fa-bell icon"></i><span v-if="$i.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></MkA>
- </div>
- </header>
- <div class="body">
- <div class="container">
- <div class="header">{{ $ts.timeline }}</div>
- <div class="body">
- <MkA to="/timeline/home" class="item" :class="{ active: tl === 'home' }"><i class="fas fa-home icon"></i>{{ $ts._timelines.home }}</MkA>
- <MkA to="/timeline/local" class="item" :class="{ active: tl === 'local' }"><i class="fas fa-comments icon"></i>{{ $ts._timelines.local }}</MkA>
- <MkA to="/timeline/social" class="item" :class="{ active: tl === 'social' }"><i class="fas fa-share-alt icon"></i>{{ $ts._timelines.social }}</MkA>
- <MkA to="/timeline/global" class="item" :class="{ active: tl === 'global' }"><i class="fas fa-globe icon"></i>{{ $ts._timelines.global }}</MkA>
- </div>
- </div>
- <div v-if="followedChannels" class="container">
- <div class="header">{{ $ts.channel }} ({{ $ts.following }})<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
- <div class="body">
- <MkA v-for="channel in followedChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }`, read: !channel.hasUnreadNote }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
- </div>
- </div>
- <div v-if="featuredChannels" class="container">
- <div class="header">{{ $ts.channel }}<button class="_button add" @click="addChannel"><i class="fas fa-plus"></i></button></div>
- <div class="body">
- <MkA v-for="channel in featuredChannels" :key="channel.id" :to="`/channels/${ channel.id }`" class="item" :class="{ active: tl === `channel:${ channel.id }` }"><i class="fas fa-satellite-dish icon"></i>{{ channel.name }}</MkA>
- </div>
- </div>
- <div v-if="lists" class="container">
- <div class="header">{{ $ts.lists }}<button class="_button add" @click="addList"><i class="fas fa-plus"></i></button></div>
- <div class="body">
- <MkA v-for="list in lists" :key="list.id" :to="`/my/list/${ list.id }`" class="item" :class="{ active: tl === `list:${ list.id }` }"><i class="fas fa-list-ul icon"></i>{{ list.name }}</MkA>
- </div>
- </div>
- <div v-if="antennas" class="container">
- <div class="header">{{ $ts.antennas }}<button class="_button add" @click="addAntenna"><i class="fas fa-plus"></i></button></div>
- <div class="body">
- <MkA v-for="antenna in antennas" :key="antenna.id" :to="`/my/antenna/${ antenna.id }`" class="item" :class="{ active: tl === `antenna:${ antenna.id }` }"><i class="fas fa-satellite icon"></i>{{ antenna.name }}</MkA>
- </div>
- </div>
- <div class="container">
- <div class="body">
- <MkA to="/my/favorites" class="item"><i class="fas fa-star icon"></i>{{ $ts.favorites }}</MkA>
- </div>
- </div>
- <MkAd class="a" :prefer="['square']"/>
- </div>
- <footer class="footer">
- <div class="left">
- <button class="_button menu" @click="showMenu">
- <i class="fas fa-bars icon"></i>
- </button>
- </div>
- <div class="right">
- <button v-tooltip="$ts.search" class="_button item search" @click="search">
- <i class="fas fa-search icon"></i>
- </button>
- <MkA v-tooltip="$ts.settings" class="item" to="/settings"><i class="fas fa-cog icon"></i></MkA>
- </div>
- </footer>
- </div>
-
- <main class="main" @contextmenu.stop="onContextmenu">
- <header class="header">
- <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">
- <keep-alive :include="['timeline']">
- <component :is="Component" :ref="changePage" class="body"/>
- </keep-alive>
- </transition>
- </router-view>
- </main>
-
- <XSide ref="side" class="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
- <div class="side widgets" :class="{ sideViewOpening }">
- <XWidgets/>
- </div>
-
- <XCommon/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import { instanceName, url } from '@/config';
-import XSidebar from '@/ui/_common_/sidebar.vue';
-import XWidgets from './widgets.vue';
-import XCommon from '../_common_/common.vue';
-import XSide from './side.vue';
-import XHeaderClock from './header-clock.vue';
-import * as os from '@/os';
-import { router } from '@/router';
-import { menuDef } from '@/menu';
-import { search } from '@/scripts/search';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
-import { store } from './store';
-import * as symbols from '@/symbols';
-import { openAccountMenu } from '@/account';
-
-export default defineComponent({
- components: {
- XCommon,
- XSidebar,
- XWidgets,
- XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
- XHeaderClock,
- },
-
- provide() {
- return {
- sideViewHook: (path) => {
- this.$refs.side.navigate(path);
- }
- };
- },
-
- data() {
- return {
- pageInfo: null,
- lists: null,
- antennas: null,
- followedChannels: null,
- featuredChannels: null,
- currentChannel: null,
- menuDef: menuDef,
- sideViewOpening: false,
- instanceName,
- };
- },
-
- computed: {
- menu() {
- return [{
- icon: 'fas fa-columns',
- text: this.$ts.openInSideView,
- action: () => {
- this.$refs.side.navigate(this.$route.path);
- }
- }, {
- icon: 'fas fa-window-maximize',
- text: this.$ts.openInWindow,
- action: () => {
- os.pageWindow(this.$route.path);
- }
- }];
- }
- },
-
- created() {
- if (window.innerWidth < 1024) {
- localStorage.setItem('ui', 'default');
- location.reload();
- }
-
- os.api('users/lists/list').then(lists => {
- this.lists = lists;
- });
-
- os.api('antennas/list').then(antennas => {
- this.antennas = antennas;
- });
-
- os.api('channels/followed', { limit: 20 }).then(channels => {
- this.followedChannels = channels;
- });
-
- // TODO: pagination
- os.api('channels/featured', { limit: 20 }).then(channels => {
- this.featuredChannels = channels;
- });
- },
-
- methods: {
- changePage(page) {
- console.log(page);
- if (page == null) return;
- if (page[symbols.PAGE_INFO]) {
- this.pageInfo = page[symbols.PAGE_INFO];
- document.title = `${this.pageInfo.title} | ${instanceName}`;
- }
- },
-
- showMenu() {
- this.$refs.menu.show();
- },
-
- post() {
- os.post();
- },
-
- search() {
- search();
- },
-
- back() {
- history.back();
- },
-
- top() {
- window.scroll({ top: 0, behavior: 'smooth' });
- },
-
- onTransition() {
- if (window._scroll) window._scroll();
- },
-
- onHeaderClick() {
- window.scroll({ top: 0, behavior: 'smooth' });
- },
-
- onContextmenu(e) {
- const isLink = (el: HTMLElement) => {
- if (el.tagName === 'A') return true;
- if (el.parentElement) {
- return isLink(el.parentElement);
- }
- };
- if (isLink(e.target)) return;
- if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
- if (window.getSelection().toString() !== '') return;
- const path = this.$route.path;
- os.contextMenu([{
- type: 'label',
- text: path,
- }, {
- icon: 'fas fa-columns',
- text: this.$ts.openInSideView,
- action: () => {
- this.$refs.side.navigate(path);
- }
- }, {
- icon: 'fas fa-window-maximize',
- text: this.$ts.openInWindow,
- action: () => {
- os.pageWindow(path);
- }
- }], e);
- },
-
- openAccountMenu,
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.mk-app {
- $header-height: 54px; // TODO: どこかに集約したい
- $ui-font-size: 1em; // TODO: どこかに集約したい
-
- // ほんとは単に 100vh と書きたいところだが... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
- height: calc(var(--vh, 1vh) * 100);
- display: flex;
-
- > .nav {
- display: flex;
- flex-direction: column;
- width: 250px;
- height: 100vh;
- border-right: solid 4px var(--divider);
-
- > .header, > .footer {
- $padding: 8px;
- display: flex;
- align-items: center;
- z-index: 1000;
- height: $header-height;
- padding: $padding;
- box-sizing: border-box;
- user-select: none;
-
- &.header {
- border-bottom: solid 0.5px var(--divider);
- }
-
- &.footer {
- border-top: solid 0.5px var(--divider);
- }
-
- > .left, > .right {
- > .item, > .menu {
- display: inline-flex;
- vertical-align: middle;
- height: ($header-height - ($padding * 2));
- width: ($header-height - ($padding * 2));
- box-sizing: border-box;
- //opacity: 0.6;
- position: relative;
- border-radius: 5px;
-
- &:hover {
- background: rgba(0, 0, 0, 0.05);
- }
-
- > .icon {
- margin: auto;
- }
-
- > .indicator {
- position: absolute;
- top: 8px;
- right: 8px;
- color: var(--indicator);
- font-size: 8px;
- line-height: 8px;
- animation: blink 1s infinite;
- }
- }
- }
-
- > .left {
- flex: 1;
- min-width: 0;
-
- > .account {
- display: flex;
- align-items: center;
- padding: 0 8px;
-
- > .avatar {
- width: 26px;
- height: 26px;
- margin-right: 8px;
- }
-
- > .text {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- font-size: 0.9em;
- }
- }
- }
-
- > .right {
- margin-left: auto;
- }
- }
-
- > .body {
- flex: 1;
- min-width: 0;
- overflow: auto;
-
- > .container {
- margin-top: 8px;
- margin-bottom: 8px;
-
- & + .container {
- margin-top: 16px;
- }
-
- > .header {
- display: flex;
- font-size: 0.9em;
- padding: 8px 16px;
- position: sticky;
- top: 0;
- background: var(--X17);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
- z-index: 1;
- color: var(--fgTransparentWeak);
-
- > .add {
- margin-left: auto;
- color: var(--fgTransparentWeak);
-
- &:hover {
- color: var(--fg);
- }
- }
- }
-
- > .body {
- padding: 0 8px;
-
- > .item {
- display: block;
- padding: 6px 8px;
- border-radius: 4px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-
- &:hover {
- text-decoration: none;
- background: rgba(0, 0, 0, 0.05);
- }
-
- &.active, &.active:hover {
- background: var(--accent);
- color: #fff !important;
- }
-
- &.read {
- color: var(--fgTransparent);
- }
-
- > .icon {
- margin-right: 8px;
- opacity: 0.6;
- }
- }
- }
- }
-
- > .a {
- margin: 12px;
- }
- }
- }
-
- > .main {
- display: flex;
- flex: 1;
- flex-direction: column;
- min-width: 0;
- height: 100vh;
- position: relative;
- background: var(--panel);
-
- > .header {
- z-index: 1000;
- height: $header-height;
- background-color: var(--panel);
- border-bottom: solid 0.5px var(--divider);
- user-select: none;
- }
-
- > .body {
- width: 100%;
- box-sizing: border-box;
- overflow: auto;
- }
- }
-
- > .side {
- width: 350px;
- border-left: solid 4px var(--divider);
- background: var(--panel);
-
- &.widgets.sideViewOpening {
- @media (max-width: 1400px) {
- display: none;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/note-header.vue b/packages/client/src/ui/chat/note-header.vue
deleted file mode 100644
index 5f87fdd14e..0000000000
--- a/packages/client/src/ui/chat/note-header.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<template>
-<header class="dehvdgxo">
- <MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
- <MkUserName :user="note.user"/>
- </MkA>
- <span v-if="note.user.isBot" class="is-bot">bot</span>
- <span class="username"><MkAcct :user="note.user"/></span>
- <div class="info">
- <MkA class="created-at" :to="notePage(note)">
- <MkTime :time="note.createdAt"/>
- </MkA>
- <span v-if="note.visibility !== 'public'" class="visibility">
- <i v-if="note.visibility === 'home'" class="fas fa-home"></i>
- <i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
- <i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
- </span>
- <span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span>
- </div>
-</header>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import { notePage } from '@/filters/note';
-import { userPage } from '@/filters/user';
-import * as os from '@/os';
-
-export default defineComponent({
- props: {
- note: {
- type: Object,
- required: true
- },
- },
-
- data() {
- return {
- };
- },
-
- methods: {
- notePage,
- userPage
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.dehvdgxo {
- display: flex;
- align-items: baseline;
- white-space: nowrap;
- font-size: 0.9em;
-
- > .name {
- display: block;
- margin: 0 .5em 0 0;
- padding: 0;
- overflow: hidden;
- font-size: 1em;
- font-weight: bold;
- text-decoration: none;
- text-overflow: ellipsis;
-
- &:hover {
- text-decoration: underline;
- }
- }
-
- > .is-bot {
- flex-shrink: 0;
- align-self: center;
- margin: 0 .5em 0 0;
- padding: 1px 6px;
- font-size: 80%;
- border: solid 0.5px var(--divider);
- border-radius: 3px;
- }
-
- > .username {
- margin: 0 .5em 0 0;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- > .info {
- font-size: 0.9em;
- opacity: 0.7;
-
- > .visibility {
- margin-left: 8px;
- }
-
- > .localOnly {
- margin-left: 8px;
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/note-preview.vue b/packages/client/src/ui/chat/note-preview.vue
deleted file mode 100644
index c28591815e..0000000000
--- a/packages/client/src/ui/chat/note-preview.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<template>
-<div class="hduudsxk">
- <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 v-if="note.cw != ''" class="text">{{ note.cw }}</span>
- <XCwButton v-model="showContent" :note="note"/>
- </p>
- <div v-show="note.cw == null || showContent" class="content">
- <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 '@/components/cw-button.vue';
-import * as os from '@/os';
-
-export default defineComponent({
- components: {
- XNoteHeader,
- XSubNoteContent,
- XCwButton,
- },
-
- props: {
- note: {
- type: Object,
- required: true
- }
- },
-
- data() {
- return {
- showContent: false
- };
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.hduudsxk {
- display: flex;
- margin: 0;
- padding: 0;
- overflow: hidden;
- font-size: 0.95em;
-
- > .avatar {
-
- @media (min-width: 350px) {
- margin: 0 10px 0 0;
- width: 44px;
- height: 44px;
- }
-
- @media (min-width: 500px) {
- 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/packages/client/src/ui/chat/note.sub.vue b/packages/client/src/ui/chat/note.sub.vue
deleted file mode 100644
index b61b7521a8..0000000000
--- a/packages/client/src/ui/chat/note.sub.vue
+++ /dev/null
@@ -1,137 +0,0 @@
-<template>
-<div class="wrpstxzv" :class="{ children }">
- <div class="main">
- <MkAvatar class="avatar" :user="note.user"/>
- <div class="body">
- <XNoteHeader class="header" :note="note" :mini="true"/>
- <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="showContent" :note="note"/>
- </p>
- <div v-show="note.cw == null || showContent" class="content">
- <XSubNote-content class="text" :note="note"/>
- </div>
- </div>
- </div>
- </div>
- <XSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :children="true"/>
-</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 '@/components/cw-button.vue';
-import * as os from '@/os';
-
-export default defineComponent({
- name: 'XSub',
-
- components: {
- XNoteHeader,
- XSubNoteContent,
- XCwButton,
- },
-
- props: {
- note: {
- type: Object,
- required: true
- },
- detail: {
- type: Boolean,
- required: false,
- default: false
- },
- children: {
- type: Boolean,
- required: false,
- default: false
- },
- // TODO
- truncate: {
- type: Boolean,
- default: true
- }
- },
-
- data() {
- return {
- showContent: false,
- replies: [],
- };
- },
-
- created() {
- if (this.detail) {
- os.api('notes/children', {
- noteId: this.note.id,
- limit: 5
- }).then(replies => {
- this.replies = replies;
- });
- }
- },
-});
-</script>
-
-<style lang="scss" scoped>
-.wrpstxzv {
- padding: 16px 16px;
- font-size: 0.8em;
-
- &.children {
- padding: 10px 0 0 16px;
- font-size: 1em;
- }
-
- > .main {
- display: flex;
-
- > .avatar {
- flex-shrink: 0;
- display: block;
- margin: 0 8px 0 0;
- width: 36px;
- height: 36px;
- }
-
- > .body {
- 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 {
- margin: 0;
- padding: 0;
- }
- }
- }
- }
- }
-
- > .reply {
- border-left: solid 0.5px var(--divider);
- margin-top: 10px;
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/note.vue b/packages/client/src/ui/chat/note.vue
deleted file mode 100644
index 6927dd0eaf..0000000000
--- a/packages/client/src/ui/chat/note.vue
+++ /dev/null
@@ -1,1142 +0,0 @@
-<template>
-<div
- v-if="!muted"
- v-show="!isDeleted"
- v-hotkey="keymap"
- class="vfzoeqcg"
- :tabindex="!isDeleted ? '-1' : null"
- :class="{ renote: isRenote, highlighted: appearNote._prId_ || appearNote._featuredId_, operating }"
->
- <XSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
- <div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ $ts.pinnedNote }}</div>
- <div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ $ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ $ts.hideThisNote }} <i class="fas fa-times"></i></button></div>
- <div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ $ts.featured }}</div>
- <div v-if="isRenote" class="renote">
- <MkAvatar class="avatar" :user="note.user"/>
- <i class="fas fa-retweet"></i>
- <I18n :src="$ts.renotedBy" tag="span">
- <template #user>
- <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
- <MkUserName :user="note.user"/>
- </MkA>
- </template>
- </I18n>
- <div class="info">
- <button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
- <i v-if="isMyRenote" class="fas fa-ellipsis-h dropdownIcon"></i>
- <MkTime :time="note.createdAt"/>
- </button>
- <span v-if="note.visibility !== 'public'" class="visibility">
- <i v-if="note.visibility === 'home'" class="fas fa-home"></i>
- <i v-else-if="note.visibility === 'followers'" class="fas fa-unlock"></i>
- <i v-else-if="note.visibility === 'specified'" class="fas fa-envelope"></i>
- </span>
- <span v-if="note.localOnly" class="localOnly"><i class="fas fa-biohazard"></i></span>
- </div>
- </div>
- <article class="article" @contextmenu.stop="onContextmenu">
- <MkAvatar class="avatar" :user="appearNote.user"/>
- <div class="main">
- <XNoteHeader class="header" :note="appearNote" :mini="true"/>
- <MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
- <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="showContent" :note="appearNote"/>
- </p>
- <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed }">
- <div class="text">
- <span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
- <MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
- <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
- <a v-if="appearNote.renote != null" class="rp">RN:</a>
- </div>
- <div v-if="appearNote.files.length > 0" class="files">
- <XMediaList :media-list="appearNote.files"/>
- </div>
- <XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
- <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
- <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
- <button v-if="collapsed" class="fade _button" @click="collapsed = false">
- <span>{{ $ts.showMore }}</span>
- </button>
- </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>
- <XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
- <footer class="footer _panel">
- <button v-tooltip="$ts.reply" class="button _button" @click="reply()">
- <template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
- <template v-else><i class="fas fa-reply"></i></template>
- <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
- </button>
- <button v-if="canRenote" ref="renoteButton" v-tooltip="$ts.renote" class="button _button" @click="renote()">
- <i class="fas fa-retweet"></i><p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
- </button>
- <button v-else class="button _button">
- <i class="fas fa-ban"></i>
- </button>
- <button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip="$ts.reaction" class="button _button" @click="react()">
- <i class="fas fa-plus"></i>
- </button>
- <button v-if="appearNote.myReaction != null" ref="reactButton" v-tooltip="$ts.reaction" class="button _button reacted" @click="undoReact(appearNote)">
- <i class="fas fa-minus"></i>
- </button>
- <button ref="menuButton" class="button _button" @click="menu()">
- <i class="fas fa-ellipsis-h"></i>
- </button>
- </footer>
- </div>
- </article>
-</div>
-<div v-else class="muted" @click="muted = false">
- <I18n :src="$ts.userSaysSomething" tag="small">
- <template #name>
- <MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
- <MkUserName :user="appearNote.user"/>
- </MkA>
- </template>
- </I18n>
-</div>
-</template>
-
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
-import * as mfm from 'mfm-js';
-import { sum } from '@/scripts/array';
-import XSub from './note.sub.vue';
-import XNoteHeader from './note-header.vue';
-import XNoteSimple from './note-preview.vue';
-import XReactionsViewer from '@/components/reactions-viewer.vue';
-import XMediaList from '@/components/media-list.vue';
-import XCwButton from '@/components/cw-button.vue';
-import XPoll from '@/components/poll.vue';
-import { pleaseLogin } from '@/scripts/please-login';
-import { focusPrev, focusNext } from '@/scripts/focus';
-import { url } from '@/config';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
-import { checkWordMute } from '@/scripts/check-word-mute';
-import { userPage } from '@/filters/user';
-import * as os from '@/os';
-import { noteActions, noteViewInterruptors } from '@/store';
-import { reactionPicker } from '@/scripts/reaction-picker';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
-
-export default defineComponent({
- components: {
- XSub,
- XNoteHeader,
- XNoteSimple,
- XReactionsViewer,
- XMediaList,
- XCwButton,
- XPoll,
- MkUrlPreview: defineAsyncComponent(() => import('@/components/url-preview.vue')),
- MkInstanceTicker: defineAsyncComponent(() => import('@/components/instance-ticker.vue')),
- },
-
- inject: {
- inChannel: {
- default: null
- },
- },
-
- props: {
- note: {
- type: Object,
- required: true
- },
- pinned: {
- type: Boolean,
- required: false,
- default: false
- },
- },
-
- emits: ['update:note'],
-
- data() {
- return {
- connection: null,
- replies: [],
- showContent: false,
- collapsed: false,
- isDeleted: false,
- muted: false,
- operating: false,
- };
- },
-
- computed: {
- rs() {
- return this.$store.state.reactions;
- },
- keymap(): any {
- return {
- 'r': () => this.reply(true),
- 'e|a|plus': () => this.react(true),
- 'q': () => this.renote(true),
- 'f|b': this.favorite,
- 'delete|ctrl+d': this.del,
- 'ctrl+q': this.renoteDirectly,
- 'up|k|shift+tab': this.focusBefore,
- 'down|j|tab': this.focusAfter,
- 'esc': this.blur,
- 'm|o': () => this.menu(true),
- 's': this.toggleShowContent,
- '1': () => this.reactDirectly(this.rs[0]),
- '2': () => this.reactDirectly(this.rs[1]),
- '3': () => this.reactDirectly(this.rs[2]),
- '4': () => this.reactDirectly(this.rs[3]),
- '5': () => this.reactDirectly(this.rs[4]),
- '6': () => this.reactDirectly(this.rs[5]),
- '7': () => this.reactDirectly(this.rs[6]),
- '8': () => this.reactDirectly(this.rs[7]),
- '9': () => this.reactDirectly(this.rs[8]),
- '0': () => this.reactDirectly(this.rs[9]),
- };
- },
-
- isRenote(): boolean {
- return (this.note.renote &&
- this.note.text == null &&
- this.note.fileIds.length == 0 &&
- this.note.poll == null);
- },
-
- appearNote(): any {
- return this.isRenote ? this.note.renote : this.note;
- },
-
- isMyNote(): boolean {
- return this.$i && (this.$i.id === this.appearNote.userId);
- },
-
- isMyRenote(): boolean {
- return this.$i && (this.$i.id === this.note.userId);
- },
-
- canRenote(): boolean {
- return ['public', 'home'].includes(this.appearNote.visibility) || this.isMyNote;
- },
-
- reactionsCount(): number {
- return this.appearNote.reactions
- ? sum(Object.values(this.appearNote.reactions))
- : 0;
- },
-
- urls(): string[] {
- if (this.appearNote.text) {
- return extractUrlFromMfm(mfm.parse(this.appearNote.text));
- } else {
- return null;
- }
- },
-
- showTicker() {
- if (this.$store.state.instanceTicker === 'always') return true;
- if (this.$store.state.instanceTicker === 'remote' && this.appearNote.user.instance) return true;
- return false;
- }
- },
-
- async created() {
- if (this.$i) {
- this.connection = os.stream;
- }
-
- this.collapsed = this.appearNote.cw == null && this.appearNote.text && (
- (this.appearNote.text.split('\n').length > 9) ||
- (this.appearNote.text.length > 500)
- );
- this.muted = await checkWordMute(this.appearNote, this.$i, this.$store.state.mutedWords);
-
- // plugin
- if (noteViewInterruptors.length > 0) {
- let result = this.note;
- for (const interruptor of noteViewInterruptors) {
- result = await interruptor.handler(JSON.parse(JSON.stringify(result)));
- }
- this.$emit('update:note', Object.freeze(result));
- }
- },
-
- mounted() {
- this.capture(true);
-
- if (this.$i) {
- this.connection.on('_connected_', this.onStreamConnected);
- }
- },
-
- beforeUnmount() {
- this.decapture(true);
-
- if (this.$i) {
- this.connection.off('_connected_', this.onStreamConnected);
- }
- },
-
- methods: {
- updateAppearNote(v) {
- this.$emit('update:note', Object.freeze(this.isRenote ? {
- ...this.note,
- renote: {
- ...this.note.renote,
- ...v
- }
- } : {
- ...this.note,
- ...v
- }));
- },
-
- readPromo() {
- os.api('promo/read', {
- noteId: this.appearNote.id
- });
- this.isDeleted = true;
- },
-
- capture(withHandler = false) {
- if (this.$i) {
- // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
- this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
- if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
- }
- },
-
- decapture(withHandler = false) {
- if (this.$i) {
- this.connection.send('un', {
- id: this.appearNote.id
- });
- if (withHandler) this.connection.off('noteUpdated', this.onStreamNoteUpdated);
- }
- },
-
- onStreamConnected() {
- this.capture();
- },
-
- onStreamNoteUpdated(data) {
- const { type, id, body } = data;
-
- if (id !== this.appearNote.id) return;
-
- switch (type) {
- case 'reacted': {
- const reaction = body.reaction;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- if (body.emoji) {
- const emojis = this.appearNote.emojis || [];
- if (!emojis.includes(body.emoji)) {
- n.emojis = [...emojis, body.emoji];
- }
- }
-
- // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
- const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
- // Increment the count
- n.reactions = {
- ...this.appearNote.reactions,
- [reaction]: currentCount + 1
- };
-
- if (body.userId === this.$i.id) {
- n.myReaction = reaction;
- }
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'unreacted': {
- const reaction = body.reaction;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる
- const currentCount = (this.appearNote.reactions || {})[reaction] || 0;
-
- // Decrement the count
- n.reactions = {
- ...this.appearNote.reactions,
- [reaction]: Math.max(0, currentCount - 1)
- };
-
- if (body.userId === this.$i.id) {
- n.myReaction = null;
- }
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'pollVoted': {
- const choice = body.choice;
-
- // DeepではなくShallowコピーであることに注意。n.reactions[reaction] = hogeとかしないように(親からもらったデータをミューテートすることになるので)
- let n = {
- ...this.appearNote,
- };
-
- const choices = [...this.appearNote.poll.choices];
- choices[choice] = {
- ...choices[choice],
- votes: choices[choice].votes + 1,
- ...(body.userId === this.$i.id ? {
- isVoted: true
- } : {})
- };
-
- n.poll = {
- ...this.appearNote.poll,
- choices: choices
- };
-
- this.updateAppearNote(n);
- break;
- }
-
- case 'deleted': {
- this.isDeleted = true;
- break;
- }
- }
- },
-
- reply(viaKeyboard = false) {
- pleaseLogin();
- this.operating = true;
- os.post({
- reply: this.appearNote,
- animation: !viaKeyboard,
- }, () => {
- this.operating = false;
- this.focus();
- });
- },
-
- renote(viaKeyboard = false) {
- pleaseLogin();
- this.operating = true;
- this.blur();
- os.popupMenu([{
- text: this.$ts.renote,
- icon: 'fas fa-retweet',
- action: () => {
- os.api('notes/create', {
- renoteId: this.appearNote.id
- });
- }
- }, {
- text: this.$ts.quote,
- icon: 'fas fa-quote-right',
- action: () => {
- os.post({
- renote: this.appearNote,
- });
- }
- }], this.$refs.renoteButton, {
- viaKeyboard
- }).then(() => {
- this.operating = false;
- });
- },
-
- renoteDirectly() {
- os.apiWithDialog('notes/create', {
- renoteId: this.appearNote.id
- }, undefined, (res: any) => {
- os.alert({
- type: 'success',
- text: this.$ts.renoted,
- });
- }, (e: Error) => {
- if (e.id === 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4') {
- os.alert({
- type: 'error',
- text: this.$ts.cantRenote,
- });
- } else if (e.id === 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a') {
- os.alert({
- type: 'error',
- text: this.$ts.cantReRenote,
- });
- }
- });
- },
-
- async react(viaKeyboard = false) {
- pleaseLogin();
- this.operating = true;
- this.blur();
- reactionPicker.show(this.$refs.reactButton, reaction => {
- os.api('notes/reactions/create', {
- noteId: this.appearNote.id,
- reaction: reaction
- });
- }, () => {
- this.operating = false;
- this.focus();
- });
- },
-
- reactDirectly(reaction) {
- os.api('notes/reactions/create', {
- noteId: this.appearNote.id,
- reaction: reaction
- });
- },
-
- undoReact(note) {
- const oldReaction = note.myReaction;
- if (!oldReaction) return;
- os.api('notes/reactions/delete', {
- noteId: note.id
- });
- },
-
- favorite() {
- pleaseLogin();
- os.apiWithDialog('notes/favorites/create', {
- noteId: this.appearNote.id
- }, undefined, (res: any) => {
- os.alert({
- type: 'success',
- text: this.$ts.favorited,
- });
- }, (e: Error) => {
- if (e.id === 'a402c12b-34dd-41d2-97d8-4d2ffd96a1a6') {
- os.alert({
- type: 'error',
- text: this.$ts.alreadyFavorited,
- });
- } else if (e.id === '6dd26674-e060-4816-909a-45ba3f4da458') {
- os.alert({
- type: 'error',
- text: this.$ts.cantFavorite,
- });
- }
- });
- },
-
- del() {
- os.confirm({
- type: 'warning',
- text: this.$ts.noteDeleteConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
-
- os.api('notes/delete', {
- noteId: this.appearNote.id
- });
- });
- },
-
- delEdit() {
- os.confirm({
- type: 'warning',
- text: this.$ts.deleteAndEditConfirm,
- }).then(({ canceled }) => {
- if (canceled) return;
-
- os.api('notes/delete', {
- noteId: this.appearNote.id
- });
-
- os.post({ initialNote: this.appearNote, renote: this.appearNote.renote, reply: this.appearNote.reply, channel: this.appearNote.channel });
- });
- },
-
- toggleFavorite(favorite: boolean) {
- os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
- noteId: this.appearNote.id
- });
- },
-
- toggleWatch(watch: boolean) {
- os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
- noteId: this.appearNote.id
- });
- },
-
- getMenu() {
- let menu;
- if (this.$i) {
- const statePromise = os.api('notes/state', {
- noteId: this.appearNote.id
- });
-
- menu = [{
- icon: 'fas fa-copy',
- text: this.$ts.copyContent,
- action: this.copyContent
- }, {
- icon: 'fas fa-link',
- text: this.$ts.copyLink,
- action: this.copyLink
- }, (this.appearNote.url || this.appearNote.uri) ? {
- icon: 'fas fa-external-link-square-alt',
- text: this.$ts.showOnRemote,
- action: () => {
- window.open(this.appearNote.url || this.appearNote.uri, '_blank');
- }
- } : undefined,
- {
- icon: 'fas fa-share-alt',
- text: this.$ts.share,
- action: this.share
- },
- null,
- statePromise.then(state => state.isFavorited ? {
- icon: 'fas fa-star',
- text: this.$ts.unfavorite,
- action: () => this.toggleFavorite(false)
- } : {
- icon: 'fas fa-star',
- text: this.$ts.favorite,
- action: () => this.toggleFavorite(true)
- }),
- {
- icon: 'fas fa-paperclip',
- text: this.$ts.clip,
- action: () => this.clip()
- },
- (this.appearNote.userId != this.$i.id) ? statePromise.then(state => state.isWatching ? {
- icon: 'fas fa-eye-slash',
- text: this.$ts.unwatch,
- action: () => this.toggleWatch(false)
- } : {
- icon: 'fas fa-eye',
- text: this.$ts.watch,
- action: () => this.toggleWatch(true)
- }) : undefined,
- this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? {
- icon: 'fas fa-thumbtack',
- text: this.$ts.unpin,
- action: () => this.togglePin(false)
- } : {
- icon: 'fas fa-thumbtack',
- text: this.$ts.pin,
- action: () => this.togglePin(true)
- } : undefined,
- /*
- ...(this.$i.isModerator || this.$i.isAdmin ? [
- null,
- {
- icon: 'fas fa-bullhorn',
- text: this.$ts.promote,
- action: this.promote
- }]
- : []
- ),*/
- ...(this.appearNote.userId != this.$i.id ? [
- null,
- {
- icon: 'fas fa-exclamation-circle',
- text: this.$ts.reportAbuse,
- action: () => {
- const u = `${url}/notes/${this.appearNote.id}`;
- os.popup(import('@/components/abuse-report-window.vue'), {
- user: this.appearNote.user,
- initialComment: `Note: ${u}\n-----\n`
- }, {}, 'closed');
- }
- }]
- : []
- ),
- ...(this.appearNote.userId == this.$i.id || this.$i.isModerator || this.$i.isAdmin ? [
- null,
- this.appearNote.userId == this.$i.id ? {
- icon: 'fas fa-edit',
- text: this.$ts.deleteAndEdit,
- action: this.delEdit
- } : undefined,
- {
- icon: 'fas fa-trash-alt',
- text: this.$ts.delete,
- danger: true,
- action: this.del
- }]
- : []
- )]
- .filter(x => x !== undefined);
- } else {
- menu = [{
- icon: 'fas fa-copy',
- text: this.$ts.copyContent,
- action: this.copyContent
- }, {
- icon: 'fas fa-link',
- text: this.$ts.copyLink,
- action: this.copyLink
- }, (this.appearNote.url || this.appearNote.uri) ? {
- icon: 'fas fa-external-link-square-alt',
- text: this.$ts.showOnRemote,
- action: () => {
- window.open(this.appearNote.url || this.appearNote.uri, '_blank');
- }
- } : undefined]
- .filter(x => x !== undefined);
- }
-
- if (noteActions.length > 0) {
- menu = menu.concat([null, ...noteActions.map(action => ({
- icon: 'fas fa-plug',
- text: action.title,
- action: () => {
- action.handler(this.appearNote);
- }
- }))]);
- }
-
- return menu;
- },
-
- onContextmenu(e) {
- const isLink = (el: HTMLElement) => {
- if (el.tagName === 'A') return true;
- if (el.parentElement) {
- return isLink(el.parentElement);
- }
- };
- if (isLink(e.target)) return;
- if (window.getSelection().toString() !== '') return;
-
- if (this.$store.state.useReactionPickerForContextMenu) {
- e.preventDefault();
- this.react();
- } else {
- os.contextMenu(this.getMenu(), e).then(this.focus);
- }
- },
-
- menu(viaKeyboard = false) {
- this.operating = true;
- os.popupMenu(this.getMenu(), this.$refs.menuButton, {
- viaKeyboard
- }).then(() => {
- this.operating = false;
- this.focus();
- });
- },
-
- showRenoteMenu(viaKeyboard = false) {
- if (!this.isMyRenote) return;
- os.popupMenu([{
- text: this.$ts.unrenote,
- icon: 'fas fa-trash-alt',
- danger: true,
- action: () => {
- os.api('notes/delete', {
- noteId: this.note.id
- });
- this.isDeleted = true;
- }
- }], this.$refs.renoteTime, {
- viaKeyboard: viaKeyboard
- });
- },
-
- toggleShowContent() {
- this.showContent = !this.showContent;
- },
-
- copyContent() {
- copyToClipboard(this.appearNote.text);
- os.success();
- },
-
- copyLink() {
- copyToClipboard(`${url}/notes/${this.appearNote.id}`);
- os.success();
- },
-
- togglePin(pin: boolean) {
- os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
- noteId: this.appearNote.id
- }, undefined, null, e => {
- if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
- os.alert({
- type: 'error',
- text: this.$ts.pinLimitExceeded
- });
- }
- });
- },
-
- async clip() {
- const clips = await os.api('clips/list');
- os.popupMenu([{
- icon: 'fas fa-plus',
- text: this.$ts.createNew,
- action: async () => {
- const { canceled, result } = await os.form(this.$ts.createNewClip, {
- name: {
- type: 'string',
- label: this.$ts.name
- },
- description: {
- type: 'string',
- required: false,
- multiline: true,
- label: this.$ts.description
- },
- isPublic: {
- type: 'boolean',
- label: this.$ts.public,
- default: false
- }
- });
- if (canceled) return;
-
- const clip = await os.apiWithDialog('clips/create', result);
-
- os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
- }
- }, null, ...clips.map(clip => ({
- text: clip.name,
- action: () => {
- os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id });
- }
- }))], this.$refs.menuButton, {
- }).then(this.focus);
- },
-
- async promote() {
- const { canceled, result: days } = await os.inputNumber({
- title: this.$ts.numberOfDays,
- });
-
- if (canceled) return;
-
- os.apiWithDialog('admin/promo/create', {
- noteId: this.appearNote.id,
- expiresAt: Date.now() + (86400000 * days)
- });
- },
-
- share() {
- navigator.share({
- title: this.$t('noteOf', { user: this.appearNote.user.name }),
- text: this.appearNote.text,
- url: `${url}/notes/${this.appearNote.id}`
- });
- },
-
- focus() {
- this.$el.focus();
- },
-
- blur() {
- this.$el.blur();
- },
-
- focusBefore() {
- focusPrev(this.$el);
- },
-
- focusAfter() {
- focusNext(this.$el);
- },
-
- userPage
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.vfzoeqcg {
- position: relative;
- contain: content;
-
- // これらの指定はパフォーマンス向上には有効だが、ノートの高さは一定でないため、
- // 下の方までスクロールすると上のノートの高さがここで決め打ちされたものに変化し、表示しているノートの位置が変わってしまう
- // ノートがマウントされたときに自身の高さを取得し contain-intrinsic-size を設定しなおせばほぼ解決できそうだが、
- // 今度はその処理自体がパフォーマンス低下の原因にならないか懸念される。また、被リアクションでも高さは変化するため、やはり多少のズレは生じる
- // 一度レンダリングされた要素はブラウザがよしなにサイズを覚えておいてくれるような実装になるまで待った方が良さそう(なるのか?)
- //content-visibility: auto;
- //contain-intrinsic-size: 0 128px;
-
- &:focus-visible {
- outline: none;
- }
-
- &:hover {
- background: rgba(0, 0, 0, 0.05);
- }
-
- &:hover, &.operating {
- > .article > .main > .footer {
- display: block;
- }
- }
-
- &.renote {
- background: rgba(128, 255, 0, 0.05);
- }
-
- &.highlighted {
- background: rgba(255, 128, 0, 0.05);
- }
-
- > .info {
- display: flex;
- align-items: center;
- padding: 12px 16px 4px 16px;
- line-height: 24px;
- font-size: 85%;
- white-space: pre;
- color: #d28a3f;
-
- > i {
- margin-right: 4px;
- }
-
- > .hide {
- margin-left: 16px;
- color: inherit;
- opacity: 0.7;
- }
- }
-
- > .info + .article {
- padding-top: 8px;
- }
-
- > .reply-to {
- opacity: 0.7;
- padding-bottom: 0;
- }
-
- > .renote {
- display: flex;
- align-items: center;
- padding: 12px 16px 4px 16px;
- line-height: 28px;
- white-space: pre;
- color: var(--renote);
- font-size: 0.9em;
-
- > .avatar {
- flex-shrink: 0;
- display: inline-block;
- width: 28px;
- height: 28px;
- margin: 0 8px 0 0;
- border-radius: 6px;
- }
-
- > i {
- margin-right: 4px;
- }
-
- > span {
- overflow: hidden;
- flex-shrink: 1;
- text-overflow: ellipsis;
- white-space: nowrap;
-
- > .name {
- font-weight: bold;
- }
- }
-
- > .info {
- margin-left: 8px;
- font-size: 0.9em;
- opacity: 0.7;
-
- > .time {
- flex-shrink: 0;
- color: inherit;
-
- > .dropdownIcon {
- margin-right: 4px;
- }
- }
-
- > .visibility {
- margin-left: 8px;
- }
-
- > .localOnly {
- margin-left: 8px;
- }
- }
- }
-
- > .renote + .article {
- padding-top: 8px;
- }
-
- > .article {
- display: flex;
- padding: 12px 16px;
-
- > .avatar {
- flex-shrink: 0;
- display: block;
- position: sticky;
- top: 0;
- margin: 0 14px 0 0;
- width: 46px;
- height: 46px;
- }
-
- > .main {
- flex: 1;
- min-width: 0;
-
- > .body {
- > .cw {
- cursor: default;
- display: block;
- margin: 0;
- padding: 0;
- overflow-wrap: break-word;
-
- > .text {
- margin-right: 8px;
- }
- }
-
- > .content {
- &.collapsed {
- position: relative;
- max-height: 9em;
- overflow: hidden;
-
- > .fade {
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 64px;
- background: linear-gradient(0deg, var(--panel), var(--X15));
-
- > span {
- display: inline-block;
- background: var(--panel);
- padding: 6px 10px;
- font-size: 0.8em;
- border-radius: 999px;
- box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
- }
-
- &:hover {
- > span {
- background: var(--panelHighlight);
- }
- }
- }
- }
-
- > .text {
- overflow-wrap: break-word;
-
- > .reply {
- color: var(--accent);
- margin-right: 0.5em;
- }
-
- > .rp {
- margin-left: 4px;
- font-style: oblique;
- color: var(--renote);
- }
- }
-
- > .files {
- max-width: 500px;
- }
-
- > .url-preview {
- margin-top: 8px;
- max-width: 500px;
- }
-
- > .poll {
- font-size: 80%;
- max-width: 500px;
- }
-
- > .renote {
- padding: 8px 0;
-
- > * {
- padding: 16px;
- border: dashed 1px var(--renote);
- border-radius: 8px;
- }
- }
- }
-
- > .channel {
- opacity: 0.7;
- font-size: 80%;
- }
- }
-
- > .footer {
- display: none;
- position: absolute;
- top: 8px;
- right: 8px;
- padding: 0 6px;
- opacity: 0.7;
-
- &:hover {
- opacity: 1;
- }
-
- > .button {
- margin: 0;
- padding: 8px;
- opacity: 0.7;
-
- &:hover {
- color: var(--accent);
- }
-
- > .count {
- display: inline;
- margin: 0 0 0 8px;
- opacity: 0.7;
- }
-
- &.reacted {
- color: var(--accent);
- }
- }
- }
- }
- }
-
- > .reply {
- border-top: solid 0.5px var(--divider);
- }
-}
-
-.muted {
- padding: 8px 16px;
- opacity: 0.7;
-
- &:hover {
- background: rgba(0, 0, 0, 0.05);
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/notes.vue b/packages/client/src/ui/chat/notes.vue
deleted file mode 100644
index 51d4afcf54..0000000000
--- a/packages/client/src/ui/chat/notes.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-<template>
-<div class="">
- <div v-if="empty" class="_fullinfo">
- <img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
- <div>{{ $ts.noNotes }}</div>
- </div>
-
- <MkLoading v-if="fetching"/>
-
- <MkError v-if="error" @retry="init()"/>
-
- <div v-show="more && reversed" style="margin-bottom: var(--margin);">
- <MkButton style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
- </MkButton>
- </div>
-
- <XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="reversed ? 'up' : 'down'" :reversed="reversed" :ad="true">
- <XNote :key="note._featuredId_ || note._prId_ || note.id" :note="note" @update:note="updated(note, $event)"/>
- </XList>
-
- <div v-show="more && !reversed" style="margin-top: var(--margin);">
- <MkButton v-appear="$store.state.enableInfiniteScroll ? fetchMore : null" style="margin: 0 auto;" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" @click="fetchMore">
- <template v-if="!moreFetching">{{ $ts.loadMore }}</template>
- <template v-if="moreFetching"><MkLoading inline/></template>
- </MkButton>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import paging from '@/scripts/paging';
-import XNote from './note.vue';
-import XList from './date-separated-list.vue';
-import MkButton from '@/components/ui/button.vue';
-
-export default defineComponent({
- components: {
- XNote, XList, MkButton,
- },
-
- mixins: [
- paging({
- before: (self) => {
- self.$emit('before');
- },
-
- after: (self, e) => {
- self.$emit('after', e);
- }
- }),
- ],
-
- props: {
- pagination: {
- required: true
- },
-
- prop: {
- type: String,
- required: false
- }
- },
-
- emits: ['before', 'after'],
-
- computed: {
- notes(): any[] {
- return this.prop ? this.items.map(item => item[this.prop]) : this.items;
- },
-
- reversed(): boolean {
- return this.pagination.reversed;
- }
- },
-
- methods: {
- updated(oldValue, newValue) {
- const i = this.notes.findIndex(n => n === oldValue);
- if (this.prop) {
- this.items[i][this.prop] = newValue;
- } else {
- this.items[i] = newValue;
- }
- },
-
- focus() {
- this.$refs.notes.focus();
- }
- }
-});
-</script>
diff --git a/packages/client/src/ui/chat/pages/channel.vue b/packages/client/src/ui/chat/pages/channel.vue
deleted file mode 100644
index 13c735cd47..0000000000
--- a/packages/client/src/ui/chat/pages/channel.vue
+++ /dev/null
@@ -1,258 +0,0 @@
-<template>
-<div v-if="channel" class="hhizbblb">
- <div v-if="date" class="info">
- <MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
- </div>
- <div ref="body" class="tl">
- <div v-if="queue > 0" class="new" :style="{ width: width + 'px', bottom: bottom + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
- <XNotes ref="tl" v-follow="true" class="tl" :pagination="pagination" @queue="queueUpdated"/>
- </div>
- <div class="bottom">
- <div v-if="typers.length > 0" class="typers">
- <I18n :src="$ts.typingUsers" text-tag="span" class="users">
- <template #users>
- <b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
- </template>
- </I18n>
- <MkEllipsis/>
- </div>
- <XPostForm :channel="channel"/>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import * as Misskey from 'misskey-js';
-import XNotes from '../notes.vue';
-import * as os from '@/os';
-import * as sound from '@/scripts/sound';
-import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
-import follow from '@/directives/follow-append';
-import XPostForm from '../post-form.vue';
-import MkInfo from '@/components/ui/info.vue';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- XNotes,
- XPostForm,
- MkInfo,
- },
-
- directives: {
- follow
- },
-
- provide() {
- return {
- inChannel: true
- };
- },
-
- props: {
- channelId: {
- type: String,
- required: true
- },
- },
-
- data() {
- return {
- channel: null as Misskey.entities.Channel | null,
- connection: null,
- pagination: null,
- baseQuery: {
- includeMyRenotes: this.$store.state.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.showLocalRenotes
- },
- queue: 0,
- width: 0,
- top: 0,
- bottom: 0,
- typers: [],
- date: null,
- [symbols.PAGE_INFO]: computed(() => ({
- title: this.channel ? this.channel.name : '-',
- subtitle: this.channel ? this.channel.description : '-',
- icon: 'fas fa-satellite-dish',
- actions: [{
- icon: this.channel?.isFollowing ? 'fas fa-star' : 'far fa-star',
- text: this.channel?.isFollowing ? this.$ts.unfollow : this.$ts.follow,
- highlighted: this.channel?.isFollowing,
- handler: this.toggleChannelFollow
- }, {
- icon: 'fas fa-search',
- text: this.$ts.inChannelSearch,
- handler: this.inChannelSearch
- }, {
- icon: 'fas fa-calendar-alt',
- text: this.$ts.jumpToSpecifiedDate,
- handler: this.timetravel
- }]
- })),
- };
- },
-
- async created() {
- this.channel = await os.api('channels/show', { channelId: this.channelId });
-
- const prepend = note => {
- (this.$refs.tl as any).prepend(note);
-
- this.$emit('note');
-
- sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
- };
-
- this.connection = markRaw(os.stream.useChannel('channel', {
- channelId: this.channelId
- }));
- this.connection.on('note', prepend);
- this.connection.on('typers', typers => {
- this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers;
- });
-
- this.pagination = {
- endpoint: 'channels/timeline',
- reversed: true,
- limit: 10,
- params: init => ({
- channelId: this.channelId,
- untilDate: this.date?.getTime(),
- ...this.baseQuery
- })
- };
- },
-
- mounted() {
-
- },
-
- beforeUnmount() {
- this.connection.dispose();
- },
-
- methods: {
- focus() {
- this.$refs.body.focus();
- },
-
- goTop() {
- const container = getScrollContainer(this.$refs.body);
- container.scrollTop = 0;
- },
-
- queueUpdated(q) {
- if (this.$refs.body.offsetWidth !== 0) {
- const rect = this.$refs.body.getBoundingClientRect();
- this.width = this.$refs.body.offsetWidth;
- this.top = rect.top;
- this.bottom = this.$refs.body.offsetHeight;
- }
- this.queue = q;
- },
-
- async inChannelSearch() {
- const { canceled, result: query } = await os.inputText({
- title: this.$ts.inChannelSearch,
- });
- if (canceled || query == null || query === '') return;
- router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.channelId}`);
- },
-
- async toggleChannelFollow() {
- if (this.channel.isFollowing) {
- await os.apiWithDialog('channels/unfollow', {
- channelId: this.channel.id
- });
- this.channel.isFollowing = false;
- } else {
- await os.apiWithDialog('channels/follow', {
- channelId: this.channel.id
- });
- this.channel.isFollowing = true;
- }
- },
-
- openChannelMenu(ev) {
- os.popupMenu([{
- text: this.$ts.copyUrl,
- icon: 'fas fa-link',
- action: () => {
- copyToClipboard(`${url}/channels/${this.currentChannel.id}`);
- }
- }], ev.currentTarget || ev.target);
- },
-
- timetravel(date?: Date) {
- this.date = date;
- this.$refs.tl.reload();
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.hhizbblb {
- display: flex;
- flex-direction: column;
- flex: 1;
- overflow: auto;
-
- > .info {
- padding: 16px 16px 0 16px;
- }
-
- > .top {
- padding: 16px 16px 0 16px;
- }
-
- > .bottom {
- padding: 0 16px 16px 16px;
- position: relative;
-
- > .typers {
- position: absolute;
- bottom: 100%;
- padding: 0 8px 0 8px;
- font-size: 0.9em;
- background: var(--panel);
- border-radius: 0 8px 0 0;
- color: var(--fgTransparentWeak);
-
- > .users {
- > .user + .user:before {
- content: ", ";
- font-weight: normal;
- }
-
- > .user:last-of-type:after {
- content: " ";
- }
- }
- }
- }
-
- > .tl {
- position: relative;
- padding: 16px 0;
- flex: 1;
- min-width: 0;
- overflow: auto;
-
- > .new {
- position: fixed;
- z-index: 1000;
-
- > button {
- display: block;
- margin: 16px auto;
- padding: 8px 16px;
- border-radius: 32px;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/pages/timeline.vue b/packages/client/src/ui/chat/pages/timeline.vue
deleted file mode 100644
index 07e847ad73..0000000000
--- a/packages/client/src/ui/chat/pages/timeline.vue
+++ /dev/null
@@ -1,221 +0,0 @@
-<template>
-<div class="dbiokgaf">
- <div v-if="date" class="info">
- <MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
- </div>
- <div class="top">
- <XPostForm/>
- </div>
- <div ref="body" class="tl">
- <div v-if="queue > 0" class="new" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
- <XNotes ref="tl" class="tl" :pagination="pagination" @queue="queueUpdated"/>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { computed, defineComponent, markRaw } from 'vue';
-import XNotes from '../notes.vue';
-import * as os from '@/os';
-import * as sound from '@/scripts/sound';
-import { scrollToBottom, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
-import follow from '@/directives/follow-append';
-import XPostForm from '../post-form.vue';
-import MkInfo from '@/components/ui/info.vue';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- XNotes,
- XPostForm,
- MkInfo,
- },
-
- directives: {
- follow
- },
-
- props: {
- src: {
- type: String,
- required: true
- },
- },
-
- data() {
- return {
- connection: null,
- connection2: null,
- pagination: null,
- baseQuery: {
- includeMyRenotes: this.$store.state.showMyRenotes,
- includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
- includeLocalRenotes: this.$store.state.showLocalRenotes
- },
- query: {},
- queue: 0,
- width: 0,
- top: 0,
- bottom: 0,
- typers: [],
- date: null,
- [symbols.PAGE_INFO]: computed(() => ({
- title: this.$ts.timeline,
- icon: 'fas fa-home',
- actions: [{
- icon: 'fas fa-calendar-alt',
- text: this.$ts.jumpToSpecifiedDate,
- handler: this.timetravel
- }]
- })),
- };
- },
-
- created() {
- const prepend = note => {
- (this.$refs.tl as any).prepend(note);
-
- this.$emit('note');
-
- sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
- };
-
- const onChangeFollowing = () => {
- if (!this.$refs.tl.backed) {
- this.$refs.tl.reload();
- }
- };
-
- let endpoint;
-
- if (this.src == 'home') {
- endpoint = 'notes/timeline';
- this.connection = markRaw(os.stream.useChannel('homeTimeline'));
- this.connection.on('note', prepend);
-
- this.connection2 = markRaw(os.stream.useChannel('main'));
- this.connection2.on('follow', onChangeFollowing);
- this.connection2.on('unfollow', onChangeFollowing);
- } else if (this.src == 'local') {
- endpoint = 'notes/local-timeline';
- this.connection = markRaw(os.stream.useChannel('localTimeline'));
- this.connection.on('note', prepend);
- } else if (this.src == 'social') {
- endpoint = 'notes/hybrid-timeline';
- this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
- this.connection.on('note', prepend);
- } else if (this.src == 'global') {
- endpoint = 'notes/global-timeline';
- this.connection = markRaw(os.stream.useChannel('globalTimeline'));
- this.connection.on('note', prepend);
- }
-
- this.pagination = {
- endpoint: endpoint,
- limit: 10,
- params: init => ({
- untilDate: this.date?.getTime(),
- ...this.baseQuery, ...this.query
- })
- };
- },
-
- mounted() {
-
- },
-
- beforeUnmount() {
- this.connection.dispose();
- if (this.connection2) this.connection2.dispose();
- },
-
- methods: {
- focus() {
- this.$refs.body.focus();
- },
-
- goTop() {
- const container = getScrollContainer(this.$refs.body);
- container.scrollTop = 0;
- },
-
- queueUpdated(q) {
- if (this.$refs.body.offsetWidth !== 0) {
- const rect = this.$refs.body.getBoundingClientRect();
- this.width = this.$refs.body.offsetWidth;
- this.top = rect.top;
- this.bottom = this.$refs.body.offsetHeight;
- }
- this.queue = q;
- },
-
- timetravel(date?: Date) {
- this.date = date;
- this.$refs.tl.reload();
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.dbiokgaf {
- display: flex;
- flex-direction: column;
- flex: 1;
- overflow: auto;
-
- > .info {
- padding: 16px 16px 0 16px;
- }
-
- > .top {
- padding: 16px 16px 0 16px;
- }
-
- > .bottom {
- padding: 0 16px 16px 16px;
- position: relative;
-
- > .typers {
- position: absolute;
- bottom: 100%;
- padding: 0 8px 0 8px;
- font-size: 0.9em;
- background: var(--panel);
- border-radius: 0 8px 0 0;
- color: var(--fgTransparentWeak);
-
- > .users {
- > .user + .user:before {
- content: ", ";
- font-weight: normal;
- }
-
- > .user:last-of-type:after {
- content: " ";
- }
- }
- }
- }
-
- > .tl {
- position: relative;
- padding: 16px 0;
- flex: 1;
- min-width: 0;
- overflow: auto;
-
- > .new {
- position: fixed;
- z-index: 1000;
-
- > button {
- display: block;
- margin: 16px auto;
- padding: 8px 16px;
- border-radius: 32px;
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/post-form.vue b/packages/client/src/ui/chat/post-form.vue
deleted file mode 100644
index 8c572e3b1c..0000000000
--- a/packages/client/src/ui/chat/post-form.vue
+++ /dev/null
@@ -1,769 +0,0 @@
-<template>
-<div class="pxiwixjf"
- @dragover.stop="onDragover"
- @dragenter="onDragenter"
- @dragleave="onDragleave"
- @drop.stop="onDrop"
->
- <div class="form">
- <div v-if="quoteId" class="with-quote"><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>
- <div class="visibleUsers">
- <span v-for="u in visibleUsers" :key="u.id">
- <MkAcct :user="u"/>
- <button class="_button" @click="removeVisibleUser(u)"><i class="fas fa-times"></i></button>
- </span>
- <button class="_buttonPrimary" @click="addVisibleUser"><i class="fas fa-plus fa-fw"></i></button>
- </div>
- </div>
- <input v-show="useCw" ref="cw" v-model="cw" class="cw" :placeholder="$ts.annotation" @keydown="onKeydown">
- <textarea ref="text" v-model="text" class="text" :class="{ withCw: useCw }" :disabled="posting" :placeholder="placeholder" @keydown="onKeydown" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
- <XPostFormAttaches class="attaches" :files="files" @updated="updateFiles" @detach="detachFile" @changeSensitive="updateFileSensitive" @changeName="updateFileName"/>
- <XPollEditor v-if="poll" :poll="poll" @destroyed="poll = null" @updated="onPollUpdate"/>
- <footer>
- <div class="left">
- <button v-tooltip="$ts.attachFile" class="_button" @click="chooseFileFrom"><i class="fas fa-photo-video"></i></button>
- <button v-tooltip="$ts.poll" class="_button" :class="{ active: poll }" @click="togglePoll"><i class="fas fa-poll-h"></i></button>
- <button v-tooltip="$ts.useCw" class="_button" :class="{ active: useCw }" @click="useCw = !useCw"><i class="fas fa-eye-slash"></i></button>
- <button v-tooltip="$ts.mention" class="_button" @click="insertMention"><i class="fas fa-at"></i></button>
- <button v-tooltip="$ts.emoji" class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
- <button v-if="postFormActions.length > 0" v-tooltip="$ts.plugin" class="_button" @click="showActions"><i class="fas fa-plug"></i></button>
- </div>
- <div class="right">
- <span class="text-count" :class="{ over: textLength > max }">{{ max - textLength }}</span>
- <span v-if="localOnly" class="local-only"><i class="fas fa-biohazard"></i></span>
- <button ref="visibilityButton" v-tooltip="$ts.visibility" class="_button visibility" :disabled="channel != null" @click="setVisibility">
- <span v-if="visibility === 'public'"><i class="fas fa-globe"></i></span>
- <span v-if="visibility === 'home'"><i class="fas fa-home"></i></span>
- <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">{{ submitText }}<i :class="reply ? 'fas fa-reply' : renote ? 'fas fa-quote-right' : 'fas fa-paper-plane'"></i></button>
- </div>
- </footer>
- </div>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import insertTextAtCursor from 'insert-text-at-cursor';
-import { length } from 'stringz';
-import { toASCII } from 'punycode/';
-import * as mfm from 'mfm-js';
-import { host, url } from '@/config';
-import { erase, unique } from '@/scripts/array';
-import { extractMentions } from '@/scripts/extract-mentions';
-import * as Acct from 'misskey-js/built/acct';
-import { formatTimeString } from '@/scripts/format-time-string';
-import { Autocomplete } from '@/scripts/autocomplete';
-import * as os from '@/os';
-import { selectFiles } from '@/scripts/select-file';
-import { notePostInterruptors, postFormActions } from '@/store';
-import { throttle } from 'throttle-debounce';
-
-export default defineComponent({
- components: {
- XPostFormAttaches: defineAsyncComponent(() => import('@/components/post-form-attaches.vue')),
- XPollEditor: defineAsyncComponent(() => import('@/components/poll-editor.vue'))
- },
-
- props: {
- reply: {
- type: Object,
- required: false
- },
- renote: {
- type: Object,
- required: false
- },
- channel: {
- type: String,
- required: false
- },
- mention: {
- type: Object,
- required: false
- },
- specified: {
- type: Object,
- required: false
- },
- initialText: {
- type: String,
- required: false
- },
- initialNote: {
- type: Object,
- required: false
- },
- share: {
- type: Boolean,
- required: false,
- default: false
- },
- autofocus: {
- type: Boolean,
- required: false,
- default: false
- },
- },
-
- emits: ['posted', 'cancel', 'esc'],
-
- data() {
- return {
- posting: false,
- text: '',
- files: [],
- poll: null,
- useCw: 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,
- visibleUsers: [],
- autocomplete: null,
- draghover: false,
- quoteId: null,
- recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
- imeText: '',
- typing: throttle(3000, () => {
- if (this.channel) {
- os.stream.send('typingOnChannel', { channel: this.channel });
- }
- }),
- postFormActions,
- };
- },
-
- computed: {
- draftKey(): string {
- let key = this.channel ? `channel:${this.channel}` : '';
-
- if (this.renote) {
- key += `renote:${this.renote.id}`;
- } else if (this.reply) {
- key += `reply:${this.reply.id}`;
- } else {
- key += 'note';
- }
-
- return key;
- },
-
- placeholder(): string {
- if (this.renote) {
- return this.$ts._postForm.quotePlaceholder;
- } else if (this.reply) {
- return this.$ts._postForm.replyPlaceholder;
- } else if (this.channel) {
- return this.$ts._postForm.channelPlaceholder;
- } else {
- const xs = [
- this.$ts._postForm._placeholders.a,
- this.$ts._postForm._placeholders.b,
- this.$ts._postForm._placeholders.c,
- this.$ts._postForm._placeholders.d,
- this.$ts._postForm._placeholders.e,
- this.$ts._postForm._placeholders.f
- ];
- return xs[Math.floor(Math.random() * xs.length)];
- }
- },
-
- submitText(): string {
- return this.renote
- ? this.$ts.quote
- : this.reply
- ? this.$ts.reply
- : this.$ts.note;
- },
-
- textLength(): number {
- return length((this.text + this.imeText).trim());
- },
-
- canPost(): boolean {
- return !this.posting &&
- (1 <= this.textLength || 1 <= this.files.length || !!this.poll || !!this.renote) &&
- (this.textLength <= this.max) &&
- (!this.poll || this.poll.choices.length >= 2);
- },
-
- max(): number {
- return this.$instance ? this.$instance.maxNoteTextLength : 1000;
- }
- },
-
- mounted() {
- if (this.initialText) {
- this.text = this.initialText;
- }
-
- if (this.mention) {
- this.text = this.mention.host ? `@${this.mention.username}@${toASCII(this.mention.host)}` : `@${this.mention.username}`;
- this.text += ' ';
- }
-
- if (this.reply && (this.reply.user.username != this.$i.username || (this.reply.user.host != null && this.reply.user.host != host))) {
- this.text = `@${this.reply.user.username}${this.reply.user.host != null ? '@' + toASCII(this.reply.user.host) : ''} `;
- }
-
- if (this.reply && this.reply.text != null) {
- const ast = mfm.parse(this.reply.text);
-
- for (const x of extractMentions(ast)) {
- const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
-
- // 自分は除外
- if (this.$i.username == x.username && x.host == null) continue;
- if (this.$i.username == x.username && x.host == host) continue;
-
- // 重複は除外
- if (this.text.indexOf(`${mention} `) != -1) continue;
-
- this.text += `${mention} `;
- }
- }
-
- if (this.channel) {
- this.visibility = 'public';
- this.localOnly = true; // TODO: チャンネルが連合するようになった折には消す
- }
-
- // 公開以外へのリプライ時は元の公開範囲を引き継ぐ
- if (this.reply && ['home', 'followers', 'specified'].includes(this.reply.visibility)) {
- this.visibility = this.reply.visibility;
- if (this.reply.visibility === 'specified') {
- os.api('users/show', {
- userIds: this.reply.visibleUserIds.filter(uid => uid !== this.$i.id && uid !== this.reply.userId)
- }).then(users => {
- this.visibleUsers.push(...users);
- });
-
- if (this.reply.userId !== this.$i.id) {
- os.api('users/show', { userId: this.reply.userId }).then(user => {
- this.visibleUsers.push(user);
- });
- }
- }
- }
-
- if (this.specified) {
- this.visibility = 'specified';
- this.visibleUsers.push(this.specified);
- }
-
- // keep cw when reply
- if (this.$store.state.keepCw && this.reply && this.reply.cw) {
- this.useCw = true;
- this.cw = this.reply.cw;
- }
-
- if (this.autofocus) {
- this.focus();
-
- this.$nextTick(() => {
- this.focus();
- });
- }
-
- // TODO: detach when unmount
- new Autocomplete(this.$refs.text, this, { model: 'text' });
- new Autocomplete(this.$refs.cw, this, { model: 'cw' });
-
- this.$nextTick(() => {
- // 書きかけの投稿を復元
- if (!this.share && !this.mention && !this.specified) {
- const draft = JSON.parse(localStorage.getItem('drafts') || '{}')[this.draftKey];
- if (draft) {
- this.text = draft.data.text;
- this.useCw = draft.data.useCw;
- this.cw = draft.data.cw;
- this.visibility = draft.data.visibility;
- this.localOnly = draft.data.localOnly;
- this.files = (draft.data.files || []).filter(e => e);
- if (draft.data.poll) {
- this.poll = draft.data.poll;
- }
- }
- }
-
- // 削除して編集
- if (this.initialNote) {
- const init = this.initialNote;
- this.text = init.text ? init.text : '';
- this.files = init.files;
- this.cw = init.cw;
- this.useCw = init.cw != null;
- if (init.poll) {
- this.poll = init.poll;
- }
- this.visibility = init.visibility;
- this.localOnly = init.localOnly;
- this.quoteId = init.renote ? init.renote.id : null;
- }
-
- this.$nextTick(() => this.watch());
- });
- },
-
- methods: {
- watch() {
- this.$watch('text', () => this.saveDraft());
- this.$watch('useCw', () => this.saveDraft());
- this.$watch('cw', () => this.saveDraft());
- this.$watch('poll', () => this.saveDraft());
- this.$watch('files', () => this.saveDraft(), { deep: true });
- this.$watch('visibility', () => this.saveDraft());
- this.$watch('localOnly', () => this.saveDraft());
- },
-
- togglePoll() {
- if (this.poll) {
- this.poll = null;
- } else {
- this.poll = {
- choices: ['', ''],
- multiple: false,
- expiresAt: null,
- expiredAfter: null,
- };
- }
- },
-
- addTag(tag: string) {
- insertTextAtCursor(this.$refs.text, ` #${tag} `);
- },
-
- focus() {
- (this.$refs.text as any).focus();
- },
-
- chooseFileFrom(ev) {
- selectFiles(ev.currentTarget || ev.target, this.$ts.attachFile).then(files => {
- for (const file of files) {
- this.files.push(file);
- }
- });
- },
-
- detachFile(id) {
- this.files = this.files.filter(x => x.id != id);
- },
-
- updateFiles(files) {
- this.files = files;
- },
-
- updateFileSensitive(file, sensitive) {
- this.files[this.files.findIndex(x => x.id === file.id)].isSensitive = sensitive;
- },
-
- updateFileName(file, name) {
- this.files[this.files.findIndex(x => x.id === file.id)].name = name;
- },
-
- upload(file: File, name?: string) {
- os.upload(file, this.$store.state.uploadFolder, name).then(res => {
- this.files.push(res);
- });
- },
-
- onPollUpdate(poll) {
- this.poll = poll;
- this.saveDraft();
- },
-
- setVisibility() {
- if (this.channel) {
- // TODO: information dialog
- return;
- }
-
- os.popup(import('@/components/visibility-picker.vue'), {
- currentVisibility: this.visibility,
- currentLocalOnly: this.localOnly,
- src: this.$refs.visibilityButton
- }, {
- changeVisibility: visibility => {
- this.visibility = visibility;
- if (this.$store.state.rememberNoteVisibility) {
- this.$store.set('visibility', visibility);
- }
- },
- changeLocalOnly: localOnly => {
- this.localOnly = localOnly;
- if (this.$store.state.rememberNoteVisibility) {
- this.$store.set('localOnly', localOnly);
- }
- }
- }, 'closed');
- },
-
- addVisibleUser() {
- os.selectUser().then(user => {
- this.visibleUsers.push(user);
- });
- },
-
- removeVisibleUser(user) {
- this.visibleUsers = erase(user, this.visibleUsers);
- },
-
- clear() {
- this.text = '';
- this.files = [];
- this.poll = null;
- this.quoteId = null;
- },
-
- onKeydown(e: KeyboardEvent) {
- if ((e.which === 10 || e.which === 13) && (e.ctrlKey || e.metaKey) && this.canPost) this.post();
- if (e.which === 27) this.$emit('esc');
- this.typing();
- },
-
- onCompositionUpdate(e: CompositionEvent) {
- this.imeText = e.data;
- this.typing();
- },
-
- onCompositionEnd(e: CompositionEvent) {
- this.imeText = '';
- },
-
- async onPaste(e: ClipboardEvent) {
- for (const { item, i } of Array.from(e.clipboardData.items).map((item, i) => ({item, i}))) {
- if (item.kind == 'file') {
- const file = item.getAsFile();
- const lio = file.name.lastIndexOf('.');
- const ext = lio >= 0 ? file.name.slice(lio) : '';
- const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
- this.upload(file, formatted);
- }
- }
-
- const paste = e.clipboardData.getData('text');
-
- if (!this.renote && !this.quoteId && paste.startsWith(url + '/notes/')) {
- e.preventDefault();
-
- os.confirm({
- type: 'info',
- text: this.$ts.quoteQuestion,
- }).then(({ canceled }) => {
- if (canceled) {
- insertTextAtCursor(this.$refs.text, paste);
- return;
- }
-
- this.quoteId = paste.substr(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
- });
- }
- },
-
- onDragover(e) {
- if (!e.dataTransfer.items[0]) return;
- const isFile = e.dataTransfer.items[0].kind == 'file';
- const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
- if (isFile || isDriveFile) {
- e.preventDefault();
- this.draghover = true;
- e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
- }
- },
-
- onDragenter(e) {
- this.draghover = true;
- },
-
- onDragleave(e) {
- this.draghover = false;
- },
-
- onDrop(e): void {
- this.draghover = false;
-
- // ファイルだったら
- if (e.dataTransfer.files.length > 0) {
- e.preventDefault();
- for (const x of Array.from(e.dataTransfer.files)) this.upload(x);
- return;
- }
-
- //#region ドライブのファイル
- const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
- if (driveFile != null && driveFile != '') {
- const file = JSON.parse(driveFile);
- this.files.push(file);
- e.preventDefault();
- }
- //#endregion
- },
-
- saveDraft() {
- const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
- data[this.draftKey] = {
- updatedAt: new Date(),
- data: {
- text: this.text,
- useCw: this.useCw,
- cw: this.cw,
- visibility: this.visibility,
- localOnly: this.localOnly,
- files: this.files,
- poll: this.poll
- }
- };
-
- localStorage.setItem('drafts', JSON.stringify(data));
- },
-
- deleteDraft() {
- const data = JSON.parse(localStorage.getItem('drafts') || '{}');
-
- delete data[this.draftKey];
-
- localStorage.setItem('drafts', JSON.stringify(data));
- },
-
- async post() {
- let data = {
- text: this.text == '' ? undefined : this.text,
- fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
- replyId: this.reply ? this.reply.id : undefined,
- renoteId: this.renote ? this.renote.id : this.quoteId ? this.quoteId : undefined,
- channelId: this.channel ? this.channel : undefined,
- poll: this.poll,
- cw: this.useCw ? this.cw || '' : undefined,
- localOnly: this.localOnly,
- visibility: this.visibility,
- visibleUserIds: this.visibility == 'specified' ? this.visibleUsers.map(u => u.id) : undefined,
- };
-
- // plugin
- if (notePostInterruptors.length > 0) {
- for (const interruptor of notePostInterruptors) {
- data = await interruptor.handler(JSON.parse(JSON.stringify(data)));
- }
- }
-
- this.posting = true;
- os.api('notes/create', data).then(() => {
- this.clear();
- this.$nextTick(() => {
- this.deleteDraft();
- this.$emit('posted');
- if (this.text && this.text != '') {
- const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
- const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
- localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
- }
- this.posting = false;
- });
- }).catch(err => {
- this.posting = false;
- os.alert({
- type: 'error',
- text: err.message + '\n' + (err as any).id,
- });
- });
- },
-
- cancel() {
- this.$emit('cancel');
- },
-
- insertMention() {
- os.selectUser().then(user => {
- insertTextAtCursor(this.$refs.text, '@' + Acct.toString(user) + ' ');
- });
- },
-
- async insertEmoji(ev) {
- os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
- },
-
- showActions(ev) {
- os.popupMenu(postFormActions.map(action => ({
- text: action.title,
- action: () => {
- action.handler({
- text: this.text
- }, (key, value) => {
- if (key === 'text') { this.text = value; }
- });
- }
- })), ev.currentTarget || ev.target);
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.pxiwixjf {
- position: relative;
- border: solid 0.5px var(--divider);
- border-radius: 8px;
-
- > .form {
- > .preview {
- padding: 16px;
- }
-
- > .with-quote {
- margin: 0 0 8px 0;
- color: var(--accent);
-
- > button {
- padding: 4px 8px;
- color: var(--accentAlpha04);
-
- &:hover {
- color: var(--accentAlpha06);
- }
-
- &:active {
- color: var(--accentDarken30);
- }
- }
- }
-
- > .to-specified {
- padding: 6px 24px;
- margin-bottom: 8px;
- overflow: auto;
- white-space: nowrap;
-
- > .visibleUsers {
- display: inline;
- top: -1px;
- font-size: 14px;
-
- > button {
- padding: 4px;
- border-radius: 8px;
- }
-
- > span {
- margin-right: 14px;
- padding: 8px 0 8px 8px;
- border-radius: 8px;
- background: var(--X4);
-
- > button {
- padding: 4px 8px;
- }
- }
- }
- }
-
- > .cw,
- > .text {
- display: block;
- box-sizing: border-box;
- padding: 16px;
- margin: 0;
- width: 100%;
- font-size: 16px;
- border: none;
- border-radius: 0;
- background: transparent;
- color: var(--fg);
- font-family: inherit;
-
- &:focus {
- outline: none;
- }
-
- &:disabled {
- opacity: 0.5;
- }
- }
-
- > .cw {
- z-index: 1;
- padding-bottom: 8px;
- border-bottom: solid 0.5px var(--divider);
- }
-
- > .text {
- max-width: 100%;
- min-width: 100%;
- min-height: 60px;
-
- &.withCw {
- padding-top: 8px;
- }
- }
-
- > footer {
- $height: 44px;
- display: flex;
- padding: 0 8px 8px 8px;
- line-height: $height;
-
- > .left {
- > button {
- display: inline-block;
- padding: 0;
- margin: 0;
- font-size: 16px;
- width: $height;
- height: $height;
- border-radius: 6px;
-
- &:hover {
- background: var(--X5);
- }
-
- &.active {
- color: var(--accent);
- }
- }
- }
-
- > .right {
- margin-left: auto;
-
- > .text-count {
- opacity: 0.7;
- }
-
- > .visibility {
- width: $height;
- margin: 0 8px;
-
- & + .localOnly {
- margin-left: 0 !important;
- }
- }
-
- > .local-only {
- margin: 0 0 0 12px;
- opacity: 0.7;
- }
-
- > .submit {
- margin: 0;
- padding: 0 12px;
- line-height: 34px;
- font-weight: bold;
- border-radius: 4px;
-
- &:disabled {
- opacity: 0.7;
- }
-
- > i {
- margin-left: 6px;
- }
- }
- }
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/side.vue b/packages/client/src/ui/chat/side.vue
deleted file mode 100644
index 548a46102b..0000000000
--- a/packages/client/src/ui/chat/side.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<template>
-<div v-if="component" class="mrajymqm _narrow_">
- <header class="header" @contextmenu.prevent.stop="onContextmenu">
- <MkHeader class="title" :info="pageInfo" :center="false"/>
- </header>
- <component :is="component" v-bind="props" :ref="changePage" class="body"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import * as os from '@/os';
-import copyToClipboard from '@/scripts/copy-to-clipboard';
-import { resolve } from '@/router';
-import { url } from '@/config';
-import * as symbols from '@/symbols';
-
-export default defineComponent({
- components: {
- },
-
- provide() {
- return {
- navHook: (path) => {
- this.navigate(path);
- }
- };
- },
-
- data() {
- return {
- path: null,
- component: null,
- props: {},
- pageInfo: null,
- history: [],
- };
- },
-
- computed: {
- url(): string {
- return url + this.path;
- }
- },
-
- methods: {
- changePage(page) {
- if (page == null) return;
- if (page[symbols.PAGE_INFO]) {
- this.pageInfo = page[symbols.PAGE_INFO];
- }
- },
-
- navigate(path, record = true) {
- if (record && this.path) this.history.push(this.path);
- this.path = path;
- const { component, props } = resolve(path);
- this.component = component;
- this.props = props;
- this.$emit('open');
- },
-
- back() {
- this.navigate(this.history.pop(), false);
- },
-
- close() {
- this.path = null;
- this.component = null;
- this.props = {};
- this.$emit('close');
- },
-
- onContextmenu(e) {
- os.contextMenu([{
- type: 'label',
- text: this.path,
- }, {
- icon: 'fas fa-expand-alt',
- text: this.$ts.showInPage,
- action: () => {
- this.$router.push(this.path);
- this.close();
- }
- }, {
- icon: 'fas fa-window-maximize',
- text: this.$ts.openInWindow,
- action: () => {
- os.pageWindow(this.path);
- this.close();
- }
- }, null, {
- icon: 'fas fa-external-link-alt',
- text: this.$ts.openInNewTab,
- action: () => {
- window.open(this.url, '_blank');
- this.close();
- }
- }, {
- icon: 'fas fa-link',
- text: this.$ts.copyLink,
- action: () => {
- copyToClipboard(this.url);
- }
- }], e);
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.mrajymqm {
- $header-height: 54px; // TODO: どこかに集約したい
-
- --root-margin: 16px;
- --margin: var(--marginHalf);
-
- height: 100%;
- overflow: auto;
- box-sizing: border-box;
-
- > .header {
- display: flex;
- position: sticky;
- z-index: 1000;
- top: 0;
- height: $header-height;
- width: 100%;
- 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);
- box-sizing: border-box;
-
- > ._button {
- height: $header-height;
- width: $header-height;
-
- &:hover {
- color: var(--fgHighlighted);
- }
- }
-
- > .title {
- flex: 1;
- position: relative;
- }
- }
-
- > .body {
-
- }
-}
-</style>
-
diff --git a/packages/client/src/ui/chat/store.ts b/packages/client/src/ui/chat/store.ts
deleted file mode 100644
index 389d56afb6..0000000000
--- a/packages/client/src/ui/chat/store.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { markRaw } from 'vue';
-import { Storage } from '../../pizzax';
-
-export const store = markRaw(new Storage('chatUi', {
- widgets: {
- where: 'account',
- default: [] as {
- name: string;
- id: string;
- data: Record<string, any>;
- }[]
- },
- tl: {
- where: 'deviceAccount',
- default: 'home'
- },
-}));
diff --git a/packages/client/src/ui/chat/sub-note-content.vue b/packages/client/src/ui/chat/sub-note-content.vue
deleted file mode 100644
index a85096ebc9..0000000000
--- a/packages/client/src/ui/chat/sub-note-content.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="wrmlmaau">
- <div class="body">
- <span v-if="note.isHidden" style="opacity: 0.5">({{ $ts.private }})</span>
- <span v-if="note.deletedAt" style="opacity: 0.5">({{ $ts.deleted }})</span>
- <MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
- <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
- <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
- </div>
- <details v-if="note.files.length > 0">
- <summary>({{ $t('withNFiles', { n: note.files.length }) }})</summary>
- <XMediaList :media-list="note.files"/>
- </details>
- <details v-if="note.poll">
- <summary>{{ $ts.poll }}</summary>
- <XPoll :note="note"/>
- </details>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import XPoll from '@/components/poll.vue';
-import XMediaList from '@/components/media-list.vue';
-import * as os from '@/os';
-
-export default defineComponent({
- components: {
- XPoll,
- XMediaList,
- },
- props: {
- note: {
- type: Object,
- required: true
- }
- },
- data() {
- return {
- };
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.wrmlmaau {
- overflow-wrap: break-word;
-
- > .body {
- > .reply {
- margin-right: 6px;
- color: var(--accent);
- }
-
- > .rp {
- margin-left: 4px;
- font-style: oblique;
- color: var(--renote);
- }
- }
-}
-</style>
diff --git a/packages/client/src/ui/chat/widgets.vue b/packages/client/src/ui/chat/widgets.vue
deleted file mode 100644
index 337d5a7b58..0000000000
--- a/packages/client/src/ui/chat/widgets.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<template>
-<div class="qydbhufi">
- <XWidgets :edit="edit" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
-
- <button v-if="edit" class="_textButton" style="font-size: 0.9em;" @click="edit = false">{{ $ts.editWidgetsExit }}</button>
- <button v-else class="_textButton" style="font-size: 0.9em;" @click="edit = true">{{ $ts.editWidgets }}</button>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, defineAsyncComponent } from 'vue';
-import XWidgets from '@/components/widgets.vue';
-import { store } from './store';
-
-export default defineComponent({
- components: {
- XWidgets,
- },
-
- data() {
- return {
- edit: false,
- widgets: store.reactiveState.widgets
- };
- },
-
- methods: {
- addWidget(widget) {
- store.set('widgets', [widget, ...store.state.widgets]);
- },
-
- removeWidget(widget) {
- store.set('widgets', store.state.widgets.filter(w => w.id != widget.id));
- },
-
- updateWidget({ id, data }) {
- // TODO: throttleしたい
- store.set('widgets', store.state.widgets.map(w => w.id === id ? {
- ...w,
- data: data
- } : w));
- },
-
- updateWidgets(widgets) {
- store.set('widgets', widgets);
- }
- }
-});
-</script>
-
-<style lang="scss" scoped>
-.qydbhufi {
- height: 100%;
- box-sizing: border-box;
- overflow: auto;
- padding: var(--margin);
-
- ::v-deep(._panel) {
- box-shadow: none;
- }
-}
-</style>
diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue
index 3563e8a888..699b992668 100644
--- a/packages/client/src/ui/classic.header.vue
+++ b/packages/client/src/ui/classic.header.vue
@@ -105,7 +105,11 @@ export default defineComponent({
}, 'closed');
},
- openAccountMenu,
+ openAccountMenu:(ev) => {
+ openAccountMenu({
+ withExtraOperation: true,
+ }, ev);
+ },
}
});
</script>
diff --git a/packages/client/src/ui/classic.side.vue b/packages/client/src/ui/classic.side.vue
index ede658626a..f816834141 100644
--- a/packages/client/src/ui/classic.side.vue
+++ b/packages/client/src/ui/classic.side.vue
@@ -72,7 +72,7 @@ export default defineComponent({
this.props = {};
},
- onContextmenu(e) {
+ onContextmenu(ev: MouseEvent) {
os.contextMenu([{
type: 'label',
text: this.path,
@@ -103,7 +103,7 @@ export default defineComponent({
action: () => {
copyToClipboard(this.url);
}
- }], e);
+ }], ev);
}
}
});
diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue
index cc9d7a9b48..afbca06c8e 100644
--- a/packages/client/src/ui/classic.sidebar.vue
+++ b/packages/client/src/ui/classic.sidebar.vue
@@ -125,7 +125,11 @@ export default defineComponent({
}, 'closed');
},
- openAccountMenu,
+ openAccountMenu:(ev) => {
+ openAccountMenu({
+ withExtraOperation: true,
+ }, ev);
+ },
}
});
</script>
diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue
index 41da973152..c61cbc433e 100644
--- a/packages/client/src/ui/classic.vue
+++ b/packages/client/src/ui/classic.vue
@@ -16,7 +16,7 @@
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<router-view v-slot="{ Component }">
<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
- <keep-alive :include="['timeline']">
+ <keep-alive :include="['MkTimelinePage']">
<component :is="Component" :ref="changePage"/>
</keep-alive>
</transition>
@@ -30,7 +30,7 @@
</div>
</div>
- <transition name="tray-back">
+ <transition :name="$store.state.animation ? 'tray-back' : ''">
<div v-if="widgetsShowing"
class="tray-back _modalBg"
@click="widgetsShowing = false"
@@ -38,7 +38,7 @@
></div>
</transition>
- <transition name="tray">
+ <transition :name="$store.state.animation ? 'tray' : ''">
<XWidgets v-if="widgetsShowing" class="tray"/>
</transition>
@@ -167,15 +167,15 @@ export default defineComponent({
if (window._scroll) window._scroll();
},
- onContextmenu(e) {
+ onContextmenu(ev: MouseEvent) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
- if (isLink(e.target)) return;
- if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+ if (isLink(ev.target)) return;
+ if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
os.contextMenu([{
@@ -193,7 +193,7 @@ export default defineComponent({
action: () => {
os.pageWindow(path);
}
- }], e);
+ }], ev);
},
onAiClick(ev) {
diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue
index 73dc83180f..51a4853e9d 100644
--- a/packages/client/src/ui/deck.vue
+++ b/packages/client/src/ui/deck.vue
@@ -29,7 +29,7 @@
<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
</div>
- <transition name="menu-back">
+ <transition :name="$store.state.animation ? 'menu-back' : ''">
<div v-if="drawerMenuShowing"
class="menu-back _modalBg"
@click="drawerMenuShowing = false"
@@ -37,7 +37,7 @@
></div>
</transition>
- <transition name="menu">
+ <transition :name="$store.state.animation ? 'menu' : ''">
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
</transition>
diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue
index d3c7cf8213..f1ce3ca838 100644
--- a/packages/client/src/ui/deck/column.vue
+++ b/packages/client/src/ui/deck/column.vue
@@ -207,8 +207,8 @@ export default defineComponent({
return items;
},
- onContextmenu(e) {
- os.contextMenu(this.getMenu(), e);
+ onContextmenu(ev: MouseEvent) {
+ os.contextMenu(this.getMenu(), ev);
},
goTop() {
@@ -224,7 +224,7 @@ export default defineComponent({
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
- setTimeout(() => {
+ window.setTimeout(() => {
this.dragging = true;
}, 10);
},
diff --git a/packages/client/src/ui/deck/direct-column.vue b/packages/client/src/ui/deck/direct-column.vue
index 6ef733dfd0..ca70f693c3 100644
--- a/packages/client/src/ui/deck/direct-column.vue
+++ b/packages/client/src/ui/deck/direct-column.vue
@@ -2,43 +2,26 @@
<XColumn :column="column" :is-stacked="isStacked">
<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
- <XNotes :pagination="pagination" @before="before()" @after="after()"/>
+ <XNotes :pagination="pagination"/>
</XColumn>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { computed } from 'vue';
import XColumn from './column.vue';
import XNotes from '@/components/notes.vue';
import * as os from '@/os';
-export default defineComponent({
- components: {
- XColumn,
- XNotes
- },
+const props = defineProps<{
+ column: Record<string, unknown>; // TODO
+ isStacked: boolean;
+}>();
- props: {
- column: {
- type: Object,
- required: true
- },
- isStacked: {
- type: Boolean,
- required: true
- }
- },
-
- data() {
- return {
- pagination: {
- endpoint: 'notes/mentions',
- limit: 10,
- params: () => ({
- visibility: 'specified'
- })
- },
- }
- },
-});
+const pagination = {
+ point: 'notes/mentions' as const,
+ limit: 10,
+ params: computed(() => ({
+ visibility: 'specified' as const,
+ })),
+};
</script>
diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue
index 744056881c..cb045e9a46 100644
--- a/packages/client/src/ui/deck/main-column.vue
+++ b/packages/client/src/ui/deck/main-column.vue
@@ -11,7 +11,7 @@
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<router-view v-slot="{ Component }">
<transition>
- <keep-alive :include="['timeline']">
+ <keep-alive :include="['MkTimelinePage']">
<component :is="Component" :ref="changePage" @contextmenu.stop="onContextmenu"/>
</keep-alive>
</transition>
@@ -64,15 +64,15 @@ export default defineComponent({
history.back();
},
- onContextmenu(e) {
+ onContextmenu(ev: MouseEvent) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
- if (isLink(e.target)) return;
- if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(e.target.tagName) || e.target.attributes['contenteditable']) return;
+ if (isLink(ev.target)) return;
+ if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = this.$route.path;
os.contextMenu([{
@@ -84,7 +84,7 @@ export default defineComponent({
action: () => {
os.pageWindow(path);
}
- }], e);
+ }], ev);
},
}
});
diff --git a/packages/client/src/ui/deck/mentions-column.vue b/packages/client/src/ui/deck/mentions-column.vue
index 4b8dc0c4ee..6822e7ef06 100644
--- a/packages/client/src/ui/deck/mentions-column.vue
+++ b/packages/client/src/ui/deck/mentions-column.vue
@@ -2,40 +2,23 @@
<XColumn :column="column" :is-stacked="isStacked">
<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
- <XNotes :pagination="pagination" @before="before()" @after="after()"/>
+ <XNotes :pagination="pagination"/>
</XColumn>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import XColumn from './column.vue';
import XNotes from '@/components/notes.vue';
import * as os from '@/os';
-export default defineComponent({
- components: {
- XColumn,
- XNotes
- },
+const props = defineProps<{
+ column: Record<string, unknown>; // TODO
+ isStacked: boolean;
+}>();
- props: {
- column: {
- type: Object,
- required: true
- },
- isStacked: {
- type: Boolean,
- required: true
- }
- },
-
- data() {
- return {
- pagination: {
- endpoint: 'notes/mentions',
- limit: 10,
- },
- }
- },
-});
+const pagination = {
+ endpoint: 'notes/mentions' as const,
+ limit: 10,
+};
</script>
diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue
index 9fc2177ee0..16cc9a4f06 100644
--- a/packages/client/src/ui/universal.vue
+++ b/packages/client/src/ui/universal.vue
@@ -9,7 +9,7 @@
<template #header><MkHeader v-if="pageInfo && !pageInfo.hideHeader" :info="pageInfo"/></template>
<router-view v-slot="{ Component }">
<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
- <keep-alive :include="['timeline']">
+ <keep-alive :include="['MkTimelinePage']">
<component :is="Component" :ref="changePage"/>
</keep-alive>
</transition>
@@ -36,7 +36,7 @@
<button class="button post _button" @click="post()"><i class="fas fa-pencil-alt"></i></button>
</div>
- <transition name="menuDrawer-back">
+ <transition :name="$store.state.animation ? 'menuDrawer-back' : ''">
<div v-if="drawerMenuShowing"
class="menuDrawer-back _modalBg"
@click="drawerMenuShowing = false"
@@ -44,11 +44,11 @@
></div>
</transition>
- <transition name="menuDrawer">
+ <transition :name="$store.state.animation ? 'menuDrawer' : ''">
<XDrawerMenu v-if="drawerMenuShowing" class="menuDrawer"/>
</transition>
- <transition name="widgetsDrawer-back">
+ <transition :name="$store.state.animation ? 'widgetsDrawer-back' : ''">
<div v-if="widgetsShowing"
class="widgetsDrawer-back _modalBg"
@click="widgetsShowing = false"
@@ -56,7 +56,7 @@
></div>
</transition>
- <transition name="widgetsDrawer">
+ <transition :name="$store.state.animation ? 'widgetsDrawer' : ''">
<XWidgets v-if="widgetsShowing" class="widgetsDrawer"/>
</transition>
diff --git a/packages/client/src/ui/visitor/b.vue b/packages/client/src/ui/visitor/b.vue
index f6ad13d9a0..c9c0a1f72e 100644
--- a/packages/client/src/ui/visitor/b.vue
+++ b/packages/client/src/ui/visitor/b.vue
@@ -25,7 +25,7 @@
</div>
</div>
- <transition name="tray-back">
+ <transition :name="$store.state.animation ? 'tray-back' : ''">
<div v-if="showMenu"
class="menu-back _modalBg"
@click="showMenu = false"
@@ -33,7 +33,7 @@
></div>
</transition>
- <transition name="tray">
+ <transition :name="$store.state.animation ? 'tray' : ''">
<div v-if="showMenu" class="menu">
<MkA to="/" class="link" active-class="active"><i class="fas fa-home icon"></i>{{ $ts.home }}</MkA>
<MkA to="/explore" class="link" active-class="active"><i class="fas fa-hashtag icon"></i>{{ $ts.explore }}</MkA>
diff --git a/packages/client/src/ui/zen.vue b/packages/client/src/ui/zen.vue
index 7c72232cfd..a7234f729b 100644
--- a/packages/client/src/ui/zen.vue
+++ b/packages/client/src/ui/zen.vue
@@ -8,7 +8,7 @@
<div class="content">
<router-view v-slot="{ Component }">
<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
- <keep-alive :include="['timeline']">
+ <keep-alive :include="['MkTimelinePage']">
<component :is="Component" :ref="changePage"/>
</keep-alive>
</transition>
diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue
index d322f4758a..acbbb7a97a 100644
--- a/packages/client/src/widgets/activity.vue
+++ b/packages/client/src/widgets/activity.vue
@@ -1,82 +1,89 @@
<template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent">
+<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent">
<template #header><i class="fas fa-chart-bar"></i>{{ $ts._widgets.activity }}</template>
<template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template>
<div>
<MkLoading v-if="fetching"/>
<template v-else>
- <XCalendar v-show="props.view === 0" :data="[].concat(activity)"/>
- <XChart v-show="props.view === 1" :data="[].concat(activity)"/>
+ <XCalendar v-show="widgetProps.view === 0" :data="[].concat(activity)"/>
+ <XChart v-show="widgetProps.view === 1" :data="[].concat(activity)"/>
</template>
</div>
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
-import define from './define';
import XCalendar from './activity.calendar.vue';
import XChart from './activity.chart.vue';
-import * as os from '@/os';
+import { $i } from '@/account';
-const widget = define({
- name: 'activity',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- transparent: {
- type: 'boolean',
- default: false,
- },
- view: {
- type: 'number',
- default: 0,
- hidden: true,
- },
- })
-});
+const name = 'activity';
-export default defineComponent({
- components: {
- MkContainer,
- XCalendar,
- XChart,
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- extends: widget,
- data() {
- return {
- fetching: true,
- activity: null,
- };
+ transparent: {
+ type: 'boolean' as const,
+ default: false,
},
- mounted() {
- os.api('charts/user/notes', {
- userId: this.$i.id,
- span: 'day',
- limit: 7 * 21
- }).then(activity => {
- this.activity = activity.diffs.normal.map((_, i) => ({
- total: activity.diffs.normal[i] + activity.diffs.reply[i] + activity.diffs.renote[i],
- notes: activity.diffs.normal[i],
- replies: activity.diffs.reply[i],
- renotes: activity.diffs.renote[i]
- }));
- this.fetching = false;
- });
+ view: {
+ type: 'number' as const,
+ default: 0,
+ hidden: true,
},
- methods: {
- toggleView() {
- if (this.props.view === 1) {
- this.props.view = 0;
- } else {
- this.props.view++;
- }
- this.save();
- }
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const activity = ref(null);
+const fetching = ref(true);
+
+const toggleView = () => {
+ if (widgetProps.view === 1) {
+ widgetProps.view = 0;
+ } else {
+ widgetProps.view++;
}
+ save();
+};
+
+os.api('charts/user/notes', {
+ userId: $i.id,
+ span: 'day',
+ limit: 7 * 21,
+}).then(res => {
+ activity.value = res.diffs.normal.map((_, i) => ({
+ total: res.diffs.normal[i] + res.diffs.reply[i] + res.diffs.renote[i],
+ notes: res.diffs.normal[i],
+ replies: res.diffs.reply[i],
+ renotes: res.diffs.renote[i]
+ }));
+ fetching.value = false;
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/aichan.vue b/packages/client/src/widgets/aichan.vue
index 891b7454d1..03e394b976 100644
--- a/packages/client/src/widgets/aichan.vue
+++ b/packages/client/src/widgets/aichan.vue
@@ -1,51 +1,65 @@
<template>
-<MkContainer :naked="props.transparent" :show-header="false">
+<MkContainer :naked="widgetProps.transparent" :show-header="false">
<iframe ref="live2d" class="dedjhjmo" src="https://misskey-dev.github.io/mascot-web/?scale=1.5&y=1.1&eyeY=100" @click="touched"></iframe>
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from './define';
-import MkContainer from '@/components/ui/container.vue';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
-const widget = define({
- name: 'ai',
- props: () => ({
- transparent: {
- type: 'boolean',
- default: false,
- },
- })
-});
+const name = 'ai';
-export default defineComponent({
- components: {
- MkContainer,
- },
- extends: widget,
- data() {
- return {
- };
- },
- mounted() {
- window.addEventListener('mousemove', ev => {
- const iframeRect = this.$refs.live2d.getBoundingClientRect();
- this.$refs.live2d.contentWindow.postMessage({
- type: 'moveCursor',
- body: {
- x: ev.clientX - iframeRect.left,
- y: ev.clientY - iframeRect.top,
- }
- }, '*');
- }, { passive: true });
+const widgetPropsDef = {
+ transparent: {
+ type: 'boolean' as const,
+ default: false,
},
- methods: {
- touched() {
- //if (this.live2d) this.live2d.changeExpression('gurugurume');
- }
- }
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const live2d = ref<HTMLIFrameElement>();
+
+const touched = () => {
+ //if (this.live2d) this.live2d.changeExpression('gurugurume');
+};
+
+onMounted(() => {
+ const onMousemove = (ev: MouseEvent) => {
+ const iframeRect = live2d.value.getBoundingClientRect();
+ live2d.value.contentWindow.postMessage({
+ type: 'moveCursor',
+ body: {
+ x: ev.clientX - iframeRect.left,
+ y: ev.clientY - iframeRect.top,
+ }
+ }, '*');
+ };
+
+ window.addEventListener('mousemove', onMousemove, { passive: true });
+ onUnmounted(() => {
+ window.removeEventListener('mousemove', onMousemove);
+ });
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/aiscript.vue b/packages/client/src/widgets/aiscript.vue
index 46c5094ee9..0a5c0d614d 100644
--- a/packages/client/src/widgets/aiscript.vue
+++ b/packages/client/src/widgets/aiscript.vue
@@ -1,9 +1,9 @@
<template>
-<MkContainer :show-header="props.showHeader">
+<MkContainer :show-header="widgetProps.showHeader">
<template #header><i class="fas fa-terminal"></i>{{ $ts._widgets.aiscript }}</template>
<div class="uylguesu _monospace">
- <textarea v-model="props.script" placeholder="(1 + 1)"></textarea>
+ <textarea v-model="widgetProps.script" placeholder="(1 + 1)"></textarea>
<button class="_buttonPrimary" @click="run">RUN</button>
<div class="logs">
<div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div>
@@ -12,97 +12,109 @@
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@/components/ui/container.vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
+import { $i } from '@/account';
-const widget = define({
- name: 'aiscript',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- script: {
- type: 'string',
- multiline: true,
- default: '(1 + 1)',
- hidden: true,
- },
- })
-});
+const name = 'aiscript';
-export default defineComponent({
- components: {
- MkContainer
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- extends: widget,
-
- data() {
- return {
- logs: [],
- };
+ script: {
+ type: 'string' as const,
+ multiline: true,
+ default: '(1 + 1)',
+ hidden: true,
},
+};
- methods: {
- async run() {
- this.logs = [];
- const aiscript = new AiScript(createAiScriptEnv({
- storageKey: 'widget',
- token: this.$i?.token,
- }), {
- in: (q) => {
- return new Promise(ok => {
- os.inputText({
- title: q,
- }).then(({ canceled, result: a }) => {
- ok(a);
- });
- });
- },
- out: (value) => {
- this.logs.push({
- id: Math.random(),
- text: value.type === 'str' ? value.value : utils.valToString(value),
- print: true
- });
- },
- log: (type, params) => {
- switch (type) {
- case 'end': this.logs.push({
- id: Math.random(),
- text: utils.valToString(params.val, true),
- print: false
- }); break;
- default: break;
- }
- }
- });
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
- let ast;
- try {
- ast = parse(this.props.script);
- } catch (e) {
- os.alert({
- type: 'error',
- text: 'Syntax error :('
- });
- return;
- }
- try {
- await aiscript.exec(ast);
- } catch (e) {
- os.alert({
- type: 'error',
- text: e
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const logs = ref<{
+ id: string;
+ text: string;
+ print: boolean;
+}[]>([]);
+
+const run = async () => {
+ logs.value = [];
+ const aiscript = new AiScript(createAiScriptEnv({
+ storageKey: 'widget',
+ token: $i?.token,
+ }), {
+ in: (q) => {
+ return new Promise(ok => {
+ os.inputText({
+ title: q,
+ }).then(({ canceled, result: a }) => {
+ ok(a);
});
- }
+ });
+ },
+ out: (value) => {
+ logs.value.push({
+ id: Math.random().toString(),
+ text: value.type === 'str' ? value.value : utils.valToString(value),
+ print: true,
+ });
},
+ log: (type, params) => {
+ switch (type) {
+ case 'end': logs.value.push({
+ id: Math.random().toString(),
+ text: utils.valToString(params.val, true),
+ print: false,
+ }); break;
+ default: break;
+ }
+ }
+ });
+
+ let ast;
+ try {
+ ast = parse(widgetProps.script);
+ } catch (e) {
+ os.alert({
+ type: 'error',
+ text: 'Syntax error :(',
+ });
+ return;
}
+ try {
+ await aiscript.exec(ast);
+ } catch (e) {
+ os.alert({
+ type: 'error',
+ text: e,
+ });
+ }
+};
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/button.vue b/packages/client/src/widgets/button.vue
index e98570862e..a33afd6e7a 100644
--- a/packages/client/src/widgets/button.vue
+++ b/packages/client/src/widgets/button.vue
@@ -1,90 +1,99 @@
<template>
<div class="mkw-button">
- <MkButton :primary="props.colored" full @click="run">
- {{ props.label }}
+ <MkButton :primary="widgetProps.colored" full @click="run">
+ {{ widgetProps.label }}
</MkButton>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import MkButton from '@/components/ui/button.vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
import { AiScript, parse, utils } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api';
+import { $i } from '@/account';
+import MkButton from '@/components/ui/button.vue';
-const widget = define({
- name: 'button',
- props: () => ({
- label: {
- type: 'string',
- default: 'BUTTON',
- },
- colored: {
- type: 'boolean',
- default: true,
- },
- script: {
- type: 'string',
- multiline: true,
- default: 'Mk:dialog("hello" "world")',
- },
- })
-});
+const name = 'button';
-export default defineComponent({
- components: {
- MkButton
+const widgetPropsDef = {
+ label: {
+ type: 'string' as const,
+ default: 'BUTTON',
},
- extends: widget,
- data() {
- return {
- };
+ colored: {
+ type: 'boolean' as const,
+ default: true,
},
- methods: {
- async run() {
- const aiscript = new AiScript(createAiScriptEnv({
- storageKey: 'widget',
- token: this.$i?.token,
- }), {
- in: (q) => {
- return new Promise(ok => {
- os.inputText({
- title: q,
- }).then(({ canceled, result: a }) => {
- ok(a);
- });
- });
- },
- out: (value) => {
- // nop
- },
- log: (type, params) => {
- // nop
- }
- });
+ script: {
+ type: 'string' as const,
+ multiline: true,
+ default: 'Mk:dialog("hello" "world")',
+ },
+};
- let ast;
- try {
- ast = parse(this.props.script);
- } catch (e) {
- os.alert({
- type: 'error',
- text: 'Syntax error :('
- });
- return;
- }
- try {
- await aiscript.exec(ast);
- } catch (e) {
- os.alert({
- type: 'error',
- text: e
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const run = async () => {
+ const aiscript = new AiScript(createAiScriptEnv({
+ storageKey: 'widget',
+ token: $i?.token,
+ }), {
+ in: (q) => {
+ return new Promise(ok => {
+ os.inputText({
+ title: q,
+ }).then(({ canceled, result: a }) => {
+ ok(a);
});
- }
+ });
},
+ out: (value) => {
+ // nop
+ },
+ log: (type, params) => {
+ // nop
+ }
+ });
+
+ let ast;
+ try {
+ ast = parse(widgetProps.script);
+ } catch (e) {
+ os.alert({
+ type: 'error',
+ text: 'Syntax error :(',
+ });
+ return;
}
+ try {
+ await aiscript.exec(ast);
+ } catch (e) {
+ os.alert({
+ type: 'error',
+ text: e,
+ });
+ }
+};
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue
index c8b52d7afc..b0e3edcb12 100644
--- a/packages/client/src/widgets/calendar.vue
+++ b/packages/client/src/widgets/calendar.vue
@@ -1,5 +1,5 @@
<template>
-<div class="mkw-calendar" :class="{ _panel: !props.transparent }">
+<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }">
<div class="calendar" :class="{ isHoliday }">
<p class="month-and-year">
<span class="year">{{ $t('yearX', { year }) }}</span>
@@ -32,77 +32,87 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
+<script lang="ts" setup>
+import { onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { i18n } from '@/i18n';
-const widget = define({
- name: 'calendar',
- props: () => ({
- transparent: {
- type: 'boolean',
- default: false,
- },
- })
-});
+const name = 'calendar';
-export default defineComponent({
- extends: widget,
- data() {
- return {
- now: new Date(),
- year: null,
- month: null,
- day: null,
- weekDay: null,
- yearP: null,
- dayP: null,
- monthP: null,
- isHoliday: null,
- clock: null
- };
- },
- created() {
- this.tick();
- this.clock = setInterval(this.tick, 1000);
+const widgetPropsDef = {
+ transparent: {
+ type: 'boolean' as const,
+ default: false,
},
- beforeUnmount() {
- clearInterval(this.clock);
- },
- methods: {
- tick() {
- const now = new Date();
- const nd = now.getDate();
- const nm = now.getMonth();
- const ny = now.getFullYear();
+};
- this.year = ny;
- this.month = nm + 1;
- this.day = nd;
- this.weekDay = [
- this.$ts._weekday.sunday,
- this.$ts._weekday.monday,
- this.$ts._weekday.tuesday,
- this.$ts._weekday.wednesday,
- this.$ts._weekday.thursday,
- this.$ts._weekday.friday,
- this.$ts._weekday.saturday
- ][now.getDay()];
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
- const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime();
- const dayDenom = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
- const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
- const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
- const yearNumer = now.getTime() - new Date(ny, 0, 1).getTime();
- const yearDenom = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
- this.dayP = dayNumer / dayDenom * 100;
- this.monthP = monthNumer / monthDenom * 100;
- this.yearP = yearNumer / yearDenom * 100;
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
- this.isHoliday = now.getDay() === 0 || now.getDay() === 6;
- }
- }
+const year = ref(0);
+const month = ref(0);
+const day = ref(0);
+const weekDay = ref('');
+const yearP = ref(0);
+const monthP = ref(0);
+const dayP = ref(0);
+const isHoliday = ref(false);
+const tick = () => {
+ const now = new Date();
+ const nd = now.getDate();
+ const nm = now.getMonth();
+ const ny = now.getFullYear();
+
+ year.value = ny;
+ month.value = nm + 1;
+ day.value = nd;
+ weekDay.value = [
+ i18n.locale._weekday.sunday,
+ i18n.locale._weekday.monday,
+ i18n.locale._weekday.tuesday,
+ i18n.locale._weekday.wednesday,
+ i18n.locale._weekday.thursday,
+ i18n.locale._weekday.friday,
+ i18n.locale._weekday.saturday
+ ][now.getDay()];
+
+ const dayNumer = now.getTime() - new Date(ny, nm, nd).getTime();
+ const dayDenom = 1000/*ms*/ * 60/*s*/ * 60/*m*/ * 24/*h*/;
+ const monthNumer = now.getTime() - new Date(ny, nm, 1).getTime();
+ const monthDenom = new Date(ny, nm + 1, 1).getTime() - new Date(ny, nm, 1).getTime();
+ const yearNumer = now.getTime() - new Date(ny, 0, 1).getTime();
+ const yearDenom = new Date(ny + 1, 0, 1).getTime() - new Date(ny, 0, 1).getTime();
+
+ dayP.value = dayNumer / dayDenom * 100;
+ monthP.value = monthNumer / monthDenom * 100;
+ yearP.value = yearNumer / yearDenom * 100;
+
+ isHoliday.value = now.getDay() === 0 || now.getDay() === 6;
+};
+
+tick();
+
+const intervalId = window.setInterval(tick, 1000);
+onUnmounted(() => {
+ window.clearInterval(intervalId);
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/clock.vue b/packages/client/src/widgets/clock.vue
index 6ca7ecd430..6acb10d74d 100644
--- a/packages/client/src/widgets/clock.vue
+++ b/packages/client/src/widgets/clock.vue
@@ -1,45 +1,56 @@
<template>
-<MkContainer :naked="props.transparent" :show-header="false">
+<MkContainer :naked="widgetProps.transparent" :show-header="false">
<div class="vubelbmv">
- <MkAnalogClock class="clock" :thickness="props.thickness"/>
+ <MkAnalogClock class="clock" :thickness="widgetProps.thickness"/>
</div>
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
+<script lang="ts" setup>
+import { } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import MkContainer from '@/components/ui/container.vue';
import MkAnalogClock from '@/components/analog-clock.vue';
-import * as os from '@/os';
-const widget = define({
- name: 'clock',
- props: () => ({
- transparent: {
- type: 'boolean',
- default: false,
- },
- thickness: {
- type: 'radio',
- default: 0.1,
- options: [{
- value: 0.1, label: 'thin'
- }, {
- value: 0.2, label: 'medium'
- }, {
- value: 0.3, label: 'thick'
- }]
- }
- })
-});
+const name = 'clock';
-export default defineComponent({
- components: {
- MkContainer,
- MkAnalogClock
+const widgetPropsDef = {
+ transparent: {
+ type: 'boolean' as const,
+ default: false,
+ },
+ thickness: {
+ type: 'radio' as const,
+ default: 0.1,
+ options: [{
+ value: 0.1, label: 'thin'
+ }, {
+ value: 0.2, label: 'medium'
+ }, {
+ value: 0.3, label: 'thick'
+ }],
},
- extends: widget,
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/define.ts b/packages/client/src/widgets/define.ts
deleted file mode 100644
index 08a346d97c..0000000000
--- a/packages/client/src/widgets/define.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { defineComponent } from 'vue';
-import { throttle } from 'throttle-debounce';
-import { Form } from '@/scripts/form';
-import * as os from '@/os';
-
-export default function <T extends Form>(data: {
- name: string;
- props?: () => T;
-}) {
- return defineComponent({
- props: {
- widget: {
- type: Object,
- required: false
- },
- settingCallback: {
- required: false
- }
- },
-
- emits: ['updateProps'],
-
- data() {
- return {
- props: this.widget ? JSON.parse(JSON.stringify(this.widget.data)) : {},
- save: throttle(3000, () => {
- this.$emit('updateProps', this.props);
- }),
- };
- },
-
- computed: {
- id(): string {
- return this.widget ? this.widget.id : null;
- },
- },
-
- created() {
- this.mergeProps();
-
- this.$watch('props', () => {
- this.mergeProps();
- }, { deep: true });
-
- if (this.settingCallback) this.settingCallback(this.setting);
- },
-
- methods: {
- mergeProps() {
- if (data.props) {
- const defaultProps = data.props();
- for (const prop of Object.keys(defaultProps)) {
- if (this.props.hasOwnProperty(prop)) continue;
- this.props[prop] = defaultProps[prop].default;
- }
- }
- },
-
- async setting() {
- const form = data.props();
- for (const item of Object.keys(form)) {
- form[item].default = this.props[item];
- }
- const { canceled, result } = await os.form(data.name, form);
- if (canceled) return;
-
- for (const key of Object.keys(result)) {
- this.props[key] = result[key];
- }
-
- this.save();
- },
- }
- });
-}
diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue
index fbf632d2de..62f052a692 100644
--- a/packages/client/src/widgets/digital-clock.vue
+++ b/packages/client/src/widgets/digital-clock.vue
@@ -1,73 +1,84 @@
<template>
-<div class="mkw-digitalClock _monospace" :class="{ _panel: !props.transparent }" :style="{ fontSize: `${props.fontSize}em` }">
+<div class="mkw-digitalClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.fontSize}em` }">
<span>
<span v-text="hh"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
<span v-text="mm"></span>
<span :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
<span v-text="ss"></span>
- <span v-if="props.showMs" :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
- <span v-if="props.showMs" v-text="ms"></span>
+ <span v-if="widgetProps.showMs" :style="{ visibility: showColon ? 'visible' : 'hidden' }">:</span>
+ <span v-if="widgetProps.showMs" v-text="ms"></span>
</span>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onUnmounted, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
-const widget = define({
- name: 'digitalClock',
- props: () => ({
- transparent: {
- type: 'boolean',
- default: false,
- },
- fontSize: {
- type: 'number',
- default: 1.5,
- step: 0.1,
- },
- showMs: {
- type: 'boolean',
- default: true,
- },
- })
-});
+const name = 'digitalClock';
-export default defineComponent({
- extends: widget,
- data() {
- return {
- clock: null,
- hh: null,
- mm: null,
- ss: null,
- ms: null,
- showColon: true,
- };
+const widgetPropsDef = {
+ transparent: {
+ type: 'boolean' as const,
+ default: false,
},
- created() {
- this.tick();
- this.$watch(() => this.props.showMs, () => {
- if (this.clock) clearInterval(this.clock);
- this.clock = setInterval(this.tick, this.props.showMs ? 10 : 1000);
- }, { immediate: true });
+ fontSize: {
+ type: 'number' as const,
+ default: 1.5,
+ step: 0.1,
},
- beforeUnmount() {
- clearInterval(this.clock);
+ showMs: {
+ type: 'boolean' as const,
+ default: true,
},
- methods: {
- tick() {
- const now = new Date();
- this.hh = now.getHours().toString().padStart(2, '0');
- this.mm = now.getMinutes().toString().padStart(2, '0');
- this.ss = now.getSeconds().toString().padStart(2, '0');
- this.ms = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
- this.showColon = now.getSeconds() % 2 === 0;
- }
- }
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+let intervalId;
+const hh = ref('');
+const mm = ref('');
+const ss = ref('');
+const ms = ref('');
+const showColon = ref(true);
+const tick = () => {
+ const now = new Date();
+ hh.value = now.getHours().toString().padStart(2, '0');
+ mm.value = now.getMinutes().toString().padStart(2, '0');
+ ss.value = now.getSeconds().toString().padStart(2, '0');
+ ms.value = Math.floor(now.getMilliseconds() / 10).toString().padStart(2, '0');
+ showColon.value = now.getSeconds() % 2 === 0;
+};
+
+tick();
+
+watch(() => widgetProps.showMs, () => {
+ if (intervalId) window.clearInterval(intervalId);
+ intervalId = window.setInterval(tick, widgetProps.showMs ? 10 : 1000);
+}, { immediate: true });
+
+onUnmounted(() => {
+ window.clearInterval(intervalId);
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue
index 736a91c52e..4c43117e48 100644
--- a/packages/client/src/widgets/federation.vue
+++ b/packages/client/src/widgets/federation.vue
@@ -1,10 +1,10 @@
<template>
-<MkContainer :show-header="props.showHeader" :foldable="foldable" :scrollable="scrollable">
+<MkContainer :show-header="widgetProps.showHeader" :foldable="foldable" :scrollable="scrollable">
<template #header><i class="fas fa-globe"></i>{{ $ts._widgets.federation }}</template>
<div class="wbrkwalb">
<MkLoading v-if="fetching"/>
- <transition-group v-else tag="div" name="chart" class="instances">
+ <transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="instances">
<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
<img v-if="instance.iconUrl" :src="instance.iconUrl" alt=""/>
<div class="body">
@@ -18,66 +18,64 @@
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import MkContainer from '@/components/ui/container.vue';
-import define from './define';
import MkMiniChart from '@/components/mini-chart.vue';
import * as os from '@/os';
-const widget = define({
- name: 'federation',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- })
-});
+const name = 'federation';
-export default defineComponent({
- components: {
- MkContainer, MkMiniChart
- },
- extends: widget,
- props: {
- foldable: {
- type: Boolean,
- required: false,
- default: false
- },
- scrollable: {
- type: Boolean,
- required: false,
- default: false
- },
- },
- data() {
- return {
- instances: [],
- charts: [],
- fetching: true,
- };
- },
- mounted() {
- this.fetch();
- this.clock = setInterval(this.fetch, 1000 * 60);
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- beforeUnmount() {
- clearInterval(this.clock);
- },
- methods: {
- async fetch() {
- const instances = await os.api('federation/instances', {
- sort: '+lastCommunicatedAt',
- limit: 5
- });
- const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
- this.instances = instances;
- this.charts = charts;
- this.fetching = false;
- }
- }
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps> & { foldable?: boolean; scrollable?: boolean; }>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; foldable?: boolean; scrollable?: boolean; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const instances = ref([]);
+const charts = ref([]);
+const fetching = ref(true);
+
+const fetch = async () => {
+ const instances = await os.api('federation/instances', {
+ sort: '+lastCommunicatedAt',
+ limit: 5
+ });
+ const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
+ instances.value = instances;
+ charts.value = charts;
+ fetching.value = false;
+};
+
+onMounted(() => {
+ fetch();
+ const intervalId = window.setInterval(fetch, 1000 * 60);
+ onUnmounted(() => {
+ window.clearInterval(intervalId);
+ });
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/job-queue.vue b/packages/client/src/widgets/job-queue.vue
index ef440881e5..4a2a3cf233 100644
--- a/packages/client/src/widgets/job-queue.vue
+++ b/packages/client/src/widgets/job-queue.vue
@@ -1,133 +1,146 @@
<template>
-<div class="mkw-jobQueue _monospace" :class="{ _panel: !props.transparent }">
+<div class="mkw-jobQueue _monospace" :class="{ _panel: !widgetProps.transparent }">
<div class="inbox">
- <div class="label">Inbox queue<i v-if="inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
+ <div class="label">Inbox queue<i v-if="current.inbox.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
<div class="values">
<div>
<div>Process</div>
- <div :class="{ inc: inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(inbox.activeSincePrevTick) }}</div>
+ <div :class="{ inc: current.inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: current.inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(current.inbox.activeSincePrevTick) }}</div>
</div>
<div>
<div>Active</div>
- <div :class="{ inc: inbox.active > prev.inbox.active, dec: inbox.active < prev.inbox.active }">{{ number(inbox.active) }}</div>
+ <div :class="{ inc: current.inbox.active > prev.inbox.active, dec: current.inbox.active < prev.inbox.active }">{{ number(current.inbox.active) }}</div>
</div>
<div>
<div>Delayed</div>
- <div :class="{ inc: inbox.delayed > prev.inbox.delayed, dec: inbox.delayed < prev.inbox.delayed }">{{ number(inbox.delayed) }}</div>
+ <div :class="{ inc: current.inbox.delayed > prev.inbox.delayed, dec: current.inbox.delayed < prev.inbox.delayed }">{{ number(current.inbox.delayed) }}</div>
</div>
<div>
<div>Waiting</div>
- <div :class="{ inc: inbox.waiting > prev.inbox.waiting, dec: inbox.waiting < prev.inbox.waiting }">{{ number(inbox.waiting) }}</div>
+ <div :class="{ inc: current.inbox.waiting > prev.inbox.waiting, dec: current.inbox.waiting < prev.inbox.waiting }">{{ number(current.inbox.waiting) }}</div>
</div>
</div>
</div>
<div class="deliver">
- <div class="label">Deliver queue<i v-if="deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
+ <div class="label">Deliver queue<i v-if="current.deliver.waiting > 0" class="fas fa-exclamation-triangle icon"></i></div>
<div class="values">
<div>
<div>Process</div>
- <div :class="{ inc: deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(deliver.activeSincePrevTick) }}</div>
+ <div :class="{ inc: current.deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: current.deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(current.deliver.activeSincePrevTick) }}</div>
</div>
<div>
<div>Active</div>
- <div :class="{ inc: deliver.active > prev.deliver.active, dec: deliver.active < prev.deliver.active }">{{ number(deliver.active) }}</div>
+ <div :class="{ inc: current.deliver.active > prev.deliver.active, dec: current.deliver.active < prev.deliver.active }">{{ number(current.deliver.active) }}</div>
</div>
<div>
<div>Delayed</div>
- <div :class="{ inc: deliver.delayed > prev.deliver.delayed, dec: deliver.delayed < prev.deliver.delayed }">{{ number(deliver.delayed) }}</div>
+ <div :class="{ inc: current.deliver.delayed > prev.deliver.delayed, dec: current.deliver.delayed < prev.deliver.delayed }">{{ number(current.deliver.delayed) }}</div>
</div>
<div>
<div>Waiting</div>
- <div :class="{ inc: deliver.waiting > prev.deliver.waiting, dec: deliver.waiting < prev.deliver.waiting }">{{ number(deliver.waiting) }}</div>
+ <div :class="{ inc: current.deliver.waiting > prev.deliver.waiting, dec: current.deliver.waiting < prev.deliver.waiting }">{{ number(current.deliver.waiting) }}</div>
</div>
</div>
</div>
</div>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from './define';
-import * as os from '@/os';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { stream } from '@/stream';
import number from '@/filters/number';
import * as sound from '@/scripts/sound';
+import * as os from '@/os';
-const widget = define({
- name: 'jobQueue',
- props: () => ({
- transparent: {
- type: 'boolean',
- default: false,
- },
- sound: {
- type: 'boolean',
- default: false,
- },
- })
-});
+const name = 'jobQueue';
-export default defineComponent({
- extends: widget,
- data() {
- return {
- connection: markRaw(os.stream.useChannel('queueStats')),
- inbox: {
- activeSincePrevTick: 0,
- active: 0,
- waiting: 0,
- delayed: 0,
- },
- deliver: {
- activeSincePrevTick: 0,
- active: 0,
- waiting: 0,
- delayed: 0,
- },
- prev: {},
- sound: sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1)
- };
+const widgetPropsDef = {
+ transparent: {
+ type: 'boolean' as const,
+ default: false,
},
- created() {
- for (const domain of ['inbox', 'deliver']) {
- this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
- }
-
- this.connection.on('stats', this.onStats);
- this.connection.on('statsLog', this.onStatsLog);
+ sound: {
+ type: 'boolean' as const,
+ default: false,
+ },
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
- this.connection.send('requestLog', {
- id: Math.random().toString().substr(2, 8),
- length: 1
- });
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const connection = stream.useChannel('queueStats');
+const current = reactive({
+ inbox: {
+ activeSincePrevTick: 0,
+ active: 0,
+ waiting: 0,
+ delayed: 0,
},
- beforeUnmount() {
- this.connection.off('stats', this.onStats);
- this.connection.off('statsLog', this.onStatsLog);
- this.connection.dispose();
+ deliver: {
+ activeSincePrevTick: 0,
+ active: 0,
+ waiting: 0,
+ delayed: 0,
},
- methods: {
- onStats(stats) {
- for (const domain of ['inbox', 'deliver']) {
- this.prev[domain] = JSON.parse(JSON.stringify(this[domain]));
- this[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
- this[domain].active = stats[domain].active;
- this[domain].waiting = stats[domain].waiting;
- this[domain].delayed = stats[domain].delayed;
+});
+const prev = reactive({} as typeof current);
+const jammedSound = sound.setVolume(sound.getAudio('syuilo/queue-jammed'), 1);
- if (this[domain].waiting > 0 && this.props.sound && this.sound.paused) {
- this.sound.play();
- }
- }
- },
+for (const domain of ['inbox', 'deliver']) {
+ prev[domain] = JSON.parse(JSON.stringify(current[domain]));
+}
- onStatsLog(statsLog) {
- for (const stats of [...statsLog].reverse()) {
- this.onStats(stats);
- }
- },
+const onStats = (stats) => {
+ for (const domain of ['inbox', 'deliver']) {
+ prev[domain] = JSON.parse(JSON.stringify(current[domain]));
+ current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
+ current[domain].active = stats[domain].active;
+ current[domain].waiting = stats[domain].waiting;
+ current[domain].delayed = stats[domain].delayed;
+
+ if (current[domain].waiting > 0 && widgetProps.sound && jammedSound.paused) {
+ jammedSound.play();
+ }
+ }
+};
- number
+const onStatsLog = (statsLog) => {
+ for (const stats of [...statsLog].reverse()) {
+ onStats(stats);
}
+};
+
+connection.on('stats', onStats);
+connection.on('statsLog', onStatsLog);
+
+connection.send('requestLog', {
+ id: Math.random().toString().substr(2, 8),
+ length: 1,
+});
+
+onUnmounted(() => {
+ connection.off('stats', onStats);
+ connection.off('statsLog', onStatsLog);
+ connection.dispose();
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue
index 9b51ada220..450598f65a 100644
--- a/packages/client/src/widgets/memo.vue
+++ b/packages/client/src/widgets/memo.vue
@@ -1,5 +1,5 @@
<template>
-<MkContainer :show-header="props.showHeader">
+<MkContainer :show-header="widgetProps.showHeader">
<template #header><i class="fas fa-sticky-note"></i>{{ $ts._widgets.memo }}</template>
<div class="otgbylcu">
@@ -9,56 +9,60 @@
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@/components/ui/container.vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
+import { defaultStore } from '@/store';
-const widget = define({
- name: 'memo',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- })
-});
+const name = 'memo';
-export default defineComponent({
- components: {
- MkContainer
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- extends: widget,
+};
- data() {
- return {
- text: null,
- changed: false,
- timeoutId: null,
- };
- },
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
- created() {
- this.text = this.$store.state.memo;
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
- this.$watch(() => this.$store.reactiveState.memo, text => {
- this.text = text;
- });
- },
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
- methods: {
- onChange() {
- this.changed = true;
- clearTimeout(this.timeoutId);
- this.timeoutId = setTimeout(this.saveMemo, 1000);
- },
+const text = ref<string | null>(defaultStore.state.memo);
+const changed = ref(false);
+let timeoutId;
- saveMemo() {
- this.$store.set('memo', this.text);
- this.changed = false;
- }
- }
+const saveMemo = () => {
+ defaultStore.set('memo', text.value);
+ changed.value = false;
+};
+
+const onChange = () => {
+ changed.value = true;
+ window.clearTimeout(timeoutId);
+ timeoutId = window.setTimeout(saveMemo, 1000);
+};
+
+watch(() => defaultStore.reactiveState.memo, newText => {
+ text.value = newText.value;
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/notifications.vue b/packages/client/src/widgets/notifications.vue
index 568705b661..8cf29c9271 100644
--- a/packages/client/src/widgets/notifications.vue
+++ b/packages/client/src/widgets/notifications.vue
@@ -1,65 +1,68 @@
<template>
-<MkContainer :style="`height: ${props.height}px;`" :show-header="props.showHeader" :scrollable="true">
+<MkContainer :style="`height: ${widgetProps.height}px;`" :show-header="widgetProps.showHeader" :scrollable="true">
<template #header><i class="fas fa-bell"></i>{{ $ts.notifications }}</template>
- <template #func><button class="_button" @click="configure()"><i class="fas fa-cog"></i></button></template>
+ <template #func><button class="_button" @click="configureNotification()"><i class="fas fa-cog"></i></button></template>
<div>
- <XNotifications :include-types="props.includingTypes"/>
+ <XNotifications :include-types="widgetProps.includingTypes"/>
</div>
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import MkContainer from '@/components/ui/container.vue';
import XNotifications from '@/components/notifications.vue';
-import define from './define';
import * as os from '@/os';
-const widget = define({
- name: 'notifications',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- height: {
- type: 'number',
- default: 300,
- },
- includingTypes: {
- type: 'array',
- hidden: true,
- default: null,
- },
- })
-});
-
-export default defineComponent({
+const name = 'notifications';
- components: {
- MkContainer,
- XNotifications,
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- extends: widget,
-
- data() {
- return {
- };
+ height: {
+ type: 'number' as const,
+ default: 300,
+ },
+ includingTypes: {
+ type: 'array' as const,
+ hidden: true,
+ default: null,
},
+};
- methods: {
- configure() {
- os.popup(import('@/components/notification-setting-window.vue'), {
- includingTypes: this.props.includingTypes,
- }, {
- done: async (res) => {
- const { includingTypes } = res;
- this.props.includingTypes = includingTypes;
- this.save();
- }
- }, 'closed');
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const configureNotification = () => {
+ os.popup(import('@/components/notification-setting-window.vue'), {
+ includingTypes: widgetProps.includingTypes,
+ }, {
+ done: async (res) => {
+ const { includingTypes } = res;
+ widgetProps.includingTypes = includingTypes;
+ save();
}
- }
+ }, 'closed');
+};
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/online-users.vue b/packages/client/src/widgets/online-users.vue
index 5b889f4816..1746a8314e 100644
--- a/packages/client/src/widgets/online-users.vue
+++ b/packages/client/src/widgets/online-users.vue
@@ -1,48 +1,60 @@
<template>
-<div class="mkw-onlineUsers" :class="{ _panel: !props.transparent, pad: !props.transparent }">
+<div class="mkw-onlineUsers" :class="{ _panel: !widgetProps.transparent, pad: !widgetProps.transparent }">
<I18n v-if="onlineUsersCount" :src="$ts.onlineUsersCount" text-tag="span" class="text">
<template #n><b>{{ onlineUsersCount }}</b></template>
</I18n>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
-const widget = define({
- name: 'onlineUsers',
- props: () => ({
- transparent: {
- type: 'boolean',
- default: true,
- },
- })
-});
+const name = 'onlineUsers';
-export default defineComponent({
- extends: widget,
- data() {
- return {
- onlineUsersCount: null,
- clock: null,
- };
- },
- created() {
- this.tick();
- this.clock = setInterval(this.tick, 1000 * 15);
+const widgetPropsDef = {
+ transparent: {
+ type: 'boolean' as const,
+ default: true,
},
- beforeUnmount() {
- clearInterval(this.clock);
- },
- methods: {
- tick() {
- os.api('get-online-users-count').then(res => {
- this.onlineUsersCount = res.count;
- });
- }
- }
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const onlineUsersCount = ref(0);
+
+const tick = () => {
+ os.api('get-online-users-count').then(res => {
+ onlineUsersCount.value = res.count;
+ });
+};
+
+onMounted(() => {
+ tick();
+ const intervalId = window.setInterval(tick, 1000 * 15);
+ onUnmounted(() => {
+ window.clearInterval(intervalId);
+ });
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/photos.vue b/packages/client/src/widgets/photos.vue
index a91d4f6c49..8f948dc643 100644
--- a/packages/client/src/widgets/photos.vue
+++ b/packages/client/src/widgets/photos.vue
@@ -1,5 +1,5 @@
<template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent" :class="$style.root" :data-transparent="props.transparent ? true : null">
+<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent" :class="$style.root" :data-transparent="widgetProps.transparent ? true : null">
<template #header><i class="fas fa-camera"></i>{{ $ts._widgets.photos }}</template>
<div class="">
@@ -14,69 +14,77 @@
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import MkContainer from '@/components/ui/container.vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import { stream } from '@/stream';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
+import { defaultStore } from '@/store';
-const widget = define({
- name: 'photos',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- transparent: {
- type: 'boolean',
- default: false,
- },
- })
-});
+const name = 'photos';
-export default defineComponent({
- components: {
- MkContainer,
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- extends: widget,
- data() {
- return {
- images: [],
- fetching: true,
- connection: null,
- };
+ transparent: {
+ type: 'boolean' as const,
+ default: false,
},
- mounted() {
- this.connection = markRaw(os.stream.useChannel('main'));
+};
- this.connection.on('driveFileCreated', this.onDriveFileCreated);
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
- os.api('drive/stream', {
- type: 'image/*',
- limit: 9
- }).then(images => {
- this.images = images;
- this.fetching = false;
- });
- },
- beforeUnmount() {
- this.connection.dispose();
- },
- methods: {
- onDriveFileCreated(file) {
- if (/^image\/.+$/.test(file.type)) {
- this.images.unshift(file);
- if (this.images.length > 9) this.images.pop();
- }
- },
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const connection = stream.useChannel('main');
+const images = ref([]);
+const fetching = ref(true);
- thumbnail(image: any): string {
- return this.$store.state.disableShowingAnimatedImages
- ? getStaticImageUrl(image.thumbnailUrl)
- : image.thumbnailUrl;
- },
+const onDriveFileCreated = (file) => {
+ if (/^image\/.+$/.test(file.type)) {
+ images.value.unshift(file);
+ if (images.value.length > 9) images.value.pop();
}
+};
+
+const thumbnail = (image: any): string => {
+ return defaultStore.state.disableShowingAnimatedImages
+ ? getStaticImageUrl(image.thumbnailUrl)
+ : image.thumbnailUrl;
+};
+
+os.api('drive/stream', {
+ type: 'image/*',
+ limit: 9
+}).then(res => {
+ images.value = res;
+ fetching.value = false;
+});
+
+connection.on('driveFileCreated', onDriveFileCreated);
+onUnmounted(() => {
+ connection.dispose();
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/post-form.vue b/packages/client/src/widgets/post-form.vue
index 6de0574cc1..51aa8fcf6b 100644
--- a/packages/client/src/widgets/post-form.vue
+++ b/packages/client/src/widgets/post-form.vue
@@ -2,22 +2,34 @@
<XPostForm class="_panel" :fixed="true" :autofocus="false"/>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import XPostForm from '@/components/post-form.vue';
-import define from './define';
-const widget = define({
- name: 'postForm',
- props: () => ({
- })
-});
+const name = 'postForm';
+
+const widgetPropsDef = {
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
-export default defineComponent({
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
- components: {
- XPostForm,
- },
- extends: widget,
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue
index b2dc77854e..9e2e503602 100644
--- a/packages/client/src/widgets/rss.vue
+++ b/packages/client/src/widgets/rss.vue
@@ -1,7 +1,7 @@
<template>
-<MkContainer :show-header="props.showHeader">
+<MkContainer :show-header="widgetProps.showHeader">
<template #header><i class="fas fa-rss-square"></i>RSS</template>
- <template #func><button class="_button" @click="setting"><i class="fas fa-cog"></i></button></template>
+ <template #func><button class="_button" @click="configure"><i class="fas fa-cog"></i></button></template>
<div class="ekmkgxbj">
<MkLoading v-if="fetching"/>
@@ -12,57 +12,66 @@
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import MkContainer from '@/components/ui/container.vue';
-import define from './define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
+import MkContainer from '@/components/ui/container.vue';
-const widget = define({
- name: 'rss',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- url: {
- type: 'string',
- default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
- },
- })
-});
+const name = 'rss';
-export default defineComponent({
- components: {
- MkContainer
- },
- extends: widget,
- data() {
- return {
- items: [],
- fetching: true,
- clock: null,
- };
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- mounted() {
- this.fetch();
- this.clock = setInterval(this.fetch, 60000);
- this.$watch(() => this.props.url, this.fetch);
+ url: {
+ type: 'string' as const,
+ default: 'http://feeds.afpbb.com/rss/afpbb/afpbbnews',
},
- beforeUnmount() {
- clearInterval(this.clock);
- },
- methods: {
- fetch() {
- fetch(`https://api.rss2json.com/v1/api.json?rss_url=${this.props.url}`, {
- }).then(res => {
- res.json().then(feed => {
- this.items = feed.items;
- this.fetching = false;
- });
- });
- },
- }
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const items = ref([]);
+const fetching = ref(true);
+
+const tick = () => {
+ fetch(`https://api.rss2json.com/v1/api.json?rss_url=${widgetProps.url}`, {}).then(res => {
+ res.json().then(feed => {
+ items.value = feed.items;
+ fetching.value = false;
+ });
+ });
+};
+
+watch(() => widgetProps.url, tick);
+
+onMounted(() => {
+ tick();
+ const intervalId = window.setInterval(tick, 60000);
+ onUnmounted(() => {
+ window.clearInterval(intervalId);
+ });
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/server-metric/disk.vue b/packages/client/src/widgets/server-metric/disk.vue
index 650101b0ee..052991b554 100644
--- a/packages/client/src/widgets/server-metric/disk.vue
+++ b/packages/client/src/widgets/server-metric/disk.vue
@@ -10,32 +10,19 @@
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
import XPie from './pie.vue';
import bytes from '@/filters/bytes';
-export default defineComponent({
- components: {
- XPie
- },
- props: {
- meta: {
- required: true,
- }
- },
- data() {
- return {
- usage: this.meta.fs.used / this.meta.fs.total,
- total: this.meta.fs.total,
- used: this.meta.fs.used,
- available: this.meta.fs.total - this.meta.fs.used,
- };
- },
- methods: {
- bytes
- }
-});
+const props = defineProps<{
+ meta: any; // TODO
+}>();
+
+const usage = $computed(() => props.meta.fs.used / props.meta.fs.total);
+const total = $computed(() => props.meta.fs.total);
+const used = $computed(() => props.meta.fs.used);
+const available = $computed(() => props.meta.fs.total - props.meta.fs.used);
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/widgets/server-metric/index.vue b/packages/client/src/widgets/server-metric/index.vue
index 019e16fb33..2caa73fa74 100644
--- a/packages/client/src/widgets/server-metric/index.vue
+++ b/packages/client/src/widgets/server-metric/index.vue
@@ -1,21 +1,22 @@
<template>
-<MkContainer :show-header="props.showHeader" :naked="props.transparent">
+<MkContainer :show-header="widgetProps.showHeader" :naked="widgetProps.transparent">
<template #header><i class="fas fa-server"></i>{{ $ts._widgets.serverMetric }}</template>
<template #func><button class="_button" @click="toggleView()"><i class="fas fa-sort"></i></button></template>
<div v-if="meta" class="mkw-serverMetric">
- <XCpuMemory v-if="props.view === 0" :connection="connection" :meta="meta"/>
- <XNet v-if="props.view === 1" :connection="connection" :meta="meta"/>
- <XCpu v-if="props.view === 2" :connection="connection" :meta="meta"/>
- <XMemory v-if="props.view === 3" :connection="connection" :meta="meta"/>
- <XDisk v-if="props.view === 4" :connection="connection" :meta="meta"/>
+ <XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/>
+ <XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
+ <XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>
+ <XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/>
+ <XDisk v-else-if="widgetProps.view === 4" :connection="connection" :meta="meta"/>
</div>
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import define from '../define';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget';
import MkContainer from '@/components/ui/container.vue';
import XCpuMemory from './cpu-mem.vue';
import XNet from './net.vue';
@@ -23,60 +24,63 @@ import XCpu from './cpu.vue';
import XMemory from './mem.vue';
import XDisk from './disk.vue';
import * as os from '@/os';
+import { stream } from '@/stream';
-const widget = define({
- name: 'serverMetric',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- transparent: {
- type: 'boolean',
- default: false,
- },
- view: {
- type: 'number',
- default: 0,
- hidden: true,
- },
- })
-});
+const name = 'serverMetric';
-export default defineComponent({
- components: {
- MkContainer,
- XCpuMemory,
- XNet,
- XCpu,
- XMemory,
- XDisk,
- },
- extends: widget,
- data() {
- return {
- meta: null,
- connection: null,
- };
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- created() {
- os.api('server-info', {}).then(res => {
- this.meta = res;
- });
- this.connection = markRaw(os.stream.useChannel('serverStats'));
+ transparent: {
+ type: 'boolean' as const,
+ default: false,
},
- unmounted() {
- this.connection.dispose();
+ view: {
+ type: 'number' as const,
+ default: 0,
+ hidden: true,
},
- methods: {
- toggleView() {
- if (this.props.view == 4) {
- this.props.view = 0;
- } else {
- this.props.view++;
- }
- this.save();
- },
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const meta = ref(null);
+
+os.api('server-info', {}).then(res => {
+ meta.value = res;
+});
+
+const toggleView = () => {
+ if (widgetProps.view == 4) {
+ widgetProps.view = 0;
+ } else {
+ widgetProps.view++;
}
+ save();
+};
+
+const connection = stream.useChannel('serverStats');
+onUnmounted(() => {
+ connection.dispose();
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/server-metric/pie.vue b/packages/client/src/widgets/server-metric/pie.vue
index 38dcf6fcd9..868dbc0484 100644
--- a/packages/client/src/widgets/server-metric/pie.vue
+++ b/packages/client/src/widgets/server-metric/pie.vue
@@ -20,30 +20,17 @@
</svg>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
-export default defineComponent({
- props: {
- value: {
- type: Number,
- required: true
- }
- },
- data() {
- return {
- r: 0.45
- };
- },
- computed: {
- color(): string {
- return `hsl(${180 - (this.value * 180)}, 80%, 70%)`;
- },
- strokeDashoffset(): number {
- return (1 - this.value) * (Math.PI * (this.r * 2));
- }
- }
-});
+const props = defineProps<{
+ value: number;
+}>();
+
+const r = 0.45;
+
+const color = $computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
+const strokeDashoffset = $computed(() => (1 - props.value) * (Math.PI * (r * 2)));
</script>
<style lang="scss" scoped>
diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue
index 0909bda67c..7b2e539685 100644
--- a/packages/client/src/widgets/slideshow.vue
+++ b/packages/client/src/widgets/slideshow.vue
@@ -1,126 +1,116 @@
<template>
-<div class="kvausudm _panel">
+<div class="kvausudm _panel" :style="{ height: widgetProps.height + 'px' }">
<div @click="choose">
- <p v-if="props.folderId == null">
- <template v-if="isCustomizeMode">{{ $t('folder-customize-mode') }}</template>
- <template v-else>{{ $ts.folder }}</template>
+ <p v-if="widgetProps.folderId == null">
+ {{ $ts.folder }}
</p>
- <p v-if="props.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
+ <p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ $t('no-image') }}</p>
<div ref="slideA" class="slide a"></div>
<div ref="slideB" class="slide b"></div>
</div>
</div>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
-import define from './define';
+<script lang="ts" setup>
+import { nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import * as os from '@/os';
-const widget = define({
- name: 'slideshow',
- props: () => ({
- height: {
- type: 'number',
- default: 300,
- },
- folderId: {
- type: 'string',
- default: null,
- hidden: true,
- },
- })
-});
+const name = 'slideshow';
-export default defineComponent({
- extends: widget,
- data() {
- return {
- images: [],
- fetching: true,
- clock: null
- };
+const widgetPropsDef = {
+ height: {
+ type: 'number' as const,
+ default: 300,
+ },
+ folderId: {
+ type: 'string' as const,
+ default: null,
+ hidden: true,
},
- mounted() {
- this.$nextTick(() => {
- this.applySize();
- });
+};
- if (this.props.folderId != null) {
- this.fetch();
- }
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
- this.clock = setInterval(this.change, 10000);
- },
- beforeUnmount() {
- clearInterval(this.clock);
- },
- methods: {
- applySize() {
- let h;
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
- if (this.props.size == 1) {
- h = 250;
- } else {
- h = 170;
- }
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
- this.$el.style.height = `${h}px`;
- },
- resize() {
- if (this.props.size == 1) {
- this.props.size = 0;
- } else {
- this.props.size++;
- }
- this.save();
+const images = ref([]);
+const fetching = ref(true);
+const slideA = ref<HTMLElement>();
+const slideB = ref<HTMLElement>();
- this.applySize();
- },
- change() {
- if (this.images.length == 0) return;
+const change = () => {
+ if (images.value.length == 0) return;
- const index = Math.floor(Math.random() * this.images.length);
- const img = `url(${ this.images[index].url })`;
+ const index = Math.floor(Math.random() * images.value.length);
+ const img = `url(${ images.value[index].url })`;
- (this.$refs.slideB as any).style.backgroundImage = img;
+ slideB.value.style.backgroundImage = img;
- this.$refs.slideB.classList.add('anime');
- setTimeout(() => {
- // 既にこのウィジェットがunmountされていたら要素がない
- if ((this.$refs.slideA as any) == null) return;
+ slideB.value.classList.add('anime');
+ window.setTimeout(() => {
+ // 既にこのウィジェットがunmountされていたら要素がない
+ if (slideA.value == null) return;
- (this.$refs.slideA as any).style.backgroundImage = img;
+ slideA.value.style.backgroundImage = img;
- this.$refs.slideB.classList.remove('anime');
- }, 1000);
- },
- fetch() {
- this.fetching = true;
+ slideB.value.classList.remove('anime');
+ }, 1000);
+};
- os.api('drive/files', {
- folderId: this.props.folderId,
- type: 'image/*',
- limit: 100
- }).then(images => {
- this.images = images;
- this.fetching = false;
- (this.$refs.slideA as any).style.backgroundImage = '';
- (this.$refs.slideB as any).style.backgroundImage = '';
- this.change();
- });
- },
- choose() {
- os.selectDriveFolder(false).then(folder => {
- if (folder == null) {
- return;
- }
- this.props.folderId = folder.id;
- this.save();
- this.fetch();
- });
+const fetch = () => {
+ fetching.value = true;
+
+ os.api('drive/files', {
+ folderId: widgetProps.folderId,
+ type: 'image/*',
+ limit: 100
+ }).then(res => {
+ images.value = res;
+ fetching.value = false;
+ slideA.value.style.backgroundImage = '';
+ slideB.value.style.backgroundImage = '';
+ change();
+ });
+};
+
+const choose = () => {
+ os.selectDriveFolder(false).then(folder => {
+ if (folder == null) {
+ return;
}
+ widgetProps.folderId = folder.id;
+ save();
+ fetch();
+ });
+};
+
+onMounted(() => {
+ if (widgetProps.folderId != null) {
+ fetch();
}
+
+ const intervalId = window.setInterval(change, 10000);
+ onUnmounted(() => {
+ window.clearInterval(intervalId);
+ });
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue
index aee6a35b1d..fa700cc8ee 100644
--- a/packages/client/src/widgets/timeline.vue
+++ b/packages/client/src/widgets/timeline.vue
@@ -1,116 +1,129 @@
<template>
-<MkContainer :show-header="props.showHeader" :style="`height: ${props.height}px;`" :scrollable="true">
+<MkContainer :show-header="widgetProps.showHeader" :style="`height: ${widgetProps.height}px;`" :scrollable="true">
<template #header>
<button class="_button" @click="choose">
- <i v-if="props.src === 'home'" class="fas fa-home"></i>
- <i v-else-if="props.src === 'local'" class="fas fa-comments"></i>
- <i v-else-if="props.src === 'social'" class="fas fa-share-alt"></i>
- <i v-else-if="props.src === 'global'" class="fas fa-globe"></i>
- <i v-else-if="props.src === 'list'" class="fas fa-list-ul"></i>
- <i v-else-if="props.src === 'antenna'" class="fas fa-satellite"></i>
- <span style="margin-left: 8px;">{{ props.src === 'list' ? props.list.name : props.src === 'antenna' ? props.antenna.name : $t('_timelines.' + props.src) }}</span>
+ <i v-if="widgetProps.src === 'home'" class="fas fa-home"></i>
+ <i v-else-if="widgetProps.src === 'local'" class="fas fa-comments"></i>
+ <i v-else-if="widgetProps.src === 'social'" class="fas fa-share-alt"></i>
+ <i v-else-if="widgetProps.src === 'global'" class="fas fa-globe"></i>
+ <i v-else-if="widgetProps.src === 'list'" class="fas fa-list-ul"></i>
+ <i v-else-if="widgetProps.src === 'antenna'" class="fas fa-satellite"></i>
+ <span style="margin-left: 8px;">{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : $t('_timelines.' + widgetProps.src) }}</span>
<i :class="menuOpened ? 'fas fa-angle-up' : 'fas fa-angle-down'" style="margin-left: 8px;"></i>
</button>
</template>
<div>
- <XTimeline :key="props.src === 'list' ? `list:${props.list.id}` : props.src === 'antenna' ? `antenna:${props.antenna.id}` : props.src" :src="props.src" :list="props.list ? props.list.id : null" :antenna="props.antenna ? props.antenna.id : null"/>
+ <XTimeline :key="widgetProps.src === 'list' ? `list:${widgetProps.list.id}` : widgetProps.src === 'antenna' ? `antenna:${widgetProps.antenna.id}` : widgetProps.src" :src="widgetProps.src" :list="widgetProps.list ? widgetProps.list.id : null" :antenna="widgetProps.antenna ? widgetProps.antenna.id : null"/>
</div>
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
+import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
import XTimeline from '@/components/timeline.vue';
-import define from './define';
-import * as os from '@/os';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
-const widget = define({
- name: 'timeline',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- height: {
- type: 'number',
- default: 300,
- },
- src: {
- type: 'string',
- default: 'home',
- hidden: true,
- },
- list: {
- type: 'object',
- default: null,
- hidden: true,
- },
- })
-});
+const name = 'timeline';
-export default defineComponent({
- components: {
- MkContainer,
- XTimeline,
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- extends: widget,
-
- data() {
- return {
- menuOpened: false,
- };
+ height: {
+ type: 'number' as const,
+ default: 300,
+ },
+ src: {
+ type: 'string' as const,
+ default: 'home',
+ hidden: true,
+ },
+ antenna: {
+ type: 'object' as const,
+ default: null,
+ hidden: true,
},
+ list: {
+ type: 'object' as const,
+ default: null,
+ hidden: true,
+ },
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure, save } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const menuOpened = ref(false);
+
+const setSrc = (src) => {
+ widgetProps.src = src;
+ save();
+};
- methods: {
- async choose(ev) {
- this.menuOpened = true;
- const [antennas, lists] = await Promise.all([
- os.api('antennas/list'),
- os.api('users/lists/list')
- ]);
- const antennaItems = antennas.map(antenna => ({
- text: antenna.name,
- icon: 'fas fa-satellite',
- action: () => {
- this.props.antenna = antenna;
- this.setSrc('antenna');
- }
- }));
- const listItems = lists.map(list => ({
- text: list.name,
- icon: 'fas fa-list-ul',
- action: () => {
- this.props.list = list;
- this.setSrc('list');
- }
- }));
- os.popupMenu([{
- text: this.$ts._timelines.home,
- icon: 'fas fa-home',
- action: () => { this.setSrc('home') }
- }, {
- text: this.$ts._timelines.local,
- icon: 'fas fa-comments',
- action: () => { this.setSrc('local') }
- }, {
- text: this.$ts._timelines.social,
- icon: 'fas fa-share-alt',
- action: () => { this.setSrc('social') }
- }, {
- text: this.$ts._timelines.global,
- icon: 'fas fa-globe',
- action: () => { this.setSrc('global') }
- }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {
- this.menuOpened = false;
- });
- },
+const choose = async (ev) => {
+ menuOpened.value = true;
+ const [antennas, lists] = await Promise.all([
+ os.api('antennas/list'),
+ os.api('users/lists/list')
+ ]);
+ const antennaItems = antennas.map(antenna => ({
+ text: antenna.name,
+ icon: 'fas fa-satellite',
+ action: () => {
+ widgetProps.antenna = antenna;
+ setSrc('antenna');
+ }
+ }));
+ const listItems = lists.map(list => ({
+ text: list.name,
+ icon: 'fas fa-list-ul',
+ action: () => {
+ widgetProps.list = list;
+ setSrc('list');
+ }
+ }));
+ os.popupMenu([{
+ text: i18n.locale._timelines.home,
+ icon: 'fas fa-home',
+ action: () => { setSrc('home') }
+ }, {
+ text: i18n.locale._timelines.local,
+ icon: 'fas fa-comments',
+ action: () => { setSrc('local') }
+ }, {
+ text: i18n.locale._timelines.social,
+ icon: 'fas fa-share-alt',
+ action: () => { setSrc('social') }
+ }, {
+ text: i18n.locale._timelines.global,
+ icon: 'fas fa-globe',
+ action: () => { setSrc('global') }
+ }, antennaItems.length > 0 ? null : undefined, ...antennaItems, listItems.length > 0 ? null : undefined, ...listItems], ev.currentTarget || ev.target).then(() => {
+ menuOpened.value = false;
+ });
+};
- setSrc(src) {
- this.props.src = src;
- this.save();
- },
- }
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue
index ffad93c02b..eb5eb4049f 100644
--- a/packages/client/src/widgets/trends.vue
+++ b/packages/client/src/widgets/trends.vue
@@ -1,10 +1,10 @@
<template>
-<MkContainer :show-header="props.showHeader">
+<MkContainer :show-header="widgetProps.showHeader">
<template #header><i class="fas fa-hashtag"></i>{{ $ts._widgets.trends }}</template>
<div class="wbrkwala">
<MkLoading v-if="fetching"/>
- <transition-group v-else tag="div" name="chart" class="tags">
+ <transition-group v-else tag="div" :name="$store.state.animation ? 'chart' : ''" class="tags">
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
@@ -17,49 +17,59 @@
</MkContainer>
</template>
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, onUnmounted, ref } from 'vue';
+import { GetFormResultType } from '@/scripts/form';
+import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import MkContainer from '@/components/ui/container.vue';
-import define from './define';
import MkMiniChart from '@/components/mini-chart.vue';
import * as os from '@/os';
-const widget = define({
- name: 'hashtags',
- props: () => ({
- showHeader: {
- type: 'boolean',
- default: true,
- },
- })
-});
+const name = 'hashtags';
-export default defineComponent({
- components: {
- MkContainer, MkMiniChart
- },
- extends: widget,
- data() {
- return {
- stats: [],
- fetching: true,
- };
- },
- mounted() {
- this.fetch();
- this.clock = setInterval(this.fetch, 1000 * 60);
- },
- beforeUnmount() {
- clearInterval(this.clock);
+const widgetPropsDef = {
+ showHeader: {
+ type: 'boolean' as const,
+ default: true,
},
- methods: {
- fetch() {
- os.api('hashtags/trend').then(stats => {
- this.stats = stats;
- this.fetching = false;
- });
- }
- }
+};
+
+type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
+
+// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
+//const props = defineProps<WidgetComponentProps<WidgetProps>>();
+//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
+const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
+const emit = defineEmits<{ (e: 'updateProps', props: WidgetProps); }>();
+
+const { widgetProps, configure } = useWidgetPropsManager(name,
+ widgetPropsDef,
+ props,
+ emit,
+);
+
+const stats = ref([]);
+const fetching = ref(true);
+
+const fetch = () => {
+ os.api('hashtags/trend').then(stats => {
+ stats.value = stats;
+ fetching.value = false;
+ });
+};
+
+onMounted(() => {
+ fetch();
+ const intervalId = window.setInterval(fetch, 1000 * 60);
+ onUnmounted(() => {
+ window.clearInterval(intervalId);
+ });
+});
+
+defineExpose<WidgetComponentExpose>({
+ name,
+ configure,
+ id: props.widget ? props.widget.id : null,
});
</script>
diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts
new file mode 100644
index 0000000000..81239bfb3b
--- /dev/null
+++ b/packages/client/src/widgets/widget.ts
@@ -0,0 +1,71 @@
+import { reactive, watch } from 'vue';
+import { throttle } from 'throttle-debounce';
+import { Form, GetFormResultType } from '@/scripts/form';
+import * as os from '@/os';
+
+export type Widget<P extends Record<string, unknown>> = {
+ id: string;
+ data: Partial<P>;
+};
+
+export type WidgetComponentProps<P extends Record<string, unknown>> = {
+ widget?: Widget<P>;
+};
+
+export type WidgetComponentEmits<P extends Record<string, unknown>> = {
+ (e: 'updateProps', props: P);
+};
+
+export type WidgetComponentExpose = {
+ name: string;
+ id: string | null;
+ configure: () => void;
+};
+
+export const useWidgetPropsManager = <F extends Form & Record<string, { default: any; }>>(
+ name: string,
+ propsDef: F,
+ props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,
+ emit: WidgetComponentEmits<GetFormResultType<F>>,
+): {
+ widgetProps: GetFormResultType<F>;
+ save: () => void;
+ configure: () => void;
+} => {
+ const widgetProps = reactive(props.widget ? JSON.parse(JSON.stringify(props.widget.data)) : {});
+
+ const mergeProps = () => {
+ for (const prop of Object.keys(propsDef)) {
+ if (widgetProps.hasOwnProperty(prop)) continue;
+ widgetProps[prop] = propsDef[prop].default;
+ }
+ };
+ watch(widgetProps, () => {
+ mergeProps();
+ }, { deep: true, immediate: true, });
+
+ const save = throttle(3000, () => {
+ emit('updateProps', widgetProps)
+ });
+
+ const configure = async () => {
+ const form = JSON.parse(JSON.stringify(propsDef));
+ for (const item of Object.keys(form)) {
+ form[item].default = widgetProps[item];
+ }
+ const { canceled, result } = await os.form(name, form);
+ if (canceled) return;
+
+ for (const key of Object.keys(result)) {
+ widgetProps[key] = result[key];
+ }
+
+ save();
+ };
+
+ return {
+ widgetProps,
+ save,
+ configure,
+ };
+};