From 9384f5399da39e53855beb8e7f8ded1aa56bf72e Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 27 Dec 2022 14:36:33 +0900 Subject: rename: client -> frontend --- .../backend/src/server/web/ClientServerService.ts | 4 +- packages/client/.eslintrc.js | 89 - packages/client/.vscode/settings.json | 11 - packages/client/@types/global.d.ts | 10 - packages/client/@types/theme.d.ts | 7 - packages/client/@types/vue.d.ts | 16 - packages/client/assets/about-icon.png | Bin 20528 -> 0 bytes packages/client/assets/dummy.png | Bin 6285 -> 0 bytes packages/client/assets/fedi.jpg | Bin 77752 -> 0 bytes packages/client/assets/label-red.svg | 6 - packages/client/assets/label.svg | 6 - packages/client/assets/misskey.svg | 7 - packages/client/assets/remove.png | Bin 424 -> 0 bytes packages/client/assets/sounds/aisha/1.mp3 | Bin 34480 -> 0 bytes packages/client/assets/sounds/aisha/2.mp3 | Bin 24031 -> 0 bytes packages/client/assets/sounds/aisha/3.mp3 | Bin 29256 -> 0 bytes .../client/assets/sounds/noizenecio/kick_gaba1.mp3 | Bin 18866 -> 0 bytes .../client/assets/sounds/noizenecio/kick_gaba2.mp3 | Bin 27144 -> 0 bytes .../client/assets/sounds/noizenecio/kick_gaba3.mp3 | Bin 19853 -> 0 bytes .../client/assets/sounds/noizenecio/kick_gaba4.mp3 | Bin 19853 -> 0 bytes .../client/assets/sounds/noizenecio/kick_gaba5.mp3 | Bin 19853 -> 0 bytes .../client/assets/sounds/noizenecio/kick_gaba6.mp3 | Bin 19853 -> 0 bytes .../client/assets/sounds/noizenecio/kick_gaba7.mp3 | Bin 19853 -> 0 bytes packages/client/assets/sounds/syuilo/down.mp3 | Bin 18240 -> 0 bytes packages/client/assets/sounds/syuilo/kick.mp3 | Bin 15672 -> 0 bytes .../assets/sounds/syuilo/pirori-square-wet.mp3 | Bin 139200 -> 0 bytes .../client/assets/sounds/syuilo/pirori-wet.mp3 | Bin 139200 -> 0 bytes packages/client/assets/sounds/syuilo/pirori.mp3 | Bin 19200 -> 0 bytes packages/client/assets/sounds/syuilo/poi1.mp3 | Bin 18240 -> 0 bytes packages/client/assets/sounds/syuilo/poi2.mp3 | Bin 18240 -> 0 bytes packages/client/assets/sounds/syuilo/pope1.mp3 | Bin 18240 -> 0 bytes packages/client/assets/sounds/syuilo/pope2.mp3 | Bin 18240 -> 0 bytes packages/client/assets/sounds/syuilo/popo.mp3 | Bin 18240 -> 0 bytes .../client/assets/sounds/syuilo/queue-jammed.mp3 | Bin 351466 -> 0 bytes packages/client/assets/sounds/syuilo/reverved.mp3 | Bin 276480 -> 0 bytes packages/client/assets/sounds/syuilo/ryukyu.mp3 | Bin 139200 -> 0 bytes packages/client/assets/sounds/syuilo/snare.mp3 | Bin 26121 -> 0 bytes .../client/assets/sounds/syuilo/square-pico.mp3 | Bin 19200 -> 0 bytes packages/client/assets/sounds/syuilo/triple.mp3 | Bin 18240 -> 0 bytes packages/client/assets/sounds/syuilo/up.mp3 | Bin 18240 -> 0 bytes packages/client/assets/sounds/syuilo/waon.mp3 | Bin 18240 -> 0 bytes packages/client/assets/tagcanvas.min.js | 21 - packages/client/assets/unread.svg | 7 - packages/client/package.json | 94 -- packages/client/src/account.ts | 238 --- packages/client/src/components/MkAbuseReport.vue | 109 -- .../client/src/components/MkAbuseReportWindow.vue | 65 - .../client/src/components/MkActiveUsersHeatmap.vue | 236 --- packages/client/src/components/MkAnalogClock.vue | 225 --- packages/client/src/components/MkAutocomplete.vue | 476 ------ packages/client/src/components/MkAvatars.vue | 24 - packages/client/src/components/MkButton.vue | 227 --- packages/client/src/components/MkCaptcha.vue | 118 -- .../src/components/MkChannelFollowButton.vue | 129 -- .../client/src/components/MkChannelPreview.vue | 154 -- packages/client/src/components/MkChart.vue | 859 ---------- packages/client/src/components/MkChartTooltip.vue | 53 - packages/client/src/components/MkCode.core.vue | 20 - packages/client/src/components/MkCode.vue | 15 - packages/client/src/components/MkContainer.vue | 275 --- packages/client/src/components/MkContextMenu.vue | 85 - packages/client/src/components/MkCropperDialog.vue | 174 -- packages/client/src/components/MkCwButton.vue | 62 - .../client/src/components/MkDateSeparatedList.vue | 189 --- packages/client/src/components/MkDialog.vue | 208 --- packages/client/src/components/MkDigitalClock.vue | 77 - packages/client/src/components/MkDrive.file.vue | 334 ---- packages/client/src/components/MkDrive.folder.vue | 330 ---- .../client/src/components/MkDrive.navFolder.vue | 147 -- packages/client/src/components/MkDrive.vue | 801 --------- .../client/src/components/MkDriveFileThumbnail.vue | 80 - .../client/src/components/MkDriveSelectDialog.vue | 58 - packages/client/src/components/MkDriveWindow.vue | 30 - .../src/components/MkEmojiPicker.section.vue | 36 - packages/client/src/components/MkEmojiPicker.vue | 569 ------- .../client/src/components/MkEmojiPickerDialog.vue | 73 - .../client/src/components/MkEmojiPickerWindow.vue | 180 -- .../client/src/components/MkFeaturedPhotos.vue | 22 - .../src/components/MkFileCaptionEditWindow.vue | 175 -- .../client/src/components/MkFileListForAdmin.vue | 117 -- packages/client/src/components/MkFolder.vue | 159 -- packages/client/src/components/MkFollowButton.vue | 187 -- .../client/src/components/MkForgotPassword.vue | 80 - packages/client/src/components/MkFormDialog.vue | 127 -- packages/client/src/components/MkFormula.vue | 24 - packages/client/src/components/MkFormulaCore.vue | 34 - .../client/src/components/MkGalleryPostPreview.vue | 115 -- packages/client/src/components/MkGoogle.vue | 51 - packages/client/src/components/MkImageViewer.vue | 77 - .../client/src/components/MkImgWithBlurhash.vue | 76 - packages/client/src/components/MkInfo.vue | 34 - .../client/src/components/MkInstanceCardMini.vue | 105 -- packages/client/src/components/MkInstanceStats.vue | 255 --- .../client/src/components/MkInstanceTicker.vue | 80 - packages/client/src/components/MkKeyValue.vue | 58 - packages/client/src/components/MkLaunchPad.vue | 138 -- packages/client/src/components/MkLink.vue | 47 - packages/client/src/components/MkMarquee.vue | 106 -- packages/client/src/components/MkMediaBanner.vue | 102 -- packages/client/src/components/MkMediaImage.vue | 130 -- packages/client/src/components/MkMediaList.vue | 189 --- packages/client/src/components/MkMediaVideo.vue | 88 - packages/client/src/components/MkMention.vue | 66 - packages/client/src/components/MkMenu.child.vue | 65 - packages/client/src/components/MkMenu.vue | 367 ---- packages/client/src/components/MkMiniChart.vue | 73 - packages/client/src/components/MkModal.vue | 406 ----- .../client/src/components/MkModalPageWindow.vue | 181 -- packages/client/src/components/MkModalWindow.vue | 146 -- packages/client/src/components/MkNote.vue | 658 -------- packages/client/src/components/MkNoteDetailed.vue | 677 -------- packages/client/src/components/MkNoteHeader.vue | 75 - packages/client/src/components/MkNotePreview.vue | 112 -- packages/client/src/components/MkNoteSimple.vue | 119 -- packages/client/src/components/MkNoteSub.vue | 140 -- packages/client/src/components/MkNotes.vue | 58 - packages/client/src/components/MkNotification.vue | 323 ---- .../src/components/MkNotificationSettingWindow.vue | 87 - .../client/src/components/MkNotificationToast.vue | 68 - packages/client/src/components/MkNotifications.vue | 104 -- packages/client/src/components/MkNumberDiff.vue | 47 - .../client/src/components/MkObjectView.value.vue | 160 -- packages/client/src/components/MkObjectView.vue | 20 - packages/client/src/components/MkPagePreview.vue | 162 -- packages/client/src/components/MkPageWindow.vue | 140 -- packages/client/src/components/MkPagination.vue | 317 ---- packages/client/src/components/MkPoll.vue | 152 -- packages/client/src/components/MkPollEditor.vue | 219 --- packages/client/src/components/MkPopupMenu.vue | 36 - packages/client/src/components/MkPostForm.vue | 1050 ------------ .../client/src/components/MkPostFormAttaches.vue | 168 -- .../client/src/components/MkPostFormDialog.vue | 19 - .../components/MkPushNotificationAllowButton.vue | 167 -- packages/client/src/components/MkReactionIcon.vue | 13 - .../client/src/components/MkReactionTooltip.vue | 43 - .../src/components/MkReactionsViewer.details.vue | 96 -- .../src/components/MkReactionsViewer.reaction.vue | 135 -- .../client/src/components/MkReactionsViewer.vue | 36 - packages/client/src/components/MkRemoteCaution.vue | 25 - packages/client/src/components/MkRenoteButton.vue | 99 -- packages/client/src/components/MkRipple.vue | 116 -- packages/client/src/components/MkSample.vue | 116 -- packages/client/src/components/MkSignin.vue | 259 --- packages/client/src/components/MkSigninDialog.vue | 46 - packages/client/src/components/MkSignup.vue | 246 --- packages/client/src/components/MkSignupDialog.vue | 46 - packages/client/src/components/MkSparkle.vue | 130 -- .../client/src/components/MkSubNoteContent.vue | 90 - packages/client/src/components/MkSuperMenu.vue | 161 -- packages/client/src/components/MkTab.vue | 73 - packages/client/src/components/MkTagCloud.vue | 90 - packages/client/src/components/MkTimeline.vue | 143 -- packages/client/src/components/MkToast.vue | 66 - .../src/components/MkTokenGenerateWindow.vue | 90 - packages/client/src/components/MkTooltip.vue | 101 -- packages/client/src/components/MkUpdated.vue | 51 - packages/client/src/components/MkUrlPreview.vue | 383 ----- .../client/src/components/MkUrlPreviewPopup.vue | 45 - packages/client/src/components/MkUserCardMini.vue | 99 -- packages/client/src/components/MkUserInfo.vue | 137 -- packages/client/src/components/MkUserList.vue | 39 - .../src/components/MkUserOnlineIndicator.vue | 45 - packages/client/src/components/MkUserPreview.vue | 184 -- .../client/src/components/MkUserSelectDialog.vue | 190 --- packages/client/src/components/MkUsersTooltip.vue | 51 - packages/client/src/components/MkVisibility.vue | 48 - .../client/src/components/MkVisibilityPicker.vue | 159 -- packages/client/src/components/MkWaitingDialog.vue | 73 - packages/client/src/components/MkWidgets.vue | 165 -- packages/client/src/components/MkWindow.vue | 571 ------- packages/client/src/components/MkYoutubePlayer.vue | 72 - packages/client/src/components/form/checkbox.vue | 144 -- packages/client/src/components/form/folder.vue | 107 -- packages/client/src/components/form/input.vue | 263 --- packages/client/src/components/form/link.vue | 95 -- packages/client/src/components/form/radio.vue | 132 -- packages/client/src/components/form/radios.vue | 83 - packages/client/src/components/form/range.vue | 259 --- packages/client/src/components/form/section.vue | 43 - packages/client/src/components/form/select.vue | 279 --- packages/client/src/components/form/slot.vue | 41 - packages/client/src/components/form/split.vue | 27 - packages/client/src/components/form/suspense.vue | 98 -- packages/client/src/components/form/switch.vue | 144 -- packages/client/src/components/form/textarea.vue | 260 --- packages/client/src/components/global/MkA.vue | 102 -- packages/client/src/components/global/MkAcct.vue | 27 - packages/client/src/components/global/MkAd.vue | 186 -- packages/client/src/components/global/MkAvatar.vue | 143 -- .../client/src/components/global/MkEllipsis.vue | 34 - packages/client/src/components/global/MkEmoji.vue | 81 - packages/client/src/components/global/MkError.vue | 36 - .../client/src/components/global/MkLoading.vue | 101 -- .../global/MkMisskeyFlavoredMarkdown.vue | 191 --- .../client/src/components/global/MkPageHeader.vue | 368 ---- packages/client/src/components/global/MkSpacer.vue | 96 -- .../src/components/global/MkStickyContainer.vue | 66 - packages/client/src/components/global/MkTime.vue | 56 - packages/client/src/components/global/MkUrl.vue | 89 - .../client/src/components/global/MkUserName.vue | 15 - .../client/src/components/global/RouterView.vue | 61 - packages/client/src/components/global/i18n.ts | 42 - packages/client/src/components/index.ts | 61 - packages/client/src/components/mfm.ts | 331 ---- packages/client/src/components/page/page.block.vue | 44 - .../client/src/components/page/page.button.vue | 66 - .../client/src/components/page/page.canvas.vue | 49 - .../client/src/components/page/page.counter.vue | 52 - packages/client/src/components/page/page.if.vue | 31 - packages/client/src/components/page/page.image.vue | 28 - packages/client/src/components/page/page.note.vue | 47 - .../src/components/page/page.number-input.vue | 55 - packages/client/src/components/page/page.post.vue | 109 -- .../src/components/page/page.radio-button.vue | 45 - .../client/src/components/page/page.section.vue | 60 - .../client/src/components/page/page.switch.vue | 55 - .../client/src/components/page/page.text-input.vue | 55 - packages/client/src/components/page/page.text.vue | 68 - .../src/components/page/page.textarea-input.vue | 47 - .../client/src/components/page/page.textarea.vue | 39 - packages/client/src/components/page/page.vue | 85 - packages/client/src/config.ts | 15 - packages/client/src/const.ts | 45 - packages/client/src/directives/adaptive-border.ts | 24 - packages/client/src/directives/anim.ts | 18 - packages/client/src/directives/appear.ts | 22 - packages/client/src/directives/click-anime.ts | 31 - packages/client/src/directives/follow-append.ts | 35 - packages/client/src/directives/get-size.ts | 54 - packages/client/src/directives/hotkey.ts | 24 - packages/client/src/directives/index.ts | 28 - packages/client/src/directives/panel.ts | 24 - packages/client/src/directives/ripple.ts | 18 - packages/client/src/directives/size.ts | 123 -- packages/client/src/directives/tooltip.ts | 93 - packages/client/src/directives/user-preview.ts | 118 -- packages/client/src/emojilist.json | 1785 -------------------- packages/client/src/events.ts | 4 - packages/client/src/filters/bytes.ts | 9 - packages/client/src/filters/note.ts | 3 - packages/client/src/filters/number.ts | 1 - packages/client/src/filters/user.ts | 15 - packages/client/src/i18n.ts | 5 - packages/client/src/init.ts | 433 ----- packages/client/src/instance.ts | 45 - packages/client/src/navbar.ts | 135 -- packages/client/src/nirax.ts | 275 --- packages/client/src/os.ts | 588 ------- packages/client/src/pages/_empty_.vue | 7 - packages/client/src/pages/_error_.vue | 89 - packages/client/src/pages/_loading_.vue | 6 - packages/client/src/pages/about-misskey.vue | 264 --- packages/client/src/pages/about.emojis.vue | 134 -- packages/client/src/pages/about.federation.vue | 106 -- packages/client/src/pages/about.vue | 166 -- packages/client/src/pages/admin-file.vue | 160 -- packages/client/src/pages/admin/_header_.vue | 292 ---- packages/client/src/pages/admin/abuses.vue | 97 -- packages/client/src/pages/admin/ads.vue | 132 -- packages/client/src/pages/admin/announcements.vue | 112 -- packages/client/src/pages/admin/bot-protection.vue | 109 -- packages/client/src/pages/admin/database.vue | 35 - packages/client/src/pages/admin/email-settings.vue | 126 -- .../client/src/pages/admin/emoji-edit-dialog.vue | 106 -- packages/client/src/pages/admin/emojis.vue | 398 ----- packages/client/src/pages/admin/files.vue | 120 -- packages/client/src/pages/admin/index.vue | 316 ---- packages/client/src/pages/admin/instance-block.vue | 51 - .../src/pages/admin/integrations.discord.vue | 60 - .../client/src/pages/admin/integrations.github.vue | 60 - .../src/pages/admin/integrations.twitter.vue | 60 - packages/client/src/pages/admin/integrations.vue | 57 - packages/client/src/pages/admin/metrics.vue | 472 ------ packages/client/src/pages/admin/object-storage.vue | 148 -- packages/client/src/pages/admin/other-settings.vue | 44 - .../src/pages/admin/overview.active-users.vue | 217 --- .../src/pages/admin/overview.ap-requests.vue | 346 ---- .../client/src/pages/admin/overview.federation.vue | 185 -- .../client/src/pages/admin/overview.heatmap.vue | 15 - .../client/src/pages/admin/overview.instances.vue | 50 - .../client/src/pages/admin/overview.moderators.vue | 55 - packages/client/src/pages/admin/overview.pie.vue | 110 -- .../src/pages/admin/overview.queue.chart.vue | 186 -- packages/client/src/pages/admin/overview.queue.vue | 127 -- .../client/src/pages/admin/overview.retention.vue | 49 - packages/client/src/pages/admin/overview.stats.vue | 155 -- packages/client/src/pages/admin/overview.users.vue | 57 - packages/client/src/pages/admin/overview.vue | 190 --- packages/client/src/pages/admin/proxy-account.vue | 62 - .../client/src/pages/admin/queue.chart.chart.vue | 186 -- packages/client/src/pages/admin/queue.chart.vue | 149 -- packages/client/src/pages/admin/queue.vue | 56 - packages/client/src/pages/admin/relays.vue | 103 -- packages/client/src/pages/admin/security.vue | 179 -- packages/client/src/pages/admin/settings.vue | 262 --- packages/client/src/pages/admin/users.vue | 170 -- packages/client/src/pages/announcements.vue | 69 - packages/client/src/pages/antenna-timeline.vue | 128 -- packages/client/src/pages/api-console.vue | 89 - packages/client/src/pages/auth.form.vue | 60 - packages/client/src/pages/auth.vue | 91 - packages/client/src/pages/channel-editor.vue | 122 -- packages/client/src/pages/channel.vue | 184 -- packages/client/src/pages/channels.vue | 79 - packages/client/src/pages/clip.vue | 129 -- packages/client/src/pages/drive.vue | 25 - packages/client/src/pages/emojis.emoji.vue | 72 - packages/client/src/pages/explore.featured.vue | 30 - packages/client/src/pages/explore.users.vue | 148 -- packages/client/src/pages/explore.vue | 87 - packages/client/src/pages/favorites.vue | 49 - packages/client/src/pages/follow-requests.vue | 153 -- packages/client/src/pages/follow.vue | 62 - packages/client/src/pages/gallery/edit.vue | 149 -- packages/client/src/pages/gallery/index.vue | 139 -- packages/client/src/pages/gallery/post.vue | 265 --- packages/client/src/pages/instance-info.vue | 258 --- packages/client/src/pages/messaging/index.vue | 327 ---- .../src/pages/messaging/messaging-room.form.vue | 364 ---- .../src/pages/messaging/messaging-room.message.vue | 367 ---- .../client/src/pages/messaging/messaging-room.vue | 411 ----- packages/client/src/pages/mfm-cheat-sheet.vue | 387 ----- packages/client/src/pages/miauth.vue | 90 - packages/client/src/pages/my-antennas/create.vue | 46 - packages/client/src/pages/my-antennas/edit.vue | 43 - packages/client/src/pages/my-antennas/editor.vue | 155 -- packages/client/src/pages/my-antennas/index.vue | 64 - packages/client/src/pages/my-clips/index.vue | 100 -- packages/client/src/pages/my-lists/index.vue | 82 - packages/client/src/pages/my-lists/list.vue | 162 -- packages/client/src/pages/not-found.vue | 22 - packages/client/src/pages/note.vue | 206 --- packages/client/src/pages/notifications.vue | 95 -- .../pages/page-editor/els/page-editor.el.image.vue | 63 - .../pages/page-editor/els/page-editor.el.note.vue | 57 - .../page-editor/els/page-editor.el.section.vue | 97 -- .../pages/page-editor/els/page-editor.el.text.vue | 54 - .../src/pages/page-editor/page-editor.blocks.vue | 65 - .../pages/page-editor/page-editor.container.vue | 155 -- .../client/src/pages/page-editor/page-editor.vue | 394 ----- packages/client/src/pages/page.vue | 277 --- packages/client/src/pages/pages.vue | 99 -- packages/client/src/pages/preview.vue | 27 - packages/client/src/pages/registry.keys.vue | 96 -- packages/client/src/pages/registry.value.vue | 123 -- packages/client/src/pages/registry.vue | 74 - packages/client/src/pages/reset-password.vue | 59 - packages/client/src/pages/scratchpad.vue | 137 -- packages/client/src/pages/search.vue | 38 - packages/client/src/pages/settings/2fa.vue | 216 --- .../client/src/pages/settings/account-info.vue | 158 -- packages/client/src/pages/settings/accounts.vue | 143 -- packages/client/src/pages/settings/api.vue | 46 - packages/client/src/pages/settings/apps.vue | 96 -- packages/client/src/pages/settings/custom-css.vue | 46 - packages/client/src/pages/settings/deck.vue | 39 - .../client/src/pages/settings/delete-account.vue | 52 - packages/client/src/pages/settings/drive.vue | 145 -- packages/client/src/pages/settings/email.vue | 111 -- packages/client/src/pages/settings/general.vue | 196 --- .../client/src/pages/settings/import-export.vue | 165 -- packages/client/src/pages/settings/index.vue | 291 ---- .../client/src/pages/settings/instance-mute.vue | 53 - packages/client/src/pages/settings/integration.vue | 99 -- packages/client/src/pages/settings/mute-block.vue | 61 - packages/client/src/pages/settings/navbar.vue | 87 - .../client/src/pages/settings/notifications.vue | 90 - packages/client/src/pages/settings/other.vue | 47 - .../client/src/pages/settings/plugin.install.vue | 124 -- packages/client/src/pages/settings/plugin.vue | 98 -- .../src/pages/settings/preferences-backups.vue | 444 ----- packages/client/src/pages/settings/privacy.vue | 100 -- packages/client/src/pages/settings/profile.vue | 220 --- packages/client/src/pages/settings/reaction.vue | 154 -- packages/client/src/pages/settings/security.vue | 160 -- .../client/src/pages/settings/sounds.sound.vue | 45 - packages/client/src/pages/settings/sounds.vue | 82 - .../src/pages/settings/statusbar.statusbar.vue | 140 -- packages/client/src/pages/settings/statusbar.vue | 54 - .../client/src/pages/settings/theme.install.vue | 80 - .../client/src/pages/settings/theme.manage.vue | 78 - packages/client/src/pages/settings/theme.vue | 409 ----- .../client/src/pages/settings/webhook.edit.vue | 95 -- packages/client/src/pages/settings/webhook.new.vue | 82 - packages/client/src/pages/settings/webhook.vue | 53 - packages/client/src/pages/settings/word-mute.vue | 128 -- packages/client/src/pages/share.vue | 169 -- packages/client/src/pages/signup-complete.vue | 41 - packages/client/src/pages/tag.vue | 35 - packages/client/src/pages/theme-editor.vue | 283 ---- packages/client/src/pages/timeline.tutorial.vue | 142 -- packages/client/src/pages/timeline.vue | 183 -- packages/client/src/pages/user-info.vue | 485 ------ packages/client/src/pages/user-list-timeline.vue | 121 -- packages/client/src/pages/user/clips.vue | 47 - packages/client/src/pages/user/follow-list.vue | 47 - packages/client/src/pages/user/followers.vue | 61 - packages/client/src/pages/user/following.vue | 61 - packages/client/src/pages/user/gallery.vue | 38 - packages/client/src/pages/user/home.vue | 530 ------ packages/client/src/pages/user/index.activity.vue | 52 - packages/client/src/pages/user/index.photos.vue | 102 -- packages/client/src/pages/user/index.timeline.vue | 45 - packages/client/src/pages/user/index.vue | 113 -- packages/client/src/pages/user/pages.vue | 30 - packages/client/src/pages/user/reactions.vue | 61 - packages/client/src/pages/welcome.entrance.a.vue | 309 ---- packages/client/src/pages/welcome.entrance.b.vue | 237 --- packages/client/src/pages/welcome.entrance.c.vue | 306 ---- packages/client/src/pages/welcome.setup.vue | 89 - packages/client/src/pages/welcome.timeline.vue | 99 -- packages/client/src/pages/welcome.vue | 30 - packages/client/src/pizzax.ts | 169 -- packages/client/src/plugin.ts | 123 -- packages/client/src/router.ts | 501 ------ packages/client/src/scripts/2fa.ts | 33 - packages/client/src/scripts/aiscript/api.ts | 43 - packages/client/src/scripts/array.ts | 149 -- packages/client/src/scripts/autocomplete.ts | 276 --- packages/client/src/scripts/chart-vline.ts | 21 - packages/client/src/scripts/check-word-mute.ts | 37 - packages/client/src/scripts/clone.ts | 18 - packages/client/src/scripts/collect-page-vars.ts | 68 - packages/client/src/scripts/contains.ts | 9 - packages/client/src/scripts/copy-to-clipboard.ts | 33 - packages/client/src/scripts/device-kind.ts | 10 - packages/client/src/scripts/emoji-base.ts | 20 - packages/client/src/scripts/emojilist.ts | 17 - .../src/scripts/extract-avg-color-from-blurhash.ts | 9 - packages/client/src/scripts/extract-mentions.ts | 11 - .../client/src/scripts/extract-url-from-mfm.ts | 19 - packages/client/src/scripts/focus.ts | 27 - packages/client/src/scripts/form.ts | 59 - packages/client/src/scripts/format-time-string.ts | 50 - packages/client/src/scripts/gen-search-query.ts | 30 - packages/client/src/scripts/get-account-from-id.ts | 7 - packages/client/src/scripts/get-note-menu.ts | 341 ---- packages/client/src/scripts/get-note-summary.ts | 55 - .../client/src/scripts/get-static-image-url.ts | 19 - packages/client/src/scripts/get-user-menu.ts | 253 --- packages/client/src/scripts/get-user-name.ts | 3 - packages/client/src/scripts/hotkey.ts | 90 - packages/client/src/scripts/hpml/block.ts | 109 -- packages/client/src/scripts/hpml/evaluator.ts | 232 --- packages/client/src/scripts/hpml/expr.ts | 79 - packages/client/src/scripts/hpml/index.ts | 103 -- packages/client/src/scripts/hpml/lib.ts | 247 --- packages/client/src/scripts/hpml/type-checker.ts | 191 --- packages/client/src/scripts/i18n.ts | 29 - packages/client/src/scripts/idb-proxy.ts | 36 - packages/client/src/scripts/initialize-sw.ts | 13 - packages/client/src/scripts/is-device-darkmode.ts | 3 - packages/client/src/scripts/keycode.ts | 33 - packages/client/src/scripts/langmap.ts | 666 -------- packages/client/src/scripts/login-id.ts | 11 - packages/client/src/scripts/lookup-user.ts | 36 - packages/client/src/scripts/media-proxy.ts | 15 - packages/client/src/scripts/mfm-tags.ts | 1 - packages/client/src/scripts/page-metadata.ts | 41 - packages/client/src/scripts/physics.ts | 152 -- packages/client/src/scripts/please-login.ts | 21 - packages/client/src/scripts/popout.ts | 23 - packages/client/src/scripts/popup-position.ts | 158 -- packages/client/src/scripts/reaction-picker.ts | 41 - packages/client/src/scripts/safe-uri-decode.ts | 7 - packages/client/src/scripts/scroll.ts | 85 - packages/client/src/scripts/search.ts | 63 - packages/client/src/scripts/select-file.ts | 103 -- .../client/src/scripts/show-suspended-dialog.ts | 10 - packages/client/src/scripts/shuffle.ts | 19 - packages/client/src/scripts/sound.ts | 66 - packages/client/src/scripts/sticky-sidebar.ts | 50 - packages/client/src/scripts/theme-editor.ts | 81 - packages/client/src/scripts/theme.ts | 148 -- packages/client/src/scripts/time.ts | 39 - packages/client/src/scripts/timezones.ts | 49 - packages/client/src/scripts/touch.ts | 23 - packages/client/src/scripts/unison-reload.ts | 15 - packages/client/src/scripts/upload.ts | 137 -- .../client/src/scripts/upload/compress-config.ts | 23 - packages/client/src/scripts/url.ts | 13 - packages/client/src/scripts/use-chart-tooltip.ts | 54 - packages/client/src/scripts/use-interval.ts | 24 - packages/client/src/scripts/use-leave-guard.ts | 47 - packages/client/src/scripts/use-note-capture.ts | 110 -- packages/client/src/scripts/use-tooltip.ts | 86 - packages/client/src/store.ts | 383 ----- packages/client/src/stream.ts | 8 - packages/client/src/style.scss | 584 ------- packages/client/src/theme-store.ts | 34 - packages/client/src/themes/_dark.json5 | 99 -- packages/client/src/themes/_light.json5 | 99 -- packages/client/src/themes/d-astro.json5 | 78 - packages/client/src/themes/d-botanical.json5 | 26 - packages/client/src/themes/d-cherry.json5 | 20 - packages/client/src/themes/d-dark.json5 | 26 - packages/client/src/themes/d-future.json5 | 27 - packages/client/src/themes/d-green-lime.json5 | 24 - packages/client/src/themes/d-green-orange.json5 | 24 - packages/client/src/themes/d-ice.json5 | 13 - packages/client/src/themes/d-persimmon.json5 | 25 - packages/client/src/themes/d-u0.json5 | 88 - packages/client/src/themes/l-apricot.json5 | 22 - packages/client/src/themes/l-cherry.json5 | 21 - packages/client/src/themes/l-coffee.json5 | 21 - packages/client/src/themes/l-light.json5 | 20 - packages/client/src/themes/l-rainy.json5 | 21 - packages/client/src/themes/l-sushi.json5 | 18 - packages/client/src/themes/l-u0.json5 | 87 - packages/client/src/themes/l-vivid.json5 | 82 - packages/client/src/types/menu.ts | 21 - packages/client/src/ui/_common_/common.vue | 139 -- .../client/src/ui/_common_/navbar-for-mobile.vue | 314 ---- packages/client/src/ui/_common_/navbar.vue | 521 ------ .../src/ui/_common_/statusbar-federation.vue | 108 -- packages/client/src/ui/_common_/statusbar-rss.vue | 93 - .../client/src/ui/_common_/statusbar-user-list.vue | 113 -- packages/client/src/ui/_common_/statusbars.vue | 92 - .../client/src/ui/_common_/stream-indicator.vue | 61 - packages/client/src/ui/_common_/sw-inject.ts | 35 - packages/client/src/ui/_common_/upload.vue | 129 -- packages/client/src/ui/classic.header.vue | 217 --- packages/client/src/ui/classic.sidebar.vue | 268 --- packages/client/src/ui/classic.vue | 320 ---- packages/client/src/ui/classic.widgets.vue | 84 - packages/client/src/ui/deck.vue | 435 ----- packages/client/src/ui/deck/antenna-column.vue | 70 - packages/client/src/ui/deck/column-core.vue | 34 - packages/client/src/ui/deck/column.vue | 398 ----- packages/client/src/ui/deck/deck-store.ts | 296 ---- packages/client/src/ui/deck/direct-column.vue | 31 - packages/client/src/ui/deck/list-column.vue | 58 - packages/client/src/ui/deck/main-column.vue | 68 - packages/client/src/ui/deck/mentions-column.vue | 28 - .../client/src/ui/deck/notifications-column.vue | 44 - packages/client/src/ui/deck/tl-column.vue | 119 -- packages/client/src/ui/deck/widgets-column.vue | 69 - packages/client/src/ui/universal.vue | 390 ----- packages/client/src/ui/universal.widgets.vue | 71 - packages/client/src/ui/visitor.vue | 19 - packages/client/src/ui/visitor/a.vue | 259 --- packages/client/src/ui/visitor/b.vue | 248 --- packages/client/src/ui/visitor/header.vue | 228 --- packages/client/src/ui/visitor/kanban.vue | 257 --- packages/client/src/ui/zen.vue | 34 - packages/client/src/widgets/activity.calendar.vue | 81 - packages/client/src/widgets/activity.chart.vue | 92 - packages/client/src/widgets/activity.vue | 90 - packages/client/src/widgets/aichan.vue | 74 - packages/client/src/widgets/aiscript.vue | 175 -- packages/client/src/widgets/button.vue | 103 -- packages/client/src/widgets/calendar.vue | 213 --- packages/client/src/widgets/clock.vue | 203 --- packages/client/src/widgets/digital-clock.vue | 92 - packages/client/src/widgets/federation.vue | 147 -- packages/client/src/widgets/index.ts | 53 - packages/client/src/widgets/instance-cloud.vue | 81 - packages/client/src/widgets/job-queue.vue | 197 --- packages/client/src/widgets/memo.vue | 111 -- packages/client/src/widgets/notifications.vue | 70 - packages/client/src/widgets/online-users.vue | 78 - packages/client/src/widgets/photos.vue | 123 -- packages/client/src/widgets/post-form.vue | 35 - packages/client/src/widgets/rss-ticker.vue | 152 -- packages/client/src/widgets/rss.vue | 96 -- .../client/src/widgets/server-metric/cpu-mem.vue | 167 -- packages/client/src/widgets/server-metric/cpu.vue | 65 - packages/client/src/widgets/server-metric/disk.vue | 57 - .../client/src/widgets/server-metric/index.vue | 87 - packages/client/src/widgets/server-metric/mem.vue | 73 - packages/client/src/widgets/server-metric/net.vue | 140 -- packages/client/src/widgets/server-metric/pie.vue | 52 - packages/client/src/widgets/slideshow.vue | 159 -- packages/client/src/widgets/timeline.vue | 129 -- packages/client/src/widgets/trends.vue | 120 -- packages/client/src/widgets/unix-clock.vue | 116 -- packages/client/src/widgets/user-list.vue | 136 -- packages/client/src/widgets/widget.ts | 73 - packages/client/tsconfig.json | 47 - packages/client/vite.config.ts | 70 - packages/client/vite.json5.ts | 38 - packages/frontend/.eslintrc.js | 89 + packages/frontend/.vscode/settings.json | 11 + packages/frontend/@types/global.d.ts | 10 + packages/frontend/@types/theme.d.ts | 7 + packages/frontend/@types/vue.d.ts | 16 + packages/frontend/assets/about-icon.png | Bin 0 -> 20528 bytes packages/frontend/assets/dummy.png | Bin 0 -> 6285 bytes packages/frontend/assets/fedi.jpg | Bin 0 -> 77752 bytes packages/frontend/assets/label-red.svg | 6 + packages/frontend/assets/label.svg | 6 + packages/frontend/assets/misskey.svg | 7 + packages/frontend/assets/remove.png | Bin 0 -> 424 bytes packages/frontend/assets/sounds/aisha/1.mp3 | Bin 0 -> 34480 bytes packages/frontend/assets/sounds/aisha/2.mp3 | Bin 0 -> 24031 bytes packages/frontend/assets/sounds/aisha/3.mp3 | Bin 0 -> 29256 bytes .../assets/sounds/noizenecio/kick_gaba1.mp3 | Bin 0 -> 18866 bytes .../assets/sounds/noizenecio/kick_gaba2.mp3 | Bin 0 -> 27144 bytes .../assets/sounds/noizenecio/kick_gaba3.mp3 | Bin 0 -> 19853 bytes .../assets/sounds/noizenecio/kick_gaba4.mp3 | Bin 0 -> 19853 bytes .../assets/sounds/noizenecio/kick_gaba5.mp3 | Bin 0 -> 19853 bytes .../assets/sounds/noizenecio/kick_gaba6.mp3 | Bin 0 -> 19853 bytes .../assets/sounds/noizenecio/kick_gaba7.mp3 | Bin 0 -> 19853 bytes packages/frontend/assets/sounds/syuilo/down.mp3 | Bin 0 -> 18240 bytes packages/frontend/assets/sounds/syuilo/kick.mp3 | Bin 0 -> 15672 bytes .../assets/sounds/syuilo/pirori-square-wet.mp3 | Bin 0 -> 139200 bytes .../frontend/assets/sounds/syuilo/pirori-wet.mp3 | Bin 0 -> 139200 bytes packages/frontend/assets/sounds/syuilo/pirori.mp3 | Bin 0 -> 19200 bytes packages/frontend/assets/sounds/syuilo/poi1.mp3 | Bin 0 -> 18240 bytes packages/frontend/assets/sounds/syuilo/poi2.mp3 | Bin 0 -> 18240 bytes packages/frontend/assets/sounds/syuilo/pope1.mp3 | Bin 0 -> 18240 bytes packages/frontend/assets/sounds/syuilo/pope2.mp3 | Bin 0 -> 18240 bytes packages/frontend/assets/sounds/syuilo/popo.mp3 | Bin 0 -> 18240 bytes .../frontend/assets/sounds/syuilo/queue-jammed.mp3 | Bin 0 -> 351466 bytes .../frontend/assets/sounds/syuilo/reverved.mp3 | Bin 0 -> 276480 bytes packages/frontend/assets/sounds/syuilo/ryukyu.mp3 | Bin 0 -> 139200 bytes packages/frontend/assets/sounds/syuilo/snare.mp3 | Bin 0 -> 26121 bytes .../frontend/assets/sounds/syuilo/square-pico.mp3 | Bin 0 -> 19200 bytes packages/frontend/assets/sounds/syuilo/triple.mp3 | Bin 0 -> 18240 bytes packages/frontend/assets/sounds/syuilo/up.mp3 | Bin 0 -> 18240 bytes packages/frontend/assets/sounds/syuilo/waon.mp3 | Bin 0 -> 18240 bytes packages/frontend/assets/tagcanvas.min.js | 21 + packages/frontend/assets/unread.svg | 7 + packages/frontend/package.json | 94 ++ packages/frontend/src/account.ts | 238 +++ packages/frontend/src/components/MkAbuseReport.vue | 109 ++ .../src/components/MkAbuseReportWindow.vue | 65 + .../src/components/MkActiveUsersHeatmap.vue | 236 +++ packages/frontend/src/components/MkAnalogClock.vue | 225 +++ .../frontend/src/components/MkAutocomplete.vue | 476 ++++++ packages/frontend/src/components/MkAvatars.vue | 24 + packages/frontend/src/components/MkButton.vue | 227 +++ packages/frontend/src/components/MkCaptcha.vue | 118 ++ .../src/components/MkChannelFollowButton.vue | 129 ++ .../frontend/src/components/MkChannelPreview.vue | 154 ++ packages/frontend/src/components/MkChart.vue | 859 ++++++++++ .../frontend/src/components/MkChartTooltip.vue | 53 + packages/frontend/src/components/MkCode.core.vue | 20 + packages/frontend/src/components/MkCode.vue | 15 + packages/frontend/src/components/MkContainer.vue | 275 +++ packages/frontend/src/components/MkContextMenu.vue | 85 + .../frontend/src/components/MkCropperDialog.vue | 174 ++ packages/frontend/src/components/MkCwButton.vue | 62 + .../src/components/MkDateSeparatedList.vue | 189 +++ packages/frontend/src/components/MkDialog.vue | 208 +++ .../frontend/src/components/MkDigitalClock.vue | 77 + packages/frontend/src/components/MkDrive.file.vue | 334 ++++ .../frontend/src/components/MkDrive.folder.vue | 330 ++++ .../frontend/src/components/MkDrive.navFolder.vue | 147 ++ packages/frontend/src/components/MkDrive.vue | 801 +++++++++ .../src/components/MkDriveFileThumbnail.vue | 80 + .../src/components/MkDriveSelectDialog.vue | 58 + packages/frontend/src/components/MkDriveWindow.vue | 30 + .../src/components/MkEmojiPicker.section.vue | 36 + packages/frontend/src/components/MkEmojiPicker.vue | 569 +++++++ .../src/components/MkEmojiPickerDialog.vue | 73 + .../src/components/MkEmojiPickerWindow.vue | 180 ++ .../frontend/src/components/MkFeaturedPhotos.vue | 22 + .../src/components/MkFileCaptionEditWindow.vue | 175 ++ .../frontend/src/components/MkFileListForAdmin.vue | 117 ++ packages/frontend/src/components/MkFolder.vue | 159 ++ .../frontend/src/components/MkFollowButton.vue | 187 ++ .../frontend/src/components/MkForgotPassword.vue | 80 + packages/frontend/src/components/MkFormDialog.vue | 127 ++ packages/frontend/src/components/MkFormula.vue | 24 + packages/frontend/src/components/MkFormulaCore.vue | 34 + .../src/components/MkGalleryPostPreview.vue | 115 ++ packages/frontend/src/components/MkGoogle.vue | 51 + packages/frontend/src/components/MkImageViewer.vue | 77 + .../frontend/src/components/MkImgWithBlurhash.vue | 76 + packages/frontend/src/components/MkInfo.vue | 34 + .../frontend/src/components/MkInstanceCardMini.vue | 105 ++ .../frontend/src/components/MkInstanceStats.vue | 255 +++ .../frontend/src/components/MkInstanceTicker.vue | 80 + packages/frontend/src/components/MkKeyValue.vue | 58 + packages/frontend/src/components/MkLaunchPad.vue | 138 ++ packages/frontend/src/components/MkLink.vue | 47 + packages/frontend/src/components/MkMarquee.vue | 106 ++ packages/frontend/src/components/MkMediaBanner.vue | 102 ++ packages/frontend/src/components/MkMediaImage.vue | 130 ++ packages/frontend/src/components/MkMediaList.vue | 189 +++ packages/frontend/src/components/MkMediaVideo.vue | 88 + packages/frontend/src/components/MkMention.vue | 66 + packages/frontend/src/components/MkMenu.child.vue | 65 + packages/frontend/src/components/MkMenu.vue | 367 ++++ packages/frontend/src/components/MkMiniChart.vue | 73 + packages/frontend/src/components/MkModal.vue | 406 +++++ .../frontend/src/components/MkModalPageWindow.vue | 181 ++ packages/frontend/src/components/MkModalWindow.vue | 146 ++ packages/frontend/src/components/MkNote.vue | 658 ++++++++ .../frontend/src/components/MkNoteDetailed.vue | 677 ++++++++ packages/frontend/src/components/MkNoteHeader.vue | 75 + packages/frontend/src/components/MkNotePreview.vue | 112 ++ packages/frontend/src/components/MkNoteSimple.vue | 119 ++ packages/frontend/src/components/MkNoteSub.vue | 140 ++ packages/frontend/src/components/MkNotes.vue | 58 + .../frontend/src/components/MkNotification.vue | 323 ++++ .../src/components/MkNotificationSettingWindow.vue | 87 + .../src/components/MkNotificationToast.vue | 68 + .../frontend/src/components/MkNotifications.vue | 104 ++ packages/frontend/src/components/MkNumberDiff.vue | 47 + .../frontend/src/components/MkObjectView.value.vue | 160 ++ packages/frontend/src/components/MkObjectView.vue | 20 + packages/frontend/src/components/MkPagePreview.vue | 162 ++ packages/frontend/src/components/MkPageWindow.vue | 140 ++ packages/frontend/src/components/MkPagination.vue | 317 ++++ packages/frontend/src/components/MkPoll.vue | 152 ++ packages/frontend/src/components/MkPollEditor.vue | 219 +++ packages/frontend/src/components/MkPopupMenu.vue | 36 + packages/frontend/src/components/MkPostForm.vue | 1050 ++++++++++++ .../frontend/src/components/MkPostFormAttaches.vue | 168 ++ .../frontend/src/components/MkPostFormDialog.vue | 19 + .../components/MkPushNotificationAllowButton.vue | 167 ++ .../frontend/src/components/MkReactionIcon.vue | 13 + .../frontend/src/components/MkReactionTooltip.vue | 43 + .../src/components/MkReactionsViewer.details.vue | 96 ++ .../src/components/MkReactionsViewer.reaction.vue | 135 ++ .../frontend/src/components/MkReactionsViewer.vue | 36 + .../frontend/src/components/MkRemoteCaution.vue | 25 + .../frontend/src/components/MkRenoteButton.vue | 99 ++ packages/frontend/src/components/MkRipple.vue | 116 ++ packages/frontend/src/components/MkSample.vue | 116 ++ packages/frontend/src/components/MkSignin.vue | 259 +++ .../frontend/src/components/MkSigninDialog.vue | 46 + packages/frontend/src/components/MkSignup.vue | 246 +++ .../frontend/src/components/MkSignupDialog.vue | 46 + packages/frontend/src/components/MkSparkle.vue | 130 ++ .../frontend/src/components/MkSubNoteContent.vue | 90 + packages/frontend/src/components/MkSuperMenu.vue | 161 ++ packages/frontend/src/components/MkTab.vue | 73 + packages/frontend/src/components/MkTagCloud.vue | 90 + packages/frontend/src/components/MkTimeline.vue | 143 ++ packages/frontend/src/components/MkToast.vue | 66 + .../src/components/MkTokenGenerateWindow.vue | 90 + packages/frontend/src/components/MkTooltip.vue | 101 ++ packages/frontend/src/components/MkUpdated.vue | 51 + packages/frontend/src/components/MkUrlPreview.vue | 383 +++++ .../frontend/src/components/MkUrlPreviewPopup.vue | 45 + .../frontend/src/components/MkUserCardMini.vue | 99 ++ packages/frontend/src/components/MkUserInfo.vue | 137 ++ packages/frontend/src/components/MkUserList.vue | 39 + .../src/components/MkUserOnlineIndicator.vue | 45 + packages/frontend/src/components/MkUserPreview.vue | 184 ++ .../frontend/src/components/MkUserSelectDialog.vue | 190 +++ .../frontend/src/components/MkUsersTooltip.vue | 51 + packages/frontend/src/components/MkVisibility.vue | 48 + .../frontend/src/components/MkVisibilityPicker.vue | 159 ++ .../frontend/src/components/MkWaitingDialog.vue | 73 + packages/frontend/src/components/MkWidgets.vue | 165 ++ packages/frontend/src/components/MkWindow.vue | 571 +++++++ .../frontend/src/components/MkYoutubePlayer.vue | 72 + packages/frontend/src/components/form/checkbox.vue | 144 ++ packages/frontend/src/components/form/folder.vue | 107 ++ packages/frontend/src/components/form/input.vue | 263 +++ packages/frontend/src/components/form/link.vue | 95 ++ packages/frontend/src/components/form/radio.vue | 132 ++ packages/frontend/src/components/form/radios.vue | 83 + packages/frontend/src/components/form/range.vue | 259 +++ packages/frontend/src/components/form/section.vue | 43 + packages/frontend/src/components/form/select.vue | 279 +++ packages/frontend/src/components/form/slot.vue | 41 + packages/frontend/src/components/form/split.vue | 27 + packages/frontend/src/components/form/suspense.vue | 98 ++ packages/frontend/src/components/form/switch.vue | 144 ++ packages/frontend/src/components/form/textarea.vue | 260 +++ packages/frontend/src/components/global/MkA.vue | 102 ++ packages/frontend/src/components/global/MkAcct.vue | 27 + packages/frontend/src/components/global/MkAd.vue | 186 ++ .../frontend/src/components/global/MkAvatar.vue | 143 ++ .../frontend/src/components/global/MkEllipsis.vue | 34 + .../frontend/src/components/global/MkEmoji.vue | 81 + .../frontend/src/components/global/MkError.vue | 36 + .../frontend/src/components/global/MkLoading.vue | 101 ++ .../global/MkMisskeyFlavoredMarkdown.vue | 191 +++ .../src/components/global/MkPageHeader.vue | 368 ++++ .../frontend/src/components/global/MkSpacer.vue | 96 ++ .../src/components/global/MkStickyContainer.vue | 66 + packages/frontend/src/components/global/MkTime.vue | 56 + packages/frontend/src/components/global/MkUrl.vue | 89 + .../frontend/src/components/global/MkUserName.vue | 15 + .../frontend/src/components/global/RouterView.vue | 61 + packages/frontend/src/components/global/i18n.ts | 42 + packages/frontend/src/components/index.ts | 61 + packages/frontend/src/components/mfm.ts | 331 ++++ .../frontend/src/components/page/page.block.vue | 44 + .../frontend/src/components/page/page.button.vue | 66 + .../frontend/src/components/page/page.canvas.vue | 49 + .../frontend/src/components/page/page.counter.vue | 52 + packages/frontend/src/components/page/page.if.vue | 31 + .../frontend/src/components/page/page.image.vue | 28 + .../frontend/src/components/page/page.note.vue | 47 + .../src/components/page/page.number-input.vue | 55 + .../frontend/src/components/page/page.post.vue | 109 ++ .../src/components/page/page.radio-button.vue | 45 + .../frontend/src/components/page/page.section.vue | 60 + .../frontend/src/components/page/page.switch.vue | 55 + .../src/components/page/page.text-input.vue | 55 + .../frontend/src/components/page/page.text.vue | 68 + .../src/components/page/page.textarea-input.vue | 47 + .../frontend/src/components/page/page.textarea.vue | 39 + packages/frontend/src/components/page/page.vue | 85 + packages/frontend/src/config.ts | 15 + packages/frontend/src/const.ts | 45 + .../frontend/src/directives/adaptive-border.ts | 24 + packages/frontend/src/directives/anim.ts | 18 + packages/frontend/src/directives/appear.ts | 22 + packages/frontend/src/directives/click-anime.ts | 31 + packages/frontend/src/directives/follow-append.ts | 35 + packages/frontend/src/directives/get-size.ts | 54 + packages/frontend/src/directives/hotkey.ts | 24 + packages/frontend/src/directives/index.ts | 28 + packages/frontend/src/directives/panel.ts | 24 + packages/frontend/src/directives/ripple.ts | 18 + packages/frontend/src/directives/size.ts | 123 ++ packages/frontend/src/directives/tooltip.ts | 93 + packages/frontend/src/directives/user-preview.ts | 118 ++ packages/frontend/src/emojilist.json | 1785 ++++++++++++++++++++ packages/frontend/src/events.ts | 4 + packages/frontend/src/filters/bytes.ts | 9 + packages/frontend/src/filters/note.ts | 3 + packages/frontend/src/filters/number.ts | 1 + packages/frontend/src/filters/user.ts | 15 + packages/frontend/src/i18n.ts | 5 + packages/frontend/src/init.ts | 433 +++++ packages/frontend/src/instance.ts | 45 + packages/frontend/src/navbar.ts | 135 ++ packages/frontend/src/nirax.ts | 275 +++ packages/frontend/src/os.ts | 588 +++++++ packages/frontend/src/pages/_empty_.vue | 7 + packages/frontend/src/pages/_error_.vue | 89 + packages/frontend/src/pages/_loading_.vue | 6 + packages/frontend/src/pages/about-misskey.vue | 264 +++ packages/frontend/src/pages/about.emojis.vue | 134 ++ packages/frontend/src/pages/about.federation.vue | 106 ++ packages/frontend/src/pages/about.vue | 166 ++ packages/frontend/src/pages/admin-file.vue | 160 ++ packages/frontend/src/pages/admin/_header_.vue | 292 ++++ packages/frontend/src/pages/admin/abuses.vue | 97 ++ packages/frontend/src/pages/admin/ads.vue | 132 ++ .../frontend/src/pages/admin/announcements.vue | 112 ++ .../frontend/src/pages/admin/bot-protection.vue | 109 ++ packages/frontend/src/pages/admin/database.vue | 35 + .../frontend/src/pages/admin/email-settings.vue | 126 ++ .../frontend/src/pages/admin/emoji-edit-dialog.vue | 106 ++ packages/frontend/src/pages/admin/emojis.vue | 398 +++++ packages/frontend/src/pages/admin/files.vue | 120 ++ packages/frontend/src/pages/admin/index.vue | 316 ++++ .../frontend/src/pages/admin/instance-block.vue | 51 + .../src/pages/admin/integrations.discord.vue | 60 + .../src/pages/admin/integrations.github.vue | 60 + .../src/pages/admin/integrations.twitter.vue | 60 + packages/frontend/src/pages/admin/integrations.vue | 57 + packages/frontend/src/pages/admin/metrics.vue | 472 ++++++ .../frontend/src/pages/admin/object-storage.vue | 148 ++ .../frontend/src/pages/admin/other-settings.vue | 44 + .../src/pages/admin/overview.active-users.vue | 217 +++ .../src/pages/admin/overview.ap-requests.vue | 346 ++++ .../src/pages/admin/overview.federation.vue | 185 ++ .../frontend/src/pages/admin/overview.heatmap.vue | 15 + .../src/pages/admin/overview.instances.vue | 50 + .../src/pages/admin/overview.moderators.vue | 55 + packages/frontend/src/pages/admin/overview.pie.vue | 110 ++ .../src/pages/admin/overview.queue.chart.vue | 186 ++ .../frontend/src/pages/admin/overview.queue.vue | 127 ++ .../src/pages/admin/overview.retention.vue | 49 + .../frontend/src/pages/admin/overview.stats.vue | 155 ++ .../frontend/src/pages/admin/overview.users.vue | 57 + packages/frontend/src/pages/admin/overview.vue | 190 +++ .../frontend/src/pages/admin/proxy-account.vue | 62 + .../frontend/src/pages/admin/queue.chart.chart.vue | 186 ++ packages/frontend/src/pages/admin/queue.chart.vue | 149 ++ packages/frontend/src/pages/admin/queue.vue | 56 + packages/frontend/src/pages/admin/relays.vue | 103 ++ packages/frontend/src/pages/admin/security.vue | 179 ++ packages/frontend/src/pages/admin/settings.vue | 262 +++ packages/frontend/src/pages/admin/users.vue | 170 ++ packages/frontend/src/pages/announcements.vue | 69 + packages/frontend/src/pages/antenna-timeline.vue | 128 ++ packages/frontend/src/pages/api-console.vue | 89 + packages/frontend/src/pages/auth.form.vue | 60 + packages/frontend/src/pages/auth.vue | 91 + packages/frontend/src/pages/channel-editor.vue | 122 ++ packages/frontend/src/pages/channel.vue | 184 ++ packages/frontend/src/pages/channels.vue | 79 + packages/frontend/src/pages/clip.vue | 129 ++ packages/frontend/src/pages/drive.vue | 25 + packages/frontend/src/pages/emojis.emoji.vue | 72 + packages/frontend/src/pages/explore.featured.vue | 30 + packages/frontend/src/pages/explore.users.vue | 148 ++ packages/frontend/src/pages/explore.vue | 87 + packages/frontend/src/pages/favorites.vue | 49 + packages/frontend/src/pages/follow-requests.vue | 153 ++ packages/frontend/src/pages/follow.vue | 62 + packages/frontend/src/pages/gallery/edit.vue | 149 ++ packages/frontend/src/pages/gallery/index.vue | 139 ++ packages/frontend/src/pages/gallery/post.vue | 265 +++ packages/frontend/src/pages/instance-info.vue | 258 +++ packages/frontend/src/pages/messaging/index.vue | 327 ++++ .../src/pages/messaging/messaging-room.form.vue | 364 ++++ .../src/pages/messaging/messaging-room.message.vue | 367 ++++ .../src/pages/messaging/messaging-room.vue | 411 +++++ packages/frontend/src/pages/mfm-cheat-sheet.vue | 387 +++++ packages/frontend/src/pages/miauth.vue | 90 + packages/frontend/src/pages/my-antennas/create.vue | 46 + packages/frontend/src/pages/my-antennas/edit.vue | 43 + packages/frontend/src/pages/my-antennas/editor.vue | 155 ++ packages/frontend/src/pages/my-antennas/index.vue | 64 + packages/frontend/src/pages/my-clips/index.vue | 100 ++ packages/frontend/src/pages/my-lists/index.vue | 82 + packages/frontend/src/pages/my-lists/list.vue | 162 ++ packages/frontend/src/pages/not-found.vue | 22 + packages/frontend/src/pages/note.vue | 206 +++ packages/frontend/src/pages/notifications.vue | 95 ++ .../pages/page-editor/els/page-editor.el.image.vue | 63 + .../pages/page-editor/els/page-editor.el.note.vue | 57 + .../page-editor/els/page-editor.el.section.vue | 97 ++ .../pages/page-editor/els/page-editor.el.text.vue | 54 + .../src/pages/page-editor/page-editor.blocks.vue | 65 + .../pages/page-editor/page-editor.container.vue | 155 ++ .../frontend/src/pages/page-editor/page-editor.vue | 394 +++++ packages/frontend/src/pages/page.vue | 277 +++ packages/frontend/src/pages/pages.vue | 99 ++ packages/frontend/src/pages/preview.vue | 27 + packages/frontend/src/pages/registry.keys.vue | 96 ++ packages/frontend/src/pages/registry.value.vue | 123 ++ packages/frontend/src/pages/registry.vue | 74 + packages/frontend/src/pages/reset-password.vue | 59 + packages/frontend/src/pages/scratchpad.vue | 137 ++ packages/frontend/src/pages/search.vue | 38 + packages/frontend/src/pages/settings/2fa.vue | 216 +++ .../frontend/src/pages/settings/account-info.vue | 158 ++ packages/frontend/src/pages/settings/accounts.vue | 143 ++ packages/frontend/src/pages/settings/api.vue | 46 + packages/frontend/src/pages/settings/apps.vue | 96 ++ .../frontend/src/pages/settings/custom-css.vue | 46 + packages/frontend/src/pages/settings/deck.vue | 39 + .../frontend/src/pages/settings/delete-account.vue | 52 + packages/frontend/src/pages/settings/drive.vue | 145 ++ packages/frontend/src/pages/settings/email.vue | 111 ++ packages/frontend/src/pages/settings/general.vue | 196 +++ .../frontend/src/pages/settings/import-export.vue | 165 ++ packages/frontend/src/pages/settings/index.vue | 291 ++++ .../frontend/src/pages/settings/instance-mute.vue | 53 + .../frontend/src/pages/settings/integration.vue | 99 ++ .../frontend/src/pages/settings/mute-block.vue | 61 + packages/frontend/src/pages/settings/navbar.vue | 87 + .../frontend/src/pages/settings/notifications.vue | 90 + packages/frontend/src/pages/settings/other.vue | 47 + .../frontend/src/pages/settings/plugin.install.vue | 124 ++ packages/frontend/src/pages/settings/plugin.vue | 98 ++ .../src/pages/settings/preferences-backups.vue | 444 +++++ packages/frontend/src/pages/settings/privacy.vue | 100 ++ packages/frontend/src/pages/settings/profile.vue | 220 +++ packages/frontend/src/pages/settings/reaction.vue | 154 ++ packages/frontend/src/pages/settings/security.vue | 160 ++ .../frontend/src/pages/settings/sounds.sound.vue | 45 + packages/frontend/src/pages/settings/sounds.vue | 82 + .../src/pages/settings/statusbar.statusbar.vue | 140 ++ packages/frontend/src/pages/settings/statusbar.vue | 54 + .../frontend/src/pages/settings/theme.install.vue | 80 + .../frontend/src/pages/settings/theme.manage.vue | 78 + packages/frontend/src/pages/settings/theme.vue | 409 +++++ .../frontend/src/pages/settings/webhook.edit.vue | 95 ++ .../frontend/src/pages/settings/webhook.new.vue | 82 + packages/frontend/src/pages/settings/webhook.vue | 53 + packages/frontend/src/pages/settings/word-mute.vue | 128 ++ packages/frontend/src/pages/share.vue | 169 ++ packages/frontend/src/pages/signup-complete.vue | 41 + packages/frontend/src/pages/tag.vue | 35 + packages/frontend/src/pages/theme-editor.vue | 283 ++++ packages/frontend/src/pages/timeline.tutorial.vue | 142 ++ packages/frontend/src/pages/timeline.vue | 183 ++ packages/frontend/src/pages/user-info.vue | 485 ++++++ packages/frontend/src/pages/user-list-timeline.vue | 121 ++ packages/frontend/src/pages/user/clips.vue | 47 + packages/frontend/src/pages/user/follow-list.vue | 47 + packages/frontend/src/pages/user/followers.vue | 61 + packages/frontend/src/pages/user/following.vue | 61 + packages/frontend/src/pages/user/gallery.vue | 38 + packages/frontend/src/pages/user/home.vue | 530 ++++++ .../frontend/src/pages/user/index.activity.vue | 52 + packages/frontend/src/pages/user/index.photos.vue | 102 ++ .../frontend/src/pages/user/index.timeline.vue | 45 + packages/frontend/src/pages/user/index.vue | 113 ++ packages/frontend/src/pages/user/pages.vue | 30 + packages/frontend/src/pages/user/reactions.vue | 61 + packages/frontend/src/pages/welcome.entrance.a.vue | 309 ++++ packages/frontend/src/pages/welcome.entrance.b.vue | 237 +++ packages/frontend/src/pages/welcome.entrance.c.vue | 306 ++++ packages/frontend/src/pages/welcome.setup.vue | 89 + packages/frontend/src/pages/welcome.timeline.vue | 99 ++ packages/frontend/src/pages/welcome.vue | 30 + packages/frontend/src/pizzax.ts | 169 ++ packages/frontend/src/plugin.ts | 123 ++ packages/frontend/src/router.ts | 501 ++++++ packages/frontend/src/scripts/2fa.ts | 33 + packages/frontend/src/scripts/aiscript/api.ts | 43 + packages/frontend/src/scripts/array.ts | 149 ++ packages/frontend/src/scripts/autocomplete.ts | 276 +++ packages/frontend/src/scripts/chart-vline.ts | 21 + packages/frontend/src/scripts/check-word-mute.ts | 37 + packages/frontend/src/scripts/clone.ts | 18 + packages/frontend/src/scripts/collect-page-vars.ts | 68 + packages/frontend/src/scripts/contains.ts | 9 + packages/frontend/src/scripts/copy-to-clipboard.ts | 33 + packages/frontend/src/scripts/device-kind.ts | 10 + packages/frontend/src/scripts/emoji-base.ts | 20 + packages/frontend/src/scripts/emojilist.ts | 17 + .../src/scripts/extract-avg-color-from-blurhash.ts | 9 + packages/frontend/src/scripts/extract-mentions.ts | 11 + .../frontend/src/scripts/extract-url-from-mfm.ts | 19 + packages/frontend/src/scripts/focus.ts | 27 + packages/frontend/src/scripts/form.ts | 59 + .../frontend/src/scripts/format-time-string.ts | 50 + packages/frontend/src/scripts/gen-search-query.ts | 30 + .../frontend/src/scripts/get-account-from-id.ts | 7 + packages/frontend/src/scripts/get-note-menu.ts | 341 ++++ packages/frontend/src/scripts/get-note-summary.ts | 55 + .../frontend/src/scripts/get-static-image-url.ts | 19 + packages/frontend/src/scripts/get-user-menu.ts | 253 +++ packages/frontend/src/scripts/get-user-name.ts | 3 + packages/frontend/src/scripts/hotkey.ts | 90 + packages/frontend/src/scripts/hpml/block.ts | 109 ++ packages/frontend/src/scripts/hpml/evaluator.ts | 232 +++ packages/frontend/src/scripts/hpml/expr.ts | 79 + packages/frontend/src/scripts/hpml/index.ts | 103 ++ packages/frontend/src/scripts/hpml/lib.ts | 247 +++ packages/frontend/src/scripts/hpml/type-checker.ts | 191 +++ packages/frontend/src/scripts/i18n.ts | 29 + packages/frontend/src/scripts/idb-proxy.ts | 36 + packages/frontend/src/scripts/initialize-sw.ts | 13 + .../frontend/src/scripts/is-device-darkmode.ts | 3 + packages/frontend/src/scripts/keycode.ts | 33 + packages/frontend/src/scripts/langmap.ts | 666 ++++++++ packages/frontend/src/scripts/login-id.ts | 11 + packages/frontend/src/scripts/lookup-user.ts | 36 + packages/frontend/src/scripts/media-proxy.ts | 15 + packages/frontend/src/scripts/mfm-tags.ts | 1 + packages/frontend/src/scripts/page-metadata.ts | 41 + packages/frontend/src/scripts/physics.ts | 152 ++ packages/frontend/src/scripts/please-login.ts | 21 + packages/frontend/src/scripts/popout.ts | 23 + packages/frontend/src/scripts/popup-position.ts | 158 ++ packages/frontend/src/scripts/reaction-picker.ts | 41 + packages/frontend/src/scripts/safe-uri-decode.ts | 7 + packages/frontend/src/scripts/scroll.ts | 85 + packages/frontend/src/scripts/search.ts | 63 + packages/frontend/src/scripts/select-file.ts | 103 ++ .../frontend/src/scripts/show-suspended-dialog.ts | 10 + packages/frontend/src/scripts/shuffle.ts | 19 + packages/frontend/src/scripts/sound.ts | 66 + packages/frontend/src/scripts/sticky-sidebar.ts | 50 + packages/frontend/src/scripts/theme-editor.ts | 81 + packages/frontend/src/scripts/theme.ts | 148 ++ packages/frontend/src/scripts/time.ts | 39 + packages/frontend/src/scripts/timezones.ts | 49 + packages/frontend/src/scripts/touch.ts | 23 + packages/frontend/src/scripts/unison-reload.ts | 15 + packages/frontend/src/scripts/upload.ts | 137 ++ .../frontend/src/scripts/upload/compress-config.ts | 23 + packages/frontend/src/scripts/url.ts | 13 + packages/frontend/src/scripts/use-chart-tooltip.ts | 54 + packages/frontend/src/scripts/use-interval.ts | 24 + packages/frontend/src/scripts/use-leave-guard.ts | 47 + packages/frontend/src/scripts/use-note-capture.ts | 110 ++ packages/frontend/src/scripts/use-tooltip.ts | 86 + packages/frontend/src/store.ts | 383 +++++ packages/frontend/src/stream.ts | 8 + packages/frontend/src/style.scss | 584 +++++++ packages/frontend/src/theme-store.ts | 34 + packages/frontend/src/themes/_dark.json5 | 99 ++ packages/frontend/src/themes/_light.json5 | 99 ++ packages/frontend/src/themes/d-astro.json5 | 78 + packages/frontend/src/themes/d-botanical.json5 | 26 + packages/frontend/src/themes/d-cherry.json5 | 20 + packages/frontend/src/themes/d-dark.json5 | 26 + packages/frontend/src/themes/d-future.json5 | 27 + packages/frontend/src/themes/d-green-lime.json5 | 24 + packages/frontend/src/themes/d-green-orange.json5 | 24 + packages/frontend/src/themes/d-ice.json5 | 13 + packages/frontend/src/themes/d-persimmon.json5 | 25 + packages/frontend/src/themes/d-u0.json5 | 88 + packages/frontend/src/themes/l-apricot.json5 | 22 + packages/frontend/src/themes/l-cherry.json5 | 21 + packages/frontend/src/themes/l-coffee.json5 | 21 + packages/frontend/src/themes/l-light.json5 | 20 + packages/frontend/src/themes/l-rainy.json5 | 21 + packages/frontend/src/themes/l-sushi.json5 | 18 + packages/frontend/src/themes/l-u0.json5 | 87 + packages/frontend/src/themes/l-vivid.json5 | 82 + packages/frontend/src/types/menu.ts | 21 + packages/frontend/src/ui/_common_/common.vue | 139 ++ .../frontend/src/ui/_common_/navbar-for-mobile.vue | 314 ++++ packages/frontend/src/ui/_common_/navbar.vue | 521 ++++++ .../src/ui/_common_/statusbar-federation.vue | 108 ++ .../frontend/src/ui/_common_/statusbar-rss.vue | 93 + .../src/ui/_common_/statusbar-user-list.vue | 113 ++ packages/frontend/src/ui/_common_/statusbars.vue | 92 + .../frontend/src/ui/_common_/stream-indicator.vue | 61 + packages/frontend/src/ui/_common_/sw-inject.ts | 35 + packages/frontend/src/ui/_common_/upload.vue | 129 ++ packages/frontend/src/ui/classic.header.vue | 217 +++ packages/frontend/src/ui/classic.sidebar.vue | 268 +++ packages/frontend/src/ui/classic.vue | 320 ++++ packages/frontend/src/ui/classic.widgets.vue | 84 + packages/frontend/src/ui/deck.vue | 435 +++++ packages/frontend/src/ui/deck/antenna-column.vue | 70 + packages/frontend/src/ui/deck/column-core.vue | 34 + packages/frontend/src/ui/deck/column.vue | 398 +++++ packages/frontend/src/ui/deck/deck-store.ts | 296 ++++ packages/frontend/src/ui/deck/direct-column.vue | 31 + packages/frontend/src/ui/deck/list-column.vue | 58 + packages/frontend/src/ui/deck/main-column.vue | 68 + packages/frontend/src/ui/deck/mentions-column.vue | 28 + .../frontend/src/ui/deck/notifications-column.vue | 44 + packages/frontend/src/ui/deck/tl-column.vue | 119 ++ packages/frontend/src/ui/deck/widgets-column.vue | 69 + packages/frontend/src/ui/universal.vue | 390 +++++ packages/frontend/src/ui/universal.widgets.vue | 71 + packages/frontend/src/ui/visitor.vue | 19 + packages/frontend/src/ui/visitor/a.vue | 259 +++ packages/frontend/src/ui/visitor/b.vue | 248 +++ packages/frontend/src/ui/visitor/header.vue | 228 +++ packages/frontend/src/ui/visitor/kanban.vue | 257 +++ packages/frontend/src/ui/zen.vue | 34 + .../frontend/src/widgets/activity.calendar.vue | 81 + packages/frontend/src/widgets/activity.chart.vue | 92 + packages/frontend/src/widgets/activity.vue | 90 + packages/frontend/src/widgets/aichan.vue | 74 + packages/frontend/src/widgets/aiscript.vue | 175 ++ packages/frontend/src/widgets/button.vue | 103 ++ packages/frontend/src/widgets/calendar.vue | 213 +++ packages/frontend/src/widgets/clock.vue | 203 +++ packages/frontend/src/widgets/digital-clock.vue | 92 + packages/frontend/src/widgets/federation.vue | 147 ++ packages/frontend/src/widgets/index.ts | 53 + packages/frontend/src/widgets/instance-cloud.vue | 81 + packages/frontend/src/widgets/job-queue.vue | 197 +++ packages/frontend/src/widgets/memo.vue | 111 ++ packages/frontend/src/widgets/notifications.vue | 70 + packages/frontend/src/widgets/online-users.vue | 78 + packages/frontend/src/widgets/photos.vue | 123 ++ packages/frontend/src/widgets/post-form.vue | 35 + packages/frontend/src/widgets/rss-ticker.vue | 152 ++ packages/frontend/src/widgets/rss.vue | 96 ++ .../frontend/src/widgets/server-metric/cpu-mem.vue | 167 ++ .../frontend/src/widgets/server-metric/cpu.vue | 65 + .../frontend/src/widgets/server-metric/disk.vue | 57 + .../frontend/src/widgets/server-metric/index.vue | 87 + .../frontend/src/widgets/server-metric/mem.vue | 73 + .../frontend/src/widgets/server-metric/net.vue | 140 ++ .../frontend/src/widgets/server-metric/pie.vue | 52 + packages/frontend/src/widgets/slideshow.vue | 159 ++ packages/frontend/src/widgets/timeline.vue | 129 ++ packages/frontend/src/widgets/trends.vue | 120 ++ packages/frontend/src/widgets/unix-clock.vue | 116 ++ packages/frontend/src/widgets/user-list.vue | 136 ++ packages/frontend/src/widgets/widget.ts | 73 + packages/frontend/tsconfig.json | 47 + packages/frontend/vite.config.ts | 70 + packages/frontend/vite.json5.ts | 38 + 1161 files changed, 68859 insertions(+), 68859 deletions(-) delete mode 100644 packages/client/.eslintrc.js delete mode 100644 packages/client/.vscode/settings.json delete mode 100644 packages/client/@types/global.d.ts delete mode 100644 packages/client/@types/theme.d.ts delete mode 100644 packages/client/@types/vue.d.ts delete mode 100644 packages/client/assets/about-icon.png delete mode 100644 packages/client/assets/dummy.png delete mode 100644 packages/client/assets/fedi.jpg delete mode 100644 packages/client/assets/label-red.svg delete mode 100644 packages/client/assets/label.svg delete mode 100644 packages/client/assets/misskey.svg delete mode 100644 packages/client/assets/remove.png delete mode 100644 packages/client/assets/sounds/aisha/1.mp3 delete mode 100644 packages/client/assets/sounds/aisha/2.mp3 delete mode 100644 packages/client/assets/sounds/aisha/3.mp3 delete mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba1.mp3 delete mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba2.mp3 delete mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba3.mp3 delete mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba4.mp3 delete mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba5.mp3 delete mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba6.mp3 delete mode 100644 packages/client/assets/sounds/noizenecio/kick_gaba7.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/down.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/kick.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/pirori-square-wet.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/pirori-wet.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/pirori.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/poi1.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/poi2.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/pope1.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/pope2.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/popo.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/queue-jammed.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/reverved.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/ryukyu.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/snare.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/square-pico.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/triple.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/up.mp3 delete mode 100644 packages/client/assets/sounds/syuilo/waon.mp3 delete mode 100644 packages/client/assets/tagcanvas.min.js delete mode 100644 packages/client/assets/unread.svg delete mode 100644 packages/client/package.json delete mode 100644 packages/client/src/account.ts delete mode 100644 packages/client/src/components/MkAbuseReport.vue delete mode 100644 packages/client/src/components/MkAbuseReportWindow.vue delete mode 100644 packages/client/src/components/MkActiveUsersHeatmap.vue delete mode 100644 packages/client/src/components/MkAnalogClock.vue delete mode 100644 packages/client/src/components/MkAutocomplete.vue delete mode 100644 packages/client/src/components/MkAvatars.vue delete mode 100644 packages/client/src/components/MkButton.vue delete mode 100644 packages/client/src/components/MkCaptcha.vue delete mode 100644 packages/client/src/components/MkChannelFollowButton.vue delete mode 100644 packages/client/src/components/MkChannelPreview.vue delete mode 100644 packages/client/src/components/MkChart.vue delete mode 100644 packages/client/src/components/MkChartTooltip.vue delete mode 100644 packages/client/src/components/MkCode.core.vue delete mode 100644 packages/client/src/components/MkCode.vue delete mode 100644 packages/client/src/components/MkContainer.vue delete mode 100644 packages/client/src/components/MkContextMenu.vue delete mode 100644 packages/client/src/components/MkCropperDialog.vue delete mode 100644 packages/client/src/components/MkCwButton.vue delete mode 100644 packages/client/src/components/MkDateSeparatedList.vue delete mode 100644 packages/client/src/components/MkDialog.vue delete mode 100644 packages/client/src/components/MkDigitalClock.vue delete mode 100644 packages/client/src/components/MkDrive.file.vue delete mode 100644 packages/client/src/components/MkDrive.folder.vue delete mode 100644 packages/client/src/components/MkDrive.navFolder.vue delete mode 100644 packages/client/src/components/MkDrive.vue delete mode 100644 packages/client/src/components/MkDriveFileThumbnail.vue delete mode 100644 packages/client/src/components/MkDriveSelectDialog.vue delete mode 100644 packages/client/src/components/MkDriveWindow.vue delete mode 100644 packages/client/src/components/MkEmojiPicker.section.vue delete mode 100644 packages/client/src/components/MkEmojiPicker.vue delete mode 100644 packages/client/src/components/MkEmojiPickerDialog.vue delete mode 100644 packages/client/src/components/MkEmojiPickerWindow.vue delete mode 100644 packages/client/src/components/MkFeaturedPhotos.vue delete mode 100644 packages/client/src/components/MkFileCaptionEditWindow.vue delete mode 100644 packages/client/src/components/MkFileListForAdmin.vue delete mode 100644 packages/client/src/components/MkFolder.vue delete mode 100644 packages/client/src/components/MkFollowButton.vue delete mode 100644 packages/client/src/components/MkForgotPassword.vue delete mode 100644 packages/client/src/components/MkFormDialog.vue delete mode 100644 packages/client/src/components/MkFormula.vue delete mode 100644 packages/client/src/components/MkFormulaCore.vue delete mode 100644 packages/client/src/components/MkGalleryPostPreview.vue delete mode 100644 packages/client/src/components/MkGoogle.vue delete mode 100644 packages/client/src/components/MkImageViewer.vue delete mode 100644 packages/client/src/components/MkImgWithBlurhash.vue delete mode 100644 packages/client/src/components/MkInfo.vue delete mode 100644 packages/client/src/components/MkInstanceCardMini.vue delete mode 100644 packages/client/src/components/MkInstanceStats.vue delete mode 100644 packages/client/src/components/MkInstanceTicker.vue delete mode 100644 packages/client/src/components/MkKeyValue.vue delete mode 100644 packages/client/src/components/MkLaunchPad.vue delete mode 100644 packages/client/src/components/MkLink.vue delete mode 100644 packages/client/src/components/MkMarquee.vue delete mode 100644 packages/client/src/components/MkMediaBanner.vue delete mode 100644 packages/client/src/components/MkMediaImage.vue delete mode 100644 packages/client/src/components/MkMediaList.vue delete mode 100644 packages/client/src/components/MkMediaVideo.vue delete mode 100644 packages/client/src/components/MkMention.vue delete mode 100644 packages/client/src/components/MkMenu.child.vue delete mode 100644 packages/client/src/components/MkMenu.vue delete mode 100644 packages/client/src/components/MkMiniChart.vue delete mode 100644 packages/client/src/components/MkModal.vue delete mode 100644 packages/client/src/components/MkModalPageWindow.vue delete mode 100644 packages/client/src/components/MkModalWindow.vue delete mode 100644 packages/client/src/components/MkNote.vue delete mode 100644 packages/client/src/components/MkNoteDetailed.vue delete mode 100644 packages/client/src/components/MkNoteHeader.vue delete mode 100644 packages/client/src/components/MkNotePreview.vue delete mode 100644 packages/client/src/components/MkNoteSimple.vue delete mode 100644 packages/client/src/components/MkNoteSub.vue delete mode 100644 packages/client/src/components/MkNotes.vue delete mode 100644 packages/client/src/components/MkNotification.vue delete mode 100644 packages/client/src/components/MkNotificationSettingWindow.vue delete mode 100644 packages/client/src/components/MkNotificationToast.vue delete mode 100644 packages/client/src/components/MkNotifications.vue delete mode 100644 packages/client/src/components/MkNumberDiff.vue delete mode 100644 packages/client/src/components/MkObjectView.value.vue delete mode 100644 packages/client/src/components/MkObjectView.vue delete mode 100644 packages/client/src/components/MkPagePreview.vue delete mode 100644 packages/client/src/components/MkPageWindow.vue delete mode 100644 packages/client/src/components/MkPagination.vue delete mode 100644 packages/client/src/components/MkPoll.vue delete mode 100644 packages/client/src/components/MkPollEditor.vue delete mode 100644 packages/client/src/components/MkPopupMenu.vue delete mode 100644 packages/client/src/components/MkPostForm.vue delete mode 100644 packages/client/src/components/MkPostFormAttaches.vue delete mode 100644 packages/client/src/components/MkPostFormDialog.vue delete mode 100644 packages/client/src/components/MkPushNotificationAllowButton.vue delete mode 100644 packages/client/src/components/MkReactionIcon.vue delete mode 100644 packages/client/src/components/MkReactionTooltip.vue delete mode 100644 packages/client/src/components/MkReactionsViewer.details.vue delete mode 100644 packages/client/src/components/MkReactionsViewer.reaction.vue delete mode 100644 packages/client/src/components/MkReactionsViewer.vue delete mode 100644 packages/client/src/components/MkRemoteCaution.vue delete mode 100644 packages/client/src/components/MkRenoteButton.vue delete mode 100644 packages/client/src/components/MkRipple.vue delete mode 100644 packages/client/src/components/MkSample.vue delete mode 100644 packages/client/src/components/MkSignin.vue delete mode 100644 packages/client/src/components/MkSigninDialog.vue delete mode 100644 packages/client/src/components/MkSignup.vue delete mode 100644 packages/client/src/components/MkSignupDialog.vue delete mode 100644 packages/client/src/components/MkSparkle.vue delete mode 100644 packages/client/src/components/MkSubNoteContent.vue delete mode 100644 packages/client/src/components/MkSuperMenu.vue delete mode 100644 packages/client/src/components/MkTab.vue delete mode 100644 packages/client/src/components/MkTagCloud.vue delete mode 100644 packages/client/src/components/MkTimeline.vue delete mode 100644 packages/client/src/components/MkToast.vue delete mode 100644 packages/client/src/components/MkTokenGenerateWindow.vue delete mode 100644 packages/client/src/components/MkTooltip.vue delete mode 100644 packages/client/src/components/MkUpdated.vue delete mode 100644 packages/client/src/components/MkUrlPreview.vue delete mode 100644 packages/client/src/components/MkUrlPreviewPopup.vue delete mode 100644 packages/client/src/components/MkUserCardMini.vue delete mode 100644 packages/client/src/components/MkUserInfo.vue delete mode 100644 packages/client/src/components/MkUserList.vue delete mode 100644 packages/client/src/components/MkUserOnlineIndicator.vue delete mode 100644 packages/client/src/components/MkUserPreview.vue delete mode 100644 packages/client/src/components/MkUserSelectDialog.vue delete mode 100644 packages/client/src/components/MkUsersTooltip.vue delete mode 100644 packages/client/src/components/MkVisibility.vue delete mode 100644 packages/client/src/components/MkVisibilityPicker.vue delete mode 100644 packages/client/src/components/MkWaitingDialog.vue delete mode 100644 packages/client/src/components/MkWidgets.vue delete mode 100644 packages/client/src/components/MkWindow.vue delete mode 100644 packages/client/src/components/MkYoutubePlayer.vue delete mode 100644 packages/client/src/components/form/checkbox.vue delete mode 100644 packages/client/src/components/form/folder.vue delete mode 100644 packages/client/src/components/form/input.vue delete mode 100644 packages/client/src/components/form/link.vue delete mode 100644 packages/client/src/components/form/radio.vue delete mode 100644 packages/client/src/components/form/radios.vue delete mode 100644 packages/client/src/components/form/range.vue delete mode 100644 packages/client/src/components/form/section.vue delete mode 100644 packages/client/src/components/form/select.vue delete mode 100644 packages/client/src/components/form/slot.vue delete mode 100644 packages/client/src/components/form/split.vue delete mode 100644 packages/client/src/components/form/suspense.vue delete mode 100644 packages/client/src/components/form/switch.vue delete mode 100644 packages/client/src/components/form/textarea.vue delete mode 100644 packages/client/src/components/global/MkA.vue delete mode 100644 packages/client/src/components/global/MkAcct.vue delete mode 100644 packages/client/src/components/global/MkAd.vue delete mode 100644 packages/client/src/components/global/MkAvatar.vue delete mode 100644 packages/client/src/components/global/MkEllipsis.vue delete mode 100644 packages/client/src/components/global/MkEmoji.vue delete mode 100644 packages/client/src/components/global/MkError.vue delete mode 100644 packages/client/src/components/global/MkLoading.vue delete mode 100644 packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue delete mode 100644 packages/client/src/components/global/MkPageHeader.vue delete mode 100644 packages/client/src/components/global/MkSpacer.vue delete mode 100644 packages/client/src/components/global/MkStickyContainer.vue delete mode 100644 packages/client/src/components/global/MkTime.vue delete mode 100644 packages/client/src/components/global/MkUrl.vue delete mode 100644 packages/client/src/components/global/MkUserName.vue delete mode 100644 packages/client/src/components/global/RouterView.vue delete mode 100644 packages/client/src/components/global/i18n.ts delete mode 100644 packages/client/src/components/index.ts delete mode 100644 packages/client/src/components/mfm.ts delete mode 100644 packages/client/src/components/page/page.block.vue delete mode 100644 packages/client/src/components/page/page.button.vue delete mode 100644 packages/client/src/components/page/page.canvas.vue delete mode 100644 packages/client/src/components/page/page.counter.vue delete mode 100644 packages/client/src/components/page/page.if.vue delete mode 100644 packages/client/src/components/page/page.image.vue delete mode 100644 packages/client/src/components/page/page.note.vue delete mode 100644 packages/client/src/components/page/page.number-input.vue delete mode 100644 packages/client/src/components/page/page.post.vue delete mode 100644 packages/client/src/components/page/page.radio-button.vue delete mode 100644 packages/client/src/components/page/page.section.vue delete mode 100644 packages/client/src/components/page/page.switch.vue delete mode 100644 packages/client/src/components/page/page.text-input.vue delete mode 100644 packages/client/src/components/page/page.text.vue delete mode 100644 packages/client/src/components/page/page.textarea-input.vue delete mode 100644 packages/client/src/components/page/page.textarea.vue delete mode 100644 packages/client/src/components/page/page.vue delete mode 100644 packages/client/src/config.ts delete mode 100644 packages/client/src/const.ts delete mode 100644 packages/client/src/directives/adaptive-border.ts delete mode 100644 packages/client/src/directives/anim.ts delete mode 100644 packages/client/src/directives/appear.ts delete mode 100644 packages/client/src/directives/click-anime.ts delete mode 100644 packages/client/src/directives/follow-append.ts delete mode 100644 packages/client/src/directives/get-size.ts delete mode 100644 packages/client/src/directives/hotkey.ts delete mode 100644 packages/client/src/directives/index.ts delete mode 100644 packages/client/src/directives/panel.ts delete mode 100644 packages/client/src/directives/ripple.ts delete mode 100644 packages/client/src/directives/size.ts delete mode 100644 packages/client/src/directives/tooltip.ts delete mode 100644 packages/client/src/directives/user-preview.ts delete mode 100644 packages/client/src/emojilist.json delete mode 100644 packages/client/src/events.ts delete mode 100644 packages/client/src/filters/bytes.ts delete mode 100644 packages/client/src/filters/note.ts delete mode 100644 packages/client/src/filters/number.ts delete mode 100644 packages/client/src/filters/user.ts delete mode 100644 packages/client/src/i18n.ts delete mode 100644 packages/client/src/init.ts delete mode 100644 packages/client/src/instance.ts delete mode 100644 packages/client/src/navbar.ts delete mode 100644 packages/client/src/nirax.ts delete mode 100644 packages/client/src/os.ts delete mode 100644 packages/client/src/pages/_empty_.vue delete mode 100644 packages/client/src/pages/_error_.vue delete mode 100644 packages/client/src/pages/_loading_.vue delete mode 100644 packages/client/src/pages/about-misskey.vue delete mode 100644 packages/client/src/pages/about.emojis.vue delete mode 100644 packages/client/src/pages/about.federation.vue delete mode 100644 packages/client/src/pages/about.vue delete mode 100644 packages/client/src/pages/admin-file.vue delete mode 100644 packages/client/src/pages/admin/_header_.vue delete mode 100644 packages/client/src/pages/admin/abuses.vue delete mode 100644 packages/client/src/pages/admin/ads.vue delete mode 100644 packages/client/src/pages/admin/announcements.vue delete mode 100644 packages/client/src/pages/admin/bot-protection.vue delete mode 100644 packages/client/src/pages/admin/database.vue delete mode 100644 packages/client/src/pages/admin/email-settings.vue delete mode 100644 packages/client/src/pages/admin/emoji-edit-dialog.vue delete mode 100644 packages/client/src/pages/admin/emojis.vue delete mode 100644 packages/client/src/pages/admin/files.vue delete mode 100644 packages/client/src/pages/admin/index.vue delete mode 100644 packages/client/src/pages/admin/instance-block.vue delete mode 100644 packages/client/src/pages/admin/integrations.discord.vue delete mode 100644 packages/client/src/pages/admin/integrations.github.vue delete mode 100644 packages/client/src/pages/admin/integrations.twitter.vue delete mode 100644 packages/client/src/pages/admin/integrations.vue delete mode 100644 packages/client/src/pages/admin/metrics.vue delete mode 100644 packages/client/src/pages/admin/object-storage.vue delete mode 100644 packages/client/src/pages/admin/other-settings.vue delete mode 100644 packages/client/src/pages/admin/overview.active-users.vue delete mode 100644 packages/client/src/pages/admin/overview.ap-requests.vue delete mode 100644 packages/client/src/pages/admin/overview.federation.vue delete mode 100644 packages/client/src/pages/admin/overview.heatmap.vue delete mode 100644 packages/client/src/pages/admin/overview.instances.vue delete mode 100644 packages/client/src/pages/admin/overview.moderators.vue delete mode 100644 packages/client/src/pages/admin/overview.pie.vue delete mode 100644 packages/client/src/pages/admin/overview.queue.chart.vue delete mode 100644 packages/client/src/pages/admin/overview.queue.vue delete mode 100644 packages/client/src/pages/admin/overview.retention.vue delete mode 100644 packages/client/src/pages/admin/overview.stats.vue delete mode 100644 packages/client/src/pages/admin/overview.users.vue delete mode 100644 packages/client/src/pages/admin/overview.vue delete mode 100644 packages/client/src/pages/admin/proxy-account.vue delete mode 100644 packages/client/src/pages/admin/queue.chart.chart.vue delete mode 100644 packages/client/src/pages/admin/queue.chart.vue delete mode 100644 packages/client/src/pages/admin/queue.vue delete mode 100644 packages/client/src/pages/admin/relays.vue delete mode 100644 packages/client/src/pages/admin/security.vue delete mode 100644 packages/client/src/pages/admin/settings.vue delete mode 100644 packages/client/src/pages/admin/users.vue delete mode 100644 packages/client/src/pages/announcements.vue delete mode 100644 packages/client/src/pages/antenna-timeline.vue delete mode 100644 packages/client/src/pages/api-console.vue delete mode 100644 packages/client/src/pages/auth.form.vue delete mode 100644 packages/client/src/pages/auth.vue delete mode 100644 packages/client/src/pages/channel-editor.vue delete mode 100644 packages/client/src/pages/channel.vue delete mode 100644 packages/client/src/pages/channels.vue delete mode 100644 packages/client/src/pages/clip.vue delete mode 100644 packages/client/src/pages/drive.vue delete mode 100644 packages/client/src/pages/emojis.emoji.vue delete mode 100644 packages/client/src/pages/explore.featured.vue delete mode 100644 packages/client/src/pages/explore.users.vue delete mode 100644 packages/client/src/pages/explore.vue delete mode 100644 packages/client/src/pages/favorites.vue delete mode 100644 packages/client/src/pages/follow-requests.vue delete mode 100644 packages/client/src/pages/follow.vue delete mode 100644 packages/client/src/pages/gallery/edit.vue delete mode 100644 packages/client/src/pages/gallery/index.vue delete mode 100644 packages/client/src/pages/gallery/post.vue delete mode 100644 packages/client/src/pages/instance-info.vue delete mode 100644 packages/client/src/pages/messaging/index.vue delete mode 100644 packages/client/src/pages/messaging/messaging-room.form.vue delete mode 100644 packages/client/src/pages/messaging/messaging-room.message.vue delete mode 100644 packages/client/src/pages/messaging/messaging-room.vue delete mode 100644 packages/client/src/pages/mfm-cheat-sheet.vue delete mode 100644 packages/client/src/pages/miauth.vue delete mode 100644 packages/client/src/pages/my-antennas/create.vue delete mode 100644 packages/client/src/pages/my-antennas/edit.vue delete mode 100644 packages/client/src/pages/my-antennas/editor.vue delete mode 100644 packages/client/src/pages/my-antennas/index.vue delete mode 100644 packages/client/src/pages/my-clips/index.vue delete mode 100644 packages/client/src/pages/my-lists/index.vue delete mode 100644 packages/client/src/pages/my-lists/list.vue delete mode 100644 packages/client/src/pages/not-found.vue delete mode 100644 packages/client/src/pages/note.vue delete mode 100644 packages/client/src/pages/notifications.vue delete mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.image.vue delete mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.note.vue delete mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.section.vue delete mode 100644 packages/client/src/pages/page-editor/els/page-editor.el.text.vue delete mode 100644 packages/client/src/pages/page-editor/page-editor.blocks.vue delete mode 100644 packages/client/src/pages/page-editor/page-editor.container.vue delete mode 100644 packages/client/src/pages/page-editor/page-editor.vue delete mode 100644 packages/client/src/pages/page.vue delete mode 100644 packages/client/src/pages/pages.vue delete mode 100644 packages/client/src/pages/preview.vue delete mode 100644 packages/client/src/pages/registry.keys.vue delete mode 100644 packages/client/src/pages/registry.value.vue delete mode 100644 packages/client/src/pages/registry.vue delete mode 100644 packages/client/src/pages/reset-password.vue delete mode 100644 packages/client/src/pages/scratchpad.vue delete mode 100644 packages/client/src/pages/search.vue delete mode 100644 packages/client/src/pages/settings/2fa.vue delete mode 100644 packages/client/src/pages/settings/account-info.vue delete mode 100644 packages/client/src/pages/settings/accounts.vue delete mode 100644 packages/client/src/pages/settings/api.vue delete mode 100644 packages/client/src/pages/settings/apps.vue delete mode 100644 packages/client/src/pages/settings/custom-css.vue delete mode 100644 packages/client/src/pages/settings/deck.vue delete mode 100644 packages/client/src/pages/settings/delete-account.vue delete mode 100644 packages/client/src/pages/settings/drive.vue delete mode 100644 packages/client/src/pages/settings/email.vue delete mode 100644 packages/client/src/pages/settings/general.vue delete mode 100644 packages/client/src/pages/settings/import-export.vue delete mode 100644 packages/client/src/pages/settings/index.vue delete mode 100644 packages/client/src/pages/settings/instance-mute.vue delete mode 100644 packages/client/src/pages/settings/integration.vue delete mode 100644 packages/client/src/pages/settings/mute-block.vue delete mode 100644 packages/client/src/pages/settings/navbar.vue delete mode 100644 packages/client/src/pages/settings/notifications.vue delete mode 100644 packages/client/src/pages/settings/other.vue delete mode 100644 packages/client/src/pages/settings/plugin.install.vue delete mode 100644 packages/client/src/pages/settings/plugin.vue delete mode 100644 packages/client/src/pages/settings/preferences-backups.vue delete mode 100644 packages/client/src/pages/settings/privacy.vue delete mode 100644 packages/client/src/pages/settings/profile.vue delete mode 100644 packages/client/src/pages/settings/reaction.vue delete mode 100644 packages/client/src/pages/settings/security.vue delete mode 100644 packages/client/src/pages/settings/sounds.sound.vue delete mode 100644 packages/client/src/pages/settings/sounds.vue delete mode 100644 packages/client/src/pages/settings/statusbar.statusbar.vue delete mode 100644 packages/client/src/pages/settings/statusbar.vue delete mode 100644 packages/client/src/pages/settings/theme.install.vue delete mode 100644 packages/client/src/pages/settings/theme.manage.vue delete mode 100644 packages/client/src/pages/settings/theme.vue delete mode 100644 packages/client/src/pages/settings/webhook.edit.vue delete mode 100644 packages/client/src/pages/settings/webhook.new.vue delete mode 100644 packages/client/src/pages/settings/webhook.vue delete mode 100644 packages/client/src/pages/settings/word-mute.vue delete mode 100644 packages/client/src/pages/share.vue delete mode 100644 packages/client/src/pages/signup-complete.vue delete mode 100644 packages/client/src/pages/tag.vue delete mode 100644 packages/client/src/pages/theme-editor.vue delete mode 100644 packages/client/src/pages/timeline.tutorial.vue delete mode 100644 packages/client/src/pages/timeline.vue delete mode 100644 packages/client/src/pages/user-info.vue delete mode 100644 packages/client/src/pages/user-list-timeline.vue delete mode 100644 packages/client/src/pages/user/clips.vue delete mode 100644 packages/client/src/pages/user/follow-list.vue delete mode 100644 packages/client/src/pages/user/followers.vue delete mode 100644 packages/client/src/pages/user/following.vue delete mode 100644 packages/client/src/pages/user/gallery.vue delete mode 100644 packages/client/src/pages/user/home.vue delete mode 100644 packages/client/src/pages/user/index.activity.vue delete mode 100644 packages/client/src/pages/user/index.photos.vue delete mode 100644 packages/client/src/pages/user/index.timeline.vue delete mode 100644 packages/client/src/pages/user/index.vue delete mode 100644 packages/client/src/pages/user/pages.vue delete mode 100644 packages/client/src/pages/user/reactions.vue delete mode 100644 packages/client/src/pages/welcome.entrance.a.vue delete mode 100644 packages/client/src/pages/welcome.entrance.b.vue delete mode 100644 packages/client/src/pages/welcome.entrance.c.vue delete mode 100644 packages/client/src/pages/welcome.setup.vue delete mode 100644 packages/client/src/pages/welcome.timeline.vue delete mode 100644 packages/client/src/pages/welcome.vue delete mode 100644 packages/client/src/pizzax.ts delete mode 100644 packages/client/src/plugin.ts delete mode 100644 packages/client/src/router.ts delete mode 100644 packages/client/src/scripts/2fa.ts delete mode 100644 packages/client/src/scripts/aiscript/api.ts delete mode 100644 packages/client/src/scripts/array.ts delete mode 100644 packages/client/src/scripts/autocomplete.ts delete mode 100644 packages/client/src/scripts/chart-vline.ts delete mode 100644 packages/client/src/scripts/check-word-mute.ts delete mode 100644 packages/client/src/scripts/clone.ts delete mode 100644 packages/client/src/scripts/collect-page-vars.ts delete mode 100644 packages/client/src/scripts/contains.ts delete mode 100644 packages/client/src/scripts/copy-to-clipboard.ts delete mode 100644 packages/client/src/scripts/device-kind.ts delete mode 100644 packages/client/src/scripts/emoji-base.ts delete mode 100644 packages/client/src/scripts/emojilist.ts delete mode 100644 packages/client/src/scripts/extract-avg-color-from-blurhash.ts delete mode 100644 packages/client/src/scripts/extract-mentions.ts delete mode 100644 packages/client/src/scripts/extract-url-from-mfm.ts delete mode 100644 packages/client/src/scripts/focus.ts delete mode 100644 packages/client/src/scripts/form.ts delete mode 100644 packages/client/src/scripts/format-time-string.ts delete mode 100644 packages/client/src/scripts/gen-search-query.ts delete mode 100644 packages/client/src/scripts/get-account-from-id.ts delete mode 100644 packages/client/src/scripts/get-note-menu.ts delete mode 100644 packages/client/src/scripts/get-note-summary.ts delete mode 100644 packages/client/src/scripts/get-static-image-url.ts delete mode 100644 packages/client/src/scripts/get-user-menu.ts delete mode 100644 packages/client/src/scripts/get-user-name.ts delete mode 100644 packages/client/src/scripts/hotkey.ts delete mode 100644 packages/client/src/scripts/hpml/block.ts delete mode 100644 packages/client/src/scripts/hpml/evaluator.ts delete mode 100644 packages/client/src/scripts/hpml/expr.ts delete mode 100644 packages/client/src/scripts/hpml/index.ts delete mode 100644 packages/client/src/scripts/hpml/lib.ts delete mode 100644 packages/client/src/scripts/hpml/type-checker.ts delete mode 100644 packages/client/src/scripts/i18n.ts delete mode 100644 packages/client/src/scripts/idb-proxy.ts delete mode 100644 packages/client/src/scripts/initialize-sw.ts delete mode 100644 packages/client/src/scripts/is-device-darkmode.ts delete mode 100644 packages/client/src/scripts/keycode.ts delete mode 100644 packages/client/src/scripts/langmap.ts delete mode 100644 packages/client/src/scripts/login-id.ts delete mode 100644 packages/client/src/scripts/lookup-user.ts delete mode 100644 packages/client/src/scripts/media-proxy.ts delete mode 100644 packages/client/src/scripts/mfm-tags.ts delete mode 100644 packages/client/src/scripts/page-metadata.ts delete mode 100644 packages/client/src/scripts/physics.ts delete mode 100644 packages/client/src/scripts/please-login.ts delete mode 100644 packages/client/src/scripts/popout.ts delete mode 100644 packages/client/src/scripts/popup-position.ts delete mode 100644 packages/client/src/scripts/reaction-picker.ts delete mode 100644 packages/client/src/scripts/safe-uri-decode.ts delete mode 100644 packages/client/src/scripts/scroll.ts delete mode 100644 packages/client/src/scripts/search.ts delete mode 100644 packages/client/src/scripts/select-file.ts delete mode 100644 packages/client/src/scripts/show-suspended-dialog.ts delete mode 100644 packages/client/src/scripts/shuffle.ts delete mode 100644 packages/client/src/scripts/sound.ts delete mode 100644 packages/client/src/scripts/sticky-sidebar.ts delete mode 100644 packages/client/src/scripts/theme-editor.ts delete mode 100644 packages/client/src/scripts/theme.ts delete mode 100644 packages/client/src/scripts/time.ts delete mode 100644 packages/client/src/scripts/timezones.ts delete mode 100644 packages/client/src/scripts/touch.ts delete mode 100644 packages/client/src/scripts/unison-reload.ts delete mode 100644 packages/client/src/scripts/upload.ts delete mode 100644 packages/client/src/scripts/upload/compress-config.ts delete mode 100644 packages/client/src/scripts/url.ts delete mode 100644 packages/client/src/scripts/use-chart-tooltip.ts delete mode 100644 packages/client/src/scripts/use-interval.ts delete mode 100644 packages/client/src/scripts/use-leave-guard.ts delete mode 100644 packages/client/src/scripts/use-note-capture.ts delete mode 100644 packages/client/src/scripts/use-tooltip.ts delete mode 100644 packages/client/src/store.ts delete mode 100644 packages/client/src/stream.ts delete mode 100644 packages/client/src/style.scss delete mode 100644 packages/client/src/theme-store.ts delete mode 100644 packages/client/src/themes/_dark.json5 delete mode 100644 packages/client/src/themes/_light.json5 delete mode 100644 packages/client/src/themes/d-astro.json5 delete mode 100644 packages/client/src/themes/d-botanical.json5 delete mode 100644 packages/client/src/themes/d-cherry.json5 delete mode 100644 packages/client/src/themes/d-dark.json5 delete mode 100644 packages/client/src/themes/d-future.json5 delete mode 100644 packages/client/src/themes/d-green-lime.json5 delete mode 100644 packages/client/src/themes/d-green-orange.json5 delete mode 100644 packages/client/src/themes/d-ice.json5 delete mode 100644 packages/client/src/themes/d-persimmon.json5 delete mode 100644 packages/client/src/themes/d-u0.json5 delete mode 100644 packages/client/src/themes/l-apricot.json5 delete mode 100644 packages/client/src/themes/l-cherry.json5 delete mode 100644 packages/client/src/themes/l-coffee.json5 delete mode 100644 packages/client/src/themes/l-light.json5 delete mode 100644 packages/client/src/themes/l-rainy.json5 delete mode 100644 packages/client/src/themes/l-sushi.json5 delete mode 100644 packages/client/src/themes/l-u0.json5 delete mode 100644 packages/client/src/themes/l-vivid.json5 delete mode 100644 packages/client/src/types/menu.ts delete mode 100644 packages/client/src/ui/_common_/common.vue delete mode 100644 packages/client/src/ui/_common_/navbar-for-mobile.vue delete mode 100644 packages/client/src/ui/_common_/navbar.vue delete mode 100644 packages/client/src/ui/_common_/statusbar-federation.vue delete mode 100644 packages/client/src/ui/_common_/statusbar-rss.vue delete mode 100644 packages/client/src/ui/_common_/statusbar-user-list.vue delete mode 100644 packages/client/src/ui/_common_/statusbars.vue delete mode 100644 packages/client/src/ui/_common_/stream-indicator.vue delete mode 100644 packages/client/src/ui/_common_/sw-inject.ts delete mode 100644 packages/client/src/ui/_common_/upload.vue delete mode 100644 packages/client/src/ui/classic.header.vue delete mode 100644 packages/client/src/ui/classic.sidebar.vue delete mode 100644 packages/client/src/ui/classic.vue delete mode 100644 packages/client/src/ui/classic.widgets.vue delete mode 100644 packages/client/src/ui/deck.vue delete mode 100644 packages/client/src/ui/deck/antenna-column.vue delete mode 100644 packages/client/src/ui/deck/column-core.vue delete mode 100644 packages/client/src/ui/deck/column.vue delete mode 100644 packages/client/src/ui/deck/deck-store.ts delete mode 100644 packages/client/src/ui/deck/direct-column.vue delete mode 100644 packages/client/src/ui/deck/list-column.vue delete mode 100644 packages/client/src/ui/deck/main-column.vue delete mode 100644 packages/client/src/ui/deck/mentions-column.vue delete mode 100644 packages/client/src/ui/deck/notifications-column.vue delete mode 100644 packages/client/src/ui/deck/tl-column.vue delete mode 100644 packages/client/src/ui/deck/widgets-column.vue delete mode 100644 packages/client/src/ui/universal.vue delete mode 100644 packages/client/src/ui/universal.widgets.vue delete mode 100644 packages/client/src/ui/visitor.vue delete mode 100644 packages/client/src/ui/visitor/a.vue delete mode 100644 packages/client/src/ui/visitor/b.vue delete mode 100644 packages/client/src/ui/visitor/header.vue delete mode 100644 packages/client/src/ui/visitor/kanban.vue delete mode 100644 packages/client/src/ui/zen.vue delete mode 100644 packages/client/src/widgets/activity.calendar.vue delete mode 100644 packages/client/src/widgets/activity.chart.vue delete mode 100644 packages/client/src/widgets/activity.vue delete mode 100644 packages/client/src/widgets/aichan.vue delete mode 100644 packages/client/src/widgets/aiscript.vue delete mode 100644 packages/client/src/widgets/button.vue delete mode 100644 packages/client/src/widgets/calendar.vue delete mode 100644 packages/client/src/widgets/clock.vue delete mode 100644 packages/client/src/widgets/digital-clock.vue delete mode 100644 packages/client/src/widgets/federation.vue delete mode 100644 packages/client/src/widgets/index.ts delete mode 100644 packages/client/src/widgets/instance-cloud.vue delete mode 100644 packages/client/src/widgets/job-queue.vue delete mode 100644 packages/client/src/widgets/memo.vue delete mode 100644 packages/client/src/widgets/notifications.vue delete mode 100644 packages/client/src/widgets/online-users.vue delete mode 100644 packages/client/src/widgets/photos.vue delete mode 100644 packages/client/src/widgets/post-form.vue delete mode 100644 packages/client/src/widgets/rss-ticker.vue delete mode 100644 packages/client/src/widgets/rss.vue delete mode 100644 packages/client/src/widgets/server-metric/cpu-mem.vue delete mode 100644 packages/client/src/widgets/server-metric/cpu.vue delete mode 100644 packages/client/src/widgets/server-metric/disk.vue delete mode 100644 packages/client/src/widgets/server-metric/index.vue delete mode 100644 packages/client/src/widgets/server-metric/mem.vue delete mode 100644 packages/client/src/widgets/server-metric/net.vue delete mode 100644 packages/client/src/widgets/server-metric/pie.vue delete mode 100644 packages/client/src/widgets/slideshow.vue delete mode 100644 packages/client/src/widgets/timeline.vue delete mode 100644 packages/client/src/widgets/trends.vue delete mode 100644 packages/client/src/widgets/unix-clock.vue delete mode 100644 packages/client/src/widgets/user-list.vue delete mode 100644 packages/client/src/widgets/widget.ts delete mode 100644 packages/client/tsconfig.json delete mode 100644 packages/client/vite.config.ts delete mode 100644 packages/client/vite.json5.ts create mode 100644 packages/frontend/.eslintrc.js create mode 100644 packages/frontend/.vscode/settings.json create mode 100644 packages/frontend/@types/global.d.ts create mode 100644 packages/frontend/@types/theme.d.ts create mode 100644 packages/frontend/@types/vue.d.ts create mode 100644 packages/frontend/assets/about-icon.png create mode 100644 packages/frontend/assets/dummy.png create mode 100644 packages/frontend/assets/fedi.jpg create mode 100644 packages/frontend/assets/label-red.svg create mode 100644 packages/frontend/assets/label.svg create mode 100644 packages/frontend/assets/misskey.svg create mode 100644 packages/frontend/assets/remove.png create mode 100644 packages/frontend/assets/sounds/aisha/1.mp3 create mode 100644 packages/frontend/assets/sounds/aisha/2.mp3 create mode 100644 packages/frontend/assets/sounds/aisha/3.mp3 create mode 100644 packages/frontend/assets/sounds/noizenecio/kick_gaba1.mp3 create mode 100644 packages/frontend/assets/sounds/noizenecio/kick_gaba2.mp3 create mode 100644 packages/frontend/assets/sounds/noizenecio/kick_gaba3.mp3 create mode 100644 packages/frontend/assets/sounds/noizenecio/kick_gaba4.mp3 create mode 100644 packages/frontend/assets/sounds/noizenecio/kick_gaba5.mp3 create mode 100644 packages/frontend/assets/sounds/noizenecio/kick_gaba6.mp3 create mode 100644 packages/frontend/assets/sounds/noizenecio/kick_gaba7.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/down.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/kick.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/pirori-square-wet.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/pirori-wet.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/pirori.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/poi1.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/poi2.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/pope1.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/pope2.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/popo.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/queue-jammed.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/reverved.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/ryukyu.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/snare.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/square-pico.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/triple.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/up.mp3 create mode 100644 packages/frontend/assets/sounds/syuilo/waon.mp3 create mode 100644 packages/frontend/assets/tagcanvas.min.js create mode 100644 packages/frontend/assets/unread.svg create mode 100644 packages/frontend/package.json create mode 100644 packages/frontend/src/account.ts create mode 100644 packages/frontend/src/components/MkAbuseReport.vue create mode 100644 packages/frontend/src/components/MkAbuseReportWindow.vue create mode 100644 packages/frontend/src/components/MkActiveUsersHeatmap.vue create mode 100644 packages/frontend/src/components/MkAnalogClock.vue create mode 100644 packages/frontend/src/components/MkAutocomplete.vue create mode 100644 packages/frontend/src/components/MkAvatars.vue create mode 100644 packages/frontend/src/components/MkButton.vue create mode 100644 packages/frontend/src/components/MkCaptcha.vue create mode 100644 packages/frontend/src/components/MkChannelFollowButton.vue create mode 100644 packages/frontend/src/components/MkChannelPreview.vue create mode 100644 packages/frontend/src/components/MkChart.vue create mode 100644 packages/frontend/src/components/MkChartTooltip.vue create mode 100644 packages/frontend/src/components/MkCode.core.vue create mode 100644 packages/frontend/src/components/MkCode.vue create mode 100644 packages/frontend/src/components/MkContainer.vue create mode 100644 packages/frontend/src/components/MkContextMenu.vue create mode 100644 packages/frontend/src/components/MkCropperDialog.vue create mode 100644 packages/frontend/src/components/MkCwButton.vue create mode 100644 packages/frontend/src/components/MkDateSeparatedList.vue create mode 100644 packages/frontend/src/components/MkDialog.vue create mode 100644 packages/frontend/src/components/MkDigitalClock.vue create mode 100644 packages/frontend/src/components/MkDrive.file.vue create mode 100644 packages/frontend/src/components/MkDrive.folder.vue create mode 100644 packages/frontend/src/components/MkDrive.navFolder.vue create mode 100644 packages/frontend/src/components/MkDrive.vue create mode 100644 packages/frontend/src/components/MkDriveFileThumbnail.vue create mode 100644 packages/frontend/src/components/MkDriveSelectDialog.vue create mode 100644 packages/frontend/src/components/MkDriveWindow.vue create mode 100644 packages/frontend/src/components/MkEmojiPicker.section.vue create mode 100644 packages/frontend/src/components/MkEmojiPicker.vue create mode 100644 packages/frontend/src/components/MkEmojiPickerDialog.vue create mode 100644 packages/frontend/src/components/MkEmojiPickerWindow.vue create mode 100644 packages/frontend/src/components/MkFeaturedPhotos.vue create mode 100644 packages/frontend/src/components/MkFileCaptionEditWindow.vue create mode 100644 packages/frontend/src/components/MkFileListForAdmin.vue create mode 100644 packages/frontend/src/components/MkFolder.vue create mode 100644 packages/frontend/src/components/MkFollowButton.vue create mode 100644 packages/frontend/src/components/MkForgotPassword.vue create mode 100644 packages/frontend/src/components/MkFormDialog.vue create mode 100644 packages/frontend/src/components/MkFormula.vue create mode 100644 packages/frontend/src/components/MkFormulaCore.vue create mode 100644 packages/frontend/src/components/MkGalleryPostPreview.vue create mode 100644 packages/frontend/src/components/MkGoogle.vue create mode 100644 packages/frontend/src/components/MkImageViewer.vue create mode 100644 packages/frontend/src/components/MkImgWithBlurhash.vue create mode 100644 packages/frontend/src/components/MkInfo.vue create mode 100644 packages/frontend/src/components/MkInstanceCardMini.vue create mode 100644 packages/frontend/src/components/MkInstanceStats.vue create mode 100644 packages/frontend/src/components/MkInstanceTicker.vue create mode 100644 packages/frontend/src/components/MkKeyValue.vue create mode 100644 packages/frontend/src/components/MkLaunchPad.vue create mode 100644 packages/frontend/src/components/MkLink.vue create mode 100644 packages/frontend/src/components/MkMarquee.vue create mode 100644 packages/frontend/src/components/MkMediaBanner.vue create mode 100644 packages/frontend/src/components/MkMediaImage.vue create mode 100644 packages/frontend/src/components/MkMediaList.vue create mode 100644 packages/frontend/src/components/MkMediaVideo.vue create mode 100644 packages/frontend/src/components/MkMention.vue create mode 100644 packages/frontend/src/components/MkMenu.child.vue create mode 100644 packages/frontend/src/components/MkMenu.vue create mode 100644 packages/frontend/src/components/MkMiniChart.vue create mode 100644 packages/frontend/src/components/MkModal.vue create mode 100644 packages/frontend/src/components/MkModalPageWindow.vue create mode 100644 packages/frontend/src/components/MkModalWindow.vue create mode 100644 packages/frontend/src/components/MkNote.vue create mode 100644 packages/frontend/src/components/MkNoteDetailed.vue create mode 100644 packages/frontend/src/components/MkNoteHeader.vue create mode 100644 packages/frontend/src/components/MkNotePreview.vue create mode 100644 packages/frontend/src/components/MkNoteSimple.vue create mode 100644 packages/frontend/src/components/MkNoteSub.vue create mode 100644 packages/frontend/src/components/MkNotes.vue create mode 100644 packages/frontend/src/components/MkNotification.vue create mode 100644 packages/frontend/src/components/MkNotificationSettingWindow.vue create mode 100644 packages/frontend/src/components/MkNotificationToast.vue create mode 100644 packages/frontend/src/components/MkNotifications.vue create mode 100644 packages/frontend/src/components/MkNumberDiff.vue create mode 100644 packages/frontend/src/components/MkObjectView.value.vue create mode 100644 packages/frontend/src/components/MkObjectView.vue create mode 100644 packages/frontend/src/components/MkPagePreview.vue create mode 100644 packages/frontend/src/components/MkPageWindow.vue create mode 100644 packages/frontend/src/components/MkPagination.vue create mode 100644 packages/frontend/src/components/MkPoll.vue create mode 100644 packages/frontend/src/components/MkPollEditor.vue create mode 100644 packages/frontend/src/components/MkPopupMenu.vue create mode 100644 packages/frontend/src/components/MkPostForm.vue create mode 100644 packages/frontend/src/components/MkPostFormAttaches.vue create mode 100644 packages/frontend/src/components/MkPostFormDialog.vue create mode 100644 packages/frontend/src/components/MkPushNotificationAllowButton.vue create mode 100644 packages/frontend/src/components/MkReactionIcon.vue create mode 100644 packages/frontend/src/components/MkReactionTooltip.vue create mode 100644 packages/frontend/src/components/MkReactionsViewer.details.vue create mode 100644 packages/frontend/src/components/MkReactionsViewer.reaction.vue create mode 100644 packages/frontend/src/components/MkReactionsViewer.vue create mode 100644 packages/frontend/src/components/MkRemoteCaution.vue create mode 100644 packages/frontend/src/components/MkRenoteButton.vue create mode 100644 packages/frontend/src/components/MkRipple.vue create mode 100644 packages/frontend/src/components/MkSample.vue create mode 100644 packages/frontend/src/components/MkSignin.vue create mode 100644 packages/frontend/src/components/MkSigninDialog.vue create mode 100644 packages/frontend/src/components/MkSignup.vue create mode 100644 packages/frontend/src/components/MkSignupDialog.vue create mode 100644 packages/frontend/src/components/MkSparkle.vue create mode 100644 packages/frontend/src/components/MkSubNoteContent.vue create mode 100644 packages/frontend/src/components/MkSuperMenu.vue create mode 100644 packages/frontend/src/components/MkTab.vue create mode 100644 packages/frontend/src/components/MkTagCloud.vue create mode 100644 packages/frontend/src/components/MkTimeline.vue create mode 100644 packages/frontend/src/components/MkToast.vue create mode 100644 packages/frontend/src/components/MkTokenGenerateWindow.vue create mode 100644 packages/frontend/src/components/MkTooltip.vue create mode 100644 packages/frontend/src/components/MkUpdated.vue create mode 100644 packages/frontend/src/components/MkUrlPreview.vue create mode 100644 packages/frontend/src/components/MkUrlPreviewPopup.vue create mode 100644 packages/frontend/src/components/MkUserCardMini.vue create mode 100644 packages/frontend/src/components/MkUserInfo.vue create mode 100644 packages/frontend/src/components/MkUserList.vue create mode 100644 packages/frontend/src/components/MkUserOnlineIndicator.vue create mode 100644 packages/frontend/src/components/MkUserPreview.vue create mode 100644 packages/frontend/src/components/MkUserSelectDialog.vue create mode 100644 packages/frontend/src/components/MkUsersTooltip.vue create mode 100644 packages/frontend/src/components/MkVisibility.vue create mode 100644 packages/frontend/src/components/MkVisibilityPicker.vue create mode 100644 packages/frontend/src/components/MkWaitingDialog.vue create mode 100644 packages/frontend/src/components/MkWidgets.vue create mode 100644 packages/frontend/src/components/MkWindow.vue create mode 100644 packages/frontend/src/components/MkYoutubePlayer.vue create mode 100644 packages/frontend/src/components/form/checkbox.vue create mode 100644 packages/frontend/src/components/form/folder.vue create mode 100644 packages/frontend/src/components/form/input.vue create mode 100644 packages/frontend/src/components/form/link.vue create mode 100644 packages/frontend/src/components/form/radio.vue create mode 100644 packages/frontend/src/components/form/radios.vue create mode 100644 packages/frontend/src/components/form/range.vue create mode 100644 packages/frontend/src/components/form/section.vue create mode 100644 packages/frontend/src/components/form/select.vue create mode 100644 packages/frontend/src/components/form/slot.vue create mode 100644 packages/frontend/src/components/form/split.vue create mode 100644 packages/frontend/src/components/form/suspense.vue create mode 100644 packages/frontend/src/components/form/switch.vue create mode 100644 packages/frontend/src/components/form/textarea.vue create mode 100644 packages/frontend/src/components/global/MkA.vue create mode 100644 packages/frontend/src/components/global/MkAcct.vue create mode 100644 packages/frontend/src/components/global/MkAd.vue create mode 100644 packages/frontend/src/components/global/MkAvatar.vue create mode 100644 packages/frontend/src/components/global/MkEllipsis.vue create mode 100644 packages/frontend/src/components/global/MkEmoji.vue create mode 100644 packages/frontend/src/components/global/MkError.vue create mode 100644 packages/frontend/src/components/global/MkLoading.vue create mode 100644 packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue create mode 100644 packages/frontend/src/components/global/MkPageHeader.vue create mode 100644 packages/frontend/src/components/global/MkSpacer.vue create mode 100644 packages/frontend/src/components/global/MkStickyContainer.vue create mode 100644 packages/frontend/src/components/global/MkTime.vue create mode 100644 packages/frontend/src/components/global/MkUrl.vue create mode 100644 packages/frontend/src/components/global/MkUserName.vue create mode 100644 packages/frontend/src/components/global/RouterView.vue create mode 100644 packages/frontend/src/components/global/i18n.ts create mode 100644 packages/frontend/src/components/index.ts create mode 100644 packages/frontend/src/components/mfm.ts create mode 100644 packages/frontend/src/components/page/page.block.vue create mode 100644 packages/frontend/src/components/page/page.button.vue create mode 100644 packages/frontend/src/components/page/page.canvas.vue create mode 100644 packages/frontend/src/components/page/page.counter.vue create mode 100644 packages/frontend/src/components/page/page.if.vue create mode 100644 packages/frontend/src/components/page/page.image.vue create mode 100644 packages/frontend/src/components/page/page.note.vue create mode 100644 packages/frontend/src/components/page/page.number-input.vue create mode 100644 packages/frontend/src/components/page/page.post.vue create mode 100644 packages/frontend/src/components/page/page.radio-button.vue create mode 100644 packages/frontend/src/components/page/page.section.vue create mode 100644 packages/frontend/src/components/page/page.switch.vue create mode 100644 packages/frontend/src/components/page/page.text-input.vue create mode 100644 packages/frontend/src/components/page/page.text.vue create mode 100644 packages/frontend/src/components/page/page.textarea-input.vue create mode 100644 packages/frontend/src/components/page/page.textarea.vue create mode 100644 packages/frontend/src/components/page/page.vue create mode 100644 packages/frontend/src/config.ts create mode 100644 packages/frontend/src/const.ts create mode 100644 packages/frontend/src/directives/adaptive-border.ts create mode 100644 packages/frontend/src/directives/anim.ts create mode 100644 packages/frontend/src/directives/appear.ts create mode 100644 packages/frontend/src/directives/click-anime.ts create mode 100644 packages/frontend/src/directives/follow-append.ts create mode 100644 packages/frontend/src/directives/get-size.ts create mode 100644 packages/frontend/src/directives/hotkey.ts create mode 100644 packages/frontend/src/directives/index.ts create mode 100644 packages/frontend/src/directives/panel.ts create mode 100644 packages/frontend/src/directives/ripple.ts create mode 100644 packages/frontend/src/directives/size.ts create mode 100644 packages/frontend/src/directives/tooltip.ts create mode 100644 packages/frontend/src/directives/user-preview.ts create mode 100644 packages/frontend/src/emojilist.json create mode 100644 packages/frontend/src/events.ts create mode 100644 packages/frontend/src/filters/bytes.ts create mode 100644 packages/frontend/src/filters/note.ts create mode 100644 packages/frontend/src/filters/number.ts create mode 100644 packages/frontend/src/filters/user.ts create mode 100644 packages/frontend/src/i18n.ts create mode 100644 packages/frontend/src/init.ts create mode 100644 packages/frontend/src/instance.ts create mode 100644 packages/frontend/src/navbar.ts create mode 100644 packages/frontend/src/nirax.ts create mode 100644 packages/frontend/src/os.ts create mode 100644 packages/frontend/src/pages/_empty_.vue create mode 100644 packages/frontend/src/pages/_error_.vue create mode 100644 packages/frontend/src/pages/_loading_.vue create mode 100644 packages/frontend/src/pages/about-misskey.vue create mode 100644 packages/frontend/src/pages/about.emojis.vue create mode 100644 packages/frontend/src/pages/about.federation.vue create mode 100644 packages/frontend/src/pages/about.vue create mode 100644 packages/frontend/src/pages/admin-file.vue create mode 100644 packages/frontend/src/pages/admin/_header_.vue create mode 100644 packages/frontend/src/pages/admin/abuses.vue create mode 100644 packages/frontend/src/pages/admin/ads.vue create mode 100644 packages/frontend/src/pages/admin/announcements.vue create mode 100644 packages/frontend/src/pages/admin/bot-protection.vue create mode 100644 packages/frontend/src/pages/admin/database.vue create mode 100644 packages/frontend/src/pages/admin/email-settings.vue create mode 100644 packages/frontend/src/pages/admin/emoji-edit-dialog.vue create mode 100644 packages/frontend/src/pages/admin/emojis.vue create mode 100644 packages/frontend/src/pages/admin/files.vue create mode 100644 packages/frontend/src/pages/admin/index.vue create mode 100644 packages/frontend/src/pages/admin/instance-block.vue create mode 100644 packages/frontend/src/pages/admin/integrations.discord.vue create mode 100644 packages/frontend/src/pages/admin/integrations.github.vue create mode 100644 packages/frontend/src/pages/admin/integrations.twitter.vue create mode 100644 packages/frontend/src/pages/admin/integrations.vue create mode 100644 packages/frontend/src/pages/admin/metrics.vue create mode 100644 packages/frontend/src/pages/admin/object-storage.vue create mode 100644 packages/frontend/src/pages/admin/other-settings.vue create mode 100644 packages/frontend/src/pages/admin/overview.active-users.vue create mode 100644 packages/frontend/src/pages/admin/overview.ap-requests.vue create mode 100644 packages/frontend/src/pages/admin/overview.federation.vue create mode 100644 packages/frontend/src/pages/admin/overview.heatmap.vue create mode 100644 packages/frontend/src/pages/admin/overview.instances.vue create mode 100644 packages/frontend/src/pages/admin/overview.moderators.vue create mode 100644 packages/frontend/src/pages/admin/overview.pie.vue create mode 100644 packages/frontend/src/pages/admin/overview.queue.chart.vue create mode 100644 packages/frontend/src/pages/admin/overview.queue.vue create mode 100644 packages/frontend/src/pages/admin/overview.retention.vue create mode 100644 packages/frontend/src/pages/admin/overview.stats.vue create mode 100644 packages/frontend/src/pages/admin/overview.users.vue create mode 100644 packages/frontend/src/pages/admin/overview.vue create mode 100644 packages/frontend/src/pages/admin/proxy-account.vue create mode 100644 packages/frontend/src/pages/admin/queue.chart.chart.vue create mode 100644 packages/frontend/src/pages/admin/queue.chart.vue create mode 100644 packages/frontend/src/pages/admin/queue.vue create mode 100644 packages/frontend/src/pages/admin/relays.vue create mode 100644 packages/frontend/src/pages/admin/security.vue create mode 100644 packages/frontend/src/pages/admin/settings.vue create mode 100644 packages/frontend/src/pages/admin/users.vue create mode 100644 packages/frontend/src/pages/announcements.vue create mode 100644 packages/frontend/src/pages/antenna-timeline.vue create mode 100644 packages/frontend/src/pages/api-console.vue create mode 100644 packages/frontend/src/pages/auth.form.vue create mode 100644 packages/frontend/src/pages/auth.vue create mode 100644 packages/frontend/src/pages/channel-editor.vue create mode 100644 packages/frontend/src/pages/channel.vue create mode 100644 packages/frontend/src/pages/channels.vue create mode 100644 packages/frontend/src/pages/clip.vue create mode 100644 packages/frontend/src/pages/drive.vue create mode 100644 packages/frontend/src/pages/emojis.emoji.vue create mode 100644 packages/frontend/src/pages/explore.featured.vue create mode 100644 packages/frontend/src/pages/explore.users.vue create mode 100644 packages/frontend/src/pages/explore.vue create mode 100644 packages/frontend/src/pages/favorites.vue create mode 100644 packages/frontend/src/pages/follow-requests.vue create mode 100644 packages/frontend/src/pages/follow.vue create mode 100644 packages/frontend/src/pages/gallery/edit.vue create mode 100644 packages/frontend/src/pages/gallery/index.vue create mode 100644 packages/frontend/src/pages/gallery/post.vue create mode 100644 packages/frontend/src/pages/instance-info.vue create mode 100644 packages/frontend/src/pages/messaging/index.vue create mode 100644 packages/frontend/src/pages/messaging/messaging-room.form.vue create mode 100644 packages/frontend/src/pages/messaging/messaging-room.message.vue create mode 100644 packages/frontend/src/pages/messaging/messaging-room.vue create mode 100644 packages/frontend/src/pages/mfm-cheat-sheet.vue create mode 100644 packages/frontend/src/pages/miauth.vue create mode 100644 packages/frontend/src/pages/my-antennas/create.vue create mode 100644 packages/frontend/src/pages/my-antennas/edit.vue create mode 100644 packages/frontend/src/pages/my-antennas/editor.vue create mode 100644 packages/frontend/src/pages/my-antennas/index.vue create mode 100644 packages/frontend/src/pages/my-clips/index.vue create mode 100644 packages/frontend/src/pages/my-lists/index.vue create mode 100644 packages/frontend/src/pages/my-lists/list.vue create mode 100644 packages/frontend/src/pages/not-found.vue create mode 100644 packages/frontend/src/pages/note.vue create mode 100644 packages/frontend/src/pages/notifications.vue create mode 100644 packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue create mode 100644 packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue create mode 100644 packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue create mode 100644 packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue create mode 100644 packages/frontend/src/pages/page-editor/page-editor.blocks.vue create mode 100644 packages/frontend/src/pages/page-editor/page-editor.container.vue create mode 100644 packages/frontend/src/pages/page-editor/page-editor.vue create mode 100644 packages/frontend/src/pages/page.vue create mode 100644 packages/frontend/src/pages/pages.vue create mode 100644 packages/frontend/src/pages/preview.vue create mode 100644 packages/frontend/src/pages/registry.keys.vue create mode 100644 packages/frontend/src/pages/registry.value.vue create mode 100644 packages/frontend/src/pages/registry.vue create mode 100644 packages/frontend/src/pages/reset-password.vue create mode 100644 packages/frontend/src/pages/scratchpad.vue create mode 100644 packages/frontend/src/pages/search.vue create mode 100644 packages/frontend/src/pages/settings/2fa.vue create mode 100644 packages/frontend/src/pages/settings/account-info.vue create mode 100644 packages/frontend/src/pages/settings/accounts.vue create mode 100644 packages/frontend/src/pages/settings/api.vue create mode 100644 packages/frontend/src/pages/settings/apps.vue create mode 100644 packages/frontend/src/pages/settings/custom-css.vue create mode 100644 packages/frontend/src/pages/settings/deck.vue create mode 100644 packages/frontend/src/pages/settings/delete-account.vue create mode 100644 packages/frontend/src/pages/settings/drive.vue create mode 100644 packages/frontend/src/pages/settings/email.vue create mode 100644 packages/frontend/src/pages/settings/general.vue create mode 100644 packages/frontend/src/pages/settings/import-export.vue create mode 100644 packages/frontend/src/pages/settings/index.vue create mode 100644 packages/frontend/src/pages/settings/instance-mute.vue create mode 100644 packages/frontend/src/pages/settings/integration.vue create mode 100644 packages/frontend/src/pages/settings/mute-block.vue create mode 100644 packages/frontend/src/pages/settings/navbar.vue create mode 100644 packages/frontend/src/pages/settings/notifications.vue create mode 100644 packages/frontend/src/pages/settings/other.vue create mode 100644 packages/frontend/src/pages/settings/plugin.install.vue create mode 100644 packages/frontend/src/pages/settings/plugin.vue create mode 100644 packages/frontend/src/pages/settings/preferences-backups.vue create mode 100644 packages/frontend/src/pages/settings/privacy.vue create mode 100644 packages/frontend/src/pages/settings/profile.vue create mode 100644 packages/frontend/src/pages/settings/reaction.vue create mode 100644 packages/frontend/src/pages/settings/security.vue create mode 100644 packages/frontend/src/pages/settings/sounds.sound.vue create mode 100644 packages/frontend/src/pages/settings/sounds.vue create mode 100644 packages/frontend/src/pages/settings/statusbar.statusbar.vue create mode 100644 packages/frontend/src/pages/settings/statusbar.vue create mode 100644 packages/frontend/src/pages/settings/theme.install.vue create mode 100644 packages/frontend/src/pages/settings/theme.manage.vue create mode 100644 packages/frontend/src/pages/settings/theme.vue create mode 100644 packages/frontend/src/pages/settings/webhook.edit.vue create mode 100644 packages/frontend/src/pages/settings/webhook.new.vue create mode 100644 packages/frontend/src/pages/settings/webhook.vue create mode 100644 packages/frontend/src/pages/settings/word-mute.vue create mode 100644 packages/frontend/src/pages/share.vue create mode 100644 packages/frontend/src/pages/signup-complete.vue create mode 100644 packages/frontend/src/pages/tag.vue create mode 100644 packages/frontend/src/pages/theme-editor.vue create mode 100644 packages/frontend/src/pages/timeline.tutorial.vue create mode 100644 packages/frontend/src/pages/timeline.vue create mode 100644 packages/frontend/src/pages/user-info.vue create mode 100644 packages/frontend/src/pages/user-list-timeline.vue create mode 100644 packages/frontend/src/pages/user/clips.vue create mode 100644 packages/frontend/src/pages/user/follow-list.vue create mode 100644 packages/frontend/src/pages/user/followers.vue create mode 100644 packages/frontend/src/pages/user/following.vue create mode 100644 packages/frontend/src/pages/user/gallery.vue create mode 100644 packages/frontend/src/pages/user/home.vue create mode 100644 packages/frontend/src/pages/user/index.activity.vue create mode 100644 packages/frontend/src/pages/user/index.photos.vue create mode 100644 packages/frontend/src/pages/user/index.timeline.vue create mode 100644 packages/frontend/src/pages/user/index.vue create mode 100644 packages/frontend/src/pages/user/pages.vue create mode 100644 packages/frontend/src/pages/user/reactions.vue create mode 100644 packages/frontend/src/pages/welcome.entrance.a.vue create mode 100644 packages/frontend/src/pages/welcome.entrance.b.vue create mode 100644 packages/frontend/src/pages/welcome.entrance.c.vue create mode 100644 packages/frontend/src/pages/welcome.setup.vue create mode 100644 packages/frontend/src/pages/welcome.timeline.vue create mode 100644 packages/frontend/src/pages/welcome.vue create mode 100644 packages/frontend/src/pizzax.ts create mode 100644 packages/frontend/src/plugin.ts create mode 100644 packages/frontend/src/router.ts create mode 100644 packages/frontend/src/scripts/2fa.ts create mode 100644 packages/frontend/src/scripts/aiscript/api.ts create mode 100644 packages/frontend/src/scripts/array.ts create mode 100644 packages/frontend/src/scripts/autocomplete.ts create mode 100644 packages/frontend/src/scripts/chart-vline.ts create mode 100644 packages/frontend/src/scripts/check-word-mute.ts create mode 100644 packages/frontend/src/scripts/clone.ts create mode 100644 packages/frontend/src/scripts/collect-page-vars.ts create mode 100644 packages/frontend/src/scripts/contains.ts create mode 100644 packages/frontend/src/scripts/copy-to-clipboard.ts create mode 100644 packages/frontend/src/scripts/device-kind.ts create mode 100644 packages/frontend/src/scripts/emoji-base.ts create mode 100644 packages/frontend/src/scripts/emojilist.ts create mode 100644 packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts create mode 100644 packages/frontend/src/scripts/extract-mentions.ts create mode 100644 packages/frontend/src/scripts/extract-url-from-mfm.ts create mode 100644 packages/frontend/src/scripts/focus.ts create mode 100644 packages/frontend/src/scripts/form.ts create mode 100644 packages/frontend/src/scripts/format-time-string.ts create mode 100644 packages/frontend/src/scripts/gen-search-query.ts create mode 100644 packages/frontend/src/scripts/get-account-from-id.ts create mode 100644 packages/frontend/src/scripts/get-note-menu.ts create mode 100644 packages/frontend/src/scripts/get-note-summary.ts create mode 100644 packages/frontend/src/scripts/get-static-image-url.ts create mode 100644 packages/frontend/src/scripts/get-user-menu.ts create mode 100644 packages/frontend/src/scripts/get-user-name.ts create mode 100644 packages/frontend/src/scripts/hotkey.ts create mode 100644 packages/frontend/src/scripts/hpml/block.ts create mode 100644 packages/frontend/src/scripts/hpml/evaluator.ts create mode 100644 packages/frontend/src/scripts/hpml/expr.ts create mode 100644 packages/frontend/src/scripts/hpml/index.ts create mode 100644 packages/frontend/src/scripts/hpml/lib.ts create mode 100644 packages/frontend/src/scripts/hpml/type-checker.ts create mode 100644 packages/frontend/src/scripts/i18n.ts create mode 100644 packages/frontend/src/scripts/idb-proxy.ts create mode 100644 packages/frontend/src/scripts/initialize-sw.ts create mode 100644 packages/frontend/src/scripts/is-device-darkmode.ts create mode 100644 packages/frontend/src/scripts/keycode.ts create mode 100644 packages/frontend/src/scripts/langmap.ts create mode 100644 packages/frontend/src/scripts/login-id.ts create mode 100644 packages/frontend/src/scripts/lookup-user.ts create mode 100644 packages/frontend/src/scripts/media-proxy.ts create mode 100644 packages/frontend/src/scripts/mfm-tags.ts create mode 100644 packages/frontend/src/scripts/page-metadata.ts create mode 100644 packages/frontend/src/scripts/physics.ts create mode 100644 packages/frontend/src/scripts/please-login.ts create mode 100644 packages/frontend/src/scripts/popout.ts create mode 100644 packages/frontend/src/scripts/popup-position.ts create mode 100644 packages/frontend/src/scripts/reaction-picker.ts create mode 100644 packages/frontend/src/scripts/safe-uri-decode.ts create mode 100644 packages/frontend/src/scripts/scroll.ts create mode 100644 packages/frontend/src/scripts/search.ts create mode 100644 packages/frontend/src/scripts/select-file.ts create mode 100644 packages/frontend/src/scripts/show-suspended-dialog.ts create mode 100644 packages/frontend/src/scripts/shuffle.ts create mode 100644 packages/frontend/src/scripts/sound.ts create mode 100644 packages/frontend/src/scripts/sticky-sidebar.ts create mode 100644 packages/frontend/src/scripts/theme-editor.ts create mode 100644 packages/frontend/src/scripts/theme.ts create mode 100644 packages/frontend/src/scripts/time.ts create mode 100644 packages/frontend/src/scripts/timezones.ts create mode 100644 packages/frontend/src/scripts/touch.ts create mode 100644 packages/frontend/src/scripts/unison-reload.ts create mode 100644 packages/frontend/src/scripts/upload.ts create mode 100644 packages/frontend/src/scripts/upload/compress-config.ts create mode 100644 packages/frontend/src/scripts/url.ts create mode 100644 packages/frontend/src/scripts/use-chart-tooltip.ts create mode 100644 packages/frontend/src/scripts/use-interval.ts create mode 100644 packages/frontend/src/scripts/use-leave-guard.ts create mode 100644 packages/frontend/src/scripts/use-note-capture.ts create mode 100644 packages/frontend/src/scripts/use-tooltip.ts create mode 100644 packages/frontend/src/store.ts create mode 100644 packages/frontend/src/stream.ts create mode 100644 packages/frontend/src/style.scss create mode 100644 packages/frontend/src/theme-store.ts create mode 100644 packages/frontend/src/themes/_dark.json5 create mode 100644 packages/frontend/src/themes/_light.json5 create mode 100644 packages/frontend/src/themes/d-astro.json5 create mode 100644 packages/frontend/src/themes/d-botanical.json5 create mode 100644 packages/frontend/src/themes/d-cherry.json5 create mode 100644 packages/frontend/src/themes/d-dark.json5 create mode 100644 packages/frontend/src/themes/d-future.json5 create mode 100644 packages/frontend/src/themes/d-green-lime.json5 create mode 100644 packages/frontend/src/themes/d-green-orange.json5 create mode 100644 packages/frontend/src/themes/d-ice.json5 create mode 100644 packages/frontend/src/themes/d-persimmon.json5 create mode 100644 packages/frontend/src/themes/d-u0.json5 create mode 100644 packages/frontend/src/themes/l-apricot.json5 create mode 100644 packages/frontend/src/themes/l-cherry.json5 create mode 100644 packages/frontend/src/themes/l-coffee.json5 create mode 100644 packages/frontend/src/themes/l-light.json5 create mode 100644 packages/frontend/src/themes/l-rainy.json5 create mode 100644 packages/frontend/src/themes/l-sushi.json5 create mode 100644 packages/frontend/src/themes/l-u0.json5 create mode 100644 packages/frontend/src/themes/l-vivid.json5 create mode 100644 packages/frontend/src/types/menu.ts create mode 100644 packages/frontend/src/ui/_common_/common.vue create mode 100644 packages/frontend/src/ui/_common_/navbar-for-mobile.vue create mode 100644 packages/frontend/src/ui/_common_/navbar.vue create mode 100644 packages/frontend/src/ui/_common_/statusbar-federation.vue create mode 100644 packages/frontend/src/ui/_common_/statusbar-rss.vue create mode 100644 packages/frontend/src/ui/_common_/statusbar-user-list.vue create mode 100644 packages/frontend/src/ui/_common_/statusbars.vue create mode 100644 packages/frontend/src/ui/_common_/stream-indicator.vue create mode 100644 packages/frontend/src/ui/_common_/sw-inject.ts create mode 100644 packages/frontend/src/ui/_common_/upload.vue create mode 100644 packages/frontend/src/ui/classic.header.vue create mode 100644 packages/frontend/src/ui/classic.sidebar.vue create mode 100644 packages/frontend/src/ui/classic.vue create mode 100644 packages/frontend/src/ui/classic.widgets.vue create mode 100644 packages/frontend/src/ui/deck.vue create mode 100644 packages/frontend/src/ui/deck/antenna-column.vue create mode 100644 packages/frontend/src/ui/deck/column-core.vue create mode 100644 packages/frontend/src/ui/deck/column.vue create mode 100644 packages/frontend/src/ui/deck/deck-store.ts create mode 100644 packages/frontend/src/ui/deck/direct-column.vue create mode 100644 packages/frontend/src/ui/deck/list-column.vue create mode 100644 packages/frontend/src/ui/deck/main-column.vue create mode 100644 packages/frontend/src/ui/deck/mentions-column.vue create mode 100644 packages/frontend/src/ui/deck/notifications-column.vue create mode 100644 packages/frontend/src/ui/deck/tl-column.vue create mode 100644 packages/frontend/src/ui/deck/widgets-column.vue create mode 100644 packages/frontend/src/ui/universal.vue create mode 100644 packages/frontend/src/ui/universal.widgets.vue create mode 100644 packages/frontend/src/ui/visitor.vue create mode 100644 packages/frontend/src/ui/visitor/a.vue create mode 100644 packages/frontend/src/ui/visitor/b.vue create mode 100644 packages/frontend/src/ui/visitor/header.vue create mode 100644 packages/frontend/src/ui/visitor/kanban.vue create mode 100644 packages/frontend/src/ui/zen.vue create mode 100644 packages/frontend/src/widgets/activity.calendar.vue create mode 100644 packages/frontend/src/widgets/activity.chart.vue create mode 100644 packages/frontend/src/widgets/activity.vue create mode 100644 packages/frontend/src/widgets/aichan.vue create mode 100644 packages/frontend/src/widgets/aiscript.vue create mode 100644 packages/frontend/src/widgets/button.vue create mode 100644 packages/frontend/src/widgets/calendar.vue create mode 100644 packages/frontend/src/widgets/clock.vue create mode 100644 packages/frontend/src/widgets/digital-clock.vue create mode 100644 packages/frontend/src/widgets/federation.vue create mode 100644 packages/frontend/src/widgets/index.ts create mode 100644 packages/frontend/src/widgets/instance-cloud.vue create mode 100644 packages/frontend/src/widgets/job-queue.vue create mode 100644 packages/frontend/src/widgets/memo.vue create mode 100644 packages/frontend/src/widgets/notifications.vue create mode 100644 packages/frontend/src/widgets/online-users.vue create mode 100644 packages/frontend/src/widgets/photos.vue create mode 100644 packages/frontend/src/widgets/post-form.vue create mode 100644 packages/frontend/src/widgets/rss-ticker.vue create mode 100644 packages/frontend/src/widgets/rss.vue create mode 100644 packages/frontend/src/widgets/server-metric/cpu-mem.vue create mode 100644 packages/frontend/src/widgets/server-metric/cpu.vue create mode 100644 packages/frontend/src/widgets/server-metric/disk.vue create mode 100644 packages/frontend/src/widgets/server-metric/index.vue create mode 100644 packages/frontend/src/widgets/server-metric/mem.vue create mode 100644 packages/frontend/src/widgets/server-metric/net.vue create mode 100644 packages/frontend/src/widgets/server-metric/pie.vue create mode 100644 packages/frontend/src/widgets/slideshow.vue create mode 100644 packages/frontend/src/widgets/timeline.vue create mode 100644 packages/frontend/src/widgets/trends.vue create mode 100644 packages/frontend/src/widgets/unix-clock.vue create mode 100644 packages/frontend/src/widgets/user-list.vue create mode 100644 packages/frontend/src/widgets/widget.ts create mode 100644 packages/frontend/tsconfig.json create mode 100644 packages/frontend/vite.config.ts create mode 100644 packages/frontend/vite.json5.ts (limited to 'packages') diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 97acfcb919..727bbc9d7b 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -38,8 +38,8 @@ const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const staticAssets = `${_dirname}/../../../assets/`; -const clientAssets = `${_dirname}/../../../../client/assets/`; -const assets = `${_dirname}/../../../../../built/_client_dist_/`; +const clientAssets = `${_dirname}/../../../../frontend/assets/`; +const assets = `${_dirname}/../../../../../built/_frontend_dist_/`; const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`; const viteOut = `${_dirname}/../../../../../built/_vite_/`; diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js deleted file mode 100644 index 6c3bfb5a6e..0000000000 --- a/packages/client/.eslintrc.js +++ /dev/null @@ -1,89 +0,0 @@ -module.exports = { - root: true, - env: { - 'node': false, - }, - parser: 'vue-eslint-parser', - parserOptions: { - 'parser': '@typescript-eslint/parser', - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - extraFileExtensions: ['.vue'], - }, - extends: [ - '../shared/.eslintrc.js', - 'plugin:vue/vue3-recommended', - ], - rules: { - '@typescript-eslint/no-empty-interface': [ - 'error', - { - 'allowSingleExtends': true, - }, - ], - '@typescript-eslint/prefer-nullish-coalescing': [ - 'error', - ], - // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - 'id-denylist': ['error', 'window', 'e'], - 'no-shadow': ['warn'], - 'vue/attributes-order': ['error', { - 'alphabetical': false, - }], - 'vue/no-use-v-if-with-v-for': ['error', { - 'allowUsingIterationVar': false, - }], - 'vue/no-ref-as-operand': 'error', - 'vue/no-multi-spaces': ['error', { - 'ignoreProperties': false, - }], - 'vue/no-v-html': 'warn', - 'vue/order-in-components': 'error', - 'vue/html-indent': ['warn', 'tab', { - 'attribute': 1, - 'baseIndent': 0, - 'closeBracket': 0, - 'alignAttributesVertically': true, - 'ignores': [], - }], - 'vue/html-closing-bracket-spacing': ['warn', { - 'startTag': 'never', - 'endTag': 'never', - 'selfClosingTag': 'never', - }], - 'vue/multi-word-component-names': 'warn', - 'vue/require-v-for-key': 'warn', - 'vue/no-unused-components': 'warn', - 'vue/valid-v-for': 'warn', - 'vue/return-in-computed-property': 'warn', - 'vue/no-setup-props-destructure': 'warn', - 'vue/max-attributes-per-line': 'off', - 'vue/html-self-closing': 'off', - 'vue/singleline-html-element-content-newline': 'off', - // (vue/vue3-recommended disabled the autofix for Vue 2 compatibility) - 'vue/v-on-event-hyphenation': ['warn', 'always', { autofix: true }], - }, - globals: { - // Node.js - 'module': false, - 'require': false, - '__dirname': false, - - // Vue - '$$': false, - '$ref': false, - '$shallowRef': false, - '$computed': false, - - // Misskey - '_DEV_': false, - '_LANGS_': false, - '_VERSION_': false, - '_ENV_': false, - '_PERF_PREFIX_': false, - '_DATA_TRANSFER_DRIVE_FILE_': false, - '_DATA_TRANSFER_DRIVE_FOLDER_': false, - '_DATA_TRANSFER_DECK_COLUMN_': false, - }, -}; diff --git a/packages/client/.vscode/settings.json b/packages/client/.vscode/settings.json deleted file mode 100644 index 4b0903b763..0000000000 --- a/packages/client/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "typescript.tsdk": "node_modules\\typescript\\lib", - "path-intellisense.mappings": { - "@": "${workspaceRoot}/packages/client/src/" - }, - "eslint.validate": [ - "javascript", - "javascriptreact", - "vue" - ] -} diff --git a/packages/client/@types/global.d.ts b/packages/client/@types/global.d.ts deleted file mode 100644 index c757482900..0000000000 --- a/packages/client/@types/global.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -type FIXME = any; - -declare const _LANGS_: string[][]; -declare const _VERSION_: string; -declare const _ENV_: string; -declare const _DEV_: boolean; -declare const _PERF_PREFIX_: string; -declare const _DATA_TRANSFER_DRIVE_FILE_: string; -declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; -declare const _DATA_TRANSFER_DECK_COLUMN_: string; diff --git a/packages/client/@types/theme.d.ts b/packages/client/@types/theme.d.ts deleted file mode 100644 index 67f724a9aa..0000000000 --- a/packages/client/@types/theme.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module '@/themes/*.json5' { - import { Theme } from "@/scripts/theme"; - - const theme: Theme; - - export default theme; -} diff --git a/packages/client/@types/vue.d.ts b/packages/client/@types/vue.d.ts deleted file mode 100644 index 9c9c34ccc5..0000000000 --- a/packages/client/@types/vue.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/// - -import type { $i } from '@/account'; -import type { defaultStore } from '@/store'; -import type { instance } from '@/instance'; -import type { i18n } from '@/i18n'; - -declare module 'vue' { - interface ComponentCustomProperties { - $i: typeof $i; - $store: typeof defaultStore; - $instance: typeof instance; - $t: typeof i18n['t']; - $ts: typeof i18n['ts']; - } -} diff --git a/packages/client/assets/about-icon.png b/packages/client/assets/about-icon.png deleted file mode 100644 index afc1f0c728..0000000000 Binary files a/packages/client/assets/about-icon.png and /dev/null differ diff --git a/packages/client/assets/dummy.png b/packages/client/assets/dummy.png deleted file mode 100644 index 39332b0c1b..0000000000 Binary files a/packages/client/assets/dummy.png and /dev/null differ diff --git a/packages/client/assets/fedi.jpg b/packages/client/assets/fedi.jpg deleted file mode 100644 index cbf3748eb8..0000000000 Binary files a/packages/client/assets/fedi.jpg and /dev/null differ diff --git a/packages/client/assets/label-red.svg b/packages/client/assets/label-red.svg deleted file mode 100644 index 45996aa9ce..0000000000 --- a/packages/client/assets/label-red.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/packages/client/assets/label.svg b/packages/client/assets/label.svg deleted file mode 100644 index b1f85f3c07..0000000000 --- a/packages/client/assets/label.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/packages/client/assets/misskey.svg b/packages/client/assets/misskey.svg deleted file mode 100644 index 3fcb2d3ecb..0000000000 --- a/packages/client/assets/misskey.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/client/assets/remove.png b/packages/client/assets/remove.png deleted file mode 100644 index c2e222a0fc..0000000000 Binary files a/packages/client/assets/remove.png and /dev/null differ diff --git a/packages/client/assets/sounds/aisha/1.mp3 b/packages/client/assets/sounds/aisha/1.mp3 deleted file mode 100644 index d8e9a2f265..0000000000 Binary files a/packages/client/assets/sounds/aisha/1.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/aisha/2.mp3 b/packages/client/assets/sounds/aisha/2.mp3 deleted file mode 100644 index 477c2eba43..0000000000 Binary files a/packages/client/assets/sounds/aisha/2.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/aisha/3.mp3 b/packages/client/assets/sounds/aisha/3.mp3 deleted file mode 100644 index fe0d8063df..0000000000 Binary files a/packages/client/assets/sounds/aisha/3.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba1.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba1.mp3 deleted file mode 100644 index 616b506c4f..0000000000 Binary files a/packages/client/assets/sounds/noizenecio/kick_gaba1.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba2.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba2.mp3 deleted file mode 100644 index 33c2837620..0000000000 Binary files a/packages/client/assets/sounds/noizenecio/kick_gaba2.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba3.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba3.mp3 deleted file mode 100644 index 1791f26573..0000000000 Binary files a/packages/client/assets/sounds/noizenecio/kick_gaba3.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba4.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba4.mp3 deleted file mode 100644 index 5f8bf468e5..0000000000 Binary files a/packages/client/assets/sounds/noizenecio/kick_gaba4.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba5.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba5.mp3 deleted file mode 100644 index dabe754b5b..0000000000 Binary files a/packages/client/assets/sounds/noizenecio/kick_gaba5.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba6.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba6.mp3 deleted file mode 100644 index 57ecb01bda..0000000000 Binary files a/packages/client/assets/sounds/noizenecio/kick_gaba6.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/noizenecio/kick_gaba7.mp3 b/packages/client/assets/sounds/noizenecio/kick_gaba7.mp3 deleted file mode 100644 index 6ba317deb1..0000000000 Binary files a/packages/client/assets/sounds/noizenecio/kick_gaba7.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/down.mp3 b/packages/client/assets/sounds/syuilo/down.mp3 deleted file mode 100644 index 4cd421139d..0000000000 Binary files a/packages/client/assets/sounds/syuilo/down.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/kick.mp3 b/packages/client/assets/sounds/syuilo/kick.mp3 deleted file mode 100644 index 4e0e72091c..0000000000 Binary files a/packages/client/assets/sounds/syuilo/kick.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/pirori-square-wet.mp3 b/packages/client/assets/sounds/syuilo/pirori-square-wet.mp3 deleted file mode 100644 index babf1fce60..0000000000 Binary files a/packages/client/assets/sounds/syuilo/pirori-square-wet.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/pirori-wet.mp3 b/packages/client/assets/sounds/syuilo/pirori-wet.mp3 deleted file mode 100644 index 25e2c46a64..0000000000 Binary files a/packages/client/assets/sounds/syuilo/pirori-wet.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/pirori.mp3 b/packages/client/assets/sounds/syuilo/pirori.mp3 deleted file mode 100644 index a745415ac0..0000000000 Binary files a/packages/client/assets/sounds/syuilo/pirori.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/poi1.mp3 b/packages/client/assets/sounds/syuilo/poi1.mp3 deleted file mode 100644 index 59dae90965..0000000000 Binary files a/packages/client/assets/sounds/syuilo/poi1.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/poi2.mp3 b/packages/client/assets/sounds/syuilo/poi2.mp3 deleted file mode 100644 index a65c653891..0000000000 Binary files a/packages/client/assets/sounds/syuilo/poi2.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/pope1.mp3 b/packages/client/assets/sounds/syuilo/pope1.mp3 deleted file mode 100644 index d6f53cfacc..0000000000 Binary files a/packages/client/assets/sounds/syuilo/pope1.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/pope2.mp3 b/packages/client/assets/sounds/syuilo/pope2.mp3 deleted file mode 100644 index fe5d95e292..0000000000 Binary files a/packages/client/assets/sounds/syuilo/pope2.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/popo.mp3 b/packages/client/assets/sounds/syuilo/popo.mp3 deleted file mode 100644 index a2a1605bbb..0000000000 Binary files a/packages/client/assets/sounds/syuilo/popo.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/queue-jammed.mp3 b/packages/client/assets/sounds/syuilo/queue-jammed.mp3 deleted file mode 100644 index 99e0c437fe..0000000000 Binary files a/packages/client/assets/sounds/syuilo/queue-jammed.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/reverved.mp3 b/packages/client/assets/sounds/syuilo/reverved.mp3 deleted file mode 100644 index 47588ef270..0000000000 Binary files a/packages/client/assets/sounds/syuilo/reverved.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/ryukyu.mp3 b/packages/client/assets/sounds/syuilo/ryukyu.mp3 deleted file mode 100644 index 9e935e3f37..0000000000 Binary files a/packages/client/assets/sounds/syuilo/ryukyu.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/snare.mp3 b/packages/client/assets/sounds/syuilo/snare.mp3 deleted file mode 100644 index 9244189c2d..0000000000 Binary files a/packages/client/assets/sounds/syuilo/snare.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/square-pico.mp3 b/packages/client/assets/sounds/syuilo/square-pico.mp3 deleted file mode 100644 index c4d8305ae7..0000000000 Binary files a/packages/client/assets/sounds/syuilo/square-pico.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/triple.mp3 b/packages/client/assets/sounds/syuilo/triple.mp3 deleted file mode 100644 index 54ab974d46..0000000000 Binary files a/packages/client/assets/sounds/syuilo/triple.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/up.mp3 b/packages/client/assets/sounds/syuilo/up.mp3 deleted file mode 100644 index 3f30867764..0000000000 Binary files a/packages/client/assets/sounds/syuilo/up.mp3 and /dev/null differ diff --git a/packages/client/assets/sounds/syuilo/waon.mp3 b/packages/client/assets/sounds/syuilo/waon.mp3 deleted file mode 100644 index a4af473861..0000000000 Binary files a/packages/client/assets/sounds/syuilo/waon.mp3 and /dev/null differ diff --git a/packages/client/assets/tagcanvas.min.js b/packages/client/assets/tagcanvas.min.js deleted file mode 100644 index bcee46e682..0000000000 --- a/packages/client/assets/tagcanvas.min.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (C) 2010-2021 Graham Breach - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ -/** - * TagCanvas 2.11 - * For more information, please contact - */ - (function(){"use strict";var r,C,p=Math.abs,o=Math.sin,l=Math.cos,g=Math.max,h=Math.min,af=Math.ceil,E=Math.sqrt,w=Math.pow,I={},D={},R={0:"0,",1:"17,",2:"34,",3:"51,",4:"68,",5:"85,",6:"102,",7:"119,",8:"136,",9:"153,",a:"170,",A:"170,",b:"187,",B:"187,",c:"204,",C:"204,",d:"221,",D:"221,",e:"238,",E:"238,",f:"255,",F:"255,"},f,d,b,T,z,F,M,c=document,v,e,P,j={};for(r=0;r<256;++r)C=r.toString(16),r<16&&(C='0'+C),D[C]=D[C.toUpperCase()]=r.toString()+',';function n(a){return typeof a!='undefined'}function B(a){return typeof a=='object'&&a!=null}function G(a,c,b){return isNaN(a)?b:h(b,g(c,a))}function x(){return!1}function q(){return(new Date).valueOf()}function ak(c,d){var b=[],e=c.length,a;for(a=0;a=1)?0:a<=-1?Math.PI:Math.acos(a)},z.unit=function(){var a=this.length();return new s(this.x/a,this.y/a,this.z/a)};function ay(b,a){a=a*Math.PI/180,b=b*Math.PI/180;var c=o(b)*l(a),d=-o(a),e=-l(b)*l(a);return new s(c,d,e)}function m(a){this[1]={1:a[0],2:a[1],3:a[2]},this[2]={1:a[3],2:a[4],3:a[5]},this[3]={1:a[6],2:a[7],3:a[8]}}T=m.prototype,m.Identity=function(){return new m([1,0,0,0,1,0,0,0,1])},m.Rotation=function(e,a){var c=o(e),d=l(e),b=1-d;return new m([d+w(a.x,2)*b,a.x*a.y*b-a.z*c,a.x*a.z*b+a.y*c,a.y*a.x*b+a.z*c,d+w(a.y,2)*b,a.y*a.z*b-a.x*c,a.z*a.x*b-a.y*c,a.z*a.y*b+a.x*c,d+w(a.z,2)*b])},T.mul=function(c){var d=[],a,b,e=c.xform?1:0;for(a=1;a<=3;++a)for(b=1;b<=3;++b)e?d.push(this[a][1]*c[1][b]+this[a][2]*c[2][b]+this[a][3]*c[3][b]):d.push(this[a][b]*c);return new m(d)},T.xform=function(b){var a={},c=b.x,d=b.y,e=b.z;return a.x=c*this[1][1]+d*this[2][1]+e*this[3][1],a.y=c*this[1][2]+d*this[2][2]+e*this[3][2],a.z=c*this[1][3]+d*this[2][3]+e*this[3][3],a};function aB(g,j,k,m,f){var a,b,c,d,e=[],h=2/g,i;i=Math.PI*(3-E(5)+(parseFloat(f)?parseFloat(f):0));for(a=0;a0)}function aC(a,c,f,d){var e=a.createLinearGradient(0,0,c,0),b;for(b in d)e.addColorStop(1-b,d[b]);a.fillStyle=e,a.fillRect(0,f,c,1)}function L(a,m,j){var l=1024,d=1,e=a.weightGradient,i,f,b,c;if(a.gCanvas)f=a.gCanvas.getContext('2d'),d=a.gCanvas.height;else{if(B(e[0])?d=e.length:e=[e],a.gCanvas=i=k(l,d),!i)return null;f=i.getContext('2d');for(b=0;b0?b=i*b/100:b=b*j,a=e.getContext('2d'),a.globalCompositeOperation='source-over',a.fillStyle='#fff',b>=i/2?(b=h(c,d)/2,a.beginPath(),a.moveTo(c/2,d/2),a.arc(c/2,d/2,b,0,2*Math.PI,!1),a.fill(),a.closePath()):(b=h(c/2,d/2,b),y(a,0,0,c,d,b,!0),a.fill()),a.globalCompositeOperation='source-in',a.drawImage(l,0,0,c,d),e)}function ao(q,m,i,b,h,a,c){var g=p(c[0]),f=p(c[1]),j=m+(g>a?g+a:a*2)*b,l=i+(f>a?f+a:a*2)*b,n=b*((a||0)+(c[0]<0?g:0)),o=b*((a||0)+(c[1]<0?f:0)),e,d;return e=k(j,l),!e?null:(d=e.getContext('2d'),h&&(d.shadowColor=h),a&&(d.shadowBlur=a*b),c&&(d.shadowOffsetX=c[0]*b,d.shadowOffsetY=c[1]*b),d.drawImage(q,n,o,m,i),{image:e,width:j/b,height:l/b})}function ae(m,o,l){var c=parseInt(m.toString().length*l),h=parseInt(l*2*m.length),j=k(c,h),g,i,e,f,b,d,n,a;if(!j)return null;g=j.getContext('2d'),g.fillStyle='#000',g.fillRect(0,0,c,h),Y(g,l+'px '+o,'#fff',m,0,0,0,0,[],'centre'),i=g.getImageData(0,0,c,h),e=i.width,f=i.height,a={min:{x:e,y:f},max:{x:-1,y:-1}};for(d=0;d0&&(ba.max.x&&(a.max.x=b),da.max.y&&(a.max.y=d));return e!=c&&(a.min.x*=c/e,a.max.x*=c/e),f!=h&&(a.min.y*=c/f,a.max.y*=c/f),j=null,a}function Q(a){return"'"+a.replace(/(\'|\")/g,'').replace(/\s*,\s*/g,"', '")+"'"}function t(b,d,a){a=a||c,a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent('on'+b,d)}function am(b,d,a){a=a||c,a.removeEventListener?a.removeEventListener(b,d):a.detachEvent('on'+b,d)}function A(g,e,j,a,b){var l=b.imageScale,h,c,k,m,f,d;if(!e.complete)return t('load',function(){A(g,e,j,a,b)},e);if(!g.complete)return t('load',function(){A(g,e,j,a,b)},g);if(j&&!j.complete)return t('load',function(){A(g,e,j,a,b)},j);e.width=e.width,e.height=e.height,l&&(g.width=e.width*l,g.height=e.height*l),a.iw=g.width,a.ih=g.height,b.txtOpt&&(c=g,h=b.zoomMax*b.txtScale,f=a.iw*h,d=a.ih*h,f0?(a.iw+=2*b.outlineIncrease,a.ih+=2*b.outlineIncrease,f=h*a.iw,d=h*a.ih,c=S(a.fimage,f,d),a.oimage=c,a.fimage=H(a.fimage,a.oimage.width,a.oimage.height)):(f=h*(a.iw+2*b.outlineIncrease),d=h*(a.ih+2*b.outlineIncrease),c=S(a.fimage,f,d),a.oimage=H(c,a.fimage.width,a.fimage.height))))),a.alt=j,a.Init()}function i(a,d){var b=c.defaultView,e=d.replace(/\-([a-z])/g,function(a){return a.charAt(1).toUpperCase()});return b&&b.getComputedStyle&&b.getComputedStyle(a,null).getPropertyValue(d)||a.currentStyle&&a.currentStyle[e]}function aj(c,d,e){var b=1,a;return d?b=1*(c.getAttribute(d)||e):(a=i(c,'font-size'))&&(b=a.indexOf('px')>-1&&a.replace('px','')*1||a.indexOf('pt')>-1&&a.replace('pt','')*1.25||a*3.3),b}function u(a){return a.target&&n(a.target.id)?a.target.id:a.srcElement.parentNode.id}function K(a,c){var b,d,e=parseInt(i(c,'width'))/c.width,f=parseInt(i(c,'height'))/c.height;return n(a.offsetX)?b={x:a.offsetX,y:a.offsetY}:(d=X(c.id),n(a.changedTouches)&&(a=a.changedTouches[0]),a.pageX&&(b={x:a.pageX-d.x,y:a.pageY-d.y})),b&&e&&f&&(b.x/=e,b.y/=f),b}function an(c){var d=c.target||c.fromElement.parentNode,b=a.tc[d.id];b&&(b.mx=b.my=-1,b.UnFreeze(),b.EndDrag())}function ad(e){var g,c=a,b,d,f=u(e);for(g in c.tc)b=c.tc[g],b.tttimer&&(clearTimeout(b.tttimer),b.tttimer=null);f&&c.tc[f]&&(b=c.tc[f],(d=K(e,b.canvas))&&(b.mx=d.x,b.my=d.y,b.Drag(e,d)),b.drawn=0)}function ap(b){var e=a,f=c.addEventListener?0:1,d=u(b);d&&b.button==f&&e.tc[d]&&e.tc[d].BeginDrag(b)}function aq(b){var f=a,g=c.addEventListener?0:1,e=u(b),d;e&&b.button==g&&f.tc[e]&&(d=f.tc[e],ad(b),!d.EndDrag()&&!d.touchState&&d.Clicked(b))}function ar(c){var e=u(c),b=e&&a.tc[e],d;b&&c.changedTouches&&(c.touches.length==1&&b.touchState==0?(b.touchState=1,b.BeginDrag(c),(d=K(c,b.canvas))&&(b.mx=d.x,b.my=d.y,b.drawn=0)):c.targetTouches.length==2&&b.pinchZoom?(b.touchState=3,b.EndDrag(),b.BeginPinch(c)):(b.EndDrag(),b.EndPinch(),b.touchState=0))}function ac(c){var d=u(c),b=d&&a.tc[d];if(b&&c.changedTouches){switch(b.touchState){case 1:b.Draw(),b.Clicked();break;break;case 2:b.EndDrag();break;case 3:b.EndPinch()}b.touchState=0}}function au(c){var f,e=a,b,d,g=u(c);for(f in e.tc)b=e.tc[f],b.tttimer&&(clearTimeout(b.tttimer),b.tttimer=null);if(b=g&&e.tc[g],b&&c.changedTouches&&b.touchState){switch(b.touchState){case 1:case 2:(d=K(c,b.canvas))&&(b.mx=d.x,b.my=d.y,b.Drag(c,d)&&(b.touchState=2));break;case 3:b.Pinch(c)}b.drawn=0}}function ab(b){var d=a,c=u(b);c&&d.tc[c]&&(b.cancelBubble=!0,b.returnValue=!1,b.preventDefault&&b.preventDefault(),d.tc[c].Wheel((b.wheelDelta||b.detail)>0))}function aw(d){var c,b=a;clearTimeout(b.scrollTimer);for(c in b.tc)b.tc[c].Pause();b.scrollTimer=setTimeout(function(){var b,c=a;for(b in c.tc)c.tc[b].Resume()},b.scrollPause)}function al(){Z(q())}function Z(b){var c=a.tc,d;a.NextFrame(a.interval),b=b||q();for(d in c)c[d].Draw(b)}function az(){requestAnimationFrame(Z)}function aA(a){setTimeout(al,a)}function X(f){var g=c.getElementById(f),b=g.getBoundingClientRect(),a=c.documentElement,d=c.body,e=window,h=e.pageXOffset||a.scrollLeft,i=e.pageYOffset||a.scrollTop,j=a.clientLeft||d.clientLeft,k=a.clientTop||d.clientTop;return{x:b.left+h-j,y:b.top+i-k}}function aI(a,b,d,e){var c=a.radius*a.z1/(a.z1+a.z2+b.z);return{x:b.x*c*d,y:b.y*c*e,z:b.z,w:(a.z1-b.z)/a.z2}}function V(a){this.e=a,this.br=0,this.line=[],this.text=[],this.original=a.innerText||a.textContent}F=V.prototype,F.Empty=function(){for(var a=0;ah?(d.push(this.line.join(' ')),this.line=[a[b]]):this.line.push(a[b]);d.push(this.line.join(' '))}return this.text=d};function _(a,b){this.ts=null,this.tc=a,this.tag=b,this.x=this.y=this.w=this.h=this.sc=1,this.z=0,this.pulse=1,this.pulsate=a.pulsateTo<1,this.colour=a.outlineColour,this.adash=~~a.outlineDash,this.agap=~~a.outlineDashSpace||this.adash,this.aspeed=a.outlineDashSpeed*1,this.colour=='tag'?this.colour=i(b.a,'color'):this.colour=='tagbg'&&(this.colour=i(b.a,'background-color')),this.Draw=this.pulsate?this.DrawPulsate:this.DrawSimple,this.radius=a.outlineRadius|0,this.SetMethod(a.outlineMethod,a.altImage)}f=_.prototype,f.SetMethod=function(a,d){var b={block:['PreDraw','DrawBlock'],colour:['PreDraw','DrawColour'],outline:['PostDraw','DrawOutline'],classic:['LastDraw','DrawOutline'],size:['PreDraw','DrawSize'],none:['LastDraw']},c=b[a]||b.outline;a=='none'?this.Draw=function(){return 1}:this.drawFunc=this[c[1]],this[c[0]]=this.Draw,d&&(this.RealPreDraw=this.PreDraw,this.PreDraw=this.DrawAlt)},f.Update=function(d,e,i,j,a,f,g,h){var b=this.tc.outlineOffset,c=2*b;this.x=a*d+g-b,this.y=a*e+h-b,this.w=a*i+c,this.h=a*j+c,this.sc=a,this.z=f},f.Ants=function(k){if(!this.adash)return;var b=this.adash,c=this.agap,a=this.aspeed,j=b+c,h=0,g=b,f=c,i=0,d=0,e;a&&(d=p(a)*(q()-this.ts)/50,a<0&&(d=864e4-d),a=~~d%j),a?(b>=a?(h=b-a,g=a):(f=j-a,i=c-f),e=[h,f,g,i]):e=[b,c],k.setLineDash(e)},f.DrawOutline=function(a,d,e,b,c,f){var g=h(this.radius,c/2,b/2);a.strokeStyle=f,this.Ants(a),y(a,d,e,b,c,g,!0)},f.DrawSize=function(i,n,m,l,k,j,a,h,g){var f=a.w,e=a.h,c,b,d;return this.pulsate?(a.image?d=(a.image.height+this.tc.outlineIncrease)/a.image.height:d=a.oscale,b=a.fimage||a.image,c=1+(d-1)*(1-this.pulse),a.h*=c,a.w*=c):b=a.oimage,a.alpha=1,a.Draw(i,h,g,b),a.h=e,a.w=f,1},f.DrawColour=function(d,h,i,e,f,g,a,b,c){return a.oimage?(this.pulse<1?(a.alpha=1-w(this.pulse,2),a.Draw(d,b,c,a.fimage),a.alpha=this.pulse):a.alpha=1,a.Draw(d,b,c,a.oimage),1):this[a.image?'DrawColourImage':'DrawColourText'](d,h,i,e,f,g,a,b,c)},f.DrawColourText=function(f,h,i,j,g,e,a,b,c){var d=a.colour;return a.colour=e,a.alpha=1,a.Draw(f,b,c),a.colour=d,1},f.DrawColourImage=function(a,q,p,o,n,m,i,r,l){var f=a.canvas,e=~~g(q,0),d=~~g(p,0),c=h(f.width-e,o)+.5|0,b=h(f.height-d,n)+.5|0,j;return v?(v.width=c,v.height=b):v=k(c,b),!v?this.SetMethod('outline'):(j=v.getContext('2d'),j.drawImage(f,e,d,c,b,0,0,c,b),a.clearRect(e,d,c,b),this.pulsate?i.alpha=1-w(this.pulse,2):i.alpha=1,i.Draw(a,r,l),a.setTransform(1,0,0,1,0,0),a.save(),a.beginPath(),a.rect(e,d,c,b),a.clip(),a.globalCompositeOperation='source-in',a.fillStyle=m,a.fillRect(e,d,c,b),a.restore(),a.globalAlpha=1,a.globalCompositeOperation='destination-over',a.drawImage(v,0,0,c,b,e,d,c,b),a.globalCompositeOperation='source-over',1)},f.DrawAlt=function(b,a,c,d,f,g){var e=this.RealPreDraw(b,a,c,d,f,g);return a.alt&&(a.DrawImage(b,c,d,a.alt),e=1),e},f.DrawBlock=function(a,d,e,b,c,f){var g=h(this.radius,c/2,b/2);a.fillStyle=f,y(a,d,e,b,c,g)},f.DrawSimple=function(a,b,c,d,e,f){var g=this.tc;return a.setTransform(1,0,0,1,0,0),a.strokeStyle=this.colour,a.lineWidth=g.outlineThickness,a.shadowBlur=a.shadowOffsetX=a.shadowOffsetY=0,a.globalAlpha=f?e:1,this.drawFunc(a,this.x,this.y,this.w,this.h,this.colour,b,c,d)},f.DrawPulsate=function(h,d,e,f){var g=q()-this.ts,c=this.tc,b=c.pulsateTo+(1-c.pulsateTo)*(.5+l(2*Math.PI*g/(1e3*c.pulsateTime))/2);return this.pulse=b=a.Smooth(1,b),this.DrawSimple(h,d,e,f,b,1)},f.Active=function(d,a,b){var c=a>=this.x&&b>=this.y&&a<=this.x+this.w&&b<=this.y+this.h;return c?this.ts=this.ts||q():this.ts=null,c},f.PreDraw=f.PostDraw=f.LastDraw=x;function J(a,h,c,b,e,f,g,d,i,j,k,l,m,n){this.tc=a,this.image=null,this.text=h,this.text_original=n,this.line_widths=[],this.title=c.title||null,this.a=c,this.position=new s(b[0],b[1],b[2]),this.x=this.y=this.z=0,this.w=e,this.h=f,this.colour=g||a.textColour,this.bgColour=d||a.bgColour,this.bgRadius=i|0,this.bgOutline=j||this.colour,this.bgOutlineThickness=k|0,this.textFont=l||a.textFont,this.padding=m|0,this.sc=this.alpha=1,this.weighted=!a.weight,this.outline=new _(a,this),this.audio=null}d=J.prototype,d.Init=function(b){var a=this.tc;this.textHeight=a.textHeight,this.HasText()?this.Measure(a.ctxt,a):(this.w=this.iw,this.h=this.ih),this.SetShadowColour=a.shadowAlpha?this.SetShadowColourAlpha:this.SetShadowColourFixed,this.SetDraw(a)},d.Draw=x,d.HasText=function(){return this.text&&this.text[0].length>0},d.EqualTo=function(a){var b=a.getElementsByTagName('img');return this.a.href!=a.href?0:b.length?this.image.src==b[0].src:(a.innerText||a.textContent)==this.text_original},d.SetImage=function(a){this.image=this.fimage=a},d.SetAudio=function(a){this.audio=a,this.audio.load()},d.SetDraw=function(a){this.Draw=this.fimage?a.ie>7?this.DrawImageIE:this.DrawImage:this.DrawText,a.noSelect&&(this.CheckActive=x)},d.MeasureText=function(d){var a,e=this.text.length,b=0,c;for(a=0;a0?c=H(c,this.oimage.width,this.oimage.height):this.oimage=H(this.oimage,c.width,c.height)),c&&(this.fimage=c,l=this.fimage.width/b,j=this.fimage.height/b),this.SetDraw(a),a.txtOpt=!!this.fimage),this.h=j,this.w=l},d.SetFont=function(a,b,c,d){this.textFont=a,this.colour=b,this.bgColour=c,this.bgOutline=d,this.Measure(this.tc.ctxt,this.tc)},d.SetWeight=function(c){var b=this.tc,e=b.weightMode.split(/[, ]/),d,a,f=c.length;if(!this.HasText())return;this.weighted=!0;for(a=0;a0&&a.weightSizeMax>a.weightSizeMin?this.textHeight=a.weightSize*(a.weightSizeMin+(a.weightSizeMax-a.weightSizeMin)*c):this.textHeight=g(1,b*a.weightSize))},d.SetShadowColourFixed=function(a,b,c){a.shadowColor=b},d.SetShadowColourAlpha=function(a,b,c){a.shadowColor=aE(b,c)},d.DrawText=function(a,h,i){var e=this.tc,g=this.x,f=this.y,c=this.sc,b,d;a.globalAlpha=this.alpha,a.fillStyle=this.colour,e.shadow&&this.SetShadowColour(a,e.shadow,this.alpha),a.font=this.font,g+=h/c,f+=i/c-this.h/2;for(b=0;b{this.stopped?this.audio.pause():this.playing=1}),1}};function a(f,o,k){var d,i,b=c.getElementById(f),l=['id','class','innerHTML'];if(!b)throw 0;if(n(window.G_vmlCanvasManager)&&(b=window.G_vmlCanvasManager.initElement(b),this.ie=parseFloat(navigator.appVersion.split('MSIE')[1])),b&&(!b.getContext||!b.getContext('2d').fillText)){i=c.createElement('DIV');for(d=0;d0?a.scrollPause=~~this.scrollPause:this.scrollPause=0,this.minTags>0&&this.repeatTags<1&&(d=this.GetTags().length)&&(this.repeatTags=af(this.minTags/d)-1),this.transform=m.Identity(),this.startTime=this.time=q(),this.mx=this.my=-1,this.centreImage&&av(this),this.Animate=this.dragControl?this.AnimateDrag:this.AnimatePosition,this.animTiming=typeof a[this.animTiming]=='function'?a[this.animTiming]:a.Smooth,this.shadowBlur||this.shadowOffset[0]||this.shadowOffset[1]?(this.ctxt.shadowColor=this.shadow,this.shadow=this.ctxt.shadowColor,this.shadowAlpha=aD()):delete this.shadow,this.activeAudio===!1?e='off':this.activeAudio&&this.LoadAudio(),this.Load(),o&&this.hideTags&&function(b){a.loaded?b.HideTags():t('load',function(){b.HideTags()},window)}(this),this.yaw=this.initial?this.initial[0]*this.maxSpeed:0,this.pitch=this.initial?this.initial[1]*this.maxSpeed:0,this.tooltip?(this.ctitle=b.title,b.title='',this.tooltip=='native'?this.Tooltip=this.TooltipNative:(this.Tooltip=this.TooltipDiv,this.ttdiv||(this.ttdiv=c.createElement('div'),this.ttdiv.className=this.tooltipClass,this.ttdiv.style.position='absolute',this.ttdiv.style.zIndex=b.style.zIndex+1,t('mouseover',function(a){a.target.style.display='none'},this.ttdiv),c.body.appendChild(this.ttdiv)))):this.Tooltip=this.TooltipNone,!this.noMouse&&!j[f]){j[f]=[['mousemove',ad],['mouseout',an],['mouseup',aq],['touchstart',ar],['touchend',ac],['touchcancel',ac],['touchmove',au]],this.dragControl&&(j[f].push(['mousedown',ap]),j[f].push(['selectstart',x])),this.wheelZoom&&(j[f].push(['mousewheel',ab]),j[f].push(['DOMMouseScroll',ab])),this.scrollPause&&j[f].push(['scroll',aw,window]);for(d=0;dthis.max_weight[a])&&(this.max_weight[a]=c),(!this.min_weight[a]||cthis.min_weight[a]&&(g=1);if(g)for(b=0;b=d&&this.my>=e)return!0},b.ToggleAudio=function(){var a=this.audioOff||e&&e.state==='suspended';a||this.currentAudio&&this.currentAudio.StopAudio(),this.audioOff=!a},b.Draw=function(s){if(this.paused)return;var l=this.canvas,i=l.width,j=l.height,q=0,p=(s-this.time)*a.interval/1e3,h=i/2+this.offsetX,g=j/2+this.offsetY,d=this.ctxt,b,f,c,o=-1,e=this.taglist,k=e.length,t=this.active&&this.active.tag,m='',u=this.frontSelect,r=this.centreFunc==x,n;if(this.time=s,this.frozen&&this.drawn)return this.Animate(i,j,p);n=this.AnimateFixed(),d.setTransform(1,0,0,1,0,0);for(c=0;c=0&&this.my>=0&&this.taglist[c].CheckActive(d,h,g),f&&f.sc>q&&(!u||f.z<=0)&&(b=f,o=c,b.tag=this.taglist[c],q=f.sc);this.active=b}this.txtOpt||this.shadow&&this.SetShadow(d),d.clearRect(0,0,i,j);for(c=0;c=this.fadeIn?(this.fadeIn=0,this.fixedAlpha=1):this.fixedAlpha=b/this.fadeIn),this.fixedAnim)&&(this.fixedAnim.transform||(this.fixedAnim.transform=this.transform),a=this.fixedAnim,b=q()-a.t0,c=a.angle,d,e=this.animTiming(a.t,b),this.transform=a.transform,b>=a.t?(this.fixedCallbackTag=a.tag,this.fixedCallback=a.cb,this.fixedAnim=this.yaw=this.pitch=0):c*=e,d=m.Rotation(c,a.axis),this.transform=this.transform.mul(d),this.fixedAnim!=0)},b.AnimatePosition=function(g,h,f){var a=this,d=a.mx,e=a.my,b,c;!a.frozen&&d>=0&&e>=0&&db&&(a.yaw=c>a.z0?a.yaw*a.decel:0),!a.ly&&d>b&&(a.pitch=d>a.z0?a.pitch*a.decel:0)},b.Zoom=function(a){this.z2=this.z1*(1/a),this.drawn=0},b.Clicked=function(b){if(this.CheckAudioIcon()){this.ToggleAudio();return}var a=this.active;try{a&&a.tag&&(this.clickToFront===!1||this.clickToFront===null?a.tag.Clicked(b):this.TagToFront(a.tag,this.clickToFront,function(){a.tag.Clicked(b)},!0))}catch(a){}},b.Wheel=function(a){var b=this.zoom+this.zoomStep*(a?1:-1);this.zoom=h(this.zoomMax,g(this.zoomMin,b)),this.Zoom(this.zoom)},b.BeginDrag=function(a){this.down=K(a,this.canvas),a.cancelBubble=!0,a.returnValue=!1,a.preventDefault&&a.preventDefault()},b.Drag=function(e,a){if(this.dragControl&&this.down){var d=this.dragThreshold*this.dragThreshold,b=a.x-this.down.x,c=a.y-this.down.y;(this.dragging||b*b+c*c>d)&&(this.dx=b,this.dy=c,this.dragging=1,this.down=a)}return this.dragging},b.EndDrag=function(){var a=this.dragging;return this.dragging=this.down=null,a};function ah(a){var b=a.targetTouches[0],c=a.targetTouches[1];return E(w(c.pageX-b.pageX,2)+w(c.pageY-b.pageY,2))}b.BeginPinch=function(a){this.pinched=[ah(a),this.zoom],a.preventDefault&&a.preventDefault()},b.Pinch=function(d){var b,c,a=this.pinched;if(!a)return;c=ah(d),b=a[1]*c/a[0],this.zoom=h(this.zoomMax,g(this.zoomMin,b)),this.Zoom(this.zoom)},b.EndPinch=function(a){this.pinched=null},b.Pause=function(){this.paused=!0},b.Resume=function(){this.paused=!1},b.SetSpeed=function(a){this.initial=a,this.yaw=a[0]*this.maxSpeed,this.pitch=a[1]*this.maxSpeed},b.FindTag=function(a){if(!n(a))return null;if(n(a.index)&&(a=a.index),!B(a))return this.taglist[a];var c,d,b;n(a.id)?(c='id',d=a.id):n(a.text)&&(c='innerText',d=a.text);for(b=0;b - - - - - diff --git a/packages/client/package.json b/packages/client/package.json deleted file mode 100644 index 0af8ffac0b..0000000000 --- a/packages/client/package.json +++ /dev/null @@ -1,94 +0,0 @@ -{ - "name": "client", - "private": true, - "scripts": { - "watch": "vite", - "build": "vite build", - "lint": "vue-tsc --noEmit && eslint --quiet \"src/**/*.{ts,vue}\"" - }, - "dependencies": { - "@discordapp/twemoji": "14.0.2", - "@rollup/plugin-alias": "4.0.2", - "@rollup/plugin-json": "6.0.0", - "@rollup/pluginutils": "5.0.2", - "@syuilo/aiscript": "0.11.1", - "@tabler/icons": "^1.118.0", - "@vitejs/plugin-vue": "4.0.0", - "@vue/compiler-sfc": "3.2.45", - "autobind-decorator": "2.4.0", - "autosize": "5.0.2", - "blurhash": "2.0.4", - "broadcast-channel": "4.18.1", - "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", - "chart.js": "4.1.1", - "chartjs-adapter-date-fns": "3.0.0", - "chartjs-chart-matrix": "^1.3.0", - "chartjs-plugin-gradient": "0.6.1", - "chartjs-plugin-zoom": "2.0.0", - "compare-versions": "5.0.1", - "cropperjs": "2.0.0-beta", - "date-fns": "2.29.3", - "escape-regexp": "0.0.1", - "eventemitter3": "5.0.0", - "idb-keyval": "6.2.0", - "insert-text-at-cursor": "0.3.0", - "is-file-animated": "1.0.2", - "json5": "2.2.2", - "katex": "0.15.6", - "matter-js": "0.18.0", - "mfm-js": "0.23.0", - "misskey-js": "0.0.14", - "photoswipe": "5.3.4", - "prismjs": "1.29.0", - "punycode": "2.1.1", - "querystring": "0.2.1", - "rndstr": "1.0.0", - "rollup": "3.8.0", - "s-age": "1.1.2", - "sass": "1.57.1", - "seedrandom": "3.0.5", - "strict-event-emitter-types": "2.0.0", - "stringz": "2.1.0", - "syuilo-password-strength": "0.0.1", - "textarea-caret": "3.1.0", - "three": "0.148.0", - "throttle-debounce": "5.0.0", - "tinycolor2": "1.4.2", - "tsc-alias": "1.8.2", - "tsconfig-paths": "4.1.1", - "twemoji-parser": "14.0.0", - "typescript": "4.9.4", - "uuid": "9.0.0", - "vanilla-tilt": "1.8.0", - "vite": "4.0.3", - "vue": "3.2.45", - "vue-prism-editor": "2.0.0-alpha.2", - "vuedraggable": "next" - }, - "devDependencies": { - "@types/escape-regexp": "0.0.1", - "@types/glob": "8.0.0", - "@types/gulp": "4.0.10", - "@types/gulp-rename": "2.0.1", - "@types/katex": "0.14.0", - "@types/matter-js": "0.18.2", - "@types/punycode": "2.1.0", - "@types/seedrandom": "3.0.3", - "@types/throttle-debounce": "5.0.0", - "@types/tinycolor2": "1.4.3", - "@types/uuid": "9.0.0", - "@types/websocket": "1.0.5", - "@types/ws": "8.5.3", - "@typescript-eslint/eslint-plugin": "5.47.0", - "@typescript-eslint/parser": "5.47.0", - "@vue/runtime-core": "3.2.45", - "cross-env": "7.0.3", - "cypress": "12.2.0", - "eslint": "8.30.0", - "eslint-plugin-import": "2.26.0", - "eslint-plugin-vue": "9.8.0", - "start-server-and-test": "1.15.2", - "vue-eslint-parser": "^9.1.0", - "vue-tsc": "^1.0.16" - } -} diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts deleted file mode 100644 index 0e991cdfb5..0000000000 --- a/packages/client/src/account.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { defineAsyncComponent, reactive } from 'vue'; -import * as misskey from 'misskey-js'; -import { showSuspendedDialog } from './scripts/show-suspended-dialog'; -import { i18n } from './i18n'; -import { del, get, set } from '@/scripts/idb-proxy'; -import { apiUrl } from '@/config'; -import { waiting, api, popup, popupMenu, success, alert } from '@/os'; -import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; - -// TODO: 他のタブと永続化されたstateを同期 - -type Account = misskey.entities.MeDetailed; - -const accountData = localStorage.getItem('account'); - -// TODO: 外部からはreadonlyに -export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; - -export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); -export const iAmAdmin = $i != null && $i.isAdmin; - -export async function signout() { - waiting(); - localStorage.removeItem('account'); - - await removeAccount($i.id); - - const accounts = await getAccounts(); - - //#region Remove service worker registration - try { - if (navigator.serviceWorker.controller) { - const registration = await navigator.serviceWorker.ready; - const push = await registration.pushManager.getSubscription(); - if (push) { - await window.fetch(`${apiUrl}/sw/unregister`, { - method: 'POST', - body: JSON.stringify({ - i: $i.token, - endpoint: push.endpoint, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - } - } - - if (accounts.length === 0) { - await navigator.serviceWorker.getRegistrations() - .then(registrations => { - return Promise.all(registrations.map(registration => registration.unregister())); - }); - } - } catch (err) {} - //#endregion - - document.cookie = 'igi=; path=/'; - - if (accounts.length > 0) login(accounts[0].token); - else unisonReload('/'); -} - -export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> { - return (await get('accounts')) || []; -} - -export async function addAccount(id: Account['id'], token: Account['token']) { - const accounts = await getAccounts(); - if (!accounts.some(x => x.id === id)) { - await set('accounts', accounts.concat([{ id, token }])); - } -} - -export async function removeAccount(id: Account['id']) { - const accounts = await getAccounts(); - accounts.splice(accounts.findIndex(x => x.id === id), 1); - - if (accounts.length > 0) await set('accounts', accounts); - else await del('accounts'); -} - -function fetchAccount(token: string): Promise { - return new Promise((done, fail) => { - // Fetch user - window.fetch(`${apiUrl}/i`, { - method: 'POST', - body: JSON.stringify({ - i: token, - }), - headers: { - 'Content-Type': 'application/json', - }, - }) - .then(res => res.json()) - .then(res => { - if (res.error) { - if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { - showSuspendedDialog().then(() => { - signout(); - }); - } else { - alert({ - type: 'error', - title: i18n.ts.failedToFetchAccountInformation, - text: JSON.stringify(res.error), - }); - } - } else { - res.token = token; - done(res); - } - }) - .catch(fail); - }); -} - -export function updateAccount(accountData) { - for (const [key, value] of Object.entries(accountData)) { - $i[key] = value; - } - localStorage.setItem('account', JSON.stringify($i)); -} - -export function refreshAccount() { - return fetchAccount($i.token).then(updateAccount); -} - -export async function login(token: Account['token'], redirect?: string) { - waiting(); - if (_DEV_) console.log('logging as token ', token); - const me = await fetchAccount(token); - localStorage.setItem('account', JSON.stringify(me)); - document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う - await addAccount(me.id, token); - - if (redirect) { - // 他のタブは再読み込みするだけ - reloadChannel.postMessage(null); - // このページはredirectで指定された先に移動 - location.href = redirect; - return; - } - - unisonReload(); -} - -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(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { - done: res => { - addAccount(res.id, res.i); - success(); - }, - }, 'closed'); - } - - function createAccount() { - popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { - done: res => { - addAccount(res.id, res.i); - switchAccountWithToken(res.i); - }, - }, 'closed'); - } - - async function switchAccount(account: misskey.entities.UserDetailed) { - const storedAccounts = await getAccounts(); - const token = storedAccounts.find(x => x.id === account.id).token; - switchAccountWithToken(token); - } - - function switchAccountWithToken(token: string) { - login(token); - } - - const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id)); - const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) }); - - 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(createItem(account)); - }); - })); - - if (opts.withExtraOperation) { - popupMenu([...[{ - type: 'link', - text: i18n.ts.profile, - to: `/@${ $i.username }`, - avatar: $i, - }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { - type: 'parent', - icon: 'ti ti-plus', - text: i18n.ts.addAccount, - children: [{ - text: i18n.ts.existingAccount, - action: () => { showSigninDialog(); }, - }, { - text: i18n.ts.createAccount, - action: () => { createAccount(); }, - }], - }, { - type: 'link', - icon: 'ti ti-users', - text: i18n.ts.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/MkAbuseReport.vue b/packages/client/src/components/MkAbuseReport.vue deleted file mode 100644 index 9a3464b640..0000000000 --- a/packages/client/src/components/MkAbuseReport.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkAbuseReportWindow.vue b/packages/client/src/components/MkAbuseReportWindow.vue deleted file mode 100644 index 039f77c859..0000000000 --- a/packages/client/src/components/MkAbuseReportWindow.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkActiveUsersHeatmap.vue b/packages/client/src/components/MkActiveUsersHeatmap.vue deleted file mode 100644 index 02b2eeeb36..0000000000 --- a/packages/client/src/components/MkActiveUsersHeatmap.vue +++ /dev/null @@ -1,236 +0,0 @@ - - - diff --git a/packages/client/src/components/MkAnalogClock.vue b/packages/client/src/components/MkAnalogClock.vue deleted file mode 100644 index 40ef626aed..0000000000 --- a/packages/client/src/components/MkAnalogClock.vue +++ /dev/null @@ -1,225 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkAutocomplete.vue b/packages/client/src/components/MkAutocomplete.vue deleted file mode 100644 index 72783921d5..0000000000 --- a/packages/client/src/components/MkAutocomplete.vue +++ /dev/null @@ -1,476 +0,0 @@ - - - - - - - diff --git a/packages/client/src/components/MkAvatars.vue b/packages/client/src/components/MkAvatars.vue deleted file mode 100644 index 162338b639..0000000000 --- a/packages/client/src/components/MkAvatars.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/packages/client/src/components/MkButton.vue b/packages/client/src/components/MkButton.vue deleted file mode 100644 index 891645bb2a..0000000000 --- a/packages/client/src/components/MkButton.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkCaptcha.vue b/packages/client/src/components/MkCaptcha.vue deleted file mode 100644 index 6d218389fc..0000000000 --- a/packages/client/src/components/MkCaptcha.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - diff --git a/packages/client/src/components/MkChannelFollowButton.vue b/packages/client/src/components/MkChannelFollowButton.vue deleted file mode 100644 index 9e275d6172..0000000000 --- a/packages/client/src/components/MkChannelFollowButton.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkChannelPreview.vue b/packages/client/src/components/MkChannelPreview.vue deleted file mode 100644 index 6ef50bddcf..0000000000 --- a/packages/client/src/components/MkChannelPreview.vue +++ /dev/null @@ -1,154 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkChart.vue b/packages/client/src/components/MkChart.vue deleted file mode 100644 index fbbc231b88..0000000000 --- a/packages/client/src/components/MkChart.vue +++ /dev/null @@ -1,859 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkChartTooltip.vue b/packages/client/src/components/MkChartTooltip.vue deleted file mode 100644 index d36f45463c..0000000000 --- a/packages/client/src/components/MkChartTooltip.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkCode.core.vue b/packages/client/src/components/MkCode.core.vue deleted file mode 100644 index b074028821..0000000000 --- a/packages/client/src/components/MkCode.core.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - diff --git a/packages/client/src/components/MkCode.vue b/packages/client/src/components/MkCode.vue deleted file mode 100644 index 1640258d5b..0000000000 --- a/packages/client/src/components/MkCode.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/packages/client/src/components/MkContainer.vue b/packages/client/src/components/MkContainer.vue deleted file mode 100644 index 6d4d5be2bc..0000000000 --- a/packages/client/src/components/MkContainer.vue +++ /dev/null @@ -1,275 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkContextMenu.vue b/packages/client/src/components/MkContextMenu.vue deleted file mode 100644 index cfc9502b41..0000000000 --- a/packages/client/src/components/MkContextMenu.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkCropperDialog.vue b/packages/client/src/components/MkCropperDialog.vue deleted file mode 100644 index ae18160dea..0000000000 --- a/packages/client/src/components/MkCropperDialog.vue +++ /dev/null @@ -1,174 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkCwButton.vue b/packages/client/src/components/MkCwButton.vue deleted file mode 100644 index ee611921ef..0000000000 --- a/packages/client/src/components/MkCwButton.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkDateSeparatedList.vue b/packages/client/src/components/MkDateSeparatedList.vue deleted file mode 100644 index 1f88bdf137..0000000000 --- a/packages/client/src/components/MkDateSeparatedList.vue +++ /dev/null @@ -1,189 +0,0 @@ - - - diff --git a/packages/client/src/components/MkDialog.vue b/packages/client/src/components/MkDialog.vue deleted file mode 100644 index 374ecd8abf..0000000000 --- a/packages/client/src/components/MkDialog.vue +++ /dev/null @@ -1,208 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkDigitalClock.vue b/packages/client/src/components/MkDigitalClock.vue deleted file mode 100644 index 9ed8d63d19..0000000000 --- a/packages/client/src/components/MkDigitalClock.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkDrive.file.vue b/packages/client/src/components/MkDrive.file.vue deleted file mode 100644 index 8c17c0530a..0000000000 --- a/packages/client/src/components/MkDrive.file.vue +++ /dev/null @@ -1,334 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkDrive.folder.vue b/packages/client/src/components/MkDrive.folder.vue deleted file mode 100644 index 82653ca0b4..0000000000 --- a/packages/client/src/components/MkDrive.folder.vue +++ /dev/null @@ -1,330 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkDrive.navFolder.vue b/packages/client/src/components/MkDrive.navFolder.vue deleted file mode 100644 index dbbfef5f05..0000000000 --- a/packages/client/src/components/MkDrive.navFolder.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkDrive.vue b/packages/client/src/components/MkDrive.vue deleted file mode 100644 index 4053870950..0000000000 --- a/packages/client/src/components/MkDrive.vue +++ /dev/null @@ -1,801 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkDriveFileThumbnail.vue b/packages/client/src/components/MkDriveFileThumbnail.vue deleted file mode 100644 index 33379ed5ca..0000000000 --- a/packages/client/src/components/MkDriveFileThumbnail.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkDriveSelectDialog.vue b/packages/client/src/components/MkDriveSelectDialog.vue deleted file mode 100644 index 3ee821b539..0000000000 --- a/packages/client/src/components/MkDriveSelectDialog.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - diff --git a/packages/client/src/components/MkDriveWindow.vue b/packages/client/src/components/MkDriveWindow.vue deleted file mode 100644 index 617200321b..0000000000 --- a/packages/client/src/components/MkDriveWindow.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/packages/client/src/components/MkEmojiPicker.section.vue b/packages/client/src/components/MkEmojiPicker.section.vue deleted file mode 100644 index f6ba7abfc4..0000000000 --- a/packages/client/src/components/MkEmojiPicker.section.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkEmojiPicker.vue b/packages/client/src/components/MkEmojiPicker.vue deleted file mode 100644 index 814f71168a..0000000000 --- a/packages/client/src/components/MkEmojiPicker.vue +++ /dev/null @@ -1,569 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkEmojiPickerDialog.vue b/packages/client/src/components/MkEmojiPickerDialog.vue deleted file mode 100644 index 3b41f9d75b..0000000000 --- a/packages/client/src/components/MkEmojiPickerDialog.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkEmojiPickerWindow.vue b/packages/client/src/components/MkEmojiPickerWindow.vue deleted file mode 100644 index 523e4ba695..0000000000 --- a/packages/client/src/components/MkEmojiPickerWindow.vue +++ /dev/null @@ -1,180 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkFeaturedPhotos.vue b/packages/client/src/components/MkFeaturedPhotos.vue deleted file mode 100644 index e58b5d2849..0000000000 --- a/packages/client/src/components/MkFeaturedPhotos.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkFileCaptionEditWindow.vue b/packages/client/src/components/MkFileCaptionEditWindow.vue deleted file mode 100644 index 73875251f0..0000000000 --- a/packages/client/src/components/MkFileCaptionEditWindow.vue +++ /dev/null @@ -1,175 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkFileListForAdmin.vue b/packages/client/src/components/MkFileListForAdmin.vue deleted file mode 100644 index 4910506a95..0000000000 --- a/packages/client/src/components/MkFileListForAdmin.vue +++ /dev/null @@ -1,117 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkFolder.vue b/packages/client/src/components/MkFolder.vue deleted file mode 100644 index 9e83b07cd7..0000000000 --- a/packages/client/src/components/MkFolder.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkFollowButton.vue b/packages/client/src/components/MkFollowButton.vue deleted file mode 100644 index ee256d9263..0000000000 --- a/packages/client/src/components/MkFollowButton.vue +++ /dev/null @@ -1,187 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkForgotPassword.vue b/packages/client/src/components/MkForgotPassword.vue deleted file mode 100644 index 1b55451c94..0000000000 --- a/packages/client/src/components/MkForgotPassword.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkFormDialog.vue b/packages/client/src/components/MkFormDialog.vue deleted file mode 100644 index b2bf76a8c7..0000000000 --- a/packages/client/src/components/MkFormDialog.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkFormula.vue b/packages/client/src/components/MkFormula.vue deleted file mode 100644 index 65a2fee930..0000000000 --- a/packages/client/src/components/MkFormula.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/packages/client/src/components/MkFormulaCore.vue b/packages/client/src/components/MkFormulaCore.vue deleted file mode 100644 index 6028db9e64..0000000000 --- a/packages/client/src/components/MkFormulaCore.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - diff --git a/packages/client/src/components/MkGalleryPostPreview.vue b/packages/client/src/components/MkGalleryPostPreview.vue deleted file mode 100644 index a133f6431b..0000000000 --- a/packages/client/src/components/MkGalleryPostPreview.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkGoogle.vue b/packages/client/src/components/MkGoogle.vue deleted file mode 100644 index d104cd4cd4..0000000000 --- a/packages/client/src/components/MkGoogle.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkImageViewer.vue b/packages/client/src/components/MkImageViewer.vue deleted file mode 100644 index f074b1a2f2..0000000000 --- a/packages/client/src/components/MkImageViewer.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkImgWithBlurhash.vue b/packages/client/src/components/MkImgWithBlurhash.vue deleted file mode 100644 index 80d7c201a4..0000000000 --- a/packages/client/src/components/MkImgWithBlurhash.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkInfo.vue b/packages/client/src/components/MkInfo.vue deleted file mode 100644 index 7aaf2c5bcb..0000000000 --- a/packages/client/src/components/MkInfo.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkInstanceCardMini.vue b/packages/client/src/components/MkInstanceCardMini.vue deleted file mode 100644 index 4625de40af..0000000000 --- a/packages/client/src/components/MkInstanceCardMini.vue +++ /dev/null @@ -1,105 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkInstanceStats.vue b/packages/client/src/components/MkInstanceStats.vue deleted file mode 100644 index 41f6f9ffd5..0000000000 --- a/packages/client/src/components/MkInstanceStats.vue +++ /dev/null @@ -1,255 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue deleted file mode 100644 index 646172fe8d..0000000000 --- a/packages/client/src/components/MkInstanceTicker.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkKeyValue.vue b/packages/client/src/components/MkKeyValue.vue deleted file mode 100644 index ff69c79641..0000000000 --- a/packages/client/src/components/MkKeyValue.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkLaunchPad.vue b/packages/client/src/components/MkLaunchPad.vue deleted file mode 100644 index 1ccc648c72..0000000000 --- a/packages/client/src/components/MkLaunchPad.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkLink.vue b/packages/client/src/components/MkLink.vue deleted file mode 100644 index 6148ec6195..0000000000 --- a/packages/client/src/components/MkLink.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkMarquee.vue b/packages/client/src/components/MkMarquee.vue deleted file mode 100644 index 5ca04b0b48..0000000000 --- a/packages/client/src/components/MkMarquee.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - diff --git a/packages/client/src/components/MkMediaBanner.vue b/packages/client/src/components/MkMediaBanner.vue deleted file mode 100644 index aa06c00fc6..0000000000 --- a/packages/client/src/components/MkMediaBanner.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkMediaImage.vue b/packages/client/src/components/MkMediaImage.vue deleted file mode 100644 index 56570eaa05..0000000000 --- a/packages/client/src/components/MkMediaImage.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkMediaList.vue b/packages/client/src/components/MkMediaList.vue deleted file mode 100644 index c6f8612182..0000000000 --- a/packages/client/src/components/MkMediaList.vue +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - diff --git a/packages/client/src/components/MkMediaVideo.vue b/packages/client/src/components/MkMediaVideo.vue deleted file mode 100644 index df0bf84116..0000000000 --- a/packages/client/src/components/MkMediaVideo.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkMention.vue b/packages/client/src/components/MkMention.vue deleted file mode 100644 index 3091b435e4..0000000000 --- a/packages/client/src/components/MkMention.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkMenu.child.vue b/packages/client/src/components/MkMenu.child.vue deleted file mode 100644 index 3ada4afbdc..0000000000 --- a/packages/client/src/components/MkMenu.child.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkMenu.vue b/packages/client/src/components/MkMenu.vue deleted file mode 100644 index 64d18b6b7c..0000000000 --- a/packages/client/src/components/MkMenu.vue +++ /dev/null @@ -1,367 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkMiniChart.vue b/packages/client/src/components/MkMiniChart.vue deleted file mode 100644 index c64ce163f9..0000000000 --- a/packages/client/src/components/MkMiniChart.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - diff --git a/packages/client/src/components/MkModal.vue b/packages/client/src/components/MkModal.vue deleted file mode 100644 index 2305a02794..0000000000 --- a/packages/client/src/components/MkModal.vue +++ /dev/null @@ -1,406 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkModalPageWindow.vue b/packages/client/src/components/MkModalPageWindow.vue deleted file mode 100644 index ced8a7a714..0000000000 --- a/packages/client/src/components/MkModalPageWindow.vue +++ /dev/null @@ -1,181 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkModalWindow.vue b/packages/client/src/components/MkModalWindow.vue deleted file mode 100644 index d977ca6e9c..0000000000 --- a/packages/client/src/components/MkModalWindow.vue +++ /dev/null @@ -1,146 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue deleted file mode 100644 index a4100e1f2c..0000000000 --- a/packages/client/src/components/MkNote.vue +++ /dev/null @@ -1,658 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue deleted file mode 100644 index 7ce8e039d9..0000000000 --- a/packages/client/src/components/MkNoteDetailed.vue +++ /dev/null @@ -1,677 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNoteHeader.vue b/packages/client/src/components/MkNoteHeader.vue deleted file mode 100644 index 333c3ddbd9..0000000000 --- a/packages/client/src/components/MkNoteHeader.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNotePreview.vue b/packages/client/src/components/MkNotePreview.vue deleted file mode 100644 index 0c81059091..0000000000 --- a/packages/client/src/components/MkNotePreview.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNoteSimple.vue b/packages/client/src/components/MkNoteSimple.vue deleted file mode 100644 index 96d29831d2..0000000000 --- a/packages/client/src/components/MkNoteSimple.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue deleted file mode 100644 index d03ce7c434..0000000000 --- a/packages/client/src/components/MkNoteSub.vue +++ /dev/null @@ -1,140 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNotes.vue b/packages/client/src/components/MkNotes.vue deleted file mode 100644 index 5abcdc2298..0000000000 --- a/packages/client/src/components/MkNotes.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNotification.vue b/packages/client/src/components/MkNotification.vue deleted file mode 100644 index 8b8d3f452d..0000000000 --- a/packages/client/src/components/MkNotification.vue +++ /dev/null @@ -1,323 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNotificationSettingWindow.vue b/packages/client/src/components/MkNotificationSettingWindow.vue deleted file mode 100644 index 75bea2976c..0000000000 --- a/packages/client/src/components/MkNotificationSettingWindow.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/packages/client/src/components/MkNotificationToast.vue b/packages/client/src/components/MkNotificationToast.vue deleted file mode 100644 index 07640792c0..0000000000 --- a/packages/client/src/components/MkNotificationToast.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNotifications.vue b/packages/client/src/components/MkNotifications.vue deleted file mode 100644 index 0e1cc06743..0000000000 --- a/packages/client/src/components/MkNotifications.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkNumberDiff.vue b/packages/client/src/components/MkNumberDiff.vue deleted file mode 100644 index e7d4a5472a..0000000000 --- a/packages/client/src/components/MkNumberDiff.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkObjectView.value.vue b/packages/client/src/components/MkObjectView.value.vue deleted file mode 100644 index 0c7230d783..0000000000 --- a/packages/client/src/components/MkObjectView.value.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkObjectView.vue b/packages/client/src/components/MkObjectView.vue deleted file mode 100644 index 55578a37f6..0000000000 --- a/packages/client/src/components/MkObjectView.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkPagePreview.vue b/packages/client/src/components/MkPagePreview.vue deleted file mode 100644 index 009582e540..0000000000 --- a/packages/client/src/components/MkPagePreview.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkPageWindow.vue b/packages/client/src/components/MkPageWindow.vue deleted file mode 100644 index 29d45558a7..0000000000 --- a/packages/client/src/components/MkPageWindow.vue +++ /dev/null @@ -1,140 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkPagination.vue b/packages/client/src/components/MkPagination.vue deleted file mode 100644 index 291409171a..0000000000 --- a/packages/client/src/components/MkPagination.vue +++ /dev/null @@ -1,317 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkPoll.vue b/packages/client/src/components/MkPoll.vue deleted file mode 100644 index a1b927e42a..0000000000 --- a/packages/client/src/components/MkPoll.vue +++ /dev/null @@ -1,152 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkPollEditor.vue b/packages/client/src/components/MkPollEditor.vue deleted file mode 100644 index 556abc5fd0..0000000000 --- a/packages/client/src/components/MkPollEditor.vue +++ /dev/null @@ -1,219 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkPopupMenu.vue b/packages/client/src/components/MkPopupMenu.vue deleted file mode 100644 index f04c7f5618..0000000000 --- a/packages/client/src/components/MkPopupMenu.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - diff --git a/packages/client/src/components/MkPostForm.vue b/packages/client/src/components/MkPostForm.vue deleted file mode 100644 index f79e5a32cd..0000000000 --- a/packages/client/src/components/MkPostForm.vue +++ /dev/null @@ -1,1050 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkA.vue b/packages/client/src/components/global/MkA.vue deleted file mode 100644 index 5a0ba0d8d3..0000000000 --- a/packages/client/src/components/global/MkA.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - diff --git a/packages/client/src/components/global/MkAcct.vue b/packages/client/src/components/global/MkAcct.vue deleted file mode 100644 index c3e806b5fb..0000000000 --- a/packages/client/src/components/global/MkAcct.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkAd.vue b/packages/client/src/components/global/MkAd.vue deleted file mode 100644 index a80efb142c..0000000000 --- a/packages/client/src/components/global/MkAd.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkAvatar.vue b/packages/client/src/components/global/MkAvatar.vue deleted file mode 100644 index 5f3e3c176d..0000000000 --- a/packages/client/src/components/global/MkAvatar.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkEllipsis.vue b/packages/client/src/components/global/MkEllipsis.vue deleted file mode 100644 index 0a46f486d6..0000000000 --- a/packages/client/src/components/global/MkEllipsis.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - diff --git a/packages/client/src/components/global/MkEmoji.vue b/packages/client/src/components/global/MkEmoji.vue deleted file mode 100644 index ce1299a39f..0000000000 --- a/packages/client/src/components/global/MkEmoji.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkError.vue b/packages/client/src/components/global/MkError.vue deleted file mode 100644 index e135d4184b..0000000000 --- a/packages/client/src/components/global/MkError.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkLoading.vue b/packages/client/src/components/global/MkLoading.vue deleted file mode 100644 index 64e12e3b44..0000000000 --- a/packages/client/src/components/global/MkLoading.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue deleted file mode 100644 index 70d0108e9f..0000000000 --- a/packages/client/src/components/global/MkMisskeyFlavoredMarkdown.vue +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - diff --git a/packages/client/src/components/global/MkPageHeader.vue b/packages/client/src/components/global/MkPageHeader.vue deleted file mode 100644 index a228dfe883..0000000000 --- a/packages/client/src/components/global/MkPageHeader.vue +++ /dev/null @@ -1,368 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkSpacer.vue b/packages/client/src/components/global/MkSpacer.vue deleted file mode 100644 index b3a42d77e7..0000000000 --- a/packages/client/src/components/global/MkSpacer.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkStickyContainer.vue b/packages/client/src/components/global/MkStickyContainer.vue deleted file mode 100644 index 44f4f065a6..0000000000 --- a/packages/client/src/components/global/MkStickyContainer.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - diff --git a/packages/client/src/components/global/MkTime.vue b/packages/client/src/components/global/MkTime.vue deleted file mode 100644 index f72b153f56..0000000000 --- a/packages/client/src/components/global/MkTime.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/packages/client/src/components/global/MkUrl.vue b/packages/client/src/components/global/MkUrl.vue deleted file mode 100644 index 9f5be96224..0000000000 --- a/packages/client/src/components/global/MkUrl.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/packages/client/src/components/global/MkUserName.vue b/packages/client/src/components/global/MkUserName.vue deleted file mode 100644 index 090de3df30..0000000000 --- a/packages/client/src/components/global/MkUserName.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/packages/client/src/components/global/RouterView.vue b/packages/client/src/components/global/RouterView.vue deleted file mode 100644 index e21a57471c..0000000000 --- a/packages/client/src/components/global/RouterView.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/packages/client/src/components/global/i18n.ts b/packages/client/src/components/global/i18n.ts deleted file mode 100644 index 1fd293ba10..0000000000 --- a/packages/client/src/components/global/i18n.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { h, defineComponent } from 'vue'; - -export default defineComponent({ - props: { - src: { - type: String, - required: true, - }, - tag: { - type: String, - required: false, - default: 'span', - }, - textTag: { - type: String, - required: false, - default: null, - }, - }, - render() { - let str = this.src; - const parsed = [] as (string | { arg: string; })[]; - while (true) { - const nextBracketOpen = str.indexOf('{'); - const nextBracketClose = str.indexOf('}'); - - if (nextBracketOpen === -1) { - parsed.push(str); - break; - } else { - if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen)); - parsed.push({ - arg: str.substring(nextBracketOpen + 1, nextBracketClose), - }); - } - - str = str.substr(nextBracketClose + 1); - } - - return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]())); - }, -}); diff --git a/packages/client/src/components/index.ts b/packages/client/src/components/index.ts deleted file mode 100644 index 8639257003..0000000000 --- a/packages/client/src/components/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { App } from 'vue'; - -import Mfm from './global/MkMisskeyFlavoredMarkdown.vue'; -import MkA from './global/MkA.vue'; -import MkAcct from './global/MkAcct.vue'; -import MkAvatar from './global/MkAvatar.vue'; -import MkEmoji from './global/MkEmoji.vue'; -import MkUserName from './global/MkUserName.vue'; -import MkEllipsis from './global/MkEllipsis.vue'; -import MkTime from './global/MkTime.vue'; -import MkUrl from './global/MkUrl.vue'; -import I18n from './global/i18n'; -import RouterView from './global/RouterView.vue'; -import MkLoading from './global/MkLoading.vue'; -import MkError from './global/MkError.vue'; -import MkAd from './global/MkAd.vue'; -import MkPageHeader from './global/MkPageHeader.vue'; -import MkSpacer from './global/MkSpacer.vue'; -import MkStickyContainer from './global/MkStickyContainer.vue'; - -export default function(app: App) { - app.component('I18n', I18n); - app.component('RouterView', RouterView); - app.component('Mfm', Mfm); - app.component('MkA', MkA); - app.component('MkAcct', MkAcct); - app.component('MkAvatar', MkAvatar); - app.component('MkEmoji', MkEmoji); - app.component('MkUserName', MkUserName); - app.component('MkEllipsis', MkEllipsis); - app.component('MkTime', MkTime); - app.component('MkUrl', MkUrl); - app.component('MkLoading', MkLoading); - app.component('MkError', MkError); - app.component('MkAd', MkAd); - app.component('MkPageHeader', MkPageHeader); - app.component('MkSpacer', MkSpacer); - app.component('MkStickyContainer', MkStickyContainer); -} - -declare module '@vue/runtime-core' { - export interface GlobalComponents { - I18n: typeof I18n; - RouterView: typeof RouterView; - Mfm: typeof Mfm; - MkA: typeof MkA; - MkAcct: typeof MkAcct; - MkAvatar: typeof MkAvatar; - MkEmoji: typeof MkEmoji; - MkUserName: typeof MkUserName; - MkEllipsis: typeof MkEllipsis; - MkTime: typeof MkTime; - MkUrl: typeof MkUrl; - MkLoading: typeof MkLoading; - MkError: typeof MkError; - MkAd: typeof MkAd; - MkPageHeader: typeof MkPageHeader; - MkSpacer: typeof MkSpacer; - MkStickyContainer: typeof MkStickyContainer; - } -} diff --git a/packages/client/src/components/mfm.ts b/packages/client/src/components/mfm.ts deleted file mode 100644 index 5b5b1caae3..0000000000 --- a/packages/client/src/components/mfm.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { VNode, defineComponent, h } from 'vue'; -import * as mfm from 'mfm-js'; -import MkUrl from '@/components/global/MkUrl.vue'; -import MkLink from '@/components/MkLink.vue'; -import MkMention from '@/components/MkMention.vue'; -import MkEmoji from '@/components/global/MkEmoji.vue'; -import { concat } from '@/scripts/array'; -import MkFormula from '@/components/MkFormula.vue'; -import MkCode from '@/components/MkCode.vue'; -import MkGoogle from '@/components/MkGoogle.vue'; -import MkSparkle from '@/components/MkSparkle.vue'; -import MkA from '@/components/global/MkA.vue'; -import { host } from '@/config'; -import { MFM_TAGS } from '@/scripts/mfm-tags'; - -export default defineComponent({ - props: { - text: { - type: String, - required: true, - }, - plain: { - type: Boolean, - default: false, - }, - nowrap: { - type: Boolean, - default: false, - }, - author: { - type: Object, - default: null, - }, - i: { - type: Object, - default: null, - }, - customEmojis: { - required: false, - }, - isNote: { - type: Boolean, - default: true, - }, - }, - - render() { - if (this.text == null || this.text === '') return; - - const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text, { fnNameList: MFM_TAGS }); - - const validTime = (t: string | null | undefined) => { - if (t == null) return null; - return t.match(/^[0-9.]+s$/) ? t : null; - }; - - const genEl = (ast: mfm.MfmNode[]) => ast.map((token): VNode | string | (VNode | string)[] => { - switch (token.type) { - case 'text': { - const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); - - if (!this.plain) { - const res: (VNode | string)[] = []; - for (const t of text.split('\n')) { - res.push(h('br')); - res.push(t); - } - res.shift(); - return res; - } else { - return [text.replace(/\n/g, ' ')]; - } - } - - case 'bold': { - return [h('b', genEl(token.children))]; - } - - case 'strike': { - return [h('del', genEl(token.children))]; - } - - case 'italic': { - return h('i', { - style: 'font-style: oblique;', - }, genEl(token.children)); - } - - case 'fn': { - // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる - let style; - switch (token.props.name) { - case 'tada': { - const speed = validTime(token.props.args.speed) || '1s'; - style = 'font-size: 150%;' + (this.$store.state.animatedMfm ? `animation: tada ${speed} linear infinite both;` : ''); - break; - } - case 'jelly': { - const speed = validTime(token.props.args.speed) || '1s'; - style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); - break; - } - case 'twitch': { - const speed = validTime(token.props.args.speed) || '0.5s'; - style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : ''; - break; - } - case 'shake': { - const speed = validTime(token.props.args.speed) || '0.5s'; - style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : ''; - break; - } - case 'spin': { - const direction = - token.props.args.left ? 'reverse' : - token.props.args.alternate ? 'alternate' : - 'normal'; - const anime = - token.props.args.x ? 'mfm-spinX' : - token.props.args.y ? 'mfm-spinY' : - 'mfm-spin'; - const speed = validTime(token.props.args.speed) || '1.5s'; - style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; - break; - } - case 'jump': { - const speed = validTime(token.props.args.speed) || '0.75s'; - style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : ''; - break; - } - case 'bounce': { - const speed = validTime(token.props.args.speed) || '0.75s'; - style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; - break; - } - case 'flip': { - const transform = - (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : - token.props.args.v ? 'scaleY(-1)' : - 'scaleX(-1)'; - style = `transform: ${transform};`; - break; - } - case 'x2': { - return h('span', { - class: 'mfm-x2', - }, genEl(token.children)); - } - case 'x3': { - return h('span', { - class: 'mfm-x3', - }, genEl(token.children)); - } - case 'x4': { - return h('span', { - class: 'mfm-x4', - }, genEl(token.children)); - } - case 'font': { - const family = - token.props.args.serif ? 'serif' : - token.props.args.monospace ? 'monospace' : - token.props.args.cursive ? 'cursive' : - token.props.args.fantasy ? 'fantasy' : - token.props.args.emoji ? 'emoji' : - token.props.args.math ? 'math' : - null; - if (family) style = `font-family: ${family};`; - break; - } - case 'blur': { - return h('span', { - class: '_mfm_blur_', - }, genEl(token.children)); - } - case 'rainbow': { - const speed = validTime(token.props.args.speed) || '1s'; - style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : ''; - break; - } - case 'sparkle': { - if (!this.$store.state.animatedMfm) { - return genEl(token.children); - } - return h(MkSparkle, {}, genEl(token.children)); - } - case 'rotate': { - const degrees = parseInt(token.props.args.deg) || '90'; - style = `transform: rotate(${degrees}deg); transform-origin: center center;`; - break; - } - } - if (style == null) { - return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']); - } else { - return h('span', { - style: 'display: inline-block;' + style, - }, genEl(token.children)); - } - } - - case 'small': { - return [h('small', { - style: 'opacity: 0.7;', - }, genEl(token.children))]; - } - - case 'center': { - return [h('div', { - style: 'text-align:center;', - }, genEl(token.children))]; - } - - case 'url': { - return [h(MkUrl, { - key: Math.random(), - url: token.props.url, - rel: 'nofollow noopener', - })]; - } - - case 'link': { - return [h(MkLink, { - key: Math.random(), - url: token.props.url, - rel: 'nofollow noopener', - }, genEl(token.children))]; - } - - case 'mention': { - return [h(MkMention, { - key: Math.random(), - host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, - username: token.props.username, - })]; - } - - case 'hashtag': { - return [h(MkA, { - key: Math.random(), - to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`, - style: 'color:var(--hashtag);', - }, `#${token.props.hashtag}`)]; - } - - case 'blockCode': { - return [h(MkCode, { - key: Math.random(), - code: token.props.code, - lang: token.props.lang, - })]; - } - - case 'inlineCode': { - return [h(MkCode, { - key: Math.random(), - code: token.props.code, - inline: true, - })]; - } - - case 'quote': { - if (!this.nowrap) { - return [h('div', { - class: 'quote', - }, genEl(token.children))]; - } else { - return [h('span', { - class: 'quote', - }, genEl(token.children))]; - } - } - - case 'emojiCode': { - return [h(MkEmoji, { - key: Math.random(), - emoji: `:${token.props.name}:`, - customEmojis: this.customEmojis, - normal: this.plain, - })]; - } - - case 'unicodeEmoji': { - return [h(MkEmoji, { - key: Math.random(), - emoji: token.props.emoji, - customEmojis: this.customEmojis, - normal: this.plain, - })]; - } - - case 'mathInline': { - return [h(MkFormula, { - key: Math.random(), - formula: token.props.formula, - block: false, - })]; - } - - case 'mathBlock': { - return [h(MkFormula, { - key: Math.random(), - formula: token.props.formula, - block: true, - })]; - } - - case 'search': { - return [h(MkGoogle, { - key: Math.random(), - q: token.props.query, - })]; - } - - case 'plain': { - return [h('span', genEl(token.children))]; - } - - default: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - console.error('unrecognized ast type:', (token as any).type); - - return []; - } - } - }).flat(Infinity) as (VNode | string)[]; - - // Parse ast to DOM - return h('span', genEl(ast)); - }, -}); diff --git a/packages/client/src/components/page/page.block.vue b/packages/client/src/components/page/page.block.vue deleted file mode 100644 index f3e7764604..0000000000 --- a/packages/client/src/components/page/page.block.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/packages/client/src/components/page/page.button.vue b/packages/client/src/components/page/page.button.vue deleted file mode 100644 index 83931021d8..0000000000 --- a/packages/client/src/components/page/page.button.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.canvas.vue b/packages/client/src/components/page/page.canvas.vue deleted file mode 100644 index 80f6c8339c..0000000000 --- a/packages/client/src/components/page/page.canvas.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.counter.vue b/packages/client/src/components/page/page.counter.vue deleted file mode 100644 index a9e1f41a54..0000000000 --- a/packages/client/src/components/page/page.counter.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.if.vue b/packages/client/src/components/page/page.if.vue deleted file mode 100644 index 372a15f0c6..0000000000 --- a/packages/client/src/components/page/page.if.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/packages/client/src/components/page/page.image.vue b/packages/client/src/components/page/page.image.vue deleted file mode 100644 index 8ba70c5855..0000000000 --- a/packages/client/src/components/page/page.image.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.note.vue b/packages/client/src/components/page/page.note.vue deleted file mode 100644 index 7d5c484a1b..0000000000 --- a/packages/client/src/components/page/page.note.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.number-input.vue b/packages/client/src/components/page/page.number-input.vue deleted file mode 100644 index 50cf6d0770..0000000000 --- a/packages/client/src/components/page/page.number-input.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.post.vue b/packages/client/src/components/page/page.post.vue deleted file mode 100644 index 0ef50d65cd..0000000000 --- a/packages/client/src/components/page/page.post.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.radio-button.vue b/packages/client/src/components/page/page.radio-button.vue deleted file mode 100644 index b4d9e01a54..0000000000 --- a/packages/client/src/components/page/page.radio-button.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/packages/client/src/components/page/page.section.vue b/packages/client/src/components/page/page.section.vue deleted file mode 100644 index 630c1f5179..0000000000 --- a/packages/client/src/components/page/page.section.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.switch.vue b/packages/client/src/components/page/page.switch.vue deleted file mode 100644 index 64dc4ff8aa..0000000000 --- a/packages/client/src/components/page/page.switch.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.text-input.vue b/packages/client/src/components/page/page.text-input.vue deleted file mode 100644 index 840649ece6..0000000000 --- a/packages/client/src/components/page/page.text-input.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.text.vue b/packages/client/src/components/page/page.text.vue deleted file mode 100644 index 689c484521..0000000000 --- a/packages/client/src/components/page/page.text.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - - - diff --git a/packages/client/src/components/page/page.textarea-input.vue b/packages/client/src/components/page/page.textarea-input.vue deleted file mode 100644 index 507e1bd97b..0000000000 --- a/packages/client/src/components/page/page.textarea-input.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/packages/client/src/components/page/page.textarea.vue b/packages/client/src/components/page/page.textarea.vue deleted file mode 100644 index f809925081..0000000000 --- a/packages/client/src/components/page/page.textarea.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/packages/client/src/components/page/page.vue b/packages/client/src/components/page/page.vue deleted file mode 100644 index b5cb73c009..0000000000 --- a/packages/client/src/components/page/page.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - - - diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts deleted file mode 100644 index f2022b0f02..0000000000 --- a/packages/client/src/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -const address = new URL(location.href); -const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content; - -export const host = address.host; -export const hostname = address.hostname; -export const url = address.origin; -export const apiUrl = url + '/api'; -export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; -export const lang = localStorage.getItem('lang'); -export const langs = _LANGS_; -export const locale = JSON.parse(localStorage.getItem('locale')); -export const version = _VERSION_; -export const instanceName = siteName === 'Misskey' ? host : siteName; -export const ui = localStorage.getItem('ui'); -export const debug = localStorage.getItem('debug') === 'true'; diff --git a/packages/client/src/const.ts b/packages/client/src/const.ts deleted file mode 100644 index 77366cf07b..0000000000 --- a/packages/client/src/const.ts +++ /dev/null @@ -1,45 +0,0 @@ -// ブラウザで直接表示することを許可するファイルの種類のリスト -// ここに含まれないものは application/octet-stream としてレスポンスされる -// SVGはXSSを生むので許可しない -export const FILE_TYPE_BROWSERSAFE = [ - // Images - 'image/png', - 'image/gif', - 'image/jpeg', - 'image/webp', - 'image/avif', - '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/adaptive-border.ts b/packages/client/src/directives/adaptive-border.ts deleted file mode 100644 index 619c9f0b6d..0000000000 --- a/packages/client/src/directives/adaptive-border.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Directive } from 'vue'; - -export default { - mounted(src, binding, vn) { - const getBgColor = (el: HTMLElement) => { - const style = window.getComputedStyle(el); - if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { - return style.backgroundColor; - } else { - return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; - } - }; - - const parentBg = getBgColor(src.parentElement); - - const myBg = window.getComputedStyle(src).backgroundColor; - - if (parentBg === myBg) { - src.style.borderColor = 'var(--divider)'; - } else { - src.style.borderColor = myBg; - } - }, -} as Directive; diff --git a/packages/client/src/directives/anim.ts b/packages/client/src/directives/anim.ts deleted file mode 100644 index 04e1c6a404..0000000000 --- a/packages/client/src/directives/anim.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Directive } from 'vue'; - -export default { - beforeMount(src, binding, vn) { - src.style.opacity = '0'; - src.style.transform = 'scale(0.9)'; - // ページネーションと相性が悪いので - //if (typeof binding.value === 'number') src.style.transitionDelay = `${binding.value * 30}ms`; - src.classList.add('_zoom'); - }, - - mounted(src, binding, vn) { - window.setTimeout(() => { - src.style.opacity = '1'; - src.style.transform = 'none'; - }, 1); - }, -} as Directive; diff --git a/packages/client/src/directives/appear.ts b/packages/client/src/directives/appear.ts deleted file mode 100644 index 7fa43fc34a..0000000000 --- a/packages/client/src/directives/appear.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Directive } from 'vue'; - -export default { - mounted(src, binding, vn) { - const fn = binding.value; - if (fn == null) return; - - const observer = new IntersectionObserver(entries => { - if (entries.some(entry => entry.isIntersecting)) { - fn(); - } - }); - - observer.observe(src); - - src._observer_ = observer; - }, - - unmounted(src, binding, vn) { - if (src._observer_) src._observer_.disconnect(); - }, -} as Directive; diff --git a/packages/client/src/directives/click-anime.ts b/packages/client/src/directives/click-anime.ts deleted file mode 100644 index e2f514b7ca..0000000000 --- a/packages/client/src/directives/click-anime.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Directive } from 'vue'; -import { defaultStore } from '@/store'; - -export default { - mounted(el, binding, vn) { - /* - if (!defaultStore.state.animation) return; - - el.classList.add('_anime_bounce_standBy'); - - el.addEventListener('mousedown', () => { - el.classList.add('_anime_bounce_standBy'); - el.classList.add('_anime_bounce_ready'); - - el.addEventListener('mouseleave', () => { - el.classList.remove('_anime_bounce_ready'); - }); - }); - - el.addEventListener('click', () => { - el.classList.add('_anime_bounce'); - }); - - el.addEventListener('animationend', () => { - el.classList.remove('_anime_bounce_ready'); - el.classList.remove('_anime_bounce'); - el.classList.add('_anime_bounce_standBy'); - }); - */ - }, -} as Directive; diff --git a/packages/client/src/directives/follow-append.ts b/packages/client/src/directives/follow-append.ts deleted file mode 100644 index 62e0ac3b94..0000000000 --- a/packages/client/src/directives/follow-append.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Directive } from 'vue'; -import { getScrollContainer, getScrollPosition } from '@/scripts/scroll'; - -export default { - mounted(src, binding, vn) { - if (binding.value === false) return; - - let isBottom = true; - - const container = getScrollContainer(src)!; - container.addEventListener('scroll', () => { - const pos = getScrollPosition(container); - const viewHeight = container.clientHeight; - const height = container.scrollHeight; - isBottom = (pos + viewHeight > height - 32); - }, { passive: true }); - container.scrollTop = container.scrollHeight; - - const ro = new ResizeObserver((entries, observer) => { - if (isBottom) { - const height = container.scrollHeight; - container.scrollTop = height; - } - }); - - ro.observe(src); - - // TODO: 新たにプロパティを作るのをやめMapを使う - src._ro_ = ro; - }, - - unmounted(src, binding, vn) { - if (src._ro_) src._ro_.unobserve(src); - }, -} as Directive; diff --git a/packages/client/src/directives/get-size.ts b/packages/client/src/directives/get-size.ts deleted file mode 100644 index ff3bdd78ac..0000000000 --- a/packages/client/src/directives/get-size.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Directive } from 'vue'; - -const mountings = new Map void; -}>(); - -function calc(src: Element) { - const info = mountings.get(src); - const height = src.clientHeight; - const width = src.clientWidth; - - if (!info) return; - - // アクティベート前などでsrcが描画されていない場合 - if (!height) { - // IntersectionObserverで表示検出する - if (!info.intersection) { - info.intersection = new IntersectionObserver(entries => { - if (entries.some(entry => entry.isIntersecting)) calc(src); - }); - } - info.intersection.observe(src); - return; - } - if (info.intersection) { - info.intersection.disconnect(); - delete info.intersection; - } - - info.fn(width, height); -} - -export default { - mounted(src, binding, vn) { - const resize = new ResizeObserver((entries, observer) => { - calc(src); - }); - resize.observe(src); - - mountings.set(src, { resize, fn: binding.value }); - calc(src); - }, - - unmounted(src, binding, vn) { - binding.value(0, 0); - const info = mountings.get(src); - if (!info) return; - info.resize.disconnect(); - if (info.intersection) info.intersection.disconnect(); - mountings.delete(src); - }, -} as Directive void>; diff --git a/packages/client/src/directives/hotkey.ts b/packages/client/src/directives/hotkey.ts deleted file mode 100644 index dfc5f646a4..0000000000 --- a/packages/client/src/directives/hotkey.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Directive } from 'vue'; -import { makeHotkey } from '../scripts/hotkey'; - -export default { - mounted(el, binding) { - el._hotkey_global = binding.modifiers.global === true; - - el._keyHandler = makeHotkey(binding.value); - - if (el._hotkey_global) { - document.addEventListener('keydown', el._keyHandler); - } else { - el.addEventListener('keydown', el._keyHandler); - } - }, - - unmounted(el) { - if (el._hotkey_global) { - document.removeEventListener('keydown', el._keyHandler); - } else { - el.removeEventListener('keydown', el._keyHandler); - } - }, -} as Directive; diff --git a/packages/client/src/directives/index.ts b/packages/client/src/directives/index.ts deleted file mode 100644 index 401a917cba..0000000000 --- a/packages/client/src/directives/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { App } from 'vue'; - -import userPreview from './user-preview'; -import size from './size'; -import getSize from './get-size'; -import ripple from './ripple'; -import tooltip from './tooltip'; -import hotkey from './hotkey'; -import appear from './appear'; -import anim from './anim'; -import clickAnime from './click-anime'; -import panel from './panel'; -import adaptiveBorder from './adaptive-border'; - -export default function(app: App) { - app.directive('userPreview', userPreview); - app.directive('user-preview', userPreview); - app.directive('size', size); - app.directive('get-size', getSize); - app.directive('ripple', ripple); - app.directive('tooltip', tooltip); - app.directive('hotkey', hotkey); - app.directive('appear', appear); - app.directive('anim', anim); - app.directive('click-anime', clickAnime); - app.directive('panel', panel); - app.directive('adaptive-border', adaptiveBorder); -} diff --git a/packages/client/src/directives/panel.ts b/packages/client/src/directives/panel.ts deleted file mode 100644 index d31dc41ed4..0000000000 --- a/packages/client/src/directives/panel.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Directive } from 'vue'; - -export default { - mounted(src, binding, vn) { - const getBgColor = (el: HTMLElement) => { - const style = window.getComputedStyle(el); - if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { - return style.backgroundColor; - } else { - return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; - } - }; - - const parentBg = getBgColor(src.parentElement); - - const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel'); - - if (parentBg === myBg) { - src.style.backgroundColor = 'var(--bg)'; - } else { - src.style.backgroundColor = 'var(--panel)'; - } - }, -} as Directive; diff --git a/packages/client/src/directives/ripple.ts b/packages/client/src/directives/ripple.ts deleted file mode 100644 index d32f7ab441..0000000000 --- a/packages/client/src/directives/ripple.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Ripple from '@/components/MkRipple.vue'; -import { popup } from '@/os'; - -export default { - mounted(el, binding, vn) { - // 明示的に false であればバインドしない - if (binding.value === false) return; - - el.addEventListener('click', () => { - const rect = el.getBoundingClientRect(); - - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - - popup(Ripple, { x, y }, {}, 'end'); - }); - }, -}; diff --git a/packages/client/src/directives/size.ts b/packages/client/src/directives/size.ts deleted file mode 100644 index da8bd78ea1..0000000000 --- a/packages/client/src/directives/size.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Directive } from 'vue'; - -type Value = { max?: number[]; min?: number[]; }; - -//const observers = new Map(); -const mountings = new Map(); - -type ClassOrder = { - add: string[]; - remove: string[]; -}; - -const isContainerQueriesSupported = ('container' in document.documentElement.style); - -const cache = new Map(); - -function getClassOrder(width: number, queue: Value): ClassOrder { - const getMaxClass = (v: number) => `max-width_${v}px`; - const getMinClass = (v: number) => `min-width_${v}px`; - - return { - add: [ - ...(queue.max ? queue.max.filter(v => width <= v).map(getMaxClass) : []), - ...(queue.min ? queue.min.filter(v => width >= v).map(getMinClass) : []), - ], - remove: [ - ...(queue.max ? queue.max.filter(v => width > v).map(getMaxClass) : []), - ...(queue.min ? queue.min.filter(v => width < v).map(getMinClass) : []), - ], - }; -} - -function applyClassOrder(el: Element, order: ClassOrder) { - el.classList.add(...order.add); - el.classList.remove(...order.remove); -} - -function getOrderName(width: number, queue: Value): string { - return `${width}|${queue.max ? queue.max.join(',') : ''}|${queue.min ? queue.min.join(',') : ''}`; -} - -function calc(el: Element) { - const info = mountings.get(el); - const width = el.clientWidth; - - if (!info || info.previousWidth === width) return; - - // アクティベート前などでsrcが描画されていない場合 - if (!width) { - // IntersectionObserverで表示検出する - if (!info.intersection) { - info.intersection = new IntersectionObserver(entries => { - if (entries.some(entry => entry.isIntersecting)) calc(el); - }); - } - info.intersection.observe(el); - return; - } - if (info.intersection) { - info.intersection.disconnect(); - delete info.intersection; - } - - mountings.set(el, { ...info, ...{ previousWidth: width, twoPreviousWidth: info.previousWidth }}); - - // Prevent infinite resizing - // https://github.com/misskey-dev/misskey/issues/9076 - if (info.twoPreviousWidth === width) { - return; - } - - const cached = cache.get(getOrderName(width, info.value)); - if (cached) { - applyClassOrder(el, cached); - } else { - const order = getClassOrder(width, info.value); - cache.set(getOrderName(width, info.value), order); - applyClassOrder(el, order); - } -} - -export default { - mounted(src, binding, vn) { - if (isContainerQueriesSupported) return; - - const resize = new ResizeObserver((entries, observer) => { - calc(src); - }); - - mountings.set(src, { - value: binding.value, - resize, - previousWidth: 0, - twoPreviousWidth: 0, - }); - - calc(src); - resize.observe(src); - }, - - updated(src, binding, vn) { - if (isContainerQueriesSupported) return; - - mountings.set(src, Object.assign({}, mountings.get(src), { value: binding.value })); - calc(src); - }, - - unmounted(src, binding, vn) { - if (isContainerQueriesSupported) return; - - const info = mountings.get(src); - if (!info) return; - info.resize.disconnect(); - if (info.intersection) info.intersection.disconnect(); - mountings.delete(src); - }, -} as Directive; diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts deleted file mode 100644 index 5d13497b5f..0000000000 --- a/packages/client/src/directives/tooltip.ts +++ /dev/null @@ -1,93 +0,0 @@ -// TODO: useTooltip関数使うようにしたい -// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明 - -import { defineAsyncComponent, Directive, ref } from 'vue'; -import { isTouchUsing } from '@/scripts/touch'; -import { popup, alert } from '@/os'; - -const start = isTouchUsing ? 'touchstart' : 'mouseover'; -const end = isTouchUsing ? 'touchend' : 'mouseleave'; - -export default { - mounted(el: HTMLElement, binding, vn) { - const delay = binding.modifiers.noDelay ? 0 : 100; - - const self = (el as any)._tooltipDirective_ = {} as any; - - self.text = binding.value as string; - self._close = null; - self.showTimer = null; - self.hideTimer = null; - self.checkTimer = null; - - self.close = () => { - if (self._close) { - window.clearInterval(self.checkTimer); - self._close(); - self._close = null; - } - }; - - if (binding.arg === 'dialog') { - el.addEventListener('click', (ev) => { - ev.preventDefault(); - ev.stopPropagation(); - alert({ - type: 'info', - text: binding.value, - }); - return false; - }); - } - - self.show = () => { - if (!document.body.contains(el)) return; - if (self._close) return; - if (self.text == null) return; - - const showing = ref(true); - popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { - showing, - text: self.text, - asMfm: binding.modifiers.mfm, - direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top', - targetElement: el, - }, {}, 'closed'); - - self._close = () => { - showing.value = false; - }; - }; - - el.addEventListener('selectstart', ev => { - ev.preventDefault(); - }); - - el.addEventListener(start, () => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); - self.showTimer = window.setTimeout(self.show, delay); - }, { passive: true }); - - el.addEventListener(end, () => { - window.clearTimeout(self.showTimer); - window.clearTimeout(self.hideTimer); - self.hideTimer = window.setTimeout(self.close, delay); - }, { passive: true }); - - el.addEventListener('click', () => { - window.clearTimeout(self.showTimer); - self.close(); - }); - }, - - updated(el, binding) { - const self = el._tooltipDirective_; - self.text = binding.value as string; - }, - - unmounted(el, binding, vn) { - const self = el._tooltipDirective_; - window.clearInterval(self.checkTimer); - }, -} as Directive; diff --git a/packages/client/src/directives/user-preview.ts b/packages/client/src/directives/user-preview.ts deleted file mode 100644 index ed5f00ca65..0000000000 --- a/packages/client/src/directives/user-preview.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { defineAsyncComponent, Directive, ref } from 'vue'; -import autobind from 'autobind-decorator'; -import { popup } from '@/os'; - -export class UserPreview { - private el; - private user; - private showTimer; - private hideTimer; - private checkTimer; - private promise; - - constructor(el, user) { - this.el = el; - this.user = user; - - this.attach(); - } - - @autobind - private show() { - if (!document.body.contains(this.el)) return; - if (this.promise) return; - - const showing = ref(true); - - popup(defineAsyncComponent(() => import('@/components/MkUserPreview.vue')), { - showing, - q: this.user, - source: this.el, - }, { - mouseover: () => { - window.clearTimeout(this.hideTimer); - }, - mouseleave: () => { - window.clearTimeout(this.showTimer); - this.hideTimer = window.setTimeout(this.close, 500); - }, - }, 'closed'); - - this.promise = { - cancel: () => { - showing.value = false; - }, - }; - - this.checkTimer = window.setInterval(() => { - if (!document.body.contains(this.el)) { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); - this.close(); - } - }, 1000); - } - - @autobind - private close() { - if (this.promise) { - window.clearInterval(this.checkTimer); - this.promise.cancel(); - this.promise = null; - } - } - - @autobind - private onMouseover() { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); - this.showTimer = window.setTimeout(this.show, 500); - } - - @autobind - private onMouseleave() { - window.clearTimeout(this.showTimer); - window.clearTimeout(this.hideTimer); - this.hideTimer = window.setTimeout(this.close, 500); - } - - @autobind - private onClick() { - window.clearTimeout(this.showTimer); - this.close(); - } - - @autobind - public attach() { - this.el.addEventListener('mouseover', this.onMouseover); - this.el.addEventListener('mouseleave', this.onMouseleave); - this.el.addEventListener('click', this.onClick); - } - - @autobind - public detach() { - this.el.removeEventListener('mouseover', this.onMouseover); - this.el.removeEventListener('mouseleave', this.onMouseleave); - this.el.removeEventListener('click', this.onClick); - window.clearInterval(this.checkTimer); - } -} - -export default { - mounted(el: HTMLElement, binding, vn) { - if (binding.value == null) return; - - // TODO: 新たにプロパティを作るのをやめMapを使う - // ただメモリ的には↓の方が省メモリかもしれないので検討中 - const self = (el as any)._userPreviewDirective_ = {} as any; - - self.preview = new UserPreview(el, binding.value); - }, - - unmounted(el, binding, vn) { - if (binding.value == null) return; - - const self = el._userPreviewDirective_; - self.preview.detach(); - }, -} as Directive; diff --git a/packages/client/src/emojilist.json b/packages/client/src/emojilist.json deleted file mode 100644 index 402e82e33b..0000000000 --- a/packages/client/src/emojilist.json +++ /dev/null @@ -1,1785 +0,0 @@ -[ - { "category": "face", "char": "😀", "name": "grinning", "keywords": ["face", "smile", "happy", "joy", ": D", "grin"] }, - { "category": "face", "char": "😬", "name": "grimacing", "keywords": ["face", "grimace", "teeth"] }, - { "category": "face", "char": "😁", "name": "grin", "keywords": ["face", "happy", "smile", "joy", "kawaii"] }, - { "category": "face", "char": "😂", "name": "joy", "keywords": ["face", "cry", "tears", "weep", "happy", "happytears", "haha"] }, - { "category": "face", "char": "🤣", "name": "rofl", "keywords": ["face", "rolling", "floor", "laughing", "lol", "haha"] }, - { "category": "face", "char": "🥳", "name": "partying", "keywords": ["face", "celebration", "woohoo"] }, - { "category": "face", "char": "😃", "name": "smiley", "keywords": ["face", "happy", "joy", "haha", ": D", ": )", "smile", "funny"] }, - { "category": "face", "char": "😄", "name": "smile", "keywords": ["face", "happy", "joy", "funny", "haha", "laugh", "like", ": D", ": )"] }, - { "category": "face", "char": "😅", "name": "sweat_smile", "keywords": ["face", "hot", "happy", "laugh", "sweat", "smile", "relief"] }, - { "category": "face", "char": "🥲", "name": "smiling_face_with_tear", "keywords": ["face"] }, - { "category": "face", "char": "😆", "name": "laughing", "keywords": ["happy", "joy", "lol", "satisfied", "haha", "face", "glad", "XD", "laugh"] }, - { "category": "face", "char": "😇", "name": "innocent", "keywords": ["face", "angel", "heaven", "halo"] }, - { "category": "face", "char": "😉", "name": "wink", "keywords": ["face", "happy", "mischievous", "secret", ";)", "smile", "eye"] }, - { "category": "face", "char": "😊", "name": "blush", "keywords": ["face", "smile", "happy", "flushed", "crush", "embarrassed", "shy", "joy"] }, - { "category": "face", "char": "🙂", "name": "slightly_smiling_face", "keywords": ["face", "smile"] }, - { "category": "face", "char": "🙃", "name": "upside_down_face", "keywords": ["face", "flipped", "silly", "smile"] }, - { "category": "face", "char": "☺️", "name": "relaxed", "keywords": ["face", "blush", "massage", "happiness"] }, - { "category": "face", "char": "😋", "name": "yum", "keywords": ["happy", "joy", "tongue", "smile", "face", "silly", "yummy", "nom", "delicious", "savouring"] }, - { "category": "face", "char": "😌", "name": "relieved", "keywords": ["face", "relaxed", "phew", "massage", "happiness"] }, - { "category": "face", "char": "😍", "name": "heart_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "heart"] }, - { "category": "face", "char": "🥰", "name": "smiling_face_with_three_hearts", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "hearts", "adore"] }, - { "category": "face", "char": "😘", "name": "kissing_heart", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] }, - { "category": "face", "char": "😗", "name": "kissing", "keywords": ["love", "like", "face", "3", "valentines", "infatuation", "kiss"] }, - { "category": "face", "char": "😙", "name": "kissing_smiling_eyes", "keywords": ["face", "affection", "valentines", "infatuation", "kiss"] }, - { "category": "face", "char": "😚", "name": "kissing_closed_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] }, - { "category": "face", "char": "😜", "name": "stuck_out_tongue_winking_eye", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "wink", "tongue"] }, - { "category": "face", "char": "🤪", "name": "zany", "keywords": ["face", "goofy", "crazy"] }, - { "category": "face", "char": "🤨", "name": "raised_eyebrow", "keywords": ["face", "distrust", "scepticism", "disapproval", "disbelief", "surprise"] }, - { "category": "face", "char": "🧐", "name": "monocle", "keywords": ["face", "stuffy", "wealthy"] }, - { "category": "face", "char": "😝", "name": "stuck_out_tongue_closed_eyes", "keywords": ["face", "prank", "playful", "mischievous", "smile", "tongue"] }, - { "category": "face", "char": "😛", "name": "stuck_out_tongue", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "tongue"] }, - { "category": "face", "char": "🤑", "name": "money_mouth_face", "keywords": ["face", "rich", "dollar", "money"] }, - { "category": "face", "char": "🤓", "name": "nerd_face", "keywords": ["face", "nerdy", "geek", "dork"] }, - { "category": "face", "char": "🥸", "name": "disguised_face", "keywords": ["face", "nose", "glasses", "incognito"] }, - { "category": "face", "char": "😎", "name": "sunglasses", "keywords": ["face", "cool", "smile", "summer", "beach", "sunglass"] }, - { "category": "face", "char": "🤩", "name": "star_struck", "keywords": ["face", "smile", "starry", "eyes", "grinning"] }, - { "category": "face", "char": "🤡", "name": "clown_face", "keywords": ["face"] }, - { "category": "face", "char": "🤠", "name": "cowboy_hat_face", "keywords": ["face", "cowgirl", "hat"] }, - { "category": "face", "char": "🤗", "name": "hugs", "keywords": ["face", "smile", "hug"] }, - { "category": "face", "char": "😏", "name": "smirk", "keywords": ["face", "smile", "mean", "prank", "smug", "sarcasm"] }, - { "category": "face", "char": "😶", "name": "no_mouth", "keywords": ["face", "hellokitty"] }, - { "category": "face", "char": "😐", "name": "neutral_face", "keywords": ["indifference", "meh", ": |", "neutral"] }, - { "category": "face", "char": "😑", "name": "expressionless", "keywords": ["face", "indifferent", "-_-", "meh", "deadpan"] }, - { "category": "face", "char": "😒", "name": "unamused", "keywords": ["indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"] }, - { "category": "face", "char": "🙄", "name": "roll_eyes", "keywords": ["face", "eyeroll", "frustrated"] }, - { "category": "face", "char": "🤔", "name": "thinking", "keywords": ["face", "hmmm", "think", "consider"] }, - { "category": "face", "char": "🤥", "name": "lying_face", "keywords": ["face", "lie", "pinocchio"] }, - { "category": "face", "char": "🤭", "name": "hand_over_mouth", "keywords": ["face", "whoops", "shock", "surprise"] }, - { "category": "face", "char": "🤫", "name": "shushing", "keywords": ["face", "quiet", "shhh"] }, - { "category": "face", "char": "🤬", "name": "symbols_over_mouth", "keywords": ["face", "swearing", "cursing", "cussing", "profanity", "expletive"] }, - { "category": "face", "char": "🤯", "name": "exploding_head", "keywords": ["face", "shocked", "mind", "blown"] }, - { "category": "face", "char": "😳", "name": "flushed", "keywords": ["face", "blush", "shy", "flattered"] }, - { "category": "face", "char": "😞", "name": "disappointed", "keywords": ["face", "sad", "upset", "depressed", ": ("] }, - { "category": "face", "char": "😟", "name": "worried", "keywords": ["face", "concern", "nervous", ": ("] }, - { "category": "face", "char": "😠", "name": "angry", "keywords": ["mad", "face", "annoyed", "frustrated"] }, - { "category": "face", "char": "😡", "name": "rage", "keywords": ["angry", "mad", "hate", "despise"] }, - { "category": "face", "char": "😔", "name": "pensive", "keywords": ["face", "sad", "depressed", "upset"] }, - { "category": "face", "char": "😕", "name": "confused", "keywords": ["face", "indifference", "huh", "weird", "hmmm", ": /"] }, - { "category": "face", "char": "🙁", "name": "slightly_frowning_face", "keywords": ["face", "frowning", "disappointed", "sad", "upset"] }, - { "category": "face", "char": "☹", "name": "frowning_face", "keywords": ["face", "sad", "upset", "frown"] }, - { "category": "face", "char": "😣", "name": "persevere", "keywords": ["face", "sick", "no", "upset", "oops"] }, - { "category": "face", "char": "😖", "name": "confounded", "keywords": ["face", "confused", "sick", "unwell", "oops", ": S"] }, - { "category": "face", "char": "😫", "name": "tired_face", "keywords": ["sick", "whine", "upset", "frustrated"] }, - { "category": "face", "char": "😩", "name": "weary", "keywords": ["face", "tired", "sleepy", "sad", "frustrated", "upset"] }, - { "category": "face", "char": "🥺", "name": "pleading", "keywords": ["face", "begging", "mercy"] }, - { "category": "face", "char": "😤", "name": "triumph", "keywords": ["face", "gas", "phew", "proud", "pride"] }, - { "category": "face", "char": "😮", "name": "open_mouth", "keywords": ["face", "surprise", "impressed", "wow", "whoa", ": O"] }, - { "category": "face", "char": "😱", "name": "scream", "keywords": ["face", "munch", "scared", "omg"] }, - { "category": "face", "char": "😨", "name": "fearful", "keywords": ["face", "scared", "terrified", "nervous", "oops", "huh"] }, - { "category": "face", "char": "😰", "name": "cold_sweat", "keywords": ["face", "nervous", "sweat"] }, - { "category": "face", "char": "😯", "name": "hushed", "keywords": ["face", "woo", "shh"] }, - { "category": "face", "char": "😦", "name": "frowning", "keywords": ["face", "aw", "what"] }, - { "category": "face", "char": "😧", "name": "anguished", "keywords": ["face", "stunned", "nervous"] }, - { "category": "face", "char": "😢", "name": "cry", "keywords": ["face", "tears", "sad", "depressed", "upset", ": '("] }, - { "category": "face", "char": "😥", "name": "disappointed_relieved", "keywords": ["face", "phew", "sweat", "nervous"] }, - { "category": "face", "char": "🤤", "name": "drooling_face", "keywords": ["face"] }, - { "category": "face", "char": "😪", "name": "sleepy", "keywords": ["face", "tired", "rest", "nap"] }, - { "category": "face", "char": "😓", "name": "sweat", "keywords": ["face", "hot", "sad", "tired", "exercise"] }, - { "category": "face", "char": "🥵", "name": "hot", "keywords": ["face", "feverish", "heat", "red", "sweating"] }, - { "category": "face", "char": "🥶", "name": "cold", "keywords": ["face", "blue", "freezing", "frozen", "frostbite", "icicles"] }, - { "category": "face", "char": "😭", "name": "sob", "keywords": ["face", "cry", "tears", "sad", "upset", "depressed"] }, - { "category": "face", "char": "😵", "name": "dizzy_face", "keywords": ["spent", "unconscious", "xox", "dizzy"] }, - { "category": "face", "char": "😲", "name": "astonished", "keywords": ["face", "xox", "surprised", "poisoned"] }, - { "category": "face", "char": "🤐", "name": "zipper_mouth_face", "keywords": ["face", "sealed", "zipper", "secret"] }, - { "category": "face", "char": "🤢", "name": "nauseated_face", "keywords": ["face", "vomit", "gross", "green", "sick", "throw up", "ill"] }, - { "category": "face", "char": "🤧", "name": "sneezing_face", "keywords": ["face", "gesundheit", "sneeze", "sick", "allergy"] }, - { "category": "face", "char": "🤮", "name": "vomiting", "keywords": ["face", "sick"] }, - { "category": "face", "char": "😷", "name": "mask", "keywords": ["face", "sick", "ill", "disease"] }, - { "category": "face", "char": "🤒", "name": "face_with_thermometer", "keywords": ["sick", "temperature", "thermometer", "cold", "fever"] }, - { "category": "face", "char": "🤕", "name": "face_with_head_bandage", "keywords": ["injured", "clumsy", "bandage", "hurt"] }, - { "category": "face", "char": "🥴", "name": "woozy", "keywords": ["face", "dizzy", "intoxicated", "tipsy", "wavy"] }, - { "category": "face", "char": "🥱", "name": "yawning", "keywords": ["face", "tired", "yawning"] }, - { "category": "face", "char": "😴", "name": "sleeping", "keywords": ["face", "tired", "sleepy", "night", "zzz"] }, - { "category": "face", "char": "💤", "name": "zzz", "keywords": ["sleepy", "tired", "dream"] }, - { "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] }, - { "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] }, - { "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] }, - { "category": "face", "char": "\uD83E\uDEE0", "name": "melting_face", "keywords": ["disappear", "dissolve", "liquid", "melt", "toketa"] }, - { "category": "face", "char": "\uD83E\uDEE2", "name": "face_with_open_eyes_and_hand_over_mouth", "keywords": ["amazement", "awe", "disbelief", "embarrass", "scared", "surprise", "ohoho"] }, - { "category": "face", "char": "\uD83E\uDEE3", "name": "face_with_peeking_eye", "keywords": ["captivated", "peep", "stare", "chunibyo"] }, - { "category": "face", "char": "\uD83E\uDEE1", "name": "saluting_face", "keywords": ["ok", "salute", "sunny", "troops", "yes", "raja"] }, - { "category": "face", "char": "\uD83E\uDEE5", "name": "dotted_line_face", "keywords": ["depressed", "disappear", "hide", "introvert", "invisible", "tensen"] }, - { "category": "face", "char": "\uD83E\uDEE4", "name": "face_with_diagonal_mouth", "keywords": ["disappointed", "meh", "skeptical", "unsure"] }, - { "category": "face", "char": "\uD83E\uDD79", "name": "face_holding_back_tears", "keywords": ["angry", "cry", "proud", "resist", "sad"] }, - { "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] }, - { "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] }, - { "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] }, - { "category": "face", "char": "👹", "name": "japanese_ogre", "keywords": ["monster", "red", "mask", "halloween", "scary", "creepy", "devil", "demon", "japanese", "ogre"] }, - { "category": "face", "char": "👺", "name": "japanese_goblin", "keywords": ["red", "evil", "mask", "monster", "scary", "creepy", "japanese", "goblin"] }, - { "category": "face", "char": "💀", "name": "skull", "keywords": ["dead", "skeleton", "creepy", "death"] }, - { "category": "face", "char": "👻", "name": "ghost", "keywords": ["halloween", "spooky", "scary"] }, - { "category": "face", "char": "👽", "name": "alien", "keywords": ["UFO", "paul", "weird", "outer_space"] }, - { "category": "face", "char": "🤖", "name": "robot", "keywords": ["computer", "machine", "bot"] }, - { "category": "face", "char": "😺", "name": "smiley_cat", "keywords": ["animal", "cats", "happy", "smile"] }, - { "category": "face", "char": "😸", "name": "smile_cat", "keywords": ["animal", "cats", "smile"] }, - { "category": "face", "char": "😹", "name": "joy_cat", "keywords": ["animal", "cats", "haha", "happy", "tears"] }, - { "category": "face", "char": "😻", "name": "heart_eyes_cat", "keywords": ["animal", "love", "like", "affection", "cats", "valentines", "heart"] }, - { "category": "face", "char": "😼", "name": "smirk_cat", "keywords": ["animal", "cats", "smirk"] }, - { "category": "face", "char": "😽", "name": "kissing_cat", "keywords": ["animal", "cats", "kiss"] }, - { "category": "face", "char": "🙀", "name": "scream_cat", "keywords": ["animal", "cats", "munch", "scared", "scream"] }, - { "category": "face", "char": "😿", "name": "crying_cat_face", "keywords": ["animal", "tears", "weep", "sad", "cats", "upset", "cry"] }, - { "category": "face", "char": "😾", "name": "pouting_cat", "keywords": ["animal", "cats"] }, - { "category": "people", "char": "🤲", "name": "palms_up", "keywords": ["hands", "gesture", "cupped", "prayer"] }, - { "category": "people", "char": "🙌", "name": "raised_hands", "keywords": ["gesture", "hooray", "yea", "celebration", "hands"] }, - { "category": "people", "char": "👏", "name": "clap", "keywords": ["hands", "praise", "applause", "congrats", "yay"] }, - { "category": "people", "char": "👋", "name": "wave", "keywords": ["hands", "gesture", "goodbye", "solong", "farewell", "hello", "hi", "palm"] }, - { "category": "people", "char": "🤙", "name": "call_me_hand", "keywords": ["hands", "gesture"] }, - { "category": "people", "char": "👍", "name": "+1", "keywords": ["thumbsup", "yes", "awesome", "good", "agree", "accept", "cool", "hand", "like"] }, - { "category": "people", "char": "👎", "name": "-1", "keywords": ["thumbsdown", "no", "dislike", "hand"] }, - { "category": "people", "char": "👊", "name": "facepunch", "keywords": ["angry", "violence", "fist", "hit", "attack", "hand"] }, - { "category": "people", "char": "✊", "name": "fist", "keywords": ["fingers", "hand", "grasp"] }, - { "category": "people", "char": "🤛", "name": "fist_left", "keywords": ["hand", "fistbump"] }, - { "category": "people", "char": "🤜", "name": "fist_right", "keywords": ["hand", "fistbump"] }, - { "category": "people", "char": "✌", "name": "v", "keywords": ["fingers", "ohyeah", "hand", "peace", "victory", "two"] }, - { "category": "people", "char": "👌", "name": "ok_hand", "keywords": ["fingers", "limbs", "perfect", "ok", "okay"] }, - { "category": "people", "char": "✋", "name": "raised_hand", "keywords": ["fingers", "stop", "highfive", "palm", "ban"] }, - { "category": "people", "char": "🤚", "name": "raised_back_of_hand", "keywords": ["fingers", "raised", "backhand"] }, - { "category": "people", "char": "👐", "name": "open_hands", "keywords": ["fingers", "butterfly", "hands", "open"] }, - { "category": "people", "char": "💪", "name": "muscle", "keywords": ["arm", "flex", "hand", "summer", "strong", "biceps"] }, - { "category": "people", "char": "🦾", "name": "mechanical_arm", "keywords": ["flex", "hand", "strong", "biceps"] }, - { "category": "people", "char": "🙏", "name": "pray", "keywords": ["please", "hope", "wish", "namaste", "highfive"] }, - { "category": "people", "char": "🦶", "name": "foot", "keywords": ["kick", "stomp"] }, - { "category": "people", "char": "🦵", "name": "leg", "keywords": ["kick", "limb"] }, - { "category": "people", "char": "🦿", "name": "mechanical_leg", "keywords": ["kick", "limb"] }, - { "category": "people", "char": "🤝", "name": "handshake", "keywords": ["agreement", "shake"] }, - { "category": "people", "char": "☝", "name": "point_up", "keywords": ["hand", "fingers", "direction", "up"] }, - { "category": "people", "char": "👆", "name": "point_up_2", "keywords": ["fingers", "hand", "direction", "up"] }, - { "category": "people", "char": "👇", "name": "point_down", "keywords": ["fingers", "hand", "direction", "down"] }, - { "category": "people", "char": "👈", "name": "point_left", "keywords": ["direction", "fingers", "hand", "left"] }, - { "category": "people", "char": "👉", "name": "point_right", "keywords": ["fingers", "hand", "direction", "right"] }, - { "category": "people", "char": "🖕", "name": "fu", "keywords": ["hand", "fingers", "rude", "middle", "flipping"] }, - { "category": "people", "char": "🖐", "name": "raised_hand_with_fingers_splayed", "keywords": ["hand", "fingers", "palm"] }, - { "category": "people", "char": "🤟", "name": "love_you", "keywords": ["hand", "fingers", "gesture"] }, - { "category": "people", "char": "🤘", "name": "metal", "keywords": ["hand", "fingers", "evil_eye", "sign_of_horns", "rock_on"] }, - { "category": "people", "char": "🤞", "name": "crossed_fingers", "keywords": ["good", "lucky"] }, - { "category": "people", "char": "🖖", "name": "vulcan_salute", "keywords": ["hand", "fingers", "spock", "star trek"] }, - { "category": "people", "char": "✍", "name": "writing_hand", "keywords": ["lower_left_ballpoint_pen", "stationery", "write", "compose"] }, - { "category": "people", "char": "\uD83E\uDEF0", "name": "hand_with_index_finger_and_thumb_crossed", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF1", "name": "rightwards_hand", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF2", "name": "leftwards_hand", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF3", "name": "palm_down_hand", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF4", "name": "palm_up_hand", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF5", "name": "index_pointing_at_the_viewer", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEF6", "name": "heart_hands", "keywords": ["moemoekyun"] }, - { "category": "people", "char": "🤏", "name": "pinching_hand", "keywords": ["hand", "fingers"] }, - { "category": "people", "char": "🤌", "name": "pinched_fingers", "keywords": ["hand", "fingers"] }, - { "category": "people", "char": "🤳", "name": "selfie", "keywords": ["camera", "phone"] }, - { "category": "people", "char": "💅", "name": "nail_care", "keywords": ["beauty", "manicure", "finger", "fashion", "nail"] }, - { "category": "people", "char": "👄", "name": "lips", "keywords": ["mouth", "kiss"] }, - { "category": "people", "char": "\uD83E\uDEE6", "name": "biting_lip", "keywords": [] }, - { "category": "people", "char": "🦷", "name": "tooth", "keywords": ["teeth", "dentist"] }, - { "category": "people", "char": "👅", "name": "tongue", "keywords": ["mouth", "playful"] }, - { "category": "people", "char": "👂", "name": "ear", "keywords": ["face", "hear", "sound", "listen"] }, - { "category": "people", "char": "🦻", "name": "ear_with_hearing_aid", "keywords": ["face", "hear", "sound", "listen"] }, - { "category": "people", "char": "👃", "name": "nose", "keywords": ["smell", "sniff"] }, - { "category": "people", "char": "👁", "name": "eye", "keywords": ["face", "look", "see", "watch", "stare"] }, - { "category": "people", "char": "👀", "name": "eyes", "keywords": ["look", "watch", "stalk", "peek", "see"] }, - { "category": "people", "char": "🧠", "name": "brain", "keywords": ["smart", "intelligent"] }, - { "category": "people", "char": "🫀", "name": "anatomical_heart", "keywords": [] }, - { "category": "people", "char": "🫁", "name": "lungs", "keywords": [] }, - { "category": "people", "char": "👤", "name": "bust_in_silhouette", "keywords": ["user", "person", "human"] }, - { "category": "people", "char": "👥", "name": "busts_in_silhouette", "keywords": ["user", "person", "human", "group", "team"] }, - { "category": "people", "char": "🗣", "name": "speaking_head", "keywords": ["user", "person", "human", "sing", "say", "talk"] }, - { "category": "people", "char": "👶", "name": "baby", "keywords": ["child", "boy", "girl", "toddler"] }, - { "category": "people", "char": "🧒", "name": "child", "keywords": ["gender-neutral", "young"] }, - { "category": "people", "char": "👦", "name": "boy", "keywords": ["man", "male", "guy", "teenager"] }, - { "category": "people", "char": "👧", "name": "girl", "keywords": ["female", "woman", "teenager"] }, - { "category": "people", "char": "🧑", "name": "adult", "keywords": ["gender-neutral", "person"] }, - { "category": "people", "char": "👨", "name": "man", "keywords": ["mustache", "father", "dad", "guy", "classy", "sir", "moustache"] }, - { "category": "people", "char": "👩", "name": "woman", "keywords": ["female", "girls", "lady"] }, - { "category": "people", "char": "🧑‍🦱", "name": "curly_hair", "keywords": ["curly", "afro", "braids", "ringlets"] }, - { "category": "people", "char": "👩‍🦱", "name": "curly_hair_woman", "keywords": ["woman", "female", "girl", "curly", "afro", "braids", "ringlets"] }, - { "category": "people", "char": "👨‍🦱", "name": "curly_hair_man", "keywords": ["man", "male", "boy", "guy", "curly", "afro", "braids", "ringlets"] }, - { "category": "people", "char": "🧑‍🦰", "name": "red_hair", "keywords": ["redhead"] }, - { "category": "people", "char": "👩‍🦰", "name": "red_hair_woman", "keywords": ["woman", "female", "girl", "ginger", "redhead"] }, - { "category": "people", "char": "👨‍🦰", "name": "red_hair_man", "keywords": ["man", "male", "boy", "guy", "ginger", "redhead"] }, - { "category": "people", "char": "👱‍♀️", "name": "blonde_woman", "keywords": ["woman", "female", "girl", "blonde", "person"] }, - { "category": "people", "char": "👱", "name": "blonde_man", "keywords": ["man", "male", "boy", "blonde", "guy", "person"] }, - { "category": "people", "char": "🧑‍🦳", "name": "white_hair", "keywords": ["gray", "old", "white"] }, - { "category": "people", "char": "👩‍🦳", "name": "white_hair_woman", "keywords": ["woman", "female", "girl", "gray", "old", "white"] }, - { "category": "people", "char": "👨‍🦳", "name": "white_hair_man", "keywords": ["man", "male", "boy", "guy", "gray", "old", "white"] }, - { "category": "people", "char": "🧑‍🦲", "name": "bald", "keywords": ["bald", "chemotherapy", "hairless", "shaven"] }, - { "category": "people", "char": "👩‍🦲", "name": "bald_woman", "keywords": ["woman", "female", "girl", "bald", "chemotherapy", "hairless", "shaven"] }, - { "category": "people", "char": "👨‍🦲", "name": "bald_man", "keywords": ["man", "male", "boy", "guy", "bald", "chemotherapy", "hairless", "shaven"] }, - { "category": "people", "char": "🧔", "name": "bearded_person", "keywords": ["person", "bewhiskered"] }, - { "category": "people", "char": "🧓", "name": "older_adult", "keywords": ["human", "elder", "senior", "gender-neutral"] }, - { "category": "people", "char": "👴", "name": "older_man", "keywords": ["human", "male", "men", "old", "elder", "senior"] }, - { "category": "people", "char": "👵", "name": "older_woman", "keywords": ["human", "female", "women", "lady", "old", "elder", "senior"] }, - { "category": "people", "char": "👲", "name": "man_with_gua_pi_mao", "keywords": ["male", "boy", "chinese"] }, - { "category": "people", "char": "🧕", "name": "woman_with_headscarf", "keywords": ["female", "hijab", "mantilla", "tichel"] }, - { "category": "people", "char": "👳‍♀️", "name": "woman_with_turban", "keywords": ["female", "indian", "hinduism", "arabs", "woman"] }, - { "category": "people", "char": "👳", "name": "man_with_turban", "keywords": ["male", "indian", "hinduism", "arabs"] }, - { "category": "people", "char": "👮‍♀️", "name": "policewoman", "keywords": ["woman", "police", "law", "legal", "enforcement", "arrest", "911", "female"] }, - { "category": "people", "char": "👮", "name": "policeman", "keywords": ["man", "police", "law", "legal", "enforcement", "arrest", "911"] }, - { "category": "people", "char": "👷‍♀️", "name": "construction_worker_woman", "keywords": ["female", "human", "wip", "build", "construction", "worker", "labor", "woman"] }, - { "category": "people", "char": "👷", "name": "construction_worker_man", "keywords": ["male", "human", "wip", "guy", "build", "construction", "worker", "labor"] }, - { "category": "people", "char": "💂‍♀️", "name": "guardswoman", "keywords": ["uk", "gb", "british", "female", "royal", "woman"] }, - { "category": "people", "char": "💂", "name": "guardsman", "keywords": ["uk", "gb", "british", "male", "guy", "royal"] }, - { "category": "people", "char": "🕵️‍♀️", "name": "female_detective", "keywords": ["human", "spy", "detective", "female", "woman"] }, - { "category": "people", "char": "🕵", "name": "male_detective", "keywords": ["human", "spy", "detective"] }, - { "category": "people", "char": "🧑‍⚕️", "name": "health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "human"] }, - { "category": "people", "char": "👩‍⚕️", "name": "woman_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "woman", "human"] }, - { "category": "people", "char": "👨‍⚕️", "name": "man_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "man", "human"] }, - { "category": "people", "char": "🧑‍🌾", "name": "farmer", "keywords": ["rancher", "gardener", "human"] }, - { "category": "people", "char": "👩‍🌾", "name": "woman_farmer", "keywords": ["rancher", "gardener", "woman", "human"] }, - { "category": "people", "char": "👨‍🌾", "name": "man_farmer", "keywords": ["rancher", "gardener", "man", "human"] }, - { "category": "people", "char": "🧑‍🍳", "name": "cook", "keywords": ["chef", "human"] }, - { "category": "people", "char": "👩‍🍳", "name": "woman_cook", "keywords": ["chef", "woman", "human"] }, - { "category": "people", "char": "👨‍🍳", "name": "man_cook", "keywords": ["chef", "man", "human"] }, - { "category": "people", "char": "🧑‍🎓", "name": "student", "keywords": ["graduate", "human"] }, - { "category": "people", "char": "👩‍🎓", "name": "woman_student", "keywords": ["graduate", "woman", "human"] }, - { "category": "people", "char": "👨‍🎓", "name": "man_student", "keywords": ["graduate", "man", "human"] }, - { "category": "people", "char": "🧑‍🎤", "name": "singer", "keywords": ["rockstar", "entertainer", "human"] }, - { "category": "people", "char": "👩‍🎤", "name": "woman_singer", "keywords": ["rockstar", "entertainer", "woman", "human"] }, - { "category": "people", "char": "👨‍🎤", "name": "man_singer", "keywords": ["rockstar", "entertainer", "man", "human"] }, - { "category": "people", "char": "🧑‍🏫", "name": "teacher", "keywords": ["instructor", "professor", "human"] }, - { "category": "people", "char": "👩‍🏫", "name": "woman_teacher", "keywords": ["instructor", "professor", "woman", "human"] }, - { "category": "people", "char": "👨‍🏫", "name": "man_teacher", "keywords": ["instructor", "professor", "man", "human"] }, - { "category": "people", "char": "🧑‍🏭", "name": "factory_worker", "keywords": ["assembly", "industrial", "human"] }, - { "category": "people", "char": "👩‍🏭", "name": "woman_factory_worker", "keywords": ["assembly", "industrial", "woman", "human"] }, - { "category": "people", "char": "👨‍🏭", "name": "man_factory_worker", "keywords": ["assembly", "industrial", "man", "human"] }, - { "category": "people", "char": "🧑‍💻", "name": "technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "human", "laptop", "computer"] }, - { "category": "people", "char": "👩‍💻", "name": "woman_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "woman", "human", "laptop", "computer"] }, - { "category": "people", "char": "👨‍💻", "name": "man_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "man", "human", "laptop", "computer"] }, - { "category": "people", "char": "🧑‍💼", "name": "office_worker", "keywords": ["business", "manager", "human"] }, - { "category": "people", "char": "👩‍💼", "name": "woman_office_worker", "keywords": ["business", "manager", "woman", "human"] }, - { "category": "people", "char": "👨‍💼", "name": "man_office_worker", "keywords": ["business", "manager", "man", "human"] }, - { "category": "people", "char": "🧑‍🔧", "name": "mechanic", "keywords": ["plumber", "human", "wrench"] }, - { "category": "people", "char": "👩‍🔧", "name": "woman_mechanic", "keywords": ["plumber", "woman", "human", "wrench"] }, - { "category": "people", "char": "👨‍🔧", "name": "man_mechanic", "keywords": ["plumber", "man", "human", "wrench"] }, - { "category": "people", "char": "🧑‍🔬", "name": "scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "human"] }, - { "category": "people", "char": "👩‍🔬", "name": "woman_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "woman", "human"] }, - { "category": "people", "char": "👨‍🔬", "name": "man_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "man", "human"] }, - { "category": "people", "char": "🧑‍🎨", "name": "artist", "keywords": ["painter", "human"] }, - { "category": "people", "char": "👩‍🎨", "name": "woman_artist", "keywords": ["painter", "woman", "human"] }, - { "category": "people", "char": "👨‍🎨", "name": "man_artist", "keywords": ["painter", "man", "human"] }, - { "category": "people", "char": "🧑‍🚒", "name": "firefighter", "keywords": ["fireman", "human"] }, - { "category": "people", "char": "👩‍🚒", "name": "woman_firefighter", "keywords": ["fireman", "woman", "human"] }, - { "category": "people", "char": "👨‍🚒", "name": "man_firefighter", "keywords": ["fireman", "man", "human"] }, - { "category": "people", "char": "🧑‍✈️", "name": "pilot", "keywords": ["aviator", "plane", "human"] }, - { "category": "people", "char": "👩‍✈️", "name": "woman_pilot", "keywords": ["aviator", "plane", "woman", "human"] }, - { "category": "people", "char": "👨‍✈️", "name": "man_pilot", "keywords": ["aviator", "plane", "man", "human"] }, - { "category": "people", "char": "🧑‍🚀", "name": "astronaut", "keywords": ["space", "rocket", "human"] }, - { "category": "people", "char": "👩‍🚀", "name": "woman_astronaut", "keywords": ["space", "rocket", "woman", "human"] }, - { "category": "people", "char": "👨‍🚀", "name": "man_astronaut", "keywords": ["space", "rocket", "man", "human"] }, - { "category": "people", "char": "🧑‍⚖️", "name": "judge", "keywords": ["justice", "court", "human"] }, - { "category": "people", "char": "👩‍⚖️", "name": "woman_judge", "keywords": ["justice", "court", "woman", "human"] }, - { "category": "people", "char": "👨‍⚖️", "name": "man_judge", "keywords": ["justice", "court", "man", "human"] }, - { "category": "people", "char": "🦸‍♀️", "name": "woman_superhero", "keywords": ["woman", "female", "good", "heroine", "superpowers"] }, - { "category": "people", "char": "🦸‍♂️", "name": "man_superhero", "keywords": ["man", "male", "good", "hero", "superpowers"] }, - { "category": "people", "char": "🦹‍♀️", "name": "woman_supervillain", "keywords": ["woman", "female", "evil", "bad", "criminal", "heroine", "superpowers"] }, - { "category": "people", "char": "🦹‍♂️", "name": "man_supervillain", "keywords": ["man", "male", "evil", "bad", "criminal", "hero", "superpowers"] }, - { "category": "people", "char": "🤶", "name": "mrs_claus", "keywords": ["woman", "female", "xmas", "mother christmas"] }, - { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF84", "name": "mx_claus", "keywords": ["xmas", "christmas"] }, - { "category": "people", "char": "🎅", "name": "santa", "keywords": ["festival", "man", "male", "xmas", "father christmas"] }, - { "category": "people", "char": "🥷", "name": "ninja", "keywords": [] }, - { "category": "people", "char": "🧙‍♀️", "name": "sorceress", "keywords": ["woman", "female", "mage", "witch"] }, - { "category": "people", "char": "🧙‍♂️", "name": "wizard", "keywords": ["man", "male", "mage", "sorcerer"] }, - { "category": "people", "char": "🧝‍♀️", "name": "woman_elf", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧝‍♂️", "name": "man_elf", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧛‍♀️", "name": "woman_vampire", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧛‍♂️", "name": "man_vampire", "keywords": ["man", "male", "dracula"] }, - { "category": "people", "char": "🧟‍♀️", "name": "woman_zombie", "keywords": ["woman", "female", "undead", "walking dead"] }, - { "category": "people", "char": "🧟‍♂️", "name": "man_zombie", "keywords": ["man", "male", "dracula", "undead", "walking dead"] }, - { "category": "people", "char": "🧞‍♀️", "name": "woman_genie", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧞‍♂️", "name": "man_genie", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧜‍♀️", "name": "mermaid", "keywords": ["woman", "female", "merwoman", "ariel"] }, - { "category": "people", "char": "🧜‍♂️", "name": "merman", "keywords": ["man", "male", "triton"] }, - { "category": "people", "char": "🧚‍♀️", "name": "woman_fairy", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧚‍♂️", "name": "man_fairy", "keywords": ["man", "male"] }, - { "category": "people", "char": "👼", "name": "angel", "keywords": ["heaven", "wings", "halo"] }, - { "category": "people", "char": "\uD83E\uDDCC", "name": "troll", "keywords": [] }, - { "category": "people", "char": "🤰", "name": "pregnant_woman", "keywords": ["baby"] }, - { "category": "people", "char": "\uD83E\uDEC3", "name": "pregnant_man", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEC4", "name": "pregnant_person", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDEC5", "name": "person_with_crown", "keywords": [] }, - { "category": "people", "char": "🤱", "name": "breastfeeding", "keywords": ["nursing", "baby"] }, - { "category": "people", "char": "\uD83D\uDC69\u200D\uD83C\uDF7C", "name": "woman_feeding_baby", "keywords": [] }, - { "category": "people", "char": "\uD83D\uDC68\u200D\uD83C\uDF7C", "name": "man_feeding_baby", "keywords": [] }, - { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF7C", "name": "person_feeding_baby", "keywords": [] }, - { "category": "people", "char": "👸", "name": "princess", "keywords": ["girl", "woman", "female", "blond", "crown", "royal", "queen"] }, - { "category": "people", "char": "🤴", "name": "prince", "keywords": ["boy", "man", "male", "crown", "royal", "king"] }, - { "category": "people", "char": "👰", "name": "person_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] }, - { "category": "people", "char": "👰", "name": "bride_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] }, - { "category": "people", "char": "🤵", "name": "person_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] }, - { "category": "people", "char": "🤵", "name": "man_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] }, - { "category": "people", "char": "🏃‍♀️", "name": "running_woman", "keywords": ["woman", "walking", "exercise", "race", "running", "female"] }, - { "category": "people", "char": "🏃", "name": "running_man", "keywords": ["man", "walking", "exercise", "race", "running"] }, - { "category": "people", "char": "🚶‍♀️", "name": "walking_woman", "keywords": ["human", "feet", "steps", "woman", "female"] }, - { "category": "people", "char": "🚶", "name": "walking_man", "keywords": ["human", "feet", "steps"] }, - { "category": "people", "char": "💃", "name": "dancer", "keywords": ["female", "girl", "woman", "fun"] }, - { "category": "people", "char": "🕺", "name": "man_dancing", "keywords": ["male", "boy", "fun", "dancer"] }, - { "category": "people", "char": "👯", "name": "dancing_women", "keywords": ["female", "bunny", "women", "girls"] }, - { "category": "people", "char": "👯‍♂️", "name": "dancing_men", "keywords": ["male", "bunny", "men", "boys"] }, - { "category": "people", "char": "👫", "name": "couple", "keywords": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"] }, - { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1", "name": "people_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"] }, - { "category": "people", "char": "👬", "name": "two_men_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "man", "human"] }, - { "category": "people", "char": "👭", "name": "two_women_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "female", "human"] }, - { "category": "people", "char": "🫂", "name": "people_hugging", "keywords": [] }, - { "category": "people", "char": "🙇‍♀️", "name": "bowing_woman", "keywords": ["woman", "female", "girl"] }, - { "category": "people", "char": "🙇", "name": "bowing_man", "keywords": ["man", "male", "boy"] }, - { "category": "people", "char": "🤦‍♂️", "name": "man_facepalming", "keywords": ["man", "male", "boy", "disbelief"] }, - { "category": "people", "char": "🤦‍♀️", "name": "woman_facepalming", "keywords": ["woman", "female", "girl", "disbelief"] }, - { "category": "people", "char": "🤷", "name": "woman_shrugging", "keywords": ["woman", "female", "girl", "confused", "indifferent", "doubt"] }, - { "category": "people", "char": "🤷‍♂️", "name": "man_shrugging", "keywords": ["man", "male", "boy", "confused", "indifferent", "doubt"] }, - { "category": "people", "char": "💁", "name": "tipping_hand_woman", "keywords": ["female", "girl", "woman", "human", "information"] }, - { "category": "people", "char": "💁‍♂️", "name": "tipping_hand_man", "keywords": ["male", "boy", "man", "human", "information"] }, - { "category": "people", "char": "🙅", "name": "no_good_woman", "keywords": ["female", "girl", "woman", "nope"] }, - { "category": "people", "char": "🙅‍♂️", "name": "no_good_man", "keywords": ["male", "boy", "man", "nope"] }, - { "category": "people", "char": "🙆", "name": "ok_woman", "keywords": ["women", "girl", "female", "pink", "human", "woman"] }, - { "category": "people", "char": "🙆‍♂️", "name": "ok_man", "keywords": ["men", "boy", "male", "blue", "human", "man"] }, - { "category": "people", "char": "🙋", "name": "raising_hand_woman", "keywords": ["female", "girl", "woman"] }, - { "category": "people", "char": "🙋‍♂️", "name": "raising_hand_man", "keywords": ["male", "boy", "man"] }, - { "category": "people", "char": "🙎", "name": "pouting_woman", "keywords": ["female", "girl", "woman"] }, - { "category": "people", "char": "🙎‍♂️", "name": "pouting_man", "keywords": ["male", "boy", "man"] }, - { "category": "people", "char": "🙍", "name": "frowning_woman", "keywords": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"] }, - { "category": "people", "char": "🙍‍♂️", "name": "frowning_man", "keywords": ["male", "boy", "man", "sad", "depressed", "discouraged", "unhappy"] }, - { "category": "people", "char": "💇", "name": "haircut_woman", "keywords": ["female", "girl", "woman"] }, - { "category": "people", "char": "💇‍♂️", "name": "haircut_man", "keywords": ["male", "boy", "man"] }, - { "category": "people", "char": "💆", "name": "massage_woman", "keywords": ["female", "girl", "woman", "head"] }, - { "category": "people", "char": "💆‍♂️", "name": "massage_man", "keywords": ["male", "boy", "man", "head"] }, - { "category": "people", "char": "🧖‍♀️", "name": "woman_in_steamy_room", "keywords": ["female", "woman", "spa", "steamroom", "sauna"] }, - { "category": "people", "char": "🧖‍♂️", "name": "man_in_steamy_room", "keywords": ["male", "man", "spa", "steamroom", "sauna"] }, - { "category": "people", "char": "🧏‍♀️", "name": "woman_deaf", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧏‍♂️", "name": "man_deaf", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧍‍♀️", "name": "woman_standing", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧍‍♂️", "name": "man_standing", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧎‍♀️", "name": "woman_kneeling", "keywords": ["woman", "female"] }, - { "category": "people", "char": "🧎‍♂️", "name": "man_kneeling", "keywords": ["man", "male"] }, - { "category": "people", "char": "🧑‍🦯", "name": "person_with_probing_cane", "keywords": ["accessibility", "blind"] }, - { "category": "people", "char": "👩‍🦯", "name": "woman_with_probing_cane", "keywords": ["woman", "female", "accessibility", "blind"] }, - { "category": "people", "char": "👨‍🦯", "name": "man_with_probing_cane", "keywords": ["man", "male", "accessibility", "blind"] }, - { "category": "people", "char": "🧑‍🦼", "name": "person_in_motorized_wheelchair", "keywords": ["accessibility"] }, - { "category": "people", "char": "👩‍🦼", "name": "woman_in_motorized_wheelchair", "keywords": ["woman", "female", "accessibility"] }, - { "category": "people", "char": "👨‍🦼", "name": "man_in_motorized_wheelchair", "keywords": ["man", "male", "accessibility"] }, - { "category": "people", "char": "🧑‍🦽", "name": "person_in_manual_wheelchair", "keywords": ["accessibility"] }, - { "category": "people", "char": "👩‍🦽", "name": "woman_in_manual_wheelchair", "keywords": ["woman", "female", "accessibility"] }, - { "category": "people", "char": "👨‍🦽", "name": "man_in_manual_wheelchair", "keywords": ["man", "male", "accessibility"] }, - { "category": "people", "char": "💑", "name": "couple_with_heart_woman_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, - { "category": "people", "char": "👩‍❤️‍👩", "name": "couple_with_heart_woman_woman", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, - { "category": "people", "char": "👨‍❤️‍👨", "name": "couple_with_heart_man_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, - { "category": "people", "char": "💏", "name": "couplekiss_man_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, - { "category": "people", "char": "👩‍❤️‍💋‍👩", "name": "couplekiss_woman_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, - { "category": "people", "char": "👨‍❤️‍💋‍👨", "name": "couplekiss_man_man", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, - { "category": "people", "char": "👪", "name": "family_man_woman_boy", "keywords": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"] }, - { "category": "people", "char": "👨‍👩‍👧", "name": "family_man_woman_girl", "keywords": ["home", "parents", "people", "human", "child"] }, - { "category": "people", "char": "👨‍👩‍👧‍👦", "name": "family_man_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👩‍👦‍👦", "name": "family_man_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👩‍👧‍👧", "name": "family_man_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩‍👩‍👦", "name": "family_woman_woman_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩‍👩‍👧", "name": "family_woman_woman_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩‍👩‍👧‍👦", "name": "family_woman_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩‍👩‍👦‍👦", "name": "family_woman_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩‍👩‍👧‍👧", "name": "family_woman_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👨‍👦", "name": "family_man_man_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👨‍👧", "name": "family_man_man_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👨‍👧‍👦", "name": "family_man_man_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👨‍👦‍👦", "name": "family_man_man_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👨‍👧‍👧", "name": "family_man_man_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, - { "category": "people", "char": "👩‍👦", "name": "family_woman_boy", "keywords": ["home", "parent", "people", "human", "child"] }, - { "category": "people", "char": "👩‍👧", "name": "family_woman_girl", "keywords": ["home", "parent", "people", "human", "child"] }, - { "category": "people", "char": "👩‍👧‍👦", "name": "family_woman_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👩‍👦‍👦", "name": "family_woman_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👩‍👧‍👧", "name": "family_woman_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👦", "name": "family_man_boy", "keywords": ["home", "parent", "people", "human", "child"] }, - { "category": "people", "char": "👨‍👧", "name": "family_man_girl", "keywords": ["home", "parent", "people", "human", "child"] }, - { "category": "people", "char": "👨‍👧‍👦", "name": "family_man_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👦‍👦", "name": "family_man_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "👨‍👧‍👧", "name": "family_man_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] }, - { "category": "people", "char": "🧶", "name": "yarn", "keywords": ["ball", "crochet", "knit"] }, - { "category": "people", "char": "🧵", "name": "thread", "keywords": ["needle", "sewing", "spool", "string"] }, - { "category": "people", "char": "🧥", "name": "coat", "keywords": ["jacket"] }, - { "category": "people", "char": "🥼", "name": "labcoat", "keywords": ["doctor", "experiment", "scientist", "chemist"] }, - { "category": "people", "char": "👚", "name": "womans_clothes", "keywords": ["fashion", "shopping_bags", "female"] }, - { "category": "people", "char": "👕", "name": "tshirt", "keywords": ["fashion", "cloth", "casual", "shirt", "tee"] }, - { "category": "people", "char": "👖", "name": "jeans", "keywords": ["fashion", "shopping"] }, - { "category": "people", "char": "👔", "name": "necktie", "keywords": ["shirt", "suitup", "formal", "fashion", "cloth", "business"] }, - { "category": "people", "char": "👗", "name": "dress", "keywords": ["clothes", "fashion", "shopping"] }, - { "category": "people", "char": "👙", "name": "bikini", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] }, - { "category": "people", "char": "🩱", "name": "one_piece_swimsuit", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] }, - { "category": "people", "char": "👘", "name": "kimono", "keywords": ["dress", "fashion", "women", "female", "japanese"] }, - { "category": "people", "char": "🥻", "name": "sari", "keywords": ["dress", "fashion", "women", "female"] }, - { "category": "people", "char": "🩲", "name": "briefs", "keywords": ["dress", "fashion"] }, - { "category": "people", "char": "🩳", "name": "shorts", "keywords": ["dress", "fashion"] }, - { "category": "people", "char": "💄", "name": "lipstick", "keywords": ["female", "girl", "fashion", "woman"] }, - { "category": "people", "char": "💋", "name": "kiss", "keywords": ["face", "lips", "love", "like", "affection", "valentines"] }, - { "category": "people", "char": "👣", "name": "footprints", "keywords": ["feet", "tracking", "walking", "beach"] }, - { "category": "people", "char": "🥿", "name": "flat_shoe", "keywords": ["ballet", "slip-on", "slipper"] }, - { "category": "people", "char": "👠", "name": "high_heel", "keywords": ["fashion", "shoes", "female", "pumps", "stiletto"] }, - { "category": "people", "char": "👡", "name": "sandal", "keywords": ["shoes", "fashion", "flip flops"] }, - { "category": "people", "char": "👢", "name": "boot", "keywords": ["shoes", "fashion"] }, - { "category": "people", "char": "👞", "name": "mans_shoe", "keywords": ["fashion", "male"] }, - { "category": "people", "char": "👟", "name": "athletic_shoe", "keywords": ["shoes", "sports", "sneakers"] }, - { "category": "people", "char": "🩴", "name": "thong_sandal", "keywords": [] }, - { "category": "people", "char": "🩰", "name": "ballet_shoes", "keywords": ["shoes", "sports"] }, - { "category": "people", "char": "🧦", "name": "socks", "keywords": ["stockings", "clothes"] }, - { "category": "people", "char": "🧤", "name": "gloves", "keywords": ["hands", "winter", "clothes"] }, - { "category": "people", "char": "🧣", "name": "scarf", "keywords": ["neck", "winter", "clothes"] }, - { "category": "people", "char": "👒", "name": "womans_hat", "keywords": ["fashion", "accessories", "female", "lady", "spring"] }, - { "category": "people", "char": "🎩", "name": "tophat", "keywords": ["magic", "gentleman", "classy", "circus"] }, - { "category": "people", "char": "🧢", "name": "billed_hat", "keywords": ["cap", "baseball"] }, - { "category": "people", "char": "⛑", "name": "rescue_worker_helmet", "keywords": ["construction", "build"] }, - { "category": "people", "char": "🪖", "name": "military_helmet", "keywords": [] }, - { "category": "people", "char": "🎓", "name": "mortar_board", "keywords": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"] }, - { "category": "people", "char": "👑", "name": "crown", "keywords": ["king", "kod", "leader", "royalty", "lord"] }, - { "category": "people", "char": "🎒", "name": "school_satchel", "keywords": ["student", "education", "bag", "backpack"] }, - { "category": "people", "char": "🧳", "name": "luggage", "keywords": ["packing", "travel"] }, - { "category": "people", "char": "👝", "name": "pouch", "keywords": ["bag", "accessories", "shopping"] }, - { "category": "people", "char": "👛", "name": "purse", "keywords": ["fashion", "accessories", "money", "sales", "shopping"] }, - { "category": "people", "char": "👜", "name": "handbag", "keywords": ["fashion", "accessory", "accessories", "shopping"] }, - { "category": "people", "char": "💼", "name": "briefcase", "keywords": ["business", "documents", "work", "law", "legal", "job", "career"] }, - { "category": "people", "char": "👓", "name": "eyeglasses", "keywords": ["fashion", "accessories", "eyesight", "nerdy", "dork", "geek"] }, - { "category": "people", "char": "🕶", "name": "dark_sunglasses", "keywords": ["face", "cool", "accessories"] }, - { "category": "people", "char": "🥽", "name": "goggles", "keywords": ["eyes", "protection", "safety"] }, - { "category": "people", "char": "💍", "name": "ring", "keywords": ["wedding", "propose", "marriage", "valentines", "diamond", "fashion", "jewelry", "gem", "engagement"] }, - { "category": "people", "char": "🌂", "name": "closed_umbrella", "keywords": ["weather", "rain", "drizzle"] }, - { "category": "animals_and_nature", "char": "🐶", "name": "dog", "keywords": ["animal", "friend", "nature", "woof", "puppy", "pet", "faithful"] }, - { "category": "animals_and_nature", "char": "🐱", "name": "cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] }, - { "category": "animals_and_nature", "char": "🐈‍⬛", "name": "black_cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] }, - { "category": "animals_and_nature", "char": "🐭", "name": "mouse", "keywords": ["animal", "nature", "cheese_wedge", "rodent"] }, - { "category": "animals_and_nature", "char": "🐹", "name": "hamster", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐰", "name": "rabbit", "keywords": ["animal", "nature", "pet", "spring", "magic", "bunny"] }, - { "category": "animals_and_nature", "char": "🦊", "name": "fox_face", "keywords": ["animal", "nature", "face"] }, - { "category": "animals_and_nature", "char": "🐻", "name": "bear", "keywords": ["animal", "nature", "wild"] }, - { "category": "animals_and_nature", "char": "🐼", "name": "panda_face", "keywords": ["animal", "nature", "panda"] }, - { "category": "animals_and_nature", "char": "🐨", "name": "koala", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐯", "name": "tiger", "keywords": ["animal", "cat", "danger", "wild", "nature", "roar"] }, - { "category": "animals_and_nature", "char": "🦁", "name": "lion", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐮", "name": "cow", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] }, - { "category": "animals_and_nature", "char": "🐷", "name": "pig", "keywords": ["animal", "oink", "nature"] }, - { "category": "animals_and_nature", "char": "🐽", "name": "pig_nose", "keywords": ["animal", "oink"] }, - { "category": "animals_and_nature", "char": "🐸", "name": "frog", "keywords": ["animal", "nature", "croak", "toad"] }, - { "category": "animals_and_nature", "char": "🦑", "name": "squid", "keywords": ["animal", "nature", "ocean", "sea"] }, - { "category": "animals_and_nature", "char": "🐙", "name": "octopus", "keywords": ["animal", "creature", "ocean", "sea", "nature", "beach"] }, - { "category": "animals_and_nature", "char": "🦐", "name": "shrimp", "keywords": ["animal", "ocean", "nature", "seafood"] }, - { "category": "animals_and_nature", "char": "🐵", "name": "monkey_face", "keywords": ["animal", "nature", "circus"] }, - { "category": "animals_and_nature", "char": "🦍", "name": "gorilla", "keywords": ["animal", "nature", "circus"] }, - { "category": "animals_and_nature", "char": "🙈", "name": "see_no_evil", "keywords": ["monkey", "animal", "nature", "haha"] }, - { "category": "animals_and_nature", "char": "🙉", "name": "hear_no_evil", "keywords": ["animal", "monkey", "nature"] }, - { "category": "animals_and_nature", "char": "🙊", "name": "speak_no_evil", "keywords": ["monkey", "animal", "nature", "omg"] }, - { "category": "animals_and_nature", "char": "🐒", "name": "monkey", "keywords": ["animal", "nature", "banana", "circus"] }, - { "category": "animals_and_nature", "char": "🐔", "name": "chicken", "keywords": ["animal", "cluck", "nature", "bird"] }, - { "category": "animals_and_nature", "char": "🐧", "name": "penguin", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐦", "name": "bird", "keywords": ["animal", "nature", "fly", "tweet", "spring"] }, - { "category": "animals_and_nature", "char": "🐤", "name": "baby_chick", "keywords": ["animal", "chicken", "bird"] }, - { "category": "animals_and_nature", "char": "🐣", "name": "hatching_chick", "keywords": ["animal", "chicken", "egg", "born", "baby", "bird"] }, - { "category": "animals_and_nature", "char": "🐥", "name": "hatched_chick", "keywords": ["animal", "chicken", "baby", "bird"] }, - { "category": "animals_and_nature", "char": "🦆", "name": "duck", "keywords": ["animal", "nature", "bird", "mallard"] }, - { "category": "animals_and_nature", "char": "🦅", "name": "eagle", "keywords": ["animal", "nature", "bird"] }, - { "category": "animals_and_nature", "char": "🦉", "name": "owl", "keywords": ["animal", "nature", "bird", "hoot"] }, - { "category": "animals_and_nature", "char": "🦇", "name": "bat", "keywords": ["animal", "nature", "blind", "vampire"] }, - { "category": "animals_and_nature", "char": "🐺", "name": "wolf", "keywords": ["animal", "nature", "wild"] }, - { "category": "animals_and_nature", "char": "🐗", "name": "boar", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐴", "name": "horse", "keywords": ["animal", "brown", "nature"] }, - { "category": "animals_and_nature", "char": "🦄", "name": "unicorn", "keywords": ["animal", "nature", "mystical"] }, - { "category": "animals_and_nature", "char": "🐝", "name": "honeybee", "keywords": ["animal", "insect", "nature", "bug", "spring", "honey"] }, - { "category": "animals_and_nature", "char": "🐛", "name": "bug", "keywords": ["animal", "insect", "nature", "worm"] }, - { "category": "animals_and_nature", "char": "🦋", "name": "butterfly", "keywords": ["animal", "insect", "nature", "caterpillar"] }, - { "category": "animals_and_nature", "char": "🐌", "name": "snail", "keywords": ["slow", "animal", "shell"] }, - { "category": "animals_and_nature", "char": "🐞", "name": "lady_beetle", "keywords": ["animal", "insect", "nature", "ladybug"] }, - { "category": "animals_and_nature", "char": "🐜", "name": "ant", "keywords": ["animal", "insect", "nature", "bug"] }, - { "category": "animals_and_nature", "char": "🦗", "name": "grasshopper", "keywords": ["animal", "cricket", "chirp"] }, - { "category": "animals_and_nature", "char": "🕷", "name": "spider", "keywords": ["animal", "arachnid"] }, - { "category": "animals_and_nature", "char": "🪲", "name": "beetle", "keywords": ["animal"] }, - { "category": "animals_and_nature", "char": "🪳", "name": "cockroach", "keywords": ["animal"] }, - { "category": "animals_and_nature", "char": "🪰", "name": "fly", "keywords": ["animal"] }, - { "category": "animals_and_nature", "char": "🪱", "name": "worm", "keywords": ["animal"] }, - { "category": "animals_and_nature", "char": "🦂", "name": "scorpion", "keywords": ["animal", "arachnid"] }, - { "category": "animals_and_nature", "char": "🦀", "name": "crab", "keywords": ["animal", "crustacean"] }, - { "category": "animals_and_nature", "char": "🐍", "name": "snake", "keywords": ["animal", "evil", "nature", "hiss", "python"] }, - { "category": "animals_and_nature", "char": "🦎", "name": "lizard", "keywords": ["animal", "nature", "reptile"] }, - { "category": "animals_and_nature", "char": "🦖", "name": "t-rex", "keywords": ["animal", "nature", "dinosaur", "tyrannosaurus", "extinct"] }, - { "category": "animals_and_nature", "char": "🦕", "name": "sauropod", "keywords": ["animal", "nature", "dinosaur", "brachiosaurus", "brontosaurus", "diplodocus", "extinct"] }, - { "category": "animals_and_nature", "char": "🐢", "name": "turtle", "keywords": ["animal", "slow", "nature", "tortoise"] }, - { "category": "animals_and_nature", "char": "🐠", "name": "tropical_fish", "keywords": ["animal", "swim", "ocean", "beach", "nemo"] }, - { "category": "animals_and_nature", "char": "🐟", "name": "fish", "keywords": ["animal", "food", "nature"] }, - { "category": "animals_and_nature", "char": "🐡", "name": "blowfish", "keywords": ["animal", "nature", "food", "sea", "ocean"] }, - { "category": "animals_and_nature", "char": "🐬", "name": "dolphin", "keywords": ["animal", "nature", "fish", "sea", "ocean", "flipper", "fins", "beach"] }, - { "category": "animals_and_nature", "char": "🦈", "name": "shark", "keywords": ["animal", "nature", "fish", "sea", "ocean", "jaws", "fins", "beach"] }, - { "category": "animals_and_nature", "char": "🐳", "name": "whale", "keywords": ["animal", "nature", "sea", "ocean"] }, - { "category": "animals_and_nature", "char": "🐋", "name": "whale2", "keywords": ["animal", "nature", "sea", "ocean"] }, - { "category": "animals_and_nature", "char": "🐊", "name": "crocodile", "keywords": ["animal", "nature", "reptile", "lizard", "alligator"] }, - { "category": "animals_and_nature", "char": "🐆", "name": "leopard", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦓", "name": "zebra", "keywords": ["animal", "nature", "stripes", "safari"] }, - { "category": "animals_and_nature", "char": "🐅", "name": "tiger2", "keywords": ["animal", "nature", "roar"] }, - { "category": "animals_and_nature", "char": "🐃", "name": "water_buffalo", "keywords": ["animal", "nature", "ox", "cow"] }, - { "category": "animals_and_nature", "char": "🐂", "name": "ox", "keywords": ["animal", "cow", "beef"] }, - { "category": "animals_and_nature", "char": "🐄", "name": "cow2", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] }, - { "category": "animals_and_nature", "char": "🦌", "name": "deer", "keywords": ["animal", "nature", "horns", "venison"] }, - { "category": "animals_and_nature", "char": "🐪", "name": "dromedary_camel", "keywords": ["animal", "hot", "desert", "hump"] }, - { "category": "animals_and_nature", "char": "🐫", "name": "camel", "keywords": ["animal", "nature", "hot", "desert", "hump"] }, - { "category": "animals_and_nature", "char": "🦒", "name": "giraffe", "keywords": ["animal", "nature", "spots", "safari"] }, - { "category": "animals_and_nature", "char": "🐘", "name": "elephant", "keywords": ["animal", "nature", "nose", "th", "circus"] }, - { "category": "animals_and_nature", "char": "🦏", "name": "rhinoceros", "keywords": ["animal", "nature", "horn"] }, - { "category": "animals_and_nature", "char": "🐐", "name": "goat", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐏", "name": "ram", "keywords": ["animal", "sheep", "nature"] }, - { "category": "animals_and_nature", "char": "🐑", "name": "sheep", "keywords": ["animal", "nature", "wool", "shipit"] }, - { "category": "animals_and_nature", "char": "🐎", "name": "racehorse", "keywords": ["animal", "gamble", "luck"] }, - { "category": "animals_and_nature", "char": "🐖", "name": "pig2", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐀", "name": "rat", "keywords": ["animal", "mouse", "rodent"] }, - { "category": "animals_and_nature", "char": "🐁", "name": "mouse2", "keywords": ["animal", "nature", "rodent"] }, - { "category": "animals_and_nature", "char": "🐓", "name": "rooster", "keywords": ["animal", "nature", "chicken"] }, - { "category": "animals_and_nature", "char": "🦃", "name": "turkey", "keywords": ["animal", "bird"] }, - { "category": "animals_and_nature", "char": "🕊", "name": "dove", "keywords": ["animal", "bird"] }, - { "category": "animals_and_nature", "char": "🐕", "name": "dog2", "keywords": ["animal", "nature", "friend", "doge", "pet", "faithful"] }, - { "category": "animals_and_nature", "char": "🐩", "name": "poodle", "keywords": ["dog", "animal", "101", "nature", "pet"] }, - { "category": "animals_and_nature", "char": "🐈", "name": "cat2", "keywords": ["animal", "meow", "pet", "cats"] }, - { "category": "animals_and_nature", "char": "🐇", "name": "rabbit2", "keywords": ["animal", "nature", "pet", "magic", "spring"] }, - { "category": "animals_and_nature", "char": "🐿", "name": "chipmunk", "keywords": ["animal", "nature", "rodent", "squirrel"] }, - { "category": "animals_and_nature", "char": "🦔", "name": "hedgehog", "keywords": ["animal", "nature", "spiny"] }, - { "category": "animals_and_nature", "char": "🦝", "name": "raccoon", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦙", "name": "llama", "keywords": ["animal", "nature", "alpaca"] }, - { "category": "animals_and_nature", "char": "🦛", "name": "hippopotamus", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦘", "name": "kangaroo", "keywords": ["animal", "nature", "australia", "joey", "hop", "marsupial"] }, - { "category": "animals_and_nature", "char": "🦡", "name": "badger", "keywords": ["animal", "nature", "honey"] }, - { "category": "animals_and_nature", "char": "🦢", "name": "swan", "keywords": ["animal", "nature", "bird"] }, - { "category": "animals_and_nature", "char": "🦚", "name": "peacock", "keywords": ["animal", "nature", "peahen", "bird"] }, - { "category": "animals_and_nature", "char": "🦜", "name": "parrot", "keywords": ["animal", "nature", "bird", "pirate", "talk"] }, - { "category": "animals_and_nature", "char": "🦞", "name": "lobster", "keywords": ["animal", "nature", "bisque", "claws", "seafood"] }, - { "category": "animals_and_nature", "char": "🦠", "name": "microbe", "keywords": ["amoeba", "bacteria", "germs"] }, - { "category": "animals_and_nature", "char": "🦟", "name": "mosquito", "keywords": ["animal", "nature", "insect", "malaria"] }, - { "category": "animals_and_nature", "char": "🦬", "name": "bison", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦣", "name": "mammoth", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦫", "name": "beaver", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐻‍❄️", "name": "polar_bear", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦤", "name": "dodo", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🪶", "name": "feather", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦭", "name": "seal", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐾", "name": "paw_prints", "keywords": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"] }, - { "category": "animals_and_nature", "char": "🐉", "name": "dragon", "keywords": ["animal", "myth", "nature", "chinese", "green"] }, - { "category": "animals_and_nature", "char": "🐲", "name": "dragon_face", "keywords": ["animal", "myth", "nature", "chinese", "green"] }, - { "category": "animals_and_nature", "char": "🦧", "name": "orangutan", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦮", "name": "guide_dog", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🐕‍🦺", "name": "service_dog", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦥", "name": "sloth", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦦", "name": "otter", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦨", "name": "skunk", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🦩", "name": "flamingo", "keywords": ["animal", "nature"] }, - { "category": "animals_and_nature", "char": "🌵", "name": "cactus", "keywords": ["vegetable", "plant", "nature"] }, - { "category": "animals_and_nature", "char": "🎄", "name": "christmas_tree", "keywords": ["festival", "vacation", "december", "xmas", "celebration"] }, - { "category": "animals_and_nature", "char": "🌲", "name": "evergreen_tree", "keywords": ["plant", "nature"] }, - { "category": "animals_and_nature", "char": "🌳", "name": "deciduous_tree", "keywords": ["plant", "nature"] }, - { "category": "animals_and_nature", "char": "🌴", "name": "palm_tree", "keywords": ["plant", "vegetable", "nature", "summer", "beach", "mojito", "tropical"] }, - { "category": "animals_and_nature", "char": "🌱", "name": "seedling", "keywords": ["plant", "nature", "grass", "lawn", "spring"] }, - { "category": "animals_and_nature", "char": "🌿", "name": "herb", "keywords": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"] }, - { "category": "animals_and_nature", "char": "☘", "name": "shamrock", "keywords": ["vegetable", "plant", "nature", "irish", "clover"] }, - { "category": "animals_and_nature", "char": "🍀", "name": "four_leaf_clover", "keywords": ["vegetable", "plant", "nature", "lucky", "irish"] }, - { "category": "animals_and_nature", "char": "🎍", "name": "bamboo", "keywords": ["plant", "nature", "vegetable", "panda", "pine_decoration"] }, - { "category": "animals_and_nature", "char": "🎋", "name": "tanabata_tree", "keywords": ["plant", "nature", "branch", "summer"] }, - { "category": "animals_and_nature", "char": "🍃", "name": "leaves", "keywords": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"] }, - { "category": "animals_and_nature", "char": "🍂", "name": "fallen_leaf", "keywords": ["nature", "plant", "vegetable", "leaves"] }, - { "category": "animals_and_nature", "char": "🍁", "name": "maple_leaf", "keywords": ["nature", "plant", "vegetable", "ca", "fall"] }, - { "category": "animals_and_nature", "char": "🌾", "name": "ear_of_rice", "keywords": ["nature", "plant"] }, - { "category": "animals_and_nature", "char": "🌺", "name": "hibiscus", "keywords": ["plant", "vegetable", "flowers", "beach"] }, - { "category": "animals_and_nature", "char": "🌻", "name": "sunflower", "keywords": ["nature", "plant", "fall"] }, - { "category": "animals_and_nature", "char": "🌹", "name": "rose", "keywords": ["flowers", "valentines", "love", "spring"] }, - { "category": "animals_and_nature", "char": "🥀", "name": "wilted_flower", "keywords": ["plant", "nature", "flower"] }, - { "category": "animals_and_nature", "char": "🌷", "name": "tulip", "keywords": ["flowers", "plant", "nature", "summer", "spring"] }, - { "category": "animals_and_nature", "char": "🌼", "name": "blossom", "keywords": ["nature", "flowers", "yellow"] }, - { "category": "animals_and_nature", "char": "🌸", "name": "cherry_blossom", "keywords": ["nature", "plant", "spring", "flower"] }, - { "category": "animals_and_nature", "char": "💐", "name": "bouquet", "keywords": ["flowers", "nature", "spring"] }, - { "category": "animals_and_nature", "char": "🍄", "name": "mushroom", "keywords": ["plant", "vegetable"] }, - { "category": "animals_and_nature", "char": "🪴", "name": "potted_plant", "keywords": ["plant"] }, - { "category": "animals_and_nature", "char": "🌰", "name": "chestnut", "keywords": ["food", "squirrel"] }, - { "category": "animals_and_nature", "char": "🎃", "name": "jack_o_lantern", "keywords": ["halloween", "light", "pumpkin", "creepy", "fall"] }, - { "category": "animals_and_nature", "char": "🐚", "name": "shell", "keywords": ["nature", "sea", "beach"] }, - { "category": "animals_and_nature", "char": "🕸", "name": "spider_web", "keywords": ["animal", "insect", "arachnid", "silk"] }, - { "category": "animals_and_nature", "char": "🌎", "name": "earth_americas", "keywords": ["globe", "world", "USA", "international"] }, - { "category": "animals_and_nature", "char": "🌍", "name": "earth_africa", "keywords": ["globe", "world", "international"] }, - { "category": "animals_and_nature", "char": "🌏", "name": "earth_asia", "keywords": ["globe", "world", "east", "international"] }, - { "category": "animals_and_nature", "char": "🪐", "name": "ringed_planet", "keywords": ["saturn"] }, - { "category": "animals_and_nature", "char": "🌕", "name": "full_moon", "keywords": ["nature", "yellow", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌖", "name": "waning_gibbous_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep", "waxing_gibbous_moon"] }, - { "category": "animals_and_nature", "char": "🌗", "name": "last_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌘", "name": "waning_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌑", "name": "new_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌒", "name": "waxing_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌓", "name": "first_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌔", "name": "waxing_gibbous_moon", "keywords": ["nature", "night", "sky", "gray", "twilight", "planet", "space", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌚", "name": "new_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌝", "name": "full_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌛", "name": "first_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌜", "name": "last_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, - { "category": "animals_and_nature", "char": "🌞", "name": "sun_with_face", "keywords": ["nature", "morning", "sky"] }, - { "category": "animals_and_nature", "char": "🌙", "name": "crescent_moon", "keywords": ["night", "sleep", "sky", "evening", "magic"] }, - { "category": "animals_and_nature", "char": "⭐", "name": "star", "keywords": ["night", "yellow"] }, - { "category": "animals_and_nature", "char": "🌟", "name": "star2", "keywords": ["night", "sparkle", "awesome", "good", "magic"] }, - { "category": "animals_and_nature", "char": "💫", "name": "dizzy", "keywords": ["star", "sparkle", "shoot", "magic"] }, - { "category": "animals_and_nature", "char": "✨", "name": "sparkles", "keywords": ["stars", "shine", "shiny", "cool", "awesome", "good", "magic"] }, - { "category": "animals_and_nature", "char": "☄", "name": "comet", "keywords": ["space"] }, - { "category": "animals_and_nature", "char": "☀️", "name": "sunny", "keywords": ["weather", "nature", "brightness", "summer", "beach", "spring"] }, - { "category": "animals_and_nature", "char": "🌤", "name": "sun_behind_small_cloud", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "⛅", "name": "partly_sunny", "keywords": ["weather", "nature", "cloudy", "morning", "fall", "spring"] }, - { "category": "animals_and_nature", "char": "🌥", "name": "sun_behind_large_cloud", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "🌦", "name": "sun_behind_rain_cloud", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "☁️", "name": "cloud", "keywords": ["weather", "sky"] }, - { "category": "animals_and_nature", "char": "🌧", "name": "cloud_with_rain", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "⛈", "name": "cloud_with_lightning_and_rain", "keywords": ["weather", "lightning"] }, - { "category": "animals_and_nature", "char": "🌩", "name": "cloud_with_lightning", "keywords": ["weather", "thunder"] }, - { "category": "animals_and_nature", "char": "⚡", "name": "zap", "keywords": ["thunder", "weather", "lightning bolt", "fast"] }, - { "category": "animals_and_nature", "char": "🔥", "name": "fire", "keywords": ["hot", "cook", "flame"] }, - { "category": "animals_and_nature", "char": "💥", "name": "boom", "keywords": ["bomb", "explode", "explosion", "collision", "blown"] }, - { "category": "animals_and_nature", "char": "❄️", "name": "snowflake", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas"] }, - { "category": "animals_and_nature", "char": "🌨", "name": "cloud_with_snow", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "⛄", "name": "snowman", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen", "without_snow"] }, - { "category": "animals_and_nature", "char": "☃", "name": "snowman_with_snow", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen"] }, - { "category": "animals_and_nature", "char": "🌬", "name": "wind_face", "keywords": ["gust", "air"] }, - { "category": "animals_and_nature", "char": "💨", "name": "dash", "keywords": ["wind", "air", "fast", "shoo", "fart", "smoke", "puff"] }, - { "category": "animals_and_nature", "char": "🌪", "name": "tornado", "keywords": ["weather", "cyclone", "twister"] }, - { "category": "animals_and_nature", "char": "🌫", "name": "fog", "keywords": ["weather"] }, - { "category": "animals_and_nature", "char": "☂", "name": "open_umbrella", "keywords": ["weather", "spring"] }, - { "category": "animals_and_nature", "char": "☔", "name": "umbrella", "keywords": ["rainy", "weather", "spring"] }, - { "category": "animals_and_nature", "char": "💧", "name": "droplet", "keywords": ["water", "drip", "faucet", "spring"] }, - { "category": "animals_and_nature", "char": "💦", "name": "sweat_drops", "keywords": ["water", "drip", "oops"] }, - { "category": "animals_and_nature", "char": "🌊", "name": "ocean", "keywords": ["sea", "water", "wave", "nature", "tsunami", "disaster"] }, - { "category": "animals_and_nature", "char": "\uD83E\uDEB7", "name": "lotus", "keywords": [] }, - { "category": "animals_and_nature", "char": "\uD83E\uDEB8", "name": "coral", "keywords": [] }, - { "category": "animals_and_nature", "char": "\uD83E\uDEB9", "name": "empty_nest", "keywords": [] }, - { "category": "animals_and_nature", "char": "\uD83E\uDEBA", "name": "nest_with_eggs", "keywords": [] }, - { "category": "food_and_drink", "char": "🍏", "name": "green_apple", "keywords": ["fruit", "nature"] }, - { "category": "food_and_drink", "char": "🍎", "name": "apple", "keywords": ["fruit", "mac", "school"] }, - { "category": "food_and_drink", "char": "🍐", "name": "pear", "keywords": ["fruit", "nature", "food"] }, - { "category": "food_and_drink", "char": "🍊", "name": "tangerine", "keywords": ["food", "fruit", "nature", "orange"] }, - { "category": "food_and_drink", "char": "🍋", "name": "lemon", "keywords": ["fruit", "nature"] }, - { "category": "food_and_drink", "char": "🍌", "name": "banana", "keywords": ["fruit", "food", "monkey"] }, - { "category": "food_and_drink", "char": "🍉", "name": "watermelon", "keywords": ["fruit", "food", "picnic", "summer"] }, - { "category": "food_and_drink", "char": "🍇", "name": "grapes", "keywords": ["fruit", "food", "wine"] }, - { "category": "food_and_drink", "char": "🍓", "name": "strawberry", "keywords": ["fruit", "food", "nature"] }, - { "category": "food_and_drink", "char": "🍈", "name": "melon", "keywords": ["fruit", "nature", "food"] }, - { "category": "food_and_drink", "char": "🍒", "name": "cherries", "keywords": ["food", "fruit"] }, - { "category": "food_and_drink", "char": "🍑", "name": "peach", "keywords": ["fruit", "nature", "food"] }, - { "category": "food_and_drink", "char": "🍍", "name": "pineapple", "keywords": ["fruit", "nature", "food"] }, - { "category": "food_and_drink", "char": "🥥", "name": "coconut", "keywords": ["fruit", "nature", "food", "palm"] }, - { "category": "food_and_drink", "char": "🥝", "name": "kiwi_fruit", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🥭", "name": "mango", "keywords": ["fruit", "food", "tropical"] }, - { "category": "food_and_drink", "char": "🥑", "name": "avocado", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🥦", "name": "broccoli", "keywords": ["fruit", "food", "vegetable"] }, - { "category": "food_and_drink", "char": "🍅", "name": "tomato", "keywords": ["fruit", "vegetable", "nature", "food"] }, - { "category": "food_and_drink", "char": "🍆", "name": "eggplant", "keywords": ["vegetable", "nature", "food", "aubergine"] }, - { "category": "food_and_drink", "char": "🥒", "name": "cucumber", "keywords": ["fruit", "food", "pickle"] }, - { "category": "food_and_drink", "char": "🫐", "name": "blueberries", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🫒", "name": "olive", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🫑", "name": "bell_pepper", "keywords": ["fruit", "food"] }, - { "category": "food_and_drink", "char": "🥕", "name": "carrot", "keywords": ["vegetable", "food", "orange"] }, - { "category": "food_and_drink", "char": "🌶", "name": "hot_pepper", "keywords": ["food", "spicy", "chilli", "chili"] }, - { "category": "food_and_drink", "char": "🥔", "name": "potato", "keywords": ["food", "tuber", "vegatable", "starch"] }, - { "category": "food_and_drink", "char": "🌽", "name": "corn", "keywords": ["food", "vegetable", "plant"] }, - { "category": "food_and_drink", "char": "🥬", "name": "leafy_greens", "keywords": ["food", "vegetable", "plant", "bok choy", "cabbage", "kale", "lettuce"] }, - { "category": "food_and_drink", "char": "🍠", "name": "sweet_potato", "keywords": ["food", "nature"] }, - { "category": "food_and_drink", "char": "🥜", "name": "peanuts", "keywords": ["food", "nut"] }, - { "category": "food_and_drink", "char": "🧄", "name": "garlic", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🧅", "name": "onion", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🍯", "name": "honey_pot", "keywords": ["bees", "sweet", "kitchen"] }, - { "category": "food_and_drink", "char": "🥐", "name": "croissant", "keywords": ["food", "bread", "french"] }, - { "category": "food_and_drink", "char": "🍞", "name": "bread", "keywords": ["food", "wheat", "breakfast", "toast"] }, - { "category": "food_and_drink", "char": "🥖", "name": "baguette_bread", "keywords": ["food", "bread", "french"] }, - { "category": "food_and_drink", "char": "🥯", "name": "bagel", "keywords": ["food", "bread", "bakery", "schmear"] }, - { "category": "food_and_drink", "char": "🥨", "name": "pretzel", "keywords": ["food", "bread", "twisted"] }, - { "category": "food_and_drink", "char": "🧀", "name": "cheese", "keywords": ["food", "chadder"] }, - { "category": "food_and_drink", "char": "🥚", "name": "egg", "keywords": ["food", "chicken", "breakfast"] }, - { "category": "food_and_drink", "char": "🥓", "name": "bacon", "keywords": ["food", "breakfast", "pork", "pig", "meat"] }, - { "category": "food_and_drink", "char": "🥩", "name": "steak", "keywords": ["food", "cow", "meat", "cut", "chop", "lambchop", "porkchop"] }, - { "category": "food_and_drink", "char": "🥞", "name": "pancakes", "keywords": ["food", "breakfast", "flapjacks", "hotcakes"] }, - { "category": "food_and_drink", "char": "🍗", "name": "poultry_leg", "keywords": ["food", "meat", "drumstick", "bird", "chicken", "turkey"] }, - { "category": "food_and_drink", "char": "🍖", "name": "meat_on_bone", "keywords": ["good", "food", "drumstick"] }, - { "category": "food_and_drink", "char": "🦴", "name": "bone", "keywords": ["skeleton"] }, - { "category": "food_and_drink", "char": "🍤", "name": "fried_shrimp", "keywords": ["food", "animal", "appetizer", "summer"] }, - { "category": "food_and_drink", "char": "🍳", "name": "fried_egg", "keywords": ["food", "breakfast", "kitchen", "egg"] }, - { "category": "food_and_drink", "char": "🍔", "name": "hamburger", "keywords": ["meat", "fast food", "beef", "cheeseburger", "mcdonalds", "burger king"] }, - { "category": "food_and_drink", "char": "🍟", "name": "fries", "keywords": ["chips", "snack", "fast food"] }, - { "category": "food_and_drink", "char": "🥙", "name": "stuffed_flatbread", "keywords": ["food", "flatbread", "stuffed", "gyro"] }, - { "category": "food_and_drink", "char": "🌭", "name": "hotdog", "keywords": ["food", "frankfurter"] }, - { "category": "food_and_drink", "char": "🍕", "name": "pizza", "keywords": ["food", "party"] }, - { "category": "food_and_drink", "char": "🥪", "name": "sandwich", "keywords": ["food", "lunch", "bread"] }, - { "category": "food_and_drink", "char": "🥫", "name": "canned_food", "keywords": ["food", "soup"] }, - { "category": "food_and_drink", "char": "🍝", "name": "spaghetti", "keywords": ["food", "italian", "noodle"] }, - { "category": "food_and_drink", "char": "🌮", "name": "taco", "keywords": ["food", "mexican"] }, - { "category": "food_and_drink", "char": "🌯", "name": "burrito", "keywords": ["food", "mexican"] }, - { "category": "food_and_drink", "char": "🥗", "name": "green_salad", "keywords": ["food", "healthy", "lettuce"] }, - { "category": "food_and_drink", "char": "🥘", "name": "shallow_pan_of_food", "keywords": ["food", "cooking", "casserole", "paella"] }, - { "category": "food_and_drink", "char": "🍜", "name": "ramen", "keywords": ["food", "japanese", "noodle", "chopsticks"] }, - { "category": "food_and_drink", "char": "🍲", "name": "stew", "keywords": ["food", "meat", "soup"] }, - { "category": "food_and_drink", "char": "🍥", "name": "fish_cake", "keywords": ["food", "japan", "sea", "beach", "narutomaki", "pink", "swirl", "kamaboko", "surimi", "ramen"] }, - { "category": "food_and_drink", "char": "🥠", "name": "fortune_cookie", "keywords": ["food", "prophecy"] }, - { "category": "food_and_drink", "char": "🍣", "name": "sushi", "keywords": ["food", "fish", "japanese", "rice"] }, - { "category": "food_and_drink", "char": "🍱", "name": "bento", "keywords": ["food", "japanese", "box"] }, - { "category": "food_and_drink", "char": "🍛", "name": "curry", "keywords": ["food", "spicy", "hot", "indian"] }, - { "category": "food_and_drink", "char": "🍙", "name": "rice_ball", "keywords": ["food", "japanese"] }, - { "category": "food_and_drink", "char": "🍚", "name": "rice", "keywords": ["food", "china", "asian"] }, - { "category": "food_and_drink", "char": "🍘", "name": "rice_cracker", "keywords": ["food", "japanese"] }, - { "category": "food_and_drink", "char": "🍢", "name": "oden", "keywords": ["food", "japanese"] }, - { "category": "food_and_drink", "char": "🍡", "name": "dango", "keywords": ["food", "dessert", "sweet", "japanese", "barbecue", "meat"] }, - { "category": "food_and_drink", "char": "🍧", "name": "shaved_ice", "keywords": ["hot", "dessert", "summer"] }, - { "category": "food_and_drink", "char": "🍨", "name": "ice_cream", "keywords": ["food", "hot", "dessert"] }, - { "category": "food_and_drink", "char": "🍦", "name": "icecream", "keywords": ["food", "hot", "dessert", "summer"] }, - { "category": "food_and_drink", "char": "🥧", "name": "pie", "keywords": ["food", "dessert", "pastry"] }, - { "category": "food_and_drink", "char": "🍰", "name": "cake", "keywords": ["food", "dessert"] }, - { "category": "food_and_drink", "char": "🧁", "name": "cupcake", "keywords": ["food", "dessert", "bakery", "sweet"] }, - { "category": "food_and_drink", "char": "🥮", "name": "moon_cake", "keywords": ["food", "autumn"] }, - { "category": "food_and_drink", "char": "🎂", "name": "birthday", "keywords": ["food", "dessert", "cake"] }, - { "category": "food_and_drink", "char": "🍮", "name": "custard", "keywords": ["dessert", "food"] }, - { "category": "food_and_drink", "char": "🍬", "name": "candy", "keywords": ["snack", "dessert", "sweet", "lolly"] }, - { "category": "food_and_drink", "char": "🍭", "name": "lollipop", "keywords": ["food", "snack", "candy", "sweet"] }, - { "category": "food_and_drink", "char": "🍫", "name": "chocolate_bar", "keywords": ["food", "snack", "dessert", "sweet"] }, - { "category": "food_and_drink", "char": "🍿", "name": "popcorn", "keywords": ["food", "movie theater", "films", "snack"] }, - { "category": "food_and_drink", "char": "🥟", "name": "dumpling", "keywords": ["food", "empanada", "pierogi", "potsticker"] }, - { "category": "food_and_drink", "char": "🍩", "name": "doughnut", "keywords": ["food", "dessert", "snack", "sweet", "donut"] }, - { "category": "food_and_drink", "char": "🍪", "name": "cookie", "keywords": ["food", "snack", "oreo", "chocolate", "sweet", "dessert"] }, - { "category": "food_and_drink", "char": "🧇", "name": "waffle", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🧆", "name": "falafel", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🧈", "name": "butter", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🦪", "name": "oyster", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🫓", "name": "flatbread", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🫔", "name": "tamale", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🫕", "name": "fondue", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🥛", "name": "milk_glass", "keywords": ["beverage", "drink", "cow"] }, - { "category": "food_and_drink", "char": "🍺", "name": "beer", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] }, - { "category": "food_and_drink", "char": "🍻", "name": "beers", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] }, - { "category": "food_and_drink", "char": "🥂", "name": "clinking_glasses", "keywords": ["beverage", "drink", "party", "alcohol", "celebrate", "cheers", "wine", "champagne", "toast"] }, - { "category": "food_and_drink", "char": "🍷", "name": "wine_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "booze"] }, - { "category": "food_and_drink", "char": "🥃", "name": "tumbler_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "liquor", "booze", "bourbon", "scotch", "whisky", "glass", "shot"] }, - { "category": "food_and_drink", "char": "🍸", "name": "cocktail", "keywords": ["drink", "drunk", "alcohol", "beverage", "booze", "mojito"] }, - { "category": "food_and_drink", "char": "🍹", "name": "tropical_drink", "keywords": ["beverage", "cocktail", "summer", "beach", "alcohol", "booze", "mojito"] }, - { "category": "food_and_drink", "char": "🍾", "name": "champagne", "keywords": ["drink", "wine", "bottle", "celebration"] }, - { "category": "food_and_drink", "char": "🍶", "name": "sake", "keywords": ["wine", "drink", "drunk", "beverage", "japanese", "alcohol", "booze"] }, - { "category": "food_and_drink", "char": "🍵", "name": "tea", "keywords": ["drink", "bowl", "breakfast", "green", "british"] }, - { "category": "food_and_drink", "char": "🥤", "name": "cup_with_straw", "keywords": ["drink", "soda"] }, - { "category": "food_and_drink", "char": "☕", "name": "coffee", "keywords": ["beverage", "caffeine", "latte", "espresso"] }, - { "category": "food_and_drink", "char": "🫖", "name": "teapot", "keywords": [] }, - { "category": "food_and_drink", "char": "🧋", "name": "bubble_tea", "keywords": ["tapioca"] }, - { "category": "food_and_drink", "char": "🍼", "name": "baby_bottle", "keywords": ["food", "container", "milk"] }, - { "category": "food_and_drink", "char": "🧃", "name": "beverage_box", "keywords": ["food", "drink"] }, - { "category": "food_and_drink", "char": "🧉", "name": "mate", "keywords": ["food", "drink"] }, - { "category": "food_and_drink", "char": "🧊", "name": "ice_cube", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "🧂", "name": "salt", "keywords": ["condiment", "shaker"] }, - { "category": "food_and_drink", "char": "🥄", "name": "spoon", "keywords": ["cutlery", "kitchen", "tableware"] }, - { "category": "food_and_drink", "char": "🍴", "name": "fork_and_knife", "keywords": ["cutlery", "kitchen"] }, - { "category": "food_and_drink", "char": "🍽", "name": "plate_with_cutlery", "keywords": ["food", "eat", "meal", "lunch", "dinner", "restaurant"] }, - { "category": "food_and_drink", "char": "🥣", "name": "bowl_with_spoon", "keywords": ["food", "breakfast", "cereal", "oatmeal", "porridge"] }, - { "category": "food_and_drink", "char": "🥡", "name": "takeout_box", "keywords": ["food", "leftovers"] }, - { "category": "food_and_drink", "char": "🥢", "name": "chopsticks", "keywords": ["food"] }, - { "category": "food_and_drink", "char": "\uD83E\uDED7", "name": "pouring_liquid", "keywords": [] }, - { "category": "food_and_drink", "char": "\uD83E\uDED8", "name": "beans", "keywords": [] }, - { "category": "food_and_drink", "char": "\uD83E\uDED9", "name": "jar", "keywords": [] }, - { "category": "activity", "char": "⚽", "name": "soccer", "keywords": ["sports", "football"] }, - { "category": "activity", "char": "🏀", "name": "basketball", "keywords": ["sports", "balls", "NBA"] }, - { "category": "activity", "char": "🏈", "name": "football", "keywords": ["sports", "balls", "NFL"] }, - { "category": "activity", "char": "⚾", "name": "baseball", "keywords": ["sports", "balls"] }, - { "category": "activity", "char": "🥎", "name": "softball", "keywords": ["sports", "balls"] }, - { "category": "activity", "char": "🎾", "name": "tennis", "keywords": ["sports", "balls", "green"] }, - { "category": "activity", "char": "🏐", "name": "volleyball", "keywords": ["sports", "balls"] }, - { "category": "activity", "char": "🏉", "name": "rugby_football", "keywords": ["sports", "team"] }, - { "category": "activity", "char": "🥏", "name": "flying_disc", "keywords": ["sports", "frisbee", "ultimate"] }, - { "category": "activity", "char": "🎱", "name": "8ball", "keywords": ["pool", "hobby", "game", "luck", "magic"] }, - { "category": "activity", "char": "⛳", "name": "golf", "keywords": ["sports", "business", "flag", "hole", "summer"] }, - { "category": "activity", "char": "🏌️‍♀️", "name": "golfing_woman", "keywords": ["sports", "business", "woman", "female"] }, - { "category": "activity", "char": "🏌", "name": "golfing_man", "keywords": ["sports", "business"] }, - { "category": "activity", "char": "🏓", "name": "ping_pong", "keywords": ["sports", "pingpong"] }, - { "category": "activity", "char": "🏸", "name": "badminton", "keywords": ["sports"] }, - { "category": "activity", "char": "🥅", "name": "goal_net", "keywords": ["sports"] }, - { "category": "activity", "char": "🏒", "name": "ice_hockey", "keywords": ["sports"] }, - { "category": "activity", "char": "🏑", "name": "field_hockey", "keywords": ["sports"] }, - { "category": "activity", "char": "🥍", "name": "lacrosse", "keywords": ["sports", "ball", "stick"] }, - { "category": "activity", "char": "🏏", "name": "cricket", "keywords": ["sports"] }, - { "category": "activity", "char": "🎿", "name": "ski", "keywords": ["sports", "winter", "cold", "snow"] }, - { "category": "activity", "char": "⛷", "name": "skier", "keywords": ["sports", "winter", "snow"] }, - { "category": "activity", "char": "🏂", "name": "snowboarder", "keywords": ["sports", "winter"] }, - { "category": "activity", "char": "🤺", "name": "person_fencing", "keywords": ["sports", "fencing", "sword"] }, - { "category": "activity", "char": "🤼‍♀️", "name": "women_wrestling", "keywords": ["sports", "wrestlers"] }, - { "category": "activity", "char": "🤼‍♂️", "name": "men_wrestling", "keywords": ["sports", "wrestlers"] }, - { "category": "activity", "char": "🤸‍♀️", "name": "woman_cartwheeling", "keywords": ["gymnastics"] }, - { "category": "activity", "char": "🤸‍♂️", "name": "man_cartwheeling", "keywords": ["gymnastics"] }, - { "category": "activity", "char": "🤾‍♀️", "name": "woman_playing_handball", "keywords": ["sports"] }, - { "category": "activity", "char": "🤾‍♂️", "name": "man_playing_handball", "keywords": ["sports"] }, - { "category": "activity", "char": "⛸", "name": "ice_skate", "keywords": ["sports"] }, - { "category": "activity", "char": "🥌", "name": "curling_stone", "keywords": ["sports"] }, - { "category": "activity", "char": "🛹", "name": "skateboard", "keywords": ["board"] }, - { "category": "activity", "char": "🛷", "name": "sled", "keywords": ["sleigh", "luge", "toboggan"] }, - { "category": "activity", "char": "🏹", "name": "bow_and_arrow", "keywords": ["sports"] }, - { "category": "activity", "char": "🎣", "name": "fishing_pole_and_fish", "keywords": ["food", "hobby", "summer"] }, - { "category": "activity", "char": "🥊", "name": "boxing_glove", "keywords": ["sports", "fighting"] }, - { "category": "activity", "char": "🥋", "name": "martial_arts_uniform", "keywords": ["judo", "karate", "taekwondo"] }, - { "category": "activity", "char": "🚣‍♀️", "name": "rowing_woman", "keywords": ["sports", "hobby", "water", "ship", "woman", "female"] }, - { "category": "activity", "char": "🚣", "name": "rowing_man", "keywords": ["sports", "hobby", "water", "ship"] }, - { "category": "activity", "char": "🧗‍♀️", "name": "climbing_woman", "keywords": ["sports", "hobby", "woman", "female", "rock"] }, - { "category": "activity", "char": "🧗‍♂️", "name": "climbing_man", "keywords": ["sports", "hobby", "man", "male", "rock"] }, - { "category": "activity", "char": "🏊‍♀️", "name": "swimming_woman", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer", "woman", "female"] }, - { "category": "activity", "char": "🏊", "name": "swimming_man", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer"] }, - { "category": "activity", "char": "🤽‍♀️", "name": "woman_playing_water_polo", "keywords": ["sports", "pool"] }, - { "category": "activity", "char": "🤽‍♂️", "name": "man_playing_water_polo", "keywords": ["sports", "pool"] }, - { "category": "activity", "char": "🧘‍♀️", "name": "woman_in_lotus_position", "keywords": ["woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"] }, - { "category": "activity", "char": "🧘‍♂️", "name": "man_in_lotus_position", "keywords": ["man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"] }, - { "category": "activity", "char": "🏄‍♀️", "name": "surfing_woman", "keywords": ["sports", "ocean", "sea", "summer", "beach", "woman", "female"] }, - { "category": "activity", "char": "🏄", "name": "surfing_man", "keywords": ["sports", "ocean", "sea", "summer", "beach"] }, - { "category": "activity", "char": "🛀", "name": "bath", "keywords": ["clean", "shower", "bathroom"] }, - { "category": "activity", "char": "⛹️‍♀️", "name": "basketball_woman", "keywords": ["sports", "human", "woman", "female"] }, - { "category": "activity", "char": "⛹", "name": "basketball_man", "keywords": ["sports", "human"] }, - { "category": "activity", "char": "🏋️‍♀️", "name": "weight_lifting_woman", "keywords": ["sports", "training", "exercise", "woman", "female"] }, - { "category": "activity", "char": "🏋", "name": "weight_lifting_man", "keywords": ["sports", "training", "exercise"] }, - { "category": "activity", "char": "🚴‍♀️", "name": "biking_woman", "keywords": ["sports", "bike", "exercise", "hipster", "woman", "female"] }, - { "category": "activity", "char": "🚴", "name": "biking_man", "keywords": ["sports", "bike", "exercise", "hipster"] }, - { "category": "activity", "char": "🚵‍♀️", "name": "mountain_biking_woman", "keywords": ["transportation", "sports", "human", "race", "bike", "woman", "female"] }, - { "category": "activity", "char": "🚵", "name": "mountain_biking_man", "keywords": ["transportation", "sports", "human", "race", "bike"] }, - { "category": "activity", "char": "🏇", "name": "horse_racing", "keywords": ["animal", "betting", "competition", "gambling", "luck"] }, - { "category": "activity", "char": "🤿", "name": "diving_mask", "keywords": ["sports"] }, - { "category": "activity", "char": "🪀", "name": "yo_yo", "keywords": ["sports"] }, - { "category": "activity", "char": "🪁", "name": "kite", "keywords": ["sports"] }, - { "category": "activity", "char": "🦺", "name": "safety_vest", "keywords": ["sports"] }, - { "category": "activity", "char": "🪡", "name": "sewing_needle", "keywords": [] }, - { "category": "activity", "char": "🪢", "name": "knot", "keywords": [] }, - { "category": "activity", "char": "🕴", "name": "business_suit_levitating", "keywords": ["suit", "business", "levitate", "hover", "jump"] }, - { "category": "activity", "char": "🏆", "name": "trophy", "keywords": ["win", "award", "contest", "place", "ftw", "ceremony"] }, - { "category": "activity", "char": "🎽", "name": "running_shirt_with_sash", "keywords": ["play", "pageant"] }, - { "category": "activity", "char": "🏅", "name": "medal_sports", "keywords": ["award", "winning"] }, - { "category": "activity", "char": "🎖", "name": "medal_military", "keywords": ["award", "winning", "army"] }, - { "category": "activity", "char": "🥇", "name": "1st_place_medal", "keywords": ["award", "winning", "first"] }, - { "category": "activity", "char": "🥈", "name": "2nd_place_medal", "keywords": ["award", "second"] }, - { "category": "activity", "char": "🥉", "name": "3rd_place_medal", "keywords": ["award", "third"] }, - { "category": "activity", "char": "🎗", "name": "reminder_ribbon", "keywords": ["sports", "cause", "support", "awareness"] }, - { "category": "activity", "char": "🏵", "name": "rosette", "keywords": ["flower", "decoration", "military"] }, - { "category": "activity", "char": "🎫", "name": "ticket", "keywords": ["event", "concert", "pass"] }, - { "category": "activity", "char": "🎟", "name": "tickets", "keywords": ["sports", "concert", "entrance"] }, - { "category": "activity", "char": "🎭", "name": "performing_arts", "keywords": ["acting", "theater", "drama"] }, - { "category": "activity", "char": "🎨", "name": "art", "keywords": ["design", "paint", "draw", "colors"] }, - { "category": "activity", "char": "🎪", "name": "circus_tent", "keywords": ["festival", "carnival", "party"] }, - { "category": "activity", "char": "🤹‍♀️", "name": "woman_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] }, - { "category": "activity", "char": "🤹‍♂️", "name": "man_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] }, - { "category": "activity", "char": "🎤", "name": "microphone", "keywords": ["sound", "music", "PA", "sing", "talkshow"] }, - { "category": "activity", "char": "🎧", "name": "headphones", "keywords": ["music", "score", "gadgets"] }, - { "category": "activity", "char": "🎼", "name": "musical_score", "keywords": ["treble", "clef", "compose"] }, - { "category": "activity", "char": "🎹", "name": "musical_keyboard", "keywords": ["piano", "instrument", "compose"] }, - { "category": "activity", "char": "🥁", "name": "drum", "keywords": ["music", "instrument", "drumsticks", "snare"] }, - { "category": "activity", "char": "🎷", "name": "saxophone", "keywords": ["music", "instrument", "jazz", "blues"] }, - { "category": "activity", "char": "🎺", "name": "trumpet", "keywords": ["music", "brass"] }, - { "category": "activity", "char": "🎸", "name": "guitar", "keywords": ["music", "instrument"] }, - { "category": "activity", "char": "🎻", "name": "violin", "keywords": ["music", "instrument", "orchestra", "symphony"] }, - { "category": "activity", "char": "🪕", "name": "banjo", "keywords": ["music", "instrument"] }, - { "category": "activity", "char": "🪗", "name": "accordion", "keywords": ["music", "instrument"] }, - { "category": "activity", "char": "🪘", "name": "long_drum", "keywords": ["music", "instrument"] }, - { "category": "activity", "char": "🎬", "name": "clapper", "keywords": ["movie", "film", "record"] }, - { "category": "activity", "char": "🎮", "name": "video_game", "keywords": ["play", "console", "PS4", "controller"] }, - { "category": "activity", "char": "👾", "name": "space_invader", "keywords": ["game", "arcade", "play"] }, - { "category": "activity", "char": "🎯", "name": "dart", "keywords": ["game", "play", "bar", "target", "bullseye"] }, - { "category": "activity", "char": "🎲", "name": "game_die", "keywords": ["dice", "random", "tabletop", "play", "luck"] }, - { "category": "activity", "char": "♟️", "name": "chess_pawn", "keywords": ["expendable"] }, - { "category": "activity", "char": "🎰", "name": "slot_machine", "keywords": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"] }, - { "category": "activity", "char": "🧩", "name": "jigsaw", "keywords": ["interlocking", "puzzle", "piece"] }, - { "category": "activity", "char": "🎳", "name": "bowling", "keywords": ["sports", "fun", "play"] }, - { "category": "activity", "char": "🪄", "name": "magic_wand", "keywords": [] }, - { "category": "activity", "char": "🪅", "name": "pinata", "keywords": [] }, - { "category": "activity", "char": "🪆", "name": "nesting_dolls", "keywords": [] }, - { "category": "activity", "char": "\uD83E\uDEAC", "name": "hamsa", "keywords": [] }, - { "category": "activity", "char": "\uD83E\uDEA9", "name": "mirror_ball", "keywords": [] }, - { "category": "travel_and_places", "char": "🚗", "name": "red_car", "keywords": ["red", "transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚕", "name": "taxi", "keywords": ["uber", "vehicle", "cars", "transportation"] }, - { "category": "travel_and_places", "char": "🚙", "name": "blue_car", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚌", "name": "bus", "keywords": ["car", "vehicle", "transportation"] }, - { "category": "travel_and_places", "char": "🚎", "name": "trolleybus", "keywords": ["bart", "transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🏎", "name": "racing_car", "keywords": ["sports", "race", "fast", "formula", "f1"] }, - { "category": "travel_and_places", "char": "🚓", "name": "police_car", "keywords": ["vehicle", "cars", "transportation", "law", "legal", "enforcement"] }, - { "category": "travel_and_places", "char": "🚑", "name": "ambulance", "keywords": ["health", "911", "hospital"] }, - { "category": "travel_and_places", "char": "🚒", "name": "fire_engine", "keywords": ["transportation", "cars", "vehicle"] }, - { "category": "travel_and_places", "char": "🚐", "name": "minibus", "keywords": ["vehicle", "car", "transportation"] }, - { "category": "travel_and_places", "char": "🚚", "name": "truck", "keywords": ["cars", "transportation"] }, - { "category": "travel_and_places", "char": "🚛", "name": "articulated_lorry", "keywords": ["vehicle", "cars", "transportation", "express"] }, - { "category": "travel_and_places", "char": "🚜", "name": "tractor", "keywords": ["vehicle", "car", "farming", "agriculture"] }, - { "category": "travel_and_places", "char": "🛴", "name": "kick_scooter", "keywords": ["vehicle", "kick", "razor"] }, - { "category": "travel_and_places", "char": "🏍", "name": "motorcycle", "keywords": ["race", "sports", "fast"] }, - { "category": "travel_and_places", "char": "🚲", "name": "bike", "keywords": ["sports", "bicycle", "exercise", "hipster"] }, - { "category": "travel_and_places", "char": "🛵", "name": "motor_scooter", "keywords": ["vehicle", "vespa", "sasha"] }, - { "category": "travel_and_places", "char": "🦽", "name": "manual_wheelchair", "keywords": ["vehicle"] }, - { "category": "travel_and_places", "char": "🦼", "name": "motorized_wheelchair", "keywords": ["vehicle"] }, - { "category": "travel_and_places", "char": "🛺", "name": "auto_rickshaw", "keywords": ["vehicle"] }, - { "category": "travel_and_places", "char": "🪂", "name": "parachute", "keywords": ["vehicle"] }, - { "category": "travel_and_places", "char": "🚨", "name": "rotating_light", "keywords": ["police", "ambulance", "911", "emergency", "alert", "error", "pinged", "law", "legal"] }, - { "category": "travel_and_places", "char": "🚔", "name": "oncoming_police_car", "keywords": ["vehicle", "law", "legal", "enforcement", "911"] }, - { "category": "travel_and_places", "char": "🚍", "name": "oncoming_bus", "keywords": ["vehicle", "transportation"] }, - { "category": "travel_and_places", "char": "🚘", "name": "oncoming_automobile", "keywords": ["car", "vehicle", "transportation"] }, - { "category": "travel_and_places", "char": "🚖", "name": "oncoming_taxi", "keywords": ["vehicle", "cars", "uber"] }, - { "category": "travel_and_places", "char": "🚡", "name": "aerial_tramway", "keywords": ["transportation", "vehicle", "ski"] }, - { "category": "travel_and_places", "char": "🚠", "name": "mountain_cableway", "keywords": ["transportation", "vehicle", "ski"] }, - { "category": "travel_and_places", "char": "🚟", "name": "suspension_railway", "keywords": ["vehicle", "transportation"] }, - { "category": "travel_and_places", "char": "🚃", "name": "railway_car", "keywords": ["transportation", "vehicle", "train"] }, - { "category": "travel_and_places", "char": "🚋", "name": "train", "keywords": ["transportation", "vehicle", "carriage", "public", "travel"] }, - { "category": "travel_and_places", "char": "🚝", "name": "monorail", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚄", "name": "bullettrain_side", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚅", "name": "bullettrain_front", "keywords": ["transportation", "vehicle", "speed", "fast", "public", "travel"] }, - { "category": "travel_and_places", "char": "🚈", "name": "light_rail", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚞", "name": "mountain_railway", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚂", "name": "steam_locomotive", "keywords": ["transportation", "vehicle", "train"] }, - { "category": "travel_and_places", "char": "🚆", "name": "train2", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚇", "name": "metro", "keywords": ["transportation", "blue-square", "mrt", "underground", "tube"] }, - { "category": "travel_and_places", "char": "🚊", "name": "tram", "keywords": ["transportation", "vehicle"] }, - { "category": "travel_and_places", "char": "🚉", "name": "station", "keywords": ["transportation", "vehicle", "public"] }, - { "category": "travel_and_places", "char": "🛸", "name": "flying_saucer", "keywords": ["transportation", "vehicle", "ufo"] }, - { "category": "travel_and_places", "char": "🚁", "name": "helicopter", "keywords": ["transportation", "vehicle", "fly"] }, - { "category": "travel_and_places", "char": "🛩", "name": "small_airplane", "keywords": ["flight", "transportation", "fly", "vehicle"] }, - { "category": "travel_and_places", "char": "✈️", "name": "airplane", "keywords": ["vehicle", "transportation", "flight", "fly"] }, - { "category": "travel_and_places", "char": "🛫", "name": "flight_departure", "keywords": ["airport", "flight", "landing"] }, - { "category": "travel_and_places", "char": "🛬", "name": "flight_arrival", "keywords": ["airport", "flight", "boarding"] }, - { "category": "travel_and_places", "char": "⛵", "name": "sailboat", "keywords": ["ship", "summer", "transportation", "water", "sailing"] }, - { "category": "travel_and_places", "char": "🛥", "name": "motor_boat", "keywords": ["ship"] }, - { "category": "travel_and_places", "char": "🚤", "name": "speedboat", "keywords": ["ship", "transportation", "vehicle", "summer"] }, - { "category": "travel_and_places", "char": "⛴", "name": "ferry", "keywords": ["boat", "ship", "yacht"] }, - { "category": "travel_and_places", "char": "🛳", "name": "passenger_ship", "keywords": ["yacht", "cruise", "ferry"] }, - { "category": "travel_and_places", "char": "🚀", "name": "rocket", "keywords": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"] }, - { "category": "travel_and_places", "char": "🛰", "name": "artificial_satellite", "keywords": ["communication", "gps", "orbit", "spaceflight", "NASA", "ISS"] }, - { "category": "travel_and_places", "char": "🛻", "name": "pickup_truck", "keywords": ["car"] }, - { "category": "travel_and_places", "char": "🛼", "name": "roller_skate", "keywords": [] }, - { "category": "travel_and_places", "char": "💺", "name": "seat", "keywords": ["sit", "airplane", "transport", "bus", "flight", "fly"] }, - { "category": "travel_and_places", "char": "🛶", "name": "canoe", "keywords": ["boat", "paddle", "water", "ship"] }, - { "category": "travel_and_places", "char": "⚓", "name": "anchor", "keywords": ["ship", "ferry", "sea", "boat"] }, - { "category": "travel_and_places", "char": "🚧", "name": "construction", "keywords": ["wip", "progress", "caution", "warning"] }, - { "category": "travel_and_places", "char": "⛽", "name": "fuelpump", "keywords": ["gas station", "petroleum"] }, - { "category": "travel_and_places", "char": "🚏", "name": "busstop", "keywords": ["transportation", "wait"] }, - { "category": "travel_and_places", "char": "🚦", "name": "vertical_traffic_light", "keywords": ["transportation", "driving"] }, - { "category": "travel_and_places", "char": "🚥", "name": "traffic_light", "keywords": ["transportation", "signal"] }, - { "category": "travel_and_places", "char": "🏁", "name": "checkered_flag", "keywords": ["contest", "finishline", "race", "gokart"] }, - { "category": "travel_and_places", "char": "🚢", "name": "ship", "keywords": ["transportation", "titanic", "deploy"] }, - { "category": "travel_and_places", "char": "🎡", "name": "ferris_wheel", "keywords": ["photo", "carnival", "londoneye"] }, - { "category": "travel_and_places", "char": "🎢", "name": "roller_coaster", "keywords": ["carnival", "playground", "photo", "fun"] }, - { "category": "travel_and_places", "char": "🎠", "name": "carousel_horse", "keywords": ["photo", "carnival"] }, - { "category": "travel_and_places", "char": "🏗", "name": "building_construction", "keywords": ["wip", "working", "progress"] }, - { "category": "travel_and_places", "char": "🌁", "name": "foggy", "keywords": ["photo", "mountain"] }, - { "category": "travel_and_places", "char": "🏭", "name": "factory", "keywords": ["building", "industry", "pollution", "smoke"] }, - { "category": "travel_and_places", "char": "⛲", "name": "fountain", "keywords": ["photo", "summer", "water", "fresh"] }, - { "category": "travel_and_places", "char": "🎑", "name": "rice_scene", "keywords": ["photo", "japan", "asia", "tsukimi"] }, - { "category": "travel_and_places", "char": "⛰", "name": "mountain", "keywords": ["photo", "nature", "environment"] }, - { "category": "travel_and_places", "char": "🏔", "name": "mountain_snow", "keywords": ["photo", "nature", "environment", "winter", "cold"] }, - { "category": "travel_and_places", "char": "🗻", "name": "mount_fuji", "keywords": ["photo", "mountain", "nature", "japanese"] }, - { "category": "travel_and_places", "char": "🌋", "name": "volcano", "keywords": ["photo", "nature", "disaster"] }, - { "category": "travel_and_places", "char": "🗾", "name": "japan", "keywords": ["nation", "country", "japanese", "asia"] }, - { "category": "travel_and_places", "char": "🏕", "name": "camping", "keywords": ["photo", "outdoors", "tent"] }, - { "category": "travel_and_places", "char": "⛺", "name": "tent", "keywords": ["photo", "camping", "outdoors"] }, - { "category": "travel_and_places", "char": "🏞", "name": "national_park", "keywords": ["photo", "environment", "nature"] }, - { "category": "travel_and_places", "char": "🛣", "name": "motorway", "keywords": ["road", "cupertino", "interstate", "highway"] }, - { "category": "travel_and_places", "char": "🛤", "name": "railway_track", "keywords": ["train", "transportation"] }, - { "category": "travel_and_places", "char": "🌅", "name": "sunrise", "keywords": ["morning", "view", "vacation", "photo"] }, - { "category": "travel_and_places", "char": "🌄", "name": "sunrise_over_mountains", "keywords": ["view", "vacation", "photo"] }, - { "category": "travel_and_places", "char": "🏜", "name": "desert", "keywords": ["photo", "warm", "saharah"] }, - { "category": "travel_and_places", "char": "🏖", "name": "beach_umbrella", "keywords": ["weather", "summer", "sunny", "sand", "mojito"] }, - { "category": "travel_and_places", "char": "🏝", "name": "desert_island", "keywords": ["photo", "tropical", "mojito"] }, - { "category": "travel_and_places", "char": "🌇", "name": "city_sunrise", "keywords": ["photo", "good morning", "dawn"] }, - { "category": "travel_and_places", "char": "🌆", "name": "city_sunset", "keywords": ["photo", "evening", "sky", "buildings"] }, - { "category": "travel_and_places", "char": "🏙", "name": "cityscape", "keywords": ["photo", "night life", "urban"] }, - { "category": "travel_and_places", "char": "🌃", "name": "night_with_stars", "keywords": ["evening", "city", "downtown"] }, - { "category": "travel_and_places", "char": "🌉", "name": "bridge_at_night", "keywords": ["photo", "sanfrancisco"] }, - { "category": "travel_and_places", "char": "🌌", "name": "milky_way", "keywords": ["photo", "space", "stars"] }, - { "category": "travel_and_places", "char": "🌠", "name": "stars", "keywords": ["night", "photo"] }, - { "category": "travel_and_places", "char": "🎇", "name": "sparkler", "keywords": ["stars", "night", "shine"] }, - { "category": "travel_and_places", "char": "🎆", "name": "fireworks", "keywords": ["photo", "festival", "carnival", "congratulations"] }, - { "category": "travel_and_places", "char": "🌈", "name": "rainbow", "keywords": ["nature", "happy", "unicorn_face", "photo", "sky", "spring"] }, - { "category": "travel_and_places", "char": "🏘", "name": "houses", "keywords": ["buildings", "photo"] }, - { "category": "travel_and_places", "char": "🏰", "name": "european_castle", "keywords": ["building", "royalty", "history"] }, - { "category": "travel_and_places", "char": "🏯", "name": "japanese_castle", "keywords": ["photo", "building"] }, - { "category": "travel_and_places", "char": "🗼", "name": "tokyo_tower", "keywords": ["photo", "japanese"] }, - { "category": "travel_and_places", "char": "", "name": "shibuya_109", "keywords": ["photo", "japanese"] }, - { "category": "travel_and_places", "char": "🏟", "name": "stadium", "keywords": ["photo", "place", "sports", "concert", "venue"] }, - { "category": "travel_and_places", "char": "🗽", "name": "statue_of_liberty", "keywords": ["american", "newyork"] }, - { "category": "travel_and_places", "char": "🏠", "name": "house", "keywords": ["building", "home"] }, - { "category": "travel_and_places", "char": "🏡", "name": "house_with_garden", "keywords": ["home", "plant", "nature"] }, - { "category": "travel_and_places", "char": "🏚", "name": "derelict_house", "keywords": ["abandon", "evict", "broken", "building"] }, - { "category": "travel_and_places", "char": "🏢", "name": "office", "keywords": ["building", "bureau", "work"] }, - { "category": "travel_and_places", "char": "🏬", "name": "department_store", "keywords": ["building", "shopping", "mall"] }, - { "category": "travel_and_places", "char": "🏣", "name": "post_office", "keywords": ["building", "envelope", "communication"] }, - { "category": "travel_and_places", "char": "🏤", "name": "european_post_office", "keywords": ["building", "email"] }, - { "category": "travel_and_places", "char": "🏥", "name": "hospital", "keywords": ["building", "health", "surgery", "doctor"] }, - { "category": "travel_and_places", "char": "🏦", "name": "bank", "keywords": ["building", "money", "sales", "cash", "business", "enterprise"] }, - { "category": "travel_and_places", "char": "🏨", "name": "hotel", "keywords": ["building", "accomodation", "checkin"] }, - { "category": "travel_and_places", "char": "🏪", "name": "convenience_store", "keywords": ["building", "shopping", "groceries"] }, - { "category": "travel_and_places", "char": "🏫", "name": "school", "keywords": ["building", "student", "education", "learn", "teach"] }, - { "category": "travel_and_places", "char": "🏩", "name": "love_hotel", "keywords": ["like", "affection", "dating"] }, - { "category": "travel_and_places", "char": "💒", "name": "wedding", "keywords": ["love", "like", "affection", "couple", "marriage", "bride", "groom"] }, - { "category": "travel_and_places", "char": "🏛", "name": "classical_building", "keywords": ["art", "culture", "history"] }, - { "category": "travel_and_places", "char": "⛪", "name": "church", "keywords": ["building", "religion", "christ"] }, - { "category": "travel_and_places", "char": "🕌", "name": "mosque", "keywords": ["islam", "worship", "minaret"] }, - { "category": "travel_and_places", "char": "🕍", "name": "synagogue", "keywords": ["judaism", "worship", "temple", "jewish"] }, - { "category": "travel_and_places", "char": "🕋", "name": "kaaba", "keywords": ["mecca", "mosque", "islam"] }, - { "category": "travel_and_places", "char": "⛩", "name": "shinto_shrine", "keywords": ["temple", "japan", "kyoto"] }, - { "category": "travel_and_places", "char": "🛕", "name": "hindu_temple", "keywords": ["temple"] }, - { "category": "travel_and_places", "char": "🪨", "name": "rock", "keywords": [] }, - { "category": "travel_and_places", "char": "🪵", "name": "wood", "keywords": [] }, - { "category": "travel_and_places", "char": "🛖", "name": "hut", "keywords": [] }, - { "category": "travel_and_places", "char": "\uD83D\uDEDD", "name": "playground_slide", "keywords": [] }, - { "category": "travel_and_places", "char": "\uD83D\uDEDE", "name": "wheel", "keywords": [] }, - { "category": "travel_and_places", "char": "\uD83D\uDEDF", "name": "ring_buoy", "keywords": [] }, - { "category": "objects", "char": "⌚", "name": "watch", "keywords": ["time", "accessories"] }, - { "category": "objects", "char": "📱", "name": "iphone", "keywords": ["technology", "apple", "gadgets", "dial"] }, - { "category": "objects", "char": "📲", "name": "calling", "keywords": ["iphone", "incoming"] }, - { "category": "objects", "char": "💻", "name": "computer", "keywords": ["technology", "laptop", "screen", "display", "monitor"] }, - { "category": "objects", "char": "⌨", "name": "keyboard", "keywords": ["technology", "computer", "type", "input", "text"] }, - { "category": "objects", "char": "🖥", "name": "desktop_computer", "keywords": ["technology", "computing", "screen"] }, - { "category": "objects", "char": "🖨", "name": "printer", "keywords": ["paper", "ink"] }, - { "category": "objects", "char": "🖱", "name": "computer_mouse", "keywords": ["click"] }, - { "category": "objects", "char": "🖲", "name": "trackball", "keywords": ["technology", "trackpad"] }, - { "category": "objects", "char": "🕹", "name": "joystick", "keywords": ["game", "play"] }, - { "category": "objects", "char": "🗜", "name": "clamp", "keywords": ["tool"] }, - { "category": "objects", "char": "💽", "name": "minidisc", "keywords": ["technology", "record", "data", "disk", "90s"] }, - { "category": "objects", "char": "💾", "name": "floppy_disk", "keywords": ["oldschool", "technology", "save", "90s", "80s"] }, - { "category": "objects", "char": "💿", "name": "cd", "keywords": ["technology", "dvd", "disk", "disc", "90s"] }, - { "category": "objects", "char": "📀", "name": "dvd", "keywords": ["cd", "disk", "disc"] }, - { "category": "objects", "char": "📼", "name": "vhs", "keywords": ["record", "video", "oldschool", "90s", "80s"] }, - { "category": "objects", "char": "📷", "name": "camera", "keywords": ["gadgets", "photography"] }, - { "category": "objects", "char": "📸", "name": "camera_flash", "keywords": ["photography", "gadgets"] }, - { "category": "objects", "char": "📹", "name": "video_camera", "keywords": ["film", "record"] }, - { "category": "objects", "char": "🎥", "name": "movie_camera", "keywords": ["film", "record"] }, - { "category": "objects", "char": "📽", "name": "film_projector", "keywords": ["video", "tape", "record", "movie"] }, - { "category": "objects", "char": "🎞", "name": "film_strip", "keywords": ["movie"] }, - { "category": "objects", "char": "📞", "name": "telephone_receiver", "keywords": ["technology", "communication", "dial"] }, - { "category": "objects", "char": "☎️", "name": "phone", "keywords": ["technology", "communication", "dial", "telephone"] }, - { "category": "objects", "char": "📟", "name": "pager", "keywords": ["bbcall", "oldschool", "90s"] }, - { "category": "objects", "char": "📠", "name": "fax", "keywords": ["communication", "technology"] }, - { "category": "objects", "char": "📺", "name": "tv", "keywords": ["technology", "program", "oldschool", "show", "television"] }, - { "category": "objects", "char": "📻", "name": "radio", "keywords": ["communication", "music", "podcast", "program"] }, - { "category": "objects", "char": "🎙", "name": "studio_microphone", "keywords": ["sing", "recording", "artist", "talkshow"] }, - { "category": "objects", "char": "🎚", "name": "level_slider", "keywords": ["scale"] }, - { "category": "objects", "char": "🎛", "name": "control_knobs", "keywords": ["dial"] }, - { "category": "objects", "char": "🧭", "name": "compass", "keywords": ["magnetic", "navigation", "orienteering"] }, - { "category": "objects", "char": "⏱", "name": "stopwatch", "keywords": ["time", "deadline"] }, - { "category": "objects", "char": "⏲", "name": "timer_clock", "keywords": ["alarm"] }, - { "category": "objects", "char": "⏰", "name": "alarm_clock", "keywords": ["time", "wake"] }, - { "category": "objects", "char": "🕰", "name": "mantelpiece_clock", "keywords": ["time"] }, - { "category": "objects", "char": "⏳", "name": "hourglass_flowing_sand", "keywords": ["oldschool", "time", "countdown"] }, - { "category": "objects", "char": "⌛", "name": "hourglass", "keywords": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"] }, - { "category": "objects", "char": "📡", "name": "satellite", "keywords": ["communication", "future", "radio", "space"] }, - { "category": "objects", "char": "🔋", "name": "battery", "keywords": ["power", "energy", "sustain"] }, - { "category": "objects", "char": "\uD83E\uDEAB", "name": "battery", "keywords": [] }, - { "category": "objects", "char": "🔌", "name": "electric_plug", "keywords": ["charger", "power"] }, - { "category": "objects", "char": "💡", "name": "bulb", "keywords": ["light", "electricity", "idea"] }, - { "category": "objects", "char": "🔦", "name": "flashlight", "keywords": ["dark", "camping", "sight", "night"] }, - { "category": "objects", "char": "🕯", "name": "candle", "keywords": ["fire", "wax"] }, - { "category": "objects", "char": "🧯", "name": "fire_extinguisher", "keywords": ["quench"] }, - { "category": "objects", "char": "🗑", "name": "wastebasket", "keywords": ["bin", "trash", "rubbish", "garbage", "toss"] }, - { "category": "objects", "char": "🛢", "name": "oil_drum", "keywords": ["barrell"] }, - { "category": "objects", "char": "💸", "name": "money_with_wings", "keywords": ["dollar", "bills", "payment", "sale"] }, - { "category": "objects", "char": "💵", "name": "dollar", "keywords": ["money", "sales", "bill", "currency"] }, - { "category": "objects", "char": "💴", "name": "yen", "keywords": ["money", "sales", "japanese", "dollar", "currency"] }, - { "category": "objects", "char": "💶", "name": "euro", "keywords": ["money", "sales", "dollar", "currency"] }, - { "category": "objects", "char": "💷", "name": "pound", "keywords": ["british", "sterling", "money", "sales", "bills", "uk", "england", "currency"] }, - { "category": "objects", "char": "💰", "name": "moneybag", "keywords": ["dollar", "payment", "coins", "sale"] }, - { "category": "objects", "char": "🪙", "name": "coin", "keywords": ["dollar", "payment", "coins", "sale"] }, - { "category": "objects", "char": "💳", "name": "credit_card", "keywords": ["money", "sales", "dollar", "bill", "payment", "shopping"] }, - { "category": "objects", "char": "\uD83E\uDEAB", "name": "identification_card", "keywords": [] }, - { "category": "objects", "char": "💎", "name": "gem", "keywords": ["blue", "ruby", "diamond", "jewelry"] }, - { "category": "objects", "char": "⚖", "name": "balance_scale", "keywords": ["law", "fairness", "weight"] }, - { "category": "objects", "char": "🧰", "name": "toolbox", "keywords": ["tools", "diy", "fix", "maintainer", "mechanic"] }, - { "category": "objects", "char": "🔧", "name": "wrench", "keywords": ["tools", "diy", "ikea", "fix", "maintainer"] }, - { "category": "objects", "char": "🔨", "name": "hammer", "keywords": ["tools", "build", "create"] }, - { "category": "objects", "char": "⚒", "name": "hammer_and_pick", "keywords": ["tools", "build", "create"] }, - { "category": "objects", "char": "🛠", "name": "hammer_and_wrench", "keywords": ["tools", "build", "create"] }, - { "category": "objects", "char": "⛏", "name": "pick", "keywords": ["tools", "dig"] }, - { "category": "objects", "char": "🪓", "name": "axe", "keywords": ["tools"] }, - { "category": "objects", "char": "🦯", "name": "probing_cane", "keywords": ["tools"] }, - { "category": "objects", "char": "🔩", "name": "nut_and_bolt", "keywords": ["handy", "tools", "fix"] }, - { "category": "objects", "char": "⚙", "name": "gear", "keywords": ["cog"] }, - { "category": "objects", "char": "🪃", "name": "boomerang", "keywords": ["tool"] }, - { "category": "objects", "char": "🪚", "name": "carpentry_saw", "keywords": ["tool"] }, - { "category": "objects", "char": "🪛", "name": "screwdriver", "keywords": ["tool"] }, - { "category": "objects", "char": "🪝", "name": "hook", "keywords": ["tool"] }, - { "category": "objects", "char": "🪜", "name": "ladder", "keywords": ["tool"] }, - { "category": "objects", "char": "🧱", "name": "brick", "keywords": ["bricks"] }, - { "category": "objects", "char": "⛓", "name": "chains", "keywords": ["lock", "arrest"] }, - { "category": "objects", "char": "🧲", "name": "magnet", "keywords": ["attraction", "magnetic"] }, - { "category": "objects", "char": "🔫", "name": "gun", "keywords": ["violence", "weapon", "pistol", "revolver"] }, - { "category": "objects", "char": "💣", "name": "bomb", "keywords": ["boom", "explode", "explosion", "terrorism"] }, - { "category": "objects", "char": "🧨", "name": "firecracker", "keywords": ["dynamite", "boom", "explode", "explosion", "explosive"] }, - { "category": "objects", "char": "🔪", "name": "hocho", "keywords": ["knife", "blade", "cutlery", "kitchen", "weapon"] }, - { "category": "objects", "char": "🗡", "name": "dagger", "keywords": ["weapon"] }, - { "category": "objects", "char": "⚔", "name": "crossed_swords", "keywords": ["weapon"] }, - { "category": "objects", "char": "🛡", "name": "shield", "keywords": ["protection", "security"] }, - { "category": "objects", "char": "🚬", "name": "smoking", "keywords": ["kills", "tobacco", "cigarette", "joint", "smoke"] }, - { "category": "objects", "char": "☠", "name": "skull_and_crossbones", "keywords": ["poison", "danger", "deadly", "scary", "death", "pirate", "evil"] }, - { "category": "objects", "char": "⚰", "name": "coffin", "keywords": ["vampire", "dead", "die", "death", "rip", "graveyard", "cemetery", "casket", "funeral", "box"] }, - { "category": "objects", "char": "⚱", "name": "funeral_urn", "keywords": ["dead", "die", "death", "rip", "ashes"] }, - { "category": "objects", "char": "🏺", "name": "amphora", "keywords": ["vase", "jar"] }, - { "category": "objects", "char": "🔮", "name": "crystal_ball", "keywords": ["disco", "party", "magic", "circus", "fortune_teller"] }, - { "category": "objects", "char": "📿", "name": "prayer_beads", "keywords": ["dhikr", "religious"] }, - { "category": "objects", "char": "🧿", "name": "nazar_amulet", "keywords": ["bead", "charm"] }, - { "category": "objects", "char": "💈", "name": "barber", "keywords": ["hair", "salon", "style"] }, - { "category": "objects", "char": "⚗", "name": "alembic", "keywords": ["distilling", "science", "experiment", "chemistry"] }, - { "category": "objects", "char": "🔭", "name": "telescope", "keywords": ["stars", "space", "zoom", "science", "astronomy"] }, - { "category": "objects", "char": "🔬", "name": "microscope", "keywords": ["laboratory", "experiment", "zoomin", "science", "study"] }, - { "category": "objects", "char": "🕳", "name": "hole", "keywords": ["embarrassing"] }, - { "category": "objects", "char": "💊", "name": "pill", "keywords": ["health", "medicine", "doctor", "pharmacy", "drug"] }, - { "category": "objects", "char": "💉", "name": "syringe", "keywords": ["health", "hospital", "drugs", "blood", "medicine", "needle", "doctor", "nurse"] }, - { "category": "objects", "char": "🩸", "name": "drop_of_blood", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, - { "category": "objects", "char": "🩹", "name": "adhesive_bandage", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, - { "category": "objects", "char": "🩺", "name": "stethoscope", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, - { "category": "objects", "char": "🪒", "name": "razor", "keywords": ["health"] }, - { "category": "objects", "char": "\uD83E\uDE7B", "name": "xray", "keywords": [] }, - { "category": "objects", "char": "\uD83E\uDE7C", "name": "crutch", "keywords": [] }, - { "category": "objects", "char": "🧬", "name": "dna", "keywords": ["biologist", "genetics", "life"] }, - { "category": "objects", "char": "🧫", "name": "petri_dish", "keywords": ["bacteria", "biology", "culture", "lab"] }, - { "category": "objects", "char": "🧪", "name": "test_tube", "keywords": ["chemistry", "experiment", "lab", "science"] }, - { "category": "objects", "char": "🌡", "name": "thermometer", "keywords": ["weather", "temperature", "hot", "cold"] }, - { "category": "objects", "char": "🧹", "name": "broom", "keywords": ["cleaning", "sweeping", "witch"] }, - { "category": "objects", "char": "🧺", "name": "basket", "keywords": ["laundry"] }, - { "category": "objects", "char": "🧻", "name": "toilet_paper", "keywords": ["roll"] }, - { "category": "objects", "char": "🏷", "name": "label", "keywords": ["sale", "tag"] }, - { "category": "objects", "char": "🔖", "name": "bookmark", "keywords": ["favorite", "label", "save"] }, - { "category": "objects", "char": "🚽", "name": "toilet", "keywords": ["restroom", "wc", "washroom", "bathroom", "potty"] }, - { "category": "objects", "char": "🚿", "name": "shower", "keywords": ["clean", "water", "bathroom"] }, - { "category": "objects", "char": "🛁", "name": "bathtub", "keywords": ["clean", "shower", "bathroom"] }, - { "category": "objects", "char": "🧼", "name": "soap", "keywords": ["bar", "bathing", "cleaning", "lather"] }, - { "category": "objects", "char": "🧽", "name": "sponge", "keywords": ["absorbing", "cleaning", "porous"] }, - { "category": "objects", "char": "🧴", "name": "lotion_bottle", "keywords": ["moisturizer", "sunscreen"] }, - { "category": "objects", "char": "🔑", "name": "key", "keywords": ["lock", "door", "password"] }, - { "category": "objects", "char": "🗝", "name": "old_key", "keywords": ["lock", "door", "password"] }, - { "category": "objects", "char": "🛋", "name": "couch_and_lamp", "keywords": ["read", "chill"] }, - { "category": "objects", "char": "🪔", "name": "diya_Lamp", "keywords": ["light", "oil"] }, - { "category": "objects", "char": "🛌", "name": "sleeping_bed", "keywords": ["bed", "rest"] }, - { "category": "objects", "char": "🛏", "name": "bed", "keywords": ["sleep", "rest"] }, - { "category": "objects", "char": "🚪", "name": "door", "keywords": ["house", "entry", "exit"] }, - { "category": "objects", "char": "🪑", "name": "chair", "keywords": ["house", "desk"] }, - { "category": "objects", "char": "🛎", "name": "bellhop_bell", "keywords": ["service"] }, - { "category": "objects", "char": "🧸", "name": "teddy_bear", "keywords": ["plush", "stuffed"] }, - { "category": "objects", "char": "🖼", "name": "framed_picture", "keywords": ["photography"] }, - { "category": "objects", "char": "🗺", "name": "world_map", "keywords": ["location", "direction"] }, - { "category": "objects", "char": "🛗", "name": "elevator", "keywords": ["household"] }, - { "category": "objects", "char": "🪞", "name": "mirror", "keywords": ["household"] }, - { "category": "objects", "char": "🪟", "name": "window", "keywords": ["household"] }, - { "category": "objects", "char": "🪠", "name": "plunger", "keywords": ["household"] }, - { "category": "objects", "char": "🪤", "name": "mouse_trap", "keywords": ["household"] }, - { "category": "objects", "char": "🪣", "name": "bucket", "keywords": ["household"] }, - { "category": "objects", "char": "🪥", "name": "toothbrush", "keywords": ["household"] }, - { "category": "objects", "char": "\uD83E\uDEE7", "name": "bubbles", "keywords": [] }, - { "category": "objects", "char": "⛱", "name": "parasol_on_ground", "keywords": ["weather", "summer"] }, - { "category": "objects", "char": "🗿", "name": "moyai", "keywords": ["rock", "easter island", "moai"] }, - { "category": "objects", "char": "🛍", "name": "shopping", "keywords": ["mall", "buy", "purchase"] }, - { "category": "objects", "char": "🛒", "name": "shopping_cart", "keywords": ["trolley"] }, - { "category": "objects", "char": "🎈", "name": "balloon", "keywords": ["party", "celebration", "birthday", "circus"] }, - { "category": "objects", "char": "🎏", "name": "flags", "keywords": ["fish", "japanese", "koinobori", "carp", "banner"] }, - { "category": "objects", "char": "🎀", "name": "ribbon", "keywords": ["decoration", "pink", "girl", "bowtie"] }, - { "category": "objects", "char": "🎁", "name": "gift", "keywords": ["present", "birthday", "christmas", "xmas"] }, - { "category": "objects", "char": "🎊", "name": "confetti_ball", "keywords": ["festival", "party", "birthday", "circus"] }, - { "category": "objects", "char": "🎉", "name": "tada", "keywords": ["party", "congratulations", "birthday", "magic", "circus", "celebration"] }, - { "category": "objects", "char": "🎎", "name": "dolls", "keywords": ["japanese", "toy", "kimono"] }, - { "category": "objects", "char": "🎐", "name": "wind_chime", "keywords": ["nature", "ding", "spring", "bell"] }, - { "category": "objects", "char": "🎌", "name": "crossed_flags", "keywords": ["japanese", "nation", "country", "border"] }, - { "category": "objects", "char": "🏮", "name": "izakaya_lantern", "keywords": ["light", "paper", "halloween", "spooky"] }, - { "category": "objects", "char": "🧧", "name": "red_envelope", "keywords": ["gift"] }, - { "category": "objects", "char": "✉️", "name": "email", "keywords": ["letter", "postal", "inbox", "communication"] }, - { "category": "objects", "char": "📩", "name": "envelope_with_arrow", "keywords": ["email", "communication"] }, - { "category": "objects", "char": "📨", "name": "incoming_envelope", "keywords": ["email", "inbox"] }, - { "category": "objects", "char": "📧", "name": "e-mail", "keywords": ["communication", "inbox"] }, - { "category": "objects", "char": "💌", "name": "love_letter", "keywords": ["email", "like", "affection", "envelope", "valentines"] }, - { "category": "objects", "char": "📮", "name": "postbox", "keywords": ["email", "letter", "envelope"] }, - { "category": "objects", "char": "📪", "name": "mailbox_closed", "keywords": ["email", "communication", "inbox"] }, - { "category": "objects", "char": "📫", "name": "mailbox", "keywords": ["email", "inbox", "communication"] }, - { "category": "objects", "char": "📬", "name": "mailbox_with_mail", "keywords": ["email", "inbox", "communication"] }, - { "category": "objects", "char": "📭", "name": "mailbox_with_no_mail", "keywords": ["email", "inbox"] }, - { "category": "objects", "char": "📦", "name": "package", "keywords": ["mail", "gift", "cardboard", "box", "moving"] }, - { "category": "objects", "char": "📯", "name": "postal_horn", "keywords": ["instrument", "music"] }, - { "category": "objects", "char": "📥", "name": "inbox_tray", "keywords": ["email", "documents"] }, - { "category": "objects", "char": "📤", "name": "outbox_tray", "keywords": ["inbox", "email"] }, - { "category": "objects", "char": "📜", "name": "scroll", "keywords": ["documents", "ancient", "history", "paper"] }, - { "category": "objects", "char": "📃", "name": "page_with_curl", "keywords": ["documents", "office", "paper"] }, - { "category": "objects", "char": "📑", "name": "bookmark_tabs", "keywords": ["favorite", "save", "order", "tidy"] }, - { "category": "objects", "char": "🧾", "name": "receipt", "keywords": ["accounting", "expenses"] }, - { "category": "objects", "char": "📊", "name": "bar_chart", "keywords": ["graph", "presentation", "stats"] }, - { "category": "objects", "char": "📈", "name": "chart_with_upwards_trend", "keywords": ["graph", "presentation", "stats", "recovery", "business", "economics", "money", "sales", "good", "success"] }, - { "category": "objects", "char": "📉", "name": "chart_with_downwards_trend", "keywords": ["graph", "presentation", "stats", "recession", "business", "economics", "money", "sales", "bad", "failure"] }, - { "category": "objects", "char": "📄", "name": "page_facing_up", "keywords": ["documents", "office", "paper", "information"] }, - { "category": "objects", "char": "📅", "name": "date", "keywords": ["calendar", "schedule"] }, - { "category": "objects", "char": "📆", "name": "calendar", "keywords": ["schedule", "date", "planning"] }, - { "category": "objects", "char": "🗓", "name": "spiral_calendar", "keywords": ["date", "schedule", "planning"] }, - { "category": "objects", "char": "📇", "name": "card_index", "keywords": ["business", "stationery"] }, - { "category": "objects", "char": "🗃", "name": "card_file_box", "keywords": ["business", "stationery"] }, - { "category": "objects", "char": "🗳", "name": "ballot_box", "keywords": ["election", "vote"] }, - { "category": "objects", "char": "🗄", "name": "file_cabinet", "keywords": ["filing", "organizing"] }, - { "category": "objects", "char": "📋", "name": "clipboard", "keywords": ["stationery", "documents"] }, - { "category": "objects", "char": "🗒", "name": "spiral_notepad", "keywords": ["memo", "stationery"] }, - { "category": "objects", "char": "📁", "name": "file_folder", "keywords": ["documents", "business", "office"] }, - { "category": "objects", "char": "📂", "name": "open_file_folder", "keywords": ["documents", "load"] }, - { "category": "objects", "char": "🗂", "name": "card_index_dividers", "keywords": ["organizing", "business", "stationery"] }, - { "category": "objects", "char": "🗞", "name": "newspaper_roll", "keywords": ["press", "headline"] }, - { "category": "objects", "char": "📰", "name": "newspaper", "keywords": ["press", "headline"] }, - { "category": "objects", "char": "📓", "name": "notebook", "keywords": ["stationery", "record", "notes", "paper", "study"] }, - { "category": "objects", "char": "📕", "name": "closed_book", "keywords": ["read", "library", "knowledge", "textbook", "learn"] }, - { "category": "objects", "char": "📗", "name": "green_book", "keywords": ["read", "library", "knowledge", "study"] }, - { "category": "objects", "char": "📘", "name": "blue_book", "keywords": ["read", "library", "knowledge", "learn", "study"] }, - { "category": "objects", "char": "📙", "name": "orange_book", "keywords": ["read", "library", "knowledge", "textbook", "study"] }, - { "category": "objects", "char": "📔", "name": "notebook_with_decorative_cover", "keywords": ["classroom", "notes", "record", "paper", "study"] }, - { "category": "objects", "char": "📒", "name": "ledger", "keywords": ["notes", "paper"] }, - { "category": "objects", "char": "📚", "name": "books", "keywords": ["literature", "library", "study"] }, - { "category": "objects", "char": "📖", "name": "open_book", "keywords": ["book", "read", "library", "knowledge", "literature", "learn", "study"] }, - { "category": "objects", "char": "🧷", "name": "safety_pin", "keywords": ["diaper"] }, - { "category": "objects", "char": "🔗", "name": "link", "keywords": ["rings", "url"] }, - { "category": "objects", "char": "📎", "name": "paperclip", "keywords": ["documents", "stationery"] }, - { "category": "objects", "char": "🖇", "name": "paperclips", "keywords": ["documents", "stationery"] }, - { "category": "objects", "char": "✂️", "name": "scissors", "keywords": ["stationery", "cut"] }, - { "category": "objects", "char": "📐", "name": "triangular_ruler", "keywords": ["stationery", "math", "architect", "sketch"] }, - { "category": "objects", "char": "📏", "name": "straight_ruler", "keywords": ["stationery", "calculate", "length", "math", "school", "drawing", "architect", "sketch"] }, - { "category": "objects", "char": "🧮", "name": "abacus", "keywords": ["calculation"] }, - { "category": "objects", "char": "📌", "name": "pushpin", "keywords": ["stationery", "mark", "here"] }, - { "category": "objects", "char": "📍", "name": "round_pushpin", "keywords": ["stationery", "location", "map", "here"] }, - { "category": "objects", "char": "🚩", "name": "triangular_flag_on_post", "keywords": ["mark", "milestone", "place"] }, - { "category": "objects", "char": "🏳", "name": "white_flag", "keywords": ["losing", "loser", "lost", "surrender", "give up", "fail"] }, - { "category": "objects", "char": "🏴", "name": "black_flag", "keywords": ["pirate"] }, - { "category": "objects", "char": "🏳️‍🌈", "name": "rainbow_flag", "keywords": ["flag", "rainbow", "pride", "gay", "lgbt", "glbt", "queer", "homosexual", "lesbian", "bisexual", "transgender"] }, - { "category": "objects", "char": "🏳️‍⚧️", "name": "transgender_flag", "keywords": ["flag", "transgender"] }, - { "category": "objects", "char": "🔐", "name": "closed_lock_with_key", "keywords": ["security", "privacy"] }, - { "category": "objects", "char": "🔒", "name": "lock", "keywords": ["security", "password", "padlock"] }, - { "category": "objects", "char": "🔓", "name": "unlock", "keywords": ["privacy", "security"] }, - { "category": "objects", "char": "🔏", "name": "lock_with_ink_pen", "keywords": ["security", "secret"] }, - { "category": "objects", "char": "🖊", "name": "pen", "keywords": ["stationery", "writing", "write"] }, - { "category": "objects", "char": "🖋", "name": "fountain_pen", "keywords": ["stationery", "writing", "write"] }, - { "category": "objects", "char": "✒️", "name": "black_nib", "keywords": ["pen", "stationery", "writing", "write"] }, - { "category": "objects", "char": "📝", "name": "memo", "keywords": ["write", "documents", "stationery", "pencil", "paper", "writing", "legal", "exam", "quiz", "test", "study", "compose"] }, - { "category": "objects", "char": "✏️", "name": "pencil2", "keywords": ["stationery", "write", "paper", "writing", "school", "study"] }, - { "category": "objects", "char": "🖍", "name": "crayon", "keywords": ["drawing", "creativity"] }, - { "category": "objects", "char": "🖌", "name": "paintbrush", "keywords": ["drawing", "creativity", "art"] }, - { "category": "objects", "char": "🔍", "name": "mag", "keywords": ["search", "zoom", "find", "detective"] }, - { "category": "objects", "char": "🔎", "name": "mag_right", "keywords": ["search", "zoom", "find", "detective"] }, - { "category": "objects", "char": "🪦", "name": "headstone", "keywords": [] }, - { "category": "objects", "char": "🪧", "name": "placard", "keywords": [] }, - { "category": "symbols", "char": "💯", "name": "100", "keywords": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"] }, - { "category": "symbols", "char": "🔢", "name": "1234", "keywords": ["numbers", "blue-square"] }, - { "category": "symbols", "char": "❤️", "name": "heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "🧡", "name": "orange_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💛", "name": "yellow_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💚", "name": "green_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💙", "name": "blue_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💜", "name": "purple_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "🤎", "name": "brown_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "🖤", "name": "black_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "🤍", "name": "white_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💔", "name": "broken_heart", "keywords": ["sad", "sorry", "break", "heart", "heartbreak"] }, - { "category": "symbols", "char": "❣", "name": "heavy_heart_exclamation", "keywords": ["decoration", "love"] }, - { "category": "symbols", "char": "💕", "name": "two_hearts", "keywords": ["love", "like", "affection", "valentines", "heart"] }, - { "category": "symbols", "char": "💞", "name": "revolving_hearts", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💓", "name": "heartbeat", "keywords": ["love", "like", "affection", "valentines", "pink", "heart"] }, - { "category": "symbols", "char": "💗", "name": "heartpulse", "keywords": ["like", "love", "affection", "valentines", "pink"] }, - { "category": "symbols", "char": "💖", "name": "sparkling_heart", "keywords": ["love", "like", "affection", "valentines"] }, - { "category": "symbols", "char": "💘", "name": "cupid", "keywords": ["love", "like", "heart", "affection", "valentines"] }, - { "category": "symbols", "char": "💝", "name": "gift_heart", "keywords": ["love", "valentines"] }, - { "category": "symbols", "char": "💟", "name": "heart_decoration", "keywords": ["purple-square", "love", "like"] }, - { "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83D\uDD25", "name": "heart_on_fire", "keywords": [] }, - { "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83E\uDE79", "name": "mending_heart", "keywords": [] }, - { "category": "symbols", "char": "☮", "name": "peace_symbol", "keywords": ["hippie"] }, - { "category": "symbols", "char": "✝", "name": "latin_cross", "keywords": ["christianity"] }, - { "category": "symbols", "char": "☪", "name": "star_and_crescent", "keywords": ["islam"] }, - { "category": "symbols", "char": "🕉", "name": "om", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] }, - { "category": "symbols", "char": "☸", "name": "wheel_of_dharma", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] }, - { "category": "symbols", "char": "✡", "name": "star_of_david", "keywords": ["judaism"] }, - { "category": "symbols", "char": "🔯", "name": "six_pointed_star", "keywords": ["purple-square", "religion", "jewish", "hexagram"] }, - { "category": "symbols", "char": "🕎", "name": "menorah", "keywords": ["hanukkah", "candles", "jewish"] }, - { "category": "symbols", "char": "☯", "name": "yin_yang", "keywords": ["balance"] }, - { "category": "symbols", "char": "☦", "name": "orthodox_cross", "keywords": ["suppedaneum", "religion"] }, - { "category": "symbols", "char": "🛐", "name": "place_of_worship", "keywords": ["religion", "church", "temple", "prayer"] }, - { "category": "symbols", "char": "⛎", "name": "ophiuchus", "keywords": ["sign", "purple-square", "constellation", "astrology"] }, - { "category": "symbols", "char": "♈", "name": "aries", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♉", "name": "taurus", "keywords": ["purple-square", "sign", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♊", "name": "gemini", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♋", "name": "cancer", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♌", "name": "leo", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♍", "name": "virgo", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♎", "name": "libra", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♏", "name": "scorpius", "keywords": ["sign", "zodiac", "purple-square", "astrology", "scorpio"] }, - { "category": "symbols", "char": "♐", "name": "sagittarius", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♑", "name": "capricorn", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, - { "category": "symbols", "char": "♒", "name": "aquarius", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, - { "category": "symbols", "char": "♓", "name": "pisces", "keywords": ["purple-square", "sign", "zodiac", "astrology"] }, - { "category": "symbols", "char": "🆔", "name": "id", "keywords": ["purple-square", "words"] }, - { "category": "symbols", "char": "⚛", "name": "atom_symbol", "keywords": ["science", "physics", "chemistry"] }, - { "category": "symbols", "char": "⚧️", "name": "transgender_symbol", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] }, - { "category": "symbols", "char": "🈳", "name": "u7a7a", "keywords": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square", "aki"] }, - { "category": "symbols", "char": "🈹", "name": "u5272", "keywords": ["cut", "divide", "chinese", "kanji", "pink-square", "waribiki"] }, - { "category": "symbols", "char": "☢", "name": "radioactive", "keywords": ["nuclear", "danger"] }, - { "category": "symbols", "char": "☣", "name": "biohazard", "keywords": ["danger"] }, - { "category": "symbols", "char": "📴", "name": "mobile_phone_off", "keywords": ["mute", "orange-square", "silence", "quiet"] }, - { "category": "symbols", "char": "📳", "name": "vibration_mode", "keywords": ["orange-square", "phone"] }, - { "category": "symbols", "char": "🈶", "name": "u6709", "keywords": ["orange-square", "chinese", "have", "kanji", "ari"] }, - { "category": "symbols", "char": "🈚", "name": "u7121", "keywords": ["nothing", "chinese", "kanji", "japanese", "orange-square", "nashi"] }, - { "category": "symbols", "char": "🈸", "name": "u7533", "keywords": ["chinese", "japanese", "kanji", "orange-square", "moushikomi"] }, - { "category": "symbols", "char": "🈺", "name": "u55b6", "keywords": ["japanese", "opening hours", "orange-square", "eigyo"] }, - { "category": "symbols", "char": "🈷️", "name": "u6708", "keywords": ["chinese", "month", "moon", "japanese", "orange-square", "kanji", "tsuki", "tsukigime", "getsugaku"] }, - { "category": "symbols", "char": "✴️", "name": "eight_pointed_black_star", "keywords": ["orange-square", "shape", "polygon"] }, - { "category": "symbols", "char": "🆚", "name": "vs", "keywords": ["words", "orange-square"] }, - { "category": "symbols", "char": "🉑", "name": "accept", "keywords": ["ok", "good", "chinese", "kanji", "agree", "yes", "orange-circle"] }, - { "category": "symbols", "char": "💮", "name": "white_flower", "keywords": ["japanese", "spring"] }, - { "category": "symbols", "char": "🉐", "name": "ideograph_advantage", "keywords": ["chinese", "kanji", "obtain", "get", "circle"] }, - { "category": "symbols", "char": "㊙️", "name": "secret", "keywords": ["privacy", "chinese", "sshh", "kanji", "red-circle"] }, - { "category": "symbols", "char": "㊗️", "name": "congratulations", "keywords": ["chinese", "kanji", "japanese", "red-circle"] }, - { "category": "symbols", "char": "🈴", "name": "u5408", "keywords": ["japanese", "chinese", "join", "kanji", "red-square", "goukaku", "pass"] }, - { "category": "symbols", "char": "🈵", "name": "u6e80", "keywords": ["full", "chinese", "japanese", "red-square", "kanji", "man"] }, - { "category": "symbols", "char": "🈲", "name": "u7981", "keywords": ["kanji", "japanese", "chinese", "forbidden", "limit", "restricted", "red-square", "kinshi"] }, - { "category": "symbols", "char": "🅰️", "name": "a", "keywords": ["red-square", "alphabet", "letter"] }, - { "category": "symbols", "char": "🅱️", "name": "b", "keywords": ["red-square", "alphabet", "letter"] }, - { "category": "symbols", "char": "🆎", "name": "ab", "keywords": ["red-square", "alphabet"] }, - { "category": "symbols", "char": "🆑", "name": "cl", "keywords": ["alphabet", "words", "red-square"] }, - { "category": "symbols", "char": "🅾️", "name": "o2", "keywords": ["alphabet", "red-square", "letter"] }, - { "category": "symbols", "char": "🆘", "name": "sos", "keywords": ["help", "red-square", "words", "emergency", "911"] }, - { "category": "symbols", "char": "⛔", "name": "no_entry", "keywords": ["limit", "security", "privacy", "bad", "denied", "stop", "circle"] }, - { "category": "symbols", "char": "📛", "name": "name_badge", "keywords": ["fire", "forbid"] }, - { "category": "symbols", "char": "🚫", "name": "no_entry_sign", "keywords": ["forbid", "stop", "limit", "denied", "disallow", "circle"] }, - { "category": "symbols", "char": "❌", "name": "x", "keywords": ["no", "delete", "remove", "cancel", "red"] }, - { "category": "symbols", "char": "⭕", "name": "o", "keywords": ["circle", "round"] }, - { "category": "symbols", "char": "🛑", "name": "stop_sign", "keywords": ["stop"] }, - { "category": "symbols", "char": "💢", "name": "anger", "keywords": ["angry", "mad"] }, - { "category": "symbols", "char": "♨️", "name": "hotsprings", "keywords": ["bath", "warm", "relax"] }, - { "category": "symbols", "char": "🚷", "name": "no_pedestrians", "keywords": ["rules", "crossing", "walking", "circle"] }, - { "category": "symbols", "char": "🚯", "name": "do_not_litter", "keywords": ["trash", "bin", "garbage", "circle"] }, - { "category": "symbols", "char": "🚳", "name": "no_bicycles", "keywords": ["cyclist", "prohibited", "circle"] }, - { "category": "symbols", "char": "🚱", "name": "non-potable_water", "keywords": ["drink", "faucet", "tap", "circle"] }, - { "category": "symbols", "char": "🔞", "name": "underage", "keywords": ["18", "drink", "pub", "night", "minor", "circle"] }, - { "category": "symbols", "char": "📵", "name": "no_mobile_phones", "keywords": ["iphone", "mute", "circle"] }, - { "category": "symbols", "char": "❗", "name": "exclamation", "keywords": ["heavy_exclamation_mark", "danger", "surprise", "punctuation", "wow", "warning"] }, - { "category": "symbols", "char": "❕", "name": "grey_exclamation", "keywords": ["surprise", "punctuation", "gray", "wow", "warning"] }, - { "category": "symbols", "char": "❓", "name": "question", "keywords": ["doubt", "confused"] }, - { "category": "symbols", "char": "❔", "name": "grey_question", "keywords": ["doubts", "gray", "huh", "confused"] }, - { "category": "symbols", "char": "‼️", "name": "bangbang", "keywords": ["exclamation", "surprise"] }, - { "category": "symbols", "char": "⁉️", "name": "interrobang", "keywords": ["wat", "punctuation", "surprise"] }, - { "category": "symbols", "char": "🔅", "name": "low_brightness", "keywords": ["sun", "afternoon", "warm", "summer"] }, - { "category": "symbols", "char": "🔆", "name": "high_brightness", "keywords": ["sun", "light"] }, - { "category": "symbols", "char": "🔱", "name": "trident", "keywords": ["weapon", "spear"] }, - { "category": "symbols", "char": "⚜", "name": "fleur_de_lis", "keywords": ["decorative", "scout"] }, - { "category": "symbols", "char": "〽️", "name": "part_alternation_mark", "keywords": ["graph", "presentation", "stats", "business", "economics", "bad"] }, - { "category": "symbols", "char": "⚠️", "name": "warning", "keywords": ["exclamation", "wip", "alert", "error", "problem", "issue"] }, - { "category": "symbols", "char": "🚸", "name": "children_crossing", "keywords": ["school", "warning", "danger", "sign", "driving", "yellow-diamond"] }, - { "category": "symbols", "char": "🔰", "name": "beginner", "keywords": ["badge", "shield"] }, - { "category": "symbols", "char": "♻️", "name": "recycle", "keywords": ["arrow", "environment", "garbage", "trash"] }, - { "category": "symbols", "char": "🈯", "name": "u6307", "keywords": ["chinese", "point", "green-square", "kanji", "reserved", "shiteiseki"] }, - { "category": "symbols", "char": "💹", "name": "chart", "keywords": ["green-square", "graph", "presentation", "stats"] }, - { "category": "symbols", "char": "❇️", "name": "sparkle", "keywords": ["stars", "green-square", "awesome", "good", "fireworks"] }, - { "category": "symbols", "char": "✳️", "name": "eight_spoked_asterisk", "keywords": ["star", "sparkle", "green-square"] }, - { "category": "symbols", "char": "❎", "name": "negative_squared_cross_mark", "keywords": ["x", "green-square", "no", "deny"] }, - { "category": "symbols", "char": "✅", "name": "white_check_mark", "keywords": ["green-square", "ok", "agree", "vote", "election", "answer", "tick"] }, - { "category": "symbols", "char": "💠", "name": "diamond_shape_with_a_dot_inside", "keywords": ["jewel", "blue", "gem", "crystal", "fancy"] }, - { "category": "symbols", "char": "🌀", "name": "cyclone", "keywords": ["weather", "swirl", "blue", "cloud", "vortex", "spiral", "whirlpool", "spin", "tornado", "hurricane", "typhoon"] }, - { "category": "symbols", "char": "➿", "name": "loop", "keywords": ["tape", "cassette"] }, - { "category": "symbols", "char": "🌐", "name": "globe_with_meridians", "keywords": ["earth", "international", "world", "internet", "interweb", "i18n"] }, - { "category": "symbols", "char": "Ⓜ️", "name": "m", "keywords": ["alphabet", "blue-circle", "letter"] }, - { "category": "symbols", "char": "🏧", "name": "atm", "keywords": ["money", "sales", "cash", "blue-square", "payment", "bank"] }, - { "category": "symbols", "char": "🈂️", "name": "sa", "keywords": ["japanese", "blue-square", "katakana"] }, - { "category": "symbols", "char": "🛂", "name": "passport_control", "keywords": ["custom", "blue-square"] }, - { "category": "symbols", "char": "🛃", "name": "customs", "keywords": ["passport", "border", "blue-square"] }, - { "category": "symbols", "char": "🛄", "name": "baggage_claim", "keywords": ["blue-square", "airport", "transport"] }, - { "category": "symbols", "char": "🛅", "name": "left_luggage", "keywords": ["blue-square", "travel"] }, - { "category": "symbols", "char": "♿", "name": "wheelchair", "keywords": ["blue-square", "disabled", "a11y", "accessibility"] }, - { "category": "symbols", "char": "🚭", "name": "no_smoking", "keywords": ["cigarette", "blue-square", "smell", "smoke"] }, - { "category": "symbols", "char": "🚾", "name": "wc", "keywords": ["toilet", "restroom", "blue-square"] }, - { "category": "symbols", "char": "🅿️", "name": "parking", "keywords": ["cars", "blue-square", "alphabet", "letter"] }, - { "category": "symbols", "char": "🚰", "name": "potable_water", "keywords": ["blue-square", "liquid", "restroom", "cleaning", "faucet"] }, - { "category": "symbols", "char": "🚹", "name": "mens", "keywords": ["toilet", "restroom", "wc", "blue-square", "gender", "male"] }, - { "category": "symbols", "char": "🚺", "name": "womens", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] }, - { "category": "symbols", "char": "🚼", "name": "baby_symbol", "keywords": ["orange-square", "child"] }, - { "category": "symbols", "char": "🚻", "name": "restroom", "keywords": ["blue-square", "toilet", "refresh", "wc", "gender"] }, - { "category": "symbols", "char": "🚮", "name": "put_litter_in_its_place", "keywords": ["blue-square", "sign", "human", "info"] }, - { "category": "symbols", "char": "🎦", "name": "cinema", "keywords": ["blue-square", "record", "film", "movie", "curtain", "stage", "theater"] }, - { "category": "symbols", "char": "📶", "name": "signal_strength", "keywords": ["blue-square", "reception", "phone", "internet", "connection", "wifi", "bluetooth", "bars"] }, - { "category": "symbols", "char": "🈁", "name": "koko", "keywords": ["blue-square", "here", "katakana", "japanese", "destination"] }, - { "category": "symbols", "char": "🆖", "name": "ng", "keywords": ["blue-square", "words", "shape", "icon"] }, - { "category": "symbols", "char": "🆗", "name": "ok", "keywords": ["good", "agree", "yes", "blue-square"] }, - { "category": "symbols", "char": "🆙", "name": "up", "keywords": ["blue-square", "above", "high"] }, - { "category": "symbols", "char": "🆒", "name": "cool", "keywords": ["words", "blue-square"] }, - { "category": "symbols", "char": "🆕", "name": "new", "keywords": ["blue-square", "words", "start"] }, - { "category": "symbols", "char": "🆓", "name": "free", "keywords": ["blue-square", "words"] }, - { "category": "symbols", "char": "0️⃣", "name": "zero", "keywords": ["0", "numbers", "blue-square", "null"] }, - { "category": "symbols", "char": "1️⃣", "name": "one", "keywords": ["blue-square", "numbers", "1"] }, - { "category": "symbols", "char": "2️⃣", "name": "two", "keywords": ["numbers", "2", "prime", "blue-square"] }, - { "category": "symbols", "char": "3️⃣", "name": "three", "keywords": ["3", "numbers", "prime", "blue-square"] }, - { "category": "symbols", "char": "4️⃣", "name": "four", "keywords": ["4", "numbers", "blue-square"] }, - { "category": "symbols", "char": "5️⃣", "name": "five", "keywords": ["5", "numbers", "blue-square", "prime"] }, - { "category": "symbols", "char": "6️⃣", "name": "six", "keywords": ["6", "numbers", "blue-square"] }, - { "category": "symbols", "char": "7️⃣", "name": "seven", "keywords": ["7", "numbers", "blue-square", "prime"] }, - { "category": "symbols", "char": "8️⃣", "name": "eight", "keywords": ["8", "blue-square", "numbers"] }, - { "category": "symbols", "char": "9️⃣", "name": "nine", "keywords": ["blue-square", "numbers", "9"] }, - { "category": "symbols", "char": "🔟", "name": "keycap_ten", "keywords": ["numbers", "10", "blue-square"] }, - { "category": "symbols", "char": "*⃣", "name": "asterisk", "keywords": ["star", "keycap"] }, - { "category": "symbols", "char": "⏏️", "name": "eject_button", "keywords": ["blue-square"] }, - { "category": "symbols", "char": "▶️", "name": "arrow_forward", "keywords": ["blue-square", "right", "direction", "play"] }, - { "category": "symbols", "char": "⏸", "name": "pause_button", "keywords": ["pause", "blue-square"] }, - { "category": "symbols", "char": "⏭", "name": "next_track_button", "keywords": ["forward", "next", "blue-square"] }, - { "category": "symbols", "char": "⏹", "name": "stop_button", "keywords": ["blue-square"] }, - { "category": "symbols", "char": "⏺", "name": "record_button", "keywords": ["blue-square"] }, - { "category": "symbols", "char": "⏯", "name": "play_or_pause_button", "keywords": ["blue-square", "play", "pause"] }, - { "category": "symbols", "char": "⏮", "name": "previous_track_button", "keywords": ["backward"] }, - { "category": "symbols", "char": "⏩", "name": "fast_forward", "keywords": ["blue-square", "play", "speed", "continue"] }, - { "category": "symbols", "char": "⏪", "name": "rewind", "keywords": ["play", "blue-square"] }, - { "category": "symbols", "char": "🔀", "name": "twisted_rightwards_arrows", "keywords": ["blue-square", "shuffle", "music", "random"] }, - { "category": "symbols", "char": "🔁", "name": "repeat", "keywords": ["loop", "record"] }, - { "category": "symbols", "char": "🔂", "name": "repeat_one", "keywords": ["blue-square", "loop"] }, - { "category": "symbols", "char": "◀️", "name": "arrow_backward", "keywords": ["blue-square", "left", "direction"] }, - { "category": "symbols", "char": "🔼", "name": "arrow_up_small", "keywords": ["blue-square", "triangle", "direction", "point", "forward", "top"] }, - { "category": "symbols", "char": "🔽", "name": "arrow_down_small", "keywords": ["blue-square", "direction", "bottom"] }, - { "category": "symbols", "char": "⏫", "name": "arrow_double_up", "keywords": ["blue-square", "direction", "top"] }, - { "category": "symbols", "char": "⏬", "name": "arrow_double_down", "keywords": ["blue-square", "direction", "bottom"] }, - { "category": "symbols", "char": "➡️", "name": "arrow_right", "keywords": ["blue-square", "next"] }, - { "category": "symbols", "char": "⬅️", "name": "arrow_left", "keywords": ["blue-square", "previous", "back"] }, - { "category": "symbols", "char": "⬆️", "name": "arrow_up", "keywords": ["blue-square", "continue", "top", "direction"] }, - { "category": "symbols", "char": "⬇️", "name": "arrow_down", "keywords": ["blue-square", "direction", "bottom"] }, - { "category": "symbols", "char": "↗️", "name": "arrow_upper_right", "keywords": ["blue-square", "point", "direction", "diagonal", "northeast"] }, - { "category": "symbols", "char": "↘️", "name": "arrow_lower_right", "keywords": ["blue-square", "direction", "diagonal", "southeast"] }, - { "category": "symbols", "char": "↙️", "name": "arrow_lower_left", "keywords": ["blue-square", "direction", "diagonal", "southwest"] }, - { "category": "symbols", "char": "↖️", "name": "arrow_upper_left", "keywords": ["blue-square", "point", "direction", "diagonal", "northwest"] }, - { "category": "symbols", "char": "↕️", "name": "arrow_up_down", "keywords": ["blue-square", "direction", "way", "vertical"] }, - { "category": "symbols", "char": "↔️", "name": "left_right_arrow", "keywords": ["shape", "direction", "horizontal", "sideways"] }, - { "category": "symbols", "char": "🔄", "name": "arrows_counterclockwise", "keywords": ["blue-square", "sync", "cycle"] }, - { "category": "symbols", "char": "↪️", "name": "arrow_right_hook", "keywords": ["blue-square", "return", "rotate", "direction"] }, - { "category": "symbols", "char": "↩️", "name": "leftwards_arrow_with_hook", "keywords": ["back", "return", "blue-square", "undo", "enter"] }, - { "category": "symbols", "char": "⤴️", "name": "arrow_heading_up", "keywords": ["blue-square", "direction", "top"] }, - { "category": "symbols", "char": "⤵️", "name": "arrow_heading_down", "keywords": ["blue-square", "direction", "bottom"] }, - { "category": "symbols", "char": "#️⃣", "name": "hash", "keywords": ["symbol", "blue-square", "twitter"] }, - { "category": "symbols", "char": "ℹ️", "name": "information_source", "keywords": ["blue-square", "alphabet", "letter"] }, - { "category": "symbols", "char": "🔤", "name": "abc", "keywords": ["blue-square", "alphabet"] }, - { "category": "symbols", "char": "🔡", "name": "abcd", "keywords": ["blue-square", "alphabet"] }, - { "category": "symbols", "char": "🔠", "name": "capital_abcd", "keywords": ["alphabet", "words", "blue-square"] }, - { "category": "symbols", "char": "🔣", "name": "symbols", "keywords": ["blue-square", "music", "note", "ampersand", "percent", "glyphs", "characters"] }, - { "category": "symbols", "char": "🎵", "name": "musical_note", "keywords": ["score", "tone", "sound"] }, - { "category": "symbols", "char": "🎶", "name": "notes", "keywords": ["music", "score"] }, - { "category": "symbols", "char": "〰️", "name": "wavy_dash", "keywords": ["draw", "line", "moustache", "mustache", "squiggle", "scribble"] }, - { "category": "symbols", "char": "➰", "name": "curly_loop", "keywords": ["scribble", "draw", "shape", "squiggle"] }, - { "category": "symbols", "char": "✔️", "name": "heavy_check_mark", "keywords": ["ok", "nike", "answer", "yes", "tick"] }, - { "category": "symbols", "char": "🔃", "name": "arrows_clockwise", "keywords": ["sync", "cycle", "round", "repeat"] }, - { "category": "symbols", "char": "➕", "name": "heavy_plus_sign", "keywords": ["math", "calculation", "addition", "more", "increase"] }, - { "category": "symbols", "char": "➖", "name": "heavy_minus_sign", "keywords": ["math", "calculation", "subtract", "less"] }, - { "category": "symbols", "char": "➗", "name": "heavy_division_sign", "keywords": ["divide", "math", "calculation"] }, - { "category": "symbols", "char": "✖️", "name": "heavy_multiplication_x", "keywords": ["math", "calculation"] }, - { "category": "symbols", "char": "\uD83D\uDFF0", "name": "heavy_equals_sign", "keywords": [] }, - { "category": "symbols", "char": "♾", "name": "infinity", "keywords": ["forever"] }, - { "category": "symbols", "char": "💲", "name": "heavy_dollar_sign", "keywords": ["money", "sales", "payment", "currency", "buck"] }, - { "category": "symbols", "char": "💱", "name": "currency_exchange", "keywords": ["money", "sales", "dollar", "travel"] }, - { "category": "symbols", "char": "©️", "name": "copyright", "keywords": ["ip", "license", "circle", "law", "legal"] }, - { "category": "symbols", "char": "®️", "name": "registered", "keywords": ["alphabet", "circle"] }, - { "category": "symbols", "char": "™️", "name": "tm", "keywords": ["trademark", "brand", "law", "legal"] }, - { "category": "symbols", "char": "🔚", "name": "end", "keywords": ["words", "arrow"] }, - { "category": "symbols", "char": "🔙", "name": "back", "keywords": ["arrow", "words", "return"] }, - { "category": "symbols", "char": "🔛", "name": "on", "keywords": ["arrow", "words"] }, - { "category": "symbols", "char": "🔝", "name": "top", "keywords": ["words", "blue-square"] }, - { "category": "symbols", "char": "🔜", "name": "soon", "keywords": ["arrow", "words"] }, - { "category": "symbols", "char": "☑️", "name": "ballot_box_with_check", "keywords": ["ok", "agree", "confirm", "black-square", "vote", "election", "yes", "tick"] }, - { "category": "symbols", "char": "🔘", "name": "radio_button", "keywords": ["input", "old", "music", "circle"] }, - { "category": "symbols", "char": "⚫", "name": "black_circle", "keywords": ["shape", "button", "round"] }, - { "category": "symbols", "char": "⚪", "name": "white_circle", "keywords": ["shape", "round"] }, - { "category": "symbols", "char": "🔴", "name": "red_circle", "keywords": ["shape", "error", "danger"] }, - { "category": "symbols", "char": "🟠", "name": "orange_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟡", "name": "yellow_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟢", "name": "green_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🔵", "name": "large_blue_circle", "keywords": ["shape", "icon", "button"] }, - { "category": "symbols", "char": "🟣", "name": "purple_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟤", "name": "brown_circle", "keywords": ["shape"] }, - { "category": "symbols", "char": "🔸", "name": "small_orange_diamond", "keywords": ["shape", "jewel", "gem"] }, - { "category": "symbols", "char": "🔹", "name": "small_blue_diamond", "keywords": ["shape", "jewel", "gem"] }, - { "category": "symbols", "char": "🔶", "name": "large_orange_diamond", "keywords": ["shape", "jewel", "gem"] }, - { "category": "symbols", "char": "🔷", "name": "large_blue_diamond", "keywords": ["shape", "jewel", "gem"] }, - { "category": "symbols", "char": "🔺", "name": "small_red_triangle", "keywords": ["shape", "direction", "up", "top"] }, - { "category": "symbols", "char": "▪️", "name": "black_small_square", "keywords": ["shape", "icon"] }, - { "category": "symbols", "char": "▫️", "name": "white_small_square", "keywords": ["shape", "icon"] }, - { "category": "symbols", "char": "⬛", "name": "black_large_square", "keywords": ["shape", "icon", "button"] }, - { "category": "symbols", "char": "⬜", "name": "white_large_square", "keywords": ["shape", "icon", "stone", "button"] }, - { "category": "symbols", "char": "🟥", "name": "red_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟧", "name": "orange_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟨", "name": "yellow_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟩", "name": "green_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟦", "name": "blue_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟪", "name": "purple_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🟫", "name": "brown_square", "keywords": ["shape"] }, - { "category": "symbols", "char": "🔻", "name": "small_red_triangle_down", "keywords": ["shape", "direction", "bottom"] }, - { "category": "symbols", "char": "◼️", "name": "black_medium_square", "keywords": ["shape", "button", "icon"] }, - { "category": "symbols", "char": "◻️", "name": "white_medium_square", "keywords": ["shape", "stone", "icon"] }, - { "category": "symbols", "char": "◾", "name": "black_medium_small_square", "keywords": ["icon", "shape", "button"] }, - { "category": "symbols", "char": "◽", "name": "white_medium_small_square", "keywords": ["shape", "stone", "icon", "button"] }, - { "category": "symbols", "char": "🔲", "name": "black_square_button", "keywords": ["shape", "input", "frame"] }, - { "category": "symbols", "char": "🔳", "name": "white_square_button", "keywords": ["shape", "input"] }, - { "category": "symbols", "char": "🔈", "name": "speaker", "keywords": ["sound", "volume", "silence", "broadcast"] }, - { "category": "symbols", "char": "🔉", "name": "sound", "keywords": ["volume", "speaker", "broadcast"] }, - { "category": "symbols", "char": "🔊", "name": "loud_sound", "keywords": ["volume", "noise", "noisy", "speaker", "broadcast"] }, - { "category": "symbols", "char": "🔇", "name": "mute", "keywords": ["sound", "volume", "silence", "quiet"] }, - { "category": "symbols", "char": "📣", "name": "mega", "keywords": ["sound", "speaker", "volume"] }, - { "category": "symbols", "char": "📢", "name": "loudspeaker", "keywords": ["volume", "sound"] }, - { "category": "symbols", "char": "🔔", "name": "bell", "keywords": ["sound", "notification", "christmas", "xmas", "chime"] }, - { "category": "symbols", "char": "🔕", "name": "no_bell", "keywords": ["sound", "volume", "mute", "quiet", "silent"] }, - { "category": "symbols", "char": "🃏", "name": "black_joker", "keywords": ["poker", "cards", "game", "play", "magic"] }, - { "category": "symbols", "char": "🀄", "name": "mahjong", "keywords": ["game", "play", "chinese", "kanji"] }, - { "category": "symbols", "char": "♠️", "name": "spades", "keywords": ["poker", "cards", "suits", "magic"] }, - { "category": "symbols", "char": "♣️", "name": "clubs", "keywords": ["poker", "cards", "magic", "suits"] }, - { "category": "symbols", "char": "♥️", "name": "hearts", "keywords": ["poker", "cards", "magic", "suits"] }, - { "category": "symbols", "char": "♦️", "name": "diamonds", "keywords": ["poker", "cards", "magic", "suits"] }, - { "category": "symbols", "char": "🎴", "name": "flower_playing_cards", "keywords": ["game", "sunset", "red"] }, - { "category": "symbols", "char": "💭", "name": "thought_balloon", "keywords": ["bubble", "cloud", "speech", "thinking", "dream"] }, - { "category": "symbols", "char": "🗯", "name": "right_anger_bubble", "keywords": ["caption", "speech", "thinking", "mad"] }, - { "category": "symbols", "char": "💬", "name": "speech_balloon", "keywords": ["bubble", "words", "message", "talk", "chatting"] }, - { "category": "symbols", "char": "🗨", "name": "left_speech_bubble", "keywords": ["words", "message", "talk", "chatting"] }, - { "category": "symbols", "char": "🕐", "name": "clock1", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕑", "name": "clock2", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕒", "name": "clock3", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕓", "name": "clock4", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕔", "name": "clock5", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕕", "name": "clock6", "keywords": ["time", "late", "early", "schedule", "dawn", "dusk"] }, - { "category": "symbols", "char": "🕖", "name": "clock7", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕗", "name": "clock8", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕘", "name": "clock9", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕙", "name": "clock10", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕚", "name": "clock11", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕛", "name": "clock12", "keywords": ["time", "noon", "midnight", "midday", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕜", "name": "clock130", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕝", "name": "clock230", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕞", "name": "clock330", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕟", "name": "clock430", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕠", "name": "clock530", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕡", "name": "clock630", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕢", "name": "clock730", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕣", "name": "clock830", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕤", "name": "clock930", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕥", "name": "clock1030", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕦", "name": "clock1130", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "symbols", "char": "🕧", "name": "clock1230", "keywords": ["time", "late", "early", "schedule"] }, - { "category": "flags", "char": "🇦🇫", "name": "afghanistan", "keywords": ["af", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇽", "name": "aland_islands", "keywords": ["Åland", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇱", "name": "albania", "keywords": ["al", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇿", "name": "algeria", "keywords": ["dz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇸", "name": "american_samoa", "keywords": ["american", "ws", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇩", "name": "andorra", "keywords": ["ad", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇴", "name": "angola", "keywords": ["ao", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇮", "name": "anguilla", "keywords": ["ai", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇶", "name": "antarctica", "keywords": ["aq", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇬", "name": "antigua_barbuda", "keywords": ["antigua", "barbuda", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇷", "name": "argentina", "keywords": ["ar", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇲", "name": "armenia", "keywords": ["am", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇼", "name": "aruba", "keywords": ["aw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇨", "name": "ascension_island", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇺", "name": "australia", "keywords": ["au", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇹", "name": "austria", "keywords": ["at", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇿", "name": "azerbaijan", "keywords": ["az", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇸", "name": "bahamas", "keywords": ["bs", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇭", "name": "bahrain", "keywords": ["bh", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇩", "name": "bangladesh", "keywords": ["bd", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇧", "name": "barbados", "keywords": ["bb", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇾", "name": "belarus", "keywords": ["by", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇪", "name": "belgium", "keywords": ["be", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇿", "name": "belize", "keywords": ["bz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇯", "name": "benin", "keywords": ["bj", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇲", "name": "bermuda", "keywords": ["bm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇹", "name": "bhutan", "keywords": ["bt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇴", "name": "bolivia", "keywords": ["bo", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇶", "name": "caribbean_netherlands", "keywords": ["bonaire", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇦", "name": "bosnia_herzegovina", "keywords": ["bosnia", "herzegovina", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇼", "name": "botswana", "keywords": ["bw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇷", "name": "brazil", "keywords": ["br", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇴", "name": "british_indian_ocean_territory", "keywords": ["british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇬", "name": "british_virgin_islands", "keywords": ["british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇳", "name": "brunei", "keywords": ["bn", "darussalam", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇬", "name": "bulgaria", "keywords": ["bg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇫", "name": "burkina_faso", "keywords": ["burkina", "faso", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇮", "name": "burundi", "keywords": ["bi", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇻", "name": "cape_verde", "keywords": ["cabo", "verde", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇭", "name": "cambodia", "keywords": ["kh", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇲", "name": "cameroon", "keywords": ["cm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇦", "name": "canada", "keywords": ["ca", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇨", "name": "canary_islands", "keywords": ["canary", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇾", "name": "cayman_islands", "keywords": ["cayman", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇫", "name": "central_african_republic", "keywords": ["central", "african", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇩", "name": "chad", "keywords": ["td", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇱", "name": "chile", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇳", "name": "cn", "keywords": ["china", "chinese", "prc", "flag", "country", "nation", "banner"] }, - { "category": "flags", "char": "🇨🇽", "name": "christmas_island", "keywords": ["christmas", "island", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇨", "name": "cocos_islands", "keywords": ["cocos", "keeling", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇴", "name": "colombia", "keywords": ["co", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇲", "name": "comoros", "keywords": ["km", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇬", "name": "congo_brazzaville", "keywords": ["congo", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇩", "name": "congo_kinshasa", "keywords": ["congo", "democratic", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇰", "name": "cook_islands", "keywords": ["cook", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇷", "name": "costa_rica", "keywords": ["costa", "rica", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇷", "name": "croatia", "keywords": ["hr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇺", "name": "cuba", "keywords": ["cu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇼", "name": "curacao", "keywords": ["curaçao", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇾", "name": "cyprus", "keywords": ["cy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇿", "name": "czech_republic", "keywords": ["cz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇰", "name": "denmark", "keywords": ["dk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇯", "name": "djibouti", "keywords": ["dj", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇲", "name": "dominica", "keywords": ["dm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇴", "name": "dominican_republic", "keywords": ["dominican", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇨", "name": "ecuador", "keywords": ["ec", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇬", "name": "egypt", "keywords": ["eg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇻", "name": "el_salvador", "keywords": ["el", "salvador", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇶", "name": "equatorial_guinea", "keywords": ["equatorial", "gn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇷", "name": "eritrea", "keywords": ["er", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇪", "name": "estonia", "keywords": ["ee", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇹", "name": "ethiopia", "keywords": ["et", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇺", "name": "eu", "keywords": ["european", "union", "flag", "banner"] }, - { "category": "flags", "char": "🇫🇰", "name": "falkland_islands", "keywords": ["falkland", "islands", "malvinas", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇴", "name": "faroe_islands", "keywords": ["faroe", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇯", "name": "fiji", "keywords": ["fj", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇮", "name": "finland", "keywords": ["fi", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇷", "name": "fr", "keywords": ["banner", "flag", "nation", "france", "french", "country"] }, - { "category": "flags", "char": "🇬🇫", "name": "french_guiana", "keywords": ["french", "guiana", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇫", "name": "french_polynesia", "keywords": ["french", "polynesia", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇫", "name": "french_southern_territories", "keywords": ["french", "southern", "territories", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇦", "name": "gabon", "keywords": ["ga", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇲", "name": "gambia", "keywords": ["gm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇪", "name": "georgia", "keywords": ["ge", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇩🇪", "name": "de", "keywords": ["german", "nation", "flag", "country", "banner"] }, - { "category": "flags", "char": "🇬🇭", "name": "ghana", "keywords": ["gh", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇮", "name": "gibraltar", "keywords": ["gi", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇷", "name": "greece", "keywords": ["gr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇱", "name": "greenland", "keywords": ["gl", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇩", "name": "grenada", "keywords": ["gd", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇵", "name": "guadeloupe", "keywords": ["gp", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇺", "name": "guam", "keywords": ["gu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇹", "name": "guatemala", "keywords": ["gt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇬", "name": "guernsey", "keywords": ["gg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇳", "name": "guinea", "keywords": ["gn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇼", "name": "guinea_bissau", "keywords": ["gw", "bissau", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇾", "name": "guyana", "keywords": ["gy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇹", "name": "haiti", "keywords": ["ht", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇳", "name": "honduras", "keywords": ["hn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇰", "name": "hong_kong", "keywords": ["hong", "kong", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇭🇺", "name": "hungary", "keywords": ["hu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇸", "name": "iceland", "keywords": ["is", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇳", "name": "india", "keywords": ["in", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇩", "name": "indonesia", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇷", "name": "iran", "keywords": ["iran, ", "islamic", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇶", "name": "iraq", "keywords": ["iq", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇪", "name": "ireland", "keywords": ["ie", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇲", "name": "isle_of_man", "keywords": ["isle", "man", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇱", "name": "israel", "keywords": ["il", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇮🇹", "name": "it", "keywords": ["italy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇮", "name": "cote_divoire", "keywords": ["ivory", "coast", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇯🇲", "name": "jamaica", "keywords": ["jm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇯🇵", "name": "jp", "keywords": ["japanese", "nation", "flag", "country", "banner"] }, - { "category": "flags", "char": "🇯🇪", "name": "jersey", "keywords": ["je", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇯🇴", "name": "jordan", "keywords": ["jo", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇿", "name": "kazakhstan", "keywords": ["kz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇪", "name": "kenya", "keywords": ["ke", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇮", "name": "kiribati", "keywords": ["ki", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇽🇰", "name": "kosovo", "keywords": ["xk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇼", "name": "kuwait", "keywords": ["kw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇬", "name": "kyrgyzstan", "keywords": ["kg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇦", "name": "laos", "keywords": ["lao", "democratic", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇻", "name": "latvia", "keywords": ["lv", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇧", "name": "lebanon", "keywords": ["lb", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇸", "name": "lesotho", "keywords": ["ls", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇷", "name": "liberia", "keywords": ["lr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇾", "name": "libya", "keywords": ["ly", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇮", "name": "liechtenstein", "keywords": ["li", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇹", "name": "lithuania", "keywords": ["lt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇺", "name": "luxembourg", "keywords": ["lu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇴", "name": "macau", "keywords": ["macao", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇰", "name": "macedonia", "keywords": ["macedonia, ", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇬", "name": "madagascar", "keywords": ["mg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇼", "name": "malawi", "keywords": ["mw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇾", "name": "malaysia", "keywords": ["my", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇻", "name": "maldives", "keywords": ["mv", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇱", "name": "mali", "keywords": ["ml", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇹", "name": "malta", "keywords": ["mt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇭", "name": "marshall_islands", "keywords": ["marshall", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇶", "name": "martinique", "keywords": ["mq", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇷", "name": "mauritania", "keywords": ["mr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇺", "name": "mauritius", "keywords": ["mu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇾🇹", "name": "mayotte", "keywords": ["yt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇽", "name": "mexico", "keywords": ["mx", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇫🇲", "name": "micronesia", "keywords": ["micronesia, ", "federated", "states", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇩", "name": "moldova", "keywords": ["moldova, ", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇨", "name": "monaco", "keywords": ["mc", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇳", "name": "mongolia", "keywords": ["mn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇪", "name": "montenegro", "keywords": ["me", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇸", "name": "montserrat", "keywords": ["ms", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇦", "name": "morocco", "keywords": ["ma", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇿", "name": "mozambique", "keywords": ["mz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇲", "name": "myanmar", "keywords": ["mm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇦", "name": "namibia", "keywords": ["na", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇷", "name": "nauru", "keywords": ["nr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇵", "name": "nepal", "keywords": ["np", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇱", "name": "netherlands", "keywords": ["nl", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇨", "name": "new_caledonia", "keywords": ["new", "caledonia", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇿", "name": "new_zealand", "keywords": ["new", "zealand", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇮", "name": "nicaragua", "keywords": ["ni", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇪", "name": "niger", "keywords": ["ne", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇬", "name": "nigeria", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇺", "name": "niue", "keywords": ["nu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇳🇫", "name": "norfolk_island", "keywords": ["norfolk", "island", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇲🇵", "name": "northern_mariana_islands", "keywords": ["northern", "mariana", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇵", "name": "north_korea", "keywords": ["north", "korea", "nation", "flag", "country", "banner"] }, - { "category": "flags", "char": "🇳🇴", "name": "norway", "keywords": ["no", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇴🇲", "name": "oman", "keywords": ["om_symbol", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇰", "name": "pakistan", "keywords": ["pk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇼", "name": "palau", "keywords": ["pw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇸", "name": "palestinian_territories", "keywords": ["palestine", "palestinian", "territories", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇦", "name": "panama", "keywords": ["pa", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇬", "name": "papua_new_guinea", "keywords": ["papua", "new", "guinea", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇾", "name": "paraguay", "keywords": ["py", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇪", "name": "peru", "keywords": ["pe", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇭", "name": "philippines", "keywords": ["ph", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇳", "name": "pitcairn_islands", "keywords": ["pitcairn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇱", "name": "poland", "keywords": ["pl", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇹", "name": "portugal", "keywords": ["pt", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇷", "name": "puerto_rico", "keywords": ["puerto", "rico", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇶🇦", "name": "qatar", "keywords": ["qa", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇪", "name": "reunion", "keywords": ["réunion", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇴", "name": "romania", "keywords": ["ro", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇺", "name": "ru", "keywords": ["russian", "federation", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇼", "name": "rwanda", "keywords": ["rw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇧🇱", "name": "st_barthelemy", "keywords": ["saint", "barthélemy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇭", "name": "st_helena", "keywords": ["saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇳", "name": "st_kitts_nevis", "keywords": ["saint", "kitts", "nevis", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇨", "name": "st_lucia", "keywords": ["saint", "lucia", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇵🇲", "name": "st_pierre_miquelon", "keywords": ["saint", "pierre", "miquelon", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇨", "name": "st_vincent_grenadines", "keywords": ["saint", "vincent", "grenadines", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇼🇸", "name": "samoa", "keywords": ["ws", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇲", "name": "san_marino", "keywords": ["san", "marino", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇹", "name": "sao_tome_principe", "keywords": ["sao", "tome", "principe", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇦", "name": "saudi_arabia", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇳", "name": "senegal", "keywords": ["sn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇷🇸", "name": "serbia", "keywords": ["rs", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇨", "name": "seychelles", "keywords": ["sc", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇱", "name": "sierra_leone", "keywords": ["sierra", "leone", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇬", "name": "singapore", "keywords": ["sg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇽", "name": "sint_maarten", "keywords": ["sint", "maarten", "dutch", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇰", "name": "slovakia", "keywords": ["sk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇮", "name": "slovenia", "keywords": ["si", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇧", "name": "solomon_islands", "keywords": ["solomon", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇴", "name": "somalia", "keywords": ["so", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇿🇦", "name": "south_africa", "keywords": ["south", "africa", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇸", "name": "south_georgia_south_sandwich_islands", "keywords": ["south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇰🇷", "name": "kr", "keywords": ["south", "korea", "nation", "flag", "country", "banner"] }, - { "category": "flags", "char": "🇸🇸", "name": "south_sudan", "keywords": ["south", "sd", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇸", "name": "es", "keywords": ["spain", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇱🇰", "name": "sri_lanka", "keywords": ["sri", "lanka", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇩", "name": "sudan", "keywords": ["sd", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇷", "name": "suriname", "keywords": ["sr", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇿", "name": "swaziland", "keywords": ["sz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇪", "name": "sweden", "keywords": ["se", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇨🇭", "name": "switzerland", "keywords": ["ch", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇸🇾", "name": "syria", "keywords": ["syrian", "arab", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇼", "name": "taiwan", "keywords": ["tw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇯", "name": "tajikistan", "keywords": ["tj", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇿", "name": "tanzania", "keywords": ["tanzania, ", "united", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇭", "name": "thailand", "keywords": ["th", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇱", "name": "timor_leste", "keywords": ["timor", "leste", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇬", "name": "togo", "keywords": ["tg", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇰", "name": "tokelau", "keywords": ["tk", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇴", "name": "tonga", "keywords": ["to", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇹", "name": "trinidad_tobago", "keywords": ["trinidad", "tobago", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇦", "name": "tristan_da_cunha", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇳", "name": "tunisia", "keywords": ["tn", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇷", "name": "tr", "keywords": ["turkey", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇲", "name": "turkmenistan", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇨", "name": "turks_caicos_islands", "keywords": ["turks", "caicos", "islands", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇹🇻", "name": "tuvalu", "keywords": ["flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇬", "name": "uganda", "keywords": ["ug", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇦", "name": "ukraine", "keywords": ["ua", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇦🇪", "name": "united_arab_emirates", "keywords": ["united", "arab", "emirates", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇬🇧", "name": "uk", "keywords": ["united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "UK", "english", "england", "union jack"] }, - { "category": "flags", "char": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", "name": "england", "keywords": ["flag", "english"] }, - { "category": "flags", "char": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "name": "scotland", "keywords": ["flag", "scottish"] }, - { "category": "flags", "char": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", "name": "wales", "keywords": ["flag", "welsh"] }, - { "category": "flags", "char": "🇺🇸", "name": "us", "keywords": ["united", "states", "america", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇮", "name": "us_virgin_islands", "keywords": ["virgin", "islands", "us", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇾", "name": "uruguay", "keywords": ["uy", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇿", "name": "uzbekistan", "keywords": ["uz", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇺", "name": "vanuatu", "keywords": ["vu", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇦", "name": "vatican_city", "keywords": ["vatican", "city", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇪", "name": "venezuela", "keywords": ["ve", "bolivarian", "republic", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇻🇳", "name": "vietnam", "keywords": ["viet", "nam", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇼🇫", "name": "wallis_futuna", "keywords": ["wallis", "futuna", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇪🇭", "name": "western_sahara", "keywords": ["western", "sahara", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇾🇪", "name": "yemen", "keywords": ["ye", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇿🇲", "name": "zambia", "keywords": ["zm", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇿🇼", "name": "zimbabwe", "keywords": ["zw", "flag", "nation", "country", "banner"] }, - { "category": "flags", "char": "🇺🇳", "name": "united_nations", "keywords": ["un", "flag", "banner"] }, - { "category": "flags", "char": "🏴‍☠️", "name": "pirate_flag", "keywords": ["skull", "crossbones", "flag", "banner"] } -] - diff --git a/packages/client/src/events.ts b/packages/client/src/events.ts deleted file mode 100644 index dbbd908b8f..0000000000 --- a/packages/client/src/events.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { EventEmitter } from 'eventemitter3'; - -// TODO: 型付け -export const globalEvents = new EventEmitter(); diff --git a/packages/client/src/filters/bytes.ts b/packages/client/src/filters/bytes.ts deleted file mode 100644 index c80f2f0ed2..0000000000 --- a/packages/client/src/filters/bytes.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default (v, digits = 0) => { - if (v == null) return '?'; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - if (v === 0) return '0'; - const isMinus = v < 0; - if (isMinus) v = -v; - const i = Math.floor(Math.log(v) / Math.log(1024)); - return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; -}; diff --git a/packages/client/src/filters/note.ts b/packages/client/src/filters/note.ts deleted file mode 100644 index cd9b7d98d2..0000000000 --- a/packages/client/src/filters/note.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const notePage = note => { - return `/notes/${note.id}`; -}; diff --git a/packages/client/src/filters/number.ts b/packages/client/src/filters/number.ts deleted file mode 100644 index 880a848ca4..0000000000 --- a/packages/client/src/filters/number.ts +++ /dev/null @@ -1 +0,0 @@ -export default n => n == null ? 'N/A' : n.toLocaleString(); diff --git a/packages/client/src/filters/user.ts b/packages/client/src/filters/user.ts deleted file mode 100644 index ff2f7e2dae..0000000000 --- a/packages/client/src/filters/user.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as misskey from 'misskey-js'; -import * as Acct from 'misskey-js/built/acct'; -import { url } from '@/config'; - -export const acct = (user: misskey.Acct) => { - return Acct.toString(user); -}; - -export const userName = (user: misskey.entities.User) => { - return user.name || user.username; -}; - -export const userPage = (user: misskey.Acct, path?, absolute = false) => { - return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; -}; diff --git a/packages/client/src/i18n.ts b/packages/client/src/i18n.ts deleted file mode 100644 index 31e066960d..0000000000 --- a/packages/client/src/i18n.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { markRaw } from 'vue'; -import { locale } from '@/config'; -import { I18n } from '@/scripts/i18n'; - -export const i18n = markRaw(new I18n(locale)); diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts deleted file mode 100644 index 508d3262b3..0000000000 --- a/packages/client/src/init.ts +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Client entry point - */ -// https://vitejs.dev/config/build-options.html#build-modulepreload -import 'vite/modulepreload-polyfill'; - -import '@/style.scss'; - -//#region account indexedDB migration -import { set } from '@/scripts/idb-proxy'; - -if (localStorage.getItem('accounts') != null) { - set('accounts', JSON.parse(localStorage.getItem('accounts'))); - localStorage.removeItem('accounts'); -} -//#endregion - -import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; -import { compareVersions } from 'compare-versions'; -import JSON5 from 'json5'; - -import widgets from '@/widgets'; -import directives from '@/directives'; -import components from '@/components'; -import { version, ui, lang, host } from '@/config'; -import { applyTheme } from '@/scripts/theme'; -import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; -import { i18n } from '@/i18n'; -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'; -import { fetchInstance, instance } from '@/instance'; -import { makeHotkey } from '@/scripts/hotkey'; -import { search } from '@/scripts/search'; -import { deviceKind } from '@/scripts/device-kind'; -import { initializeSw } from '@/scripts/initialize-sw'; -import { reloadChannel } from '@/scripts/unison-reload'; -import { reactionPicker } from '@/scripts/reaction-picker'; -import { getUrlWithoutLoginId } from '@/scripts/login-id'; -import { getAccountFromId } from '@/scripts/get-account-from-id'; - -(async () => { - console.info(`Misskey v${version}`); - - if (_DEV_) { - console.warn('Development mode!!!'); - - console.info(`vue ${vueVersion}`); - - (window as any).$i = $i; - (window as any).$store = defaultStore; - - window.addEventListener('error', event => { - console.error(event); - /* - alert({ - type: 'error', - title: 'DEV: Unhandled error', - text: event.message - }); - */ - }); - - window.addEventListener('unhandledrejection', event => { - console.error(event); - /* - alert({ - type: 'error', - title: 'DEV: Unhandled promise rejection', - text: event.reason - }); - */ - }); - } - - // タッチデバイスでCSSの:hoverを機能させる - document.addEventListener('touchend', () => {}, { passive: true }); - - // 一斉リロード - reloadChannel.addEventListener('message', path => { - if (path !== null) location.href = path; - else location.reload(); - }); - - // If mobile, insert the viewport meta tag - if (['smartphone', 'tablet'].includes(deviceKind)) { - const viewport = document.getElementsByName('viewport').item(0); - viewport.setAttribute('content', - `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); - } - - //#region Set lang attr - const html = document.documentElement; - html.setAttribute('lang', lang); - //#endregion - - //#region loginId - const params = new URLSearchParams(location.search); - const loginId = params.get('loginId'); - - if (loginId) { - const target = getUrlWithoutLoginId(location.href); - - if (!$i || $i.id !== loginId) { - const account = await getAccountFromId(loginId); - if (account) { - await login(account.token, target); - } - } - - history.replaceState({ misskey: 'loginId' }, '', target); - } - - //#endregion - - //#region Fetch user - if ($i && $i.token) { - if (_DEV_) { - console.log('account cache found. refreshing...'); - } - - refreshAccount(); - } else { - if (_DEV_) { - console.log('no account cache found.'); - } - - // 連携ログインの場合用にCookieを参照する - const i = (document.cookie.match(/igi=(\w+)/) ?? [null, null])[1]; - - if (i != null && i !== 'null') { - if (_DEV_) { - console.log('signing...'); - } - - try { - document.body.innerHTML = '
Please wait...
'; - await login(i); - } catch (err) { - // Render the error screen - // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) - document.body.innerHTML = '
Oops!
'; - } - } else { - if (_DEV_) { - console.log('not signed in'); - } - } - } - //#endregion - - const fetchInstanceMetaPromise = fetchInstance(); - - fetchInstanceMetaPromise.then(() => { - localStorage.setItem('v', instance.version); - - // Init service worker - initializeSw(); - }); - - const app = createApp( - window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : - !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : - ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : - ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : - defineAsyncComponent(() => import('@/ui/universal.vue')), - ); - - if (_DEV_) { - app.config.performance = true; - } - - app.config.globalProperties = { - $i, - $store: defaultStore, - $instance: instance, - $t: i18n.t, - $ts: i18n.ts, - }; - - widgets(app); - directives(app); - components(app); - - const splash = document.getElementById('splash'); - // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) - if (splash) splash.addEventListener('transitionend', () => { - splash.remove(); - }); - - // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 - // なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する - const rootEl = (() => { - const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; - - const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID); - - if (currentEl) { - console.warn('multiple import detected'); - return currentEl; - } - - const rootEl = document.createElement('div'); - rootEl.id = MISSKEY_MOUNT_DIV_ID; - document.body.appendChild(rootEl); - return rootEl; - })(); - - app.mount(rootEl); - - // boot.jsのやつを解除 - window.onerror = null; - window.onunhandledrejection = null; - - reactionPicker.init(); - - if (splash) { - splash.style.opacity = '0'; - splash.style.pointerEvents = 'none'; - } - - // クライアントが更新されたか? - const lastVersion = localStorage.getItem('lastVersion'); - if (lastVersion !== version) { - localStorage.setItem('lastVersion', version); - - // テーマリビルドするため - localStorage.removeItem('theme'); - - try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため - if (lastVersion != null && compareVersions(version, lastVersion) === 1) { - // ログインしてる場合だけ - if ($i) { - popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); - } - } - } catch (err) { - } - } - - // NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため) - watch(defaultStore.reactiveState.darkMode, (darkMode) => { - applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); - }, { immediate: localStorage.theme == null }); - - const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); - const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); - - watch(darkTheme, (theme) => { - if (defaultStore.state.darkMode) { - applyTheme(theme); - } - }); - - watch(lightTheme, (theme) => { - if (!defaultStore.state.darkMode) { - applyTheme(theme); - } - }); - - //#region Sync dark mode - if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', isDeviceDarkmode()); - } - - window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', mql.matches); - } - }); - //#endregion - - fetchInstanceMetaPromise.then(() => { - if (defaultStore.state.themeInitial) { - if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); - if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme)); - defaultStore.set('themeInitial', false); - } - }); - - watch(defaultStore.reactiveState.useBlurEffectForModal, v => { - document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); - }, { immediate: true }); - - watch(defaultStore.reactiveState.useBlurEffect, v => { - if (v) { - document.documentElement.style.removeProperty('--blur'); - } else { - document.documentElement.style.setProperty('--blur', 'none'); - } - }, { immediate: true }); - - let reloadDialogShowing = false; - stream.on('_disconnected_', async () => { - if (defaultStore.state.serverDisconnectedBehavior === 'reload') { - location.reload(); - } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { - if (reloadDialogShowing) return; - reloadDialogShowing = true; - const { canceled } = await confirm({ - type: 'warning', - title: i18n.ts.disconnectedFromServer, - text: i18n.ts.reloadConfirm, - }); - reloadDialogShowing = false; - if (!canceled) { - location.reload(); - } - } - }); - - stream.on('emojiAdded', emojiData => { - // TODO - //store.commit('instance/set', ); - }); - - for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { - import('./plugin').then(({ install }) => { - install(plugin); - }); - } - - const hotkeys = { - 'd': (): void => { - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': search, - }; - - if ($i) { - // only add post shortcuts if logged in - hotkeys['p|n'] = post; - - if ($i.isDeleted) { - alert({ - type: 'warning', - text: i18n.ts.accountDeletionInProgress, - }); - } - - const lastUsed = localStorage.getItem('lastUsed'); - if (lastUsed) { - const lastUsedDate = parseInt(lastUsed, 10); - // 二時間以上前なら - if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { - toast(i18n.t('welcomeBackWithName', { - name: $i.name || $i.username, - })); - } - } - localStorage.setItem('lastUsed', Date.now().toString()); - - if ('Notification' in window) { - // 許可を得ていなかったらリクエスト - if (Notification.permission === 'default') { - Notification.requestPermission(); - } - } - - const main = markRaw(stream.useChannel('main', null, 'System')); - - // 自分の情報が更新されたとき - main.on('meUpdated', i => { - updateAccount(i); - }); - - main.on('readAllNotifications', () => { - updateAccount({ hasUnreadNotification: false }); - }); - - main.on('unreadNotification', () => { - updateAccount({ hasUnreadNotification: true }); - }); - - main.on('unreadMention', () => { - updateAccount({ hasUnreadMentions: true }); - }); - - main.on('readAllUnreadMentions', () => { - updateAccount({ hasUnreadMentions: false }); - }); - - main.on('unreadSpecifiedNote', () => { - updateAccount({ hasUnreadSpecifiedNotes: true }); - }); - - main.on('readAllUnreadSpecifiedNotes', () => { - updateAccount({ hasUnreadSpecifiedNotes: false }); - }); - - main.on('readAllMessagingMessages', () => { - updateAccount({ hasUnreadMessagingMessage: false }); - }); - - main.on('unreadMessagingMessage', () => { - updateAccount({ hasUnreadMessagingMessage: true }); - sound.play('chatBg'); - }); - - main.on('readAllAntennas', () => { - updateAccount({ hasUnreadAntenna: false }); - }); - - main.on('unreadAntenna', () => { - updateAccount({ hasUnreadAntenna: true }); - sound.play('antenna'); - }); - - main.on('readAllAnnouncements', () => { - updateAccount({ hasUnreadAnnouncement: false }); - }); - - main.on('readAllChannels', () => { - updateAccount({ hasUnreadChannel: false }); - }); - - main.on('unreadChannel', () => { - updateAccount({ hasUnreadChannel: true }); - sound.play('channel'); - }); - - // トークンが再生成されたとき - // このままではMisskeyが利用できないので強制的にサインアウトさせる - main.on('myTokenRegenerated', () => { - signout(); - }); - } - - // shortcut - document.addEventListener('keydown', makeHotkey(hotkeys)); -})(); diff --git a/packages/client/src/instance.ts b/packages/client/src/instance.ts deleted file mode 100644 index 51464f32fb..0000000000 --- a/packages/client/src/instance.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { computed, reactive } from 'vue'; -import * as Misskey from 'misskey-js'; -import { api } from './os'; - -// TODO: 他のタブと永続化されたstateを同期 - -const instanceData = localStorage.getItem('instance'); - -// TODO: instanceをリアクティブにするかは再考の余地あり - -export const instance: Misskey.entities.InstanceMetadata = reactive(instanceData ? JSON.parse(instanceData) : { - // TODO: set default values -}); - -export async function fetchInstance() { - const meta = await api('meta', { - detail: false, - }); - - for (const [k, v] of Object.entries(meta)) { - instance[k] = v; - } - - localStorage.setItem('instance', JSON.stringify(instance)); -} - -export const emojiCategories = computed(() => { - if (instance.emojis == null) return []; - const categories = new Set(); - for (const emoji of instance.emojis) { - categories.add(emoji.category); - } - return Array.from(categories); -}); - -export const emojiTags = computed(() => { - if (instance.emojis == null) return []; - const tags = new Set(); - for (const emoji of instance.emojis) { - for (const tag of emoji.aliases) { - tags.add(tag); - } - } - return Array.from(tags); -}); diff --git a/packages/client/src/navbar.ts b/packages/client/src/navbar.ts deleted file mode 100644 index 31e6cd64a4..0000000000 --- a/packages/client/src/navbar.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { computed, ref, reactive } from 'vue'; -import { $i } from './account'; -import { search } from '@/scripts/search'; -import * as os from '@/os'; -import { i18n } from '@/i18n'; -import { ui } from '@/config'; -import { unisonReload } from '@/scripts/unison-reload'; - -export const navbarItemDef = reactive({ - notifications: { - title: 'notifications', - icon: 'ti ti-bell', - show: computed(() => $i != null), - indicated: computed(() => $i != null && $i.hasUnreadNotification), - to: '/my/notifications', - }, - messaging: { - title: 'messaging', - icon: 'ti ti-messages', - show: computed(() => $i != null), - indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage), - to: '/my/messaging', - }, - drive: { - title: 'drive', - icon: 'ti ti-cloud', - show: computed(() => $i != null), - to: '/my/drive', - }, - followRequests: { - title: 'followRequests', - icon: 'ti ti-user-plus', - show: computed(() => $i != null && $i.isLocked), - indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest), - to: '/my/follow-requests', - }, - explore: { - title: 'explore', - icon: 'ti ti-hash', - to: '/explore', - }, - announcements: { - title: 'announcements', - icon: 'ti ti-speakerphone', - indicated: computed(() => $i != null && $i.hasUnreadAnnouncement), - to: '/announcements', - }, - search: { - title: 'search', - icon: 'ti ti-search', - action: () => search(), - }, - lists: { - title: 'lists', - icon: 'ti ti-list', - show: computed(() => $i != null), - to: '/my/lists', - }, - /* - groups: { - title: 'groups', - icon: 'ti ti-users', - show: computed(() => $i != null), - to: '/my/groups', - }, - */ - antennas: { - title: 'antennas', - icon: 'ti ti-antenna', - show: computed(() => $i != null), - to: '/my/antennas', - }, - favorites: { - title: 'favorites', - icon: 'ti ti-star', - show: computed(() => $i != null), - to: '/my/favorites', - }, - pages: { - title: 'pages', - icon: 'ti ti-news', - to: '/pages', - }, - gallery: { - title: 'gallery', - icon: 'ti ti-icons', - to: '/gallery', - }, - clips: { - title: 'clip', - icon: 'ti ti-paperclip', - show: computed(() => $i != null), - to: '/my/clips', - }, - channels: { - title: 'channel', - icon: 'ti ti-device-tv', - to: '/channels', - }, - ui: { - title: 'switchUi', - icon: 'ti ti-devices', - action: (ev) => { - os.popupMenu([{ - text: i18n.ts.default, - active: ui === 'default' || ui === null, - action: () => { - localStorage.setItem('ui', 'default'); - unisonReload(); - }, - }, { - text: i18n.ts.deck, - active: ui === 'deck', - action: () => { - localStorage.setItem('ui', 'deck'); - unisonReload(); - }, - }, { - text: i18n.ts.classic, - active: ui === 'classic', - action: () => { - localStorage.setItem('ui', 'classic'); - unisonReload(); - }, - }], ev.currentTarget ?? ev.target); - }, - }, - reload: { - title: 'reload', - icon: 'ti ti-refresh', - action: (ev) => { - location.reload(); - }, - }, -}); diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts deleted file mode 100644 index 53e73a8d48..0000000000 --- a/packages/client/src/nirax.ts +++ /dev/null @@ -1,275 +0,0 @@ -// NIRAX --- A lightweight router - -import { EventEmitter } from 'eventemitter3'; -import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue'; -import { pleaseLogin } from '@/scripts/please-login'; -import { safeURIDecode } from '@/scripts/safe-uri-decode'; - -type RouteDef = { - path: string; - component: Component; - query?: Record; - loginRequired?: boolean; - name?: string; - hash?: string; - globalCacheKey?: string; - children?: RouteDef[]; -}; - -type ParsedPath = (string | { - name: string; - startsWith?: string; - wildcard?: boolean; - optional?: boolean; -})[]; - -export type Resolved = { route: RouteDef; props: Map; child?: Resolved; }; - -function parsePath(path: string): ParsedPath { - const res = [] as ParsedPath; - - path = path.substring(1); - - for (const part of path.split('/')) { - if (part.includes(':')) { - const prefix = part.substring(0, part.indexOf(':')); - const placeholder = part.substring(part.indexOf(':') + 1); - const wildcard = placeholder.includes('(*)'); - const optional = placeholder.endsWith('?'); - res.push({ - name: placeholder.replace('(*)', '').replace('?', ''), - startsWith: prefix !== '' ? prefix : undefined, - wildcard, - optional, - }); - } else if (part.length !== 0) { - res.push(part); - } - } - - return res; -} - -export class Router extends EventEmitter<{ - change: (ctx: { - beforePath: string; - path: string; - resolved: Resolved; - key: string; - }) => void; - replace: (ctx: { - path: string; - key: string; - }) => void; - push: (ctx: { - beforePath: string; - path: string; - route: RouteDef | null; - props: Map | null; - key: string; - }) => void; - same: () => void; -}> { - private routes: RouteDef[]; - public current: Resolved; - public currentRef: ShallowRef = shallowRef(); - public currentRoute: ShallowRef = shallowRef(); - private currentPath: string; - private currentKey = Date.now().toString(); - - public navHook: ((path: string, flag?: any) => boolean) | null = null; - - constructor(routes: Router['routes'], currentPath: Router['currentPath']) { - super(); - - this.routes = routes; - this.currentPath = currentPath; - this.navigate(currentPath, null, false); - } - - public resolve(path: string): Resolved | null { - let queryString: string | null = null; - let hash: string | null = null; - if (path[0] === '/') path = path.substring(1); - if (path.includes('#')) { - hash = path.substring(path.indexOf('#') + 1); - path = path.substring(0, path.indexOf('#')); - } - if (path.includes('?')) { - queryString = path.substring(path.indexOf('?') + 1); - path = path.substring(0, path.indexOf('?')); - } - - if (_DEV_) console.log('Routing: ', path, queryString); - - function check(routes: RouteDef[], _parts: string[]): Resolved | null { - forEachRouteLoop: - for (const route of routes) { - let parts = [..._parts]; - const props = new Map(); - - pathMatchLoop: - for (const p of parsePath(route.path)) { - if (typeof p === 'string') { - if (p === parts[0]) { - parts.shift(); - } else { - continue forEachRouteLoop; - } - } else { - if (parts[0] == null && !p.optional) { - continue forEachRouteLoop; - } - if (p.wildcard) { - if (parts.length !== 0) { - props.set(p.name, safeURIDecode(parts.join('/'))); - parts = []; - } - break pathMatchLoop; - } else { - if (p.startsWith) { - if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop; - - props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length))); - parts.shift(); - } else { - if (parts[0]) { - props.set(p.name, safeURIDecode(parts[0])); - } - parts.shift(); - } - } - } - } - - if (parts.length === 0) { - if (route.children) { - const child = check(route.children, []); - if (child) { - return { - route, - props, - child, - }; - } else { - continue forEachRouteLoop; - } - } - - if (route.hash != null && hash != null) { - props.set(route.hash, safeURIDecode(hash)); - } - - if (route.query != null && queryString != null) { - const queryObject = [...new URLSearchParams(queryString).entries()] - .reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); - - for (const q in route.query) { - const as = route.query[q]; - if (queryObject[q]) { - props.set(as, safeURIDecode(queryObject[q])); - } - } - } - - return { - route, - props, - }; - } else { - if (route.children) { - const child = check(route.children, parts); - if (child) { - return { - route, - props, - child, - }; - } else { - continue forEachRouteLoop; - } - } else { - continue forEachRouteLoop; - } - } - } - - return null; - } - - const _parts = path.split('/').filter(part => part.length !== 0); - - return check(this.routes, _parts); - } - - private navigate(path: string, key: string | null | undefined, emitChange = true) { - const beforePath = this.currentPath; - this.currentPath = path; - - const res = this.resolve(this.currentPath); - - if (res == null) { - throw new Error('no route found for: ' + path); - } - - if (res.route.loginRequired) { - pleaseLogin('/'); - } - - const isSamePath = beforePath === path; - if (isSamePath && key == null) key = this.currentKey; - this.current = res; - this.currentRef.value = res; - this.currentRoute.value = res.route; - this.currentKey = res.route.globalCacheKey ?? key ?? path; - - if (emitChange) { - this.emit('change', { - beforePath, - path, - resolved: res, - key: this.currentKey, - }); - } - - return res; - } - - public getCurrentPath() { - return this.currentPath; - } - - public getCurrentKey() { - return this.currentKey; - } - - public push(path: string, flag?: any) { - const beforePath = this.currentPath; - if (path === beforePath) { - this.emit('same'); - return; - } - if (this.navHook) { - const cancel = this.navHook(path, flag); - if (cancel) return; - } - const res = this.navigate(path, null); - this.emit('push', { - beforePath, - path, - route: res.route, - props: res.props, - key: this.currentKey, - }); - } - - public replace(path: string, key?: string | null, emitEvent = true) { - this.navigate(path, key); - if (emitEvent) { - this.emit('replace', { - path, - key: this.currentKey, - }); - } - } -} diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts deleted file mode 100644 index 7e57dcb4af..0000000000 --- a/packages/client/src/os.ts +++ /dev/null @@ -1,588 +0,0 @@ -// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する - -import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue'; -import { EventEmitter } from 'eventemitter3'; -import insertTextAtCursor from 'insert-text-at-cursor'; -import * as Misskey from 'misskey-js'; -import { apiUrl, url } from '@/config'; -import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; -import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; -import { MenuItem } from '@/types/menu'; -import { $i } from '@/account'; - -export const pendingApiRequestsCount = ref(0); - -const apiClient = new Misskey.api.APIClient({ - origin: url, -}); - -export const api = ((endpoint: string, data: Record = {}, token?: string | null | undefined) => { - pendingApiRequestsCount.value++; - - const onFinally = () => { - pendingApiRequestsCount.value--; - }; - - const promise = new Promise((resolve, reject) => { - // Append a credential - if ($i) (data as any).i = $i.token; - if (token !== undefined) (data as any).i = token; - - // Send request - window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; -}) as typeof apiClient.request; - -export const apiGet = ((endpoint: string, data: Record = {}) => { - pendingApiRequestsCount.value++; - - const onFinally = () => { - pendingApiRequestsCount.value--; - }; - - const query = new URLSearchParams(data); - - const promise = new Promise((resolve, reject) => { - // Send request - window.fetch(`${apiUrl}/${endpoint}?${query}`, { - method: 'GET', - credentials: 'omit', - cache: 'default', - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; -}) as typeof apiClient.request; - -export const apiWithDialog = (( - endpoint: string, - data: Record = {}, - token?: string | null | undefined, -) => { - const promise = api(endpoint, data, token); - promiseDialog(promise, null, (err) => { - alert({ - type: 'error', - text: err.message + '\n' + (err as any).id, - }); - }); - - return promise; -}) as typeof api; - -export function promiseDialog>( - promise: T, - onSuccess?: ((res: any) => void) | null, - onFailure?: ((err: Error) => void) | null, - text?: string, -): T { - const showing = ref(true); - const success = ref(false); - - promise.then(res => { - if (onSuccess) { - showing.value = false; - onSuccess(res); - } else { - success.value = true; - window.setTimeout(() => { - showing.value = false; - }, 1000); - } - }).catch(err => { - showing.value = false; - if (onFailure) { - onFailure(err); - } else { - alert({ - type: 'error', - text: err, - }); - } - }); - - // NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない) - popup(MkWaitingDialog, { - success: success, - showing: showing, - text: text, - }, {}, 'closed'); - - return promise; -} - -let popupIdCount = 0; -export const popups = ref([]) as Ref<{ - id: any; - component: any; - props: Record; -}[]>; - -const zIndexes = { - low: 1000000, - middle: 2000000, - high: 3000000, -}; -export function claimZIndex(priority: 'low' | 'middle' | 'high' = 'low'): number { - zIndexes[priority] += 100; - return zIndexes[priority]; -} - -export async function popup(component: Component, props: Record, events = {}, disposeEvent?: string) { - markRaw(component); - - const id = ++popupIdCount; - const dispose = () => { - // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ? - window.setTimeout(() => { - popups.value = popups.value.filter(popup => popup.id !== id); - }, 0); - }; - const state = { - component, - props, - events: disposeEvent ? { - ...events, - [disposeEvent]: dispose, - } : events, - id, - }; - - popups.value.push(state); - - return { - dispose, - }; -} - -export function pageWindow(path: string) { - popup(defineAsyncComponent(() => import('@/components/MkPageWindow.vue')), { - initialPath: path, - }, {}, 'closed'); -} - -export function modalPageWindow(path: string) { - popup(defineAsyncComponent(() => import('@/components/MkModalPageWindow.vue')), { - initialPath: path, - }, {}, 'closed'); -} - -export function toast(message: string) { - popup(defineAsyncComponent(() => import('@/components/MkToast.vue')), { - message, - }, {}, 'closed'); -} - -export function alert(props: { - type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; - title?: string | null; - text?: string | null; -}): Promise { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), props, { - done: result => { - resolve(); - }, - }, 'closed'); - }); -} - -export function confirm(props: { - type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; - title?: string | null; - text?: string | null; -}): Promise<{ canceled: boolean }> { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { - ...props, - showCancelButton: true, - }, { - done: result => { - resolve(result ? result : { canceled: true }); - }, - }, 'closed'); - }); -} - -export function inputText(props: { - type?: 'text' | 'email' | 'password' | 'url'; - title?: string | null; - text?: string | null; - placeholder?: string | null; - default?: string | null; -}): Promise<{ canceled: true; result: undefined; } | { - canceled: false; result: string; -}> { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { - title: props.title, - text: props.text, - input: { - type: props.type, - placeholder: props.placeholder, - default: props.default, - }, - }, { - done: result => { - resolve(result ? result : { canceled: true }); - }, - }, 'closed'); - }); -} - -export function inputNumber(props: { - title?: string | null; - text?: string | null; - placeholder?: string | null; - default?: number | null; -}): Promise<{ canceled: true; result: undefined; } | { - canceled: false; result: number; -}> { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { - title: props.title, - text: props.text, - input: { - type: 'number', - placeholder: props.placeholder, - default: props.default, - }, - }, { - done: result => { - resolve(result ? result : { canceled: true }); - }, - }, 'closed'); - }); -} - -export function inputDate(props: { - title?: string | null; - text?: string | null; - placeholder?: string | null; - default?: Date | null; -}): Promise<{ canceled: true; result: undefined; } | { - canceled: false; result: Date; -}> { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { - title: props.title, - text: props.text, - input: { - type: 'date', - placeholder: props.placeholder, - default: props.default, - }, - }, { - done: result => { - resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true }); - }, - }, 'closed'); - }); -} - -export function select(props: { - title?: string | null; - text?: string | null; - default?: string | null; -} & ({ - items: { - value: C; - text: string; - }[]; -} | { - groupedItems: { - label: string; - items: { - value: C; - text: string; - }[]; - }[]; -})): Promise<{ canceled: true; result: undefined; } | { - canceled: false; result: C; -}> { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { - title: props.title, - text: props.text, - select: { - items: props.items, - groupedItems: props.groupedItems, - default: props.default, - }, - }, { - done: result => { - resolve(result ? result : { canceled: true }); - }, - }, 'closed'); - }); -} - -export function success() { - return new Promise((resolve, reject) => { - const showing = ref(true); - window.setTimeout(() => { - showing.value = false; - }, 1000); - popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { - success: true, - showing: showing, - }, { - done: () => resolve(), - }, 'closed'); - }); -} - -export function waiting() { - return new Promise((resolve, reject) => { - const showing = ref(true); - popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { - success: false, - showing: showing, - }, { - done: () => resolve(), - }, 'closed'); - }); -} - -export function form(title, form) { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form }, { - done: result => { - resolve(result); - }, - }, 'closed'); - }); -} - -export async function selectUser() { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {}, { - ok: user => { - resolve(user); - }, - }, 'closed'); - }); -} - -export async function selectDriveFile(multiple: boolean) { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { - type: 'file', - multiple, - }, { - done: files => { - if (files) { - resolve(multiple ? files : files[0]); - } - }, - }, 'closed'); - }); -} - -export async function selectDriveFolder(multiple: boolean) { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { - type: 'folder', - multiple, - }, { - done: folders => { - if (folders) { - resolve(multiple ? folders : folders[0]); - } - }, - }, 'closed'); - }); -} - -export async function pickEmoji(src: HTMLElement | null, opts) { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { - src, - ...opts, - }, { - done: emoji => { - resolve(emoji); - }, - }, 'closed'); - }); -} - -export async function cropImage(image: Misskey.entities.DriveFile, options: { - aspectRatio: number; -}): Promise { - return new Promise((resolve, reject) => { - popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { - file: image, - aspectRatio: options.aspectRatio, - }, { - ok: x => { - resolve(x); - }, - }, 'closed'); - }); -} - -type AwaitType = - T extends Promise ? U : - T extends (...args: any[]) => Promise ? V : - T; -let openingEmojiPicker: AwaitType> | null = null; -let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null; -export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) { - if (openingEmojiPicker) return; - - activeTextarea = initialTextarea; - - const textareas = document.querySelectorAll('textarea, input'); - for (const textarea of Array.from(textareas)) { - textarea.addEventListener('focus', () => { - activeTextarea = textarea; - }); - } - - const observer = new MutationObserver(records => { - for (const record of records) { - for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) { - const textareas = node.querySelectorAll('textarea, input') as NodeListOf>; - for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) { - if (document.activeElement === textarea) activeTextarea = textarea; - textarea.addEventListener('focus', () => { - activeTextarea = textarea; - }); - } - } - } - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: false, - characterData: false, - }); - - openingEmojiPicker = await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerWindow.vue')), { - src, - ...opts, - }, { - chosen: emoji => { - insertTextAtCursor(activeTextarea, emoji); - }, - closed: () => { - openingEmojiPicker!.dispose(); - openingEmojiPicker = null; - observer.disconnect(); - }, - }); -} - -export function popupMenu(items: MenuItem[] | Ref, src?: HTMLElement, options?: { - align?: string; - width?: number; - viaKeyboard?: boolean; -}) { - return new Promise((resolve, reject) => { - let dispose; - popup(defineAsyncComponent(() => import('@/components/MkPopupMenu.vue')), { - items, - src, - width: options?.width, - align: options?.align, - viaKeyboard: options?.viaKeyboard, - }, { - closed: () => { - resolve(); - dispose(); - }, - }).then(res => { - dispose = res.dispose; - }); - }); -} - -export function contextMenu(items: MenuItem[] | Ref, ev: MouseEvent) { - ev.preventDefault(); - return new Promise((resolve, reject) => { - let dispose; - popup(defineAsyncComponent(() => import('@/components/MkContextMenu.vue')), { - items, - ev, - }, { - closed: () => { - resolve(); - dispose(); - }, - }).then(res => { - dispose = res.dispose; - }); - }); -} - -export function post(props: Record = {}) { - return new Promise((resolve, reject) => { - // NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない - // NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、 - // Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、 - // 複数のpost formを開いたときに場合によってはエラーになる - // もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが - let dispose; - popup(MkPostFormDialog, props, { - closed: () => { - resolve(); - dispose(); - }, - }).then(res => { - dispose = res.dispose; - }); - }); -} - -export const deckGlobalEvents = new EventEmitter(); - -/* -export function checkExistence(fileData: ArrayBuffer): Promise { - return new Promise((resolve, reject) => { - const data = new FormData(); - data.append('md5', getMD5(fileData)); - - os.api('drive/files/find-by-hash', { - md5: getMD5(fileData) - }).then(resp => { - resolve(resp.length > 0 ? resp[0] : null); - }); - }); -}*/ diff --git a/packages/client/src/pages/_empty_.vue b/packages/client/src/pages/_empty_.vue deleted file mode 100644 index 000b6decc9..0000000000 --- a/packages/client/src/pages/_empty_.vue +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/packages/client/src/pages/_error_.vue b/packages/client/src/pages/_error_.vue deleted file mode 100644 index 232d525347..0000000000 --- a/packages/client/src/pages/_error_.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/_loading_.vue b/packages/client/src/pages/_loading_.vue deleted file mode 100644 index 1dd2e46e10..0000000000 --- a/packages/client/src/pages/_loading_.vue +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/packages/client/src/pages/about-misskey.vue b/packages/client/src/pages/about-misskey.vue deleted file mode 100644 index 3ec972bcda..0000000000 --- a/packages/client/src/pages/about-misskey.vue +++ /dev/null @@ -1,264 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/about.emojis.vue b/packages/client/src/pages/about.emojis.vue deleted file mode 100644 index 53ce1e4b75..0000000000 --- a/packages/client/src/pages/about.emojis.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/about.federation.vue b/packages/client/src/pages/about.federation.vue deleted file mode 100644 index 6c92ab1264..0000000000 --- a/packages/client/src/pages/about.federation.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/about.vue b/packages/client/src/pages/about.vue deleted file mode 100644 index 0ed692c5c5..0000000000 --- a/packages/client/src/pages/about.vue +++ /dev/null @@ -1,166 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin-file.vue b/packages/client/src/pages/admin-file.vue deleted file mode 100644 index a11249e75d..0000000000 --- a/packages/client/src/pages/admin-file.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue deleted file mode 100644 index bdb41b2d2c..0000000000 --- a/packages/client/src/pages/admin/_header_.vue +++ /dev/null @@ -1,292 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue deleted file mode 100644 index 973ec871ab..0000000000 --- a/packages/client/src/pages/admin/abuses.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue deleted file mode 100644 index 2ec926c65c..0000000000 --- a/packages/client/src/pages/admin/ads.vue +++ /dev/null @@ -1,132 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue deleted file mode 100644 index 607ad8aa02..0000000000 --- a/packages/client/src/pages/admin/announcements.vue +++ /dev/null @@ -1,112 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue deleted file mode 100644 index d03961cf95..0000000000 --- a/packages/client/src/pages/admin/bot-protection.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue deleted file mode 100644 index 5a0d3d5e51..0000000000 --- a/packages/client/src/pages/admin/database.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue deleted file mode 100644 index 6c9dee1704..0000000000 --- a/packages/client/src/pages/admin/email-settings.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/emoji-edit-dialog.vue b/packages/client/src/pages/admin/emoji-edit-dialog.vue deleted file mode 100644 index bd601cb1de..0000000000 --- a/packages/client/src/pages/admin/emoji-edit-dialog.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue deleted file mode 100644 index 14c8466d73..0000000000 --- a/packages/client/src/pages/admin/emojis.vue +++ /dev/null @@ -1,398 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue deleted file mode 100644 index 8ad6bd4fc0..0000000000 --- a/packages/client/src/pages/admin/files.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue deleted file mode 100644 index 6c07a87eeb..0000000000 --- a/packages/client/src/pages/admin/index.vue +++ /dev/null @@ -1,316 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue deleted file mode 100644 index 1bdd174de4..0000000000 --- a/packages/client/src/pages/admin/instance-block.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/integrations.discord.vue b/packages/client/src/pages/admin/integrations.discord.vue deleted file mode 100644 index 0a69c44c93..0000000000 --- a/packages/client/src/pages/admin/integrations.discord.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/integrations.github.vue b/packages/client/src/pages/admin/integrations.github.vue deleted file mode 100644 index 66419d5891..0000000000 --- a/packages/client/src/pages/admin/integrations.github.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/integrations.twitter.vue b/packages/client/src/pages/admin/integrations.twitter.vue deleted file mode 100644 index 1e8d882b9c..0000000000 --- a/packages/client/src/pages/admin/integrations.twitter.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue deleted file mode 100644 index 9cc35baefd..0000000000 --- a/packages/client/src/pages/admin/integrations.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/metrics.vue b/packages/client/src/pages/admin/metrics.vue deleted file mode 100644 index db8e448639..0000000000 --- a/packages/client/src/pages/admin/metrics.vue +++ /dev/null @@ -1,472 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue deleted file mode 100644 index f2ab30eaa5..0000000000 --- a/packages/client/src/pages/admin/object-storage.vue +++ /dev/null @@ -1,148 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue deleted file mode 100644 index 62dff6ce7f..0000000000 --- a/packages/client/src/pages/admin/other-settings.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/overview.active-users.vue b/packages/client/src/pages/admin/overview.active-users.vue deleted file mode 100644 index c3ce5ac901..0000000000 --- a/packages/client/src/pages/admin/overview.active-users.vue +++ /dev/null @@ -1,217 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.ap-requests.vue b/packages/client/src/pages/admin/overview.ap-requests.vue deleted file mode 100644 index 024ffdc245..0000000000 --- a/packages/client/src/pages/admin/overview.ap-requests.vue +++ /dev/null @@ -1,346 +0,0 @@ - - - - - - diff --git a/packages/client/src/pages/admin/overview.federation.vue b/packages/client/src/pages/admin/overview.federation.vue deleted file mode 100644 index 71f5a054b4..0000000000 --- a/packages/client/src/pages/admin/overview.federation.vue +++ /dev/null @@ -1,185 +0,0 @@ - - - - - - diff --git a/packages/client/src/pages/admin/overview.heatmap.vue b/packages/client/src/pages/admin/overview.heatmap.vue deleted file mode 100644 index 16d1c83b9f..0000000000 --- a/packages/client/src/pages/admin/overview.heatmap.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.instances.vue b/packages/client/src/pages/admin/overview.instances.vue deleted file mode 100644 index 29848bf03b..0000000000 --- a/packages/client/src/pages/admin/overview.instances.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.moderators.vue b/packages/client/src/pages/admin/overview.moderators.vue deleted file mode 100644 index a1f63c8711..0000000000 --- a/packages/client/src/pages/admin/overview.moderators.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.pie.vue b/packages/client/src/pages/admin/overview.pie.vue deleted file mode 100644 index 94509cf006..0000000000 --- a/packages/client/src/pages/admin/overview.pie.vue +++ /dev/null @@ -1,110 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.queue.chart.vue b/packages/client/src/pages/admin/overview.queue.chart.vue deleted file mode 100644 index 1e095bddaa..0000000000 --- a/packages/client/src/pages/admin/overview.queue.chart.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.queue.vue b/packages/client/src/pages/admin/overview.queue.vue deleted file mode 100644 index 72ebddc72f..0000000000 --- a/packages/client/src/pages/admin/overview.queue.vue +++ /dev/null @@ -1,127 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.retention.vue b/packages/client/src/pages/admin/overview.retention.vue deleted file mode 100644 index feac6f8118..0000000000 --- a/packages/client/src/pages/admin/overview.retention.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.stats.vue b/packages/client/src/pages/admin/overview.stats.vue deleted file mode 100644 index 4dcf7e751a..0000000000 --- a/packages/client/src/pages/admin/overview.stats.vue +++ /dev/null @@ -1,155 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.users.vue b/packages/client/src/pages/admin/overview.users.vue deleted file mode 100644 index 5d4be11742..0000000000 --- a/packages/client/src/pages/admin/overview.users.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue deleted file mode 100644 index d656e55200..0000000000 --- a/packages/client/src/pages/admin/overview.vue +++ /dev/null @@ -1,190 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue deleted file mode 100644 index 5d0d67980e..0000000000 --- a/packages/client/src/pages/admin/proxy-account.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/queue.chart.chart.vue b/packages/client/src/pages/admin/queue.chart.chart.vue deleted file mode 100644 index 5777674ae3..0000000000 --- a/packages/client/src/pages/admin/queue.chart.chart.vue +++ /dev/null @@ -1,186 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/queue.chart.vue b/packages/client/src/pages/admin/queue.chart.vue deleted file mode 100644 index 186a22c43e..0000000000 --- a/packages/client/src/pages/admin/queue.chart.vue +++ /dev/null @@ -1,149 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue deleted file mode 100644 index 8d19b49fc5..0000000000 --- a/packages/client/src/pages/admin/queue.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue deleted file mode 100644 index 4768ae67b1..0000000000 --- a/packages/client/src/pages/admin/relays.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue deleted file mode 100644 index 2682bda337..0000000000 --- a/packages/client/src/pages/admin/security.vue +++ /dev/null @@ -1,179 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue deleted file mode 100644 index 460eb92694..0000000000 --- a/packages/client/src/pages/admin/settings.vue +++ /dev/null @@ -1,262 +0,0 @@ - - - diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue deleted file mode 100644 index d466e21907..0000000000 --- a/packages/client/src/pages/admin/users.vue +++ /dev/null @@ -1,170 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/announcements.vue b/packages/client/src/pages/announcements.vue deleted file mode 100644 index 6a93b3b9fa..0000000000 --- a/packages/client/src/pages/announcements.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue deleted file mode 100644 index 0b2c284c99..0000000000 --- a/packages/client/src/pages/antenna-timeline.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/api-console.vue b/packages/client/src/pages/api-console.vue deleted file mode 100644 index 1d5339b44c..0000000000 --- a/packages/client/src/pages/api-console.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - diff --git a/packages/client/src/pages/auth.form.vue b/packages/client/src/pages/auth.form.vue deleted file mode 100644 index 1546735266..0000000000 --- a/packages/client/src/pages/auth.form.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - diff --git a/packages/client/src/pages/auth.vue b/packages/client/src/pages/auth.vue deleted file mode 100644 index bb55881a22..0000000000 --- a/packages/client/src/pages/auth.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/channel-editor.vue b/packages/client/src/pages/channel-editor.vue deleted file mode 100644 index 5ae7e63f99..0000000000 --- a/packages/client/src/pages/channel-editor.vue +++ /dev/null @@ -1,122 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/channel.vue b/packages/client/src/pages/channel.vue deleted file mode 100644 index f271bb270f..0000000000 --- a/packages/client/src/pages/channel.vue +++ /dev/null @@ -1,184 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/channels.vue b/packages/client/src/pages/channels.vue deleted file mode 100644 index 34e9dac196..0000000000 --- a/packages/client/src/pages/channels.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - diff --git a/packages/client/src/pages/clip.vue b/packages/client/src/pages/clip.vue deleted file mode 100644 index e0fbcb6bed..0000000000 --- a/packages/client/src/pages/clip.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/drive.vue b/packages/client/src/pages/drive.vue deleted file mode 100644 index 04ade5c207..0000000000 --- a/packages/client/src/pages/drive.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - diff --git a/packages/client/src/pages/emojis.emoji.vue b/packages/client/src/pages/emojis.emoji.vue deleted file mode 100644 index 40fe496520..0000000000 --- a/packages/client/src/pages/emojis.emoji.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/explore.featured.vue b/packages/client/src/pages/explore.featured.vue deleted file mode 100644 index 18a371a086..0000000000 --- a/packages/client/src/pages/explore.featured.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/packages/client/src/pages/explore.users.vue b/packages/client/src/pages/explore.users.vue deleted file mode 100644 index bfee0a6c07..0000000000 --- a/packages/client/src/pages/explore.users.vue +++ /dev/null @@ -1,148 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/explore.vue b/packages/client/src/pages/explore.vue deleted file mode 100644 index 6b0bcdaf62..0000000000 --- a/packages/client/src/pages/explore.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue deleted file mode 100644 index ab47efec71..0000000000 --- a/packages/client/src/pages/favorites.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/follow-requests.vue b/packages/client/src/pages/follow-requests.vue deleted file mode 100644 index b20679ccc1..0000000000 --- a/packages/client/src/pages/follow-requests.vue +++ /dev/null @@ -1,153 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/follow.vue b/packages/client/src/pages/follow.vue deleted file mode 100644 index 828246d678..0000000000 --- a/packages/client/src/pages/follow.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - diff --git a/packages/client/src/pages/gallery/edit.vue b/packages/client/src/pages/gallery/edit.vue deleted file mode 100644 index c8111d7890..0000000000 --- a/packages/client/src/pages/gallery/edit.vue +++ /dev/null @@ -1,149 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/gallery/index.vue b/packages/client/src/pages/gallery/index.vue deleted file mode 100644 index 24a634bab5..0000000000 --- a/packages/client/src/pages/gallery/index.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/gallery/post.vue b/packages/client/src/pages/gallery/post.vue deleted file mode 100644 index 85ab1048be..0000000000 --- a/packages/client/src/pages/gallery/post.vue +++ /dev/null @@ -1,265 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/instance-info.vue b/packages/client/src/pages/instance-info.vue deleted file mode 100644 index a2a1254360..0000000000 --- a/packages/client/src/pages/instance-info.vue +++ /dev/null @@ -1,258 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/messaging/index.vue b/packages/client/src/pages/messaging/index.vue deleted file mode 100644 index 0d30998330..0000000000 --- a/packages/client/src/pages/messaging/index.vue +++ /dev/null @@ -1,327 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/messaging/messaging-room.form.vue b/packages/client/src/pages/messaging/messaging-room.form.vue deleted file mode 100644 index 84572815c0..0000000000 --- a/packages/client/src/pages/messaging/messaging-room.form.vue +++ /dev/null @@ -1,364 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/messaging/messaging-room.message.vue b/packages/client/src/pages/messaging/messaging-room.message.vue deleted file mode 100644 index dbf0e37b73..0000000000 --- a/packages/client/src/pages/messaging/messaging-room.message.vue +++ /dev/null @@ -1,367 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/messaging/messaging-room.vue b/packages/client/src/pages/messaging/messaging-room.vue deleted file mode 100644 index b6eeb9260e..0000000000 --- a/packages/client/src/pages/messaging/messaging-room.vue +++ /dev/null @@ -1,411 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/mfm-cheat-sheet.vue b/packages/client/src/pages/mfm-cheat-sheet.vue deleted file mode 100644 index 7c85dfb7ad..0000000000 --- a/packages/client/src/pages/mfm-cheat-sheet.vue +++ /dev/null @@ -1,387 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/miauth.vue b/packages/client/src/pages/miauth.vue deleted file mode 100644 index 5de072cbfa..0000000000 --- a/packages/client/src/pages/miauth.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/my-antennas/create.vue b/packages/client/src/pages/my-antennas/create.vue deleted file mode 100644 index 005b036696..0000000000 --- a/packages/client/src/pages/my-antennas/create.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/my-antennas/edit.vue b/packages/client/src/pages/my-antennas/edit.vue deleted file mode 100644 index cb583faaeb..0000000000 --- a/packages/client/src/pages/my-antennas/edit.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/my-antennas/editor.vue b/packages/client/src/pages/my-antennas/editor.vue deleted file mode 100644 index a409a734b5..0000000000 --- a/packages/client/src/pages/my-antennas/editor.vue +++ /dev/null @@ -1,155 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/my-antennas/index.vue b/packages/client/src/pages/my-antennas/index.vue deleted file mode 100644 index 9daf23f9b5..0000000000 --- a/packages/client/src/pages/my-antennas/index.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/my-clips/index.vue b/packages/client/src/pages/my-clips/index.vue deleted file mode 100644 index dd6b5b3a37..0000000000 --- a/packages/client/src/pages/my-clips/index.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/my-lists/index.vue b/packages/client/src/pages/my-lists/index.vue deleted file mode 100644 index 3476436b27..0000000000 --- a/packages/client/src/pages/my-lists/index.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/my-lists/list.vue b/packages/client/src/pages/my-lists/list.vue deleted file mode 100644 index f6234ffe44..0000000000 --- a/packages/client/src/pages/my-lists/list.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/not-found.vue b/packages/client/src/pages/not-found.vue deleted file mode 100644 index e58e44ef79..0000000000 --- a/packages/client/src/pages/not-found.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/packages/client/src/pages/note.vue b/packages/client/src/pages/note.vue deleted file mode 100644 index ba2bb91239..0000000000 --- a/packages/client/src/pages/note.vue +++ /dev/null @@ -1,206 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/notifications.vue b/packages/client/src/pages/notifications.vue deleted file mode 100644 index 7106951de2..0000000000 --- a/packages/client/src/pages/notifications.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue deleted file mode 100644 index a84cb1e80e..0000000000 --- a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue +++ /dev/null @@ -1,63 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.note.vue b/packages/client/src/pages/page-editor/els/page-editor.el.note.vue deleted file mode 100644 index dc2a620c09..0000000000 --- a/packages/client/src/pages/page-editor/els/page-editor.el.note.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.section.vue b/packages/client/src/pages/page-editor/els/page-editor.el.section.vue deleted file mode 100644 index 27324bdaef..0000000000 --- a/packages/client/src/pages/page-editor/els/page-editor.el.section.vue +++ /dev/null @@ -1,97 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.text.vue b/packages/client/src/pages/page-editor/els/page-editor.el.text.vue deleted file mode 100644 index 6f11e2a08b..0000000000 --- a/packages/client/src/pages/page-editor/els/page-editor.el.text.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/page-editor/page-editor.blocks.vue b/packages/client/src/pages/page-editor/page-editor.blocks.vue deleted file mode 100644 index f99fcb202f..0000000000 --- a/packages/client/src/pages/page-editor/page-editor.blocks.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/page-editor/page-editor.container.vue b/packages/client/src/pages/page-editor/page-editor.container.vue deleted file mode 100644 index 15cdda5efb..0000000000 --- a/packages/client/src/pages/page-editor/page-editor.container.vue +++ /dev/null @@ -1,155 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue deleted file mode 100644 index 968aa12de2..0000000000 --- a/packages/client/src/pages/page-editor/page-editor.vue +++ /dev/null @@ -1,394 +0,0 @@ - - - - - - - diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue deleted file mode 100644 index a95bfe485c..0000000000 --- a/packages/client/src/pages/page.vue +++ /dev/null @@ -1,277 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/pages.vue b/packages/client/src/pages/pages.vue deleted file mode 100644 index b077180df8..0000000000 --- a/packages/client/src/pages/pages.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/preview.vue b/packages/client/src/pages/preview.vue deleted file mode 100644 index 354f686e46..0000000000 --- a/packages/client/src/pages/preview.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/registry.keys.vue b/packages/client/src/pages/registry.keys.vue deleted file mode 100644 index f179fbe957..0000000000 --- a/packages/client/src/pages/registry.keys.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/registry.value.vue b/packages/client/src/pages/registry.value.vue deleted file mode 100644 index 378420b1ba..0000000000 --- a/packages/client/src/pages/registry.value.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/registry.vue b/packages/client/src/pages/registry.vue deleted file mode 100644 index a2c65294fc..0000000000 --- a/packages/client/src/pages/registry.vue +++ /dev/null @@ -1,74 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/reset-password.vue b/packages/client/src/pages/reset-password.vue deleted file mode 100644 index 8ec15f6425..0000000000 --- a/packages/client/src/pages/reset-password.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/scratchpad.vue b/packages/client/src/pages/scratchpad.vue deleted file mode 100644 index edb2d8e18c..0000000000 --- a/packages/client/src/pages/scratchpad.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue deleted file mode 100644 index c080b763bb..0000000000 --- a/packages/client/src/pages/search.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/2fa.vue b/packages/client/src/pages/settings/2fa.vue deleted file mode 100644 index 1803129aaa..0000000000 --- a/packages/client/src/pages/settings/2fa.vue +++ /dev/null @@ -1,216 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/account-info.vue b/packages/client/src/pages/settings/account-info.vue deleted file mode 100644 index ccd99c162a..0000000000 --- a/packages/client/src/pages/settings/account-info.vue +++ /dev/null @@ -1,158 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/accounts.vue b/packages/client/src/pages/settings/accounts.vue deleted file mode 100644 index 493d3b2618..0000000000 --- a/packages/client/src/pages/settings/accounts.vue +++ /dev/null @@ -1,143 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/api.vue b/packages/client/src/pages/settings/api.vue deleted file mode 100644 index 8d7291cd10..0000000000 --- a/packages/client/src/pages/settings/api.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/apps.vue b/packages/client/src/pages/settings/apps.vue deleted file mode 100644 index 05abadff23..0000000000 --- a/packages/client/src/pages/settings/apps.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/custom-css.vue b/packages/client/src/pages/settings/custom-css.vue deleted file mode 100644 index 2caad22b7b..0000000000 --- a/packages/client/src/pages/settings/custom-css.vue +++ /dev/null @@ -1,46 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/deck.vue b/packages/client/src/pages/settings/deck.vue deleted file mode 100644 index 82cefe05d5..0000000000 --- a/packages/client/src/pages/settings/deck.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/delete-account.vue b/packages/client/src/pages/settings/delete-account.vue deleted file mode 100644 index 8a25ff39f0..0000000000 --- a/packages/client/src/pages/settings/delete-account.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue deleted file mode 100644 index 2d45b1add8..0000000000 --- a/packages/client/src/pages/settings/drive.vue +++ /dev/null @@ -1,145 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/email.vue b/packages/client/src/pages/settings/email.vue deleted file mode 100644 index 3fff8c6b1d..0000000000 --- a/packages/client/src/pages/settings/email.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/general.vue b/packages/client/src/pages/settings/general.vue deleted file mode 100644 index 84d99d2fd7..0000000000 --- a/packages/client/src/pages/settings/general.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue deleted file mode 100644 index 7db267c142..0000000000 --- a/packages/client/src/pages/settings/import-export.vue +++ /dev/null @@ -1,165 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue deleted file mode 100644 index 01436cd554..0000000000 --- a/packages/client/src/pages/settings/index.vue +++ /dev/null @@ -1,291 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue deleted file mode 100644 index 54504de188..0000000000 --- a/packages/client/src/pages/settings/instance-mute.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue deleted file mode 100644 index 557fe778e6..0000000000 --- a/packages/client/src/pages/settings/integration.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/mute-block.vue b/packages/client/src/pages/settings/mute-block.vue deleted file mode 100644 index 1cf33d34db..0000000000 --- a/packages/client/src/pages/settings/mute-block.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/navbar.vue b/packages/client/src/pages/settings/navbar.vue deleted file mode 100644 index 0b2776ec90..0000000000 --- a/packages/client/src/pages/settings/navbar.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue deleted file mode 100644 index e85fede157..0000000000 --- a/packages/client/src/pages/settings/notifications.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/other.vue b/packages/client/src/pages/settings/other.vue deleted file mode 100644 index 40bb202789..0000000000 --- a/packages/client/src/pages/settings/other.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/plugin.install.vue b/packages/client/src/pages/settings/plugin.install.vue deleted file mode 100644 index 550bba242e..0000000000 --- a/packages/client/src/pages/settings/plugin.install.vue +++ /dev/null @@ -1,124 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/plugin.vue b/packages/client/src/pages/settings/plugin.vue deleted file mode 100644 index 905efd833d..0000000000 --- a/packages/client/src/pages/settings/plugin.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/preferences-backups.vue b/packages/client/src/pages/settings/preferences-backups.vue deleted file mode 100644 index f427a170c4..0000000000 --- a/packages/client/src/pages/settings/preferences-backups.vue +++ /dev/null @@ -1,444 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/privacy.vue b/packages/client/src/pages/settings/privacy.vue deleted file mode 100644 index 915ca05767..0000000000 --- a/packages/client/src/pages/settings/privacy.vue +++ /dev/null @@ -1,100 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/profile.vue b/packages/client/src/pages/settings/profile.vue deleted file mode 100644 index 14eeeaaa11..0000000000 --- a/packages/client/src/pages/settings/profile.vue +++ /dev/null @@ -1,220 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/reaction.vue b/packages/client/src/pages/settings/reaction.vue deleted file mode 100644 index 2748cd7d4e..0000000000 --- a/packages/client/src/pages/settings/reaction.vue +++ /dev/null @@ -1,154 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/security.vue b/packages/client/src/pages/settings/security.vue deleted file mode 100644 index 33f49eb3ef..0000000000 --- a/packages/client/src/pages/settings/security.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/sounds.sound.vue b/packages/client/src/pages/settings/sounds.sound.vue deleted file mode 100644 index 62627c6333..0000000000 --- a/packages/client/src/pages/settings/sounds.sound.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/sounds.vue b/packages/client/src/pages/settings/sounds.vue deleted file mode 100644 index ef60b2c3c9..0000000000 --- a/packages/client/src/pages/settings/sounds.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/statusbar.statusbar.vue b/packages/client/src/pages/settings/statusbar.statusbar.vue deleted file mode 100644 index 608222386e..0000000000 --- a/packages/client/src/pages/settings/statusbar.statusbar.vue +++ /dev/null @@ -1,140 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/statusbar.vue b/packages/client/src/pages/settings/statusbar.vue deleted file mode 100644 index 86c69fa2c3..0000000000 --- a/packages/client/src/pages/settings/statusbar.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/theme.install.vue b/packages/client/src/pages/settings/theme.install.vue deleted file mode 100644 index 52a436e18d..0000000000 --- a/packages/client/src/pages/settings/theme.install.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/theme.manage.vue b/packages/client/src/pages/settings/theme.manage.vue deleted file mode 100644 index 409f0af650..0000000000 --- a/packages/client/src/pages/settings/theme.manage.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/theme.vue b/packages/client/src/pages/settings/theme.vue deleted file mode 100644 index f37c213b06..0000000000 --- a/packages/client/src/pages/settings/theme.vue +++ /dev/null @@ -1,409 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/settings/webhook.edit.vue b/packages/client/src/pages/settings/webhook.edit.vue deleted file mode 100644 index c8ec1ea586..0000000000 --- a/packages/client/src/pages/settings/webhook.edit.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/webhook.new.vue b/packages/client/src/pages/settings/webhook.new.vue deleted file mode 100644 index 00a547da69..0000000000 --- a/packages/client/src/pages/settings/webhook.new.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/webhook.vue b/packages/client/src/pages/settings/webhook.vue deleted file mode 100644 index 9be23ee4f0..0000000000 --- a/packages/client/src/pages/settings/webhook.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/packages/client/src/pages/settings/word-mute.vue b/packages/client/src/pages/settings/word-mute.vue deleted file mode 100644 index 6961d8151d..0000000000 --- a/packages/client/src/pages/settings/word-mute.vue +++ /dev/null @@ -1,128 +0,0 @@ - - - diff --git a/packages/client/src/pages/share.vue b/packages/client/src/pages/share.vue deleted file mode 100644 index a7e797eeab..0000000000 --- a/packages/client/src/pages/share.vue +++ /dev/null @@ -1,169 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/signup-complete.vue b/packages/client/src/pages/signup-complete.vue deleted file mode 100644 index 5459532310..0000000000 --- a/packages/client/src/pages/signup-complete.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/tag.vue b/packages/client/src/pages/tag.vue deleted file mode 100644 index 72775ed5c9..0000000000 --- a/packages/client/src/pages/tag.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/packages/client/src/pages/theme-editor.vue b/packages/client/src/pages/theme-editor.vue deleted file mode 100644 index d8ff170ca2..0000000000 --- a/packages/client/src/pages/theme-editor.vue +++ /dev/null @@ -1,283 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue deleted file mode 100644 index ae7b098b90..0000000000 --- a/packages/client/src/pages/timeline.tutorial.vue +++ /dev/null @@ -1,142 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/timeline.vue b/packages/client/src/pages/timeline.vue deleted file mode 100644 index 1c9e389367..0000000000 --- a/packages/client/src/pages/timeline.vue +++ /dev/null @@ -1,183 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue deleted file mode 100644 index addc8db9e6..0000000000 --- a/packages/client/src/pages/user-info.vue +++ /dev/null @@ -1,485 +0,0 @@ - - - - - - - diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue deleted file mode 100644 index fdb3167375..0000000000 --- a/packages/client/src/pages/user-list-timeline.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/clips.vue b/packages/client/src/pages/user/clips.vue deleted file mode 100644 index 8c71aacb0c..0000000000 --- a/packages/client/src/pages/user/clips.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/follow-list.vue b/packages/client/src/pages/user/follow-list.vue deleted file mode 100644 index d42acd838f..0000000000 --- a/packages/client/src/pages/user/follow-list.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/followers.vue b/packages/client/src/pages/user/followers.vue deleted file mode 100644 index 17c2843381..0000000000 --- a/packages/client/src/pages/user/followers.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/following.vue b/packages/client/src/pages/user/following.vue deleted file mode 100644 index 03892ec03d..0000000000 --- a/packages/client/src/pages/user/following.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/gallery.vue b/packages/client/src/pages/user/gallery.vue deleted file mode 100644 index b80e83fb11..0000000000 --- a/packages/client/src/pages/user/gallery.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/home.vue b/packages/client/src/pages/user/home.vue deleted file mode 100644 index 43c1b37e1d..0000000000 --- a/packages/client/src/pages/user/home.vue +++ /dev/null @@ -1,530 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/index.activity.vue b/packages/client/src/pages/user/index.activity.vue deleted file mode 100644 index 523072d2e6..0000000000 --- a/packages/client/src/pages/user/index.activity.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/packages/client/src/pages/user/index.photos.vue b/packages/client/src/pages/user/index.photos.vue deleted file mode 100644 index b33979a79d..0000000000 --- a/packages/client/src/pages/user/index.photos.vue +++ /dev/null @@ -1,102 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/index.timeline.vue b/packages/client/src/pages/user/index.timeline.vue deleted file mode 100644 index 41983a5ae8..0000000000 --- a/packages/client/src/pages/user/index.timeline.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/index.vue b/packages/client/src/pages/user/index.vue deleted file mode 100644 index 6e895cd8d7..0000000000 --- a/packages/client/src/pages/user/index.vue +++ /dev/null @@ -1,113 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/pages.vue b/packages/client/src/pages/user/pages.vue deleted file mode 100644 index 7833d6c42c..0000000000 --- a/packages/client/src/pages/user/pages.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/user/reactions.vue b/packages/client/src/pages/user/reactions.vue deleted file mode 100644 index ab3df34301..0000000000 --- a/packages/client/src/pages/user/reactions.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/welcome.entrance.a.vue b/packages/client/src/pages/welcome.entrance.a.vue deleted file mode 100644 index bfa54d39f2..0000000000 --- a/packages/client/src/pages/welcome.entrance.a.vue +++ /dev/null @@ -1,309 +0,0 @@ - - - - - - - diff --git a/packages/client/src/pages/welcome.entrance.b.vue b/packages/client/src/pages/welcome.entrance.b.vue deleted file mode 100644 index 8230adaf1f..0000000000 --- a/packages/client/src/pages/welcome.entrance.b.vue +++ /dev/null @@ -1,237 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/welcome.entrance.c.vue b/packages/client/src/pages/welcome.entrance.c.vue deleted file mode 100644 index d2d07bb1f0..0000000000 --- a/packages/client/src/pages/welcome.entrance.c.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/welcome.setup.vue b/packages/client/src/pages/welcome.setup.vue deleted file mode 100644 index 2729d30d4b..0000000000 --- a/packages/client/src/pages/welcome.setup.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/welcome.timeline.vue b/packages/client/src/pages/welcome.timeline.vue deleted file mode 100644 index d6a88540d1..0000000000 --- a/packages/client/src/pages/welcome.timeline.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - diff --git a/packages/client/src/pages/welcome.vue b/packages/client/src/pages/welcome.vue deleted file mode 100644 index a1c3fc2abb..0000000000 --- a/packages/client/src/pages/welcome.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts deleted file mode 100644 index 642e1f4f7f..0000000000 --- a/packages/client/src/pizzax.ts +++ /dev/null @@ -1,169 +0,0 @@ -// PIZZAX --- A lightweight store - -import { onUnmounted, Ref, ref, watch } from 'vue'; -import { $i } from './account'; -import { api } from './os'; -import { stream } from './stream'; - -type StateDef = Record; - -type ArrayElement = A extends readonly (infer T)[] ? T : never; - -const connection = $i && stream.useChannel('main'); - -export class Storage { - public readonly key: string; - public readonly keyForLocalStorage: string; - - public readonly def: T; - - // TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487 - public readonly state: { [K in keyof T]: T[K]['default'] }; - public readonly reactiveState: { [K in keyof T]: Ref }; - - constructor(key: string, def: T) { - this.key = key; - this.keyForLocalStorage = 'pizzax::' + key; - this.def = def; - - // TODO: indexedDBにする - const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}'); - const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {}; - const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {}; - - const state = {}; - const reactiveState = {}; - for (const [k, v] of Object.entries(def)) { - if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) { - state[k] = deviceState[k]; - } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) { - state[k] = registryCache[k]; - } else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) { - state[k] = deviceAccountState[k]; - } else { - state[k] = v.default; - if (_DEV_) console.log('Use default value', k, v.default); - } - } - for (const [k, v] of Object.entries(state)) { - reactiveState[k] = ref(v); - } - this.state = state as any; - this.reactiveState = reactiveState as any; - - if ($i) { - // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) - window.setTimeout(() => { - api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => { - const cache = {}; - for (const [k, v] of Object.entries(def)) { - if (v.where === 'account') { - if (Object.prototype.hasOwnProperty.call(kvs, k)) { - state[k] = kvs[k]; - reactiveState[k].value = kvs[k]; - cache[k] = kvs[k]; - } else { - state[k] = v.default; - reactiveState[k].value = v.default; - } - } - } - 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; - - 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)); - } - }); - } - } - - public set(key: K, value: T[K]['default']): void { - if (_DEV_) console.log('set', key, value); - - this.state[key] = value; - this.reactiveState[key].value = value; - - switch (this.def[key].where) { - case 'device': { - const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}'); - deviceState[key] = value; - localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState)); - break; - } - case 'deviceAccount': { - if ($i == null) break; - const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}'); - deviceAccountState[key] = value; - localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState)); - break; - } - case 'account': { - if ($i == null) break; - const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}'); - cache[key] = value; - localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache)); - api('i/registry/set', { - scope: ['client', this.key], - key: key, - value: value, - }); - break; - } - } - } - - public push(key: K, value: ArrayElement): void { - const currentState = this.state[key]; - this.set(key, [...currentState, value]); - } - - public reset(key: keyof T) { - this.set(key, this.def[key].default); - } - - /** - * 特定のキーの、簡易的なgetter/setterを作ります - * 主にvue場で設定コントロールのmodelとして使う用 - */ - public makeGetterSetter(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) { - const valueRef = ref(this.state[key]); - - const stop = watch(this.reactiveState[key], val => { - valueRef.value = val; - }); - - // NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする - onUnmounted(() => { - stop(); - }); - - // TODO: VueのcustomRef使うと良い感じになるかも - return { - get: () => { - if (getter) { - return getter(valueRef.value); - } else { - return valueRef.value; - } - }, - set: (value: unknown) => { - const val = setter ? setter(value) : value; - this.set(key, val); - valueRef.value = val; - }, - }; - } -} diff --git a/packages/client/src/plugin.ts b/packages/client/src/plugin.ts deleted file mode 100644 index 3a00cd0455..0000000000 --- a/packages/client/src/plugin.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { AiScript, utils, values } from '@syuilo/aiscript'; -import { deserialize } from '@syuilo/aiscript/built/serializer'; -import { jsToVal } from '@syuilo/aiscript/built/interpreter/util'; -import { createAiScriptEnv } from '@/scripts/aiscript/api'; -import { inputText } from '@/os'; -import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store'; - -const pluginContexts = new Map(); - -export function install(plugin) { - console.info('Plugin installed:', plugin.name, 'v' + plugin.version); - - const aiscript = new AiScript(createPluginEnv({ - plugin: plugin, - storageKey: 'plugins:' + plugin.id, - }), { - in: (q) => { - return new Promise(ok => { - inputText({ - title: q, - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - console.log(value); - }, - log: (type, params) => { - }, - }); - - initPlugin({ plugin, aiscript }); - - aiscript.exec(deserialize(plugin.ast)); -} - -function createPluginEnv(opts) { - const config = new Map(); - for (const [k, v] of Object.entries(opts.plugin.config || {})) { - config.set(k, jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default)); - } - - return { - ...createAiScriptEnv({ ...opts, token: opts.plugin.token }), - //#region Deprecated - 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { - registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => { - registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { - registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - //#endregion - 'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => { - registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => { - registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => { - registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); - }), - 'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => { - registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler }); - }), - 'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => { - registerNotePostInterruptor({ pluginId: opts.plugin.id, handler }); - }), - 'Plugin:open_url': values.FN_NATIVE(([url]) => { - window.open(url.value, '_blank'); - }), - 'Plugin:config': values.OBJ(config), - }; -} - -function initPlugin({ plugin, aiscript }) { - pluginContexts.set(plugin.id, aiscript); -} - -function registerPostFormAction({ pluginId, title, handler }) { - postFormActions.push({ - title, handler: (form, update) => { - pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => { - update(key.value, value.value); - })]); - }, - }); -} - -function registerUserAction({ pluginId, title, handler }) { - userActions.push({ - title, handler: (user) => { - pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]); - }, - }); -} - -function registerNoteAction({ pluginId, title, handler }) { - noteActions.push({ - title, handler: (note) => { - pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]); - }, - }); -} - -function registerNoteViewInterruptor({ pluginId, handler }) { - noteViewInterruptors.push({ - handler: async (note) => { - return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)])); - }, - }); -} - -function registerNotePostInterruptor({ pluginId, handler }) { - notePostInterruptors.push({ - handler: async (note) => { - return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)])); - }, - }); -} diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts deleted file mode 100644 index 111b15e0a6..0000000000 --- a/packages/client/src/router.ts +++ /dev/null @@ -1,501 +0,0 @@ -import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue'; -import { Router } from '@/nirax'; -import { $i, iAmModerator } from '@/account'; -import MkLoading from '@/pages/_loading_.vue'; -import MkError from '@/pages/_error_.vue'; -import { ui } from '@/config'; - -const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ - loader: loader, - loadingComponent: MkLoading, - errorComponent: MkError, -}); - -export const routes = [{ - path: '/@:initUser/pages/:initPageName/view-source', - component: page(() => import('./pages/page-editor/page-editor.vue')), -}, { - path: '/@:username/pages/:pageName', - component: page(() => import('./pages/page.vue')), -}, { - path: '/@:acct/following', - component: page(() => import('./pages/user/following.vue')), -}, { - path: '/@:acct/followers', - component: page(() => import('./pages/user/followers.vue')), -}, { - name: 'user', - path: '/@:acct/:page?', - component: page(() => import('./pages/user/index.vue')), -}, { - name: 'note', - path: '/notes/:noteId', - component: page(() => import('./pages/note.vue')), -}, { - path: '/clips/:clipId', - component: page(() => import('./pages/clip.vue')), -}, { - path: '/user-info/:userId', - component: page(() => import('./pages/user-info.vue')), -}, { - path: '/instance-info/:host', - component: page(() => import('./pages/instance-info.vue')), -}, { - name: 'settings', - path: '/settings', - component: page(() => import('./pages/settings/index.vue')), - loginRequired: true, - children: [{ - path: '/profile', - name: 'profile', - component: page(() => import('./pages/settings/profile.vue')), - }, { - path: '/privacy', - name: 'privacy', - component: page(() => import('./pages/settings/privacy.vue')), - }, { - path: '/reaction', - name: 'reaction', - component: page(() => import('./pages/settings/reaction.vue')), - }, { - path: '/drive', - name: 'drive', - component: page(() => import('./pages/settings/drive.vue')), - }, { - path: '/notifications', - name: 'notifications', - component: page(() => import('./pages/settings/notifications.vue')), - }, { - path: '/email', - name: 'email', - component: page(() => import('./pages/settings/email.vue')), - }, { - path: '/integration', - name: 'integration', - component: page(() => import('./pages/settings/integration.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('./pages/settings/security.vue')), - }, { - path: '/general', - name: 'general', - component: page(() => import('./pages/settings/general.vue')), - }, { - path: '/theme/install', - name: 'theme', - component: page(() => import('./pages/settings/theme.install.vue')), - }, { - path: '/theme/manage', - name: 'theme', - component: page(() => import('./pages/settings/theme.manage.vue')), - }, { - path: '/theme', - name: 'theme', - component: page(() => import('./pages/settings/theme.vue')), - }, { - path: '/navbar', - name: 'navbar', - component: page(() => import('./pages/settings/navbar.vue')), - }, { - path: '/statusbar', - name: 'statusbar', - component: page(() => import('./pages/settings/statusbar.vue')), - }, { - path: '/sounds', - name: 'sounds', - component: page(() => import('./pages/settings/sounds.vue')), - }, { - path: '/plugin/install', - name: 'plugin', - component: page(() => import('./pages/settings/plugin.install.vue')), - }, { - path: '/plugin', - name: 'plugin', - component: page(() => import('./pages/settings/plugin.vue')), - }, { - path: '/import-export', - name: 'import-export', - component: page(() => import('./pages/settings/import-export.vue')), - }, { - path: '/instance-mute', - name: 'instance-mute', - component: page(() => import('./pages/settings/instance-mute.vue')), - }, { - path: '/mute-block', - name: 'mute-block', - component: page(() => import('./pages/settings/mute-block.vue')), - }, { - path: '/word-mute', - name: 'word-mute', - component: page(() => import('./pages/settings/word-mute.vue')), - }, { - path: '/api', - name: 'api', - component: page(() => import('./pages/settings/api.vue')), - }, { - path: '/apps', - name: 'api', - component: page(() => import('./pages/settings/apps.vue')), - }, { - path: '/webhook/edit/:webhookId', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.edit.vue')), - }, { - path: '/webhook/new', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.new.vue')), - }, { - path: '/webhook', - name: 'webhook', - component: page(() => import('./pages/settings/webhook.vue')), - }, { - path: '/deck', - name: 'deck', - component: page(() => import('./pages/settings/deck.vue')), - }, { - path: '/preferences-backups', - name: 'preferences-backups', - component: page(() => import('./pages/settings/preferences-backups.vue')), - }, { - path: '/custom-css', - name: 'general', - component: page(() => import('./pages/settings/custom-css.vue')), - }, { - path: '/accounts', - name: 'profile', - component: page(() => import('./pages/settings/accounts.vue')), - }, { - path: '/account-info', - name: 'other', - component: page(() => import('./pages/settings/account-info.vue')), - }, { - path: '/delete-account', - name: 'other', - component: page(() => import('./pages/settings/delete-account.vue')), - }, { - path: '/other', - name: 'other', - component: page(() => import('./pages/settings/other.vue')), - }, { - path: '/', - component: page(() => import('./pages/_empty_.vue')), - }], -}, { - path: '/reset-password/:token?', - component: page(() => import('./pages/reset-password.vue')), -}, { - path: '/signup-complete/:code', - component: page(() => import('./pages/signup-complete.vue')), -}, { - path: '/announcements', - component: page(() => import('./pages/announcements.vue')), -}, { - path: '/about', - component: page(() => import('./pages/about.vue')), - hash: 'initialTab', -}, { - path: '/about-misskey', - component: page(() => import('./pages/about-misskey.vue')), -}, { - path: '/theme-editor', - component: page(() => import('./pages/theme-editor.vue')), - loginRequired: true, -}, { - path: '/explore/tags/:tag', - component: page(() => import('./pages/explore.vue')), -}, { - path: '/explore', - component: page(() => import('./pages/explore.vue')), -}, { - path: '/search', - component: page(() => import('./pages/search.vue')), - query: { - q: 'query', - channel: 'channel', - }, -}, { - path: '/authorize-follow', - component: page(() => import('./pages/follow.vue')), - loginRequired: true, -}, { - path: '/share', - component: page(() => import('./pages/share.vue')), - loginRequired: true, -}, { - path: '/api-console', - component: page(() => import('./pages/api-console.vue')), - loginRequired: true, -}, { - path: '/mfm-cheat-sheet', - component: page(() => import('./pages/mfm-cheat-sheet.vue')), -}, { - path: '/scratchpad', - component: page(() => import('./pages/scratchpad.vue')), -}, { - path: '/preview', - component: page(() => import('./pages/preview.vue')), -}, { - path: '/auth/:token', - component: page(() => import('./pages/auth.vue')), -}, { - path: '/miauth/:session', - component: page(() => import('./pages/miauth.vue')), - query: { - callback: 'callback', - name: 'name', - icon: 'icon', - permission: 'permission', - }, -}, { - path: '/tags/:tag', - component: page(() => import('./pages/tag.vue')), -}, { - path: '/pages/new', - component: page(() => import('./pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages/edit/:initPageId', - component: page(() => import('./pages/page-editor/page-editor.vue')), - loginRequired: true, -}, { - path: '/pages', - component: page(() => import('./pages/pages.vue')), -}, { - path: '/gallery/:postId/edit', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/new', - component: page(() => import('./pages/gallery/edit.vue')), - loginRequired: true, -}, { - path: '/gallery/:postId', - component: page(() => import('./pages/gallery/post.vue')), -}, { - path: '/gallery', - component: page(() => import('./pages/gallery/index.vue')), -}, { - path: '/channels/:channelId/edit', - component: page(() => import('./pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/new', - component: page(() => import('./pages/channel-editor.vue')), - loginRequired: true, -}, { - path: '/channels/:channelId', - component: page(() => import('./pages/channel.vue')), -}, { - path: '/channels', - component: page(() => import('./pages/channels.vue')), -}, { - path: '/registry/keys/system/:path(*)?', - component: page(() => import('./pages/registry.keys.vue')), -}, { - path: '/registry/value/system/:path(*)?', - component: page(() => import('./pages/registry.value.vue')), -}, { - path: '/registry', - component: page(() => import('./pages/registry.vue')), -}, { - path: '/admin/file/:fileId', - component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), -}, { - path: '/admin', - component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')), - children: [{ - path: '/overview', - name: 'overview', - component: page(() => import('./pages/admin/overview.vue')), - }, { - path: '/users', - name: 'users', - component: page(() => import('./pages/admin/users.vue')), - }, { - path: '/emojis', - name: 'emojis', - component: page(() => import('./pages/admin/emojis.vue')), - }, { - path: '/queue', - name: 'queue', - component: page(() => import('./pages/admin/queue.vue')), - }, { - path: '/files', - name: 'files', - component: page(() => import('./pages/admin/files.vue')), - }, { - path: '/announcements', - name: 'announcements', - component: page(() => import('./pages/admin/announcements.vue')), - }, { - path: '/ads', - name: 'ads', - component: page(() => import('./pages/admin/ads.vue')), - }, { - path: '/database', - name: 'database', - component: page(() => import('./pages/admin/database.vue')), - }, { - path: '/abuses', - name: 'abuses', - component: page(() => import('./pages/admin/abuses.vue')), - }, { - path: '/settings', - name: 'settings', - component: page(() => import('./pages/admin/settings.vue')), - }, { - path: '/email-settings', - name: 'email-settings', - component: page(() => import('./pages/admin/email-settings.vue')), - }, { - path: '/object-storage', - name: 'object-storage', - component: page(() => import('./pages/admin/object-storage.vue')), - }, { - path: '/security', - name: 'security', - component: page(() => import('./pages/admin/security.vue')), - }, { - path: '/relays', - name: 'relays', - component: page(() => import('./pages/admin/relays.vue')), - }, { - path: '/integrations', - name: 'integrations', - component: page(() => import('./pages/admin/integrations.vue')), - }, { - path: '/instance-block', - name: 'instance-block', - component: page(() => import('./pages/admin/instance-block.vue')), - }, { - path: '/proxy-account', - name: 'proxy-account', - component: page(() => import('./pages/admin/proxy-account.vue')), - }, { - path: '/other-settings', - name: 'other-settings', - component: page(() => import('./pages/admin/other-settings.vue')), - }, { - path: '/', - component: page(() => import('./pages/_empty_.vue')), - }], -}, { - path: '/my/notifications', - component: page(() => import('./pages/notifications.vue')), - loginRequired: true, -}, { - path: '/my/favorites', - component: page(() => import('./pages/favorites.vue')), - loginRequired: true, -}, { - name: 'messaging', - path: '/my/messaging', - component: page(() => import('./pages/messaging/index.vue')), - loginRequired: true, -}, { - path: '/my/messaging/:userAcct', - component: page(() => import('./pages/messaging/messaging-room.vue')), - loginRequired: true, -}, { - path: '/my/messaging/group/:groupId', - component: page(() => import('./pages/messaging/messaging-room.vue')), - loginRequired: true, -}, { - path: '/my/drive/folder/:folder', - component: page(() => import('./pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/drive', - component: page(() => import('./pages/drive.vue')), - loginRequired: true, -}, { - path: '/my/follow-requests', - component: page(() => import('./pages/follow-requests.vue')), - loginRequired: true, -}, { - path: '/my/lists/:listId', - component: page(() => import('./pages/my-lists/list.vue')), - loginRequired: true, -}, { - path: '/my/lists', - component: page(() => import('./pages/my-lists/index.vue')), - loginRequired: true, -}, { - path: '/my/clips', - component: page(() => import('./pages/my-clips/index.vue')), - loginRequired: true, -}, { - path: '/my/antennas/create', - component: page(() => import('./pages/my-antennas/create.vue')), - loginRequired: true, -}, { - path: '/my/antennas/:antennaId', - component: page(() => import('./pages/my-antennas/edit.vue')), - loginRequired: true, -}, { - path: '/my/antennas', - component: page(() => import('./pages/my-antennas/index.vue')), - loginRequired: true, -}, { - path: '/timeline/list/:listId', - component: page(() => import('./pages/user-list-timeline.vue')), - loginRequired: true, -}, { - path: '/timeline/antenna/:antennaId', - component: page(() => import('./pages/antenna-timeline.vue')), - loginRequired: true, -}, { - name: 'index', - path: '/', - component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')), - globalCacheKey: 'index', -}, { - path: '/:(*)', - component: page(() => import('./pages/not-found.vue')), -}]; - -export const mainRouter = new Router(routes, location.pathname + location.search + location.hash); - -window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); - -// TODO: このファイルでスクロール位置も管理する設計だとdeckに対応できないのでなんとかする -// スクロール位置取得+スクロール位置設定関数をprovideする感じでも良いかも - -const scrollPosStore = new Map(); - -window.setInterval(() => { - scrollPosStore.set(window.history.state?.key, window.scrollY); -}, 1000); - -mainRouter.addListener('push', ctx => { - window.history.pushState({ key: ctx.key }, '', ctx.path); - const scrollPos = scrollPosStore.get(ctx.key) ?? 0; - window.scroll({ top: scrollPos, behavior: 'instant' }); - if (scrollPos !== 0) { - window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール - window.scroll({ top: scrollPos, behavior: 'instant' }); - }, 100); - } -}); - -mainRouter.addListener('replace', ctx => { - window.history.replaceState({ key: ctx.key }, '', ctx.path); -}); - -mainRouter.addListener('same', () => { - window.scroll({ top: 0, behavior: 'smooth' }); -}); - -window.addEventListener('popstate', (event) => { - mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key, false); - const scrollPos = scrollPosStore.get(event.state?.key) ?? 0; - window.scroll({ top: scrollPos, behavior: 'instant' }); - window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール - window.scroll({ top: scrollPos, behavior: 'instant' }); - }, 100); -}); - -export function useRouter(): Router { - return inject('router', null) ?? mainRouter; -} diff --git a/packages/client/src/scripts/2fa.ts b/packages/client/src/scripts/2fa.ts deleted file mode 100644 index 62a38ff02a..0000000000 --- a/packages/client/src/scripts/2fa.ts +++ /dev/null @@ -1,33 +0,0 @@ -export function byteify(string: string, encoding: 'ascii' | 'base64' | 'hex') { - switch (encoding) { - case 'ascii': - return Uint8Array.from(string, c => c.charCodeAt(0)); - case 'base64': - return Uint8Array.from( - atob( - string - .replace(/-/g, '+') - .replace(/_/g, '/'), - ), - c => c.charCodeAt(0), - ); - case 'hex': - return new Uint8Array( - string - .match(/.{1,2}/g) - .map(byte => parseInt(byte, 16)), - ); - } -} - -export function hexify(buffer: ArrayBuffer) { - return Array.from(new Uint8Array(buffer)) - .reduce( - (str, byte) => str + byte.toString(16).padStart(2, '0'), - '', - ); -} - -export function stringify(buffer: ArrayBuffer) { - return String.fromCharCode(... new Uint8Array(buffer)); -} diff --git a/packages/client/src/scripts/aiscript/api.ts b/packages/client/src/scripts/aiscript/api.ts deleted file mode 100644 index 6debcb8a13..0000000000 --- a/packages/client/src/scripts/aiscript/api.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { utils, values } from '@syuilo/aiscript'; -import * as os from '@/os'; -import { $i } from '@/account'; - -export function createAiScriptEnv(opts) { - let apiRequests = 0; - return { - USER_ID: $i ? values.STR($i.id) : values.NULL, - USER_NAME: $i ? values.STR($i.name) : values.NULL, - USER_USERNAME: $i ? values.STR($i.username) : values.NULL, - 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { - await os.alert({ - type: type ? type.value : 'info', - title: title.value, - text: text.value, - }); - }), - 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { - const confirm = await os.confirm({ - type: type ? type.value : 'question', - title: title.value, - text: text.value, - }); - return confirm.canceled ? values.FALSE : values.TRUE; - }), - 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { - if (token) utils.assertString(token); - apiRequests++; - if (apiRequests > 16) return values.NULL; - const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null)); - return utils.jsToVal(res); - }), - 'Mk:save': values.FN_NATIVE(([key, value]) => { - utils.assertString(key); - localStorage.setItem('aiscript:' + opts.storageKey + ':' + key.value, JSON.stringify(utils.valToJs(value))); - return values.NULL; - }), - 'Mk:load': values.FN_NATIVE(([key]) => { - utils.assertString(key); - return utils.jsToVal(JSON.parse(localStorage.getItem('aiscript:' + opts.storageKey + ':' + key.value))); - }), - }; -} diff --git a/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts deleted file mode 100644 index 4620c8b735..0000000000 --- a/packages/client/src/scripts/array.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { EndoRelation, Predicate } from './relation'; - -/** - * Count the number of elements that satisfy the predicate - */ - -export function countIf(f: Predicate, xs: T[]): number { - return xs.filter(f).length; -} - -/** - * Count the number of elements that is equal to the element - */ -export function count(a: T, xs: T[]): number { - return countIf(x => x === a, xs); -} - -/** - * Concatenate an array of arrays - */ -export function concat(xss: T[][]): T[] { - return ([] as T[]).concat(...xss); -} - -/** - * Intersperse the element between the elements of the array - * @param sep The element to be interspersed - */ -export function intersperse(sep: T, xs: T[]): T[] { - return concat(xs.map(x => [sep, x])).slice(1); -} - -/** - * Returns the array of elements that is not equal to the element - */ -export function erase(a: T, xs: T[]): T[] { - return xs.filter(x => x !== a); -} - -/** - * Finds the array of all elements in the first array not contained in the second array. - * The order of result values are determined by the first array. - */ -export function difference(xs: T[], ys: T[]): T[] { - return xs.filter(x => !ys.includes(x)); -} - -/** - * Remove all but the first element from every group of equivalent elements - */ -export function unique(xs: T[]): T[] { - return [...new Set(xs)]; -} - -export function uniqueBy(values: TValue[], keySelector: (value: TValue) => TKey): TValue[] { - const map = new Map(); - - for (const value of values) { - const key = keySelector(value); - if (!map.has(key)) map.set(key, value); - } - - return [...map.values()]; -} - -export function sum(xs: number[]): number { - return xs.reduce((a, b) => a + b, 0); -} - -export function maximum(xs: number[]): number { - return Math.max(...xs); -} - -/** - * Splits an array based on the equivalence relation. - * The concatenation of the result is equal to the argument. - */ -export function groupBy(f: EndoRelation, xs: T[]): T[][] { - const groups = [] as T[][]; - for (const x of xs) { - if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { - groups[groups.length - 1].push(x); - } else { - groups.push([x]); - } - } - return groups; -} - -/** - * Splits an array based on the equivalence relation induced by the function. - * The concatenation of the result is equal to the argument. - */ -export function groupOn(f: (x: T) => S, xs: T[]): T[][] { - return groupBy((a, b) => f(a) === f(b), xs); -} - -export function groupByX(collections: T[], keySelector: (x: T) => string) { - return collections.reduce((obj: Record, item: T) => { - const key = keySelector(item); - if (typeof obj[key] === 'undefined') { - obj[key] = []; - } - - obj[key].push(item); - - return obj; - }, {}); -} - -/** - * Compare two arrays by lexicographical order - */ -export function lessThan(xs: number[], ys: number[]): boolean { - for (let i = 0; i < Math.min(xs.length, ys.length); i++) { - if (xs[i] < ys[i]) return true; - if (xs[i] > ys[i]) return false; - } - return xs.length < ys.length; -} - -/** - * Returns the longest prefix of elements that satisfy the predicate - */ -export function takeWhile(f: Predicate, xs: T[]): T[] { - const ys: T[] = []; - for (const x of xs) { - if (f(x)) { - ys.push(x); - } else { - break; - } - } - return ys; -} - -export function cumulativeSum(xs: number[]): number[] { - const ys = Array.from(xs); // deep copy - for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1]; - return ys; -} - -export function toArray(x: T | T[] | undefined): T[] { - return Array.isArray(x) ? x : x != null ? [x] : []; -} - -export function toSingle(x: T | T[] | undefined): T | undefined { - return Array.isArray(x) ? x[0] : x; -} diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts deleted file mode 100644 index 1bae3790f5..0000000000 --- a/packages/client/src/scripts/autocomplete.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { nextTick, Ref, ref, defineAsyncComponent } from 'vue'; -import getCaretCoordinates from 'textarea-caret'; -import { toASCII } from 'punycode/'; -import { popup } from '@/os'; - -export class Autocomplete { - private suggestion: { - x: Ref; - y: Ref; - q: Ref; - close: () => void; - } | null; - private textarea: HTMLInputElement | HTMLTextAreaElement; - private currentType: string; - private textRef: Ref; - private opening: boolean; - - private get text(): string { - // Use raw .value to get the latest value - // (Because v-model does not update while composition) - return this.textarea.value; - } - - private set text(text: string) { - // Use ref value to notify other watchers - // (Because .value setter never fires input/change events) - this.textRef.value = text; - } - - /** - * 対象のテキストエリアを与えてインスタンスを初期化します。 - */ - constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref) { - //#region BIND - this.onInput = this.onInput.bind(this); - this.complete = this.complete.bind(this); - this.close = this.close.bind(this); - //#endregion - - this.suggestion = null; - this.textarea = textarea; - this.textRef = textRef; - this.opening = false; - - this.attach(); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 - */ - public attach() { - this.textarea.addEventListener('input', this.onInput); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 - */ - public detach() { - this.textarea.removeEventListener('input', this.onInput); - this.close(); - } - - /** - * テキスト入力時 - */ - private onInput() { - const caretPos = this.textarea.selectionStart; - const text = this.text.substr(0, caretPos).split('\n').pop()!; - - const mentionIndex = text.lastIndexOf('@'); - const hashtagIndex = text.lastIndexOf('#'); - const emojiIndex = text.lastIndexOf(':'); - const mfmTagIndex = text.lastIndexOf('$'); - - const max = Math.max( - mentionIndex, - hashtagIndex, - emojiIndex, - mfmTagIndex); - - if (max === -1) { - this.close(); - return; - } - - const isMention = mentionIndex !== -1; - const isHashtag = hashtagIndex !== -1; - const isMfmTag = mfmTagIndex !== -1; - const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); - - let opened = false; - - if (isMention) { - const username = text.substr(mentionIndex + 1); - if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) { - this.open('user', username); - opened = true; - } else if (username === '') { - this.open('user', null); - opened = true; - } - } - - if (isHashtag && !opened) { - const hashtag = text.substr(hashtagIndex + 1); - if (!hashtag.includes(' ')) { - this.open('hashtag', hashtag); - opened = true; - } - } - - if (isEmoji && !opened) { - const emoji = text.substr(emojiIndex + 1); - if (!emoji.includes(' ')) { - this.open('emoji', emoji); - opened = true; - } - } - - if (isMfmTag && !opened) { - const mfmTag = text.substr(mfmTagIndex + 1); - if (!mfmTag.includes(' ')) { - this.open('mfmTag', mfmTag.replace('[', '')); - opened = true; - } - } - - if (!opened) { - this.close(); - } - } - - /** - * サジェストを提示します。 - */ - private async open(type: string, q: string | null) { - if (type !== this.currentType) { - this.close(); - } - if (this.opening) return; - this.opening = true; - this.currentType = type; - - //#region サジェストを表示すべき位置を計算 - const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); - - const rect = this.textarea.getBoundingClientRect(); - - const x = rect.left + caretPosition.left - this.textarea.scrollLeft; - const y = rect.top + caretPosition.top - this.textarea.scrollTop; - //#endregion - - if (this.suggestion) { - this.suggestion.x.value = x; - this.suggestion.y.value = y; - this.suggestion.q.value = q; - - this.opening = false; - } else { - const _x = ref(x); - const _y = ref(y); - const _q = ref(q); - - const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), { - textarea: this.textarea, - close: this.close, - type: type, - q: _q, - x: _x, - y: _y, - }, { - done: (res) => { - this.complete(res); - }, - }); - - this.suggestion = { - q: _q, - x: _x, - y: _y, - close: () => dispose(), - }; - - this.opening = false; - } - } - - /** - * サジェストを閉じます。 - */ - private close() { - if (this.suggestion == null) return; - - this.suggestion.close(); - this.suggestion = null; - - this.textarea.focus(); - } - - /** - * オートコンプリートする - */ - private complete({ type, value }) { - this.close(); - - const caret = this.textarea.selectionStart; - - if (type === 'user') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('@')); - const after = source.substr(caret); - - const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; - - // 挿入 - this.text = `${trimmedBefore}@${acct} ${after}`; - - // キャレットを戻す - nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (acct.length + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type === 'hashtag') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('#')); - const after = source.substr(caret); - - // 挿入 - this.text = `${trimmedBefore}#${value} ${after}`; - - // キャレットを戻す - nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (value.length + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type === 'emoji') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf(':')); - const after = source.substr(caret); - - // 挿入 - this.text = trimmedBefore + value + after; - - // キャレットを戻す - nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + value.length; - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type === 'mfmTag') { - const source = this.text; - - const before = source.substr(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('$')); - const after = source.substr(caret); - - // 挿入 - this.text = `${trimmedBefore}$[${value} ]${after}`; - - // キャレットを戻す - nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (value.length + 3); - this.textarea.setSelectionRange(pos, pos); - }); - } - } -} diff --git a/packages/client/src/scripts/chart-vline.ts b/packages/client/src/scripts/chart-vline.ts deleted file mode 100644 index 8e3c4436b2..0000000000 --- a/packages/client/src/scripts/chart-vline.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const chartVLine = (vLineColor: string) => ({ - id: 'vLine', - beforeDraw(chart, args, options) { - if (chart.tooltip?._active?.length) { - const activePoint = chart.tooltip._active[0]; - const ctx = chart.ctx; - const x = activePoint.element.x; - const topY = chart.scales.y.top; - const bottomY = chart.scales.y.bottom; - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(x, bottomY); - ctx.lineTo(x, topY); - ctx.lineWidth = 1; - ctx.strokeStyle = vLineColor; - ctx.stroke(); - ctx.restore(); - } - }, -}); diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts deleted file mode 100644 index 35d40a6e08..0000000000 --- a/packages/client/src/scripts/check-word-mute.ts +++ /dev/null @@ -1,37 +0,0 @@ -export function checkWordMute(note: Record, me: Record | null | undefined, mutedWords: Array): boolean { - // 自分自身 - if (me && (note.userId === me.id)) return false; - - if (mutedWords.length > 0) { - const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); - - if (text === '') return false; - - const matched = mutedWords.some(filter => { - if (Array.isArray(filter)) { - // Clean up - const filteredFilter = filter.filter(keyword => keyword !== ''); - if (filteredFilter.length === 0) return false; - - return filteredFilter.every(keyword => text.includes(keyword)); - } else { - // represents RegExp - const regexp = filter.match(/^\/(.+)\/(.*)$/); - - // This should never happen due to input sanitisation. - if (!regexp) return false; - - try { - return new RegExp(regexp[1], regexp[2]).test(text); - } catch (err) { - // This should never happen due to input sanitisation. - return false; - } - } - }); - - if (matched) return true; - } - - return false; -} diff --git a/packages/client/src/scripts/clone.ts b/packages/client/src/scripts/clone.ts deleted file mode 100644 index 16fad24129..0000000000 --- a/packages/client/src/scripts/clone.ts +++ /dev/null @@ -1,18 +0,0 @@ -// structredCloneが遅いため -// SEE: http://var.blog.jp/archives/86038606.html - -type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; - -export function deepClone(x: T): T { - if (typeof x === 'object') { - if (x === null) return x; - if (Array.isArray(x)) return x.map(deepClone) as T; - const obj = {} as Record; - for (const [k, v] of Object.entries(x)) { - obj[k] = deepClone(v); - } - return obj as T; - } else { - return x; - } -} diff --git a/packages/client/src/scripts/collect-page-vars.ts b/packages/client/src/scripts/collect-page-vars.ts deleted file mode 100644 index 76b68beaf6..0000000000 --- a/packages/client/src/scripts/collect-page-vars.ts +++ /dev/null @@ -1,68 +0,0 @@ -interface StringPageVar { - name: string, - type: 'string', - value: string -} - -interface NumberPageVar { - name: string, - type: 'number', - value: number -} - -interface BooleanPageVar { - name: string, - type: 'boolean', - value: boolean -} - -type PageVar = StringPageVar | NumberPageVar | BooleanPageVar; - -export function collectPageVars(content): PageVar[] { - const pageVars: PageVar[] = []; - const collect = (xs: any[]): void => { - for (const x of xs) { - if (x.type === 'textInput') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '', - }); - } else if (x.type === 'textareaInput') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '', - }); - } else if (x.type === 'numberInput') { - pageVars.push({ - name: x.name, - type: 'number', - value: x.default || 0, - }); - } else if (x.type === 'switch') { - pageVars.push({ - name: x.name, - type: 'boolean', - value: x.default || false, - }); - } else if (x.type === 'counter') { - pageVars.push({ - name: x.name, - type: 'number', - value: 0, - }); - } else if (x.type === 'radioButton') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '', - }); - } else if (x.children) { - collect(x.children); - } - } - }; - collect(content); - return pageVars; -} diff --git a/packages/client/src/scripts/contains.ts b/packages/client/src/scripts/contains.ts deleted file mode 100644 index 256e09d293..0000000000 --- a/packages/client/src/scripts/contains.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default (parent, child, checkSame = true) => { - if (checkSame && parent === child) return true; - let node = child.parentNode; - while (node) { - if (node === parent) return true; - node = node.parentNode; - } - return false; -}; diff --git a/packages/client/src/scripts/copy-to-clipboard.ts b/packages/client/src/scripts/copy-to-clipboard.ts deleted file mode 100644 index ab13cab970..0000000000 --- a/packages/client/src/scripts/copy-to-clipboard.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Clipboardに値をコピー(TODO: 文字列以外も対応) - */ -export default val => { - // 空div 生成 - const tmp = document.createElement('div'); - // 選択用のタグ生成 - const pre = document.createElement('pre'); - - // 親要素のCSSで user-select: none だとコピーできないので書き換える - pre.style.webkitUserSelect = 'auto'; - pre.style.userSelect = 'auto'; - - tmp.appendChild(pre).textContent = val; - - // 要素を画面外へ - const s = tmp.style; - s.position = 'fixed'; - s.right = '200%'; - - // body に追加 - document.body.appendChild(tmp); - // 要素を選択 - document.getSelection().selectAllChildren(tmp); - - // クリップボードにコピー - const result = document.execCommand('copy'); - - // 要素削除 - document.body.removeChild(tmp); - - return result; -}; diff --git a/packages/client/src/scripts/device-kind.ts b/packages/client/src/scripts/device-kind.ts deleted file mode 100644 index 544cac0604..0000000000 --- a/packages/client/src/scripts/device-kind.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defaultStore } from '@/store'; - -const ua = navigator.userAgent.toLowerCase(); -const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); -const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); - -export const deviceKind = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind - : isSmartphone ? 'smartphone' - : isTablet ? 'tablet' - : 'desktop'; diff --git a/packages/client/src/scripts/emoji-base.ts b/packages/client/src/scripts/emoji-base.ts deleted file mode 100644 index 3f05642d57..0000000000 --- a/packages/client/src/scripts/emoji-base.ts +++ /dev/null @@ -1,20 +0,0 @@ -const twemojiSvgBase = '/twemoji'; -const fluentEmojiPngBase = '/fluent-emoji'; - -export function char2twemojiFilePath(char: string): string { - 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); - const fileName = codes.join('-'); - return `${twemojiSvgBase}/${fileName}.svg`; -} - -export function char2fluentEmojiFilePath(char: string): string { - let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); - // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 - if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); - return `${fluentEmojiPngBase}/${fileName}.png`; -} diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts deleted file mode 100644 index bc52fa7a43..0000000000 --- a/packages/client/src/scripts/emojilist.ts +++ /dev/null @@ -1,17 +0,0 @@ -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: typeof unicodeEmojiCategories[number]; -} - -// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from '../emojilist.json'; - -export const emojilist = _emojilist as UnicodeEmojiDef[]; - -export function getEmojiName(char: string): string | undefined { - return emojilist.find(emo => emo.char === char)?.name; -} diff --git a/packages/client/src/scripts/extract-avg-color-from-blurhash.ts b/packages/client/src/scripts/extract-avg-color-from-blurhash.ts deleted file mode 100644 index af517f2672..0000000000 --- a/packages/client/src/scripts/extract-avg-color-from-blurhash.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function extractAvgColorFromBlurhash(hash: string) { - return typeof hash === 'string' - ? '#' + [...hash.slice(2, 6)] - .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) - .reduce((a, c) => a * 83 + c, 0) - .toString(16) - .padStart(6, '0') - : undefined; -} diff --git a/packages/client/src/scripts/extract-mentions.ts b/packages/client/src/scripts/extract-mentions.ts deleted file mode 100644 index cc19b161a8..0000000000 --- a/packages/client/src/scripts/extract-mentions.ts +++ /dev/null @@ -1,11 +0,0 @@ -// test is located in test/extract-mentions - -import * as mfm from 'mfm-js'; - -export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { - // TODO: 重複を削除 - const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention'); - const mentions = mentionNodes.map(x => x.props); - - return mentions; -} diff --git a/packages/client/src/scripts/extract-url-from-mfm.ts b/packages/client/src/scripts/extract-url-from-mfm.ts deleted file mode 100644 index 34e3eb6c19..0000000000 --- a/packages/client/src/scripts/extract-url-from-mfm.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as mfm from 'mfm-js'; -import { unique } from '@/scripts/array'; - -// unique without hash -// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] -const removeHash = (x: string) => x.replace(/#[^#]*$/, ''); - -export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] { - const urlNodes = mfm.extract(nodes, (node) => { - return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent)); - }); - const urls: string[] = unique(urlNodes.map(x => x.props.url)); - - return urls.reduce((array, url) => { - const urlWithoutHash = removeHash(url); - if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url); - return array; - }, [] as string[]); -} diff --git a/packages/client/src/scripts/focus.ts b/packages/client/src/scripts/focus.ts deleted file mode 100644 index d6802fa322..0000000000 --- a/packages/client/src/scripts/focus.ts +++ /dev/null @@ -1,27 +0,0 @@ -export function focusPrev(el: Element | null, self = false, scroll = true) { - if (el == null) return; - if (!self) el = el.previousElementSibling; - if (el) { - if (el.hasAttribute('tabindex')) { - (el as HTMLElement).focus({ - preventScroll: !scroll, - }); - } else { - focusPrev(el.previousElementSibling, true); - } - } -} - -export function focusNext(el: Element | null, self = false, scroll = true) { - if (el == null) return; - if (!self) el = el.nextElementSibling; - if (el) { - if (el.hasAttribute('tabindex')) { - (el as HTMLElement).focus({ - preventScroll: !scroll, - }); - } else { - focusPrev(el.nextElementSibling, true); - } - } -} diff --git a/packages/client/src/scripts/form.ts b/packages/client/src/scripts/form.ts deleted file mode 100644 index 7f321cc0ae..0000000000 --- a/packages/client/src/scripts/form.ts +++ /dev/null @@ -1,59 +0,0 @@ -export type FormItem = { - label?: string; - type: 'string'; - default: string | null; - hidden?: boolean; - multiline?: boolean; -} | { - label?: string; - type: 'number'; - default: number | null; - hidden?: boolean; - step?: number; -} | { - label?: string; - type: 'boolean'; - default: boolean | null; - hidden?: boolean; -} | { - label?: string; - type: 'enum'; - default: string | null; - hidden?: boolean; - enum: string[]; -} | { - label?: string; - type: 'radio'; - default: unknown | null; - hidden?: boolean; - options: { - label: string; - value: unknown; - }[]; -} | { - label?: string; - type: 'object'; - default: Record | null; - hidden: true; -} | { - label?: string; - type: 'array'; - default: unknown[] | null; - hidden: true; -}; - -export type Form = Record; - -type GetItemType = - 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 - : never; - -export type GetFormResultType = { - [P in keyof F]: GetItemType; -}; diff --git a/packages/client/src/scripts/format-time-string.ts b/packages/client/src/scripts/format-time-string.ts deleted file mode 100644 index c20db5e827..0000000000 --- a/packages/client/src/scripts/format-time-string.ts +++ /dev/null @@ -1,50 +0,0 @@ -const defaultLocaleStringFormats: {[index: string]: string} = { - 'weekday': 'narrow', - 'era': 'narrow', - 'year': 'numeric', - 'month': 'numeric', - 'day': 'numeric', - 'hour': 'numeric', - 'minute': 'numeric', - 'second': 'numeric', - 'timeZoneName': 'short', -}; - -function formatLocaleString(date: Date, format: string): string { - return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => { - if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) { - return date.toLocaleString(window.navigator.language, { [kind]: option ? option : defaultLocaleStringFormats[kind] }); - } else { - return match; - } - }); -} - -export function formatDateTimeString(date: Date, format: string): string { - return format - .replace(/yyyy/g, date.getFullYear().toString()) - .replace(/yy/g, date.getFullYear().toString().slice(-2)) - .replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long' })) - .replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short' })) - .replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2)) - .replace(/M/g, (date.getMonth() + 1).toString()) - .replace(/dd/g, (`0${date.getDate()}`).slice(-2)) - .replace(/d/g, date.getDate().toString()) - .replace(/HH/g, (`0${date.getHours()}`).slice(-2)) - .replace(/H/g, date.getHours().toString()) - .replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2)) - .replace(/h/g, ((date.getHours() % 12) || 12).toString()) - .replace(/mm/g, (`0${date.getMinutes()}`).slice(-2)) - .replace(/m/g, date.getMinutes().toString()) - .replace(/ss/g, (`0${date.getSeconds()}`).slice(-2)) - .replace(/s/g, date.getSeconds().toString()) - .replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM'); -} - -export function formatTimeString(date: Date, format: string): string { - return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => { - if (localeformat) return formatLocaleString(date, localeformat); - if (datetimeformat) return formatDateTimeString(date, datetimeformat); - return match; - }); -} diff --git a/packages/client/src/scripts/gen-search-query.ts b/packages/client/src/scripts/gen-search-query.ts deleted file mode 100644 index da7d622632..0000000000 --- a/packages/client/src/scripts/gen-search-query.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as Acct from 'misskey-js/built/acct'; -import { host as localHost } from '@/config'; - -export async function genSearchQuery(v: any, q: string) { - let host: string; - let userId: string; - if (q.split(' ').some(x => x.startsWith('@'))) { - for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) { - if (at.includes('.')) { - if (at === localHost || at === '.') { - host = null; - } else { - host = at; - } - } else { - const user = await v.os.api('users/show', Acct.parse(at)).catch(x => null); - if (user) { - userId = user.id; - } else { - // todo: show error - } - } - } - } - return { - query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '), - host: host, - userId: userId, - }; -} diff --git a/packages/client/src/scripts/get-account-from-id.ts b/packages/client/src/scripts/get-account-from-id.ts deleted file mode 100644 index 1da897f176..0000000000 --- a/packages/client/src/scripts/get-account-from-id.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { get } from '@/scripts/idb-proxy'; - -export async function getAccountFromId(id: string) { - const accounts = await get('accounts') as { token: string; id: string; }[]; - if (!accounts) console.log('Accounts are not recorded'); - return accounts.find(account => account.id === id); -} diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts deleted file mode 100644 index 7656770894..0000000000 --- a/packages/client/src/scripts/get-note-menu.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { defineAsyncComponent, Ref, inject } from 'vue'; -import * as misskey from 'misskey-js'; -import { pleaseLogin } from './please-login'; -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 { notePage } from '@/filters/note'; - -export function getNoteMenu(props: { - note: misskey.entities.Note; - menuButton: Ref; - translation: Ref; - translating: Ref; - isDeleted: Ref; - currentClipPage?: Ref; -}) { - const isRenote = ( - props.note.renote != null && - props.note.text == null && - props.note.fileIds.length === 0 && - props.note.poll == null - ); - - const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note; - - function del(): void { - os.confirm({ - type: 'warning', - text: i18n.ts.noteDeleteConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - os.api('notes/delete', { - noteId: appearNote.id, - }); - }); - } - - function delEdit(): void { - os.confirm({ - type: 'warning', - text: i18n.ts.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 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, res => { - if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { - os.alert({ - type: 'error', - text: i18n.ts.pinLimitExceeded, - }); - } - }); - } - - async function clip(): Promise { - const clips = await os.api('clips/list'); - os.popupMenu([{ - icon: 'ti ti-plus', - text: i18n.ts.createNew, - action: async () => { - const { canceled, result } = await os.form(i18n.ts.createNewClip, { - name: { - type: 'string', - label: i18n.ts.name, - }, - description: { - type: 'string', - required: false, - multiline: true, - label: i18n.ts.description, - }, - isPublic: { - type: 'boolean', - label: i18n.ts.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.promiseDialog( - os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), - null, - async (err) => { - if (err.id === '734806c4-542c-463a-9311-15c512803965') { - const confirm = await os.confirm({ - type: 'warning', - text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), - }); - if (!confirm.canceled) { - os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); - if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true; - } - } else { - os.alert({ - type: 'error', - text: err.message + '\n' + err.id, - }); - } - }, - ); - }, - }))], props.menuButton.value, { - }).then(focus); - } - - async function unclip(): Promise { - os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id }); - props.isDeleted.value = true; - } - - async function promote(): Promise { - const { canceled, result: days } = await os.inputNumber({ - title: i18n.ts.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}`, - }); - } - function notedetails(): void { - os.pageWindow(`/notes/${appearNote.id}`); - } - async function translate(): Promise { - 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 = [ - ...( - props.currentClipPage?.value.userId === $i.id ? [{ - icon: 'ti ti-backspace', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, null] : [] - ), { - icon: 'ti ti-external-link', - text: i18n.ts.details, - action: notedetails, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, { - icon: 'ti ti-link', - text: i18n.ts.copyLink, - action: copyLink, - }, (appearNote.url || appearNote.uri) ? { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url || appearNote.uri, '_blank'); - }, - } : undefined, - { - icon: 'ti ti-share', - text: i18n.ts.share, - action: share, - }, - instance.translatorAvailable ? { - icon: 'ti ti-language-hiragana', - text: i18n.ts.translate, - action: translate, - } : undefined, - null, - statePromise.then(state => state.isFavorited ? { - icon: 'ti ti-star-off', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'ti ti-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - }), - { - icon: 'ti ti-paperclip', - text: i18n.ts.clip, - action: () => clip(), - }, - statePromise.then(state => state.isMutedThread ? { - icon: 'ti ti-message-off', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false), - } : { - icon: 'ti ti-message-off', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true), - }), - appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => togglePin(false), - } : { - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => togglePin(true), - } : undefined, - /* - ...($i.isModerator || $i.isAdmin ? [ - null, - { - icon: 'fas fa-bullhorn', - text: i18n.ts.promote, - action: promote - }] - : [] - ),*/ - ...(appearNote.userId !== $i.id ? [ - null, - { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: () => { - const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; - os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { - user: appearNote.user, - initialComment: `Note: ${u}\n-----\n`, - }, {}, 'closed'); - }, - }] - : [] - ), - ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ - null, - appearNote.userId === $i.id ? { - icon: 'ti ti-edit', - text: i18n.ts.deleteAndEdit, - action: delEdit, - } : undefined, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: del, - }] - : [] - )] - .filter(x => x !== undefined); - } else { - menu = [{ - icon: 'ti ti-external-link', - text: i18n.ts.detailed, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, { - icon: 'ti ti-link', - text: i18n.ts.copyLink, - action: copyLink, - }, (appearNote.url || appearNote.uri) ? { - icon: 'ti ti-external-link', - text: i18n.ts.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: 'ti ti-plug', - text: action.title, - action: () => { - action.handler(appearNote); - }, - }))]); - } - - return menu; -} diff --git a/packages/client/src/scripts/get-note-summary.ts b/packages/client/src/scripts/get-note-summary.ts deleted file mode 100644 index d57e1c3029..0000000000 --- a/packages/client/src/scripts/get-note-summary.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as misskey from 'misskey-js'; -import { i18n } from '@/i18n'; - -/** - * 投稿を表す文字列を取得します。 - * @param {*} note (packされた)投稿 - */ -export const getNoteSummary = (note: misskey.entities.Note): string => { - if (note.deletedAt) { - return `(${i18n.ts.deletedNote})`; - } - - if (note.isHidden) { - return `(${i18n.ts.invisibleNote})`; - } - - let summary = ''; - - // 本文 - if (note.cw != null) { - summary += note.cw; - } else { - summary += note.text ? note.text : ''; - } - - // ファイルが添付されているとき - if ((note.files || []).length !== 0) { - summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`; - } - - // 投票が添付されているとき - if (note.poll) { - summary += ` (${i18n.ts.poll})`; - } - - // 返信のとき - if (note.replyId) { - if (note.reply) { - summary += `\n\nRE: ${getNoteSummary(note.reply)}`; - } else { - summary += '\n\nRE: ...'; - } - } - - // Renoteのとき - if (note.renoteId) { - if (note.renote) { - summary += `\n\nRN: ${getNoteSummary(note.renote)}`; - } else { - summary += '\n\nRN: ...'; - } - } - - return summary.trim(); -}; diff --git a/packages/client/src/scripts/get-static-image-url.ts b/packages/client/src/scripts/get-static-image-url.ts deleted file mode 100644 index cbd1761983..0000000000 --- a/packages/client/src/scripts/get-static-image-url.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { url as instanceUrl } from '@/config'; -import * as url from '@/scripts/url'; - -export function getStaticImageUrl(baseUrl: string): string { - const u = new URL(baseUrl); - if (u.href.startsWith(`${instanceUrl}/proxy/`)) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; - } - - // 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する - const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`; - - return `${instanceUrl}/proxy/${dummy}?${url.query({ - url: u.href, - static: '1', - })}`; -} diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts deleted file mode 100644 index 2faacffdfc..0000000000 --- a/packages/client/src/scripts/get-user-menu.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as Acct from 'misskey-js/built/acct'; -import { defineAsyncComponent } from 'vue'; -import { i18n } from '@/i18n'; -import copyToClipboard from '@/scripts/copy-to-clipboard'; -import { host } from '@/config'; -import * as os from '@/os'; -import { userActions } from '@/store'; -import { $i, iAmModerator } from '@/account'; -import { mainRouter } from '@/router'; -import { Router } from '@/nirax'; - -export function getUserMenu(user, router: Router = mainRouter) { - const meId = $i ? $i.id : null; - - async function pushList() { - const t = i18n.ts.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく - const lists = await os.api('users/lists/list'); - if (lists.length === 0) { - os.alert({ - type: 'error', - text: i18n.ts.youHaveNoLists, - }); - return; - } - const { canceled, result: listId } = await os.select({ - title: t, - items: lists.map(list => ({ - value: list.id, text: list.name, - })), - }); - if (canceled) return; - os.apiWithDialog('users/lists/push', { - listId: listId, - userId: user.id, - }); - } - - async function inviteGroup() { - const groups = await os.api('users/groups/owned'); - if (groups.length === 0) { - os.alert({ - type: 'error', - text: i18n.ts.youHaveNoGroups, - }); - return; - } - const { canceled, result: groupId } = await os.select({ - title: i18n.ts.group, - items: groups.map(group => ({ - value: group.id, text: group.name, - })), - }); - if (canceled) return; - os.apiWithDialog('users/groups/invite', { - groupId: groupId, - userId: user.id, - }); - } - - async function toggleMute() { - if (user.isMuted) { - os.apiWithDialog('mute/delete', { - userId: user.id, - }).then(() => { - user.isMuted = false; - }); - } else { - const { canceled, result: period } = await os.select({ - title: i18n.ts.mutePeriod, - items: [{ - value: 'indefinitely', text: i18n.ts.indefinitely, - }, { - value: 'tenMinutes', text: i18n.ts.tenMinutes, - }, { - value: 'oneHour', text: i18n.ts.oneHour, - }, { - value: 'oneDay', text: i18n.ts.oneDay, - }, { - value: 'oneWeek', text: i18n.ts.oneWeek, - }], - default: 'indefinitely', - }); - if (canceled) return; - - const expiresAt = period === 'indefinitely' ? null - : period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10) - : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : null; - - os.apiWithDialog('mute/create', { - userId: user.id, - expiresAt, - }).then(() => { - user.isMuted = true; - }); - } - } - - async function toggleBlock() { - if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return; - - os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { - userId: user.id, - }).then(() => { - user.isBlocking = !user.isBlocking; - }); - } - - async function toggleSilence() { - if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return; - - os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { - userId: user.id, - }).then(() => { - user.isSilenced = !user.isSilenced; - }); - } - - async function toggleSuspend() { - if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; - - os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { - userId: user.id, - }).then(() => { - user.isSuspended = !user.isSuspended; - }); - } - - function reportAbuse() { - os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { - user: user, - }, {}, 'closed'); - } - - async function getConfirmed(text: string): Promise { - const confirm = await os.confirm({ - type: 'warning', - title: 'confirm', - text, - }); - - return !confirm.canceled; - } - - async function invalidateFollow() { - os.apiWithDialog('following/invalidate', { - userId: user.id, - }).then(() => { - user.isFollowed = !user.isFollowed; - }); - } - - let menu = [{ - icon: 'ti ti-at', - text: i18n.ts.copyUsername, - action: () => { - copyToClipboard(`@${user.username}@${user.host || host}`); - }, - }, { - icon: 'ti ti-rss', - text: i18n.ts.copyRSS, - action: () => { - copyToClipboard(`${user.host || host}/@${user.username}.atom`); - } - }, { - icon: 'ti ti-info-circle', - text: i18n.ts.info, - action: () => { - router.push(`/user-info/${user.id}`); - }, - }, { - icon: 'ti ti-mail', - text: i18n.ts.sendMessage, - action: () => { - os.post({ specified: user }); - }, - }, meId !== user.id ? { - type: 'link', - icon: 'ti ti-messages', - text: i18n.ts.startMessaging, - to: '/my/messaging/' + Acct.toString(user), - } : undefined, null, { - icon: 'ti ti-list', - text: i18n.ts.addToList, - action: pushList, - }, meId !== user.id ? { - icon: 'ti ti-users', - text: i18n.ts.inviteToGroup, - action: inviteGroup, - } : undefined] as any; - - if ($i && meId !== user.id) { - menu = menu.concat([null, { - icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', - text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, - action: toggleMute, - }, { - icon: 'ti ti-ban', - text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, - action: toggleBlock, - }]); - - if (user.isFollowed) { - menu = menu.concat([{ - icon: 'ti ti-link-off', - text: i18n.ts.breakFollow, - action: invalidateFollow, - }]); - } - - menu = menu.concat([null, { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }]); - - if (iAmModerator) { - menu = menu.concat([null, { - icon: 'ti ti-microphone-2-off', - text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence, - action: toggleSilence, - }, { - icon: 'ti ti-snowflake', - text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend, - action: toggleSuspend, - }]); - } - } - - if ($i && meId === user.id) { - menu = menu.concat([null, { - icon: 'ti ti-pencil', - text: i18n.ts.editProfile, - action: () => { - router.push('/settings/profile'); - }, - }]); - } - - if (userActions.length > 0) { - menu = menu.concat([null, ...userActions.map(action => ({ - icon: 'ti ti-plug', - text: action.title, - action: () => { - action.handler(user); - }, - }))]); - } - - return menu; -} diff --git a/packages/client/src/scripts/get-user-name.ts b/packages/client/src/scripts/get-user-name.ts deleted file mode 100644 index d499ea0203..0000000000 --- a/packages/client/src/scripts/get-user-name.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function(user: { name?: string | null, username: string }): string { - return user.name || user.username; -} diff --git a/packages/client/src/scripts/hotkey.ts b/packages/client/src/scripts/hotkey.ts deleted file mode 100644 index 4a0ded637d..0000000000 --- a/packages/client/src/scripts/hotkey.ts +++ /dev/null @@ -1,90 +0,0 @@ -import keyCode from './keycode'; - -type Callback = (ev: KeyboardEvent) => void; - -type Keymap = Record; - -type Pattern = { - which: string[]; - ctrl?: boolean; - shift?: boolean; - alt?: boolean; -}; - -type Action = { - patterns: Pattern[]; - callback: Callback; - allowRepeat: boolean; -}; - -const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { - const result = { - patterns: [], - callback, - allowRepeat: true, - } as Action; - - if (patterns.match(/^\(.*\)$/) !== null) { - result.allowRepeat = false; - patterns = patterns.slice(1, -1); - } - - result.patterns = patterns.split('|').map(part => { - const pattern = { - which: [], - ctrl: false, - alt: false, - shift: false, - } as Pattern; - - const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); - for (const key of keys) { - switch (key) { - case 'ctrl': pattern.ctrl = true; break; - case 'alt': pattern.alt = true; break; - case 'shift': pattern.shift = true; break; - default: pattern.which = keyCode(key).map(k => k.toLowerCase()); - } - } - - return pattern; - }); - - return result; -}); - -const ignoreElemens = ['input', 'textarea']; - -function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean { - const key = ev.code.toLowerCase(); - return patterns.some(pattern => pattern.which.includes(key) && - pattern.ctrl === ev.ctrlKey && - pattern.shift === ev.shiftKey && - pattern.alt === ev.altKey && - !ev.metaKey, - ); -} - -export const makeHotkey = (keymap: Keymap) => { - const actions = parseKeymap(keymap); - - return (ev: KeyboardEvent) => { - if (document.activeElement) { - if (ignoreElemens.some(el => document.activeElement!.matches(el))) return; - if (document.activeElement.attributes['contenteditable']) return; - } - - for (const action of actions) { - const matched = match(ev, action.patterns); - - if (matched) { - if (!action.allowRepeat && ev.repeat) return; - - ev.preventDefault(); - ev.stopPropagation(); - action.callback(ev); - break; - } - } - }; -}; diff --git a/packages/client/src/scripts/hpml/block.ts b/packages/client/src/scripts/hpml/block.ts deleted file mode 100644 index 804c5c1124..0000000000 --- a/packages/client/src/scripts/hpml/block.ts +++ /dev/null @@ -1,109 +0,0 @@ -// blocks - -export type BlockBase = { - id: string; - type: string; -}; - -export type TextBlock = BlockBase & { - type: 'text'; - text: string; -}; - -export type SectionBlock = BlockBase & { - type: 'section'; - title: string; - children: (Block | VarBlock)[]; -}; - -export type ImageBlock = BlockBase & { - type: 'image'; - fileId: string | null; -}; - -export type ButtonBlock = BlockBase & { - type: 'button'; - text: any; - primary: boolean; - action: string; - content: string; - event: string; - message: string; - var: string; - fn: string; -}; - -export type IfBlock = BlockBase & { - type: 'if'; - var: string; - children: Block[]; -}; - -export type TextareaBlock = BlockBase & { - type: 'textarea'; - text: string; -}; - -export type PostBlock = BlockBase & { - type: 'post'; - text: string; - attachCanvasImage: boolean; - canvasId: string; -}; - -export type CanvasBlock = BlockBase & { - type: 'canvas'; - name: string; // canvas id - width: number; - height: number; -}; - -export type NoteBlock = BlockBase & { - type: 'note'; - detailed: boolean; - note: string | null; -}; - -export type Block = - TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock; - -// variable blocks - -export type VarBlockBase = BlockBase & { - name: string; -}; - -export type NumberInputVarBlock = VarBlockBase & { - type: 'numberInput'; - text: string; -}; - -export type TextInputVarBlock = VarBlockBase & { - type: 'textInput'; - text: string; -}; - -export type SwitchVarBlock = VarBlockBase & { - type: 'switch'; - text: string; -}; - -export type RadioButtonVarBlock = VarBlockBase & { - type: 'radioButton'; - title: string; - values: string[]; -}; - -export type CounterVarBlock = VarBlockBase & { - type: 'counter'; - text: string; - inc: number; -}; - -export type VarBlock = - NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock; - -const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter']; -export function isVarBlock(block: Block): block is VarBlock { - return varBlock.includes(block.type); -} diff --git a/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts deleted file mode 100644 index 196b3142a1..0000000000 --- a/packages/client/src/scripts/hpml/evaluator.ts +++ /dev/null @@ -1,232 +0,0 @@ -import autobind from 'autobind-decorator'; -import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.'; -import { version } from '@/config'; -import { AiScript, utils, values } from '@syuilo/aiscript'; -import { createAiScriptEnv } from '../aiscript/api'; -import { collectPageVars } from '../collect-page-vars'; -import { initHpmlLib, initAiLib } from './lib'; -import * as os from '@/os'; -import { markRaw, ref, Ref, unref } from 'vue'; -import { Expr, isLiteralValue, Variable } from './expr'; - -/** - * Hpml evaluator - */ -export class Hpml { - private variables: Variable[]; - private pageVars: PageVar[]; - private envVars: Record; - public aiscript?: AiScript; - public pageVarUpdatedCallback?: values.VFn; - public canvases: Record = {}; - public vars: Ref> = ref({}); - public page: Record; - - private opts: { - randomSeed: string; visitor?: any; url?: string; - enableAiScript: boolean; - }; - - constructor(page: Hpml['page'], opts: Hpml['opts']) { - this.page = page; - this.variables = this.page.variables; - this.pageVars = collectPageVars(this.page.content); - this.opts = opts; - - if (this.opts.enableAiScript) { - this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({ - storageKey: 'pages:' + this.page.id, - }), ...initAiLib(this) }, { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - console.log(value); - }, - log: (type, params) => { - }, - })); - - this.aiscript.scope.opts.onUpdated = (name, value) => { - this.eval(); - }; - } - - const date = new Date(); - - this.envVars = { - AI: 'kawaii', - VERSION: version, - URL: this.page ? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}` : '', - LOGIN: opts.visitor != null, - NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '', - USERNAME: opts.visitor ? opts.visitor.username : '', - USERID: opts.visitor ? opts.visitor.id : '', - NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0, - FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0, - FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0, - IS_CAT: opts.visitor ? opts.visitor.isCat : false, - SEED: opts.randomSeed ? opts.randomSeed : '', - YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`, - AISCRIPT_DISABLED: !this.opts.enableAiScript, - NULL: null, - }; - - this.eval(); - } - - @autobind - public eval() { - try { - this.vars.value = this.evaluateVars(); - } catch (err) { - //this.onError(e); - } - } - - @autobind - public interpolate(str: string) { - if (str == null) return null; - return str.replace(/{(.+?)}/g, match => { - const v = unref(this.vars)[match.slice(1, -1).trim()]; - return v == null ? 'NULL' : v.toString(); - }); - } - - @autobind - public callAiScript(fn: string) { - try { - if (this.aiscript) this.aiscript.execFn(this.aiscript.scope.get(fn), []); - } catch (err) {} - } - - @autobind - public registerCanvas(id: string, canvas: any) { - this.canvases[id] = canvas; - } - - @autobind - public updatePageVar(name: string, value: any) { - const pageVar = this.pageVars.find(v => v.name === name); - if (pageVar !== undefined) { - pageVar.value = value; - if (this.pageVarUpdatedCallback) { - if (this.aiscript) this.aiscript.execFn(this.pageVarUpdatedCallback, [values.STR(name), utils.jsToVal(value)]); - } - } else { - throw new HpmlError(`No such page var '${name}'`); - } - } - - @autobind - public updateRandomSeed(seed: string) { - this.opts.randomSeed = seed; - this.envVars.SEED = seed; - } - - @autobind - private _interpolateScope(str: string, scope: HpmlScope) { - return str.replace(/{(.+?)}/g, match => { - const v = scope.getState(match.slice(1, -1).trim()); - return v == null ? 'NULL' : v.toString(); - }); - } - - @autobind - public evaluateVars(): Record { - const values: Record = {}; - - for (const [k, v] of Object.entries(this.envVars)) { - values[k] = v; - } - - for (const v of this.pageVars) { - values[v.name] = v.value; - } - - for (const v of this.variables) { - values[v.name] = this.evaluate(v, new HpmlScope([values])); - } - - return values; - } - - @autobind - private evaluate(expr: Expr, scope: HpmlScope): any { - if (isLiteralValue(expr)) { - if (expr.type === null) { - return null; - } - - if (expr.type === 'number') { - return parseInt((expr.value as any), 10); - } - - if (expr.type === 'text' || expr.type === 'multiLineText') { - return this._interpolateScope(expr.value || '', scope); - } - - if (expr.type === 'textList') { - return this._interpolateScope(expr.value || '', scope).trim().split('\n'); - } - - if (expr.type === 'ref') { - return scope.getState(expr.value); - } - - if (expr.type === 'aiScriptVar') { - if (this.aiscript) { - try { - return utils.valToJs(this.aiscript.scope.get(expr.value)); - } catch (err) { - return null; - } - } else { - return null; - } - } - - // Define user function - if (expr.type === 'fn') { - return { - slots: expr.value.slots.map(x => x.name), - exec: (slotArg: Record) => { - return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id)); - }, - } as Fn; - } - return; - } - - // Call user function - if (expr.type.startsWith('fn:')) { - const fnName = expr.type.split(':')[1]; - const fn = scope.getState(fnName); - const args = {} as Record; - for (let i = 0; i < fn.slots.length; i++) { - const name = fn.slots[i]; - args[name] = this.evaluate(expr.args[i], scope); - } - return fn.exec(args); - } - - if (expr.args === undefined) return null; - - const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor); - - // Call function - const fnName = expr.type; - const fn = (funcs as any)[fnName]; - if (fn == null) { - throw new HpmlError(`No such function '${fnName}'`); - } else { - return fn(...expr.args.map(x => this.evaluate(x, scope))); - } - } -} diff --git a/packages/client/src/scripts/hpml/expr.ts b/packages/client/src/scripts/hpml/expr.ts deleted file mode 100644 index 18c7c2a14b..0000000000 --- a/packages/client/src/scripts/hpml/expr.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { literalDefs, Type } from '.'; - -export type ExprBase = { - id: string; -}; - -// value - -export type EmptyValue = ExprBase & { - type: null; - value: null; -}; - -export type TextValue = ExprBase & { - type: 'text'; - value: string; -}; - -export type MultiLineTextValue = ExprBase & { - type: 'multiLineText'; - value: string; -}; - -export type TextListValue = ExprBase & { - type: 'textList'; - value: string; -}; - -export type NumberValue = ExprBase & { - type: 'number'; - value: number; -}; - -export type RefValue = ExprBase & { - type: 'ref'; - value: string; // value is variable name -}; - -export type AiScriptRefValue = ExprBase & { - type: 'aiScriptVar'; - value: string; // value is variable name -}; - -export type UserFnValue = ExprBase & { - type: 'fn'; - value: UserFnInnerValue; -}; -type UserFnInnerValue = { - slots: { - name: string; - type: Type; - }[]; - expression: Expr; -}; - -export type Value = - EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue; - -export function isLiteralValue(expr: Expr): expr is Value { - if (expr.type == null) return true; - if (literalDefs[expr.type]) return true; - return false; -} - -// call function - -export type CallFn = ExprBase & { // "fn:hoge" or string - type: string; - args: Expr[]; - value: null; -}; - -// variable -export type Variable = (Value | CallFn) & { - name: string; -}; - -// expression -export type Expr = Variable | Value | CallFn; diff --git a/packages/client/src/scripts/hpml/index.ts b/packages/client/src/scripts/hpml/index.ts deleted file mode 100644 index 9a55a5c286..0000000000 --- a/packages/client/src/scripts/hpml/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Hpml - */ - -import autobind from 'autobind-decorator'; -import { Hpml } from './evaluator'; -import { funcDefs } from './lib'; - -export type Fn = { - slots: string[]; - exec: (args: Record) => ReturnType; -}; - -export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null; - -export const literalDefs: Record = { - text: { out: 'string', category: 'value', icon: 'ti ti-quote' }, - multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left' }, - textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list' }, - number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up' }, - ref: { out: null, category: 'value', icon: 'fas fa-magic' }, - aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic' }, - fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt' }, -}; - -export const blockDefs = [ - ...Object.entries(literalDefs).map(([k, v]) => ({ - type: k, out: v.out, category: v.category, icon: v.icon, - })), - ...Object.entries(funcDefs).map(([k, v]) => ({ - type: k, out: v.out, category: v.category, icon: v.icon, - })), -]; - -export type PageVar = { name: string; value: any; type: Type; }; - -export const envVarsDef: Record = { - AI: 'string', - URL: 'string', - VERSION: 'string', - LOGIN: 'boolean', - NAME: 'string', - USERNAME: 'string', - USERID: 'string', - NOTES_COUNT: 'number', - FOLLOWERS_COUNT: 'number', - FOLLOWING_COUNT: 'number', - IS_CAT: 'boolean', - SEED: null, - YMD: 'string', - AISCRIPT_DISABLED: 'boolean', - NULL: null, -}; - -export class HpmlScope { - private layerdStates: Record[]; - public name: string; - - constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) { - this.layerdStates = layerdStates; - this.name = name || 'anonymous'; - } - - @autobind - public createChildScope(states: Record, name?: HpmlScope['name']): HpmlScope { - const layer = [states, ...this.layerdStates]; - return new HpmlScope(layer, name); - } - - /** - * 指定した名前の変数の値を取得します - * @param name 変数名 - */ - @autobind - public getState(name: string): any { - for (const later of this.layerdStates) { - const state = later[name]; - if (state !== undefined) { - return state; - } - } - - throw new HpmlError( - `No such variable '${name}' in scope '${this.name}'`, { - scope: this.layerdStates, - }); - } -} - -export class HpmlError extends Error { - public info?: any; - - constructor(message: string, info?: any) { - super(message); - - this.info = info; - - // Maintains proper stack trace for where our error was thrown (only available on V8) - if (Error.captureStackTrace) { - Error.captureStackTrace(this, HpmlError); - } - } -} diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts deleted file mode 100644 index b684876a7f..0000000000 --- a/packages/client/src/scripts/hpml/lib.ts +++ /dev/null @@ -1,247 +0,0 @@ -import tinycolor from 'tinycolor2'; -import { Hpml } from './evaluator'; -import { values, utils } from '@syuilo/aiscript'; -import { Fn, HpmlScope } from '.'; -import { Expr } from './expr'; -import seedrandom from 'seedrandom'; - -/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color -// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs -Chart.pluginService.register({ - beforeDraw: (chart, easing) => { - if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) { - const ctx = chart.chart.ctx; - ctx.save(); - ctx.fillStyle = chart.config.options.chartArea.backgroundColor; - ctx.fillRect(0, 0, chart.chart.width, chart.chart.height); - ctx.restore(); - } - } -}); -*/ - -export function initAiLib(hpml: Hpml) { - return { - 'MkPages:updated': values.FN_NATIVE(([callback]) => { - hpml.pageVarUpdatedCallback = (callback as values.VFn); - }), - 'MkPages:get_canvas': values.FN_NATIVE(([id]) => { - utils.assertString(id); - const canvas = hpml.canvases[id.value]; - const ctx = canvas.getContext('2d'); - return values.OBJ(new Map([ - ['clear_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.clearRect(x.value, y.value, width.value, height.value); })], - ['fill_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.fillRect(x.value, y.value, width.value, height.value); })], - ['stroke_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.strokeRect(x.value, y.value, width.value, height.value); })], - ['fill_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.fillText(text.value, x.value, y.value, width ? width.value : undefined); })], - ['stroke_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.strokeText(text.value, x.value, y.value, width ? width.value : undefined); })], - ['set_line_width', values.FN_NATIVE(([width]) => { ctx.lineWidth = width.value; })], - ['set_font', values.FN_NATIVE(([font]) => { ctx.font = font.value; })], - ['set_fill_style', values.FN_NATIVE(([style]) => { ctx.fillStyle = style.value; })], - ['set_stroke_style', values.FN_NATIVE(([style]) => { ctx.strokeStyle = style.value; })], - ['begin_path', values.FN_NATIVE(() => { ctx.beginPath(); })], - ['close_path', values.FN_NATIVE(() => { ctx.closePath(); })], - ['move_to', values.FN_NATIVE(([x, y]) => { ctx.moveTo(x.value, y.value); })], - ['line_to', values.FN_NATIVE(([x, y]) => { ctx.lineTo(x.value, y.value); })], - ['arc', values.FN_NATIVE(([x, y, radius, startAngle, endAngle]) => { ctx.arc(x.value, y.value, radius.value, startAngle.value, endAngle.value); })], - ['rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.rect(x.value, y.value, width.value, height.value); })], - ['fill', values.FN_NATIVE(() => { ctx.fill(); })], - ['stroke', values.FN_NATIVE(() => { ctx.stroke(); })], - ])); - }), - 'MkPages:chart': values.FN_NATIVE(([id, opts]) => { - /* TODO - utils.assertString(id); - utils.assertObject(opts); - const canvas = hpml.canvases[id.value]; - const color = getComputedStyle(document.documentElement).getPropertyValue('--accent'); - Chart.defaults.color = '#555'; - const chart = new Chart(canvas, { - type: opts.value.get('type').value, - data: { - labels: opts.value.get('labels').value.map(x => x.value), - datasets: opts.value.get('datasets').value.map(x => ({ - label: x.value.has('label') ? x.value.get('label').value : '', - data: x.value.get('data').value.map(x => x.value), - pointRadius: 0, - lineTension: 0, - borderWidth: 2, - borderColor: x.value.has('color') ? x.value.get('color') : color, - backgroundColor: tinycolor(x.value.has('color') ? x.value.get('color') : color).setAlpha(0.1).toRgbString(), - })) - }, - options: { - responsive: false, - devicePixelRatio: 1.5, - title: { - display: opts.value.has('title'), - text: opts.value.has('title') ? opts.value.get('title').value : '', - fontSize: 14, - }, - layout: { - padding: { - left: 32, - right: 32, - top: opts.value.has('title') ? 16 : 32, - bottom: 16 - } - }, - legend: { - display: opts.value.get('datasets').value.filter(x => x.value.has('label') && x.value.get('label').value).length === 0 ? false : true, - position: 'bottom', - labels: { - boxWidth: 16, - } - }, - tooltips: { - enabled: false, - }, - chartArea: { - backgroundColor: '#fff' - }, - ...(opts.value.get('type').value === 'radar' ? { - scale: { - ticks: { - display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : false, - min: opts.value.has('min') ? opts.value.get('min').value : undefined, - max: opts.value.has('max') ? opts.value.get('max').value : undefined, - maxTicksLimit: 8, - }, - pointLabels: { - fontSize: 12 - } - } - } : { - scales: { - yAxes: [{ - ticks: { - display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : true, - min: opts.value.has('min') ? opts.value.get('min').value : undefined, - max: opts.value.has('max') ? opts.value.get('max').value : undefined, - } - }] - } - }) - } - }); - */ - }), - }; -} - -export const funcDefs: Record = { - if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'ti ti-share' }, - for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle' }, - not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, - or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, - and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, - add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-plus' }, - subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-minus' }, - multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-x' }, - divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' }, - mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' }, - round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator' }, - eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals' }, - notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal' }, - gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than' }, - lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than' }, - gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal' }, - ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal' }, - strLen: { in: ['string'], out: 'number', category: 'text', icon: 'ti ti-quote' }, - strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, - stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt' }, - numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt' }, - splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt' }, - pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent' }, - listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent' }, - rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, - dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, - seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, - random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, - dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, - seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, - randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' }, - dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' }, - seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice' }, - DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice' }, // dailyRandomPickWithProbabilityMapping -}; - -export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) { - const date = new Date(); - const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; - - // SHOULD be fine to ignore since it's intended + function shape isn't defined - // eslint-disable-next-line @typescript-eslint/ban-types - const funcs: Record = { - not: (a: boolean) => !a, - or: (a: boolean, b: boolean) => a || b, - and: (a: boolean, b: boolean) => a && b, - eq: (a: any, b: any) => a === b, - notEq: (a: any, b: any) => a !== b, - gt: (a: number, b: number) => a > b, - lt: (a: number, b: number) => a < b, - gtEq: (a: number, b: number) => a >= b, - ltEq: (a: number, b: number) => a <= b, - if: (bool: boolean, a: any, b: any) => bool ? a : b, - for: (times: number, fn: Fn) => { - const result: any[] = []; - for (let i = 0; i < times; i++) { - result.push(fn.exec({ - [fn.slots[0]]: i + 1, - })); - } - return result; - }, - add: (a: number, b: number) => a + b, - subtract: (a: number, b: number) => a - b, - multiply: (a: number, b: number) => a * b, - divide: (a: number, b: number) => a / b, - mod: (a: number, b: number) => a % b, - round: (a: number) => Math.round(a), - strLen: (a: string) => a.length, - strPick: (a: string, b: number) => a[b - 1], - strReplace: (a: string, b: string, c: string) => a.split(b).join(c), - strReverse: (a: string) => a.split('').reverse().join(''), - join: (texts: string[], separator: string) => texts.join(separator || ''), - stringToNumber: (a: string) => parseInt(a), - numberToString: (a: number) => a.toString(), - splitStrByLine: (a: string) => a.split('\n'), - pick: (list: any[], i: number) => list[i - 1], - listLen: (list: any[]) => list.length, - random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability, - rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)), - randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)], - dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability, - dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)), - dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)], - seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability, - seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)), - seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)], - DRPWPM: (list: string[]) => { - const xs: any[] = []; - let totalFactor = 0; - for (const x of list) { - const parts = x.split(' '); - const factor = parseInt(parts.pop()!, 10); - const text = parts.join(' '); - totalFactor += factor; - xs.push({ factor, text }); - } - const r = seedrandom(`${day}:${expr.id}`)() * totalFactor; - let stackedFactor = 0; - for (const x of xs) { - if (r >= stackedFactor && r <= stackedFactor + x.factor) { - return x.text; - } else { - stackedFactor += x.factor; - } - } - return xs[0].text; - }, - }; - - return funcs; -} diff --git a/packages/client/src/scripts/hpml/type-checker.ts b/packages/client/src/scripts/hpml/type-checker.ts deleted file mode 100644 index 24c9ed8bcb..0000000000 --- a/packages/client/src/scripts/hpml/type-checker.ts +++ /dev/null @@ -1,191 +0,0 @@ -import autobind from 'autobind-decorator'; -import { isLiteralValue } from './expr'; -import { funcDefs } from './lib'; -import { envVarsDef } from '.'; -import type { Type, PageVar } from '.'; -import type { Expr, Variable } from './expr'; - -type TypeError = { - arg: number; - expect: Type; - actual: Type; -}; - -/** - * Hpml type checker - */ -export class HpmlTypeChecker { - public variables: Variable[]; - public pageVars: PageVar[]; - - constructor(variables: HpmlTypeChecker['variables'] = [], pageVars: HpmlTypeChecker['pageVars'] = []) { - this.variables = variables; - this.pageVars = pageVars; - } - - @autobind - public typeCheck(v: Expr): TypeError | null { - if (isLiteralValue(v)) return null; - - const def = funcDefs[v.type || '']; - if (def == null) { - throw new Error('Unknown type: ' + v.type); - } - - const generic: Type[] = []; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - const type = this.infer(v.args[i]); - if (type === null) continue; - - if (typeof arg === 'number') { - if (generic[arg] === undefined) { - generic[arg] = type; - } else if (type !== generic[arg]) { - return { - arg: i, - expect: generic[arg], - actual: type, - }; - } - } else if (type !== arg) { - return { - arg: i, - expect: arg, - actual: type, - }; - } - } - - return null; - } - - @autobind - public getExpectedType(v: Expr, slot: number): Type { - const def = funcDefs[v.type || '']; - if (def == null) { - throw new Error('Unknown type: ' + v.type); - } - - const generic: Type[] = []; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - const type = this.infer(v.args[i]); - if (type === null) continue; - - if (typeof arg === 'number') { - if (generic[arg] === undefined) { - generic[arg] = type; - } - } - } - - if (typeof def.in[slot] === 'number') { - return generic[def.in[slot]] ?? null; - } else { - return def.in[slot]; - } - } - - @autobind - public infer(v: Expr): Type { - if (v.type === null) return null; - if (v.type === 'text') return 'string'; - if (v.type === 'multiLineText') return 'string'; - if (v.type === 'textList') return 'stringArray'; - if (v.type === 'number') return 'number'; - if (v.type === 'ref') { - const variable = this.variables.find(va => va.name === v.value); - if (variable) { - return this.infer(variable); - } - - const pageVar = this.pageVars.find(va => va.name === v.value); - if (pageVar) { - return pageVar.type; - } - - const envVar = envVarsDef[v.value || '']; - if (envVar !== undefined) { - return envVar; - } - - return null; - } - if (v.type === 'aiScriptVar') return null; - if (v.type === 'fn') return null; // todo - if (v.type.startsWith('fn:')) return null; // todo - - const generic: Type[] = []; - - const def = funcDefs[v.type]; - - for (let i = 0; i < def.in.length; i++) { - const arg = def.in[i]; - if (typeof arg === 'number') { - const type = this.infer(v.args[i]); - - if (generic[arg] === undefined) { - generic[arg] = type; - } else { - if (type !== generic[arg]) { - generic[arg] = null; - } - } - } - } - - if (typeof def.out === 'number') { - return generic[def.out]; - } else { - return def.out; - } - } - - @autobind - public getVarByName(name: string): Variable { - const v = this.variables.find(x => x.name === name); - if (v !== undefined) { - return v; - } else { - throw new Error(`No such variable '${name}'`); - } - } - - @autobind - public getVarsByType(type: Type): Variable[] { - if (type == null) return this.variables; - return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type)); - } - - @autobind - public getEnvVarsByType(type: Type): string[] { - if (type == null) return Object.keys(envVarsDef); - return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k); - } - - @autobind - public getPageVarsByType(type: Type): string[] { - if (type == null) return this.pageVars.map(v => v.name); - return this.pageVars.filter(v => type === v.type).map(v => v.name); - } - - @autobind - public isUsedName(name: string) { - if (this.variables.some(v => v.name === name)) { - return true; - } - - if (this.pageVars.some(v => v.name === name)) { - return true; - } - - if (envVarsDef[name]) { - return true; - } - - return false; - } -} diff --git a/packages/client/src/scripts/i18n.ts b/packages/client/src/scripts/i18n.ts deleted file mode 100644 index 54184386da..0000000000 --- a/packages/client/src/scripts/i18n.ts +++ /dev/null @@ -1,29 +0,0 @@ -export class I18n> { - public ts: T; - - constructor(locale: T) { - this.ts = locale; - - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - // string にしているのは、ドット区切りでのパス指定を許可するため - // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record): string { - try { - let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; - - if (args) { - for (const [k, v] of Object.entries(args)) { - str = str.replace(`{${k}}`, v.toString()); - } - } - return str; - } catch (err) { - console.warn(`missing localization '${key}'`); - return key; - } - } -} diff --git a/packages/client/src/scripts/idb-proxy.ts b/packages/client/src/scripts/idb-proxy.ts deleted file mode 100644 index 77bb84463c..0000000000 --- a/packages/client/src/scripts/idb-proxy.ts +++ /dev/null @@ -1,36 +0,0 @@ -// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、 -// indexedDBが使えない環境ではlocalStorageを使う -import { - get as iget, - set as iset, - del as idel, -} from 'idb-keyval'; - -const fallbackName = (key: string) => `idbfallback::${key}`; - -let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true; - -if (idbAvailable) { - iset('idb-test', 'test').catch(err => { - console.error('idb error', err); - console.error('indexedDB is unavailable. It will use localStorage.'); - idbAvailable = false; - }); -} else { - console.error('indexedDB is unavailable. It will use localStorage.'); -} - -export async function get(key: string) { - if (idbAvailable) return iget(key); - return JSON.parse(localStorage.getItem(fallbackName(key))); -} - -export async function set(key: string, val: any) { - if (idbAvailable) return iset(key, val); - return localStorage.setItem(fallbackName(key), JSON.stringify(val)); -} - -export async function del(key: string) { - if (idbAvailable) return idel(key); - return localStorage.removeItem(fallbackName(key)); -} diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts deleted file mode 100644 index de52f30523..0000000000 --- a/packages/client/src/scripts/initialize-sw.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { lang } from '@/config'; - -export async function initializeSw() { - if (!('serviceWorker' in navigator)) return; - - navigator.serviceWorker.register(`/sw.js`, { scope: '/', type: 'classic' }); - navigator.serviceWorker.ready.then(registration => { - registration.active?.postMessage({ - msg: 'initialize', - lang, - }); - }); -} diff --git a/packages/client/src/scripts/is-device-darkmode.ts b/packages/client/src/scripts/is-device-darkmode.ts deleted file mode 100644 index 854f38e517..0000000000 --- a/packages/client/src/scripts/is-device-darkmode.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isDeviceDarkmode() { - return window.matchMedia('(prefers-color-scheme: dark)').matches; -} diff --git a/packages/client/src/scripts/keycode.ts b/packages/client/src/scripts/keycode.ts deleted file mode 100644 index 69f6a82803..0000000000 --- a/packages/client/src/scripts/keycode.ts +++ /dev/null @@ -1,33 +0,0 @@ -export default (input: string): string[] => { - if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) { - const codes = aliases[input]; - return Array.isArray(codes) ? codes : [codes]; - } else { - return [input]; - } -}; - -export const aliases = { - 'esc': 'Escape', - 'enter': ['Enter', 'NumpadEnter'], - 'up': 'ArrowUp', - 'down': 'ArrowDown', - 'left': 'ArrowLeft', - 'right': 'ArrowRight', - 'plus': ['NumpadAdd', 'Semicolon'], -}; - -/*! -* Programmatically add the following -*/ - -// lower case chars -for (let i = 97; i < 123; i++) { - const char = String.fromCharCode(i); - aliases[char] = `Key${char.toUpperCase()}`; -} - -// numbers -for (let i = 0; i < 10; i++) { - aliases[i] = [`Numpad${i}`, `Digit${i}`]; -} diff --git a/packages/client/src/scripts/langmap.ts b/packages/client/src/scripts/langmap.ts deleted file mode 100644 index 25f5b366c8..0000000000 --- a/packages/client/src/scripts/langmap.ts +++ /dev/null @@ -1,666 +0,0 @@ -// TODO: sharedに置いてバックエンドのと統合したい -export const langmap = { - 'ach': { - nativeName: 'Lwo', - }, - 'ady': { - nativeName: 'Адыгэбзэ', - }, - 'af': { - nativeName: 'Afrikaans', - }, - 'af-NA': { - nativeName: 'Afrikaans (Namibia)', - }, - 'af-ZA': { - nativeName: 'Afrikaans (South Africa)', - }, - 'ak': { - nativeName: 'Tɕɥi', - }, - 'ar': { - nativeName: 'العربية', - }, - 'ar-AR': { - nativeName: 'العربية', - }, - 'ar-MA': { - nativeName: 'العربية', - }, - 'ar-SA': { - nativeName: 'العربية (السعودية)', - }, - 'ay-BO': { - nativeName: 'Aymar aru', - }, - 'az': { - nativeName: 'Azərbaycan dili', - }, - 'az-AZ': { - nativeName: 'Azərbaycan dili', - }, - 'be-BY': { - nativeName: 'Беларуская', - }, - 'bg': { - nativeName: 'Български', - }, - 'bg-BG': { - nativeName: 'Български', - }, - 'bn': { - nativeName: 'বাংলা', - }, - 'bn-IN': { - nativeName: 'বাংলা (ভারত)', - }, - 'bn-BD': { - nativeName: 'বাংলা(বাংলাদেশ)', - }, - 'br': { - nativeName: 'Brezhoneg', - }, - 'bs-BA': { - nativeName: 'Bosanski', - }, - 'ca': { - nativeName: 'Català', - }, - 'ca-ES': { - nativeName: 'Català', - }, - 'cak': { - nativeName: 'Maya Kaqchikel', - }, - 'ck-US': { - nativeName: 'ᏣᎳᎩ (tsalagi)', - }, - 'cs': { - nativeName: 'Čeština', - }, - 'cs-CZ': { - nativeName: 'Čeština', - }, - 'cy': { - nativeName: 'Cymraeg', - }, - 'cy-GB': { - nativeName: 'Cymraeg', - }, - 'da': { - nativeName: 'Dansk', - }, - 'da-DK': { - nativeName: 'Dansk', - }, - 'de': { - nativeName: 'Deutsch', - }, - 'de-AT': { - nativeName: 'Deutsch (Österreich)', - }, - 'de-DE': { - nativeName: 'Deutsch (Deutschland)', - }, - 'de-CH': { - nativeName: 'Deutsch (Schweiz)', - }, - 'dsb': { - nativeName: 'Dolnoserbšćina', - }, - 'el': { - nativeName: 'Ελληνικά', - }, - 'el-GR': { - nativeName: 'Ελληνικά', - }, - 'en': { - nativeName: 'English', - }, - 'en-GB': { - nativeName: 'English (UK)', - }, - 'en-AU': { - nativeName: 'English (Australia)', - }, - 'en-CA': { - nativeName: 'English (Canada)', - }, - 'en-IE': { - nativeName: 'English (Ireland)', - }, - 'en-IN': { - nativeName: 'English (India)', - }, - 'en-PI': { - nativeName: 'English (Pirate)', - }, - 'en-SG': { - nativeName: 'English (Singapore)', - }, - 'en-UD': { - nativeName: 'English (Upside Down)', - }, - 'en-US': { - nativeName: 'English (US)', - }, - 'en-ZA': { - nativeName: 'English (South Africa)', - }, - 'en@pirate': { - nativeName: 'English (Pirate)', - }, - 'eo': { - nativeName: 'Esperanto', - }, - 'eo-EO': { - nativeName: 'Esperanto', - }, - 'es': { - nativeName: 'Español', - }, - 'es-AR': { - nativeName: 'Español (Argentine)', - }, - 'es-419': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-CL': { - nativeName: 'Español (Chile)', - }, - 'es-CO': { - nativeName: 'Español (Colombia)', - }, - 'es-EC': { - nativeName: 'Español (Ecuador)', - }, - 'es-ES': { - nativeName: 'Español (España)', - }, - 'es-LA': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-NI': { - nativeName: 'Español (Nicaragua)', - }, - 'es-MX': { - nativeName: 'Español (México)', - }, - 'es-US': { - nativeName: 'Español (Estados Unidos)', - }, - 'es-VE': { - nativeName: 'Español (Venezuela)', - }, - 'et': { - nativeName: 'eesti keel', - }, - 'et-EE': { - nativeName: 'Eesti (Estonia)', - }, - 'eu': { - nativeName: 'Euskara', - }, - 'eu-ES': { - nativeName: 'Euskara', - }, - 'fa': { - nativeName: 'فارسی', - }, - 'fa-IR': { - nativeName: 'فارسی', - }, - 'fb-LT': { - nativeName: 'Leet Speak', - }, - 'ff': { - nativeName: 'Fulah', - }, - 'fi': { - nativeName: 'Suomi', - }, - 'fi-FI': { - nativeName: 'Suomi', - }, - 'fo': { - nativeName: 'Føroyskt', - }, - 'fo-FO': { - nativeName: 'Føroyskt (Færeyjar)', - }, - 'fr': { - nativeName: 'Français', - }, - 'fr-CA': { - nativeName: 'Français (Canada)', - }, - 'fr-FR': { - nativeName: 'Français (France)', - }, - 'fr-BE': { - nativeName: 'Français (Belgique)', - }, - 'fr-CH': { - nativeName: 'Français (Suisse)', - }, - 'fy-NL': { - nativeName: 'Frysk', - }, - 'ga': { - nativeName: 'Gaeilge', - }, - 'ga-IE': { - nativeName: 'Gaeilge', - }, - 'gd': { - nativeName: 'Gàidhlig', - }, - 'gl': { - nativeName: 'Galego', - }, - 'gl-ES': { - nativeName: 'Galego', - }, - 'gn-PY': { - nativeName: 'Avañe\'ẽ', - }, - 'gu-IN': { - nativeName: 'ગુજરાતી', - }, - 'gv': { - nativeName: 'Gaelg', - }, - 'gx-GR': { - nativeName: 'Ἑλληνική ἀρχαία', - }, - 'he': { - nativeName: 'עברית‏', - }, - 'he-IL': { - nativeName: 'עברית‏', - }, - 'hi': { - nativeName: 'हिन्दी', - }, - 'hi-IN': { - nativeName: 'हिन्दी', - }, - 'hr': { - nativeName: 'Hrvatski', - }, - 'hr-HR': { - nativeName: 'Hrvatski', - }, - 'hsb': { - nativeName: 'Hornjoserbšćina', - }, - 'ht': { - nativeName: 'Kreyòl', - }, - 'hu': { - nativeName: 'Magyar', - }, - 'hu-HU': { - nativeName: 'Magyar', - }, - 'hy': { - nativeName: 'Հայերեն', - }, - 'hy-AM': { - nativeName: 'Հայերեն (Հայաստան)', - }, - 'id': { - nativeName: 'Bahasa Indonesia', - }, - 'id-ID': { - nativeName: 'Bahasa Indonesia', - }, - 'is': { - nativeName: 'Íslenska', - }, - 'is-IS': { - nativeName: 'Íslenska (Iceland)', - }, - 'it': { - nativeName: 'Italiano', - }, - 'it-IT': { - nativeName: 'Italiano', - }, - 'ja': { - nativeName: '日本語', - }, - 'ja-JP': { - nativeName: '日本語 (日本)', - }, - 'jv-ID': { - nativeName: 'Basa Jawa', - }, - 'ka-GE': { - nativeName: 'ქართული', - }, - 'kk-KZ': { - nativeName: 'Қазақша', - }, - 'km': { - nativeName: 'ភាសាខ្មែរ', - }, - 'kl': { - nativeName: 'kalaallisut', - }, - 'km-KH': { - nativeName: 'ភាសាខ្មែរ', - }, - 'kab': { - nativeName: 'Taqbaylit', - }, - 'kn': { - nativeName: 'ಕನ್ನಡ', - }, - 'kn-IN': { - nativeName: 'ಕನ್ನಡ (India)', - }, - 'ko': { - nativeName: '한국어', - }, - 'ko-KR': { - nativeName: '한국어 (한국)', - }, - 'ku-TR': { - nativeName: 'Kurdî', - }, - 'kw': { - nativeName: 'Kernewek', - }, - 'la': { - nativeName: 'Latin', - }, - 'la-VA': { - nativeName: 'Latin', - }, - 'lb': { - nativeName: 'Lëtzebuergesch', - }, - 'li-NL': { - nativeName: 'Lèmbörgs', - }, - 'lt': { - nativeName: 'Lietuvių', - }, - 'lt-LT': { - nativeName: 'Lietuvių', - }, - 'lv': { - nativeName: 'Latviešu', - }, - 'lv-LV': { - nativeName: 'Latviešu', - }, - 'mai': { - nativeName: 'मैथिली, মৈথিলী', - }, - 'mg-MG': { - nativeName: 'Malagasy', - }, - 'mk': { - nativeName: 'Македонски', - }, - 'mk-MK': { - nativeName: 'Македонски (Македонски)', - }, - 'ml': { - nativeName: 'മലയാളം', - }, - 'ml-IN': { - nativeName: 'മലയാളം', - }, - 'mn-MN': { - nativeName: 'Монгол', - }, - 'mr': { - nativeName: 'मराठी', - }, - 'mr-IN': { - nativeName: 'मराठी', - }, - 'ms': { - nativeName: 'Bahasa Melayu', - }, - 'ms-MY': { - nativeName: 'Bahasa Melayu', - }, - 'mt': { - nativeName: 'Malti', - }, - 'mt-MT': { - nativeName: 'Malti', - }, - 'my': { - nativeName: 'ဗမာစကာ', - }, - 'no': { - nativeName: 'Norsk', - }, - 'nb': { - nativeName: 'Norsk (bokmål)', - }, - 'nb-NO': { - nativeName: 'Norsk (bokmål)', - }, - 'ne': { - nativeName: 'नेपाली', - }, - 'ne-NP': { - nativeName: 'नेपाली', - }, - 'nl': { - nativeName: 'Nederlands', - }, - 'nl-BE': { - nativeName: 'Nederlands (België)', - }, - 'nl-NL': { - nativeName: 'Nederlands (Nederland)', - }, - 'nn-NO': { - nativeName: 'Norsk (nynorsk)', - }, - 'oc': { - nativeName: 'Occitan', - }, - 'or-IN': { - nativeName: 'ଓଡ଼ିଆ', - }, - 'pa': { - nativeName: 'ਪੰਜਾਬੀ', - }, - 'pa-IN': { - nativeName: 'ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)', - }, - 'pl': { - nativeName: 'Polski', - }, - 'pl-PL': { - nativeName: 'Polski', - }, - 'ps-AF': { - nativeName: 'پښتو', - }, - 'pt': { - nativeName: 'Português', - }, - 'pt-BR': { - nativeName: 'Português (Brasil)', - }, - 'pt-PT': { - nativeName: 'Português (Portugal)', - }, - 'qu-PE': { - nativeName: 'Qhichwa', - }, - 'rm-CH': { - nativeName: 'Rumantsch', - }, - 'ro': { - nativeName: 'Română', - }, - 'ro-RO': { - nativeName: 'Română', - }, - 'ru': { - nativeName: 'Русский', - }, - 'ru-RU': { - nativeName: 'Русский', - }, - 'sa-IN': { - nativeName: 'संस्कृतम्', - }, - 'se-NO': { - nativeName: 'Davvisámegiella', - }, - 'sh': { - nativeName: 'српскохрватски', - }, - 'si-LK': { - nativeName: 'සිංහල', - }, - 'sk': { - nativeName: 'Slovenčina', - }, - 'sk-SK': { - nativeName: 'Slovenčina (Slovakia)', - }, - 'sl': { - nativeName: 'Slovenščina', - }, - 'sl-SI': { - nativeName: 'Slovenščina', - }, - 'so-SO': { - nativeName: 'Soomaaliga', - }, - 'sq': { - nativeName: 'Shqip', - }, - 'sq-AL': { - nativeName: 'Shqip', - }, - 'sr': { - nativeName: 'Српски', - }, - 'sr-RS': { - nativeName: 'Српски (Serbia)', - }, - 'su': { - nativeName: 'Basa Sunda', - }, - 'sv': { - nativeName: 'Svenska', - }, - 'sv-SE': { - nativeName: 'Svenska', - }, - 'sw': { - nativeName: 'Kiswahili', - }, - 'sw-KE': { - nativeName: 'Kiswahili', - }, - 'ta': { - nativeName: 'தமிழ்', - }, - 'ta-IN': { - nativeName: 'தமிழ்', - }, - 'te': { - nativeName: 'తెలుగు', - }, - 'te-IN': { - nativeName: 'తెలుగు', - }, - 'tg': { - nativeName: 'забо́ни тоҷикӣ́', - }, - 'tg-TJ': { - nativeName: 'тоҷикӣ', - }, - 'th': { - nativeName: 'ภาษาไทย', - }, - 'th-TH': { - nativeName: 'ภาษาไทย (ประเทศไทย)', - }, - 'fil': { - nativeName: 'Filipino', - }, - 'tlh': { - nativeName: 'tlhIngan-Hol', - }, - 'tr': { - nativeName: 'Türkçe', - }, - 'tr-TR': { - nativeName: 'Türkçe', - }, - 'tt-RU': { - nativeName: 'татарча', - }, - 'uk': { - nativeName: 'Українська', - }, - 'uk-UA': { - nativeName: 'Українська', - }, - 'ur': { - nativeName: 'اردو', - }, - 'ur-PK': { - nativeName: 'اردو', - }, - 'uz': { - nativeName: 'O\'zbek', - }, - 'uz-UZ': { - nativeName: 'O\'zbek', - }, - 'vi': { - nativeName: 'Tiếng Việt', - }, - 'vi-VN': { - nativeName: 'Tiếng Việt', - }, - 'xh-ZA': { - nativeName: 'isiXhosa', - }, - 'yi': { - nativeName: 'ייִדיש', - }, - 'yi-DE': { - nativeName: 'ייִדיש (German)', - }, - 'zh': { - nativeName: '中文', - }, - 'zh-Hans': { - nativeName: '中文简体', - }, - 'zh-Hant': { - nativeName: '中文繁體', - }, - 'zh-CN': { - nativeName: '中文(中国大陆)', - }, - 'zh-HK': { - nativeName: '中文(香港)', - }, - 'zh-SG': { - nativeName: '中文(新加坡)', - }, - 'zh-TW': { - nativeName: '中文(台灣)', - }, - 'zu-ZA': { - nativeName: 'isiZulu', - }, -}; diff --git a/packages/client/src/scripts/login-id.ts b/packages/client/src/scripts/login-id.ts deleted file mode 100644 index 0f9c6be4a9..0000000000 --- a/packages/client/src/scripts/login-id.ts +++ /dev/null @@ -1,11 +0,0 @@ -export function getUrlWithLoginId(url: string, loginId: string) { - const u = new URL(url, origin); - u.searchParams.append('loginId', loginId); - return u.toString(); -} - -export function getUrlWithoutLoginId(url: string) { - const u = new URL(url); - u.searchParams.delete('loginId'); - return u.toString(); -} diff --git a/packages/client/src/scripts/lookup-user.ts b/packages/client/src/scripts/lookup-user.ts deleted file mode 100644 index 3ab9d55300..0000000000 --- a/packages/client/src/scripts/lookup-user.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as Acct from 'misskey-js/built/acct'; -import { i18n } from '@/i18n'; -import * as os from '@/os'; - -export async function lookupUser() { - const { canceled, result } = await os.inputText({ - title: i18n.ts.usernameOrUserId, - }); - if (canceled) return; - - const show = (user) => { - os.pageWindow(`/user-info/${user.id}`); - }; - - const usernamePromise = os.api('users/show', Acct.parse(result)); - const idPromise = os.api('users/show', { userId: result }); - let _notFound = false; - const notFound = () => { - if (_notFound) { - os.alert({ - type: 'error', - text: i18n.ts.noSuchUser, - }); - } else { - _notFound = true; - } - }; - usernamePromise.then(show).catch(err => { - if (err.code === 'NO_SUCH_USER') { - notFound(); - } - }); - idPromise.then(show).catch(err => { - notFound(); - }); -} diff --git a/packages/client/src/scripts/media-proxy.ts b/packages/client/src/scripts/media-proxy.ts deleted file mode 100644 index aaf7f9e610..0000000000 --- a/packages/client/src/scripts/media-proxy.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { query } from '@/scripts/url'; -import { url } from '@/config'; - -export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string { - return `${url}/proxy/image.webp?${query({ - url: imageUrl, - fallback: '1', - ...(type ? { [type]: '1' } : {}), - })}`; -} - -export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return getProxiedImageUrl(imageUrl, type); -} diff --git a/packages/client/src/scripts/mfm-tags.ts b/packages/client/src/scripts/mfm-tags.ts deleted file mode 100644 index 18e8d7038a..0000000000 --- a/packages/client/src/scripts/mfm-tags.ts +++ /dev/null @@ -1 +0,0 @@ -export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle', 'rotate']; diff --git a/packages/client/src/scripts/page-metadata.ts b/packages/client/src/scripts/page-metadata.ts deleted file mode 100644 index 0db8369f9d..0000000000 --- a/packages/client/src/scripts/page-metadata.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as misskey from 'misskey-js'; -import { ComputedRef, inject, isRef, onActivated, onMounted, provide, ref, Ref } from 'vue'; - -export const setPageMetadata = Symbol('setPageMetadata'); -export const pageMetadataProvider = Symbol('pageMetadataProvider'); - -export type PageMetadata = { - title: string; - subtitle?: string; - icon?: string | null; - avatar?: misskey.entities.User | null; - userName?: misskey.entities.User | null; - bg?: string; -}; - -export function definePageMetadata(metadata: PageMetadata | null | Ref | ComputedRef): void { - const _metadata = isRef(metadata) ? metadata : ref(metadata); - - provide(pageMetadataProvider, _metadata); - - const set = inject(setPageMetadata) as any; - if (set) { - set(_metadata); - - onMounted(() => { - set(_metadata); - }); - - onActivated(() => { - set(_metadata); - }); - } -} - -export function provideMetadataReceiver(callback: (info: ComputedRef) => void): void { - provide(setPageMetadata, callback); -} - -export function injectPageMetadata(): PageMetadata | undefined { - return inject(pageMetadataProvider); -} diff --git a/packages/client/src/scripts/physics.ts b/packages/client/src/scripts/physics.ts deleted file mode 100644 index efda80f074..0000000000 --- a/packages/client/src/scripts/physics.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as Matter from 'matter-js'; - -export function physics(container: HTMLElement) { - const containerWidth = container.offsetWidth; - const containerHeight = container.offsetHeight; - const containerCenterX = containerWidth / 2; - - // サイズ固定化(要らないかも?) - container.style.position = 'relative'; - container.style.boxSizing = 'border-box'; - container.style.width = `${containerWidth}px`; - container.style.height = `${containerHeight}px`; - - // create engine - const engine = Matter.Engine.create({ - constraintIterations: 4, - positionIterations: 8, - velocityIterations: 8, - }); - - const world = engine.world; - - // create renderer - const render = Matter.Render.create({ - engine: engine, - //element: document.getElementById('debug'), - options: { - width: containerWidth, - height: containerHeight, - background: 'transparent', // transparent to hide - wireframeBackground: 'transparent', // transparent to hide - }, - }); - - // Disable to hide debug - Matter.Render.run(render); - - // create runner - const runner = Matter.Runner.create(); - Matter.Runner.run(runner, engine); - - const groundThickness = 1024; - const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, { - isStatic: true, - restitution: 0.1, - friction: 2, - }); - - //const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts); - //const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts); - - Matter.World.add(world, [ - ground, - //wallRight, - //wallLeft, - ]); - - const objEls = Array.from(container.children) as HTMLElement[]; - const objs: Matter.Body[] = []; - for (const objEl of objEls) { - const left = objEl.dataset.physicsX ? parseInt(objEl.dataset.physicsX) : objEl.offsetLeft; - const top = objEl.dataset.physicsY ? parseInt(objEl.dataset.physicsY) : objEl.offsetTop; - - let obj: Matter.Body; - if (objEl.classList.contains('_physics_circle_')) { - obj = Matter.Bodies.circle( - left + (objEl.offsetWidth / 2), - top + (objEl.offsetHeight / 2), - Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2, - { - restitution: 0.5, - }, - ); - } else { - const style = window.getComputedStyle(objEl); - obj = Matter.Bodies.rectangle( - left + (objEl.offsetWidth / 2), - top + (objEl.offsetHeight / 2), - objEl.offsetWidth, - objEl.offsetHeight, - { - chamfer: { radius: parseInt(style.borderRadius || '0', 10) }, - restitution: 0.5, - }, - ); - } - objEl.id = obj.id.toString(); - objs.push(obj); - } - - Matter.World.add(engine.world, objs); - - // Add mouse control - - const mouse = Matter.Mouse.create(container); - const mouseConstraint = Matter.MouseConstraint.create(engine, { - mouse: mouse, - constraint: { - stiffness: 0.1, - render: { - visible: false, - }, - }, - }); - - Matter.World.add(engine.world, mouseConstraint); - - // keep the mouse in sync with rendering - render.mouse = mouse; - - for (const objEl of objEls) { - objEl.style.position = 'absolute'; - objEl.style.top = '0'; - objEl.style.left = '0'; - objEl.style.margin = '0'; - } - - window.requestAnimationFrame(update); - - let stop = false; - - function update() { - for (const objEl of objEls) { - const obj = objs.find(obj => obj.id.toString() === objEl.id.toString()); - if (obj == null) continue; - - const x = (obj.position.x - objEl.offsetWidth / 2); - const y = (obj.position.y - objEl.offsetHeight / 2); - const angle = obj.angle; - objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; - } - - if (!stop) { - window.requestAnimationFrame(update); - } - } - - // 奈落に落ちたオブジェクトは消す - const intervalId = window.setInterval(() => { - for (const obj of objs) { - if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj); - } - }, 1000 * 10); - - return { - stop: () => { - stop = true; - Matter.Runner.stop(runner); - window.clearInterval(intervalId); - }, - }; -} diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts deleted file mode 100644 index b8fb853cc1..0000000000 --- a/packages/client/src/scripts/please-login.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineAsyncComponent } from 'vue'; -import { $i } from '@/account'; -import { i18n } from '@/i18n'; -import { popup } from '@/os'; - -export function pleaseLogin(path?: string) { - if ($i) return; - - popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { - autoSet: true, - message: i18n.ts.signinRequired, - }, { - cancelled: () => { - if (path) { - window.location.href = path; - } - }, - }, 'closed'); - - if (!path) throw new Error('signin required'); -} diff --git a/packages/client/src/scripts/popout.ts b/packages/client/src/scripts/popout.ts deleted file mode 100644 index 580031d0a3..0000000000 --- a/packages/client/src/scripts/popout.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as config from '@/config'; -import { appendQuery } from './url'; - -export function popout(path: string, w?: HTMLElement) { - let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; - url = appendQuery(url, 'zen'); - if (w) { - const position = w.getBoundingClientRect(); - const width = parseInt(getComputedStyle(w, '').width, 10); - const height = parseInt(getComputedStyle(w, '').height, 10); - const x = window.screenX + position.left; - const y = window.screenY + position.top; - window.open(url, url, - `width=${width}, height=${height}, top=${y}, left=${x}`); - } else { - const width = 400; - const height = 500; - const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2); - const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2); - window.open(url, url, - `width=${width}, height=${height}, top=${x}, left=${y}`); - } -} diff --git a/packages/client/src/scripts/popup-position.ts b/packages/client/src/scripts/popup-position.ts deleted file mode 100644 index e84eebf103..0000000000 --- a/packages/client/src/scripts/popup-position.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Ref } from 'vue'; - -export function calcPopupPosition(el: HTMLElement, props: { - anchorElement: HTMLElement | null; - innerMargin: number; - direction: 'top' | 'bottom' | 'left' | 'right'; - align: 'top' | 'bottom' | 'left' | 'right' | 'center'; - alignOffset?: number; - x?: number; - y?: number; -}): { top: number; left: number; transformOrigin: string; } { - const contentWidth = el.offsetWidth; - const contentHeight = el.offsetHeight; - - let rect: DOMRect; - - if (props.anchorElement) { - rect = props.anchorElement.getBoundingClientRect(); - } - - const calcPosWhenTop = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; - } else { - left = props.x; - top = (props.y - contentHeight) - props.innerMargin; - } - - left -= (el.offsetWidth / 2); - - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenBottom = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin; - } else { - left = props.x; - top = (props.y) + props.innerMargin; - } - - left -= (el.offsetWidth / 2); - - if (left + contentWidth - window.pageXOffset > window.innerWidth) { - left = window.innerWidth - contentWidth + window.pageXOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenLeft = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; - top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); - } else { - left = (props.x - contentWidth) - props.innerMargin; - top = props.y; - } - - top -= (el.offsetHeight / 2); - - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - contentHeight + window.pageYOffset - 1; - } - - return [left, top]; - }; - - const calcPosWhenRight = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin; - - if (props.align === 'top') { - top = rect.top + window.pageYOffset; - if (props.alignOffset != null) top += props.alignOffset; - } else if (props.align === 'bottom') { - // TODO - } else { // center - top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); - top -= (el.offsetHeight / 2); - } - } else { - left = props.x + props.innerMargin; - top = props.y; - top -= (el.offsetHeight / 2); - } - - if (top + contentHeight - window.pageYOffset > window.innerHeight) { - top = window.innerHeight - contentHeight + window.pageYOffset - 1; - } - - return [left, top]; - }; - - const calc = (): { - left: number; - top: number; - transformOrigin: string; - } => { - switch (props.direction) { - case 'top': { - const [left, top] = calcPosWhenTop(); - - // ツールチップを上に向かって表示するスペースがなければ下に向かって出す - if (top - window.pageYOffset < 0) { - const [left, top] = calcPosWhenBottom(); - return { left, top, transformOrigin: 'center top' }; - } - - return { left, top, transformOrigin: 'center bottom' }; - } - - case 'bottom': { - const [left, top] = calcPosWhenBottom(); - // TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す - return { left, top, transformOrigin: 'center top' }; - } - - case 'left': { - const [left, top] = calcPosWhenLeft(); - - // ツールチップを左に向かって表示するスペースがなければ右に向かって出す - if (left - window.pageXOffset < 0) { - const [left, top] = calcPosWhenRight(); - return { left, top, transformOrigin: 'left center' }; - } - - return { left, top, transformOrigin: 'right center' }; - } - - case 'right': { - const [left, top] = calcPosWhenRight(); - // TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す - return { left, top, transformOrigin: 'left center' }; - } - } - }; - - return calc(); -} diff --git a/packages/client/src/scripts/reaction-picker.ts b/packages/client/src/scripts/reaction-picker.ts deleted file mode 100644 index fe32e719da..0000000000 --- a/packages/client/src/scripts/reaction-picker.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineAsyncComponent, Ref, ref } from 'vue'; -import { popup } from '@/os'; - -class ReactionPicker { - private src: Ref = ref(null); - private manualShowing = ref(false); - private onChosen?: (reaction: string) => void; - private onClosed?: () => void; - - constructor() { - // nop - } - - public async init() { - await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { - src: this.src, - asReactionPicker: true, - manualShowing: this.manualShowing, - }, { - done: reaction => { - this.onChosen!(reaction); - }, - close: () => { - this.manualShowing.value = false; - }, - closed: () => { - this.src.value = null; - this.onClosed!(); - }, - }); - } - - public show(src: HTMLElement, onChosen: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) { - this.src.value = src; - this.manualShowing.value = true; - this.onChosen = onChosen; - this.onClosed = onClosed; - } -} - -export const reactionPicker = new ReactionPicker(); diff --git a/packages/client/src/scripts/safe-uri-decode.ts b/packages/client/src/scripts/safe-uri-decode.ts deleted file mode 100644 index 301b56d7fd..0000000000 --- a/packages/client/src/scripts/safe-uri-decode.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} diff --git a/packages/client/src/scripts/scroll.ts b/packages/client/src/scripts/scroll.ts deleted file mode 100644 index f5bc6bf9ce..0000000000 --- a/packages/client/src/scripts/scroll.ts +++ /dev/null @@ -1,85 +0,0 @@ -type ScrollBehavior = 'auto' | 'smooth' | 'instant'; - -export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { - if (el == null || el.tagName === 'HTML') return null; - const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y'); - if (overflow === 'scroll' || overflow === 'auto') { - return el; - } else { - return getScrollContainer(el.parentElement); - } -} - -export function getScrollPosition(el: Element | null): number { - const container = getScrollContainer(el); - return container == null ? window.scrollY : container.scrollTop; -} - -export function isTopVisible(el: Element | null): boolean { - const scrollTop = getScrollPosition(el); - const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる - - return scrollTop <= topPosition; -} - -export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) { - if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance; - return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance; -} - -export function onScrollTop(el: Element, cb) { - const container = getScrollContainer(el) || window; - const onScroll = ev => { - if (!document.body.contains(el)) return; - if (isTopVisible(el)) { - cb(); - container.removeEventListener('scroll', onScroll); - } - }; - container.addEventListener('scroll', onScroll, { passive: true }); -} - -export function onScrollBottom(el: Element, cb) { - const container = getScrollContainer(el) || window; - const onScroll = ev => { - if (!document.body.contains(el)) return; - const pos = getScrollPosition(el); - if (pos + el.clientHeight > el.scrollHeight - 1) { - cb(); - container.removeEventListener('scroll', onScroll); - } - }; - container.addEventListener('scroll', onScroll, { passive: true }); -} - -export function scroll(el: Element, options: { - top?: number; - left?: number; - behavior?: ScrollBehavior; -}) { - const container = getScrollContainer(el); - if (container == null) { - window.scroll(options); - } else { - container.scroll(options); - } -} - -export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 0, ...options }); -} - -export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する -} - -export function isBottom(el: Element, asobi = 0) { - const container = getScrollContainer(el); - const current = container - ? el.scrollTop + el.offsetHeight - : window.scrollY + window.innerHeight; - const max = container - ? el.scrollHeight - : document.body.offsetHeight; - return current >= (max - asobi); -} diff --git a/packages/client/src/scripts/search.ts b/packages/client/src/scripts/search.ts deleted file mode 100644 index 64914d3d65..0000000000 --- a/packages/client/src/scripts/search.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as os from '@/os'; -import { i18n } from '@/i18n'; -import { mainRouter } from '@/router'; - -export async function search() { - const { canceled, result: query } = await os.inputText({ - title: i18n.ts.search, - }); - if (canceled || query == null || query === '') return; - - const q = query.trim(); - - if (q.startsWith('@') && !q.includes(' ')) { - mainRouter.push(`/${q}`); - return; - } - - if (q.startsWith('#')) { - mainRouter.push(`/tags/${encodeURIComponent(q.substr(1))}`); - return; - } - - // like 2018/03/12 - if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) { - const date = new Date(q.replace(/-/g, '/')); - - // 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは - // 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので - // 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の - // 結果になってしまい、2018/03/12 のコンテンツは含まれない) - if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) { - date.setHours(23, 59, 59, 999); - } - - // TODO - //v.$root.$emit('warp', date); - os.alert({ - icon: 'fas fa-history', - iconOnly: true, autoClose: true, - }); - return; - } - - if (q.startsWith('https://')) { - const promise = os.api('ap/show', { - uri: q, - }); - - os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); - - const res = await promise; - - if (res.type === 'User') { - mainRouter.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type === 'Note') { - mainRouter.push(`/notes/${res.object.id}`); - } - - return; - } - - mainRouter.push(`/search?q=${encodeURIComponent(q)}`); -} diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts deleted file mode 100644 index ec5f8f65e9..0000000000 --- a/packages/client/src/scripts/select-file.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { ref } from 'vue'; -import { DriveFile } from 'misskey-js/built/entities'; -import * as os from '@/os'; -import { stream } from '@/stream'; -import { i18n } from '@/i18n'; -import { defaultStore } from '@/store'; -import { uploadFile } from '@/scripts/upload'; - -function select(src: any, label: string | null, multiple: boolean): Promise { - return new Promise((res, rej) => { - const keepOriginal = ref(defaultStore.state.keepOriginalUploading); - - const chooseFileFromPc = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = multiple; - input.onchange = () => { - const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); - - Promise.all(promises).then(driveFiles => { - res(multiple ? driveFiles : driveFiles[0]); - }).catch(err => { - // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない - }); - - // 一応廃棄 - (window as any).__misskey_input_ref__ = null; - }; - - // https://qiita.com/fukasawah/items/b9dc732d95d99551013d - // iOS Safari で正常に動かす為のおまじない - (window as any).__misskey_input_ref__ = input; - - input.click(); - }; - - const chooseFileFromDrive = () => { - os.selectDriveFile(multiple).then(files => { - res(files); - }); - }; - - const chooseFileFromUrl = () => { - os.inputText({ - title: i18n.ts.uploadFromUrl, - type: 'url', - placeholder: i18n.ts.uploadFromUrlDescription, - }).then(({ canceled, result: url }) => { - if (canceled) return; - - const marker = Math.random().toString(); // TODO: UUIDとか使う - - const connection = stream.useChannel('main'); - connection.on('urlUploadFinished', urlResponse => { - if (urlResponse.marker === marker) { - res(multiple ? [urlResponse.file] : urlResponse.file); - connection.dispose(); - } - }); - - os.api('drive/files/upload-from-url', { - url: url, - folderId: defaultStore.state.uploadFolder, - marker, - }); - - os.alert({ - title: i18n.ts.uploadFromUrlRequested, - text: i18n.ts.uploadFromUrlMayTakeTime, - }); - }); - }; - - os.popupMenu([label ? { - text: label, - type: 'label', - } : undefined, { - type: 'switch', - text: i18n.ts.keepOriginalUploading, - ref: keepOriginal, - }, { - text: i18n.ts.upload, - icon: 'ti ti-upload', - action: chooseFileFromPc, - }, { - text: i18n.ts.fromDrive, - icon: 'ti ti-cloud', - action: chooseFileFromDrive, - }, { - text: i18n.ts.fromUrl, - icon: 'ti ti-link', - action: chooseFileFromUrl, - }], src); - }); -} - -export function selectFile(src: any, label: string | null = null): Promise { - return select(src, label, false) as Promise; -} - -export function selectFiles(src: any, label: string | null = null): Promise { - return select(src, label, true) as Promise; -} diff --git a/packages/client/src/scripts/show-suspended-dialog.ts b/packages/client/src/scripts/show-suspended-dialog.ts deleted file mode 100644 index e11569ecd4..0000000000 --- a/packages/client/src/scripts/show-suspended-dialog.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as os from '@/os'; -import { i18n } from '@/i18n'; - -export function showSuspendedDialog() { - return os.alert({ - type: 'error', - title: i18n.ts.yourAccountSuspendedTitle, - text: i18n.ts.yourAccountSuspendedDescription, - }); -} diff --git a/packages/client/src/scripts/shuffle.ts b/packages/client/src/scripts/shuffle.ts deleted file mode 100644 index 05e6cdfbcf..0000000000 --- a/packages/client/src/scripts/shuffle.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 配列をシャッフル (破壊的) - */ -export function shuffle(array: T): T { - let currentIndex = array.length, randomIndex; - - // While there remain elements to shuffle. - while (currentIndex !== 0) { - // Pick a remaining element. - randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex--; - - // And swap it with the current element. - [array[currentIndex], array[randomIndex]] = [ - array[randomIndex], array[currentIndex]]; - } - - return array; -} diff --git a/packages/client/src/scripts/sound.ts b/packages/client/src/scripts/sound.ts deleted file mode 100644 index 9d1f603235..0000000000 --- a/packages/client/src/scripts/sound.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ColdDeviceStorage } from '@/store'; - -const cache = new Map(); - -export const soundsTypes = [ - null, - 'syuilo/up', - 'syuilo/down', - 'syuilo/pope1', - 'syuilo/pope2', - 'syuilo/waon', - 'syuilo/popo', - 'syuilo/triple', - 'syuilo/poi1', - 'syuilo/poi2', - 'syuilo/pirori', - 'syuilo/pirori-wet', - 'syuilo/pirori-square-wet', - 'syuilo/square-pico', - 'syuilo/reverved', - 'syuilo/ryukyu', - 'syuilo/kick', - 'syuilo/snare', - 'syuilo/queue-jammed', - 'aisha/1', - 'aisha/2', - 'aisha/3', - 'noizenecio/kick_gaba1', - 'noizenecio/kick_gaba2', - 'noizenecio/kick_gaba3', - 'noizenecio/kick_gaba4', - 'noizenecio/kick_gaba5', - 'noizenecio/kick_gaba6', - 'noizenecio/kick_gaba7', -] as const; - -export function getAudio(file: string, useCache = true): HTMLAudioElement { - let audio: HTMLAudioElement; - if (useCache && cache.has(file)) { - audio = cache.get(file); - } else { - audio = new Audio(`/client-assets/sounds/${file}.mp3`); - if (useCache) cache.set(file, audio); - } - return audio; -} - -export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement { - const masterVolume = ColdDeviceStorage.get('sound_masterVolume'); - audio.volume = masterVolume - ((1 - volume) * masterVolume); - return audio; -} - -export function play(type: string) { - const sound = ColdDeviceStorage.get('sound_' + type as any); - if (sound.type == null) return; - playFile(sound.type, sound.volume); -} - -export function playFile(file: string, volume: number) { - const masterVolume = ColdDeviceStorage.get('sound_masterVolume'); - if (masterVolume === 0) return; - - const audio = setVolume(getAudio(file), volume); - audio.play(); -} diff --git a/packages/client/src/scripts/sticky-sidebar.ts b/packages/client/src/scripts/sticky-sidebar.ts deleted file mode 100644 index c67b8f37ac..0000000000 --- a/packages/client/src/scripts/sticky-sidebar.ts +++ /dev/null @@ -1,50 +0,0 @@ -export class StickySidebar { - private lastScrollTop = 0; - private container: HTMLElement; - private el: HTMLElement; - private spacer: HTMLElement; - private marginTop: number; - private isTop = false; - private isBottom = false; - private offsetTop: number; - private globalHeaderHeight: number = 59; - - constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) { - this.container = container; - this.el = this.container.children[0] as HTMLElement; - this.el.style.position = 'sticky'; - this.spacer = document.createElement('div'); - this.container.prepend(this.spacer); - this.marginTop = marginTop; - this.offsetTop = this.container.getBoundingClientRect().top; - this.globalHeaderHeight = globalHeaderHeight; - } - - public calc(scrollTop: number) { - if (scrollTop > this.lastScrollTop) { // downscroll - const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight); - this.el.style.bottom = null; - this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`; - - this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight); - - if (this.isTop) { - this.isTop = false; - this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`; - } - } else { // upscroll - const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight; - this.el.style.top = null; - this.el.style.bottom = `${-overflow}px`; - - this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop; - - if (this.isBottom) { - this.isBottom = false; - this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`; - } - } - - this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop; - } -} diff --git a/packages/client/src/scripts/theme-editor.ts b/packages/client/src/scripts/theme-editor.ts deleted file mode 100644 index 944875ff15..0000000000 --- a/packages/client/src/scripts/theme-editor.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { v4 as uuid } from 'uuid'; - -import { themeProps, Theme } from './theme'; - -export type Default = null; -export type Color = string; -export type FuncName = 'alpha' | 'darken' | 'lighten'; -export type Func = { type: 'func'; name: FuncName; arg: number; value: string; }; -export type RefProp = { type: 'refProp'; key: string; }; -export type RefConst = { type: 'refConst'; key: string; }; -export type Css = { type: 'css'; value: string; }; - -export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default; - -export type ThemeViewModel = [ string, ThemeValue ][]; - -export const fromThemeString = (str?: string) : ThemeValue => { - if (!str) return null; - if (str.startsWith(':')) { - const parts = str.slice(1).split('<'); - const name = parts[0] as FuncName; - const arg = parseFloat(parts[1]); - const value = parts[2].startsWith('@') ? parts[2].slice(1) : ''; - return { type: 'func', name, arg, value }; - } else if (str.startsWith('@')) { - return { - type: 'refProp', - key: str.slice(1), - }; - } else if (str.startsWith('$')) { - return { - type: 'refConst', - key: str.slice(1), - }; - } else if (str.startsWith('"')) { - return { - type: 'css', - value: str.substr(1).trim(), - }; - } else { - return str; - } -}; - -export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => { - if (typeof value === 'string') return value; - switch (value.type) { - case 'func': return `:${value.name}<${value.arg}<@${value.value}`; - case 'refProp': return `@${value.key}`; - case 'refConst': return `$${value.key}`; - case 'css': return `" ${value.value}`; - } -}; - -export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => { - const props = { } as { [key: string]: string }; - for (const [key, value] of vm) { - if (value === null) continue; - props[key] = toThemeString(value); - } - - return { - id: uuid(), - name, desc, author, props, base, - }; -}; - -export const convertToViewModel = (theme: Theme): ThemeViewModel => { - const vm: ThemeViewModel = []; - // プロパティの登録 - vm.push(...themeProps.map(key => [key, fromThemeString(theme.props[key])] as [ string, ThemeValue ])); - - // 定数の登録 - const consts = Object - .keys(theme.props) - .filter(k => k.startsWith('$')) - .map(k => [k, fromThemeString(theme.props[k])] as [ string, ThemeValue ]); - - vm.push(...consts); - return vm; -}; diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts deleted file mode 100644 index 62a2b9459a..0000000000 --- a/packages/client/src/scripts/theme.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { ref } from 'vue'; -import tinycolor from 'tinycolor2'; -import { globalEvents } from '@/events'; - -export type Theme = { - id: string; - name: string; - author: string; - desc?: string; - base?: 'dark' | 'light'; - props: Record; -}; - -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; -import { deepClone } from './clone'; - -export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); - -export const getBuiltinThemes = () => Promise.all( - [ - 'l-light', - 'l-coffee', - 'l-apricot', - 'l-rainy', - 'l-vivid', - 'l-cherry', - 'l-sushi', - 'l-u0', - - 'd-dark', - 'd-persimmon', - 'd-astro', - 'd-future', - 'd-botanical', - 'd-green-lime', - 'd-green-orange', - 'd-cherry', - 'd-ice', - 'd-u0', - ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)), -); - -export const getBuiltinThemesRef = () => { - const builtinThemes = ref([]); - getBuiltinThemes().then(themes => builtinThemes.value = themes); - return builtinThemes; -}; - -let timeout = null; - -export function applyTheme(theme: Theme, persist = true) { - if (timeout) window.clearTimeout(timeout); - - document.documentElement.classList.add('_themeChanging_'); - - timeout = window.setTimeout(() => { - document.documentElement.classList.remove('_themeChanging_'); - }, 1000); - - const colorSchema = theme.base === 'dark' ? 'dark' : 'light'; - - // Deep copy - const _theme = deepClone(theme); - - if (_theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); - if (base) _theme.props = Object.assign({}, base.props, _theme.props); - } - - const props = compile(_theme); - - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', props['htmlThemeColor']); - break; - } - } - - for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--${k}`, v.toString()); - } - - document.documentElement.style.setProperty('color-schema', colorSchema); - - if (persist) { - localStorage.setItem('theme', JSON.stringify(props)); - localStorage.setItem('colorSchema', colorSchema); - } - - // 色計算など再度行えるようにクライアント全体に通知 - globalEvents.emit('themeChanged'); -} - -function compile(theme: Theme): Record { - function getColor(val: string): tinycolor.Instance { - // ref (prop) - if (val[0] === '@') { - return getColor(theme.props[val.substr(1)]); - } - - // ref (const) - else if (val[0] === '$') { - return getColor(theme.props[val]); - } - - // func - else if (val[0] === ':') { - const parts = val.split('<'); - const func = parts.shift().substr(1); - const arg = parseFloat(parts.shift()); - const color = getColor(parts.join('<')); - - switch (func) { - case 'darken': return color.darken(arg); - case 'lighten': return color.lighten(arg); - case 'alpha': return color.setAlpha(arg); - case 'hue': return color.spin(arg); - case 'saturate': return color.saturate(arg); - } - } - - // other case - return tinycolor(val); - } - - const props = {}; - - for (const [k, v] of Object.entries(theme.props)) { - if (k.startsWith('$')) continue; // ignore const - - props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); - } - - return props; -} - -function genValue(c: tinycolor.Instance): string { - return c.toRgbString(); -} - -export function validateTheme(theme: Record): boolean { - if (theme.id == null || typeof theme.id !== 'string') return false; - if (theme.name == null || typeof theme.name !== 'string') return false; - if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false; - if (theme.props == null || typeof theme.props !== 'object') return false; - return true; -} diff --git a/packages/client/src/scripts/time.ts b/packages/client/src/scripts/time.ts deleted file mode 100644 index 34e8b6b17c..0000000000 --- a/packages/client/src/scripts/time.ts +++ /dev/null @@ -1,39 +0,0 @@ -const dateTimeIntervals = { - 'day': 86400000, - 'hour': 3600000, - 'ms': 1, -}; - -export function dateUTC(time: number[]): Date { - const d = time.length === 2 ? Date.UTC(time[0], time[1]) - : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) - : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) - : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) - : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) - : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) - : null; - - if (!d) throw 'wrong number of arguments'; - - return new Date(d); -} - -export function isTimeSame(a: Date, b: Date): boolean { - return a.getTime() === b.getTime(); -} - -export function isTimeBefore(a: Date, b: Date): boolean { - return (a.getTime() - b.getTime()) < 0; -} - -export function isTimeAfter(a: Date, b: Date): boolean { - return (a.getTime() - b.getTime()) > 0; -} - -export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { - return new Date(x.getTime() + (value * dateTimeIntervals[span])); -} - -export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { - return new Date(x.getTime() - (value * dateTimeIntervals[span])); -} diff --git a/packages/client/src/scripts/timezones.ts b/packages/client/src/scripts/timezones.ts deleted file mode 100644 index 8ce07323f6..0000000000 --- a/packages/client/src/scripts/timezones.ts +++ /dev/null @@ -1,49 +0,0 @@ -export const timezones = [{ - name: 'UTC', - abbrev: 'UTC', - offset: 0, -}, { - name: 'Europe/Berlin', - abbrev: 'CET', - offset: 60, -}, { - name: 'Asia/Tokyo', - abbrev: 'JST', - offset: 540, -}, { - name: 'Asia/Seoul', - abbrev: 'KST', - offset: 540, -}, { - name: 'Asia/Shanghai', - abbrev: 'CST', - offset: 480, -}, { - name: 'Australia/Sydney', - abbrev: 'AEST', - offset: 600, -}, { - name: 'Australia/Darwin', - abbrev: 'ACST', - offset: 570, -}, { - name: 'Australia/Perth', - abbrev: 'AWST', - offset: 480, -}, { - name: 'America/New_York', - abbrev: 'EST', - offset: -300, -}, { - name: 'America/Mexico_City', - abbrev: 'CST', - offset: -360, -}, { - name: 'America/Phoenix', - abbrev: 'MST', - offset: -420, -}, { - name: 'America/Los_Angeles', - abbrev: 'PST', - offset: -480, -}]; diff --git a/packages/client/src/scripts/touch.ts b/packages/client/src/scripts/touch.ts deleted file mode 100644 index 5251bc2e27..0000000000 --- a/packages/client/src/scripts/touch.ts +++ /dev/null @@ -1,23 +0,0 @@ -const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; - -export let isTouchUsing = false; - -export let isScreenTouching = false; - -if (isTouchSupported) { - window.addEventListener('touchstart', () => { - // maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも - // タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする - isTouchUsing = true; - - isScreenTouching = true; - }, { passive: true }); - - window.addEventListener('touchend', () => { - // 子要素のtouchstartイベントでstopPropagation()が呼ばれると親要素に伝搬されずタッチされたと判定されないため、 - // touchendイベントでもtouchstartイベントと同様にtrueにする - isTouchUsing = true; - - isScreenTouching = false; - }, { passive: true }); -} diff --git a/packages/client/src/scripts/unison-reload.ts b/packages/client/src/scripts/unison-reload.ts deleted file mode 100644 index 59af584c1b..0000000000 --- a/packages/client/src/scripts/unison-reload.ts +++ /dev/null @@ -1,15 +0,0 @@ -// SafariがBroadcastChannel未実装なのでライブラリを使う -import { BroadcastChannel } from 'broadcast-channel'; - -export const reloadChannel = new BroadcastChannel('reload'); - -// BroadcastChannelを用いて、クライアントが一斉にreloadするようにします。 -export function unisonReload(path?: string) { - if (path !== undefined) { - reloadChannel.postMessage(path); - location.href = path; - } else { - reloadChannel.postMessage(null); - location.reload(); - } -} diff --git a/packages/client/src/scripts/upload.ts b/packages/client/src/scripts/upload.ts deleted file mode 100644 index 9a39652ef5..0000000000 --- a/packages/client/src/scripts/upload.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { reactive, ref } from 'vue'; -import * as Misskey from 'misskey-js'; -import { readAndCompressImage } from 'browser-image-resizer'; -import { getCompressionConfig } from './upload/compress-config'; -import { defaultStore } from '@/store'; -import { apiUrl } from '@/config'; -import { $i } from '@/account'; -import { alert } from '@/os'; -import { i18n } from '@/i18n'; - -type Uploading = { - id: string; - name: string; - progressMax: number | undefined; - progressValue: number | undefined; - img: string; -}; -export const uploads = ref([]); - -const mimeTypeMap = { - 'image/webp': 'webp', - 'image/jpeg': 'jpg', - 'image/png': 'png', -} as const; - -export function uploadFile( - file: File, - folder?: any, - name?: string, - keepOriginal: boolean = defaultStore.state.keepOriginalUploading, -): Promise { - if ($i == null) throw new Error('Not logged in'); - - if (folder && typeof folder === 'object') folder = folder.id; - - return new Promise((resolve, reject) => { - const id = Math.random().toString(); - - const reader = new FileReader(); - reader.onload = async (): Promise => { - const ctx = reactive({ - id: id, - name: name ?? file.name ?? 'untitled', - progressMax: undefined, - progressValue: undefined, - img: window.URL.createObjectURL(file), - }); - - uploads.value.push(ctx); - - const config = !keepOriginal ? await getCompressionConfig(file) : undefined; - let resizedImage: Blob | undefined; - if (config) { - try { - const resized = await readAndCompressImage(file, config); - if (resized.size < file.size || file.type === 'image/webp') { - // The compression may not always reduce the file size - // (and WebP is not browser safe yet) - resizedImage = resized; - } - if (_DEV_) { - const saved = ((1 - resized.size / file.size) * 100).toFixed(2); - console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`); - } - - ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name; - } catch (err) { - console.error('Failed to resize image', err); - } - } - - const formData = new FormData(); - formData.append('i', $i.token); - formData.append('force', 'true'); - formData.append('file', resizedImage ?? file); - formData.append('name', ctx.name); - if (folder) formData.append('folderId', folder); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = ((ev: ProgressEvent) => { - if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { - // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい - uploads.value = uploads.value.filter(x => x.id !== id); - - if (ev.target?.response) { - const res = JSON.parse(ev.target.response); - if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseInappropriate, - }); - } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseNoFreeSpace, - }); - } else { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, - }); - } - } else { - alert({ - type: 'error', - title: 'Failed to upload', - text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, - }); - } - - reject(); - return; - } - - const driveFile = JSON.parse(ev.target.response); - - resolve(driveFile); - - uploads.value = uploads.value.filter(x => x.id !== id); - }) as (ev: ProgressEvent) => any; - - xhr.upload.onprogress = ev => { - if (ev.lengthComputable) { - ctx.progressMax = ev.total; - ctx.progressValue = ev.loaded; - } - }; - - xhr.send(formData); - }; - reader.readAsArrayBuffer(file); - }); -} diff --git a/packages/client/src/scripts/upload/compress-config.ts b/packages/client/src/scripts/upload/compress-config.ts deleted file mode 100644 index 793c78ad20..0000000000 --- a/packages/client/src/scripts/upload/compress-config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import isAnimated from 'is-file-animated'; -import type { BrowserImageResizerConfig } from 'browser-image-resizer'; - -const compressTypeMap = { - 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/png': { quality: 1, mimeType: 'image/png' }, - 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, -} as const; - -export async function getCompressionConfig(file: File): Promise { - const imgConfig = compressTypeMap[file.type]; - if (!imgConfig || await isAnimated(file)) { - return; - } - - return { - maxWidth: 2048, - maxHeight: 2048, - debug: true, - ...imgConfig, - }; -} diff --git a/packages/client/src/scripts/url.ts b/packages/client/src/scripts/url.ts deleted file mode 100644 index 86735de9f0..0000000000 --- a/packages/client/src/scripts/url.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function query(obj: Record): string { - const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) - .reduce((a, [k, v]) => (a[k] = v, a), {} as Record); - - return Object.entries(params) - .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) - .join('&'); -} - -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; -} diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts deleted file mode 100644 index 881e5e9ad5..0000000000 --- a/packages/client/src/scripts/use-chart-tooltip.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { onUnmounted, ref } from 'vue'; -import * as os from '@/os'; -import MkChartTooltip from '@/components/MkChartTooltip.vue'; - -export function useChartTooltip(opts: { position: 'top' | 'middle' } = { position: 'top' }) { - const tooltipShowing = ref(false); - const tooltipX = ref(0); - const tooltipY = ref(0); - const tooltipTitle = ref(null); - const tooltipSeries = ref(null); - let disposeTooltipComponent; - - os.popup(MkChartTooltip, { - showing: tooltipShowing, - x: tooltipX, - y: tooltipY, - title: tooltipTitle, - series: tooltipSeries, - }, {}).then(({ dispose }) => { - disposeTooltipComponent = dispose; - }); - - onUnmounted(() => { - if (disposeTooltipComponent) disposeTooltipComponent(); - }); - - function handler(context) { - if (context.tooltip.opacity === 0) { - tooltipShowing.value = false; - return; - } - - tooltipTitle.value = context.tooltip.title[0]; - tooltipSeries.value = context.tooltip.body.map((b, i) => ({ - backgroundColor: context.tooltip.labelColors[i].backgroundColor, - borderColor: context.tooltip.labelColors[i].borderColor, - text: b.lines[0], - })); - - const rect = context.chart.canvas.getBoundingClientRect(); - - tooltipShowing.value = true; - tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; - if (opts.position === 'top') { - tooltipY.value = rect.top + window.pageYOffset; - } else if (opts.position === 'middle') { - tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; - } - } - - return { - handler, - }; -} diff --git a/packages/client/src/scripts/use-interval.ts b/packages/client/src/scripts/use-interval.ts deleted file mode 100644 index 201ba417ef..0000000000 --- a/packages/client/src/scripts/use-interval.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { onMounted, onUnmounted } from 'vue'; - -export function useInterval(fn: () => void, interval: number, options: { - immediate: boolean; - afterMounted: boolean; -}): void { - if (Number.isNaN(interval)) return; - - let intervalId: number | null = null; - - if (options.afterMounted) { - onMounted(() => { - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - }); - } else { - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - } - - onUnmounted(() => { - if (intervalId) window.clearInterval(intervalId); - }); -} diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts deleted file mode 100644 index a93b84d1fe..0000000000 --- a/packages/client/src/scripts/use-leave-guard.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { inject, onUnmounted, Ref } from 'vue'; -import { i18n } from '@/i18n'; -import * as os from '@/os'; - -export function useLeaveGuard(enabled: Ref) { - /* TODO - const setLeaveGuard = inject('setLeaveGuard'); - - if (setLeaveGuard) { - setLeaveGuard(async () => { - if (!enabled.value) return false; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return canceled; - }); - } else { - onBeforeRouteLeave(async (to, from) => { - if (!enabled.value) return true; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.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 deleted file mode 100644 index e6bdb345c4..0000000000 --- a/packages/client/src/scripts/use-note-capture.ts +++ /dev/null @@ -1,110 +0,0 @@ -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; - note: Ref; - isDeletedRef: Ref; -}) { - const note = props.note; - const connection = $i ? stream : null; - - function onStreamNoteUpdated(noteData): void { - const { type, id, body } = noteData; - - if (id !== note.value.id) return; - - switch (type) { - case 'reacted': { - const reaction = body.reaction; - - if (body.emoji) { - const emojis = note.value.emojis || []; - if (!emojis.includes(body.emoji)) { - note.value.emojis = [...emojis, body.emoji]; - } - } - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (note.value.reactions || {})[reaction] || 0; - - note.value.reactions[reaction] = currentCount + 1; - - if ($i && (body.userId === $i.id)) { - note.value.myReaction = reaction; - } - break; - } - - case 'unreacted': { - const reaction = body.reaction; - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (note.value.reactions || {})[reaction] || 0; - - note.value.reactions[reaction] = Math.max(0, currentCount - 1); - - if ($i && (body.userId === $i.id)) { - note.value.myReaction = null; - } - break; - } - - case 'pollVoted': { - const choice = body.choice; - - const choices = [...note.value.poll.choices]; - choices[choice] = { - ...choices[choice], - votes: choices[choice].votes + 1, - ...($i && (body.userId === $i.id) ? { - isVoted: true, - } : {}), - }; - - note.value.poll.choices = choices; - break; - } - - case 'deleted': { - props.isDeletedRef.value = true; - break; - } - } - } - - function capture(withHandler = false): void { - if (connection) { - // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id }); - if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); - } - } - - function decapture(withHandler = false): void { - if (connection) { - connection.send('un', { - id: note.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/scripts/use-tooltip.ts b/packages/client/src/scripts/use-tooltip.ts deleted file mode 100644 index 1f6e0fb6ce..0000000000 --- a/packages/client/src/scripts/use-tooltip.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Ref, ref, watch, onUnmounted } from 'vue'; - -export function useTooltip( - elRef: Ref, - onShow: (showing: Ref) => void, - delay = 300, -): void { - let isHovering = false; - - // iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ - // 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる - // TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...? - let shouldIgnoreMouseover = false; - - let timeoutId: number; - - let changeShowingState: (() => void) | null; - - const open = () => { - close(); - if (!isHovering) return; - if (elRef.value == null) return; - const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; - if (!document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため - - const showing = ref(true); - onShow(showing); - changeShowingState = () => { - showing.value = false; - }; - }; - - const close = () => { - if (changeShowingState != null) { - changeShowingState(); - changeShowingState = null; - } - }; - - const onMouseover = () => { - if (isHovering) return; - if (shouldIgnoreMouseover) return; - isHovering = true; - timeoutId = window.setTimeout(open, delay); - }; - - const onMouseleave = () => { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - close(); - }; - - const onTouchstart = () => { - shouldIgnoreMouseover = true; - if (isHovering) return; - isHovering = true; - timeoutId = window.setTimeout(open, delay); - }; - - const onTouchend = () => { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - close(); - }; - - const stop = watch(elRef, () => { - if (elRef.value) { - stop(); - const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; - el.addEventListener('mouseover', onMouseover, { passive: true }); - el.addEventListener('mouseleave', onMouseleave, { passive: true }); - el.addEventListener('touchstart', onTouchstart, { passive: true }); - el.addEventListener('touchend', onTouchend, { passive: true }); - el.addEventListener('click', close, { passive: true }); - } - }, { - immediate: true, - flush: 'post', - }); - - onUnmounted(() => { - close(); - }); -} diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts deleted file mode 100644 index 1bedab5fad..0000000000 --- a/packages/client/src/store.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { markRaw, ref } from 'vue'; -import { Storage } from './pizzax'; -import { Theme } from './scripts/theme'; - -interface PostFormAction { - title: string, - handler: (form: T, update: (key: unknown, value: unknown) => void) => void; -} - -interface UserAction { - title: string, - handler: (user: UserDetailed) => void; -} - -interface NoteAction { - title: string, - handler: (note: Note) => void; -} - -interface NoteViewInterruptor { - handler: (note: Note) => unknown; -} - -interface NotePostInterruptor { - handler: (note: FIXME) => unknown; -} - -export const postFormActions: PostFormAction[] = []; -export const userActions: UserAction[] = []; -export const noteActions: NoteAction[] = []; -export const noteViewInterruptors: NoteViewInterruptor[] = []; -export const notePostInterruptors: NotePostInterruptor[] = []; - -// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう) -// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない -export const defaultStore = markRaw(new Storage('base', { - tutorial: { - where: 'account', - default: 0, - }, - keepCw: { - where: 'account', - default: true, - }, - showFullAcct: { - where: 'account', - default: false, - }, - rememberNoteVisibility: { - where: 'account', - default: false, - }, - defaultNoteVisibility: { - where: 'account', - default: 'public', - }, - defaultNoteLocalOnly: { - where: 'account', - default: false, - }, - uploadFolder: { - where: 'account', - default: null as string | null, - }, - pastedFileName: { - where: 'account', - default: 'yyyy-MM-dd HH-mm-ss [{{number}}]', - }, - keepOriginalUploading: { - where: 'account', - default: false, - }, - memo: { - where: 'account', - default: null, - }, - reactions: { - where: 'account', - default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], - }, - mutedWords: { - where: 'account', - default: [], - }, - mutedAds: { - where: 'account', - default: [] as string[], - }, - - menu: { - where: 'deviceAccount', - default: [ - 'notifications', - 'favorites', - 'drive', - 'followRequests', - '-', - 'explore', - 'announcements', - 'search', - '-', - 'ui', - ], - }, - visibility: { - where: 'deviceAccount', - default: 'public' as 'public' | 'home' | 'followers' | 'specified', - }, - localOnly: { - where: 'deviceAccount', - default: false, - }, - statusbars: { - where: 'deviceAccount', - default: [] as { - name: string; - id: string; - type: string; - size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge'; - black: boolean; - props: Record; - }[], - }, - widgets: { - where: 'deviceAccount', - default: [] as { - name: string; - id: string; - place: string | null; - data: Record; - }[], - }, - tl: { - where: 'deviceAccount', - default: { - src: 'home' as 'home' | 'local' | 'social' | 'global', - arg: null, - }, - }, - - overridedDeviceKind: { - where: 'device', - default: null as null | 'smartphone' | 'tablet' | 'desktop', - }, - serverDisconnectedBehavior: { - where: 'device', - default: 'quiet' as 'quiet' | 'reload' | 'dialog', - }, - nsfw: { - where: 'device', - default: 'respect' as 'respect' | 'force' | 'ignore', - }, - animation: { - where: 'device', - default: true, - }, - animatedMfm: { - where: 'device', - default: false, - }, - loadRawImages: { - where: 'device', - default: false, - }, - imageNewTab: { - where: 'device', - default: false, - }, - disableShowingAnimatedImages: { - where: 'device', - default: false, - }, - disablePagesScript: { - where: 'device', - default: false, - }, - emojiStyle: { - where: 'device', - default: 'twemoji', // twemoji / fluentEmoji / native - }, - disableDrawer: { - where: 'device', - default: false, - }, - useBlurEffectForModal: { - where: 'device', - default: true, - }, - useBlurEffect: { - where: 'device', - default: true, - }, - showFixedPostForm: { - where: 'device', - default: false, - }, - enableInfiniteScroll: { - where: 'device', - default: true, - }, - useReactionPickerForContextMenu: { - where: 'device', - default: false, - }, - showGapBetweenNotesInTimeline: { - where: 'device', - default: false, - }, - darkMode: { - where: 'device', - default: false, - }, - instanceTicker: { - where: 'device', - default: 'remote' as 'none' | 'remote' | 'always', - }, - reactionPickerSize: { - where: 'device', - default: 1, - }, - reactionPickerWidth: { - where: 'device', - default: 1, - }, - reactionPickerHeight: { - where: 'device', - default: 2, - }, - reactionPickerUseDrawerForMobile: { - where: 'device', - default: true, - }, - recentlyUsedEmojis: { - where: 'device', - default: [] as string[], - }, - recentlyUsedUsers: { - where: 'device', - default: [] as string[], - }, - defaultSideView: { - where: 'device', - default: false, - }, - menuDisplay: { - where: 'device', - default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top', - }, - reportError: { - where: 'device', - default: false, - }, - squareAvatars: { - where: 'device', - default: false, - }, - postFormWithHashtags: { - where: 'device', - default: false, - }, - postFormHashtags: { - where: 'device', - default: '', - }, - themeInitial: { - where: 'device', - default: true, - }, - numberOfPageCache: { - where: 'device', - default: 5, - }, - aiChanMode: { - where: 'device', - default: false, - }, -})); - -// TODO: 他のタブと永続化されたstateを同期 - -const PREFIX = 'miux:'; - -type Plugin = { - id: string; - name: string; - active: boolean; - configData: Record; - token: string; - ast: any[]; -}; - -interface Watcher { - key: string; - callback: (value: unknown) => void; -} - -/** - * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) - */ -import lightTheme from '@/themes/l-light.json5'; -import darkTheme from '@/themes/d-green-lime.json5'; -import { Note, UserDetailed } from 'misskey-js/built/entities'; - -export class ColdDeviceStorage { - public static default = { - lightTheme, - darkTheme, - syncDeviceDarkMode: true, - plugins: [] as Plugin[], - mediaVolume: 0.5, - sound_masterVolume: 0.3, - sound_note: { type: 'syuilo/down', volume: 1 }, - sound_noteMy: { type: 'syuilo/up', volume: 1 }, - sound_notification: { type: 'syuilo/pope2', volume: 1 }, - sound_chat: { type: 'syuilo/pope1', volume: 1 }, - sound_chatBg: { type: 'syuilo/waon', volume: 1 }, - sound_antenna: { type: 'syuilo/triple', volume: 1 }, - sound_channel: { type: 'syuilo/square-pico', volume: 1 }, - }; - - public static watchers: Watcher[] = []; - - public static get(key: T): typeof ColdDeviceStorage.default[T] { - // TODO: indexedDBにする - // ただしその際はnullチェックではなくキー存在チェックにしないとダメ - // (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある) - const value = localStorage.getItem(PREFIX + key); - if (value == null) { - return ColdDeviceStorage.default[key]; - } else { - return JSON.parse(value); - } - } - - public static set(key: T, value: typeof ColdDeviceStorage.default[T]): void { - // 呼び出し側のバグ等で undefined が来ることがある - // undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (value === undefined) { - console.error(`attempt to store undefined value for key '${key}'`); - return; - } - - localStorage.setItem(PREFIX + key, JSON.stringify(value)); - - for (const watcher of this.watchers) { - if (watcher.key === key) watcher.callback(value); - } - } - - public static watch(key, callback) { - this.watchers.push({ key, callback }); - } - - // TODO: VueのcustomRef使うと良い感じになるかも - public static ref(key: T) { - const v = ColdDeviceStorage.get(key); - const r = ref(v); - // TODO: このままではwatcherがリークするので開放する方法を考える - this.watch(key, v => { - r.value = v; - }); - return r; - } - - /** - * 特定のキーの、簡易的なgetter/setterを作ります - * 主にvue場で設定コントロールのmodelとして使う用 - */ - public static makeGetterSetter(key: K) { - // TODO: VueのcustomRef使うと良い感じになるかも - const valueRef = ColdDeviceStorage.ref(key); - return { - get: () => { - return valueRef.value; - }, - set: (value: unknown) => { - const val = value; - ColdDeviceStorage.set(key, val); - }, - }; - } -} diff --git a/packages/client/src/stream.ts b/packages/client/src/stream.ts deleted file mode 100644 index dea3459b86..0000000000 --- a/packages/client/src/stream.ts +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 8b7a846863..0000000000 --- a/packages/client/src/style.scss +++ /dev/null @@ -1,584 +0,0 @@ -@charset "utf-8"; - -:root { - --radius: 12px; - --marginFull: 16px; - --marginHalf: 10px; - - --margin: var(--marginFull); - - @media (max-width: 500px) { - --margin: var(--marginHalf); - } - - //--ad: rgb(255 169 0 / 10%); -} - -::selection { - color: #fff; - background-color: var(--accent); -} - -html { - touch-action: manipulation; - background-color: var(--bg); - background-attachment: fixed; - background-size: cover; - background-position: center; - color: var(--fg); - accent-color: var(--accent); - overflow: auto; - overflow-wrap: break-word; - font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; - font-size: 14px; - line-height: 1.35; - text-size-adjust: 100%; - tab-size: 2; - - &, * { - scrollbar-color: var(--scrollbarHandle) inherit; - scrollbar-width: thin; - - &::-webkit-scrollbar { - width: 6px; - height: 6px; - } - - &::-webkit-scrollbar-track { - background: inherit; - } - - &::-webkit-scrollbar-thumb { - background: var(--scrollbarHandle); - - &:hover { - background: var(--scrollbarHandleHover); - } - - &:active { - background: var(--accent); - } - } - } - - &.f-1 { - font-size: 15px; - } - - &.f-2 { - font-size: 16px; - } - - &.f-3 { - font-size: 17px; - } - - &.useSystemFont { - font-family: 'Hiragino Maru Gothic Pro', sans-serif; - } -} - -html._themeChanging_ { - &, * { - transition: background 1s ease, border 1s ease !important; - } -} - -html, body { - margin: 0; - padding: 0; - scroll-behavior: smooth; -} - -a { - text-decoration: none; - cursor: pointer; - color: inherit; - tap-highlight-color: transparent; - -webkit-tap-highlight-color: transparent; - - &:hover { - text-decoration: underline; - } -} - -textarea, input { - tap-highlight-color: transparent; - -webkit-tap-highlight-color: transparent; -} - -optgroup, option { - background: var(--panel); - color: var(--fg); -} - -hr { - margin: var(--margin) 0 var(--margin) 0; - border: none; - height: 1px; - background: var(--divider); -} - -.ti { - vertical-align: -10%; - line-height: 0.9em; - - &:before { - font-size: 130%; - } -} - -.ti-fw { - display: inline-block; - text-align: center; -} - -._indicatorCircle { - display: inline-block; - width: 1em; - height: 1em; - border-radius: 100%; - background: currentColor; -} - -._noSelect { - user-select: none; - -webkit-user-select: none; - -webkit-touch-callout: none; -} - -._ghost { - &, * { - @extend ._noSelect; - pointer-events: none; - } -} - -._modalBg { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--modalBg); - -webkit-backdrop-filter: var(--modalBgFilter); - backdrop-filter: var(--modalBgFilter); -} - -._shadow { - box-shadow: 0px 4px 32px var(--shadow) !important; -} - -._button { - appearance: none; - display: inline-block; - padding: 0; - margin: 0; // for Safari - background: none; - border: none; - cursor: pointer; - color: inherit; - touch-action: manipulation; - tap-highlight-color: transparent; - -webkit-tap-highlight-color: transparent; - font-size: 1em; - font-family: inherit; - line-height: inherit; - max-width: 100%; - - &, * { - @extend ._noSelect; - } - - * { - pointer-events: none; - } - - &:focus-visible { - outline: none; - } - - &:disabled { - opacity: 0.5; - cursor: default; - } -} - -._buttonPrimary { - @extend ._button; - color: var(--fgOnAccent); - background: var(--accent); - - &:not(:disabled):hover { - background: var(--X8); - } - - &:not(:disabled):active { - background: var(--X9); - } -} - -._buttonGradate { - @extend ._buttonPrimary; - color: var(--fgOnAccent); - background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); - - &:not(:disabled):hover { - background: linear-gradient(90deg, var(--X8), var(--X8)); - } - - &:not(:disabled):active { - background: linear-gradient(90deg, var(--X8), var(--X8)); - } -} - -._help { - color: var(--accent); - cursor: help -} - -._textButton { - @extend ._button; - color: var(--accent); - - &:not(:disabled):hover { - text-decoration: underline; - } -} - -._inputs { - display: flex; - margin: 32px 0; - - &:first-child { - margin-top: 8px; - } - - &:last-child { - margin-bottom: 8px; - } - - > * { - flex: 1; - margin: 0 !important; - - &:not(:first-child) { - margin-left: 8px !important; - } - - &:not(:last-child) { - margin-right: 8px !important; - } - } -} - -._panel { - background: var(--panel); - border-radius: var(--radius); - overflow: clip; -} - -._block { - @extend ._panel; - - & + ._block { - margin-top: var(--margin); - } -} - -._gap { - margin: var(--margin) 0; -} - -// TODO: 廃止 -._card { - @extend ._panel; - - // TODO: _cardTitle に - > ._title { - margin: 0; - padding: 22px 32px; - font-size: 1em; - border-bottom: solid 1px var(--panelHeaderDivider); - font-weight: bold; - background: var(--panelHeaderBg); - color: var(--panelHeaderFg); - - @media (max-width: 500px) { - padding: 16px; - font-size: 1em; - } - } - - // TODO: _cardContent に - > ._content { - padding: 32px; - - @media (max-width: 500px) { - padding: 16px; - } - - &._noPad { - padding: 0 !important; - } - - & + ._content { - border-top: solid 0.5px var(--divider); - } - } - - // TODO: _cardFooter に - > ._footer { - border-top: solid 0.5px var(--divider); - padding: 24px 32px; - - @media (max-width: 500px) { - padding: 16px; - } - } -} - -._borderButton { - @extend ._button; - display: block; - width: 100%; - padding: 10px; - box-sizing: border-box; - text-align: center; - border: solid 0.5px var(--divider); - border-radius: var(--radius); - - &:active { - border-color: var(--accent); - } -} - -._popup { - background: var(--popup); - border-radius: var(--radius); - contain: content; -} - -// TODO: 廃止 -._monolithic_ { - ._section:not(:empty) { - box-sizing: border-box; - padding: var(--root-margin, 32px); - - @media (max-width: 500px) { - --root-margin: 10px; - } - - & + ._section:not(:empty) { - border-top: solid 0.5px var(--divider); - } - } -} - -._narrow_ ._card { - > ._title { - padding: 16px; - font-size: 1em; - } - - > ._content { - padding: 16px; - } - - > ._footer { - padding: 16px; - } -} - -._acrylic { - background: var(--acrylicPanel); - -webkit-backdrop-filter: var(--blur, blur(15px)); - backdrop-filter: var(--blur, blur(15px)); -} - -._formBlock { - margin: 1.5em 0; -} - -._formRoot { - > ._formBlock:first-child { - margin-top: 0; - } - - > ._formBlock:last-child { - margin-bottom: 0; - } -} - -._formLinksGrid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - grid-gap: 12px; -} - -._formLinks { - > *:not(:last-child) { - margin-bottom: 8px; - } -} - -._beta { - margin-left: 0.7em; - font-size: 65%; - padding: 2px 3px; - color: var(--accent); - border: solid 1px var(--accent); - border-radius: 4px; - vertical-align: top; -} - -._table { - > ._row { - display: flex; - - &:not(:last-child) { - margin-bottom: 16px; - - @media (max-width: 500px) { - margin-bottom: 8px; - } - } - - > ._cell { - flex: 1; - - > ._label { - font-size: 80%; - opacity: 0.7; - - > ._icon { - margin-right: 4px; - display: none; - } - } - } - } -} - -._fullinfo { - padding: 64px 32px; - text-align: center; - - > img { - vertical-align: bottom; - height: 128px; - margin-bottom: 16px; - border-radius: 16px; - } -} - -._keyValue { - display: flex; - - > * { - flex: 1; - } -} - -._link { - color: var(--link); -} - -._caption { - font-size: 0.8em; - opacity: 0.7; -} - -._monospace { - font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; -} - -._code { - @extend ._monospace; - background: #2d2d2d; - color: #ccc; - font-size: 14px; - line-height: 1.5; - padding: 5px; -} - -.prism-editor__textarea:focus { - outline: none; -} - -._zoom { - transition-duration: 0.5s, 0.5s; - transition-property: opacity, transform; - transition-timing-function: cubic-bezier(0,.5,.5,1); -} - -.zoom-enter-active, .zoom-leave-active { - transition: opacity 0.5s, transform 0.5s !important; -} -.zoom-enter-from, .zoom-leave-to { - opacity: 0; - transform: scale(0.9); -} - -@keyframes blink { - 0% { opacity: 1; transform: scale(1); } - 30% { opacity: 1; transform: scale(1); } - 90% { opacity: 0; transform: scale(0.5); } -} - -@keyframes tada { - from { - transform: scale3d(1, 1, 1); - } - - 10%, - 20% { - transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - } - - 30%, - 50%, - 70%, - 90% { - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - } - - 40%, - 60%, - 80% { - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - } - - to { - transform: scale3d(1, 1, 1); - } -} - -._anime_bounce { - will-change: transform; - animation: bounce ease 0.7s; - animation-iteration-count: 1; - transform-origin: 50% 50%; -} -._anime_bounce_ready { - will-change: transform; - transform: scaleX(0.90) scaleY(0.90) ; -} -._anime_bounce_standBy { - transition: transform 0.1s ease; -} - -@keyframes bounce{ - 0% { - transform: scaleX(0.90) scaleY(0.90) ; - } - 19% { - transform: scaleX(1.10) scaleY(1.10) ; - } - 48% { - transform: scaleX(0.95) scaleY(0.95) ; - } - 100% { - transform: scaleX(1.00) scaleY(1.00) ; - } -} diff --git a/packages/client/src/theme-store.ts b/packages/client/src/theme-store.ts deleted file mode 100644 index fdc92ed793..0000000000 --- a/packages/client/src/theme-store.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { api } from '@/os'; -import { $i } from '@/account'; -import { Theme } from './scripts/theme'; - -const lsCacheKey = $i ? `themes:${$i.id}` : ''; - -export function getThemes(): Theme[] { - return JSON.parse(localStorage.getItem(lsCacheKey) || '[]'); -} - -export async function fetchThemes(): Promise { - if ($i == null) return; - - try { - const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' }); - localStorage.setItem(lsCacheKey, JSON.stringify(themes)); - } catch (err) { - if (err.code === 'NO_SUCH_KEY') return; - throw err; - } -} - -export async function addTheme(theme: Theme): Promise { - await fetchThemes(); - const themes = getThemes().concat(theme); - await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); - localStorage.setItem(lsCacheKey, JSON.stringify(themes)); -} - -export async function removeTheme(theme: Theme): Promise { - const themes = getThemes().filter(t => t.id !== theme.id); - await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); - localStorage.setItem(lsCacheKey, JSON.stringify(themes)); -} diff --git a/packages/client/src/themes/_dark.json5 b/packages/client/src/themes/_dark.json5 deleted file mode 100644 index 88ec8a5459..0000000000 --- a/packages/client/src/themes/_dark.json5 +++ /dev/null @@ -1,99 +0,0 @@ -// ダークテーマのベーステーマ -// このテーマが直接使われることは無い -{ - id: 'dark', - - name: 'Dark', - author: 'syuilo', - desc: 'Default dark theme', - kind: 'dark', - - props: { - accent: '#86b300', - accentDarken: ':darken<10<@accent', - accentLighten: ':lighten<10<@accent', - accentedBg: ':alpha<0.15<@accent', - focus: ':alpha<0.3<@accent', - bg: '#000', - acrylicBg: ':alpha<0.5<@bg', - fg: '#dadada', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgHighlighted: ':lighten<3<@fg', - fgOnAccent: '#fff', - divider: 'rgba(255, 255, 255, 0.1)', - indicator: '@accent', - panel: ':lighten<3<@bg', - panelHighlight: ':lighten<3<@panel', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - panelBorder: '" solid 1px var(--divider)', - acrylicPanel: ':alpha<0.5<@panel', - windowHeader: ':alpha<0.85<@panel', - popup: ':lighten<3<@panel', - shadow: 'rgba(0, 0, 0, 0.3)', - header: ':alpha<0.7<@panel', - navBg: '@panel', - navFg: '@fg', - navHoverFg: ':lighten<17<@fg', - navActive: '@accent', - navIndicator: '@indicator', - link: '#44a4c1', - hashtag: '#ff9156', - mention: '@accent', - mentionMe: '@mention', - renote: '#229e82', - modalBg: 'rgba(0, 0, 0, 0.5)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - dateLabelFg: '@fg', - infoBg: '#253142', - infoFg: '#fff', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - switchBg: 'rgba(255, 255, 255, 0.15)', - cwBg: '#687390', - cwFg: '#393f4f', - cwHoverBg: '#707b97', - buttonBg: 'rgba(255, 255, 255, 0.05)', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - swutchOffBg: 'rgba(255, 255, 255, 0.1)', - swutchOffFg: '@fg', - swutchOnBg: '@accentedBg', - swutchOnFg: '@accent', - inputBorder: 'rgba(255, 255, 255, 0.1)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - badge: '#31b1ce', - messageBg: '@bg', - success: '#86b300', - error: '#ec4137', - warn: '#ecb637', - codeString: '#ffb675', - codeNumber: '#cfff9e', - codeBoolean: '#c59eff', - deckDivider: '#000', - htmlThemeColor: '@bg', - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - }, -} diff --git a/packages/client/src/themes/_light.json5 b/packages/client/src/themes/_light.json5 deleted file mode 100644 index bad1291c83..0000000000 --- a/packages/client/src/themes/_light.json5 +++ /dev/null @@ -1,99 +0,0 @@ -// ライトテーマのベーステーマ -// このテーマが直接使われることは無い -{ - id: 'light', - - name: 'Light', - author: 'syuilo', - desc: 'Default light theme', - kind: 'light', - - props: { - accent: '#86b300', - accentDarken: ':darken<10<@accent', - accentLighten: ':lighten<10<@accent', - accentedBg: ':alpha<0.15<@accent', - focus: ':alpha<0.3<@accent', - bg: '#fff', - acrylicBg: ':alpha<0.5<@bg', - fg: '#5f5f5f', - fgTransparentWeak: ':alpha<0.75<@fg', - fgTransparent: ':alpha<0.5<@fg', - fgHighlighted: ':darken<3<@fg', - fgOnAccent: '#fff', - divider: 'rgba(0, 0, 0, 0.1)', - indicator: '@accent', - panel: ':lighten<3<@bg', - panelHighlight: ':darken<3<@panel', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - panelBorder: '" solid 1px var(--divider)', - acrylicPanel: ':alpha<0.5<@panel', - windowHeader: ':alpha<0.85<@panel', - popup: ':lighten<3<@panel', - shadow: 'rgba(0, 0, 0, 0.1)', - header: ':alpha<0.7<@panel', - navBg: '@panel', - navFg: '@fg', - navHoverFg: ':darken<17<@fg', - navActive: '@accent', - navIndicator: '@indicator', - link: '#44a4c1', - hashtag: '#ff9156', - mention: '@accent', - mentionMe: '@mention', - renote: '#229e82', - modalBg: 'rgba(0, 0, 0, 0.3)', - scrollbarHandle: 'rgba(0, 0, 0, 0.2)', - scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', - dateLabelFg: '@fg', - infoBg: '#e5f5ff', - infoFg: '#72818a', - infoWarnBg: '#fff0db', - infoWarnFg: '#8f6e31', - switchBg: 'rgba(0, 0, 0, 0.15)', - cwBg: '#b1b9c1', - cwFg: '#fff', - cwHoverBg: '#bbc4ce', - buttonBg: 'rgba(0, 0, 0, 0.05)', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - swutchOffBg: 'rgba(0, 0, 0, 0.1)', - swutchOffFg: '@panel', - swutchOnBg: '@accent', - swutchOnFg: '@fgOnAccent', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - listItemHoverBg: 'rgba(0, 0, 0, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', - wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', - badge: '#31b1ce', - messageBg: '@bg', - success: '#86b300', - error: '#ec4137', - warn: '#ecb637', - codeString: '#b98710', - codeNumber: '#0fbbbb', - codeBoolean: '#62b70c', - deckDivider: ':darken<3<@bg', - htmlThemeColor: '@bg', - X2: ':darken<2<@panel', - X3: 'rgba(0, 0, 0, 0.05)', - X4: 'rgba(0, 0, 0, 0.1)', - X5: 'rgba(0, 0, 0, 0.05)', - X6: 'rgba(0, 0, 0, 0.25)', - X7: 'rgba(0, 0, 0, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.1)', - X12: 'rgba(0, 0, 0, 0.1)', - X13: 'rgba(0, 0, 0, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - }, -} diff --git a/packages/client/src/themes/d-astro.json5 b/packages/client/src/themes/d-astro.json5 deleted file mode 100644 index c6a927ec3a..0000000000 --- a/packages/client/src/themes/d-astro.json5 +++ /dev/null @@ -1,78 +0,0 @@ -{ - id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea', - base: 'dark', - name: 'Mi Astro Dark', - author: 'syuilo', - props: { - bg: '#232125', - fg: '#efdab9', - cwBg: '#687390', - cwFg: '#393f4f', - link: '#78b0a0', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: '#2a272b', - accent: '#81c08b', - header: ':alpha<0.7<@bg', - infoBg: '#253142', - infoFg: '#fff', - renote: '#659CC8', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: 'rgba(255, 255, 255, 0.1)', - hashtag: '#ff9156', - mention: '#ffd152', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', - acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#707b97', - indicator: '@accent', - mentionMe: '#fb5d38', - messageBg: '@bg', - navActive: '@accent', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@accent', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - buttonGradateA: '@accent', - buttonGradateB: ':hue<-20<@accent', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - }, -} diff --git a/packages/client/src/themes/d-botanical.json5 b/packages/client/src/themes/d-botanical.json5 deleted file mode 100644 index c03b95e2d7..0000000000 --- a/packages/client/src/themes/d-botanical.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - id: '504debaf-4912-6a4c-5059-1db08a76b737', - - name: 'Mi Botanical Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(148, 179, 0)', - bg: 'rgb(37, 38, 36)', - fg: 'rgb(216, 212, 199)', - fgHighlighted: '#fff', - divider: 'rgba(255, 255, 255, 0.14)', - panel: 'rgb(47, 47, 44)', - panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', - header: ':alpha<0.7<@panel', - navBg: '#363636', - renote: '@accent', - mention: 'rgb(212, 153, 76)', - mentionMe: 'rgb(212, 210, 76)', - hashtag: '#5bcbb0', - link: '@accent', - }, -} diff --git a/packages/client/src/themes/d-cherry.json5 b/packages/client/src/themes/d-cherry.json5 deleted file mode 100644 index a7e1ad1c80..0000000000 --- a/packages/client/src/themes/d-cherry.json5 +++ /dev/null @@ -1,20 +0,0 @@ -{ - id: '679b3b87-a4e9-4789-8696-b56c15cc33b0', - - name: 'Mi Cherry Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(255, 89, 117)', - bg: 'rgb(28, 28, 37)', - fg: 'rgb(236, 239, 244)', - panel: 'rgb(35, 35, 47)', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - divider: 'rgb(63, 63, 80)', - }, -} diff --git a/packages/client/src/themes/d-dark.json5 b/packages/client/src/themes/d-dark.json5 deleted file mode 100644 index d24ce4df69..0000000000 --- a/packages/client/src/themes/d-dark.json5 +++ /dev/null @@ -1,26 +0,0 @@ -{ - id: '8050783a-7f63-445a-b270-36d0f6ba1677', - - name: 'Mi Dark', - author: 'syuilo', - desc: 'Default light theme', - - base: 'dark', - - props: { - bg: '#232323', - fg: 'rgb(199, 209, 216)', - fgHighlighted: '#fff', - divider: 'rgba(255, 255, 255, 0.14)', - panel: '#2d2d2d', - panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', - header: ':alpha<0.7<@panel', - navBg: '#363636', - renote: '@accent', - mention: '#da6d35', - mentionMe: '#d44c4c', - hashtag: '#4cb8d4', - link: '@accent', - }, -} diff --git a/packages/client/src/themes/d-future.json5 b/packages/client/src/themes/d-future.json5 deleted file mode 100644 index b6fa1ab0c1..0000000000 --- a/packages/client/src/themes/d-future.json5 +++ /dev/null @@ -1,27 +0,0 @@ -{ - id: '32a637ef-b47a-4775-bb7b-bacbb823f865', - - name: 'Mi Future Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#63e2b7', - bg: '#101014', - fg: '#D5D5D6', - fgHighlighted: '#fff', - fgOnAccent: '#000', - divider: 'rgba(255, 255, 255, 0.1)', - panel: '#18181c', - panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', - renote: '@accent', - mention: '#f2c97d', - mentionMe: '@accent', - hashtag: '#70c0e8', - link: '#e88080', - buttonGradateA: '@accent', - buttonGradateB: ':saturate<30<:hue<30<@accent', - }, -} diff --git a/packages/client/src/themes/d-green-lime.json5 b/packages/client/src/themes/d-green-lime.json5 deleted file mode 100644 index a6983b9ac2..0000000000 --- a/packages/client/src/themes/d-green-lime.json5 +++ /dev/null @@ -1,24 +0,0 @@ -{ - id: '02816013-8107-440f-877e-865083ffe194', - - name: 'Mi Green+Lime Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#b4e900', - bg: '#0C1210', - fg: '#dee7e4', - fgHighlighted: '#fff', - fgOnAccent: '#192320', - divider: '#e7fffb24', - panel: '#192320', - panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', - popup: '#293330', - renote: '@accent', - mentionMe: '#ffaa00', - link: '#24d7ce', - }, -} diff --git a/packages/client/src/themes/d-green-orange.json5 b/packages/client/src/themes/d-green-orange.json5 deleted file mode 100644 index 62adc39e29..0000000000 --- a/packages/client/src/themes/d-green-orange.json5 +++ /dev/null @@ -1,24 +0,0 @@ -{ - id: 'dc489603-27b5-424a-9b25-1ff6aec9824a', - - name: 'Mi Green+Orange Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#e97f00', - bg: '#0C1210', - fg: '#dee7e4', - fgHighlighted: '#fff', - fgOnAccent: '#192320', - divider: '#e7fffb24', - panel: '#192320', - panelHeaderBg: '@panel', - panelHeaderDivider: '@divider', - popup: '#293330', - renote: '@accent', - mentionMe: '#b4e900', - link: '#24d7ce', - }, -} diff --git a/packages/client/src/themes/d-ice.json5 b/packages/client/src/themes/d-ice.json5 deleted file mode 100644 index 179b060dcf..0000000000 --- a/packages/client/src/themes/d-ice.json5 +++ /dev/null @@ -1,13 +0,0 @@ -{ - id: '66e7e5a9-cd43-42cd-837d-12f47841fa34', - - name: 'Mi Ice Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: '#47BFE8', - bg: '#212526', - }, -} diff --git a/packages/client/src/themes/d-persimmon.json5 b/packages/client/src/themes/d-persimmon.json5 deleted file mode 100644 index e36265ff10..0000000000 --- a/packages/client/src/themes/d-persimmon.json5 +++ /dev/null @@ -1,25 +0,0 @@ -{ - id: 'c503d768-7c70-4db2-a4e6-08264304bc8d', - - name: 'Mi Persimmon Dark', - author: 'syuilo', - - base: 'dark', - - props: { - accent: 'rgb(206, 102, 65)', - bg: 'rgb(31, 33, 31)', - fg: '#cdd8c7', - fgHighlighted: '#fff', - divider: 'rgba(255, 255, 255, 0.14)', - panel: 'rgb(41, 43, 41)', - infoFg: '@fg', - infoBg: '#333c3b', - navBg: '#141714', - renote: '@accent', - mention: '@accent', - mentionMe: '#de6161', - hashtag: '#68bad0', - link: '#a1c758', - }, -} diff --git a/packages/client/src/themes/d-u0.json5 b/packages/client/src/themes/d-u0.json5 deleted file mode 100644 index b270f809ac..0000000000 --- a/packages/client/src/themes/d-u0.json5 +++ /dev/null @@ -1,88 +0,0 @@ -{ - id: '7a5bc13b-df8f-4d44-8e94-4452f0c634bb', - base: 'dark', - name: 'Mi U0 Dark', - props: { - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - bg: '#172426', - fg: '#dadada', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - cwBg: '#687390', - cwFg: '#393f4f', - link: '@accent', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: ':lighten<3<@bg', - popup: ':lighten<3<@panel', - accent: '#00a497', - header: ':alpha<0.7<@panel', - infoBg: '#253142', - infoFg: '#fff', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: 'rgba(255, 255, 255, 0.1)', - hashtag: '#e6b422', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: 'rgba(255, 255, 255, 0.05)', - switchBg: 'rgba(255, 255, 255, 0.15)', - acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#707b97', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - accentedBg: ':alpha<0.15<@accent', - codeNumber: '#cfff9e', - codeString: '#ffb675', - fgOnAccent: '#fff', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - codeBoolean: '#c59eff', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@indicator', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: 'rgba(255, 255, 255, 0.2)', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - deckDivider: '#142022', - }, -} diff --git a/packages/client/src/themes/l-apricot.json5 b/packages/client/src/themes/l-apricot.json5 deleted file mode 100644 index 1ed5525575..0000000000 --- a/packages/client/src/themes/l-apricot.json5 +++ /dev/null @@ -1,22 +0,0 @@ -{ - id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', - - name: 'Mi Apricot Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: 'rgb(234, 154, 82)', - bg: '#e6e5e2', - fg: 'rgb(149, 143, 139)', - panel: '#EEECE8', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - infoBg: 'rgb(226, 235, 241)', - }, -} diff --git a/packages/client/src/themes/l-cherry.json5 b/packages/client/src/themes/l-cherry.json5 deleted file mode 100644 index 5ad240241e..0000000000 --- a/packages/client/src/themes/l-cherry.json5 +++ /dev/null @@ -1,21 +0,0 @@ -{ - id: 'ac168876-f737-4074-a3fc-a370c732ef48', - - name: 'Mi Cherry Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: 'rgb(219, 96, 114)', - bg: 'rgb(254, 248, 249)', - fg: 'rgb(152, 13, 26)', - panel: 'rgb(255, 255, 255)', - renote: '@accent', - link: 'rgb(156, 187, 5)', - mention: '@accent', - hashtag: '@accent', - divider: 'rgba(134, 51, 51, 0.1)', - inputBorderHover: 'rgb(238, 221, 222)', - }, -} diff --git a/packages/client/src/themes/l-coffee.json5 b/packages/client/src/themes/l-coffee.json5 deleted file mode 100644 index fbcd4fa9ef..0000000000 --- a/packages/client/src/themes/l-coffee.json5 +++ /dev/null @@ -1,21 +0,0 @@ -{ - id: '6ed80faa-74f0-42c2-98e4-a64d9e138eab', - - name: 'Mi Coffee Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#9f8989', - bg: '#f5f3f3', - fg: '#7f6666', - panel: '#fff', - divider: 'rgba(87, 68, 68, 0.1)', - renote: 'rgb(160, 172, 125)', - link: 'rgb(137, 151, 159)', - mention: '@accent', - mentionMe: 'rgb(170, 149, 98)', - hashtag: '@accent', - }, -} diff --git a/packages/client/src/themes/l-light.json5 b/packages/client/src/themes/l-light.json5 deleted file mode 100644 index 248355c945..0000000000 --- a/packages/client/src/themes/l-light.json5 +++ /dev/null @@ -1,20 +0,0 @@ -{ - id: '4eea646f-7afa-4645-83e9-83af0333cd37', - - name: 'Mi Light', - author: 'syuilo', - desc: 'Default light theme', - - base: 'light', - - props: { - bg: '#f9f9f9', - fg: '#676767', - divider: '#e8e8e8', - header: ':alpha<0.7<@panel', - navBg: '#fff', - panel: '#fff', - panelHeaderDivider: '@divider', - mentionMe: 'rgb(0, 179, 70)', - }, -} diff --git a/packages/client/src/themes/l-rainy.json5 b/packages/client/src/themes/l-rainy.json5 deleted file mode 100644 index 283dd74c6c..0000000000 --- a/packages/client/src/themes/l-rainy.json5 +++ /dev/null @@ -1,21 +0,0 @@ -{ - id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96', - - name: 'Mi Rainy Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#5db0da', - bg: 'rgb(246 248 249)', - fg: '#636b71', - panel: '#fff', - divider: 'rgb(230 233 234)', - panelHeaderDivider: '@divider', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '@accent', - }, -} diff --git a/packages/client/src/themes/l-sushi.json5 b/packages/client/src/themes/l-sushi.json5 deleted file mode 100644 index 5846927d65..0000000000 --- a/packages/client/src/themes/l-sushi.json5 +++ /dev/null @@ -1,18 +0,0 @@ -{ - id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c', - - name: 'Mi Sushi Light', - author: 'syuilo', - - base: 'light', - - props: { - accent: '#e36749', - bg: '#f0eee9', - fg: '#5f5f5f', - renote: '@accent', - link: '@accent', - mention: '@accent', - hashtag: '#229e82', - }, -} diff --git a/packages/client/src/themes/l-u0.json5 b/packages/client/src/themes/l-u0.json5 deleted file mode 100644 index 03b114ba39..0000000000 --- a/packages/client/src/themes/l-u0.json5 +++ /dev/null @@ -1,87 +0,0 @@ -{ - id: 'e2c940b5-6e9a-4c03-b738-261c720c426d', - base: 'light', - name: 'Mi U0 Light', - props: { - X2: ':darken<2<@panel', - X3: 'rgba(255, 255, 255, 0.05)', - X4: 'rgba(255, 255, 255, 0.1)', - X5: 'rgba(255, 255, 255, 0.05)', - X6: 'rgba(255, 255, 255, 0.15)', - X7: 'rgba(255, 255, 255, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - bg: '#e7e7eb', - fg: '#5f5f5f', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.3)', - X12: 'rgba(255, 255, 255, 0.1)', - X13: 'rgba(255, 255, 255, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - cwBg: '#687390', - cwFg: '#393f4f', - link: '@accent', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: ':lighten<3<@bg', - popup: ':lighten<3<@panel', - accent: '#478384', - header: ':alpha<0.7<@panel', - infoBg: '#253142', - infoFg: '#fff', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.3)', - divider: '#4646461a', - hashtag: '#1f3134', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.5)', - success: '#86b300', - buttonBg: '#0000000d', - switchBg: 'rgba(255, 255, 255, 0.15)', - acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#707b97', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - accentedBg: ':alpha<0.15<@accent', - codeNumber: '#cfff9e', - codeString: '#ffb675', - fgOnAccent: '#fff', - infoWarnBg: '#42321c', - infoWarnFg: '#ffbd3e', - navHoverFg: ':lighten<17<@fg', - codeBoolean: '#c59eff', - dateLabelFg: '@fg', - inputBorder: 'rgba(255, 255, 255, 0.1)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@indicator', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: '#0000001a', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':lighten<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - buttonGradateA: '@accent', - buttonGradateB: ':hue<20<@accent', - htmlThemeColor: '@bg', - panelHighlight: ':lighten<3<@panel', - listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - scrollbarHandle: '#74747433', - inputBorderHover: 'rgba(255, 255, 255, 0.2)', - wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: 'rgba(0, 0, 0, 0)', - scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', - }, -} diff --git a/packages/client/src/themes/l-vivid.json5 b/packages/client/src/themes/l-vivid.json5 deleted file mode 100644 index b3c08f38ae..0000000000 --- a/packages/client/src/themes/l-vivid.json5 +++ /dev/null @@ -1,82 +0,0 @@ -{ - id: '6128c2a9-5c54-43fe-a47d-17942356470b', - - name: 'Mi Vivid Light', - author: 'syuilo', - - base: 'light', - - props: { - bg: '#fafafa', - fg: '#444', - cwBg: '#b1b9c1', - cwFg: '#fff', - link: '#ff9400', - warn: '#ecb637', - badge: '#31b1ce', - error: '#ec4137', - focus: ':alpha<0.3<@accent', - navBg: '@panel', - navFg: '@fg', - panel: '#fff', - accent: '#008cff', - header: ':alpha<0.7<@panel', - infoBg: '#e5f5ff', - infoFg: '#72818a', - renote: '@accent', - shadow: 'rgba(0, 0, 0, 0.1)', - divider: 'rgba(0, 0, 0, 0.08)', - hashtag: '#92d400', - mention: '@accent', - modalBg: 'rgba(0, 0, 0, 0.3)', - success: '#86b300', - buttonBg: 'rgba(0, 0, 0, 0.05)', - acrylicBg: ':alpha<0.5<@bg', - cwHoverBg: '#bbc4ce', - indicator: '@accent', - mentionMe: '@mention', - messageBg: '@bg', - navActive: '@accent', - infoWarnBg: '#fff0db', - infoWarnFg: '#8f6e31', - navHoverFg: ':darken<17<@fg', - dateLabelFg: '@fg', - inputBorder: 'rgba(0, 0, 0, 0.1)', - inputBorderHover: 'rgba(0, 0, 0, 0.2)', - panelBorder: '" solid 1px var(--divider)', - accentDarken: ':darken<10<@accent', - acrylicPanel: ':alpha<0.5<@panel', - navIndicator: '@accent', - accentLighten: ':lighten<10<@accent', - buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', - fgHighlighted: ':darken<3<@fg', - fgTransparent: ':alpha<0.5<@fg', - panelHeaderBg: ':lighten<3<@panel', - panelHeaderFg: '@fg', - htmlThemeColor: '@bg', - panelHighlight: ':darken<3<@panel', - listItemHoverBg: 'rgba(0, 0, 0, 0.03)', - scrollbarHandle: 'rgba(0, 0, 0, 0.2)', - wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', - fgTransparentWeak: ':alpha<0.75<@fg', - panelHeaderDivider: '@divider', - scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', - X2: ':darken<2<@panel', - X3: 'rgba(0, 0, 0, 0.05)', - X4: 'rgba(0, 0, 0, 0.1)', - X5: 'rgba(0, 0, 0, 0.05)', - X6: 'rgba(0, 0, 0, 0.25)', - X7: 'rgba(0, 0, 0, 0.05)', - X8: ':lighten<5<@accent', - X9: ':darken<5<@accent', - X10: ':alpha<0.4<@accent', - X11: 'rgba(0, 0, 0, 0.1)', - X12: 'rgba(0, 0, 0, 0.1)', - X13: 'rgba(0, 0, 0, 0.15)', - X14: ':alpha<0.5<@navBg', - X15: ':alpha<0<@panel', - X16: ':alpha<0.7<@panel', - X17: ':alpha<0.8<@bg', - }, -} diff --git a/packages/client/src/types/menu.ts b/packages/client/src/types/menu.ts deleted file mode 100644 index 972f6db214..0000000000 --- a/packages/client/src/types/menu.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Misskey from 'misskey-js'; -import { Ref } from 'vue'; - -export type MenuAction = (ev: MouseEvent) => void; - -export type MenuDivider = null; -export type MenuNull = undefined; -export type MenuLabel = { type: 'label', text: string }; -export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; -export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; -export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; -export type MenuSwitch = { type: 'switch', ref: Ref, text: string, disabled?: boolean }; -export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; -export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] }; - -export type MenuPending = { type: 'pending' }; - -type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent; -type OuterPromiseMenuItem = Promise; -export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; -export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent; diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue deleted file mode 100644 index 7f3fc0e4af..0000000000 --- a/packages/client/src/ui/_common_/common.vue +++ /dev/null @@ -1,139 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue deleted file mode 100644 index 50b28de063..0000000000 --- a/packages/client/src/ui/_common_/navbar-for-mobile.vue +++ /dev/null @@ -1,314 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue deleted file mode 100644 index b82da15f13..0000000000 --- a/packages/client/src/ui/_common_/navbar.vue +++ /dev/null @@ -1,521 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/_common_/statusbar-federation.vue b/packages/client/src/ui/_common_/statusbar-federation.vue deleted file mode 100644 index 24fc4f6f6d..0000000000 --- a/packages/client/src/ui/_common_/statusbar-federation.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/_common_/statusbar-rss.vue b/packages/client/src/ui/_common_/statusbar-rss.vue deleted file mode 100644 index e7f88e4984..0000000000 --- a/packages/client/src/ui/_common_/statusbar-rss.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/_common_/statusbar-user-list.vue b/packages/client/src/ui/_common_/statusbar-user-list.vue deleted file mode 100644 index f4d989c387..0000000000 --- a/packages/client/src/ui/_common_/statusbar-user-list.vue +++ /dev/null @@ -1,113 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/_common_/statusbars.vue b/packages/client/src/ui/_common_/statusbars.vue deleted file mode 100644 index 114ca5be8c..0000000000 --- a/packages/client/src/ui/_common_/statusbars.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/_common_/stream-indicator.vue b/packages/client/src/ui/_common_/stream-indicator.vue deleted file mode 100644 index a855de8ab9..0000000000 --- a/packages/client/src/ui/_common_/stream-indicator.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/_common_/sw-inject.ts b/packages/client/src/ui/_common_/sw-inject.ts deleted file mode 100644 index 8676d2d48d..0000000000 --- a/packages/client/src/ui/_common_/sw-inject.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { inject } from 'vue'; -import { post } from '@/os'; -import { $i, login } from '@/account'; -import { defaultStore } from '@/store'; -import { getAccountFromId } from '@/scripts/get-account-from-id'; -import { mainRouter } from '@/router'; - -export function swInject() { - navigator.serviceWorker.addEventListener('message', ev => { - if (_DEV_) { - console.log('sw msg', ev.data); - } - - if (ev.data.type !== 'order') return; - - if (ev.data.loginId !== $i?.id) { - return getAccountFromId(ev.data.loginId).then(account => { - if (!account) return; - return login(account.token, ev.data.url); - }); - } - - switch (ev.data.order) { - case 'post': - return post(ev.data.options); - case 'push': - if (mainRouter.currentRoute.value.path === ev.data.url) { - return window.scroll({ top: 0, behavior: 'smooth' }); - } - return mainRouter.push(ev.data.url); - default: - return; - } - }); -} diff --git a/packages/client/src/ui/_common_/upload.vue b/packages/client/src/ui/_common_/upload.vue deleted file mode 100644 index 70882bd251..0000000000 --- a/packages/client/src/ui/_common_/upload.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/classic.header.vue b/packages/client/src/ui/classic.header.vue deleted file mode 100644 index 46d79e6355..0000000000 --- a/packages/client/src/ui/classic.header.vue +++ /dev/null @@ -1,217 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/classic.sidebar.vue b/packages/client/src/ui/classic.sidebar.vue deleted file mode 100644 index dac09ea703..0000000000 --- a/packages/client/src/ui/classic.sidebar.vue +++ /dev/null @@ -1,268 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/classic.vue b/packages/client/src/ui/classic.vue deleted file mode 100644 index 0e726c11ed..0000000000 --- a/packages/client/src/ui/classic.vue +++ /dev/null @@ -1,320 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/classic.widgets.vue b/packages/client/src/ui/classic.widgets.vue deleted file mode 100644 index 163ec982ce..0000000000 --- a/packages/client/src/ui/classic.widgets.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/deck.vue b/packages/client/src/ui/deck.vue deleted file mode 100644 index f3415cfd09..0000000000 --- a/packages/client/src/ui/deck.vue +++ /dev/null @@ -1,435 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/deck/antenna-column.vue b/packages/client/src/ui/deck/antenna-column.vue deleted file mode 100644 index ba14530662..0000000000 --- a/packages/client/src/ui/deck/antenna-column.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/deck/column-core.vue b/packages/client/src/ui/deck/column-core.vue deleted file mode 100644 index 30c0dc5e1c..0000000000 --- a/packages/client/src/ui/deck/column-core.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - diff --git a/packages/client/src/ui/deck/column.vue b/packages/client/src/ui/deck/column.vue deleted file mode 100644 index 2a99b621e6..0000000000 --- a/packages/client/src/ui/deck/column.vue +++ /dev/null @@ -1,398 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/deck/deck-store.ts b/packages/client/src/ui/deck/deck-store.ts deleted file mode 100644 index 56db7398e5..0000000000 --- a/packages/client/src/ui/deck/deck-store.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { throttle } from 'throttle-debounce'; -import { markRaw } from 'vue'; -import { notificationTypes } from 'misskey-js'; -import { Storage } from '../../pizzax'; -import { i18n } from '@/i18n'; -import { api } from '@/os'; -import { deepClone } from '@/scripts/clone'; - -type ColumnWidget = { - name: string; - id: string; - data: Record; -}; - -export type Column = { - id: string; - type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'list' | 'mentions' | 'direct'; - name: string | null; - width: number; - widgets?: ColumnWidget[]; - active?: boolean; - flexible?: boolean; - antennaId?: string; - listId?: string; - includingTypes?: typeof notificationTypes[number][]; - tl?: 'home' | 'local' | 'social' | 'global'; -}; - -export const deckStore = markRaw(new Storage('deck', { - profile: { - where: 'deviceAccount', - default: 'default', - }, - columns: { - where: 'deviceAccount', - default: [] as Column[], - }, - layout: { - where: 'deviceAccount', - default: [] as Column['id'][][], - }, - columnAlign: { - where: 'deviceAccount', - default: 'left' as 'left' | 'right' | 'center', - }, - alwaysShowMainColumn: { - where: 'deviceAccount', - default: true, - }, - navWindow: { - where: 'deviceAccount', - default: true, - }, -})); - -export const loadDeck = async () => { - let deck; - - try { - deck = await api('i/registry/get', { - scope: ['client', 'deck', 'profiles'], - key: deckStore.state.profile, - }); - } catch (err) { - if (err.code === 'NO_SUCH_KEY') { - // 後方互換性のため - if (deckStore.state.profile === 'default') { - saveDeck(); - return; - } - - deckStore.set('columns', []); - deckStore.set('layout', []); - return; - } - throw err; - } - - deckStore.set('columns', deck.columns); - deckStore.set('layout', deck.layout); -}; - -// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する -export const saveDeck = throttle(1000, () => { - api('i/registry/set', { - scope: ['client', 'deck', 'profiles'], - key: deckStore.state.profile, - value: { - columns: deckStore.reactiveState.columns.value, - layout: deckStore.reactiveState.layout.value, - }, - }); -}); - -export async function getProfiles(): Promise { - return await api('i/registry/keys', { - scope: ['client', 'deck', 'profiles'], - }); -} - -export async function deleteProfile(key: string): Promise { - return await api('i/registry/remove', { - scope: ['client', 'deck', 'profiles'], - key: key, - }); -} - -export function addColumn(column: Column) { - if (column.name === undefined) column.name = null; - deckStore.push('columns', column); - deckStore.push('layout', [column.id]); - saveDeck(); -} - -export function removeColumn(id: Column['id']) { - deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id)); - deckStore.set('layout', deckStore.state.layout - .map(ids => ids.filter(_id => _id !== id)) - .filter(ids => ids.length > 0)); - saveDeck(); -} - -export function swapColumn(a: Column['id'], b: Column['id']) { - const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1); - const aY = deckStore.state.layout[aX].findIndex(id => id === a); - const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1); - const bY = deckStore.state.layout[bX].findIndex(id => id === b); - const layout = deepClone(deckStore.state.layout); - layout[aX][aY] = b; - layout[bX][bY] = a; - deckStore.set('layout', layout); - saveDeck(); -} - -export function swapLeftColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - deckStore.state.layout.some((ids, i) => { - if (ids.includes(id)) { - const left = deckStore.state.layout[i - 1]; - if (left) { - layout[i - 1] = deckStore.state.layout[i]; - layout[i] = left; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapRightColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - deckStore.state.layout.some((ids, i) => { - if (ids.includes(id)) { - const right = deckStore.state.layout[i + 1]; - if (right) { - layout[i + 1] = deckStore.state.layout[i]; - layout[i] = right; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapUpColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const ids = deepClone(deckStore.state.layout[idsIndex]); - ids.some((x, i) => { - if (x === id) { - const up = ids[i - 1]; - if (up) { - ids[i - 1] = id; - ids[i] = up; - - layout[idsIndex] = ids; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function swapDownColumn(id: Column['id']) { - const layout = deepClone(deckStore.state.layout); - const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const ids = deepClone(deckStore.state.layout[idsIndex]); - ids.some((x, i) => { - if (x === id) { - const down = ids[i + 1]; - if (down) { - ids[i + 1] = id; - ids[i] = down; - - layout[idsIndex] = ids; - deckStore.set('layout', layout); - } - return true; - } - }); - saveDeck(); -} - -export function stackLeftColumn(id: Column['id']) { - let layout = deepClone(deckStore.state.layout); - const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); - layout = layout.map(ids => ids.filter(_id => _id !== id)); - layout[i - 1].push(id); - layout = layout.filter(ids => ids.length > 0); - deckStore.set('layout', layout); - saveDeck(); -} - -export function popRightColumn(id: Column['id']) { - let layout = deepClone(deckStore.state.layout); - const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); - const affected = layout[i]; - layout = layout.map(ids => ids.filter(_id => _id !== id)); - layout.splice(i + 1, 0, [id]); - layout = layout.filter(ids => ids.length > 0); - deckStore.set('layout', layout); - - const columns = deepClone(deckStore.state.columns); - for (const column of columns) { - if (affected.includes(column.id)) { - column.active = true; - } - } - deckStore.set('columns', columns); - - saveDeck(); -} - -export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - if (column.widgets == null) column.widgets = []; - column.widgets.unshift(widget); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - column.widgets = column.widgets.filter(w => w.id !== widget.id); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - column.widgets = widgets; - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const column = deepClone(deckStore.state.columns[columnIndex]); - if (column == null) return; - column.widgets = column.widgets.map(w => w.id === widgetId ? { - ...w, - data: widgetData, - } : w); - columns[columnIndex] = column; - deckStore.set('columns', columns); - saveDeck(); -} - -export function updateColumn(id: Column['id'], column: Partial) { - const columns = deepClone(deckStore.state.columns); - const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); - const currentColumn = deepClone(deckStore.state.columns[columnIndex]); - if (currentColumn == null) return; - for (const [k, v] of Object.entries(column)) { - currentColumn[k] = v; - } - columns[columnIndex] = currentColumn; - deckStore.set('columns', columns); - saveDeck(); -} diff --git a/packages/client/src/ui/deck/direct-column.vue b/packages/client/src/ui/deck/direct-column.vue deleted file mode 100644 index 75b018cacd..0000000000 --- a/packages/client/src/ui/deck/direct-column.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/packages/client/src/ui/deck/list-column.vue b/packages/client/src/ui/deck/list-column.vue deleted file mode 100644 index d9f3f7b4e7..0000000000 --- a/packages/client/src/ui/deck/list-column.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/deck/main-column.vue b/packages/client/src/ui/deck/main-column.vue deleted file mode 100644 index 0c66172397..0000000000 --- a/packages/client/src/ui/deck/main-column.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/packages/client/src/ui/deck/mentions-column.vue b/packages/client/src/ui/deck/mentions-column.vue deleted file mode 100644 index 16962956a0..0000000000 --- a/packages/client/src/ui/deck/mentions-column.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/packages/client/src/ui/deck/notifications-column.vue b/packages/client/src/ui/deck/notifications-column.vue deleted file mode 100644 index 9d133035fe..0000000000 --- a/packages/client/src/ui/deck/notifications-column.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/packages/client/src/ui/deck/tl-column.vue b/packages/client/src/ui/deck/tl-column.vue deleted file mode 100644 index 49b29145ff..0000000000 --- a/packages/client/src/ui/deck/tl-column.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/deck/widgets-column.vue b/packages/client/src/ui/deck/widgets-column.vue deleted file mode 100644 index fc61d18ff6..0000000000 --- a/packages/client/src/ui/deck/widgets-column.vue +++ /dev/null @@ -1,69 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/universal.vue b/packages/client/src/ui/universal.vue deleted file mode 100644 index b91bf476e8..0000000000 --- a/packages/client/src/ui/universal.vue +++ /dev/null @@ -1,390 +0,0 @@ - - - - - - - diff --git a/packages/client/src/ui/universal.widgets.vue b/packages/client/src/ui/universal.widgets.vue deleted file mode 100644 index 33fb492836..0000000000 --- a/packages/client/src/ui/universal.widgets.vue +++ /dev/null @@ -1,71 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/visitor.vue b/packages/client/src/ui/visitor.vue deleted file mode 100644 index ec9150d346..0000000000 --- a/packages/client/src/ui/visitor.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/packages/client/src/ui/visitor/a.vue b/packages/client/src/ui/visitor/a.vue deleted file mode 100644 index f8db7a9d09..0000000000 --- a/packages/client/src/ui/visitor/a.vue +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - - diff --git a/packages/client/src/ui/visitor/b.vue b/packages/client/src/ui/visitor/b.vue deleted file mode 100644 index 275008a8f8..0000000000 --- a/packages/client/src/ui/visitor/b.vue +++ /dev/null @@ -1,248 +0,0 @@ - - - - - - - diff --git a/packages/client/src/ui/visitor/header.vue b/packages/client/src/ui/visitor/header.vue deleted file mode 100644 index 7300b12a75..0000000000 --- a/packages/client/src/ui/visitor/header.vue +++ /dev/null @@ -1,228 +0,0 @@ - - - - - diff --git a/packages/client/src/ui/visitor/kanban.vue b/packages/client/src/ui/visitor/kanban.vue deleted file mode 100644 index 51e47f277d..0000000000 --- a/packages/client/src/ui/visitor/kanban.vue +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - diff --git a/packages/client/src/ui/zen.vue b/packages/client/src/ui/zen.vue deleted file mode 100644 index 84c96a1dae..0000000000 --- a/packages/client/src/ui/zen.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/activity.calendar.vue b/packages/client/src/widgets/activity.calendar.vue deleted file mode 100644 index 84f6af1c13..0000000000 --- a/packages/client/src/widgets/activity.calendar.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/activity.chart.vue b/packages/client/src/widgets/activity.chart.vue deleted file mode 100644 index b61e419f94..0000000000 --- a/packages/client/src/widgets/activity.chart.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/activity.vue b/packages/client/src/widgets/activity.vue deleted file mode 100644 index 238a05ca09..0000000000 --- a/packages/client/src/widgets/activity.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - diff --git a/packages/client/src/widgets/aichan.vue b/packages/client/src/widgets/aichan.vue deleted file mode 100644 index 828490fd9c..0000000000 --- a/packages/client/src/widgets/aichan.vue +++ /dev/null @@ -1,74 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/aiscript.vue b/packages/client/src/widgets/aiscript.vue deleted file mode 100644 index 4009edb8b8..0000000000 --- a/packages/client/src/widgets/aiscript.vue +++ /dev/null @@ -1,175 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/button.vue b/packages/client/src/widgets/button.vue deleted file mode 100644 index f0148d7f4e..0000000000 --- a/packages/client/src/widgets/button.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/calendar.vue b/packages/client/src/widgets/calendar.vue deleted file mode 100644 index 99bd36e2fc..0000000000 --- a/packages/client/src/widgets/calendar.vue +++ /dev/null @@ -1,213 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/clock.vue b/packages/client/src/widgets/clock.vue deleted file mode 100644 index dc99b6631e..0000000000 --- a/packages/client/src/widgets/clock.vue +++ /dev/null @@ -1,203 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/digital-clock.vue b/packages/client/src/widgets/digital-clock.vue deleted file mode 100644 index d2bfd523f3..0000000000 --- a/packages/client/src/widgets/digital-clock.vue +++ /dev/null @@ -1,92 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue deleted file mode 100644 index 3374783b0c..0000000000 --- a/packages/client/src/widgets/federation.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/index.ts b/packages/client/src/widgets/index.ts deleted file mode 100644 index 39826f13c8..0000000000 --- a/packages/client/src/widgets/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { App, defineAsyncComponent } from 'vue'; - -export default function(app: App) { - app.component('MkwMemo', defineAsyncComponent(() => import('./memo.vue'))); - app.component('MkwNotifications', defineAsyncComponent(() => import('./notifications.vue'))); - app.component('MkwTimeline', defineAsyncComponent(() => import('./timeline.vue'))); - app.component('MkwCalendar', defineAsyncComponent(() => import('./calendar.vue'))); - app.component('MkwRss', defineAsyncComponent(() => import('./rss.vue'))); - app.component('MkwRssTicker', defineAsyncComponent(() => import('./rss-ticker.vue'))); - app.component('MkwTrends', defineAsyncComponent(() => import('./trends.vue'))); - app.component('MkwClock', defineAsyncComponent(() => import('./clock.vue'))); - app.component('MkwActivity', defineAsyncComponent(() => import('./activity.vue'))); - app.component('MkwPhotos', defineAsyncComponent(() => import('./photos.vue'))); - app.component('MkwDigitalClock', defineAsyncComponent(() => import('./digital-clock.vue'))); - app.component('MkwUnixClock', defineAsyncComponent(() => import('./unix-clock.vue'))); - app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue'))); - app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue'))); - app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue'))); - app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue'))); - app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue'))); - app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue'))); - app.component('MkwInstanceCloud', defineAsyncComponent(() => import('./instance-cloud.vue'))); - app.component('MkwButton', defineAsyncComponent(() => import('./button.vue'))); - app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue'))); - app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue'))); - app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue'))); -} - -export const widgets = [ - 'memo', - 'notifications', - 'timeline', - 'calendar', - 'rss', - 'rssTicker', - 'trends', - 'clock', - 'activity', - 'photos', - 'digitalClock', - 'unixClock', - 'federation', - 'instanceCloud', - 'postForm', - 'slideshow', - 'serverMetric', - 'onlineUsers', - 'jobQueue', - 'button', - 'aiscript', - 'aichan', - 'userList', -]; diff --git a/packages/client/src/widgets/instance-cloud.vue b/packages/client/src/widgets/instance-cloud.vue deleted file mode 100644 index 4965616995..0000000000 --- a/packages/client/src/widgets/instance-cloud.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/job-queue.vue b/packages/client/src/widgets/job-queue.vue deleted file mode 100644 index 9f19c51825..0000000000 --- a/packages/client/src/widgets/job-queue.vue +++ /dev/null @@ -1,197 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/memo.vue b/packages/client/src/widgets/memo.vue deleted file mode 100644 index 1cc0e10bba..0000000000 --- a/packages/client/src/widgets/memo.vue +++ /dev/null @@ -1,111 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/notifications.vue b/packages/client/src/widgets/notifications.vue deleted file mode 100644 index e697209444..0000000000 --- a/packages/client/src/widgets/notifications.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - diff --git a/packages/client/src/widgets/online-users.vue b/packages/client/src/widgets/online-users.vue deleted file mode 100644 index e9ab79b111..0000000000 --- a/packages/client/src/widgets/online-users.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/photos.vue b/packages/client/src/widgets/photos.vue deleted file mode 100644 index 4ad5324053..0000000000 --- a/packages/client/src/widgets/photos.vue +++ /dev/null @@ -1,123 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/post-form.vue b/packages/client/src/widgets/post-form.vue deleted file mode 100644 index f1708775ba..0000000000 --- a/packages/client/src/widgets/post-form.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/packages/client/src/widgets/rss-ticker.vue b/packages/client/src/widgets/rss-ticker.vue deleted file mode 100644 index 44c21d1836..0000000000 --- a/packages/client/src/widgets/rss-ticker.vue +++ /dev/null @@ -1,152 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/rss.vue b/packages/client/src/widgets/rss.vue deleted file mode 100644 index c0338c8e47..0000000000 --- a/packages/client/src/widgets/rss.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/server-metric/cpu-mem.vue b/packages/client/src/widgets/server-metric/cpu-mem.vue deleted file mode 100644 index 80a8e427e1..0000000000 --- a/packages/client/src/widgets/server-metric/cpu-mem.vue +++ /dev/null @@ -1,167 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/server-metric/cpu.vue b/packages/client/src/widgets/server-metric/cpu.vue deleted file mode 100644 index e7b2226d1f..0000000000 --- a/packages/client/src/widgets/server-metric/cpu.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/server-metric/disk.vue b/packages/client/src/widgets/server-metric/disk.vue deleted file mode 100644 index 3d22d05383..0000000000 --- a/packages/client/src/widgets/server-metric/disk.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/server-metric/index.vue b/packages/client/src/widgets/server-metric/index.vue deleted file mode 100644 index bc3fca6fc1..0000000000 --- a/packages/client/src/widgets/server-metric/index.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/packages/client/src/widgets/server-metric/mem.vue b/packages/client/src/widgets/server-metric/mem.vue deleted file mode 100644 index 6018eb4265..0000000000 --- a/packages/client/src/widgets/server-metric/mem.vue +++ /dev/null @@ -1,73 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/server-metric/net.vue b/packages/client/src/widgets/server-metric/net.vue deleted file mode 100644 index ab8b0fe471..0000000000 --- a/packages/client/src/widgets/server-metric/net.vue +++ /dev/null @@ -1,140 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/server-metric/pie.vue b/packages/client/src/widgets/server-metric/pie.vue deleted file mode 100644 index 868dbc0484..0000000000 --- a/packages/client/src/widgets/server-metric/pie.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/slideshow.vue b/packages/client/src/widgets/slideshow.vue deleted file mode 100644 index e317b8ab94..0000000000 --- a/packages/client/src/widgets/slideshow.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/timeline.vue b/packages/client/src/widgets/timeline.vue deleted file mode 100644 index e48444d33f..0000000000 --- a/packages/client/src/widgets/timeline.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - diff --git a/packages/client/src/widgets/trends.vue b/packages/client/src/widgets/trends.vue deleted file mode 100644 index 02eec0431e..0000000000 --- a/packages/client/src/widgets/trends.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/unix-clock.vue b/packages/client/src/widgets/unix-clock.vue deleted file mode 100644 index cf85ac782c..0000000000 --- a/packages/client/src/widgets/unix-clock.vue +++ /dev/null @@ -1,116 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/user-list.vue b/packages/client/src/widgets/user-list.vue deleted file mode 100644 index 9ffbf0d8e3..0000000000 --- a/packages/client/src/widgets/user-list.vue +++ /dev/null @@ -1,136 +0,0 @@ - - - - - diff --git a/packages/client/src/widgets/widget.ts b/packages/client/src/widgets/widget.ts deleted file mode 100644 index 8bd56a5966..0000000000 --- a/packages/client/src/widgets/widget.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { reactive, watch } from 'vue'; -import { throttle } from 'throttle-debounce'; -import { Form, GetFormResultType } from '@/scripts/form'; -import * as os from '@/os'; -import { deepClone } from '@/scripts/clone'; - -export type Widget

> = { - id: string; - data: Partial

; -}; - -export type WidgetComponentProps

> = { - widget?: Widget

; -}; - -export type WidgetComponentEmits

> = { - (ev: 'updateProps', props: P); -}; - -export type WidgetComponentExpose = { - name: string; - id: string | null; - configure: () => void; -}; - -export const useWidgetPropsManager = >( - name: string, - propsDef: F, - props: Readonly>>, - emit: WidgetComponentEmits>, -): { - widgetProps: GetFormResultType; - save: () => void; - configure: () => void; -} => { - const widgetProps = reactive(props.widget ? deepClone(props.widget.data) : {}); - - const mergeProps = () => { - for (const prop of Object.keys(propsDef)) { - if (typeof widgetProps[prop] === 'undefined') { - widgetProps[prop] = propsDef[prop].default; - } - } - }; - watch(widgetProps, () => { - mergeProps(); - }, { deep: true, immediate: true }); - - const save = throttle(3000, () => { - emit('updateProps', widgetProps); - }); - - const configure = async () => { - const form = deepClone(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, - }; -}; diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json deleted file mode 100644 index 86109f600a..0000000000 --- a/packages/client/tsconfig.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "noEmitOnError": false, - "noImplicitAny": false, - "noImplicitReturns": true, - "noUnusedParameters": false, - "noUnusedLocals": true, - "noFallthroughCasesInSwitch": true, - "declaration": false, - "sourceMap": false, - "target": "es2017", - "module": "esnext", - "moduleResolution": "node", - "removeComments": false, - "noLib": false, - "strict": true, - "strictNullChecks": true, - "experimentalDecorators": true, - "resolveJsonModule": true, - "allowSyntheticDefaultImports": true, - "isolatedModules": true, - "useDefineForClassFields": true, - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"], - }, - "typeRoots": [ - "node_modules/@types", - "@types", - ], - "types": [ - "vite/client", - ], - "lib": [ - "esnext", - "dom" - ], - "jsx": "preserve" - }, - "compileOnSave": false, - "include": [ - ".eslintrc.js", - "./**/*.ts", - "./**/*.vue" - ] -} diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts deleted file mode 100644 index 1acf5301b7..0000000000 --- a/packages/client/vite.config.ts +++ /dev/null @@ -1,70 +0,0 @@ -import pluginVue from '@vitejs/plugin-vue'; -import { defineConfig } from 'vite'; - -import locales from '../../locales'; -import meta from '../../package.json'; -import pluginJson5 from './vite.json5'; - -const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; - -export default defineConfig(({ command, mode }) => { - - return { - base: '/vite/', - - plugins: [ - pluginVue({ - reactivityTransform: true, - }), - pluginJson5(), - ], - - resolve: { - extensions, - alias: { - '@/': __dirname + '/src/', - '/client-assets/': __dirname + '/assets/', - '/static-assets/': __dirname + '/../backend/assets/', - }, - }, - - define: { - _VERSION_: JSON.stringify(meta.version), - _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), - _ENV_: JSON.stringify(process.env.NODE_ENV), - _DEV_: process.env.NODE_ENV !== 'production', - _PERF_PREFIX_: JSON.stringify('Misskey:'), - _DATA_TRANSFER_DRIVE_FILE_: JSON.stringify('mk_drive_file'), - _DATA_TRANSFER_DRIVE_FOLDER_: JSON.stringify('mk_drive_folder'), - _DATA_TRANSFER_DECK_COLUMN_: JSON.stringify('mk_deck_column'), - __VUE_OPTIONS_API__: true, - __VUE_PROD_DEVTOOLS__: false, - }, - - build: { - target: [ - 'chrome100', - 'firefox100', - 'safari15', - 'es2017', // TODO: そのうち消す - ], - manifest: 'manifest.json', - rollupOptions: { - input: { - app: './src/init.ts', - }, - output: { - manualChunks: { - vue: ['vue'], - }, - }, - }, - cssCodeSplit: true, - outDir: __dirname + '/../../built/_vite_', - assetsDir: '.', - emptyOutDir: false, - sourcemap: process.env.NODE_ENV === 'development', - reportCompressedSize: false, - }, - }; -}); diff --git a/packages/client/vite.json5.ts b/packages/client/vite.json5.ts deleted file mode 100644 index 0a37fbff44..0000000000 --- a/packages/client/vite.json5.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json - -import JSON5 from 'json5'; -import { Plugin } from 'rollup'; -import { createFilter, dataToEsm } from '@rollup/pluginutils'; -import { RollupJsonOptions } from '@rollup/plugin-json'; - -export default function json5(options: RollupJsonOptions = {}): Plugin { - const filter = createFilter(options.include, options.exclude); - const indent = 'indent' in options ? options.indent : '\t'; - - return { - name: 'json5', - - // eslint-disable-next-line no-shadow - transform(json, id) { - if (id.slice(-6) !== '.json5' || !filter(id)) return null; - - try { - const parsed = JSON5.parse(json); - return { - code: dataToEsm(parsed, { - preferConst: options.preferConst, - compact: options.compact, - namedExports: options.namedExports, - indent, - }), - map: { mappings: '' }, - }; - } catch (err) { - const message = 'Could not parse JSON file'; - const position = parseInt(/[\d]/.exec(err.message)[0], 10); - this.warn({ message, id, position }); - return null; - } - }, - }; -} diff --git a/packages/frontend/.eslintrc.js b/packages/frontend/.eslintrc.js new file mode 100644 index 0000000000..6c3bfb5a6e --- /dev/null +++ b/packages/frontend/.eslintrc.js @@ -0,0 +1,89 @@ +module.exports = { + root: true, + env: { + 'node': false, + }, + parser: 'vue-eslint-parser', + parserOptions: { + 'parser': '@typescript-eslint/parser', + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + extraFileExtensions: ['.vue'], + }, + extends: [ + '../shared/.eslintrc.js', + 'plugin:vue/vue3-recommended', + ], + rules: { + '@typescript-eslint/no-empty-interface': [ + 'error', + { + 'allowSingleExtends': true, + }, + ], + '@typescript-eslint/prefer-nullish-coalescing': [ + 'error', + ], + // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため + // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため + 'id-denylist': ['error', 'window', 'e'], + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + 'alphabetical': false, + }], + 'vue/no-use-v-if-with-v-for': ['error', { + 'allowUsingIterationVar': false, + }], + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + 'ignoreProperties': false, + }], + 'vue/no-v-html': 'warn', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + 'attribute': 1, + 'baseIndent': 0, + 'closeBracket': 0, + 'alignAttributesVertically': true, + 'ignores': [], + }], + 'vue/html-closing-bracket-spacing': ['warn', { + 'startTag': 'never', + 'endTag': 'never', + 'selfClosingTag': 'never', + }], + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-destructure': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', + // (vue/vue3-recommended disabled the autofix for Vue 2 compatibility) + 'vue/v-on-event-hyphenation': ['warn', 'always', { autofix: true }], + }, + globals: { + // Node.js + 'module': false, + 'require': false, + '__dirname': false, + + // Vue + '$$': false, + '$ref': false, + '$shallowRef': false, + '$computed': false, + + // Misskey + '_DEV_': false, + '_LANGS_': false, + '_VERSION_': false, + '_ENV_': false, + '_PERF_PREFIX_': false, + '_DATA_TRANSFER_DRIVE_FILE_': false, + '_DATA_TRANSFER_DRIVE_FOLDER_': false, + '_DATA_TRANSFER_DECK_COLUMN_': false, + }, +}; diff --git a/packages/frontend/.vscode/settings.json b/packages/frontend/.vscode/settings.json new file mode 100644 index 0000000000..1a79b6a7dc --- /dev/null +++ b/packages/frontend/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib", + "path-intellisense.mappings": { + "@": "${workspaceRoot}/packages/frontend/src/" + }, + "eslint.validate": [ + "javascript", + "javascriptreact", + "vue" + ] +} diff --git a/packages/frontend/@types/global.d.ts b/packages/frontend/@types/global.d.ts new file mode 100644 index 0000000000..c757482900 --- /dev/null +++ b/packages/frontend/@types/global.d.ts @@ -0,0 +1,10 @@ +type FIXME = any; + +declare const _LANGS_: string[][]; +declare const _VERSION_: string; +declare const _ENV_: string; +declare const _DEV_: boolean; +declare const _PERF_PREFIX_: string; +declare const _DATA_TRANSFER_DRIVE_FILE_: string; +declare const _DATA_TRANSFER_DRIVE_FOLDER_: string; +declare const _DATA_TRANSFER_DECK_COLUMN_: string; diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts new file mode 100644 index 0000000000..67f724a9aa --- /dev/null +++ b/packages/frontend/@types/theme.d.ts @@ -0,0 +1,7 @@ +declare module '@/themes/*.json5' { + import { Theme } from "@/scripts/theme"; + + const theme: Theme; + + export default theme; +} diff --git a/packages/frontend/@types/vue.d.ts b/packages/frontend/@types/vue.d.ts new file mode 100644 index 0000000000..9c9c34ccc5 --- /dev/null +++ b/packages/frontend/@types/vue.d.ts @@ -0,0 +1,16 @@ +/// + +import type { $i } from '@/account'; +import type { defaultStore } from '@/store'; +import type { instance } from '@/instance'; +import type { i18n } from '@/i18n'; + +declare module 'vue' { + interface ComponentCustomProperties { + $i: typeof $i; + $store: typeof defaultStore; + $instance: typeof instance; + $t: typeof i18n['t']; + $ts: typeof i18n['ts']; + } +} diff --git a/packages/frontend/assets/about-icon.png b/packages/frontend/assets/about-icon.png new file mode 100644 index 0000000000..afc1f0c728 Binary files /dev/null and b/packages/frontend/assets/about-icon.png differ diff --git a/packages/frontend/assets/dummy.png b/packages/frontend/assets/dummy.png new file mode 100644 index 0000000000..39332b0c1b Binary files /dev/null and b/packages/frontend/assets/dummy.png differ diff --git a/packages/frontend/assets/fedi.jpg b/packages/frontend/assets/fedi.jpg new file mode 100644 index 0000000000..cbf3748eb8 Binary files /dev/null and b/packages/frontend/assets/fedi.jpg differ diff --git a/packages/frontend/assets/label-red.svg b/packages/frontend/assets/label-red.svg new file mode 100644 index 0000000000..45996aa9ce --- /dev/null +++ b/packages/frontend/assets/label-red.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/packages/frontend/assets/label.svg b/packages/frontend/assets/label.svg new file mode 100644 index 0000000000..b1f85f3c07 --- /dev/null +++ b/packages/frontend/assets/label.svg @@ -0,0 +1,6 @@ + + + + + diff --git a/packages/frontend/assets/misskey.svg b/packages/frontend/assets/misskey.svg new file mode 100644 index 0000000000..3fcb2d3ecb --- /dev/null +++ b/packages/frontend/assets/misskey.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/frontend/assets/remove.png b/packages/frontend/assets/remove.png new file mode 100644 index 0000000000..c2e222a0fc Binary files /dev/null and b/packages/frontend/assets/remove.png differ diff --git a/packages/frontend/assets/sounds/aisha/1.mp3 b/packages/frontend/assets/sounds/aisha/1.mp3 new file mode 100644 index 0000000000..d8e9a2f265 Binary files /dev/null and b/packages/frontend/assets/sounds/aisha/1.mp3 differ diff --git a/packages/frontend/assets/sounds/aisha/2.mp3 b/packages/frontend/assets/sounds/aisha/2.mp3 new file mode 100644 index 0000000000..477c2eba43 Binary files /dev/null and b/packages/frontend/assets/sounds/aisha/2.mp3 differ diff --git a/packages/frontend/assets/sounds/aisha/3.mp3 b/packages/frontend/assets/sounds/aisha/3.mp3 new file mode 100644 index 0000000000..fe0d8063df Binary files /dev/null and b/packages/frontend/assets/sounds/aisha/3.mp3 differ diff --git a/packages/frontend/assets/sounds/noizenecio/kick_gaba1.mp3 b/packages/frontend/assets/sounds/noizenecio/kick_gaba1.mp3 new file mode 100644 index 0000000000..616b506c4f Binary files /dev/null and b/packages/frontend/assets/sounds/noizenecio/kick_gaba1.mp3 differ diff --git a/packages/frontend/assets/sounds/noizenecio/kick_gaba2.mp3 b/packages/frontend/assets/sounds/noizenecio/kick_gaba2.mp3 new file mode 100644 index 0000000000..33c2837620 Binary files /dev/null and b/packages/frontend/assets/sounds/noizenecio/kick_gaba2.mp3 differ diff --git a/packages/frontend/assets/sounds/noizenecio/kick_gaba3.mp3 b/packages/frontend/assets/sounds/noizenecio/kick_gaba3.mp3 new file mode 100644 index 0000000000..1791f26573 Binary files /dev/null and b/packages/frontend/assets/sounds/noizenecio/kick_gaba3.mp3 differ diff --git a/packages/frontend/assets/sounds/noizenecio/kick_gaba4.mp3 b/packages/frontend/assets/sounds/noizenecio/kick_gaba4.mp3 new file mode 100644 index 0000000000..5f8bf468e5 Binary files /dev/null and b/packages/frontend/assets/sounds/noizenecio/kick_gaba4.mp3 differ diff --git a/packages/frontend/assets/sounds/noizenecio/kick_gaba5.mp3 b/packages/frontend/assets/sounds/noizenecio/kick_gaba5.mp3 new file mode 100644 index 0000000000..dabe754b5b Binary files /dev/null and b/packages/frontend/assets/sounds/noizenecio/kick_gaba5.mp3 differ diff --git a/packages/frontend/assets/sounds/noizenecio/kick_gaba6.mp3 b/packages/frontend/assets/sounds/noizenecio/kick_gaba6.mp3 new file mode 100644 index 0000000000..57ecb01bda Binary files /dev/null and b/packages/frontend/assets/sounds/noizenecio/kick_gaba6.mp3 differ diff --git a/packages/frontend/assets/sounds/noizenecio/kick_gaba7.mp3 b/packages/frontend/assets/sounds/noizenecio/kick_gaba7.mp3 new file mode 100644 index 0000000000..6ba317deb1 Binary files /dev/null and b/packages/frontend/assets/sounds/noizenecio/kick_gaba7.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/down.mp3 b/packages/frontend/assets/sounds/syuilo/down.mp3 new file mode 100644 index 0000000000..4cd421139d Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/down.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/kick.mp3 b/packages/frontend/assets/sounds/syuilo/kick.mp3 new file mode 100644 index 0000000000..4e0e72091c Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/kick.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/pirori-square-wet.mp3 b/packages/frontend/assets/sounds/syuilo/pirori-square-wet.mp3 new file mode 100644 index 0000000000..babf1fce60 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/pirori-square-wet.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/pirori-wet.mp3 b/packages/frontend/assets/sounds/syuilo/pirori-wet.mp3 new file mode 100644 index 0000000000..25e2c46a64 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/pirori-wet.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/pirori.mp3 b/packages/frontend/assets/sounds/syuilo/pirori.mp3 new file mode 100644 index 0000000000..a745415ac0 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/pirori.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/poi1.mp3 b/packages/frontend/assets/sounds/syuilo/poi1.mp3 new file mode 100644 index 0000000000..59dae90965 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/poi1.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/poi2.mp3 b/packages/frontend/assets/sounds/syuilo/poi2.mp3 new file mode 100644 index 0000000000..a65c653891 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/poi2.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/pope1.mp3 b/packages/frontend/assets/sounds/syuilo/pope1.mp3 new file mode 100644 index 0000000000..d6f53cfacc Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/pope1.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/pope2.mp3 b/packages/frontend/assets/sounds/syuilo/pope2.mp3 new file mode 100644 index 0000000000..fe5d95e292 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/pope2.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/popo.mp3 b/packages/frontend/assets/sounds/syuilo/popo.mp3 new file mode 100644 index 0000000000..a2a1605bbb Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/popo.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/queue-jammed.mp3 b/packages/frontend/assets/sounds/syuilo/queue-jammed.mp3 new file mode 100644 index 0000000000..99e0c437fe Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/queue-jammed.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/reverved.mp3 b/packages/frontend/assets/sounds/syuilo/reverved.mp3 new file mode 100644 index 0000000000..47588ef270 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/reverved.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/ryukyu.mp3 b/packages/frontend/assets/sounds/syuilo/ryukyu.mp3 new file mode 100644 index 0000000000..9e935e3f37 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/ryukyu.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/snare.mp3 b/packages/frontend/assets/sounds/syuilo/snare.mp3 new file mode 100644 index 0000000000..9244189c2d Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/snare.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/square-pico.mp3 b/packages/frontend/assets/sounds/syuilo/square-pico.mp3 new file mode 100644 index 0000000000..c4d8305ae7 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/square-pico.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/triple.mp3 b/packages/frontend/assets/sounds/syuilo/triple.mp3 new file mode 100644 index 0000000000..54ab974d46 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/triple.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/up.mp3 b/packages/frontend/assets/sounds/syuilo/up.mp3 new file mode 100644 index 0000000000..3f30867764 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/up.mp3 differ diff --git a/packages/frontend/assets/sounds/syuilo/waon.mp3 b/packages/frontend/assets/sounds/syuilo/waon.mp3 new file mode 100644 index 0000000000..a4af473861 Binary files /dev/null and b/packages/frontend/assets/sounds/syuilo/waon.mp3 differ diff --git a/packages/frontend/assets/tagcanvas.min.js b/packages/frontend/assets/tagcanvas.min.js new file mode 100644 index 0000000000..bcee46e682 --- /dev/null +++ b/packages/frontend/assets/tagcanvas.min.js @@ -0,0 +1,21 @@ +/** + * Copyright (C) 2010-2021 Graham Breach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +/** + * TagCanvas 2.11 + * For more information, please contact + */ + (function(){"use strict";var r,C,p=Math.abs,o=Math.sin,l=Math.cos,g=Math.max,h=Math.min,af=Math.ceil,E=Math.sqrt,w=Math.pow,I={},D={},R={0:"0,",1:"17,",2:"34,",3:"51,",4:"68,",5:"85,",6:"102,",7:"119,",8:"136,",9:"153,",a:"170,",A:"170,",b:"187,",B:"187,",c:"204,",C:"204,",d:"221,",D:"221,",e:"238,",E:"238,",f:"255,",F:"255,"},f,d,b,T,z,F,M,c=document,v,e,P,j={};for(r=0;r<256;++r)C=r.toString(16),r<16&&(C='0'+C),D[C]=D[C.toUpperCase()]=r.toString()+',';function n(a){return typeof a!='undefined'}function B(a){return typeof a=='object'&&a!=null}function G(a,c,b){return isNaN(a)?b:h(b,g(c,a))}function x(){return!1}function q(){return(new Date).valueOf()}function ak(c,d){var b=[],e=c.length,a;for(a=0;a=1)?0:a<=-1?Math.PI:Math.acos(a)},z.unit=function(){var a=this.length();return new s(this.x/a,this.y/a,this.z/a)};function ay(b,a){a=a*Math.PI/180,b=b*Math.PI/180;var c=o(b)*l(a),d=-o(a),e=-l(b)*l(a);return new s(c,d,e)}function m(a){this[1]={1:a[0],2:a[1],3:a[2]},this[2]={1:a[3],2:a[4],3:a[5]},this[3]={1:a[6],2:a[7],3:a[8]}}T=m.prototype,m.Identity=function(){return new m([1,0,0,0,1,0,0,0,1])},m.Rotation=function(e,a){var c=o(e),d=l(e),b=1-d;return new m([d+w(a.x,2)*b,a.x*a.y*b-a.z*c,a.x*a.z*b+a.y*c,a.y*a.x*b+a.z*c,d+w(a.y,2)*b,a.y*a.z*b-a.x*c,a.z*a.x*b-a.y*c,a.z*a.y*b+a.x*c,d+w(a.z,2)*b])},T.mul=function(c){var d=[],a,b,e=c.xform?1:0;for(a=1;a<=3;++a)for(b=1;b<=3;++b)e?d.push(this[a][1]*c[1][b]+this[a][2]*c[2][b]+this[a][3]*c[3][b]):d.push(this[a][b]*c);return new m(d)},T.xform=function(b){var a={},c=b.x,d=b.y,e=b.z;return a.x=c*this[1][1]+d*this[2][1]+e*this[3][1],a.y=c*this[1][2]+d*this[2][2]+e*this[3][2],a.z=c*this[1][3]+d*this[2][3]+e*this[3][3],a};function aB(g,j,k,m,f){var a,b,c,d,e=[],h=2/g,i;i=Math.PI*(3-E(5)+(parseFloat(f)?parseFloat(f):0));for(a=0;a0)}function aC(a,c,f,d){var e=a.createLinearGradient(0,0,c,0),b;for(b in d)e.addColorStop(1-b,d[b]);a.fillStyle=e,a.fillRect(0,f,c,1)}function L(a,m,j){var l=1024,d=1,e=a.weightGradient,i,f,b,c;if(a.gCanvas)f=a.gCanvas.getContext('2d'),d=a.gCanvas.height;else{if(B(e[0])?d=e.length:e=[e],a.gCanvas=i=k(l,d),!i)return null;f=i.getContext('2d');for(b=0;b0?b=i*b/100:b=b*j,a=e.getContext('2d'),a.globalCompositeOperation='source-over',a.fillStyle='#fff',b>=i/2?(b=h(c,d)/2,a.beginPath(),a.moveTo(c/2,d/2),a.arc(c/2,d/2,b,0,2*Math.PI,!1),a.fill(),a.closePath()):(b=h(c/2,d/2,b),y(a,0,0,c,d,b,!0),a.fill()),a.globalCompositeOperation='source-in',a.drawImage(l,0,0,c,d),e)}function ao(q,m,i,b,h,a,c){var g=p(c[0]),f=p(c[1]),j=m+(g>a?g+a:a*2)*b,l=i+(f>a?f+a:a*2)*b,n=b*((a||0)+(c[0]<0?g:0)),o=b*((a||0)+(c[1]<0?f:0)),e,d;return e=k(j,l),!e?null:(d=e.getContext('2d'),h&&(d.shadowColor=h),a&&(d.shadowBlur=a*b),c&&(d.shadowOffsetX=c[0]*b,d.shadowOffsetY=c[1]*b),d.drawImage(q,n,o,m,i),{image:e,width:j/b,height:l/b})}function ae(m,o,l){var c=parseInt(m.toString().length*l),h=parseInt(l*2*m.length),j=k(c,h),g,i,e,f,b,d,n,a;if(!j)return null;g=j.getContext('2d'),g.fillStyle='#000',g.fillRect(0,0,c,h),Y(g,l+'px '+o,'#fff',m,0,0,0,0,[],'centre'),i=g.getImageData(0,0,c,h),e=i.width,f=i.height,a={min:{x:e,y:f},max:{x:-1,y:-1}};for(d=0;d0&&(ba.max.x&&(a.max.x=b),da.max.y&&(a.max.y=d));return e!=c&&(a.min.x*=c/e,a.max.x*=c/e),f!=h&&(a.min.y*=c/f,a.max.y*=c/f),j=null,a}function Q(a){return"'"+a.replace(/(\'|\")/g,'').replace(/\s*,\s*/g,"', '")+"'"}function t(b,d,a){a=a||c,a.addEventListener?a.addEventListener(b,d,!1):a.attachEvent('on'+b,d)}function am(b,d,a){a=a||c,a.removeEventListener?a.removeEventListener(b,d):a.detachEvent('on'+b,d)}function A(g,e,j,a,b){var l=b.imageScale,h,c,k,m,f,d;if(!e.complete)return t('load',function(){A(g,e,j,a,b)},e);if(!g.complete)return t('load',function(){A(g,e,j,a,b)},g);if(j&&!j.complete)return t('load',function(){A(g,e,j,a,b)},j);e.width=e.width,e.height=e.height,l&&(g.width=e.width*l,g.height=e.height*l),a.iw=g.width,a.ih=g.height,b.txtOpt&&(c=g,h=b.zoomMax*b.txtScale,f=a.iw*h,d=a.ih*h,f0?(a.iw+=2*b.outlineIncrease,a.ih+=2*b.outlineIncrease,f=h*a.iw,d=h*a.ih,c=S(a.fimage,f,d),a.oimage=c,a.fimage=H(a.fimage,a.oimage.width,a.oimage.height)):(f=h*(a.iw+2*b.outlineIncrease),d=h*(a.ih+2*b.outlineIncrease),c=S(a.fimage,f,d),a.oimage=H(c,a.fimage.width,a.fimage.height))))),a.alt=j,a.Init()}function i(a,d){var b=c.defaultView,e=d.replace(/\-([a-z])/g,function(a){return a.charAt(1).toUpperCase()});return b&&b.getComputedStyle&&b.getComputedStyle(a,null).getPropertyValue(d)||a.currentStyle&&a.currentStyle[e]}function aj(c,d,e){var b=1,a;return d?b=1*(c.getAttribute(d)||e):(a=i(c,'font-size'))&&(b=a.indexOf('px')>-1&&a.replace('px','')*1||a.indexOf('pt')>-1&&a.replace('pt','')*1.25||a*3.3),b}function u(a){return a.target&&n(a.target.id)?a.target.id:a.srcElement.parentNode.id}function K(a,c){var b,d,e=parseInt(i(c,'width'))/c.width,f=parseInt(i(c,'height'))/c.height;return n(a.offsetX)?b={x:a.offsetX,y:a.offsetY}:(d=X(c.id),n(a.changedTouches)&&(a=a.changedTouches[0]),a.pageX&&(b={x:a.pageX-d.x,y:a.pageY-d.y})),b&&e&&f&&(b.x/=e,b.y/=f),b}function an(c){var d=c.target||c.fromElement.parentNode,b=a.tc[d.id];b&&(b.mx=b.my=-1,b.UnFreeze(),b.EndDrag())}function ad(e){var g,c=a,b,d,f=u(e);for(g in c.tc)b=c.tc[g],b.tttimer&&(clearTimeout(b.tttimer),b.tttimer=null);f&&c.tc[f]&&(b=c.tc[f],(d=K(e,b.canvas))&&(b.mx=d.x,b.my=d.y,b.Drag(e,d)),b.drawn=0)}function ap(b){var e=a,f=c.addEventListener?0:1,d=u(b);d&&b.button==f&&e.tc[d]&&e.tc[d].BeginDrag(b)}function aq(b){var f=a,g=c.addEventListener?0:1,e=u(b),d;e&&b.button==g&&f.tc[e]&&(d=f.tc[e],ad(b),!d.EndDrag()&&!d.touchState&&d.Clicked(b))}function ar(c){var e=u(c),b=e&&a.tc[e],d;b&&c.changedTouches&&(c.touches.length==1&&b.touchState==0?(b.touchState=1,b.BeginDrag(c),(d=K(c,b.canvas))&&(b.mx=d.x,b.my=d.y,b.drawn=0)):c.targetTouches.length==2&&b.pinchZoom?(b.touchState=3,b.EndDrag(),b.BeginPinch(c)):(b.EndDrag(),b.EndPinch(),b.touchState=0))}function ac(c){var d=u(c),b=d&&a.tc[d];if(b&&c.changedTouches){switch(b.touchState){case 1:b.Draw(),b.Clicked();break;break;case 2:b.EndDrag();break;case 3:b.EndPinch()}b.touchState=0}}function au(c){var f,e=a,b,d,g=u(c);for(f in e.tc)b=e.tc[f],b.tttimer&&(clearTimeout(b.tttimer),b.tttimer=null);if(b=g&&e.tc[g],b&&c.changedTouches&&b.touchState){switch(b.touchState){case 1:case 2:(d=K(c,b.canvas))&&(b.mx=d.x,b.my=d.y,b.Drag(c,d)&&(b.touchState=2));break;case 3:b.Pinch(c)}b.drawn=0}}function ab(b){var d=a,c=u(b);c&&d.tc[c]&&(b.cancelBubble=!0,b.returnValue=!1,b.preventDefault&&b.preventDefault(),d.tc[c].Wheel((b.wheelDelta||b.detail)>0))}function aw(d){var c,b=a;clearTimeout(b.scrollTimer);for(c in b.tc)b.tc[c].Pause();b.scrollTimer=setTimeout(function(){var b,c=a;for(b in c.tc)c.tc[b].Resume()},b.scrollPause)}function al(){Z(q())}function Z(b){var c=a.tc,d;a.NextFrame(a.interval),b=b||q();for(d in c)c[d].Draw(b)}function az(){requestAnimationFrame(Z)}function aA(a){setTimeout(al,a)}function X(f){var g=c.getElementById(f),b=g.getBoundingClientRect(),a=c.documentElement,d=c.body,e=window,h=e.pageXOffset||a.scrollLeft,i=e.pageYOffset||a.scrollTop,j=a.clientLeft||d.clientLeft,k=a.clientTop||d.clientTop;return{x:b.left+h-j,y:b.top+i-k}}function aI(a,b,d,e){var c=a.radius*a.z1/(a.z1+a.z2+b.z);return{x:b.x*c*d,y:b.y*c*e,z:b.z,w:(a.z1-b.z)/a.z2}}function V(a){this.e=a,this.br=0,this.line=[],this.text=[],this.original=a.innerText||a.textContent}F=V.prototype,F.Empty=function(){for(var a=0;ah?(d.push(this.line.join(' ')),this.line=[a[b]]):this.line.push(a[b]);d.push(this.line.join(' '))}return this.text=d};function _(a,b){this.ts=null,this.tc=a,this.tag=b,this.x=this.y=this.w=this.h=this.sc=1,this.z=0,this.pulse=1,this.pulsate=a.pulsateTo<1,this.colour=a.outlineColour,this.adash=~~a.outlineDash,this.agap=~~a.outlineDashSpace||this.adash,this.aspeed=a.outlineDashSpeed*1,this.colour=='tag'?this.colour=i(b.a,'color'):this.colour=='tagbg'&&(this.colour=i(b.a,'background-color')),this.Draw=this.pulsate?this.DrawPulsate:this.DrawSimple,this.radius=a.outlineRadius|0,this.SetMethod(a.outlineMethod,a.altImage)}f=_.prototype,f.SetMethod=function(a,d){var b={block:['PreDraw','DrawBlock'],colour:['PreDraw','DrawColour'],outline:['PostDraw','DrawOutline'],classic:['LastDraw','DrawOutline'],size:['PreDraw','DrawSize'],none:['LastDraw']},c=b[a]||b.outline;a=='none'?this.Draw=function(){return 1}:this.drawFunc=this[c[1]],this[c[0]]=this.Draw,d&&(this.RealPreDraw=this.PreDraw,this.PreDraw=this.DrawAlt)},f.Update=function(d,e,i,j,a,f,g,h){var b=this.tc.outlineOffset,c=2*b;this.x=a*d+g-b,this.y=a*e+h-b,this.w=a*i+c,this.h=a*j+c,this.sc=a,this.z=f},f.Ants=function(k){if(!this.adash)return;var b=this.adash,c=this.agap,a=this.aspeed,j=b+c,h=0,g=b,f=c,i=0,d=0,e;a&&(d=p(a)*(q()-this.ts)/50,a<0&&(d=864e4-d),a=~~d%j),a?(b>=a?(h=b-a,g=a):(f=j-a,i=c-f),e=[h,f,g,i]):e=[b,c],k.setLineDash(e)},f.DrawOutline=function(a,d,e,b,c,f){var g=h(this.radius,c/2,b/2);a.strokeStyle=f,this.Ants(a),y(a,d,e,b,c,g,!0)},f.DrawSize=function(i,n,m,l,k,j,a,h,g){var f=a.w,e=a.h,c,b,d;return this.pulsate?(a.image?d=(a.image.height+this.tc.outlineIncrease)/a.image.height:d=a.oscale,b=a.fimage||a.image,c=1+(d-1)*(1-this.pulse),a.h*=c,a.w*=c):b=a.oimage,a.alpha=1,a.Draw(i,h,g,b),a.h=e,a.w=f,1},f.DrawColour=function(d,h,i,e,f,g,a,b,c){return a.oimage?(this.pulse<1?(a.alpha=1-w(this.pulse,2),a.Draw(d,b,c,a.fimage),a.alpha=this.pulse):a.alpha=1,a.Draw(d,b,c,a.oimage),1):this[a.image?'DrawColourImage':'DrawColourText'](d,h,i,e,f,g,a,b,c)},f.DrawColourText=function(f,h,i,j,g,e,a,b,c){var d=a.colour;return a.colour=e,a.alpha=1,a.Draw(f,b,c),a.colour=d,1},f.DrawColourImage=function(a,q,p,o,n,m,i,r,l){var f=a.canvas,e=~~g(q,0),d=~~g(p,0),c=h(f.width-e,o)+.5|0,b=h(f.height-d,n)+.5|0,j;return v?(v.width=c,v.height=b):v=k(c,b),!v?this.SetMethod('outline'):(j=v.getContext('2d'),j.drawImage(f,e,d,c,b,0,0,c,b),a.clearRect(e,d,c,b),this.pulsate?i.alpha=1-w(this.pulse,2):i.alpha=1,i.Draw(a,r,l),a.setTransform(1,0,0,1,0,0),a.save(),a.beginPath(),a.rect(e,d,c,b),a.clip(),a.globalCompositeOperation='source-in',a.fillStyle=m,a.fillRect(e,d,c,b),a.restore(),a.globalAlpha=1,a.globalCompositeOperation='destination-over',a.drawImage(v,0,0,c,b,e,d,c,b),a.globalCompositeOperation='source-over',1)},f.DrawAlt=function(b,a,c,d,f,g){var e=this.RealPreDraw(b,a,c,d,f,g);return a.alt&&(a.DrawImage(b,c,d,a.alt),e=1),e},f.DrawBlock=function(a,d,e,b,c,f){var g=h(this.radius,c/2,b/2);a.fillStyle=f,y(a,d,e,b,c,g)},f.DrawSimple=function(a,b,c,d,e,f){var g=this.tc;return a.setTransform(1,0,0,1,0,0),a.strokeStyle=this.colour,a.lineWidth=g.outlineThickness,a.shadowBlur=a.shadowOffsetX=a.shadowOffsetY=0,a.globalAlpha=f?e:1,this.drawFunc(a,this.x,this.y,this.w,this.h,this.colour,b,c,d)},f.DrawPulsate=function(h,d,e,f){var g=q()-this.ts,c=this.tc,b=c.pulsateTo+(1-c.pulsateTo)*(.5+l(2*Math.PI*g/(1e3*c.pulsateTime))/2);return this.pulse=b=a.Smooth(1,b),this.DrawSimple(h,d,e,f,b,1)},f.Active=function(d,a,b){var c=a>=this.x&&b>=this.y&&a<=this.x+this.w&&b<=this.y+this.h;return c?this.ts=this.ts||q():this.ts=null,c},f.PreDraw=f.PostDraw=f.LastDraw=x;function J(a,h,c,b,e,f,g,d,i,j,k,l,m,n){this.tc=a,this.image=null,this.text=h,this.text_original=n,this.line_widths=[],this.title=c.title||null,this.a=c,this.position=new s(b[0],b[1],b[2]),this.x=this.y=this.z=0,this.w=e,this.h=f,this.colour=g||a.textColour,this.bgColour=d||a.bgColour,this.bgRadius=i|0,this.bgOutline=j||this.colour,this.bgOutlineThickness=k|0,this.textFont=l||a.textFont,this.padding=m|0,this.sc=this.alpha=1,this.weighted=!a.weight,this.outline=new _(a,this),this.audio=null}d=J.prototype,d.Init=function(b){var a=this.tc;this.textHeight=a.textHeight,this.HasText()?this.Measure(a.ctxt,a):(this.w=this.iw,this.h=this.ih),this.SetShadowColour=a.shadowAlpha?this.SetShadowColourAlpha:this.SetShadowColourFixed,this.SetDraw(a)},d.Draw=x,d.HasText=function(){return this.text&&this.text[0].length>0},d.EqualTo=function(a){var b=a.getElementsByTagName('img');return this.a.href!=a.href?0:b.length?this.image.src==b[0].src:(a.innerText||a.textContent)==this.text_original},d.SetImage=function(a){this.image=this.fimage=a},d.SetAudio=function(a){this.audio=a,this.audio.load()},d.SetDraw=function(a){this.Draw=this.fimage?a.ie>7?this.DrawImageIE:this.DrawImage:this.DrawText,a.noSelect&&(this.CheckActive=x)},d.MeasureText=function(d){var a,e=this.text.length,b=0,c;for(a=0;a0?c=H(c,this.oimage.width,this.oimage.height):this.oimage=H(this.oimage,c.width,c.height)),c&&(this.fimage=c,l=this.fimage.width/b,j=this.fimage.height/b),this.SetDraw(a),a.txtOpt=!!this.fimage),this.h=j,this.w=l},d.SetFont=function(a,b,c,d){this.textFont=a,this.colour=b,this.bgColour=c,this.bgOutline=d,this.Measure(this.tc.ctxt,this.tc)},d.SetWeight=function(c){var b=this.tc,e=b.weightMode.split(/[, ]/),d,a,f=c.length;if(!this.HasText())return;this.weighted=!0;for(a=0;a0&&a.weightSizeMax>a.weightSizeMin?this.textHeight=a.weightSize*(a.weightSizeMin+(a.weightSizeMax-a.weightSizeMin)*c):this.textHeight=g(1,b*a.weightSize))},d.SetShadowColourFixed=function(a,b,c){a.shadowColor=b},d.SetShadowColourAlpha=function(a,b,c){a.shadowColor=aE(b,c)},d.DrawText=function(a,h,i){var e=this.tc,g=this.x,f=this.y,c=this.sc,b,d;a.globalAlpha=this.alpha,a.fillStyle=this.colour,e.shadow&&this.SetShadowColour(a,e.shadow,this.alpha),a.font=this.font,g+=h/c,f+=i/c-this.h/2;for(b=0;b{this.stopped?this.audio.pause():this.playing=1}),1}};function a(f,o,k){var d,i,b=c.getElementById(f),l=['id','class','innerHTML'];if(!b)throw 0;if(n(window.G_vmlCanvasManager)&&(b=window.G_vmlCanvasManager.initElement(b),this.ie=parseFloat(navigator.appVersion.split('MSIE')[1])),b&&(!b.getContext||!b.getContext('2d').fillText)){i=c.createElement('DIV');for(d=0;d0?a.scrollPause=~~this.scrollPause:this.scrollPause=0,this.minTags>0&&this.repeatTags<1&&(d=this.GetTags().length)&&(this.repeatTags=af(this.minTags/d)-1),this.transform=m.Identity(),this.startTime=this.time=q(),this.mx=this.my=-1,this.centreImage&&av(this),this.Animate=this.dragControl?this.AnimateDrag:this.AnimatePosition,this.animTiming=typeof a[this.animTiming]=='function'?a[this.animTiming]:a.Smooth,this.shadowBlur||this.shadowOffset[0]||this.shadowOffset[1]?(this.ctxt.shadowColor=this.shadow,this.shadow=this.ctxt.shadowColor,this.shadowAlpha=aD()):delete this.shadow,this.activeAudio===!1?e='off':this.activeAudio&&this.LoadAudio(),this.Load(),o&&this.hideTags&&function(b){a.loaded?b.HideTags():t('load',function(){b.HideTags()},window)}(this),this.yaw=this.initial?this.initial[0]*this.maxSpeed:0,this.pitch=this.initial?this.initial[1]*this.maxSpeed:0,this.tooltip?(this.ctitle=b.title,b.title='',this.tooltip=='native'?this.Tooltip=this.TooltipNative:(this.Tooltip=this.TooltipDiv,this.ttdiv||(this.ttdiv=c.createElement('div'),this.ttdiv.className=this.tooltipClass,this.ttdiv.style.position='absolute',this.ttdiv.style.zIndex=b.style.zIndex+1,t('mouseover',function(a){a.target.style.display='none'},this.ttdiv),c.body.appendChild(this.ttdiv)))):this.Tooltip=this.TooltipNone,!this.noMouse&&!j[f]){j[f]=[['mousemove',ad],['mouseout',an],['mouseup',aq],['touchstart',ar],['touchend',ac],['touchcancel',ac],['touchmove',au]],this.dragControl&&(j[f].push(['mousedown',ap]),j[f].push(['selectstart',x])),this.wheelZoom&&(j[f].push(['mousewheel',ab]),j[f].push(['DOMMouseScroll',ab])),this.scrollPause&&j[f].push(['scroll',aw,window]);for(d=0;dthis.max_weight[a])&&(this.max_weight[a]=c),(!this.min_weight[a]||cthis.min_weight[a]&&(g=1);if(g)for(b=0;b=d&&this.my>=e)return!0},b.ToggleAudio=function(){var a=this.audioOff||e&&e.state==='suspended';a||this.currentAudio&&this.currentAudio.StopAudio(),this.audioOff=!a},b.Draw=function(s){if(this.paused)return;var l=this.canvas,i=l.width,j=l.height,q=0,p=(s-this.time)*a.interval/1e3,h=i/2+this.offsetX,g=j/2+this.offsetY,d=this.ctxt,b,f,c,o=-1,e=this.taglist,k=e.length,t=this.active&&this.active.tag,m='',u=this.frontSelect,r=this.centreFunc==x,n;if(this.time=s,this.frozen&&this.drawn)return this.Animate(i,j,p);n=this.AnimateFixed(),d.setTransform(1,0,0,1,0,0);for(c=0;c=0&&this.my>=0&&this.taglist[c].CheckActive(d,h,g),f&&f.sc>q&&(!u||f.z<=0)&&(b=f,o=c,b.tag=this.taglist[c],q=f.sc);this.active=b}this.txtOpt||this.shadow&&this.SetShadow(d),d.clearRect(0,0,i,j);for(c=0;c=this.fadeIn?(this.fadeIn=0,this.fixedAlpha=1):this.fixedAlpha=b/this.fadeIn),this.fixedAnim)&&(this.fixedAnim.transform||(this.fixedAnim.transform=this.transform),a=this.fixedAnim,b=q()-a.t0,c=a.angle,d,e=this.animTiming(a.t,b),this.transform=a.transform,b>=a.t?(this.fixedCallbackTag=a.tag,this.fixedCallback=a.cb,this.fixedAnim=this.yaw=this.pitch=0):c*=e,d=m.Rotation(c,a.axis),this.transform=this.transform.mul(d),this.fixedAnim!=0)},b.AnimatePosition=function(g,h,f){var a=this,d=a.mx,e=a.my,b,c;!a.frozen&&d>=0&&e>=0&&db&&(a.yaw=c>a.z0?a.yaw*a.decel:0),!a.ly&&d>b&&(a.pitch=d>a.z0?a.pitch*a.decel:0)},b.Zoom=function(a){this.z2=this.z1*(1/a),this.drawn=0},b.Clicked=function(b){if(this.CheckAudioIcon()){this.ToggleAudio();return}var a=this.active;try{a&&a.tag&&(this.clickToFront===!1||this.clickToFront===null?a.tag.Clicked(b):this.TagToFront(a.tag,this.clickToFront,function(){a.tag.Clicked(b)},!0))}catch(a){}},b.Wheel=function(a){var b=this.zoom+this.zoomStep*(a?1:-1);this.zoom=h(this.zoomMax,g(this.zoomMin,b)),this.Zoom(this.zoom)},b.BeginDrag=function(a){this.down=K(a,this.canvas),a.cancelBubble=!0,a.returnValue=!1,a.preventDefault&&a.preventDefault()},b.Drag=function(e,a){if(this.dragControl&&this.down){var d=this.dragThreshold*this.dragThreshold,b=a.x-this.down.x,c=a.y-this.down.y;(this.dragging||b*b+c*c>d)&&(this.dx=b,this.dy=c,this.dragging=1,this.down=a)}return this.dragging},b.EndDrag=function(){var a=this.dragging;return this.dragging=this.down=null,a};function ah(a){var b=a.targetTouches[0],c=a.targetTouches[1];return E(w(c.pageX-b.pageX,2)+w(c.pageY-b.pageY,2))}b.BeginPinch=function(a){this.pinched=[ah(a),this.zoom],a.preventDefault&&a.preventDefault()},b.Pinch=function(d){var b,c,a=this.pinched;if(!a)return;c=ah(d),b=a[1]*c/a[0],this.zoom=h(this.zoomMax,g(this.zoomMin,b)),this.Zoom(this.zoom)},b.EndPinch=function(a){this.pinched=null},b.Pause=function(){this.paused=!0},b.Resume=function(){this.paused=!1},b.SetSpeed=function(a){this.initial=a,this.yaw=a[0]*this.maxSpeed,this.pitch=a[1]*this.maxSpeed},b.FindTag=function(a){if(!n(a))return null;if(n(a.index)&&(a=a.index),!B(a))return this.taglist[a];var c,d,b;n(a.id)?(c='id',d=a.id):n(a.text)&&(c='innerText',d=a.text);for(b=0;b + + + + + diff --git a/packages/frontend/package.json b/packages/frontend/package.json new file mode 100644 index 0000000000..c23adf7c70 --- /dev/null +++ b/packages/frontend/package.json @@ -0,0 +1,94 @@ +{ + "name": "frontend", + "private": true, + "scripts": { + "watch": "vite", + "build": "vite build", + "lint": "vue-tsc --noEmit && eslint --quiet \"src/**/*.{ts,vue}\"" + }, + "dependencies": { + "@discordapp/twemoji": "14.0.2", + "@rollup/plugin-alias": "4.0.2", + "@rollup/plugin-json": "6.0.0", + "@rollup/pluginutils": "5.0.2", + "@syuilo/aiscript": "0.11.1", + "@tabler/icons": "^1.118.0", + "@vitejs/plugin-vue": "4.0.0", + "@vue/compiler-sfc": "3.2.45", + "autobind-decorator": "2.4.0", + "autosize": "5.0.2", + "blurhash": "2.0.4", + "broadcast-channel": "4.18.1", + "browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3", + "chart.js": "4.1.1", + "chartjs-adapter-date-fns": "3.0.0", + "chartjs-chart-matrix": "^1.3.0", + "chartjs-plugin-gradient": "0.6.1", + "chartjs-plugin-zoom": "2.0.0", + "compare-versions": "5.0.1", + "cropperjs": "2.0.0-beta", + "date-fns": "2.29.3", + "escape-regexp": "0.0.1", + "eventemitter3": "5.0.0", + "idb-keyval": "6.2.0", + "insert-text-at-cursor": "0.3.0", + "is-file-animated": "1.0.2", + "json5": "2.2.2", + "katex": "0.15.6", + "matter-js": "0.18.0", + "mfm-js": "0.23.0", + "misskey-js": "0.0.14", + "photoswipe": "5.3.4", + "prismjs": "1.29.0", + "punycode": "2.1.1", + "querystring": "0.2.1", + "rndstr": "1.0.0", + "rollup": "3.8.0", + "s-age": "1.1.2", + "sass": "1.57.1", + "seedrandom": "3.0.5", + "strict-event-emitter-types": "2.0.0", + "stringz": "2.1.0", + "syuilo-password-strength": "0.0.1", + "textarea-caret": "3.1.0", + "three": "0.148.0", + "throttle-debounce": "5.0.0", + "tinycolor2": "1.4.2", + "tsc-alias": "1.8.2", + "tsconfig-paths": "4.1.1", + "twemoji-parser": "14.0.0", + "typescript": "4.9.4", + "uuid": "9.0.0", + "vanilla-tilt": "1.8.0", + "vite": "4.0.3", + "vue": "3.2.45", + "vue-prism-editor": "2.0.0-alpha.2", + "vuedraggable": "next" + }, + "devDependencies": { + "@types/escape-regexp": "0.0.1", + "@types/glob": "8.0.0", + "@types/gulp": "4.0.10", + "@types/gulp-rename": "2.0.1", + "@types/katex": "0.14.0", + "@types/matter-js": "0.18.2", + "@types/punycode": "2.1.0", + "@types/seedrandom": "3.0.3", + "@types/throttle-debounce": "5.0.0", + "@types/tinycolor2": "1.4.3", + "@types/uuid": "9.0.0", + "@types/websocket": "1.0.5", + "@types/ws": "8.5.3", + "@typescript-eslint/eslint-plugin": "5.47.0", + "@typescript-eslint/parser": "5.47.0", + "@vue/runtime-core": "3.2.45", + "cross-env": "7.0.3", + "cypress": "12.2.0", + "eslint": "8.30.0", + "eslint-plugin-import": "2.26.0", + "eslint-plugin-vue": "9.8.0", + "start-server-and-test": "1.15.2", + "vue-eslint-parser": "^9.1.0", + "vue-tsc": "^1.0.16" + } +} diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts new file mode 100644 index 0000000000..0e991cdfb5 --- /dev/null +++ b/packages/frontend/src/account.ts @@ -0,0 +1,238 @@ +import { defineAsyncComponent, reactive } from 'vue'; +import * as misskey from 'misskey-js'; +import { showSuspendedDialog } from './scripts/show-suspended-dialog'; +import { i18n } from './i18n'; +import { del, get, set } from '@/scripts/idb-proxy'; +import { apiUrl } from '@/config'; +import { waiting, api, popup, popupMenu, success, alert } from '@/os'; +import { unisonReload, reloadChannel } from '@/scripts/unison-reload'; + +// TODO: 他のタブと永続化されたstateを同期 + +type Account = misskey.entities.MeDetailed; + +const accountData = localStorage.getItem('account'); + +// TODO: 外部からはreadonlyに +export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; + +export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); +export const iAmAdmin = $i != null && $i.isAdmin; + +export async function signout() { + waiting(); + localStorage.removeItem('account'); + + await removeAccount($i.id); + + const accounts = await getAccounts(); + + //#region Remove service worker registration + try { + if (navigator.serviceWorker.controller) { + const registration = await navigator.serviceWorker.ready; + const push = await registration.pushManager.getSubscription(); + if (push) { + await window.fetch(`${apiUrl}/sw/unregister`, { + method: 'POST', + body: JSON.stringify({ + i: $i.token, + endpoint: push.endpoint, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + } + + if (accounts.length === 0) { + await navigator.serviceWorker.getRegistrations() + .then(registrations => { + return Promise.all(registrations.map(registration => registration.unregister())); + }); + } + } catch (err) {} + //#endregion + + document.cookie = 'igi=; path=/'; + + if (accounts.length > 0) login(accounts[0].token); + else unisonReload('/'); +} + +export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> { + return (await get('accounts')) || []; +} + +export async function addAccount(id: Account['id'], token: Account['token']) { + const accounts = await getAccounts(); + if (!accounts.some(x => x.id === id)) { + await set('accounts', accounts.concat([{ id, token }])); + } +} + +export async function removeAccount(id: Account['id']) { + const accounts = await getAccounts(); + accounts.splice(accounts.findIndex(x => x.id === id), 1); + + if (accounts.length > 0) await set('accounts', accounts); + else await del('accounts'); +} + +function fetchAccount(token: string): Promise { + return new Promise((done, fail) => { + // Fetch user + window.fetch(`${apiUrl}/i`, { + method: 'POST', + body: JSON.stringify({ + i: token, + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(res => res.json()) + .then(res => { + if (res.error) { + if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') { + showSuspendedDialog().then(() => { + signout(); + }); + } else { + alert({ + type: 'error', + title: i18n.ts.failedToFetchAccountInformation, + text: JSON.stringify(res.error), + }); + } + } else { + res.token = token; + done(res); + } + }) + .catch(fail); + }); +} + +export function updateAccount(accountData) { + for (const [key, value] of Object.entries(accountData)) { + $i[key] = value; + } + localStorage.setItem('account', JSON.stringify($i)); +} + +export function refreshAccount() { + return fetchAccount($i.token).then(updateAccount); +} + +export async function login(token: Account['token'], redirect?: string) { + waiting(); + if (_DEV_) console.log('logging as token ', token); + const me = await fetchAccount(token); + localStorage.setItem('account', JSON.stringify(me)); + document.cookie = `token=${token}; path=/; max-age=31536000`; // bull dashboardの認証とかで使う + await addAccount(me.id, token); + + if (redirect) { + // 他のタブは再読み込みするだけ + reloadChannel.postMessage(null); + // このページはredirectで指定された先に移動 + location.href = redirect; + return; + } + + unisonReload(); +} + +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(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { + done: res => { + addAccount(res.id, res.i); + success(); + }, + }, 'closed'); + } + + function createAccount() { + popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { + done: res => { + addAccount(res.id, res.i); + switchAccountWithToken(res.i); + }, + }, 'closed'); + } + + async function switchAccount(account: misskey.entities.UserDetailed) { + const storedAccounts = await getAccounts(); + const token = storedAccounts.find(x => x.id === account.id).token; + switchAccountWithToken(token); + } + + function switchAccountWithToken(token: string) { + login(token); + } + + const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id)); + const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) }); + + 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(createItem(account)); + }); + })); + + if (opts.withExtraOperation) { + popupMenu([...[{ + type: 'link', + text: i18n.ts.profile, + to: `/@${ $i.username }`, + avatar: $i, + }, null, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { + type: 'parent', + icon: 'ti ti-plus', + text: i18n.ts.addAccount, + children: [{ + text: i18n.ts.existingAccount, + action: () => { showSigninDialog(); }, + }, { + text: i18n.ts.createAccount, + action: () => { createAccount(); }, + }], + }, { + type: 'link', + icon: 'ti ti-users', + text: i18n.ts.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/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue new file mode 100644 index 0000000000..9a3464b640 --- /dev/null +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue new file mode 100644 index 0000000000..039f77c859 --- /dev/null +++ b/packages/frontend/src/components/MkAbuseReportWindow.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/packages/frontend/src/components/MkActiveUsersHeatmap.vue b/packages/frontend/src/components/MkActiveUsersHeatmap.vue new file mode 100644 index 0000000000..02b2eeeb36 --- /dev/null +++ b/packages/frontend/src/components/MkActiveUsersHeatmap.vue @@ -0,0 +1,236 @@ + + + diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue new file mode 100644 index 0000000000..40ef626aed --- /dev/null +++ b/packages/frontend/src/components/MkAnalogClock.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue new file mode 100644 index 0000000000..72783921d5 --- /dev/null +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -0,0 +1,476 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue new file mode 100644 index 0000000000..162338b639 --- /dev/null +++ b/packages/frontend/src/components/MkAvatars.vue @@ -0,0 +1,24 @@ + + + diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue new file mode 100644 index 0000000000..891645bb2a --- /dev/null +++ b/packages/frontend/src/components/MkButton.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue new file mode 100644 index 0000000000..6d218389fc --- /dev/null +++ b/packages/frontend/src/components/MkCaptcha.vue @@ -0,0 +1,118 @@ + + + diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue new file mode 100644 index 0000000000..9e275d6172 --- /dev/null +++ b/packages/frontend/src/components/MkChannelFollowButton.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue new file mode 100644 index 0000000000..6ef50bddcf --- /dev/null +++ b/packages/frontend/src/components/MkChannelPreview.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue new file mode 100644 index 0000000000..fbbc231b88 --- /dev/null +++ b/packages/frontend/src/components/MkChart.vue @@ -0,0 +1,859 @@ + + + + + diff --git a/packages/frontend/src/components/MkChartTooltip.vue b/packages/frontend/src/components/MkChartTooltip.vue new file mode 100644 index 0000000000..d36f45463c --- /dev/null +++ b/packages/frontend/src/components/MkChartTooltip.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue new file mode 100644 index 0000000000..b074028821 --- /dev/null +++ b/packages/frontend/src/components/MkCode.core.vue @@ -0,0 +1,20 @@ + + + + diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue new file mode 100644 index 0000000000..1640258d5b --- /dev/null +++ b/packages/frontend/src/components/MkCode.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue new file mode 100644 index 0000000000..6d4d5be2bc --- /dev/null +++ b/packages/frontend/src/components/MkContainer.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue new file mode 100644 index 0000000000..cfc9502b41 --- /dev/null +++ b/packages/frontend/src/components/MkContextMenu.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue new file mode 100644 index 0000000000..ae18160dea --- /dev/null +++ b/packages/frontend/src/components/MkCropperDialog.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue new file mode 100644 index 0000000000..ee611921ef --- /dev/null +++ b/packages/frontend/src/components/MkCwButton.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue new file mode 100644 index 0000000000..1f88bdf137 --- /dev/null +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -0,0 +1,189 @@ + + + diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue new file mode 100644 index 0000000000..374ecd8abf --- /dev/null +++ b/packages/frontend/src/components/MkDialog.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue new file mode 100644 index 0000000000..9ed8d63d19 --- /dev/null +++ b/packages/frontend/src/components/MkDigitalClock.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue new file mode 100644 index 0000000000..8c17c0530a --- /dev/null +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -0,0 +1,334 @@ + + + + + diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue new file mode 100644 index 0000000000..82653ca0b4 --- /dev/null +++ b/packages/frontend/src/components/MkDrive.folder.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue new file mode 100644 index 0000000000..dbbfef5f05 --- /dev/null +++ b/packages/frontend/src/components/MkDrive.navFolder.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue new file mode 100644 index 0000000000..4053870950 --- /dev/null +++ b/packages/frontend/src/components/MkDrive.vue @@ -0,0 +1,801 @@ + + + + + diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue new file mode 100644 index 0000000000..33379ed5ca --- /dev/null +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveSelectDialog.vue new file mode 100644 index 0000000000..3ee821b539 --- /dev/null +++ b/packages/frontend/src/components/MkDriveSelectDialog.vue @@ -0,0 +1,58 @@ + + + diff --git a/packages/frontend/src/components/MkDriveWindow.vue b/packages/frontend/src/components/MkDriveWindow.vue new file mode 100644 index 0000000000..617200321b --- /dev/null +++ b/packages/frontend/src/components/MkDriveWindow.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue new file mode 100644 index 0000000000..f6ba7abfc4 --- /dev/null +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue new file mode 100644 index 0000000000..814f71168a --- /dev/null +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -0,0 +1,569 @@ + + + + + diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue new file mode 100644 index 0000000000..3b41f9d75b --- /dev/null +++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue new file mode 100644 index 0000000000..523e4ba695 --- /dev/null +++ b/packages/frontend/src/components/MkEmojiPickerWindow.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/packages/frontend/src/components/MkFeaturedPhotos.vue b/packages/frontend/src/components/MkFeaturedPhotos.vue new file mode 100644 index 0000000000..e58b5d2849 --- /dev/null +++ b/packages/frontend/src/components/MkFeaturedPhotos.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue new file mode 100644 index 0000000000..73875251f0 --- /dev/null +++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue new file mode 100644 index 0000000000..4910506a95 --- /dev/null +++ b/packages/frontend/src/components/MkFileListForAdmin.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue new file mode 100644 index 0000000000..9e83b07cd7 --- /dev/null +++ b/packages/frontend/src/components/MkFolder.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue new file mode 100644 index 0000000000..ee256d9263 --- /dev/null +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue new file mode 100644 index 0000000000..1b55451c94 --- /dev/null +++ b/packages/frontend/src/components/MkForgotPassword.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue new file mode 100644 index 0000000000..b2bf76a8c7 --- /dev/null +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/packages/frontend/src/components/MkFormula.vue b/packages/frontend/src/components/MkFormula.vue new file mode 100644 index 0000000000..65a2fee930 --- /dev/null +++ b/packages/frontend/src/components/MkFormula.vue @@ -0,0 +1,24 @@ + + + diff --git a/packages/frontend/src/components/MkFormulaCore.vue b/packages/frontend/src/components/MkFormulaCore.vue new file mode 100644 index 0000000000..6028db9e64 --- /dev/null +++ b/packages/frontend/src/components/MkFormulaCore.vue @@ -0,0 +1,34 @@ + + + + + + diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue new file mode 100644 index 0000000000..a133f6431b --- /dev/null +++ b/packages/frontend/src/components/MkGalleryPostPreview.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue new file mode 100644 index 0000000000..d104cd4cd4 --- /dev/null +++ b/packages/frontend/src/components/MkGoogle.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/frontend/src/components/MkImageViewer.vue b/packages/frontend/src/components/MkImageViewer.vue new file mode 100644 index 0000000000..f074b1a2f2 --- /dev/null +++ b/packages/frontend/src/components/MkImageViewer.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue new file mode 100644 index 0000000000..80d7c201a4 --- /dev/null +++ b/packages/frontend/src/components/MkImgWithBlurhash.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue new file mode 100644 index 0000000000..7aaf2c5bcb --- /dev/null +++ b/packages/frontend/src/components/MkInfo.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue new file mode 100644 index 0000000000..4625de40af --- /dev/null +++ b/packages/frontend/src/components/MkInstanceCardMini.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue new file mode 100644 index 0000000000..41f6f9ffd5 --- /dev/null +++ b/packages/frontend/src/components/MkInstanceStats.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue new file mode 100644 index 0000000000..646172fe8d --- /dev/null +++ b/packages/frontend/src/components/MkInstanceTicker.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue new file mode 100644 index 0000000000..ff69c79641 --- /dev/null +++ b/packages/frontend/src/components/MkKeyValue.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue new file mode 100644 index 0000000000..1ccc648c72 --- /dev/null +++ b/packages/frontend/src/components/MkLaunchPad.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue new file mode 100644 index 0000000000..6148ec6195 --- /dev/null +++ b/packages/frontend/src/components/MkLink.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/packages/frontend/src/components/MkMarquee.vue b/packages/frontend/src/components/MkMarquee.vue new file mode 100644 index 0000000000..5ca04b0b48 --- /dev/null +++ b/packages/frontend/src/components/MkMarquee.vue @@ -0,0 +1,106 @@ + + + diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue new file mode 100644 index 0000000000..aa06c00fc6 --- /dev/null +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue new file mode 100644 index 0000000000..56570eaa05 --- /dev/null +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue new file mode 100644 index 0000000000..c6f8612182 --- /dev/null +++ b/packages/frontend/src/components/MkMediaList.vue @@ -0,0 +1,189 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue new file mode 100644 index 0000000000..df0bf84116 --- /dev/null +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue new file mode 100644 index 0000000000..3091b435e4 --- /dev/null +++ b/packages/frontend/src/components/MkMention.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue new file mode 100644 index 0000000000..3ada4afbdc --- /dev/null +++ b/packages/frontend/src/components/MkMenu.child.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue new file mode 100644 index 0000000000..64d18b6b7c --- /dev/null +++ b/packages/frontend/src/components/MkMenu.vue @@ -0,0 +1,367 @@ + + + + + diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue new file mode 100644 index 0000000000..c64ce163f9 --- /dev/null +++ b/packages/frontend/src/components/MkMiniChart.vue @@ -0,0 +1,73 @@ + + + diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue new file mode 100644 index 0000000000..2305a02794 --- /dev/null +++ b/packages/frontend/src/components/MkModal.vue @@ -0,0 +1,406 @@ + + + + + diff --git a/packages/frontend/src/components/MkModalPageWindow.vue b/packages/frontend/src/components/MkModalPageWindow.vue new file mode 100644 index 0000000000..ced8a7a714 --- /dev/null +++ b/packages/frontend/src/components/MkModalPageWindow.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue new file mode 100644 index 0000000000..d977ca6e9c --- /dev/null +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue new file mode 100644 index 0000000000..a4100e1f2c --- /dev/null +++ b/packages/frontend/src/components/MkNote.vue @@ -0,0 +1,658 @@ + + + + + diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue new file mode 100644 index 0000000000..7ce8e039d9 --- /dev/null +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -0,0 +1,677 @@ + + + + + diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue new file mode 100644 index 0000000000..333c3ddbd9 --- /dev/null +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue new file mode 100644 index 0000000000..0c81059091 --- /dev/null +++ b/packages/frontend/src/components/MkNotePreview.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue new file mode 100644 index 0000000000..96d29831d2 --- /dev/null +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue new file mode 100644 index 0000000000..d03ce7c434 --- /dev/null +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue new file mode 100644 index 0000000000..5abcdc2298 --- /dev/null +++ b/packages/frontend/src/components/MkNotes.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue new file mode 100644 index 0000000000..8b8d3f452d --- /dev/null +++ b/packages/frontend/src/components/MkNotification.vue @@ -0,0 +1,323 @@ + + + + + diff --git a/packages/frontend/src/components/MkNotificationSettingWindow.vue b/packages/frontend/src/components/MkNotificationSettingWindow.vue new file mode 100644 index 0000000000..75bea2976c --- /dev/null +++ b/packages/frontend/src/components/MkNotificationSettingWindow.vue @@ -0,0 +1,87 @@ + + + diff --git a/packages/frontend/src/components/MkNotificationToast.vue b/packages/frontend/src/components/MkNotificationToast.vue new file mode 100644 index 0000000000..07640792c0 --- /dev/null +++ b/packages/frontend/src/components/MkNotificationToast.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue new file mode 100644 index 0000000000..0e1cc06743 --- /dev/null +++ b/packages/frontend/src/components/MkNotifications.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/packages/frontend/src/components/MkNumberDiff.vue b/packages/frontend/src/components/MkNumberDiff.vue new file mode 100644 index 0000000000..e7d4a5472a --- /dev/null +++ b/packages/frontend/src/components/MkNumberDiff.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue new file mode 100644 index 0000000000..0c7230d783 --- /dev/null +++ b/packages/frontend/src/components/MkObjectView.value.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/packages/frontend/src/components/MkObjectView.vue b/packages/frontend/src/components/MkObjectView.vue new file mode 100644 index 0000000000..55578a37f6 --- /dev/null +++ b/packages/frontend/src/components/MkObjectView.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue new file mode 100644 index 0000000000..009582e540 --- /dev/null +++ b/packages/frontend/src/components/MkPagePreview.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue new file mode 100644 index 0000000000..29d45558a7 --- /dev/null +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue new file mode 100644 index 0000000000..291409171a --- /dev/null +++ b/packages/frontend/src/components/MkPagination.vue @@ -0,0 +1,317 @@ + + + + + diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue new file mode 100644 index 0000000000..a1b927e42a --- /dev/null +++ b/packages/frontend/src/components/MkPoll.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue new file mode 100644 index 0000000000..556abc5fd0 --- /dev/null +++ b/packages/frontend/src/components/MkPollEditor.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue new file mode 100644 index 0000000000..f04c7f5618 --- /dev/null +++ b/packages/frontend/src/components/MkPopupMenu.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue new file mode 100644 index 0000000000..f79e5a32cd --- /dev/null +++ b/packages/frontend/src/components/MkPostForm.vue @@ -0,0 +1,1050 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue new file mode 100644 index 0000000000..5a0ba0d8d3 --- /dev/null +++ b/packages/frontend/src/components/global/MkA.vue @@ -0,0 +1,102 @@ + + + diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue new file mode 100644 index 0000000000..c3e806b5fb --- /dev/null +++ b/packages/frontend/src/components/global/MkAcct.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue new file mode 100644 index 0000000000..a80efb142c --- /dev/null +++ b/packages/frontend/src/components/global/MkAd.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue new file mode 100644 index 0000000000..5f3e3c176d --- /dev/null +++ b/packages/frontend/src/components/global/MkAvatar.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkEllipsis.vue b/packages/frontend/src/components/global/MkEllipsis.vue new file mode 100644 index 0000000000..0a46f486d6 --- /dev/null +++ b/packages/frontend/src/components/global/MkEllipsis.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue new file mode 100644 index 0000000000..ce1299a39f --- /dev/null +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue new file mode 100644 index 0000000000..e135d4184b --- /dev/null +++ b/packages/frontend/src/components/global/MkError.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue new file mode 100644 index 0000000000..64e12e3b44 --- /dev/null +++ b/packages/frontend/src/components/global/MkLoading.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue new file mode 100644 index 0000000000..70d0108e9f --- /dev/null +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.vue @@ -0,0 +1,191 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue new file mode 100644 index 0000000000..a228dfe883 --- /dev/null +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -0,0 +1,368 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkSpacer.vue b/packages/frontend/src/components/global/MkSpacer.vue new file mode 100644 index 0000000000..b3a42d77e7 --- /dev/null +++ b/packages/frontend/src/components/global/MkSpacer.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue new file mode 100644 index 0000000000..44f4f065a6 --- /dev/null +++ b/packages/frontend/src/components/global/MkStickyContainer.vue @@ -0,0 +1,66 @@ + + + + + + + diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue new file mode 100644 index 0000000000..f72b153f56 --- /dev/null +++ b/packages/frontend/src/components/global/MkTime.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue new file mode 100644 index 0000000000..9f5be96224 --- /dev/null +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkUserName.vue b/packages/frontend/src/components/global/MkUserName.vue new file mode 100644 index 0000000000..090de3df30 --- /dev/null +++ b/packages/frontend/src/components/global/MkUserName.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue new file mode 100644 index 0000000000..e21a57471c --- /dev/null +++ b/packages/frontend/src/components/global/RouterView.vue @@ -0,0 +1,61 @@ + + + diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts new file mode 100644 index 0000000000..1fd293ba10 --- /dev/null +++ b/packages/frontend/src/components/global/i18n.ts @@ -0,0 +1,42 @@ +import { h, defineComponent } from 'vue'; + +export default defineComponent({ + props: { + src: { + type: String, + required: true, + }, + tag: { + type: String, + required: false, + default: 'span', + }, + textTag: { + type: String, + required: false, + default: null, + }, + }, + render() { + let str = this.src; + const parsed = [] as (string | { arg: string; })[]; + while (true) { + const nextBracketOpen = str.indexOf('{'); + const nextBracketClose = str.indexOf('}'); + + if (nextBracketOpen === -1) { + parsed.push(str); + break; + } else { + if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen)); + parsed.push({ + arg: str.substring(nextBracketOpen + 1, nextBracketClose), + }); + } + + str = str.substr(nextBracketClose + 1); + } + + return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]())); + }, +}); diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts new file mode 100644 index 0000000000..8639257003 --- /dev/null +++ b/packages/frontend/src/components/index.ts @@ -0,0 +1,61 @@ +import { App } from 'vue'; + +import Mfm from './global/MkMisskeyFlavoredMarkdown.vue'; +import MkA from './global/MkA.vue'; +import MkAcct from './global/MkAcct.vue'; +import MkAvatar from './global/MkAvatar.vue'; +import MkEmoji from './global/MkEmoji.vue'; +import MkUserName from './global/MkUserName.vue'; +import MkEllipsis from './global/MkEllipsis.vue'; +import MkTime from './global/MkTime.vue'; +import MkUrl from './global/MkUrl.vue'; +import I18n from './global/i18n'; +import RouterView from './global/RouterView.vue'; +import MkLoading from './global/MkLoading.vue'; +import MkError from './global/MkError.vue'; +import MkAd from './global/MkAd.vue'; +import MkPageHeader from './global/MkPageHeader.vue'; +import MkSpacer from './global/MkSpacer.vue'; +import MkStickyContainer from './global/MkStickyContainer.vue'; + +export default function(app: App) { + app.component('I18n', I18n); + app.component('RouterView', RouterView); + app.component('Mfm', Mfm); + app.component('MkA', MkA); + app.component('MkAcct', MkAcct); + app.component('MkAvatar', MkAvatar); + app.component('MkEmoji', MkEmoji); + app.component('MkUserName', MkUserName); + app.component('MkEllipsis', MkEllipsis); + app.component('MkTime', MkTime); + app.component('MkUrl', MkUrl); + app.component('MkLoading', MkLoading); + app.component('MkError', MkError); + app.component('MkAd', MkAd); + app.component('MkPageHeader', MkPageHeader); + app.component('MkSpacer', MkSpacer); + app.component('MkStickyContainer', MkStickyContainer); +} + +declare module '@vue/runtime-core' { + export interface GlobalComponents { + I18n: typeof I18n; + RouterView: typeof RouterView; + Mfm: typeof Mfm; + MkA: typeof MkA; + MkAcct: typeof MkAcct; + MkAvatar: typeof MkAvatar; + MkEmoji: typeof MkEmoji; + MkUserName: typeof MkUserName; + MkEllipsis: typeof MkEllipsis; + MkTime: typeof MkTime; + MkUrl: typeof MkUrl; + MkLoading: typeof MkLoading; + MkError: typeof MkError; + MkAd: typeof MkAd; + MkPageHeader: typeof MkPageHeader; + MkSpacer: typeof MkSpacer; + MkStickyContainer: typeof MkStickyContainer; + } +} diff --git a/packages/frontend/src/components/mfm.ts b/packages/frontend/src/components/mfm.ts new file mode 100644 index 0000000000..5b5b1caae3 --- /dev/null +++ b/packages/frontend/src/components/mfm.ts @@ -0,0 +1,331 @@ +import { VNode, defineComponent, h } from 'vue'; +import * as mfm from 'mfm-js'; +import MkUrl from '@/components/global/MkUrl.vue'; +import MkLink from '@/components/MkLink.vue'; +import MkMention from '@/components/MkMention.vue'; +import MkEmoji from '@/components/global/MkEmoji.vue'; +import { concat } from '@/scripts/array'; +import MkFormula from '@/components/MkFormula.vue'; +import MkCode from '@/components/MkCode.vue'; +import MkGoogle from '@/components/MkGoogle.vue'; +import MkSparkle from '@/components/MkSparkle.vue'; +import MkA from '@/components/global/MkA.vue'; +import { host } from '@/config'; +import { MFM_TAGS } from '@/scripts/mfm-tags'; + +export default defineComponent({ + props: { + text: { + type: String, + required: true, + }, + plain: { + type: Boolean, + default: false, + }, + nowrap: { + type: Boolean, + default: false, + }, + author: { + type: Object, + default: null, + }, + i: { + type: Object, + default: null, + }, + customEmojis: { + required: false, + }, + isNote: { + type: Boolean, + default: true, + }, + }, + + render() { + if (this.text == null || this.text === '') return; + + const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text, { fnNameList: MFM_TAGS }); + + const validTime = (t: string | null | undefined) => { + if (t == null) return null; + return t.match(/^[0-9.]+s$/) ? t : null; + }; + + const genEl = (ast: mfm.MfmNode[]) => ast.map((token): VNode | string | (VNode | string)[] => { + switch (token.type) { + case 'text': { + const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); + + if (!this.plain) { + const res: (VNode | string)[] = []; + for (const t of text.split('\n')) { + res.push(h('br')); + res.push(t); + } + res.shift(); + return res; + } else { + return [text.replace(/\n/g, ' ')]; + } + } + + case 'bold': { + return [h('b', genEl(token.children))]; + } + + case 'strike': { + return [h('del', genEl(token.children))]; + } + + case 'italic': { + return h('i', { + style: 'font-style: oblique;', + }, genEl(token.children)); + } + + case 'fn': { + // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる + let style; + switch (token.props.name) { + case 'tada': { + const speed = validTime(token.props.args.speed) || '1s'; + style = 'font-size: 150%;' + (this.$store.state.animatedMfm ? `animation: tada ${speed} linear infinite both;` : ''); + break; + } + case 'jelly': { + const speed = validTime(token.props.args.speed) || '1s'; + style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); + break; + } + case 'twitch': { + const speed = validTime(token.props.args.speed) || '0.5s'; + style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : ''; + break; + } + case 'shake': { + const speed = validTime(token.props.args.speed) || '0.5s'; + style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : ''; + break; + } + case 'spin': { + const direction = + token.props.args.left ? 'reverse' : + token.props.args.alternate ? 'alternate' : + 'normal'; + const anime = + token.props.args.x ? 'mfm-spinX' : + token.props.args.y ? 'mfm-spinY' : + 'mfm-spin'; + const speed = validTime(token.props.args.speed) || '1.5s'; + style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; + break; + } + case 'jump': { + const speed = validTime(token.props.args.speed) || '0.75s'; + style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : ''; + break; + } + case 'bounce': { + const speed = validTime(token.props.args.speed) || '0.75s'; + style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; + break; + } + case 'flip': { + const transform = + (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : + token.props.args.v ? 'scaleY(-1)' : + 'scaleX(-1)'; + style = `transform: ${transform};`; + break; + } + case 'x2': { + return h('span', { + class: 'mfm-x2', + }, genEl(token.children)); + } + case 'x3': { + return h('span', { + class: 'mfm-x3', + }, genEl(token.children)); + } + case 'x4': { + return h('span', { + class: 'mfm-x4', + }, genEl(token.children)); + } + case 'font': { + const family = + token.props.args.serif ? 'serif' : + token.props.args.monospace ? 'monospace' : + token.props.args.cursive ? 'cursive' : + token.props.args.fantasy ? 'fantasy' : + token.props.args.emoji ? 'emoji' : + token.props.args.math ? 'math' : + null; + if (family) style = `font-family: ${family};`; + break; + } + case 'blur': { + return h('span', { + class: '_mfm_blur_', + }, genEl(token.children)); + } + case 'rainbow': { + const speed = validTime(token.props.args.speed) || '1s'; + style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : ''; + break; + } + case 'sparkle': { + if (!this.$store.state.animatedMfm) { + return genEl(token.children); + } + return h(MkSparkle, {}, genEl(token.children)); + } + case 'rotate': { + const degrees = parseInt(token.props.args.deg) || '90'; + style = `transform: rotate(${degrees}deg); transform-origin: center center;`; + break; + } + } + if (style == null) { + return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']']); + } else { + return h('span', { + style: 'display: inline-block;' + style, + }, genEl(token.children)); + } + } + + case 'small': { + return [h('small', { + style: 'opacity: 0.7;', + }, genEl(token.children))]; + } + + case 'center': { + return [h('div', { + style: 'text-align:center;', + }, genEl(token.children))]; + } + + case 'url': { + return [h(MkUrl, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + })]; + } + + case 'link': { + return [h(MkLink, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + }, genEl(token.children))]; + } + + case 'mention': { + return [h(MkMention, { + key: Math.random(), + host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, + username: token.props.username, + })]; + } + + case 'hashtag': { + return [h(MkA, { + key: Math.random(), + to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`, + style: 'color:var(--hashtag);', + }, `#${token.props.hashtag}`)]; + } + + case 'blockCode': { + return [h(MkCode, { + key: Math.random(), + code: token.props.code, + lang: token.props.lang, + })]; + } + + case 'inlineCode': { + return [h(MkCode, { + key: Math.random(), + code: token.props.code, + inline: true, + })]; + } + + case 'quote': { + if (!this.nowrap) { + return [h('div', { + class: 'quote', + }, genEl(token.children))]; + } else { + return [h('span', { + class: 'quote', + }, genEl(token.children))]; + } + } + + case 'emojiCode': { + return [h(MkEmoji, { + key: Math.random(), + emoji: `:${token.props.name}:`, + customEmojis: this.customEmojis, + normal: this.plain, + })]; + } + + case 'unicodeEmoji': { + return [h(MkEmoji, { + key: Math.random(), + emoji: token.props.emoji, + customEmojis: this.customEmojis, + normal: this.plain, + })]; + } + + case 'mathInline': { + return [h(MkFormula, { + key: Math.random(), + formula: token.props.formula, + block: false, + })]; + } + + case 'mathBlock': { + return [h(MkFormula, { + key: Math.random(), + formula: token.props.formula, + block: true, + })]; + } + + case 'search': { + return [h(MkGoogle, { + key: Math.random(), + q: token.props.query, + })]; + } + + case 'plain': { + return [h('span', genEl(token.children))]; + } + + default: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + console.error('unrecognized ast type:', (token as any).type); + + return []; + } + } + }).flat(Infinity) as (VNode | string)[]; + + // Parse ast to DOM + return h('span', genEl(ast)); + }, +}); diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue new file mode 100644 index 0000000000..f3e7764604 --- /dev/null +++ b/packages/frontend/src/components/page/page.block.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/frontend/src/components/page/page.button.vue b/packages/frontend/src/components/page/page.button.vue new file mode 100644 index 0000000000..83931021d8 --- /dev/null +++ b/packages/frontend/src/components/page/page.button.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.canvas.vue b/packages/frontend/src/components/page/page.canvas.vue new file mode 100644 index 0000000000..80f6c8339c --- /dev/null +++ b/packages/frontend/src/components/page/page.canvas.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.counter.vue b/packages/frontend/src/components/page/page.counter.vue new file mode 100644 index 0000000000..a9e1f41a54 --- /dev/null +++ b/packages/frontend/src/components/page/page.counter.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.if.vue b/packages/frontend/src/components/page/page.if.vue new file mode 100644 index 0000000000..372a15f0c6 --- /dev/null +++ b/packages/frontend/src/components/page/page.if.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue new file mode 100644 index 0000000000..8ba70c5855 --- /dev/null +++ b/packages/frontend/src/components/page/page.image.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue new file mode 100644 index 0000000000..7d5c484a1b --- /dev/null +++ b/packages/frontend/src/components/page/page.note.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.number-input.vue b/packages/frontend/src/components/page/page.number-input.vue new file mode 100644 index 0000000000..50cf6d0770 --- /dev/null +++ b/packages/frontend/src/components/page/page.number-input.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.post.vue b/packages/frontend/src/components/page/page.post.vue new file mode 100644 index 0000000000..0ef50d65cd --- /dev/null +++ b/packages/frontend/src/components/page/page.post.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.radio-button.vue b/packages/frontend/src/components/page/page.radio-button.vue new file mode 100644 index 0000000000..b4d9e01a54 --- /dev/null +++ b/packages/frontend/src/components/page/page.radio-button.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue new file mode 100644 index 0000000000..630c1f5179 --- /dev/null +++ b/packages/frontend/src/components/page/page.section.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.switch.vue b/packages/frontend/src/components/page/page.switch.vue new file mode 100644 index 0000000000..64dc4ff8aa --- /dev/null +++ b/packages/frontend/src/components/page/page.switch.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.text-input.vue b/packages/frontend/src/components/page/page.text-input.vue new file mode 100644 index 0000000000..840649ece6 --- /dev/null +++ b/packages/frontend/src/components/page/page.text-input.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue new file mode 100644 index 0000000000..689c484521 --- /dev/null +++ b/packages/frontend/src/components/page/page.text.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/packages/frontend/src/components/page/page.textarea-input.vue b/packages/frontend/src/components/page/page.textarea-input.vue new file mode 100644 index 0000000000..507e1bd97b --- /dev/null +++ b/packages/frontend/src/components/page/page.textarea-input.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/frontend/src/components/page/page.textarea.vue b/packages/frontend/src/components/page/page.textarea.vue new file mode 100644 index 0000000000..f809925081 --- /dev/null +++ b/packages/frontend/src/components/page/page.textarea.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue new file mode 100644 index 0000000000..b5cb73c009 --- /dev/null +++ b/packages/frontend/src/components/page/page.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/packages/frontend/src/config.ts b/packages/frontend/src/config.ts new file mode 100644 index 0000000000..f2022b0f02 --- /dev/null +++ b/packages/frontend/src/config.ts @@ -0,0 +1,15 @@ +const address = new URL(location.href); +const siteName = (document.querySelector('meta[property="og:site_name"]') as HTMLMetaElement)?.content; + +export const host = address.host; +export const hostname = address.hostname; +export const url = address.origin; +export const apiUrl = url + '/api'; +export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming'; +export const lang = localStorage.getItem('lang'); +export const langs = _LANGS_; +export const locale = JSON.parse(localStorage.getItem('locale')); +export const version = _VERSION_; +export const instanceName = siteName === 'Misskey' ? host : siteName; +export const ui = localStorage.getItem('ui'); +export const debug = localStorage.getItem('debug') === 'true'; diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts new file mode 100644 index 0000000000..77366cf07b --- /dev/null +++ b/packages/frontend/src/const.ts @@ -0,0 +1,45 @@ +// ブラウザで直接表示することを許可するファイルの種類のリスト +// ここに含まれないものは application/octet-stream としてレスポンスされる +// SVGはXSSを生むので許可しない +export const FILE_TYPE_BROWSERSAFE = [ + // Images + 'image/png', + 'image/gif', + 'image/jpeg', + 'image/webp', + 'image/avif', + '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/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts new file mode 100644 index 0000000000..619c9f0b6d --- /dev/null +++ b/packages/frontend/src/directives/adaptive-border.ts @@ -0,0 +1,24 @@ +import { Directive } from 'vue'; + +export default { + mounted(src, binding, vn) { + const getBgColor = (el: HTMLElement) => { + const style = window.getComputedStyle(el); + if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { + return style.backgroundColor; + } else { + return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; + } + }; + + const parentBg = getBgColor(src.parentElement); + + const myBg = window.getComputedStyle(src).backgroundColor; + + if (parentBg === myBg) { + src.style.borderColor = 'var(--divider)'; + } else { + src.style.borderColor = myBg; + } + }, +} as Directive; diff --git a/packages/frontend/src/directives/anim.ts b/packages/frontend/src/directives/anim.ts new file mode 100644 index 0000000000..04e1c6a404 --- /dev/null +++ b/packages/frontend/src/directives/anim.ts @@ -0,0 +1,18 @@ +import { Directive } from 'vue'; + +export default { + beforeMount(src, binding, vn) { + src.style.opacity = '0'; + src.style.transform = 'scale(0.9)'; + // ページネーションと相性が悪いので + //if (typeof binding.value === 'number') src.style.transitionDelay = `${binding.value * 30}ms`; + src.classList.add('_zoom'); + }, + + mounted(src, binding, vn) { + window.setTimeout(() => { + src.style.opacity = '1'; + src.style.transform = 'none'; + }, 1); + }, +} as Directive; diff --git a/packages/frontend/src/directives/appear.ts b/packages/frontend/src/directives/appear.ts new file mode 100644 index 0000000000..7fa43fc34a --- /dev/null +++ b/packages/frontend/src/directives/appear.ts @@ -0,0 +1,22 @@ +import { Directive } from 'vue'; + +export default { + mounted(src, binding, vn) { + const fn = binding.value; + if (fn == null) return; + + const observer = new IntersectionObserver(entries => { + if (entries.some(entry => entry.isIntersecting)) { + fn(); + } + }); + + observer.observe(src); + + src._observer_ = observer; + }, + + unmounted(src, binding, vn) { + if (src._observer_) src._observer_.disconnect(); + }, +} as Directive; diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts new file mode 100644 index 0000000000..e2f514b7ca --- /dev/null +++ b/packages/frontend/src/directives/click-anime.ts @@ -0,0 +1,31 @@ +import { Directive } from 'vue'; +import { defaultStore } from '@/store'; + +export default { + mounted(el, binding, vn) { + /* + if (!defaultStore.state.animation) return; + + el.classList.add('_anime_bounce_standBy'); + + el.addEventListener('mousedown', () => { + el.classList.add('_anime_bounce_standBy'); + el.classList.add('_anime_bounce_ready'); + + el.addEventListener('mouseleave', () => { + el.classList.remove('_anime_bounce_ready'); + }); + }); + + el.addEventListener('click', () => { + el.classList.add('_anime_bounce'); + }); + + el.addEventListener('animationend', () => { + el.classList.remove('_anime_bounce_ready'); + el.classList.remove('_anime_bounce'); + el.classList.add('_anime_bounce_standBy'); + }); + */ + }, +} as Directive; diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts new file mode 100644 index 0000000000..62e0ac3b94 --- /dev/null +++ b/packages/frontend/src/directives/follow-append.ts @@ -0,0 +1,35 @@ +import { Directive } from 'vue'; +import { getScrollContainer, getScrollPosition } from '@/scripts/scroll'; + +export default { + mounted(src, binding, vn) { + if (binding.value === false) return; + + let isBottom = true; + + const container = getScrollContainer(src)!; + container.addEventListener('scroll', () => { + const pos = getScrollPosition(container); + const viewHeight = container.clientHeight; + const height = container.scrollHeight; + isBottom = (pos + viewHeight > height - 32); + }, { passive: true }); + container.scrollTop = container.scrollHeight; + + const ro = new ResizeObserver((entries, observer) => { + if (isBottom) { + const height = container.scrollHeight; + container.scrollTop = height; + } + }); + + ro.observe(src); + + // TODO: 新たにプロパティを作るのをやめMapを使う + src._ro_ = ro; + }, + + unmounted(src, binding, vn) { + if (src._ro_) src._ro_.unobserve(src); + }, +} as Directive; diff --git a/packages/frontend/src/directives/get-size.ts b/packages/frontend/src/directives/get-size.ts new file mode 100644 index 0000000000..ff3bdd78ac --- /dev/null +++ b/packages/frontend/src/directives/get-size.ts @@ -0,0 +1,54 @@ +import { Directive } from 'vue'; + +const mountings = new Map void; +}>(); + +function calc(src: Element) { + const info = mountings.get(src); + const height = src.clientHeight; + const width = src.clientWidth; + + if (!info) return; + + // アクティベート前などでsrcが描画されていない場合 + if (!height) { + // IntersectionObserverで表示検出する + if (!info.intersection) { + info.intersection = new IntersectionObserver(entries => { + if (entries.some(entry => entry.isIntersecting)) calc(src); + }); + } + info.intersection.observe(src); + return; + } + if (info.intersection) { + info.intersection.disconnect(); + delete info.intersection; + } + + info.fn(width, height); +} + +export default { + mounted(src, binding, vn) { + const resize = new ResizeObserver((entries, observer) => { + calc(src); + }); + resize.observe(src); + + mountings.set(src, { resize, fn: binding.value }); + calc(src); + }, + + unmounted(src, binding, vn) { + binding.value(0, 0); + const info = mountings.get(src); + if (!info) return; + info.resize.disconnect(); + if (info.intersection) info.intersection.disconnect(); + mountings.delete(src); + }, +} as Directive void>; diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts new file mode 100644 index 0000000000..dfc5f646a4 --- /dev/null +++ b/packages/frontend/src/directives/hotkey.ts @@ -0,0 +1,24 @@ +import { Directive } from 'vue'; +import { makeHotkey } from '../scripts/hotkey'; + +export default { + mounted(el, binding) { + el._hotkey_global = binding.modifiers.global === true; + + el._keyHandler = makeHotkey(binding.value); + + if (el._hotkey_global) { + document.addEventListener('keydown', el._keyHandler); + } else { + el.addEventListener('keydown', el._keyHandler); + } + }, + + unmounted(el) { + if (el._hotkey_global) { + document.removeEventListener('keydown', el._keyHandler); + } else { + el.removeEventListener('keydown', el._keyHandler); + } + }, +} as Directive; diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts new file mode 100644 index 0000000000..401a917cba --- /dev/null +++ b/packages/frontend/src/directives/index.ts @@ -0,0 +1,28 @@ +import { App } from 'vue'; + +import userPreview from './user-preview'; +import size from './size'; +import getSize from './get-size'; +import ripple from './ripple'; +import tooltip from './tooltip'; +import hotkey from './hotkey'; +import appear from './appear'; +import anim from './anim'; +import clickAnime from './click-anime'; +import panel from './panel'; +import adaptiveBorder from './adaptive-border'; + +export default function(app: App) { + app.directive('userPreview', userPreview); + app.directive('user-preview', userPreview); + app.directive('size', size); + app.directive('get-size', getSize); + app.directive('ripple', ripple); + app.directive('tooltip', tooltip); + app.directive('hotkey', hotkey); + app.directive('appear', appear); + app.directive('anim', anim); + app.directive('click-anime', clickAnime); + app.directive('panel', panel); + app.directive('adaptive-border', adaptiveBorder); +} diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts new file mode 100644 index 0000000000..d31dc41ed4 --- /dev/null +++ b/packages/frontend/src/directives/panel.ts @@ -0,0 +1,24 @@ +import { Directive } from 'vue'; + +export default { + mounted(src, binding, vn) { + const getBgColor = (el: HTMLElement) => { + const style = window.getComputedStyle(el); + if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { + return style.backgroundColor; + } else { + return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; + } + }; + + const parentBg = getBgColor(src.parentElement); + + const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel'); + + if (parentBg === myBg) { + src.style.backgroundColor = 'var(--bg)'; + } else { + src.style.backgroundColor = 'var(--panel)'; + } + }, +} as Directive; diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts new file mode 100644 index 0000000000..d32f7ab441 --- /dev/null +++ b/packages/frontend/src/directives/ripple.ts @@ -0,0 +1,18 @@ +import Ripple from '@/components/MkRipple.vue'; +import { popup } from '@/os'; + +export default { + mounted(el, binding, vn) { + // 明示的に false であればバインドしない + if (binding.value === false) return; + + el.addEventListener('click', () => { + const rect = el.getBoundingClientRect(); + + const x = rect.left + (el.offsetWidth / 2); + const y = rect.top + (el.offsetHeight / 2); + + popup(Ripple, { x, y }, {}, 'end'); + }); + }, +}; diff --git a/packages/frontend/src/directives/size.ts b/packages/frontend/src/directives/size.ts new file mode 100644 index 0000000000..da8bd78ea1 --- /dev/null +++ b/packages/frontend/src/directives/size.ts @@ -0,0 +1,123 @@ +import { Directive } from 'vue'; + +type Value = { max?: number[]; min?: number[]; }; + +//const observers = new Map(); +const mountings = new Map(); + +type ClassOrder = { + add: string[]; + remove: string[]; +}; + +const isContainerQueriesSupported = ('container' in document.documentElement.style); + +const cache = new Map(); + +function getClassOrder(width: number, queue: Value): ClassOrder { + const getMaxClass = (v: number) => `max-width_${v}px`; + const getMinClass = (v: number) => `min-width_${v}px`; + + return { + add: [ + ...(queue.max ? queue.max.filter(v => width <= v).map(getMaxClass) : []), + ...(queue.min ? queue.min.filter(v => width >= v).map(getMinClass) : []), + ], + remove: [ + ...(queue.max ? queue.max.filter(v => width > v).map(getMaxClass) : []), + ...(queue.min ? queue.min.filter(v => width < v).map(getMinClass) : []), + ], + }; +} + +function applyClassOrder(el: Element, order: ClassOrder) { + el.classList.add(...order.add); + el.classList.remove(...order.remove); +} + +function getOrderName(width: number, queue: Value): string { + return `${width}|${queue.max ? queue.max.join(',') : ''}|${queue.min ? queue.min.join(',') : ''}`; +} + +function calc(el: Element) { + const info = mountings.get(el); + const width = el.clientWidth; + + if (!info || info.previousWidth === width) return; + + // アクティベート前などでsrcが描画されていない場合 + if (!width) { + // IntersectionObserverで表示検出する + if (!info.intersection) { + info.intersection = new IntersectionObserver(entries => { + if (entries.some(entry => entry.isIntersecting)) calc(el); + }); + } + info.intersection.observe(el); + return; + } + if (info.intersection) { + info.intersection.disconnect(); + delete info.intersection; + } + + mountings.set(el, { ...info, ...{ previousWidth: width, twoPreviousWidth: info.previousWidth }}); + + // Prevent infinite resizing + // https://github.com/misskey-dev/misskey/issues/9076 + if (info.twoPreviousWidth === width) { + return; + } + + const cached = cache.get(getOrderName(width, info.value)); + if (cached) { + applyClassOrder(el, cached); + } else { + const order = getClassOrder(width, info.value); + cache.set(getOrderName(width, info.value), order); + applyClassOrder(el, order); + } +} + +export default { + mounted(src, binding, vn) { + if (isContainerQueriesSupported) return; + + const resize = new ResizeObserver((entries, observer) => { + calc(src); + }); + + mountings.set(src, { + value: binding.value, + resize, + previousWidth: 0, + twoPreviousWidth: 0, + }); + + calc(src); + resize.observe(src); + }, + + updated(src, binding, vn) { + if (isContainerQueriesSupported) return; + + mountings.set(src, Object.assign({}, mountings.get(src), { value: binding.value })); + calc(src); + }, + + unmounted(src, binding, vn) { + if (isContainerQueriesSupported) return; + + const info = mountings.get(src); + if (!info) return; + info.resize.disconnect(); + if (info.intersection) info.intersection.disconnect(); + mountings.delete(src); + }, +} as Directive; diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts new file mode 100644 index 0000000000..5d13497b5f --- /dev/null +++ b/packages/frontend/src/directives/tooltip.ts @@ -0,0 +1,93 @@ +// TODO: useTooltip関数使うようにしたい +// ただディレクティブ内でonUnmountedなどのcomposition api使えるのか不明 + +import { defineAsyncComponent, Directive, ref } from 'vue'; +import { isTouchUsing } from '@/scripts/touch'; +import { popup, alert } from '@/os'; + +const start = isTouchUsing ? 'touchstart' : 'mouseover'; +const end = isTouchUsing ? 'touchend' : 'mouseleave'; + +export default { + mounted(el: HTMLElement, binding, vn) { + const delay = binding.modifiers.noDelay ? 0 : 100; + + const self = (el as any)._tooltipDirective_ = {} as any; + + self.text = binding.value as string; + self._close = null; + self.showTimer = null; + self.hideTimer = null; + self.checkTimer = null; + + self.close = () => { + if (self._close) { + window.clearInterval(self.checkTimer); + self._close(); + self._close = null; + } + }; + + if (binding.arg === 'dialog') { + el.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + alert({ + type: 'info', + text: binding.value, + }); + return false; + }); + } + + self.show = () => { + if (!document.body.contains(el)) return; + if (self._close) return; + if (self.text == null) return; + + const showing = ref(true); + popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { + showing, + text: self.text, + asMfm: binding.modifiers.mfm, + direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top', + targetElement: el, + }, {}, 'closed'); + + self._close = () => { + showing.value = false; + }; + }; + + el.addEventListener('selectstart', ev => { + ev.preventDefault(); + }); + + el.addEventListener(start, () => { + window.clearTimeout(self.showTimer); + window.clearTimeout(self.hideTimer); + self.showTimer = window.setTimeout(self.show, delay); + }, { passive: true }); + + el.addEventListener(end, () => { + window.clearTimeout(self.showTimer); + window.clearTimeout(self.hideTimer); + self.hideTimer = window.setTimeout(self.close, delay); + }, { passive: true }); + + el.addEventListener('click', () => { + window.clearTimeout(self.showTimer); + self.close(); + }); + }, + + updated(el, binding) { + const self = el._tooltipDirective_; + self.text = binding.value as string; + }, + + unmounted(el, binding, vn) { + const self = el._tooltipDirective_; + window.clearInterval(self.checkTimer); + }, +} as Directive; diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts new file mode 100644 index 0000000000..ed5f00ca65 --- /dev/null +++ b/packages/frontend/src/directives/user-preview.ts @@ -0,0 +1,118 @@ +import { defineAsyncComponent, Directive, ref } from 'vue'; +import autobind from 'autobind-decorator'; +import { popup } from '@/os'; + +export class UserPreview { + private el; + private user; + private showTimer; + private hideTimer; + private checkTimer; + private promise; + + constructor(el, user) { + this.el = el; + this.user = user; + + this.attach(); + } + + @autobind + private show() { + if (!document.body.contains(this.el)) return; + if (this.promise) return; + + const showing = ref(true); + + popup(defineAsyncComponent(() => import('@/components/MkUserPreview.vue')), { + showing, + q: this.user, + source: this.el, + }, { + mouseover: () => { + window.clearTimeout(this.hideTimer); + }, + mouseleave: () => { + window.clearTimeout(this.showTimer); + this.hideTimer = window.setTimeout(this.close, 500); + }, + }, 'closed'); + + this.promise = { + cancel: () => { + showing.value = false; + }, + }; + + this.checkTimer = window.setInterval(() => { + if (!document.body.contains(this.el)) { + window.clearTimeout(this.showTimer); + window.clearTimeout(this.hideTimer); + this.close(); + } + }, 1000); + } + + @autobind + private close() { + if (this.promise) { + window.clearInterval(this.checkTimer); + this.promise.cancel(); + this.promise = null; + } + } + + @autobind + private onMouseover() { + window.clearTimeout(this.showTimer); + window.clearTimeout(this.hideTimer); + this.showTimer = window.setTimeout(this.show, 500); + } + + @autobind + private onMouseleave() { + window.clearTimeout(this.showTimer); + window.clearTimeout(this.hideTimer); + this.hideTimer = window.setTimeout(this.close, 500); + } + + @autobind + private onClick() { + window.clearTimeout(this.showTimer); + this.close(); + } + + @autobind + public attach() { + this.el.addEventListener('mouseover', this.onMouseover); + this.el.addEventListener('mouseleave', this.onMouseleave); + this.el.addEventListener('click', this.onClick); + } + + @autobind + public detach() { + this.el.removeEventListener('mouseover', this.onMouseover); + this.el.removeEventListener('mouseleave', this.onMouseleave); + this.el.removeEventListener('click', this.onClick); + window.clearInterval(this.checkTimer); + } +} + +export default { + mounted(el: HTMLElement, binding, vn) { + if (binding.value == null) return; + + // TODO: 新たにプロパティを作るのをやめMapを使う + // ただメモリ的には↓の方が省メモリかもしれないので検討中 + const self = (el as any)._userPreviewDirective_ = {} as any; + + self.preview = new UserPreview(el, binding.value); + }, + + unmounted(el, binding, vn) { + if (binding.value == null) return; + + const self = el._userPreviewDirective_; + self.preview.detach(); + }, +} as Directive; diff --git a/packages/frontend/src/emojilist.json b/packages/frontend/src/emojilist.json new file mode 100644 index 0000000000..402e82e33b --- /dev/null +++ b/packages/frontend/src/emojilist.json @@ -0,0 +1,1785 @@ +[ + { "category": "face", "char": "😀", "name": "grinning", "keywords": ["face", "smile", "happy", "joy", ": D", "grin"] }, + { "category": "face", "char": "😬", "name": "grimacing", "keywords": ["face", "grimace", "teeth"] }, + { "category": "face", "char": "😁", "name": "grin", "keywords": ["face", "happy", "smile", "joy", "kawaii"] }, + { "category": "face", "char": "😂", "name": "joy", "keywords": ["face", "cry", "tears", "weep", "happy", "happytears", "haha"] }, + { "category": "face", "char": "🤣", "name": "rofl", "keywords": ["face", "rolling", "floor", "laughing", "lol", "haha"] }, + { "category": "face", "char": "🥳", "name": "partying", "keywords": ["face", "celebration", "woohoo"] }, + { "category": "face", "char": "😃", "name": "smiley", "keywords": ["face", "happy", "joy", "haha", ": D", ": )", "smile", "funny"] }, + { "category": "face", "char": "😄", "name": "smile", "keywords": ["face", "happy", "joy", "funny", "haha", "laugh", "like", ": D", ": )"] }, + { "category": "face", "char": "😅", "name": "sweat_smile", "keywords": ["face", "hot", "happy", "laugh", "sweat", "smile", "relief"] }, + { "category": "face", "char": "🥲", "name": "smiling_face_with_tear", "keywords": ["face"] }, + { "category": "face", "char": "😆", "name": "laughing", "keywords": ["happy", "joy", "lol", "satisfied", "haha", "face", "glad", "XD", "laugh"] }, + { "category": "face", "char": "😇", "name": "innocent", "keywords": ["face", "angel", "heaven", "halo"] }, + { "category": "face", "char": "😉", "name": "wink", "keywords": ["face", "happy", "mischievous", "secret", ";)", "smile", "eye"] }, + { "category": "face", "char": "😊", "name": "blush", "keywords": ["face", "smile", "happy", "flushed", "crush", "embarrassed", "shy", "joy"] }, + { "category": "face", "char": "🙂", "name": "slightly_smiling_face", "keywords": ["face", "smile"] }, + { "category": "face", "char": "🙃", "name": "upside_down_face", "keywords": ["face", "flipped", "silly", "smile"] }, + { "category": "face", "char": "☺️", "name": "relaxed", "keywords": ["face", "blush", "massage", "happiness"] }, + { "category": "face", "char": "😋", "name": "yum", "keywords": ["happy", "joy", "tongue", "smile", "face", "silly", "yummy", "nom", "delicious", "savouring"] }, + { "category": "face", "char": "😌", "name": "relieved", "keywords": ["face", "relaxed", "phew", "massage", "happiness"] }, + { "category": "face", "char": "😍", "name": "heart_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "heart"] }, + { "category": "face", "char": "🥰", "name": "smiling_face_with_three_hearts", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "crush", "hearts", "adore"] }, + { "category": "face", "char": "😘", "name": "kissing_heart", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] }, + { "category": "face", "char": "😗", "name": "kissing", "keywords": ["love", "like", "face", "3", "valentines", "infatuation", "kiss"] }, + { "category": "face", "char": "😙", "name": "kissing_smiling_eyes", "keywords": ["face", "affection", "valentines", "infatuation", "kiss"] }, + { "category": "face", "char": "😚", "name": "kissing_closed_eyes", "keywords": ["face", "love", "like", "affection", "valentines", "infatuation", "kiss"] }, + { "category": "face", "char": "😜", "name": "stuck_out_tongue_winking_eye", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "wink", "tongue"] }, + { "category": "face", "char": "🤪", "name": "zany", "keywords": ["face", "goofy", "crazy"] }, + { "category": "face", "char": "🤨", "name": "raised_eyebrow", "keywords": ["face", "distrust", "scepticism", "disapproval", "disbelief", "surprise"] }, + { "category": "face", "char": "🧐", "name": "monocle", "keywords": ["face", "stuffy", "wealthy"] }, + { "category": "face", "char": "😝", "name": "stuck_out_tongue_closed_eyes", "keywords": ["face", "prank", "playful", "mischievous", "smile", "tongue"] }, + { "category": "face", "char": "😛", "name": "stuck_out_tongue", "keywords": ["face", "prank", "childish", "playful", "mischievous", "smile", "tongue"] }, + { "category": "face", "char": "🤑", "name": "money_mouth_face", "keywords": ["face", "rich", "dollar", "money"] }, + { "category": "face", "char": "🤓", "name": "nerd_face", "keywords": ["face", "nerdy", "geek", "dork"] }, + { "category": "face", "char": "🥸", "name": "disguised_face", "keywords": ["face", "nose", "glasses", "incognito"] }, + { "category": "face", "char": "😎", "name": "sunglasses", "keywords": ["face", "cool", "smile", "summer", "beach", "sunglass"] }, + { "category": "face", "char": "🤩", "name": "star_struck", "keywords": ["face", "smile", "starry", "eyes", "grinning"] }, + { "category": "face", "char": "🤡", "name": "clown_face", "keywords": ["face"] }, + { "category": "face", "char": "🤠", "name": "cowboy_hat_face", "keywords": ["face", "cowgirl", "hat"] }, + { "category": "face", "char": "🤗", "name": "hugs", "keywords": ["face", "smile", "hug"] }, + { "category": "face", "char": "😏", "name": "smirk", "keywords": ["face", "smile", "mean", "prank", "smug", "sarcasm"] }, + { "category": "face", "char": "😶", "name": "no_mouth", "keywords": ["face", "hellokitty"] }, + { "category": "face", "char": "😐", "name": "neutral_face", "keywords": ["indifference", "meh", ": |", "neutral"] }, + { "category": "face", "char": "😑", "name": "expressionless", "keywords": ["face", "indifferent", "-_-", "meh", "deadpan"] }, + { "category": "face", "char": "😒", "name": "unamused", "keywords": ["indifference", "bored", "straight face", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side_eye"] }, + { "category": "face", "char": "🙄", "name": "roll_eyes", "keywords": ["face", "eyeroll", "frustrated"] }, + { "category": "face", "char": "🤔", "name": "thinking", "keywords": ["face", "hmmm", "think", "consider"] }, + { "category": "face", "char": "🤥", "name": "lying_face", "keywords": ["face", "lie", "pinocchio"] }, + { "category": "face", "char": "🤭", "name": "hand_over_mouth", "keywords": ["face", "whoops", "shock", "surprise"] }, + { "category": "face", "char": "🤫", "name": "shushing", "keywords": ["face", "quiet", "shhh"] }, + { "category": "face", "char": "🤬", "name": "symbols_over_mouth", "keywords": ["face", "swearing", "cursing", "cussing", "profanity", "expletive"] }, + { "category": "face", "char": "🤯", "name": "exploding_head", "keywords": ["face", "shocked", "mind", "blown"] }, + { "category": "face", "char": "😳", "name": "flushed", "keywords": ["face", "blush", "shy", "flattered"] }, + { "category": "face", "char": "😞", "name": "disappointed", "keywords": ["face", "sad", "upset", "depressed", ": ("] }, + { "category": "face", "char": "😟", "name": "worried", "keywords": ["face", "concern", "nervous", ": ("] }, + { "category": "face", "char": "😠", "name": "angry", "keywords": ["mad", "face", "annoyed", "frustrated"] }, + { "category": "face", "char": "😡", "name": "rage", "keywords": ["angry", "mad", "hate", "despise"] }, + { "category": "face", "char": "😔", "name": "pensive", "keywords": ["face", "sad", "depressed", "upset"] }, + { "category": "face", "char": "😕", "name": "confused", "keywords": ["face", "indifference", "huh", "weird", "hmmm", ": /"] }, + { "category": "face", "char": "🙁", "name": "slightly_frowning_face", "keywords": ["face", "frowning", "disappointed", "sad", "upset"] }, + { "category": "face", "char": "☹", "name": "frowning_face", "keywords": ["face", "sad", "upset", "frown"] }, + { "category": "face", "char": "😣", "name": "persevere", "keywords": ["face", "sick", "no", "upset", "oops"] }, + { "category": "face", "char": "😖", "name": "confounded", "keywords": ["face", "confused", "sick", "unwell", "oops", ": S"] }, + { "category": "face", "char": "😫", "name": "tired_face", "keywords": ["sick", "whine", "upset", "frustrated"] }, + { "category": "face", "char": "😩", "name": "weary", "keywords": ["face", "tired", "sleepy", "sad", "frustrated", "upset"] }, + { "category": "face", "char": "🥺", "name": "pleading", "keywords": ["face", "begging", "mercy"] }, + { "category": "face", "char": "😤", "name": "triumph", "keywords": ["face", "gas", "phew", "proud", "pride"] }, + { "category": "face", "char": "😮", "name": "open_mouth", "keywords": ["face", "surprise", "impressed", "wow", "whoa", ": O"] }, + { "category": "face", "char": "😱", "name": "scream", "keywords": ["face", "munch", "scared", "omg"] }, + { "category": "face", "char": "😨", "name": "fearful", "keywords": ["face", "scared", "terrified", "nervous", "oops", "huh"] }, + { "category": "face", "char": "😰", "name": "cold_sweat", "keywords": ["face", "nervous", "sweat"] }, + { "category": "face", "char": "😯", "name": "hushed", "keywords": ["face", "woo", "shh"] }, + { "category": "face", "char": "😦", "name": "frowning", "keywords": ["face", "aw", "what"] }, + { "category": "face", "char": "😧", "name": "anguished", "keywords": ["face", "stunned", "nervous"] }, + { "category": "face", "char": "😢", "name": "cry", "keywords": ["face", "tears", "sad", "depressed", "upset", ": '("] }, + { "category": "face", "char": "😥", "name": "disappointed_relieved", "keywords": ["face", "phew", "sweat", "nervous"] }, + { "category": "face", "char": "🤤", "name": "drooling_face", "keywords": ["face"] }, + { "category": "face", "char": "😪", "name": "sleepy", "keywords": ["face", "tired", "rest", "nap"] }, + { "category": "face", "char": "😓", "name": "sweat", "keywords": ["face", "hot", "sad", "tired", "exercise"] }, + { "category": "face", "char": "🥵", "name": "hot", "keywords": ["face", "feverish", "heat", "red", "sweating"] }, + { "category": "face", "char": "🥶", "name": "cold", "keywords": ["face", "blue", "freezing", "frozen", "frostbite", "icicles"] }, + { "category": "face", "char": "😭", "name": "sob", "keywords": ["face", "cry", "tears", "sad", "upset", "depressed"] }, + { "category": "face", "char": "😵", "name": "dizzy_face", "keywords": ["spent", "unconscious", "xox", "dizzy"] }, + { "category": "face", "char": "😲", "name": "astonished", "keywords": ["face", "xox", "surprised", "poisoned"] }, + { "category": "face", "char": "🤐", "name": "zipper_mouth_face", "keywords": ["face", "sealed", "zipper", "secret"] }, + { "category": "face", "char": "🤢", "name": "nauseated_face", "keywords": ["face", "vomit", "gross", "green", "sick", "throw up", "ill"] }, + { "category": "face", "char": "🤧", "name": "sneezing_face", "keywords": ["face", "gesundheit", "sneeze", "sick", "allergy"] }, + { "category": "face", "char": "🤮", "name": "vomiting", "keywords": ["face", "sick"] }, + { "category": "face", "char": "😷", "name": "mask", "keywords": ["face", "sick", "ill", "disease"] }, + { "category": "face", "char": "🤒", "name": "face_with_thermometer", "keywords": ["sick", "temperature", "thermometer", "cold", "fever"] }, + { "category": "face", "char": "🤕", "name": "face_with_head_bandage", "keywords": ["injured", "clumsy", "bandage", "hurt"] }, + { "category": "face", "char": "🥴", "name": "woozy", "keywords": ["face", "dizzy", "intoxicated", "tipsy", "wavy"] }, + { "category": "face", "char": "🥱", "name": "yawning", "keywords": ["face", "tired", "yawning"] }, + { "category": "face", "char": "😴", "name": "sleeping", "keywords": ["face", "tired", "sleepy", "night", "zzz"] }, + { "category": "face", "char": "💤", "name": "zzz", "keywords": ["sleepy", "tired", "dream"] }, + { "category": "face", "char": "\uD83D\uDE36\u200D\uD83C\uDF2B\uFE0F", "name": "face_in_clouds", "keywords": [] }, + { "category": "face", "char": "\uD83D\uDE2E\u200D\uD83D\uDCA8", "name": "face_exhaling", "keywords": [] }, + { "category": "face", "char": "\uD83D\uDE35\u200D\uD83D\uDCAB", "name": "face_with_spiral_eyes", "keywords": [] }, + { "category": "face", "char": "\uD83E\uDEE0", "name": "melting_face", "keywords": ["disappear", "dissolve", "liquid", "melt", "toketa"] }, + { "category": "face", "char": "\uD83E\uDEE2", "name": "face_with_open_eyes_and_hand_over_mouth", "keywords": ["amazement", "awe", "disbelief", "embarrass", "scared", "surprise", "ohoho"] }, + { "category": "face", "char": "\uD83E\uDEE3", "name": "face_with_peeking_eye", "keywords": ["captivated", "peep", "stare", "chunibyo"] }, + { "category": "face", "char": "\uD83E\uDEE1", "name": "saluting_face", "keywords": ["ok", "salute", "sunny", "troops", "yes", "raja"] }, + { "category": "face", "char": "\uD83E\uDEE5", "name": "dotted_line_face", "keywords": ["depressed", "disappear", "hide", "introvert", "invisible", "tensen"] }, + { "category": "face", "char": "\uD83E\uDEE4", "name": "face_with_diagonal_mouth", "keywords": ["disappointed", "meh", "skeptical", "unsure"] }, + { "category": "face", "char": "\uD83E\uDD79", "name": "face_holding_back_tears", "keywords": ["angry", "cry", "proud", "resist", "sad"] }, + { "category": "face", "char": "💩", "name": "poop", "keywords": ["hankey", "shitface", "fail", "turd", "shit"] }, + { "category": "face", "char": "😈", "name": "smiling_imp", "keywords": ["devil", "horns"] }, + { "category": "face", "char": "👿", "name": "imp", "keywords": ["devil", "angry", "horns"] }, + { "category": "face", "char": "👹", "name": "japanese_ogre", "keywords": ["monster", "red", "mask", "halloween", "scary", "creepy", "devil", "demon", "japanese", "ogre"] }, + { "category": "face", "char": "👺", "name": "japanese_goblin", "keywords": ["red", "evil", "mask", "monster", "scary", "creepy", "japanese", "goblin"] }, + { "category": "face", "char": "💀", "name": "skull", "keywords": ["dead", "skeleton", "creepy", "death"] }, + { "category": "face", "char": "👻", "name": "ghost", "keywords": ["halloween", "spooky", "scary"] }, + { "category": "face", "char": "👽", "name": "alien", "keywords": ["UFO", "paul", "weird", "outer_space"] }, + { "category": "face", "char": "🤖", "name": "robot", "keywords": ["computer", "machine", "bot"] }, + { "category": "face", "char": "😺", "name": "smiley_cat", "keywords": ["animal", "cats", "happy", "smile"] }, + { "category": "face", "char": "😸", "name": "smile_cat", "keywords": ["animal", "cats", "smile"] }, + { "category": "face", "char": "😹", "name": "joy_cat", "keywords": ["animal", "cats", "haha", "happy", "tears"] }, + { "category": "face", "char": "😻", "name": "heart_eyes_cat", "keywords": ["animal", "love", "like", "affection", "cats", "valentines", "heart"] }, + { "category": "face", "char": "😼", "name": "smirk_cat", "keywords": ["animal", "cats", "smirk"] }, + { "category": "face", "char": "😽", "name": "kissing_cat", "keywords": ["animal", "cats", "kiss"] }, + { "category": "face", "char": "🙀", "name": "scream_cat", "keywords": ["animal", "cats", "munch", "scared", "scream"] }, + { "category": "face", "char": "😿", "name": "crying_cat_face", "keywords": ["animal", "tears", "weep", "sad", "cats", "upset", "cry"] }, + { "category": "face", "char": "😾", "name": "pouting_cat", "keywords": ["animal", "cats"] }, + { "category": "people", "char": "🤲", "name": "palms_up", "keywords": ["hands", "gesture", "cupped", "prayer"] }, + { "category": "people", "char": "🙌", "name": "raised_hands", "keywords": ["gesture", "hooray", "yea", "celebration", "hands"] }, + { "category": "people", "char": "👏", "name": "clap", "keywords": ["hands", "praise", "applause", "congrats", "yay"] }, + { "category": "people", "char": "👋", "name": "wave", "keywords": ["hands", "gesture", "goodbye", "solong", "farewell", "hello", "hi", "palm"] }, + { "category": "people", "char": "🤙", "name": "call_me_hand", "keywords": ["hands", "gesture"] }, + { "category": "people", "char": "👍", "name": "+1", "keywords": ["thumbsup", "yes", "awesome", "good", "agree", "accept", "cool", "hand", "like"] }, + { "category": "people", "char": "👎", "name": "-1", "keywords": ["thumbsdown", "no", "dislike", "hand"] }, + { "category": "people", "char": "👊", "name": "facepunch", "keywords": ["angry", "violence", "fist", "hit", "attack", "hand"] }, + { "category": "people", "char": "✊", "name": "fist", "keywords": ["fingers", "hand", "grasp"] }, + { "category": "people", "char": "🤛", "name": "fist_left", "keywords": ["hand", "fistbump"] }, + { "category": "people", "char": "🤜", "name": "fist_right", "keywords": ["hand", "fistbump"] }, + { "category": "people", "char": "✌", "name": "v", "keywords": ["fingers", "ohyeah", "hand", "peace", "victory", "two"] }, + { "category": "people", "char": "👌", "name": "ok_hand", "keywords": ["fingers", "limbs", "perfect", "ok", "okay"] }, + { "category": "people", "char": "✋", "name": "raised_hand", "keywords": ["fingers", "stop", "highfive", "palm", "ban"] }, + { "category": "people", "char": "🤚", "name": "raised_back_of_hand", "keywords": ["fingers", "raised", "backhand"] }, + { "category": "people", "char": "👐", "name": "open_hands", "keywords": ["fingers", "butterfly", "hands", "open"] }, + { "category": "people", "char": "💪", "name": "muscle", "keywords": ["arm", "flex", "hand", "summer", "strong", "biceps"] }, + { "category": "people", "char": "🦾", "name": "mechanical_arm", "keywords": ["flex", "hand", "strong", "biceps"] }, + { "category": "people", "char": "🙏", "name": "pray", "keywords": ["please", "hope", "wish", "namaste", "highfive"] }, + { "category": "people", "char": "🦶", "name": "foot", "keywords": ["kick", "stomp"] }, + { "category": "people", "char": "🦵", "name": "leg", "keywords": ["kick", "limb"] }, + { "category": "people", "char": "🦿", "name": "mechanical_leg", "keywords": ["kick", "limb"] }, + { "category": "people", "char": "🤝", "name": "handshake", "keywords": ["agreement", "shake"] }, + { "category": "people", "char": "☝", "name": "point_up", "keywords": ["hand", "fingers", "direction", "up"] }, + { "category": "people", "char": "👆", "name": "point_up_2", "keywords": ["fingers", "hand", "direction", "up"] }, + { "category": "people", "char": "👇", "name": "point_down", "keywords": ["fingers", "hand", "direction", "down"] }, + { "category": "people", "char": "👈", "name": "point_left", "keywords": ["direction", "fingers", "hand", "left"] }, + { "category": "people", "char": "👉", "name": "point_right", "keywords": ["fingers", "hand", "direction", "right"] }, + { "category": "people", "char": "🖕", "name": "fu", "keywords": ["hand", "fingers", "rude", "middle", "flipping"] }, + { "category": "people", "char": "🖐", "name": "raised_hand_with_fingers_splayed", "keywords": ["hand", "fingers", "palm"] }, + { "category": "people", "char": "🤟", "name": "love_you", "keywords": ["hand", "fingers", "gesture"] }, + { "category": "people", "char": "🤘", "name": "metal", "keywords": ["hand", "fingers", "evil_eye", "sign_of_horns", "rock_on"] }, + { "category": "people", "char": "🤞", "name": "crossed_fingers", "keywords": ["good", "lucky"] }, + { "category": "people", "char": "🖖", "name": "vulcan_salute", "keywords": ["hand", "fingers", "spock", "star trek"] }, + { "category": "people", "char": "✍", "name": "writing_hand", "keywords": ["lower_left_ballpoint_pen", "stationery", "write", "compose"] }, + { "category": "people", "char": "\uD83E\uDEF0", "name": "hand_with_index_finger_and_thumb_crossed", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF1", "name": "rightwards_hand", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF2", "name": "leftwards_hand", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF3", "name": "palm_down_hand", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF4", "name": "palm_up_hand", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF5", "name": "index_pointing_at_the_viewer", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEF6", "name": "heart_hands", "keywords": ["moemoekyun"] }, + { "category": "people", "char": "🤏", "name": "pinching_hand", "keywords": ["hand", "fingers"] }, + { "category": "people", "char": "🤌", "name": "pinched_fingers", "keywords": ["hand", "fingers"] }, + { "category": "people", "char": "🤳", "name": "selfie", "keywords": ["camera", "phone"] }, + { "category": "people", "char": "💅", "name": "nail_care", "keywords": ["beauty", "manicure", "finger", "fashion", "nail"] }, + { "category": "people", "char": "👄", "name": "lips", "keywords": ["mouth", "kiss"] }, + { "category": "people", "char": "\uD83E\uDEE6", "name": "biting_lip", "keywords": [] }, + { "category": "people", "char": "🦷", "name": "tooth", "keywords": ["teeth", "dentist"] }, + { "category": "people", "char": "👅", "name": "tongue", "keywords": ["mouth", "playful"] }, + { "category": "people", "char": "👂", "name": "ear", "keywords": ["face", "hear", "sound", "listen"] }, + { "category": "people", "char": "🦻", "name": "ear_with_hearing_aid", "keywords": ["face", "hear", "sound", "listen"] }, + { "category": "people", "char": "👃", "name": "nose", "keywords": ["smell", "sniff"] }, + { "category": "people", "char": "👁", "name": "eye", "keywords": ["face", "look", "see", "watch", "stare"] }, + { "category": "people", "char": "👀", "name": "eyes", "keywords": ["look", "watch", "stalk", "peek", "see"] }, + { "category": "people", "char": "🧠", "name": "brain", "keywords": ["smart", "intelligent"] }, + { "category": "people", "char": "🫀", "name": "anatomical_heart", "keywords": [] }, + { "category": "people", "char": "🫁", "name": "lungs", "keywords": [] }, + { "category": "people", "char": "👤", "name": "bust_in_silhouette", "keywords": ["user", "person", "human"] }, + { "category": "people", "char": "👥", "name": "busts_in_silhouette", "keywords": ["user", "person", "human", "group", "team"] }, + { "category": "people", "char": "🗣", "name": "speaking_head", "keywords": ["user", "person", "human", "sing", "say", "talk"] }, + { "category": "people", "char": "👶", "name": "baby", "keywords": ["child", "boy", "girl", "toddler"] }, + { "category": "people", "char": "🧒", "name": "child", "keywords": ["gender-neutral", "young"] }, + { "category": "people", "char": "👦", "name": "boy", "keywords": ["man", "male", "guy", "teenager"] }, + { "category": "people", "char": "👧", "name": "girl", "keywords": ["female", "woman", "teenager"] }, + { "category": "people", "char": "🧑", "name": "adult", "keywords": ["gender-neutral", "person"] }, + { "category": "people", "char": "👨", "name": "man", "keywords": ["mustache", "father", "dad", "guy", "classy", "sir", "moustache"] }, + { "category": "people", "char": "👩", "name": "woman", "keywords": ["female", "girls", "lady"] }, + { "category": "people", "char": "🧑‍🦱", "name": "curly_hair", "keywords": ["curly", "afro", "braids", "ringlets"] }, + { "category": "people", "char": "👩‍🦱", "name": "curly_hair_woman", "keywords": ["woman", "female", "girl", "curly", "afro", "braids", "ringlets"] }, + { "category": "people", "char": "👨‍🦱", "name": "curly_hair_man", "keywords": ["man", "male", "boy", "guy", "curly", "afro", "braids", "ringlets"] }, + { "category": "people", "char": "🧑‍🦰", "name": "red_hair", "keywords": ["redhead"] }, + { "category": "people", "char": "👩‍🦰", "name": "red_hair_woman", "keywords": ["woman", "female", "girl", "ginger", "redhead"] }, + { "category": "people", "char": "👨‍🦰", "name": "red_hair_man", "keywords": ["man", "male", "boy", "guy", "ginger", "redhead"] }, + { "category": "people", "char": "👱‍♀️", "name": "blonde_woman", "keywords": ["woman", "female", "girl", "blonde", "person"] }, + { "category": "people", "char": "👱", "name": "blonde_man", "keywords": ["man", "male", "boy", "blonde", "guy", "person"] }, + { "category": "people", "char": "🧑‍🦳", "name": "white_hair", "keywords": ["gray", "old", "white"] }, + { "category": "people", "char": "👩‍🦳", "name": "white_hair_woman", "keywords": ["woman", "female", "girl", "gray", "old", "white"] }, + { "category": "people", "char": "👨‍🦳", "name": "white_hair_man", "keywords": ["man", "male", "boy", "guy", "gray", "old", "white"] }, + { "category": "people", "char": "🧑‍🦲", "name": "bald", "keywords": ["bald", "chemotherapy", "hairless", "shaven"] }, + { "category": "people", "char": "👩‍🦲", "name": "bald_woman", "keywords": ["woman", "female", "girl", "bald", "chemotherapy", "hairless", "shaven"] }, + { "category": "people", "char": "👨‍🦲", "name": "bald_man", "keywords": ["man", "male", "boy", "guy", "bald", "chemotherapy", "hairless", "shaven"] }, + { "category": "people", "char": "🧔", "name": "bearded_person", "keywords": ["person", "bewhiskered"] }, + { "category": "people", "char": "🧓", "name": "older_adult", "keywords": ["human", "elder", "senior", "gender-neutral"] }, + { "category": "people", "char": "👴", "name": "older_man", "keywords": ["human", "male", "men", "old", "elder", "senior"] }, + { "category": "people", "char": "👵", "name": "older_woman", "keywords": ["human", "female", "women", "lady", "old", "elder", "senior"] }, + { "category": "people", "char": "👲", "name": "man_with_gua_pi_mao", "keywords": ["male", "boy", "chinese"] }, + { "category": "people", "char": "🧕", "name": "woman_with_headscarf", "keywords": ["female", "hijab", "mantilla", "tichel"] }, + { "category": "people", "char": "👳‍♀️", "name": "woman_with_turban", "keywords": ["female", "indian", "hinduism", "arabs", "woman"] }, + { "category": "people", "char": "👳", "name": "man_with_turban", "keywords": ["male", "indian", "hinduism", "arabs"] }, + { "category": "people", "char": "👮‍♀️", "name": "policewoman", "keywords": ["woman", "police", "law", "legal", "enforcement", "arrest", "911", "female"] }, + { "category": "people", "char": "👮", "name": "policeman", "keywords": ["man", "police", "law", "legal", "enforcement", "arrest", "911"] }, + { "category": "people", "char": "👷‍♀️", "name": "construction_worker_woman", "keywords": ["female", "human", "wip", "build", "construction", "worker", "labor", "woman"] }, + { "category": "people", "char": "👷", "name": "construction_worker_man", "keywords": ["male", "human", "wip", "guy", "build", "construction", "worker", "labor"] }, + { "category": "people", "char": "💂‍♀️", "name": "guardswoman", "keywords": ["uk", "gb", "british", "female", "royal", "woman"] }, + { "category": "people", "char": "💂", "name": "guardsman", "keywords": ["uk", "gb", "british", "male", "guy", "royal"] }, + { "category": "people", "char": "🕵️‍♀️", "name": "female_detective", "keywords": ["human", "spy", "detective", "female", "woman"] }, + { "category": "people", "char": "🕵", "name": "male_detective", "keywords": ["human", "spy", "detective"] }, + { "category": "people", "char": "🧑‍⚕️", "name": "health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "human"] }, + { "category": "people", "char": "👩‍⚕️", "name": "woman_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "woman", "human"] }, + { "category": "people", "char": "👨‍⚕️", "name": "man_health_worker", "keywords": ["doctor", "nurse", "therapist", "healthcare", "man", "human"] }, + { "category": "people", "char": "🧑‍🌾", "name": "farmer", "keywords": ["rancher", "gardener", "human"] }, + { "category": "people", "char": "👩‍🌾", "name": "woman_farmer", "keywords": ["rancher", "gardener", "woman", "human"] }, + { "category": "people", "char": "👨‍🌾", "name": "man_farmer", "keywords": ["rancher", "gardener", "man", "human"] }, + { "category": "people", "char": "🧑‍🍳", "name": "cook", "keywords": ["chef", "human"] }, + { "category": "people", "char": "👩‍🍳", "name": "woman_cook", "keywords": ["chef", "woman", "human"] }, + { "category": "people", "char": "👨‍🍳", "name": "man_cook", "keywords": ["chef", "man", "human"] }, + { "category": "people", "char": "🧑‍🎓", "name": "student", "keywords": ["graduate", "human"] }, + { "category": "people", "char": "👩‍🎓", "name": "woman_student", "keywords": ["graduate", "woman", "human"] }, + { "category": "people", "char": "👨‍🎓", "name": "man_student", "keywords": ["graduate", "man", "human"] }, + { "category": "people", "char": "🧑‍🎤", "name": "singer", "keywords": ["rockstar", "entertainer", "human"] }, + { "category": "people", "char": "👩‍🎤", "name": "woman_singer", "keywords": ["rockstar", "entertainer", "woman", "human"] }, + { "category": "people", "char": "👨‍🎤", "name": "man_singer", "keywords": ["rockstar", "entertainer", "man", "human"] }, + { "category": "people", "char": "🧑‍🏫", "name": "teacher", "keywords": ["instructor", "professor", "human"] }, + { "category": "people", "char": "👩‍🏫", "name": "woman_teacher", "keywords": ["instructor", "professor", "woman", "human"] }, + { "category": "people", "char": "👨‍🏫", "name": "man_teacher", "keywords": ["instructor", "professor", "man", "human"] }, + { "category": "people", "char": "🧑‍🏭", "name": "factory_worker", "keywords": ["assembly", "industrial", "human"] }, + { "category": "people", "char": "👩‍🏭", "name": "woman_factory_worker", "keywords": ["assembly", "industrial", "woman", "human"] }, + { "category": "people", "char": "👨‍🏭", "name": "man_factory_worker", "keywords": ["assembly", "industrial", "man", "human"] }, + { "category": "people", "char": "🧑‍💻", "name": "technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "human", "laptop", "computer"] }, + { "category": "people", "char": "👩‍💻", "name": "woman_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "woman", "human", "laptop", "computer"] }, + { "category": "people", "char": "👨‍💻", "name": "man_technologist", "keywords": ["coder", "developer", "engineer", "programmer", "software", "man", "human", "laptop", "computer"] }, + { "category": "people", "char": "🧑‍💼", "name": "office_worker", "keywords": ["business", "manager", "human"] }, + { "category": "people", "char": "👩‍💼", "name": "woman_office_worker", "keywords": ["business", "manager", "woman", "human"] }, + { "category": "people", "char": "👨‍💼", "name": "man_office_worker", "keywords": ["business", "manager", "man", "human"] }, + { "category": "people", "char": "🧑‍🔧", "name": "mechanic", "keywords": ["plumber", "human", "wrench"] }, + { "category": "people", "char": "👩‍🔧", "name": "woman_mechanic", "keywords": ["plumber", "woman", "human", "wrench"] }, + { "category": "people", "char": "👨‍🔧", "name": "man_mechanic", "keywords": ["plumber", "man", "human", "wrench"] }, + { "category": "people", "char": "🧑‍🔬", "name": "scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "human"] }, + { "category": "people", "char": "👩‍🔬", "name": "woman_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "woman", "human"] }, + { "category": "people", "char": "👨‍🔬", "name": "man_scientist", "keywords": ["biologist", "chemist", "engineer", "physicist", "man", "human"] }, + { "category": "people", "char": "🧑‍🎨", "name": "artist", "keywords": ["painter", "human"] }, + { "category": "people", "char": "👩‍🎨", "name": "woman_artist", "keywords": ["painter", "woman", "human"] }, + { "category": "people", "char": "👨‍🎨", "name": "man_artist", "keywords": ["painter", "man", "human"] }, + { "category": "people", "char": "🧑‍🚒", "name": "firefighter", "keywords": ["fireman", "human"] }, + { "category": "people", "char": "👩‍🚒", "name": "woman_firefighter", "keywords": ["fireman", "woman", "human"] }, + { "category": "people", "char": "👨‍🚒", "name": "man_firefighter", "keywords": ["fireman", "man", "human"] }, + { "category": "people", "char": "🧑‍✈️", "name": "pilot", "keywords": ["aviator", "plane", "human"] }, + { "category": "people", "char": "👩‍✈️", "name": "woman_pilot", "keywords": ["aviator", "plane", "woman", "human"] }, + { "category": "people", "char": "👨‍✈️", "name": "man_pilot", "keywords": ["aviator", "plane", "man", "human"] }, + { "category": "people", "char": "🧑‍🚀", "name": "astronaut", "keywords": ["space", "rocket", "human"] }, + { "category": "people", "char": "👩‍🚀", "name": "woman_astronaut", "keywords": ["space", "rocket", "woman", "human"] }, + { "category": "people", "char": "👨‍🚀", "name": "man_astronaut", "keywords": ["space", "rocket", "man", "human"] }, + { "category": "people", "char": "🧑‍⚖️", "name": "judge", "keywords": ["justice", "court", "human"] }, + { "category": "people", "char": "👩‍⚖️", "name": "woman_judge", "keywords": ["justice", "court", "woman", "human"] }, + { "category": "people", "char": "👨‍⚖️", "name": "man_judge", "keywords": ["justice", "court", "man", "human"] }, + { "category": "people", "char": "🦸‍♀️", "name": "woman_superhero", "keywords": ["woman", "female", "good", "heroine", "superpowers"] }, + { "category": "people", "char": "🦸‍♂️", "name": "man_superhero", "keywords": ["man", "male", "good", "hero", "superpowers"] }, + { "category": "people", "char": "🦹‍♀️", "name": "woman_supervillain", "keywords": ["woman", "female", "evil", "bad", "criminal", "heroine", "superpowers"] }, + { "category": "people", "char": "🦹‍♂️", "name": "man_supervillain", "keywords": ["man", "male", "evil", "bad", "criminal", "hero", "superpowers"] }, + { "category": "people", "char": "🤶", "name": "mrs_claus", "keywords": ["woman", "female", "xmas", "mother christmas"] }, + { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF84", "name": "mx_claus", "keywords": ["xmas", "christmas"] }, + { "category": "people", "char": "🎅", "name": "santa", "keywords": ["festival", "man", "male", "xmas", "father christmas"] }, + { "category": "people", "char": "🥷", "name": "ninja", "keywords": [] }, + { "category": "people", "char": "🧙‍♀️", "name": "sorceress", "keywords": ["woman", "female", "mage", "witch"] }, + { "category": "people", "char": "🧙‍♂️", "name": "wizard", "keywords": ["man", "male", "mage", "sorcerer"] }, + { "category": "people", "char": "🧝‍♀️", "name": "woman_elf", "keywords": ["woman", "female"] }, + { "category": "people", "char": "🧝‍♂️", "name": "man_elf", "keywords": ["man", "male"] }, + { "category": "people", "char": "🧛‍♀️", "name": "woman_vampire", "keywords": ["woman", "female"] }, + { "category": "people", "char": "🧛‍♂️", "name": "man_vampire", "keywords": ["man", "male", "dracula"] }, + { "category": "people", "char": "🧟‍♀️", "name": "woman_zombie", "keywords": ["woman", "female", "undead", "walking dead"] }, + { "category": "people", "char": "🧟‍♂️", "name": "man_zombie", "keywords": ["man", "male", "dracula", "undead", "walking dead"] }, + { "category": "people", "char": "🧞‍♀️", "name": "woman_genie", "keywords": ["woman", "female"] }, + { "category": "people", "char": "🧞‍♂️", "name": "man_genie", "keywords": ["man", "male"] }, + { "category": "people", "char": "🧜‍♀️", "name": "mermaid", "keywords": ["woman", "female", "merwoman", "ariel"] }, + { "category": "people", "char": "🧜‍♂️", "name": "merman", "keywords": ["man", "male", "triton"] }, + { "category": "people", "char": "🧚‍♀️", "name": "woman_fairy", "keywords": ["woman", "female"] }, + { "category": "people", "char": "🧚‍♂️", "name": "man_fairy", "keywords": ["man", "male"] }, + { "category": "people", "char": "👼", "name": "angel", "keywords": ["heaven", "wings", "halo"] }, + { "category": "people", "char": "\uD83E\uDDCC", "name": "troll", "keywords": [] }, + { "category": "people", "char": "🤰", "name": "pregnant_woman", "keywords": ["baby"] }, + { "category": "people", "char": "\uD83E\uDEC3", "name": "pregnant_man", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEC4", "name": "pregnant_person", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDEC5", "name": "person_with_crown", "keywords": [] }, + { "category": "people", "char": "🤱", "name": "breastfeeding", "keywords": ["nursing", "baby"] }, + { "category": "people", "char": "\uD83D\uDC69\u200D\uD83C\uDF7C", "name": "woman_feeding_baby", "keywords": [] }, + { "category": "people", "char": "\uD83D\uDC68\u200D\uD83C\uDF7C", "name": "man_feeding_baby", "keywords": [] }, + { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83C\uDF7C", "name": "person_feeding_baby", "keywords": [] }, + { "category": "people", "char": "👸", "name": "princess", "keywords": ["girl", "woman", "female", "blond", "crown", "royal", "queen"] }, + { "category": "people", "char": "🤴", "name": "prince", "keywords": ["boy", "man", "male", "crown", "royal", "king"] }, + { "category": "people", "char": "👰", "name": "person_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] }, + { "category": "people", "char": "👰", "name": "bride_with_veil", "keywords": ["couple", "marriage", "wedding", "woman", "bride"] }, + { "category": "people", "char": "🤵", "name": "person_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] }, + { "category": "people", "char": "🤵", "name": "man_in_tuxedo", "keywords": ["couple", "marriage", "wedding", "groom"] }, + { "category": "people", "char": "🏃‍♀️", "name": "running_woman", "keywords": ["woman", "walking", "exercise", "race", "running", "female"] }, + { "category": "people", "char": "🏃", "name": "running_man", "keywords": ["man", "walking", "exercise", "race", "running"] }, + { "category": "people", "char": "🚶‍♀️", "name": "walking_woman", "keywords": ["human", "feet", "steps", "woman", "female"] }, + { "category": "people", "char": "🚶", "name": "walking_man", "keywords": ["human", "feet", "steps"] }, + { "category": "people", "char": "💃", "name": "dancer", "keywords": ["female", "girl", "woman", "fun"] }, + { "category": "people", "char": "🕺", "name": "man_dancing", "keywords": ["male", "boy", "fun", "dancer"] }, + { "category": "people", "char": "👯", "name": "dancing_women", "keywords": ["female", "bunny", "women", "girls"] }, + { "category": "people", "char": "👯‍♂️", "name": "dancing_men", "keywords": ["male", "bunny", "men", "boys"] }, + { "category": "people", "char": "👫", "name": "couple", "keywords": ["pair", "people", "human", "love", "date", "dating", "like", "affection", "valentines", "marriage"] }, + { "category": "people", "char": "\uD83E\uDDD1\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1", "name": "people_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "human"] }, + { "category": "people", "char": "👬", "name": "two_men_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "man", "human"] }, + { "category": "people", "char": "👭", "name": "two_women_holding_hands", "keywords": ["pair", "couple", "love", "like", "bromance", "friendship", "people", "female", "human"] }, + { "category": "people", "char": "🫂", "name": "people_hugging", "keywords": [] }, + { "category": "people", "char": "🙇‍♀️", "name": "bowing_woman", "keywords": ["woman", "female", "girl"] }, + { "category": "people", "char": "🙇", "name": "bowing_man", "keywords": ["man", "male", "boy"] }, + { "category": "people", "char": "🤦‍♂️", "name": "man_facepalming", "keywords": ["man", "male", "boy", "disbelief"] }, + { "category": "people", "char": "🤦‍♀️", "name": "woman_facepalming", "keywords": ["woman", "female", "girl", "disbelief"] }, + { "category": "people", "char": "🤷", "name": "woman_shrugging", "keywords": ["woman", "female", "girl", "confused", "indifferent", "doubt"] }, + { "category": "people", "char": "🤷‍♂️", "name": "man_shrugging", "keywords": ["man", "male", "boy", "confused", "indifferent", "doubt"] }, + { "category": "people", "char": "💁", "name": "tipping_hand_woman", "keywords": ["female", "girl", "woman", "human", "information"] }, + { "category": "people", "char": "💁‍♂️", "name": "tipping_hand_man", "keywords": ["male", "boy", "man", "human", "information"] }, + { "category": "people", "char": "🙅", "name": "no_good_woman", "keywords": ["female", "girl", "woman", "nope"] }, + { "category": "people", "char": "🙅‍♂️", "name": "no_good_man", "keywords": ["male", "boy", "man", "nope"] }, + { "category": "people", "char": "🙆", "name": "ok_woman", "keywords": ["women", "girl", "female", "pink", "human", "woman"] }, + { "category": "people", "char": "🙆‍♂️", "name": "ok_man", "keywords": ["men", "boy", "male", "blue", "human", "man"] }, + { "category": "people", "char": "🙋", "name": "raising_hand_woman", "keywords": ["female", "girl", "woman"] }, + { "category": "people", "char": "🙋‍♂️", "name": "raising_hand_man", "keywords": ["male", "boy", "man"] }, + { "category": "people", "char": "🙎", "name": "pouting_woman", "keywords": ["female", "girl", "woman"] }, + { "category": "people", "char": "🙎‍♂️", "name": "pouting_man", "keywords": ["male", "boy", "man"] }, + { "category": "people", "char": "🙍", "name": "frowning_woman", "keywords": ["female", "girl", "woman", "sad", "depressed", "discouraged", "unhappy"] }, + { "category": "people", "char": "🙍‍♂️", "name": "frowning_man", "keywords": ["male", "boy", "man", "sad", "depressed", "discouraged", "unhappy"] }, + { "category": "people", "char": "💇", "name": "haircut_woman", "keywords": ["female", "girl", "woman"] }, + { "category": "people", "char": "💇‍♂️", "name": "haircut_man", "keywords": ["male", "boy", "man"] }, + { "category": "people", "char": "💆", "name": "massage_woman", "keywords": ["female", "girl", "woman", "head"] }, + { "category": "people", "char": "💆‍♂️", "name": "massage_man", "keywords": ["male", "boy", "man", "head"] }, + { "category": "people", "char": "🧖‍♀️", "name": "woman_in_steamy_room", "keywords": ["female", "woman", "spa", "steamroom", "sauna"] }, + { "category": "people", "char": "🧖‍♂️", "name": "man_in_steamy_room", "keywords": ["male", "man", "spa", "steamroom", "sauna"] }, + { "category": "people", "char": "🧏‍♀️", "name": "woman_deaf", "keywords": ["woman", "female"] }, + { "category": "people", "char": "🧏‍♂️", "name": "man_deaf", "keywords": ["man", "male"] }, + { "category": "people", "char": "🧍‍♀️", "name": "woman_standing", "keywords": ["woman", "female"] }, + { "category": "people", "char": "🧍‍♂️", "name": "man_standing", "keywords": ["man", "male"] }, + { "category": "people", "char": "🧎‍♀️", "name": "woman_kneeling", "keywords": ["woman", "female"] }, + { "category": "people", "char": "🧎‍♂️", "name": "man_kneeling", "keywords": ["man", "male"] }, + { "category": "people", "char": "🧑‍🦯", "name": "person_with_probing_cane", "keywords": ["accessibility", "blind"] }, + { "category": "people", "char": "👩‍🦯", "name": "woman_with_probing_cane", "keywords": ["woman", "female", "accessibility", "blind"] }, + { "category": "people", "char": "👨‍🦯", "name": "man_with_probing_cane", "keywords": ["man", "male", "accessibility", "blind"] }, + { "category": "people", "char": "🧑‍🦼", "name": "person_in_motorized_wheelchair", "keywords": ["accessibility"] }, + { "category": "people", "char": "👩‍🦼", "name": "woman_in_motorized_wheelchair", "keywords": ["woman", "female", "accessibility"] }, + { "category": "people", "char": "👨‍🦼", "name": "man_in_motorized_wheelchair", "keywords": ["man", "male", "accessibility"] }, + { "category": "people", "char": "🧑‍🦽", "name": "person_in_manual_wheelchair", "keywords": ["accessibility"] }, + { "category": "people", "char": "👩‍🦽", "name": "woman_in_manual_wheelchair", "keywords": ["woman", "female", "accessibility"] }, + { "category": "people", "char": "👨‍🦽", "name": "man_in_manual_wheelchair", "keywords": ["man", "male", "accessibility"] }, + { "category": "people", "char": "💑", "name": "couple_with_heart_woman_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, + { "category": "people", "char": "👩‍❤️‍👩", "name": "couple_with_heart_woman_woman", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, + { "category": "people", "char": "👨‍❤️‍👨", "name": "couple_with_heart_man_man", "keywords": ["pair", "love", "like", "affection", "human", "dating", "valentines", "marriage"] }, + { "category": "people", "char": "💏", "name": "couplekiss_man_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, + { "category": "people", "char": "👩‍❤️‍💋‍👩", "name": "couplekiss_woman_woman", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, + { "category": "people", "char": "👨‍❤️‍💋‍👨", "name": "couplekiss_man_man", "keywords": ["pair", "valentines", "love", "like", "dating", "marriage"] }, + { "category": "people", "char": "👪", "name": "family_man_woman_boy", "keywords": ["home", "parents", "child", "mom", "dad", "father", "mother", "people", "human"] }, + { "category": "people", "char": "👨‍👩‍👧", "name": "family_man_woman_girl", "keywords": ["home", "parents", "people", "human", "child"] }, + { "category": "people", "char": "👨‍👩‍👧‍👦", "name": "family_man_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👩‍👦‍👦", "name": "family_man_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👩‍👧‍👧", "name": "family_man_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👩‍👩‍👦", "name": "family_woman_woman_boy", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👩‍👩‍👧", "name": "family_woman_woman_girl", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👩‍👩‍👧‍👦", "name": "family_woman_woman_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👩‍👩‍👦‍👦", "name": "family_woman_woman_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👩‍👩‍👧‍👧", "name": "family_woman_woman_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👨‍👦", "name": "family_man_man_boy", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👨‍👧", "name": "family_man_man_girl", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👨‍👧‍👦", "name": "family_man_man_girl_boy", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👨‍👦‍👦", "name": "family_man_man_boy_boy", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👨‍👧‍👧", "name": "family_man_man_girl_girl", "keywords": ["home", "parents", "people", "human", "children"] }, + { "category": "people", "char": "👩‍👦", "name": "family_woman_boy", "keywords": ["home", "parent", "people", "human", "child"] }, + { "category": "people", "char": "👩‍👧", "name": "family_woman_girl", "keywords": ["home", "parent", "people", "human", "child"] }, + { "category": "people", "char": "👩‍👧‍👦", "name": "family_woman_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] }, + { "category": "people", "char": "👩‍👦‍👦", "name": "family_woman_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] }, + { "category": "people", "char": "👩‍👧‍👧", "name": "family_woman_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👦", "name": "family_man_boy", "keywords": ["home", "parent", "people", "human", "child"] }, + { "category": "people", "char": "👨‍👧", "name": "family_man_girl", "keywords": ["home", "parent", "people", "human", "child"] }, + { "category": "people", "char": "👨‍👧‍👦", "name": "family_man_girl_boy", "keywords": ["home", "parent", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👦‍👦", "name": "family_man_boy_boy", "keywords": ["home", "parent", "people", "human", "children"] }, + { "category": "people", "char": "👨‍👧‍👧", "name": "family_man_girl_girl", "keywords": ["home", "parent", "people", "human", "children"] }, + { "category": "people", "char": "🧶", "name": "yarn", "keywords": ["ball", "crochet", "knit"] }, + { "category": "people", "char": "🧵", "name": "thread", "keywords": ["needle", "sewing", "spool", "string"] }, + { "category": "people", "char": "🧥", "name": "coat", "keywords": ["jacket"] }, + { "category": "people", "char": "🥼", "name": "labcoat", "keywords": ["doctor", "experiment", "scientist", "chemist"] }, + { "category": "people", "char": "👚", "name": "womans_clothes", "keywords": ["fashion", "shopping_bags", "female"] }, + { "category": "people", "char": "👕", "name": "tshirt", "keywords": ["fashion", "cloth", "casual", "shirt", "tee"] }, + { "category": "people", "char": "👖", "name": "jeans", "keywords": ["fashion", "shopping"] }, + { "category": "people", "char": "👔", "name": "necktie", "keywords": ["shirt", "suitup", "formal", "fashion", "cloth", "business"] }, + { "category": "people", "char": "👗", "name": "dress", "keywords": ["clothes", "fashion", "shopping"] }, + { "category": "people", "char": "👙", "name": "bikini", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] }, + { "category": "people", "char": "🩱", "name": "one_piece_swimsuit", "keywords": ["swimming", "female", "woman", "girl", "fashion", "beach", "summer"] }, + { "category": "people", "char": "👘", "name": "kimono", "keywords": ["dress", "fashion", "women", "female", "japanese"] }, + { "category": "people", "char": "🥻", "name": "sari", "keywords": ["dress", "fashion", "women", "female"] }, + { "category": "people", "char": "🩲", "name": "briefs", "keywords": ["dress", "fashion"] }, + { "category": "people", "char": "🩳", "name": "shorts", "keywords": ["dress", "fashion"] }, + { "category": "people", "char": "💄", "name": "lipstick", "keywords": ["female", "girl", "fashion", "woman"] }, + { "category": "people", "char": "💋", "name": "kiss", "keywords": ["face", "lips", "love", "like", "affection", "valentines"] }, + { "category": "people", "char": "👣", "name": "footprints", "keywords": ["feet", "tracking", "walking", "beach"] }, + { "category": "people", "char": "🥿", "name": "flat_shoe", "keywords": ["ballet", "slip-on", "slipper"] }, + { "category": "people", "char": "👠", "name": "high_heel", "keywords": ["fashion", "shoes", "female", "pumps", "stiletto"] }, + { "category": "people", "char": "👡", "name": "sandal", "keywords": ["shoes", "fashion", "flip flops"] }, + { "category": "people", "char": "👢", "name": "boot", "keywords": ["shoes", "fashion"] }, + { "category": "people", "char": "👞", "name": "mans_shoe", "keywords": ["fashion", "male"] }, + { "category": "people", "char": "👟", "name": "athletic_shoe", "keywords": ["shoes", "sports", "sneakers"] }, + { "category": "people", "char": "🩴", "name": "thong_sandal", "keywords": [] }, + { "category": "people", "char": "🩰", "name": "ballet_shoes", "keywords": ["shoes", "sports"] }, + { "category": "people", "char": "🧦", "name": "socks", "keywords": ["stockings", "clothes"] }, + { "category": "people", "char": "🧤", "name": "gloves", "keywords": ["hands", "winter", "clothes"] }, + { "category": "people", "char": "🧣", "name": "scarf", "keywords": ["neck", "winter", "clothes"] }, + { "category": "people", "char": "👒", "name": "womans_hat", "keywords": ["fashion", "accessories", "female", "lady", "spring"] }, + { "category": "people", "char": "🎩", "name": "tophat", "keywords": ["magic", "gentleman", "classy", "circus"] }, + { "category": "people", "char": "🧢", "name": "billed_hat", "keywords": ["cap", "baseball"] }, + { "category": "people", "char": "⛑", "name": "rescue_worker_helmet", "keywords": ["construction", "build"] }, + { "category": "people", "char": "🪖", "name": "military_helmet", "keywords": [] }, + { "category": "people", "char": "🎓", "name": "mortar_board", "keywords": ["school", "college", "degree", "university", "graduation", "cap", "hat", "legal", "learn", "education"] }, + { "category": "people", "char": "👑", "name": "crown", "keywords": ["king", "kod", "leader", "royalty", "lord"] }, + { "category": "people", "char": "🎒", "name": "school_satchel", "keywords": ["student", "education", "bag", "backpack"] }, + { "category": "people", "char": "🧳", "name": "luggage", "keywords": ["packing", "travel"] }, + { "category": "people", "char": "👝", "name": "pouch", "keywords": ["bag", "accessories", "shopping"] }, + { "category": "people", "char": "👛", "name": "purse", "keywords": ["fashion", "accessories", "money", "sales", "shopping"] }, + { "category": "people", "char": "👜", "name": "handbag", "keywords": ["fashion", "accessory", "accessories", "shopping"] }, + { "category": "people", "char": "💼", "name": "briefcase", "keywords": ["business", "documents", "work", "law", "legal", "job", "career"] }, + { "category": "people", "char": "👓", "name": "eyeglasses", "keywords": ["fashion", "accessories", "eyesight", "nerdy", "dork", "geek"] }, + { "category": "people", "char": "🕶", "name": "dark_sunglasses", "keywords": ["face", "cool", "accessories"] }, + { "category": "people", "char": "🥽", "name": "goggles", "keywords": ["eyes", "protection", "safety"] }, + { "category": "people", "char": "💍", "name": "ring", "keywords": ["wedding", "propose", "marriage", "valentines", "diamond", "fashion", "jewelry", "gem", "engagement"] }, + { "category": "people", "char": "🌂", "name": "closed_umbrella", "keywords": ["weather", "rain", "drizzle"] }, + { "category": "animals_and_nature", "char": "🐶", "name": "dog", "keywords": ["animal", "friend", "nature", "woof", "puppy", "pet", "faithful"] }, + { "category": "animals_and_nature", "char": "🐱", "name": "cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] }, + { "category": "animals_and_nature", "char": "🐈‍⬛", "name": "black_cat", "keywords": ["animal", "meow", "nature", "pet", "kitten"] }, + { "category": "animals_and_nature", "char": "🐭", "name": "mouse", "keywords": ["animal", "nature", "cheese_wedge", "rodent"] }, + { "category": "animals_and_nature", "char": "🐹", "name": "hamster", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐰", "name": "rabbit", "keywords": ["animal", "nature", "pet", "spring", "magic", "bunny"] }, + { "category": "animals_and_nature", "char": "🦊", "name": "fox_face", "keywords": ["animal", "nature", "face"] }, + { "category": "animals_and_nature", "char": "🐻", "name": "bear", "keywords": ["animal", "nature", "wild"] }, + { "category": "animals_and_nature", "char": "🐼", "name": "panda_face", "keywords": ["animal", "nature", "panda"] }, + { "category": "animals_and_nature", "char": "🐨", "name": "koala", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐯", "name": "tiger", "keywords": ["animal", "cat", "danger", "wild", "nature", "roar"] }, + { "category": "animals_and_nature", "char": "🦁", "name": "lion", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐮", "name": "cow", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] }, + { "category": "animals_and_nature", "char": "🐷", "name": "pig", "keywords": ["animal", "oink", "nature"] }, + { "category": "animals_and_nature", "char": "🐽", "name": "pig_nose", "keywords": ["animal", "oink"] }, + { "category": "animals_and_nature", "char": "🐸", "name": "frog", "keywords": ["animal", "nature", "croak", "toad"] }, + { "category": "animals_and_nature", "char": "🦑", "name": "squid", "keywords": ["animal", "nature", "ocean", "sea"] }, + { "category": "animals_and_nature", "char": "🐙", "name": "octopus", "keywords": ["animal", "creature", "ocean", "sea", "nature", "beach"] }, + { "category": "animals_and_nature", "char": "🦐", "name": "shrimp", "keywords": ["animal", "ocean", "nature", "seafood"] }, + { "category": "animals_and_nature", "char": "🐵", "name": "monkey_face", "keywords": ["animal", "nature", "circus"] }, + { "category": "animals_and_nature", "char": "🦍", "name": "gorilla", "keywords": ["animal", "nature", "circus"] }, + { "category": "animals_and_nature", "char": "🙈", "name": "see_no_evil", "keywords": ["monkey", "animal", "nature", "haha"] }, + { "category": "animals_and_nature", "char": "🙉", "name": "hear_no_evil", "keywords": ["animal", "monkey", "nature"] }, + { "category": "animals_and_nature", "char": "🙊", "name": "speak_no_evil", "keywords": ["monkey", "animal", "nature", "omg"] }, + { "category": "animals_and_nature", "char": "🐒", "name": "monkey", "keywords": ["animal", "nature", "banana", "circus"] }, + { "category": "animals_and_nature", "char": "🐔", "name": "chicken", "keywords": ["animal", "cluck", "nature", "bird"] }, + { "category": "animals_and_nature", "char": "🐧", "name": "penguin", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐦", "name": "bird", "keywords": ["animal", "nature", "fly", "tweet", "spring"] }, + { "category": "animals_and_nature", "char": "🐤", "name": "baby_chick", "keywords": ["animal", "chicken", "bird"] }, + { "category": "animals_and_nature", "char": "🐣", "name": "hatching_chick", "keywords": ["animal", "chicken", "egg", "born", "baby", "bird"] }, + { "category": "animals_and_nature", "char": "🐥", "name": "hatched_chick", "keywords": ["animal", "chicken", "baby", "bird"] }, + { "category": "animals_and_nature", "char": "🦆", "name": "duck", "keywords": ["animal", "nature", "bird", "mallard"] }, + { "category": "animals_and_nature", "char": "🦅", "name": "eagle", "keywords": ["animal", "nature", "bird"] }, + { "category": "animals_and_nature", "char": "🦉", "name": "owl", "keywords": ["animal", "nature", "bird", "hoot"] }, + { "category": "animals_and_nature", "char": "🦇", "name": "bat", "keywords": ["animal", "nature", "blind", "vampire"] }, + { "category": "animals_and_nature", "char": "🐺", "name": "wolf", "keywords": ["animal", "nature", "wild"] }, + { "category": "animals_and_nature", "char": "🐗", "name": "boar", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐴", "name": "horse", "keywords": ["animal", "brown", "nature"] }, + { "category": "animals_and_nature", "char": "🦄", "name": "unicorn", "keywords": ["animal", "nature", "mystical"] }, + { "category": "animals_and_nature", "char": "🐝", "name": "honeybee", "keywords": ["animal", "insect", "nature", "bug", "spring", "honey"] }, + { "category": "animals_and_nature", "char": "🐛", "name": "bug", "keywords": ["animal", "insect", "nature", "worm"] }, + { "category": "animals_and_nature", "char": "🦋", "name": "butterfly", "keywords": ["animal", "insect", "nature", "caterpillar"] }, + { "category": "animals_and_nature", "char": "🐌", "name": "snail", "keywords": ["slow", "animal", "shell"] }, + { "category": "animals_and_nature", "char": "🐞", "name": "lady_beetle", "keywords": ["animal", "insect", "nature", "ladybug"] }, + { "category": "animals_and_nature", "char": "🐜", "name": "ant", "keywords": ["animal", "insect", "nature", "bug"] }, + { "category": "animals_and_nature", "char": "🦗", "name": "grasshopper", "keywords": ["animal", "cricket", "chirp"] }, + { "category": "animals_and_nature", "char": "🕷", "name": "spider", "keywords": ["animal", "arachnid"] }, + { "category": "animals_and_nature", "char": "🪲", "name": "beetle", "keywords": ["animal"] }, + { "category": "animals_and_nature", "char": "🪳", "name": "cockroach", "keywords": ["animal"] }, + { "category": "animals_and_nature", "char": "🪰", "name": "fly", "keywords": ["animal"] }, + { "category": "animals_and_nature", "char": "🪱", "name": "worm", "keywords": ["animal"] }, + { "category": "animals_and_nature", "char": "🦂", "name": "scorpion", "keywords": ["animal", "arachnid"] }, + { "category": "animals_and_nature", "char": "🦀", "name": "crab", "keywords": ["animal", "crustacean"] }, + { "category": "animals_and_nature", "char": "🐍", "name": "snake", "keywords": ["animal", "evil", "nature", "hiss", "python"] }, + { "category": "animals_and_nature", "char": "🦎", "name": "lizard", "keywords": ["animal", "nature", "reptile"] }, + { "category": "animals_and_nature", "char": "🦖", "name": "t-rex", "keywords": ["animal", "nature", "dinosaur", "tyrannosaurus", "extinct"] }, + { "category": "animals_and_nature", "char": "🦕", "name": "sauropod", "keywords": ["animal", "nature", "dinosaur", "brachiosaurus", "brontosaurus", "diplodocus", "extinct"] }, + { "category": "animals_and_nature", "char": "🐢", "name": "turtle", "keywords": ["animal", "slow", "nature", "tortoise"] }, + { "category": "animals_and_nature", "char": "🐠", "name": "tropical_fish", "keywords": ["animal", "swim", "ocean", "beach", "nemo"] }, + { "category": "animals_and_nature", "char": "🐟", "name": "fish", "keywords": ["animal", "food", "nature"] }, + { "category": "animals_and_nature", "char": "🐡", "name": "blowfish", "keywords": ["animal", "nature", "food", "sea", "ocean"] }, + { "category": "animals_and_nature", "char": "🐬", "name": "dolphin", "keywords": ["animal", "nature", "fish", "sea", "ocean", "flipper", "fins", "beach"] }, + { "category": "animals_and_nature", "char": "🦈", "name": "shark", "keywords": ["animal", "nature", "fish", "sea", "ocean", "jaws", "fins", "beach"] }, + { "category": "animals_and_nature", "char": "🐳", "name": "whale", "keywords": ["animal", "nature", "sea", "ocean"] }, + { "category": "animals_and_nature", "char": "🐋", "name": "whale2", "keywords": ["animal", "nature", "sea", "ocean"] }, + { "category": "animals_and_nature", "char": "🐊", "name": "crocodile", "keywords": ["animal", "nature", "reptile", "lizard", "alligator"] }, + { "category": "animals_and_nature", "char": "🐆", "name": "leopard", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦓", "name": "zebra", "keywords": ["animal", "nature", "stripes", "safari"] }, + { "category": "animals_and_nature", "char": "🐅", "name": "tiger2", "keywords": ["animal", "nature", "roar"] }, + { "category": "animals_and_nature", "char": "🐃", "name": "water_buffalo", "keywords": ["animal", "nature", "ox", "cow"] }, + { "category": "animals_and_nature", "char": "🐂", "name": "ox", "keywords": ["animal", "cow", "beef"] }, + { "category": "animals_and_nature", "char": "🐄", "name": "cow2", "keywords": ["beef", "ox", "animal", "nature", "moo", "milk"] }, + { "category": "animals_and_nature", "char": "🦌", "name": "deer", "keywords": ["animal", "nature", "horns", "venison"] }, + { "category": "animals_and_nature", "char": "🐪", "name": "dromedary_camel", "keywords": ["animal", "hot", "desert", "hump"] }, + { "category": "animals_and_nature", "char": "🐫", "name": "camel", "keywords": ["animal", "nature", "hot", "desert", "hump"] }, + { "category": "animals_and_nature", "char": "🦒", "name": "giraffe", "keywords": ["animal", "nature", "spots", "safari"] }, + { "category": "animals_and_nature", "char": "🐘", "name": "elephant", "keywords": ["animal", "nature", "nose", "th", "circus"] }, + { "category": "animals_and_nature", "char": "🦏", "name": "rhinoceros", "keywords": ["animal", "nature", "horn"] }, + { "category": "animals_and_nature", "char": "🐐", "name": "goat", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐏", "name": "ram", "keywords": ["animal", "sheep", "nature"] }, + { "category": "animals_and_nature", "char": "🐑", "name": "sheep", "keywords": ["animal", "nature", "wool", "shipit"] }, + { "category": "animals_and_nature", "char": "🐎", "name": "racehorse", "keywords": ["animal", "gamble", "luck"] }, + { "category": "animals_and_nature", "char": "🐖", "name": "pig2", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐀", "name": "rat", "keywords": ["animal", "mouse", "rodent"] }, + { "category": "animals_and_nature", "char": "🐁", "name": "mouse2", "keywords": ["animal", "nature", "rodent"] }, + { "category": "animals_and_nature", "char": "🐓", "name": "rooster", "keywords": ["animal", "nature", "chicken"] }, + { "category": "animals_and_nature", "char": "🦃", "name": "turkey", "keywords": ["animal", "bird"] }, + { "category": "animals_and_nature", "char": "🕊", "name": "dove", "keywords": ["animal", "bird"] }, + { "category": "animals_and_nature", "char": "🐕", "name": "dog2", "keywords": ["animal", "nature", "friend", "doge", "pet", "faithful"] }, + { "category": "animals_and_nature", "char": "🐩", "name": "poodle", "keywords": ["dog", "animal", "101", "nature", "pet"] }, + { "category": "animals_and_nature", "char": "🐈", "name": "cat2", "keywords": ["animal", "meow", "pet", "cats"] }, + { "category": "animals_and_nature", "char": "🐇", "name": "rabbit2", "keywords": ["animal", "nature", "pet", "magic", "spring"] }, + { "category": "animals_and_nature", "char": "🐿", "name": "chipmunk", "keywords": ["animal", "nature", "rodent", "squirrel"] }, + { "category": "animals_and_nature", "char": "🦔", "name": "hedgehog", "keywords": ["animal", "nature", "spiny"] }, + { "category": "animals_and_nature", "char": "🦝", "name": "raccoon", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦙", "name": "llama", "keywords": ["animal", "nature", "alpaca"] }, + { "category": "animals_and_nature", "char": "🦛", "name": "hippopotamus", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦘", "name": "kangaroo", "keywords": ["animal", "nature", "australia", "joey", "hop", "marsupial"] }, + { "category": "animals_and_nature", "char": "🦡", "name": "badger", "keywords": ["animal", "nature", "honey"] }, + { "category": "animals_and_nature", "char": "🦢", "name": "swan", "keywords": ["animal", "nature", "bird"] }, + { "category": "animals_and_nature", "char": "🦚", "name": "peacock", "keywords": ["animal", "nature", "peahen", "bird"] }, + { "category": "animals_and_nature", "char": "🦜", "name": "parrot", "keywords": ["animal", "nature", "bird", "pirate", "talk"] }, + { "category": "animals_and_nature", "char": "🦞", "name": "lobster", "keywords": ["animal", "nature", "bisque", "claws", "seafood"] }, + { "category": "animals_and_nature", "char": "🦠", "name": "microbe", "keywords": ["amoeba", "bacteria", "germs"] }, + { "category": "animals_and_nature", "char": "🦟", "name": "mosquito", "keywords": ["animal", "nature", "insect", "malaria"] }, + { "category": "animals_and_nature", "char": "🦬", "name": "bison", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦣", "name": "mammoth", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦫", "name": "beaver", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐻‍❄️", "name": "polar_bear", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦤", "name": "dodo", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🪶", "name": "feather", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦭", "name": "seal", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐾", "name": "paw_prints", "keywords": ["animal", "tracking", "footprints", "dog", "cat", "pet", "feet"] }, + { "category": "animals_and_nature", "char": "🐉", "name": "dragon", "keywords": ["animal", "myth", "nature", "chinese", "green"] }, + { "category": "animals_and_nature", "char": "🐲", "name": "dragon_face", "keywords": ["animal", "myth", "nature", "chinese", "green"] }, + { "category": "animals_and_nature", "char": "🦧", "name": "orangutan", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦮", "name": "guide_dog", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🐕‍🦺", "name": "service_dog", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦥", "name": "sloth", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦦", "name": "otter", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦨", "name": "skunk", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🦩", "name": "flamingo", "keywords": ["animal", "nature"] }, + { "category": "animals_and_nature", "char": "🌵", "name": "cactus", "keywords": ["vegetable", "plant", "nature"] }, + { "category": "animals_and_nature", "char": "🎄", "name": "christmas_tree", "keywords": ["festival", "vacation", "december", "xmas", "celebration"] }, + { "category": "animals_and_nature", "char": "🌲", "name": "evergreen_tree", "keywords": ["plant", "nature"] }, + { "category": "animals_and_nature", "char": "🌳", "name": "deciduous_tree", "keywords": ["plant", "nature"] }, + { "category": "animals_and_nature", "char": "🌴", "name": "palm_tree", "keywords": ["plant", "vegetable", "nature", "summer", "beach", "mojito", "tropical"] }, + { "category": "animals_and_nature", "char": "🌱", "name": "seedling", "keywords": ["plant", "nature", "grass", "lawn", "spring"] }, + { "category": "animals_and_nature", "char": "🌿", "name": "herb", "keywords": ["vegetable", "plant", "medicine", "weed", "grass", "lawn"] }, + { "category": "animals_and_nature", "char": "☘", "name": "shamrock", "keywords": ["vegetable", "plant", "nature", "irish", "clover"] }, + { "category": "animals_and_nature", "char": "🍀", "name": "four_leaf_clover", "keywords": ["vegetable", "plant", "nature", "lucky", "irish"] }, + { "category": "animals_and_nature", "char": "🎍", "name": "bamboo", "keywords": ["plant", "nature", "vegetable", "panda", "pine_decoration"] }, + { "category": "animals_and_nature", "char": "🎋", "name": "tanabata_tree", "keywords": ["plant", "nature", "branch", "summer"] }, + { "category": "animals_and_nature", "char": "🍃", "name": "leaves", "keywords": ["nature", "plant", "tree", "vegetable", "grass", "lawn", "spring"] }, + { "category": "animals_and_nature", "char": "🍂", "name": "fallen_leaf", "keywords": ["nature", "plant", "vegetable", "leaves"] }, + { "category": "animals_and_nature", "char": "🍁", "name": "maple_leaf", "keywords": ["nature", "plant", "vegetable", "ca", "fall"] }, + { "category": "animals_and_nature", "char": "🌾", "name": "ear_of_rice", "keywords": ["nature", "plant"] }, + { "category": "animals_and_nature", "char": "🌺", "name": "hibiscus", "keywords": ["plant", "vegetable", "flowers", "beach"] }, + { "category": "animals_and_nature", "char": "🌻", "name": "sunflower", "keywords": ["nature", "plant", "fall"] }, + { "category": "animals_and_nature", "char": "🌹", "name": "rose", "keywords": ["flowers", "valentines", "love", "spring"] }, + { "category": "animals_and_nature", "char": "🥀", "name": "wilted_flower", "keywords": ["plant", "nature", "flower"] }, + { "category": "animals_and_nature", "char": "🌷", "name": "tulip", "keywords": ["flowers", "plant", "nature", "summer", "spring"] }, + { "category": "animals_and_nature", "char": "🌼", "name": "blossom", "keywords": ["nature", "flowers", "yellow"] }, + { "category": "animals_and_nature", "char": "🌸", "name": "cherry_blossom", "keywords": ["nature", "plant", "spring", "flower"] }, + { "category": "animals_and_nature", "char": "💐", "name": "bouquet", "keywords": ["flowers", "nature", "spring"] }, + { "category": "animals_and_nature", "char": "🍄", "name": "mushroom", "keywords": ["plant", "vegetable"] }, + { "category": "animals_and_nature", "char": "🪴", "name": "potted_plant", "keywords": ["plant"] }, + { "category": "animals_and_nature", "char": "🌰", "name": "chestnut", "keywords": ["food", "squirrel"] }, + { "category": "animals_and_nature", "char": "🎃", "name": "jack_o_lantern", "keywords": ["halloween", "light", "pumpkin", "creepy", "fall"] }, + { "category": "animals_and_nature", "char": "🐚", "name": "shell", "keywords": ["nature", "sea", "beach"] }, + { "category": "animals_and_nature", "char": "🕸", "name": "spider_web", "keywords": ["animal", "insect", "arachnid", "silk"] }, + { "category": "animals_and_nature", "char": "🌎", "name": "earth_americas", "keywords": ["globe", "world", "USA", "international"] }, + { "category": "animals_and_nature", "char": "🌍", "name": "earth_africa", "keywords": ["globe", "world", "international"] }, + { "category": "animals_and_nature", "char": "🌏", "name": "earth_asia", "keywords": ["globe", "world", "east", "international"] }, + { "category": "animals_and_nature", "char": "🪐", "name": "ringed_planet", "keywords": ["saturn"] }, + { "category": "animals_and_nature", "char": "🌕", "name": "full_moon", "keywords": ["nature", "yellow", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌖", "name": "waning_gibbous_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep", "waxing_gibbous_moon"] }, + { "category": "animals_and_nature", "char": "🌗", "name": "last_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌘", "name": "waning_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌑", "name": "new_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌒", "name": "waxing_crescent_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌓", "name": "first_quarter_moon", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌔", "name": "waxing_gibbous_moon", "keywords": ["nature", "night", "sky", "gray", "twilight", "planet", "space", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌚", "name": "new_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌝", "name": "full_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌛", "name": "first_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌜", "name": "last_quarter_moon_with_face", "keywords": ["nature", "twilight", "planet", "space", "night", "evening", "sleep"] }, + { "category": "animals_and_nature", "char": "🌞", "name": "sun_with_face", "keywords": ["nature", "morning", "sky"] }, + { "category": "animals_and_nature", "char": "🌙", "name": "crescent_moon", "keywords": ["night", "sleep", "sky", "evening", "magic"] }, + { "category": "animals_and_nature", "char": "⭐", "name": "star", "keywords": ["night", "yellow"] }, + { "category": "animals_and_nature", "char": "🌟", "name": "star2", "keywords": ["night", "sparkle", "awesome", "good", "magic"] }, + { "category": "animals_and_nature", "char": "💫", "name": "dizzy", "keywords": ["star", "sparkle", "shoot", "magic"] }, + { "category": "animals_and_nature", "char": "✨", "name": "sparkles", "keywords": ["stars", "shine", "shiny", "cool", "awesome", "good", "magic"] }, + { "category": "animals_and_nature", "char": "☄", "name": "comet", "keywords": ["space"] }, + { "category": "animals_and_nature", "char": "☀️", "name": "sunny", "keywords": ["weather", "nature", "brightness", "summer", "beach", "spring"] }, + { "category": "animals_and_nature", "char": "🌤", "name": "sun_behind_small_cloud", "keywords": ["weather"] }, + { "category": "animals_and_nature", "char": "⛅", "name": "partly_sunny", "keywords": ["weather", "nature", "cloudy", "morning", "fall", "spring"] }, + { "category": "animals_and_nature", "char": "🌥", "name": "sun_behind_large_cloud", "keywords": ["weather"] }, + { "category": "animals_and_nature", "char": "🌦", "name": "sun_behind_rain_cloud", "keywords": ["weather"] }, + { "category": "animals_and_nature", "char": "☁️", "name": "cloud", "keywords": ["weather", "sky"] }, + { "category": "animals_and_nature", "char": "🌧", "name": "cloud_with_rain", "keywords": ["weather"] }, + { "category": "animals_and_nature", "char": "⛈", "name": "cloud_with_lightning_and_rain", "keywords": ["weather", "lightning"] }, + { "category": "animals_and_nature", "char": "🌩", "name": "cloud_with_lightning", "keywords": ["weather", "thunder"] }, + { "category": "animals_and_nature", "char": "⚡", "name": "zap", "keywords": ["thunder", "weather", "lightning bolt", "fast"] }, + { "category": "animals_and_nature", "char": "🔥", "name": "fire", "keywords": ["hot", "cook", "flame"] }, + { "category": "animals_and_nature", "char": "💥", "name": "boom", "keywords": ["bomb", "explode", "explosion", "collision", "blown"] }, + { "category": "animals_and_nature", "char": "❄️", "name": "snowflake", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas"] }, + { "category": "animals_and_nature", "char": "🌨", "name": "cloud_with_snow", "keywords": ["weather"] }, + { "category": "animals_and_nature", "char": "⛄", "name": "snowman", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen", "without_snow"] }, + { "category": "animals_and_nature", "char": "☃", "name": "snowman_with_snow", "keywords": ["winter", "season", "cold", "weather", "christmas", "xmas", "frozen"] }, + { "category": "animals_and_nature", "char": "🌬", "name": "wind_face", "keywords": ["gust", "air"] }, + { "category": "animals_and_nature", "char": "💨", "name": "dash", "keywords": ["wind", "air", "fast", "shoo", "fart", "smoke", "puff"] }, + { "category": "animals_and_nature", "char": "🌪", "name": "tornado", "keywords": ["weather", "cyclone", "twister"] }, + { "category": "animals_and_nature", "char": "🌫", "name": "fog", "keywords": ["weather"] }, + { "category": "animals_and_nature", "char": "☂", "name": "open_umbrella", "keywords": ["weather", "spring"] }, + { "category": "animals_and_nature", "char": "☔", "name": "umbrella", "keywords": ["rainy", "weather", "spring"] }, + { "category": "animals_and_nature", "char": "💧", "name": "droplet", "keywords": ["water", "drip", "faucet", "spring"] }, + { "category": "animals_and_nature", "char": "💦", "name": "sweat_drops", "keywords": ["water", "drip", "oops"] }, + { "category": "animals_and_nature", "char": "🌊", "name": "ocean", "keywords": ["sea", "water", "wave", "nature", "tsunami", "disaster"] }, + { "category": "animals_and_nature", "char": "\uD83E\uDEB7", "name": "lotus", "keywords": [] }, + { "category": "animals_and_nature", "char": "\uD83E\uDEB8", "name": "coral", "keywords": [] }, + { "category": "animals_and_nature", "char": "\uD83E\uDEB9", "name": "empty_nest", "keywords": [] }, + { "category": "animals_and_nature", "char": "\uD83E\uDEBA", "name": "nest_with_eggs", "keywords": [] }, + { "category": "food_and_drink", "char": "🍏", "name": "green_apple", "keywords": ["fruit", "nature"] }, + { "category": "food_and_drink", "char": "🍎", "name": "apple", "keywords": ["fruit", "mac", "school"] }, + { "category": "food_and_drink", "char": "🍐", "name": "pear", "keywords": ["fruit", "nature", "food"] }, + { "category": "food_and_drink", "char": "🍊", "name": "tangerine", "keywords": ["food", "fruit", "nature", "orange"] }, + { "category": "food_and_drink", "char": "🍋", "name": "lemon", "keywords": ["fruit", "nature"] }, + { "category": "food_and_drink", "char": "🍌", "name": "banana", "keywords": ["fruit", "food", "monkey"] }, + { "category": "food_and_drink", "char": "🍉", "name": "watermelon", "keywords": ["fruit", "food", "picnic", "summer"] }, + { "category": "food_and_drink", "char": "🍇", "name": "grapes", "keywords": ["fruit", "food", "wine"] }, + { "category": "food_and_drink", "char": "🍓", "name": "strawberry", "keywords": ["fruit", "food", "nature"] }, + { "category": "food_and_drink", "char": "🍈", "name": "melon", "keywords": ["fruit", "nature", "food"] }, + { "category": "food_and_drink", "char": "🍒", "name": "cherries", "keywords": ["food", "fruit"] }, + { "category": "food_and_drink", "char": "🍑", "name": "peach", "keywords": ["fruit", "nature", "food"] }, + { "category": "food_and_drink", "char": "🍍", "name": "pineapple", "keywords": ["fruit", "nature", "food"] }, + { "category": "food_and_drink", "char": "🥥", "name": "coconut", "keywords": ["fruit", "nature", "food", "palm"] }, + { "category": "food_and_drink", "char": "🥝", "name": "kiwi_fruit", "keywords": ["fruit", "food"] }, + { "category": "food_and_drink", "char": "🥭", "name": "mango", "keywords": ["fruit", "food", "tropical"] }, + { "category": "food_and_drink", "char": "🥑", "name": "avocado", "keywords": ["fruit", "food"] }, + { "category": "food_and_drink", "char": "🥦", "name": "broccoli", "keywords": ["fruit", "food", "vegetable"] }, + { "category": "food_and_drink", "char": "🍅", "name": "tomato", "keywords": ["fruit", "vegetable", "nature", "food"] }, + { "category": "food_and_drink", "char": "🍆", "name": "eggplant", "keywords": ["vegetable", "nature", "food", "aubergine"] }, + { "category": "food_and_drink", "char": "🥒", "name": "cucumber", "keywords": ["fruit", "food", "pickle"] }, + { "category": "food_and_drink", "char": "🫐", "name": "blueberries", "keywords": ["fruit", "food"] }, + { "category": "food_and_drink", "char": "🫒", "name": "olive", "keywords": ["fruit", "food"] }, + { "category": "food_and_drink", "char": "🫑", "name": "bell_pepper", "keywords": ["fruit", "food"] }, + { "category": "food_and_drink", "char": "🥕", "name": "carrot", "keywords": ["vegetable", "food", "orange"] }, + { "category": "food_and_drink", "char": "🌶", "name": "hot_pepper", "keywords": ["food", "spicy", "chilli", "chili"] }, + { "category": "food_and_drink", "char": "🥔", "name": "potato", "keywords": ["food", "tuber", "vegatable", "starch"] }, + { "category": "food_and_drink", "char": "🌽", "name": "corn", "keywords": ["food", "vegetable", "plant"] }, + { "category": "food_and_drink", "char": "🥬", "name": "leafy_greens", "keywords": ["food", "vegetable", "plant", "bok choy", "cabbage", "kale", "lettuce"] }, + { "category": "food_and_drink", "char": "🍠", "name": "sweet_potato", "keywords": ["food", "nature"] }, + { "category": "food_and_drink", "char": "🥜", "name": "peanuts", "keywords": ["food", "nut"] }, + { "category": "food_and_drink", "char": "🧄", "name": "garlic", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🧅", "name": "onion", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🍯", "name": "honey_pot", "keywords": ["bees", "sweet", "kitchen"] }, + { "category": "food_and_drink", "char": "🥐", "name": "croissant", "keywords": ["food", "bread", "french"] }, + { "category": "food_and_drink", "char": "🍞", "name": "bread", "keywords": ["food", "wheat", "breakfast", "toast"] }, + { "category": "food_and_drink", "char": "🥖", "name": "baguette_bread", "keywords": ["food", "bread", "french"] }, + { "category": "food_and_drink", "char": "🥯", "name": "bagel", "keywords": ["food", "bread", "bakery", "schmear"] }, + { "category": "food_and_drink", "char": "🥨", "name": "pretzel", "keywords": ["food", "bread", "twisted"] }, + { "category": "food_and_drink", "char": "🧀", "name": "cheese", "keywords": ["food", "chadder"] }, + { "category": "food_and_drink", "char": "🥚", "name": "egg", "keywords": ["food", "chicken", "breakfast"] }, + { "category": "food_and_drink", "char": "🥓", "name": "bacon", "keywords": ["food", "breakfast", "pork", "pig", "meat"] }, + { "category": "food_and_drink", "char": "🥩", "name": "steak", "keywords": ["food", "cow", "meat", "cut", "chop", "lambchop", "porkchop"] }, + { "category": "food_and_drink", "char": "🥞", "name": "pancakes", "keywords": ["food", "breakfast", "flapjacks", "hotcakes"] }, + { "category": "food_and_drink", "char": "🍗", "name": "poultry_leg", "keywords": ["food", "meat", "drumstick", "bird", "chicken", "turkey"] }, + { "category": "food_and_drink", "char": "🍖", "name": "meat_on_bone", "keywords": ["good", "food", "drumstick"] }, + { "category": "food_and_drink", "char": "🦴", "name": "bone", "keywords": ["skeleton"] }, + { "category": "food_and_drink", "char": "🍤", "name": "fried_shrimp", "keywords": ["food", "animal", "appetizer", "summer"] }, + { "category": "food_and_drink", "char": "🍳", "name": "fried_egg", "keywords": ["food", "breakfast", "kitchen", "egg"] }, + { "category": "food_and_drink", "char": "🍔", "name": "hamburger", "keywords": ["meat", "fast food", "beef", "cheeseburger", "mcdonalds", "burger king"] }, + { "category": "food_and_drink", "char": "🍟", "name": "fries", "keywords": ["chips", "snack", "fast food"] }, + { "category": "food_and_drink", "char": "🥙", "name": "stuffed_flatbread", "keywords": ["food", "flatbread", "stuffed", "gyro"] }, + { "category": "food_and_drink", "char": "🌭", "name": "hotdog", "keywords": ["food", "frankfurter"] }, + { "category": "food_and_drink", "char": "🍕", "name": "pizza", "keywords": ["food", "party"] }, + { "category": "food_and_drink", "char": "🥪", "name": "sandwich", "keywords": ["food", "lunch", "bread"] }, + { "category": "food_and_drink", "char": "🥫", "name": "canned_food", "keywords": ["food", "soup"] }, + { "category": "food_and_drink", "char": "🍝", "name": "spaghetti", "keywords": ["food", "italian", "noodle"] }, + { "category": "food_and_drink", "char": "🌮", "name": "taco", "keywords": ["food", "mexican"] }, + { "category": "food_and_drink", "char": "🌯", "name": "burrito", "keywords": ["food", "mexican"] }, + { "category": "food_and_drink", "char": "🥗", "name": "green_salad", "keywords": ["food", "healthy", "lettuce"] }, + { "category": "food_and_drink", "char": "🥘", "name": "shallow_pan_of_food", "keywords": ["food", "cooking", "casserole", "paella"] }, + { "category": "food_and_drink", "char": "🍜", "name": "ramen", "keywords": ["food", "japanese", "noodle", "chopsticks"] }, + { "category": "food_and_drink", "char": "🍲", "name": "stew", "keywords": ["food", "meat", "soup"] }, + { "category": "food_and_drink", "char": "🍥", "name": "fish_cake", "keywords": ["food", "japan", "sea", "beach", "narutomaki", "pink", "swirl", "kamaboko", "surimi", "ramen"] }, + { "category": "food_and_drink", "char": "🥠", "name": "fortune_cookie", "keywords": ["food", "prophecy"] }, + { "category": "food_and_drink", "char": "🍣", "name": "sushi", "keywords": ["food", "fish", "japanese", "rice"] }, + { "category": "food_and_drink", "char": "🍱", "name": "bento", "keywords": ["food", "japanese", "box"] }, + { "category": "food_and_drink", "char": "🍛", "name": "curry", "keywords": ["food", "spicy", "hot", "indian"] }, + { "category": "food_and_drink", "char": "🍙", "name": "rice_ball", "keywords": ["food", "japanese"] }, + { "category": "food_and_drink", "char": "🍚", "name": "rice", "keywords": ["food", "china", "asian"] }, + { "category": "food_and_drink", "char": "🍘", "name": "rice_cracker", "keywords": ["food", "japanese"] }, + { "category": "food_and_drink", "char": "🍢", "name": "oden", "keywords": ["food", "japanese"] }, + { "category": "food_and_drink", "char": "🍡", "name": "dango", "keywords": ["food", "dessert", "sweet", "japanese", "barbecue", "meat"] }, + { "category": "food_and_drink", "char": "🍧", "name": "shaved_ice", "keywords": ["hot", "dessert", "summer"] }, + { "category": "food_and_drink", "char": "🍨", "name": "ice_cream", "keywords": ["food", "hot", "dessert"] }, + { "category": "food_and_drink", "char": "🍦", "name": "icecream", "keywords": ["food", "hot", "dessert", "summer"] }, + { "category": "food_and_drink", "char": "🥧", "name": "pie", "keywords": ["food", "dessert", "pastry"] }, + { "category": "food_and_drink", "char": "🍰", "name": "cake", "keywords": ["food", "dessert"] }, + { "category": "food_and_drink", "char": "🧁", "name": "cupcake", "keywords": ["food", "dessert", "bakery", "sweet"] }, + { "category": "food_and_drink", "char": "🥮", "name": "moon_cake", "keywords": ["food", "autumn"] }, + { "category": "food_and_drink", "char": "🎂", "name": "birthday", "keywords": ["food", "dessert", "cake"] }, + { "category": "food_and_drink", "char": "🍮", "name": "custard", "keywords": ["dessert", "food"] }, + { "category": "food_and_drink", "char": "🍬", "name": "candy", "keywords": ["snack", "dessert", "sweet", "lolly"] }, + { "category": "food_and_drink", "char": "🍭", "name": "lollipop", "keywords": ["food", "snack", "candy", "sweet"] }, + { "category": "food_and_drink", "char": "🍫", "name": "chocolate_bar", "keywords": ["food", "snack", "dessert", "sweet"] }, + { "category": "food_and_drink", "char": "🍿", "name": "popcorn", "keywords": ["food", "movie theater", "films", "snack"] }, + { "category": "food_and_drink", "char": "🥟", "name": "dumpling", "keywords": ["food", "empanada", "pierogi", "potsticker"] }, + { "category": "food_and_drink", "char": "🍩", "name": "doughnut", "keywords": ["food", "dessert", "snack", "sweet", "donut"] }, + { "category": "food_and_drink", "char": "🍪", "name": "cookie", "keywords": ["food", "snack", "oreo", "chocolate", "sweet", "dessert"] }, + { "category": "food_and_drink", "char": "🧇", "name": "waffle", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🧆", "name": "falafel", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🧈", "name": "butter", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🦪", "name": "oyster", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🫓", "name": "flatbread", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🫔", "name": "tamale", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🫕", "name": "fondue", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🥛", "name": "milk_glass", "keywords": ["beverage", "drink", "cow"] }, + { "category": "food_and_drink", "char": "🍺", "name": "beer", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] }, + { "category": "food_and_drink", "char": "🍻", "name": "beers", "keywords": ["relax", "beverage", "drink", "drunk", "party", "pub", "summer", "alcohol", "booze"] }, + { "category": "food_and_drink", "char": "🥂", "name": "clinking_glasses", "keywords": ["beverage", "drink", "party", "alcohol", "celebrate", "cheers", "wine", "champagne", "toast"] }, + { "category": "food_and_drink", "char": "🍷", "name": "wine_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "booze"] }, + { "category": "food_and_drink", "char": "🥃", "name": "tumbler_glass", "keywords": ["drink", "beverage", "drunk", "alcohol", "liquor", "booze", "bourbon", "scotch", "whisky", "glass", "shot"] }, + { "category": "food_and_drink", "char": "🍸", "name": "cocktail", "keywords": ["drink", "drunk", "alcohol", "beverage", "booze", "mojito"] }, + { "category": "food_and_drink", "char": "🍹", "name": "tropical_drink", "keywords": ["beverage", "cocktail", "summer", "beach", "alcohol", "booze", "mojito"] }, + { "category": "food_and_drink", "char": "🍾", "name": "champagne", "keywords": ["drink", "wine", "bottle", "celebration"] }, + { "category": "food_and_drink", "char": "🍶", "name": "sake", "keywords": ["wine", "drink", "drunk", "beverage", "japanese", "alcohol", "booze"] }, + { "category": "food_and_drink", "char": "🍵", "name": "tea", "keywords": ["drink", "bowl", "breakfast", "green", "british"] }, + { "category": "food_and_drink", "char": "🥤", "name": "cup_with_straw", "keywords": ["drink", "soda"] }, + { "category": "food_and_drink", "char": "☕", "name": "coffee", "keywords": ["beverage", "caffeine", "latte", "espresso"] }, + { "category": "food_and_drink", "char": "🫖", "name": "teapot", "keywords": [] }, + { "category": "food_and_drink", "char": "🧋", "name": "bubble_tea", "keywords": ["tapioca"] }, + { "category": "food_and_drink", "char": "🍼", "name": "baby_bottle", "keywords": ["food", "container", "milk"] }, + { "category": "food_and_drink", "char": "🧃", "name": "beverage_box", "keywords": ["food", "drink"] }, + { "category": "food_and_drink", "char": "🧉", "name": "mate", "keywords": ["food", "drink"] }, + { "category": "food_and_drink", "char": "🧊", "name": "ice_cube", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "🧂", "name": "salt", "keywords": ["condiment", "shaker"] }, + { "category": "food_and_drink", "char": "🥄", "name": "spoon", "keywords": ["cutlery", "kitchen", "tableware"] }, + { "category": "food_and_drink", "char": "🍴", "name": "fork_and_knife", "keywords": ["cutlery", "kitchen"] }, + { "category": "food_and_drink", "char": "🍽", "name": "plate_with_cutlery", "keywords": ["food", "eat", "meal", "lunch", "dinner", "restaurant"] }, + { "category": "food_and_drink", "char": "🥣", "name": "bowl_with_spoon", "keywords": ["food", "breakfast", "cereal", "oatmeal", "porridge"] }, + { "category": "food_and_drink", "char": "🥡", "name": "takeout_box", "keywords": ["food", "leftovers"] }, + { "category": "food_and_drink", "char": "🥢", "name": "chopsticks", "keywords": ["food"] }, + { "category": "food_and_drink", "char": "\uD83E\uDED7", "name": "pouring_liquid", "keywords": [] }, + { "category": "food_and_drink", "char": "\uD83E\uDED8", "name": "beans", "keywords": [] }, + { "category": "food_and_drink", "char": "\uD83E\uDED9", "name": "jar", "keywords": [] }, + { "category": "activity", "char": "⚽", "name": "soccer", "keywords": ["sports", "football"] }, + { "category": "activity", "char": "🏀", "name": "basketball", "keywords": ["sports", "balls", "NBA"] }, + { "category": "activity", "char": "🏈", "name": "football", "keywords": ["sports", "balls", "NFL"] }, + { "category": "activity", "char": "⚾", "name": "baseball", "keywords": ["sports", "balls"] }, + { "category": "activity", "char": "🥎", "name": "softball", "keywords": ["sports", "balls"] }, + { "category": "activity", "char": "🎾", "name": "tennis", "keywords": ["sports", "balls", "green"] }, + { "category": "activity", "char": "🏐", "name": "volleyball", "keywords": ["sports", "balls"] }, + { "category": "activity", "char": "🏉", "name": "rugby_football", "keywords": ["sports", "team"] }, + { "category": "activity", "char": "🥏", "name": "flying_disc", "keywords": ["sports", "frisbee", "ultimate"] }, + { "category": "activity", "char": "🎱", "name": "8ball", "keywords": ["pool", "hobby", "game", "luck", "magic"] }, + { "category": "activity", "char": "⛳", "name": "golf", "keywords": ["sports", "business", "flag", "hole", "summer"] }, + { "category": "activity", "char": "🏌️‍♀️", "name": "golfing_woman", "keywords": ["sports", "business", "woman", "female"] }, + { "category": "activity", "char": "🏌", "name": "golfing_man", "keywords": ["sports", "business"] }, + { "category": "activity", "char": "🏓", "name": "ping_pong", "keywords": ["sports", "pingpong"] }, + { "category": "activity", "char": "🏸", "name": "badminton", "keywords": ["sports"] }, + { "category": "activity", "char": "🥅", "name": "goal_net", "keywords": ["sports"] }, + { "category": "activity", "char": "🏒", "name": "ice_hockey", "keywords": ["sports"] }, + { "category": "activity", "char": "🏑", "name": "field_hockey", "keywords": ["sports"] }, + { "category": "activity", "char": "🥍", "name": "lacrosse", "keywords": ["sports", "ball", "stick"] }, + { "category": "activity", "char": "🏏", "name": "cricket", "keywords": ["sports"] }, + { "category": "activity", "char": "🎿", "name": "ski", "keywords": ["sports", "winter", "cold", "snow"] }, + { "category": "activity", "char": "⛷", "name": "skier", "keywords": ["sports", "winter", "snow"] }, + { "category": "activity", "char": "🏂", "name": "snowboarder", "keywords": ["sports", "winter"] }, + { "category": "activity", "char": "🤺", "name": "person_fencing", "keywords": ["sports", "fencing", "sword"] }, + { "category": "activity", "char": "🤼‍♀️", "name": "women_wrestling", "keywords": ["sports", "wrestlers"] }, + { "category": "activity", "char": "🤼‍♂️", "name": "men_wrestling", "keywords": ["sports", "wrestlers"] }, + { "category": "activity", "char": "🤸‍♀️", "name": "woman_cartwheeling", "keywords": ["gymnastics"] }, + { "category": "activity", "char": "🤸‍♂️", "name": "man_cartwheeling", "keywords": ["gymnastics"] }, + { "category": "activity", "char": "🤾‍♀️", "name": "woman_playing_handball", "keywords": ["sports"] }, + { "category": "activity", "char": "🤾‍♂️", "name": "man_playing_handball", "keywords": ["sports"] }, + { "category": "activity", "char": "⛸", "name": "ice_skate", "keywords": ["sports"] }, + { "category": "activity", "char": "🥌", "name": "curling_stone", "keywords": ["sports"] }, + { "category": "activity", "char": "🛹", "name": "skateboard", "keywords": ["board"] }, + { "category": "activity", "char": "🛷", "name": "sled", "keywords": ["sleigh", "luge", "toboggan"] }, + { "category": "activity", "char": "🏹", "name": "bow_and_arrow", "keywords": ["sports"] }, + { "category": "activity", "char": "🎣", "name": "fishing_pole_and_fish", "keywords": ["food", "hobby", "summer"] }, + { "category": "activity", "char": "🥊", "name": "boxing_glove", "keywords": ["sports", "fighting"] }, + { "category": "activity", "char": "🥋", "name": "martial_arts_uniform", "keywords": ["judo", "karate", "taekwondo"] }, + { "category": "activity", "char": "🚣‍♀️", "name": "rowing_woman", "keywords": ["sports", "hobby", "water", "ship", "woman", "female"] }, + { "category": "activity", "char": "🚣", "name": "rowing_man", "keywords": ["sports", "hobby", "water", "ship"] }, + { "category": "activity", "char": "🧗‍♀️", "name": "climbing_woman", "keywords": ["sports", "hobby", "woman", "female", "rock"] }, + { "category": "activity", "char": "🧗‍♂️", "name": "climbing_man", "keywords": ["sports", "hobby", "man", "male", "rock"] }, + { "category": "activity", "char": "🏊‍♀️", "name": "swimming_woman", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer", "woman", "female"] }, + { "category": "activity", "char": "🏊", "name": "swimming_man", "keywords": ["sports", "exercise", "human", "athlete", "water", "summer"] }, + { "category": "activity", "char": "🤽‍♀️", "name": "woman_playing_water_polo", "keywords": ["sports", "pool"] }, + { "category": "activity", "char": "🤽‍♂️", "name": "man_playing_water_polo", "keywords": ["sports", "pool"] }, + { "category": "activity", "char": "🧘‍♀️", "name": "woman_in_lotus_position", "keywords": ["woman", "female", "meditation", "yoga", "serenity", "zen", "mindfulness"] }, + { "category": "activity", "char": "🧘‍♂️", "name": "man_in_lotus_position", "keywords": ["man", "male", "meditation", "yoga", "serenity", "zen", "mindfulness"] }, + { "category": "activity", "char": "🏄‍♀️", "name": "surfing_woman", "keywords": ["sports", "ocean", "sea", "summer", "beach", "woman", "female"] }, + { "category": "activity", "char": "🏄", "name": "surfing_man", "keywords": ["sports", "ocean", "sea", "summer", "beach"] }, + { "category": "activity", "char": "🛀", "name": "bath", "keywords": ["clean", "shower", "bathroom"] }, + { "category": "activity", "char": "⛹️‍♀️", "name": "basketball_woman", "keywords": ["sports", "human", "woman", "female"] }, + { "category": "activity", "char": "⛹", "name": "basketball_man", "keywords": ["sports", "human"] }, + { "category": "activity", "char": "🏋️‍♀️", "name": "weight_lifting_woman", "keywords": ["sports", "training", "exercise", "woman", "female"] }, + { "category": "activity", "char": "🏋", "name": "weight_lifting_man", "keywords": ["sports", "training", "exercise"] }, + { "category": "activity", "char": "🚴‍♀️", "name": "biking_woman", "keywords": ["sports", "bike", "exercise", "hipster", "woman", "female"] }, + { "category": "activity", "char": "🚴", "name": "biking_man", "keywords": ["sports", "bike", "exercise", "hipster"] }, + { "category": "activity", "char": "🚵‍♀️", "name": "mountain_biking_woman", "keywords": ["transportation", "sports", "human", "race", "bike", "woman", "female"] }, + { "category": "activity", "char": "🚵", "name": "mountain_biking_man", "keywords": ["transportation", "sports", "human", "race", "bike"] }, + { "category": "activity", "char": "🏇", "name": "horse_racing", "keywords": ["animal", "betting", "competition", "gambling", "luck"] }, + { "category": "activity", "char": "🤿", "name": "diving_mask", "keywords": ["sports"] }, + { "category": "activity", "char": "🪀", "name": "yo_yo", "keywords": ["sports"] }, + { "category": "activity", "char": "🪁", "name": "kite", "keywords": ["sports"] }, + { "category": "activity", "char": "🦺", "name": "safety_vest", "keywords": ["sports"] }, + { "category": "activity", "char": "🪡", "name": "sewing_needle", "keywords": [] }, + { "category": "activity", "char": "🪢", "name": "knot", "keywords": [] }, + { "category": "activity", "char": "🕴", "name": "business_suit_levitating", "keywords": ["suit", "business", "levitate", "hover", "jump"] }, + { "category": "activity", "char": "🏆", "name": "trophy", "keywords": ["win", "award", "contest", "place", "ftw", "ceremony"] }, + { "category": "activity", "char": "🎽", "name": "running_shirt_with_sash", "keywords": ["play", "pageant"] }, + { "category": "activity", "char": "🏅", "name": "medal_sports", "keywords": ["award", "winning"] }, + { "category": "activity", "char": "🎖", "name": "medal_military", "keywords": ["award", "winning", "army"] }, + { "category": "activity", "char": "🥇", "name": "1st_place_medal", "keywords": ["award", "winning", "first"] }, + { "category": "activity", "char": "🥈", "name": "2nd_place_medal", "keywords": ["award", "second"] }, + { "category": "activity", "char": "🥉", "name": "3rd_place_medal", "keywords": ["award", "third"] }, + { "category": "activity", "char": "🎗", "name": "reminder_ribbon", "keywords": ["sports", "cause", "support", "awareness"] }, + { "category": "activity", "char": "🏵", "name": "rosette", "keywords": ["flower", "decoration", "military"] }, + { "category": "activity", "char": "🎫", "name": "ticket", "keywords": ["event", "concert", "pass"] }, + { "category": "activity", "char": "🎟", "name": "tickets", "keywords": ["sports", "concert", "entrance"] }, + { "category": "activity", "char": "🎭", "name": "performing_arts", "keywords": ["acting", "theater", "drama"] }, + { "category": "activity", "char": "🎨", "name": "art", "keywords": ["design", "paint", "draw", "colors"] }, + { "category": "activity", "char": "🎪", "name": "circus_tent", "keywords": ["festival", "carnival", "party"] }, + { "category": "activity", "char": "🤹‍♀️", "name": "woman_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] }, + { "category": "activity", "char": "🤹‍♂️", "name": "man_juggling", "keywords": ["juggle", "balance", "skill", "multitask"] }, + { "category": "activity", "char": "🎤", "name": "microphone", "keywords": ["sound", "music", "PA", "sing", "talkshow"] }, + { "category": "activity", "char": "🎧", "name": "headphones", "keywords": ["music", "score", "gadgets"] }, + { "category": "activity", "char": "🎼", "name": "musical_score", "keywords": ["treble", "clef", "compose"] }, + { "category": "activity", "char": "🎹", "name": "musical_keyboard", "keywords": ["piano", "instrument", "compose"] }, + { "category": "activity", "char": "🥁", "name": "drum", "keywords": ["music", "instrument", "drumsticks", "snare"] }, + { "category": "activity", "char": "🎷", "name": "saxophone", "keywords": ["music", "instrument", "jazz", "blues"] }, + { "category": "activity", "char": "🎺", "name": "trumpet", "keywords": ["music", "brass"] }, + { "category": "activity", "char": "🎸", "name": "guitar", "keywords": ["music", "instrument"] }, + { "category": "activity", "char": "🎻", "name": "violin", "keywords": ["music", "instrument", "orchestra", "symphony"] }, + { "category": "activity", "char": "🪕", "name": "banjo", "keywords": ["music", "instrument"] }, + { "category": "activity", "char": "🪗", "name": "accordion", "keywords": ["music", "instrument"] }, + { "category": "activity", "char": "🪘", "name": "long_drum", "keywords": ["music", "instrument"] }, + { "category": "activity", "char": "🎬", "name": "clapper", "keywords": ["movie", "film", "record"] }, + { "category": "activity", "char": "🎮", "name": "video_game", "keywords": ["play", "console", "PS4", "controller"] }, + { "category": "activity", "char": "👾", "name": "space_invader", "keywords": ["game", "arcade", "play"] }, + { "category": "activity", "char": "🎯", "name": "dart", "keywords": ["game", "play", "bar", "target", "bullseye"] }, + { "category": "activity", "char": "🎲", "name": "game_die", "keywords": ["dice", "random", "tabletop", "play", "luck"] }, + { "category": "activity", "char": "♟️", "name": "chess_pawn", "keywords": ["expendable"] }, + { "category": "activity", "char": "🎰", "name": "slot_machine", "keywords": ["bet", "gamble", "vegas", "fruit machine", "luck", "casino"] }, + { "category": "activity", "char": "🧩", "name": "jigsaw", "keywords": ["interlocking", "puzzle", "piece"] }, + { "category": "activity", "char": "🎳", "name": "bowling", "keywords": ["sports", "fun", "play"] }, + { "category": "activity", "char": "🪄", "name": "magic_wand", "keywords": [] }, + { "category": "activity", "char": "🪅", "name": "pinata", "keywords": [] }, + { "category": "activity", "char": "🪆", "name": "nesting_dolls", "keywords": [] }, + { "category": "activity", "char": "\uD83E\uDEAC", "name": "hamsa", "keywords": [] }, + { "category": "activity", "char": "\uD83E\uDEA9", "name": "mirror_ball", "keywords": [] }, + { "category": "travel_and_places", "char": "🚗", "name": "red_car", "keywords": ["red", "transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🚕", "name": "taxi", "keywords": ["uber", "vehicle", "cars", "transportation"] }, + { "category": "travel_and_places", "char": "🚙", "name": "blue_car", "keywords": ["transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🚌", "name": "bus", "keywords": ["car", "vehicle", "transportation"] }, + { "category": "travel_and_places", "char": "🚎", "name": "trolleybus", "keywords": ["bart", "transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🏎", "name": "racing_car", "keywords": ["sports", "race", "fast", "formula", "f1"] }, + { "category": "travel_and_places", "char": "🚓", "name": "police_car", "keywords": ["vehicle", "cars", "transportation", "law", "legal", "enforcement"] }, + { "category": "travel_and_places", "char": "🚑", "name": "ambulance", "keywords": ["health", "911", "hospital"] }, + { "category": "travel_and_places", "char": "🚒", "name": "fire_engine", "keywords": ["transportation", "cars", "vehicle"] }, + { "category": "travel_and_places", "char": "🚐", "name": "minibus", "keywords": ["vehicle", "car", "transportation"] }, + { "category": "travel_and_places", "char": "🚚", "name": "truck", "keywords": ["cars", "transportation"] }, + { "category": "travel_and_places", "char": "🚛", "name": "articulated_lorry", "keywords": ["vehicle", "cars", "transportation", "express"] }, + { "category": "travel_and_places", "char": "🚜", "name": "tractor", "keywords": ["vehicle", "car", "farming", "agriculture"] }, + { "category": "travel_and_places", "char": "🛴", "name": "kick_scooter", "keywords": ["vehicle", "kick", "razor"] }, + { "category": "travel_and_places", "char": "🏍", "name": "motorcycle", "keywords": ["race", "sports", "fast"] }, + { "category": "travel_and_places", "char": "🚲", "name": "bike", "keywords": ["sports", "bicycle", "exercise", "hipster"] }, + { "category": "travel_and_places", "char": "🛵", "name": "motor_scooter", "keywords": ["vehicle", "vespa", "sasha"] }, + { "category": "travel_and_places", "char": "🦽", "name": "manual_wheelchair", "keywords": ["vehicle"] }, + { "category": "travel_and_places", "char": "🦼", "name": "motorized_wheelchair", "keywords": ["vehicle"] }, + { "category": "travel_and_places", "char": "🛺", "name": "auto_rickshaw", "keywords": ["vehicle"] }, + { "category": "travel_and_places", "char": "🪂", "name": "parachute", "keywords": ["vehicle"] }, + { "category": "travel_and_places", "char": "🚨", "name": "rotating_light", "keywords": ["police", "ambulance", "911", "emergency", "alert", "error", "pinged", "law", "legal"] }, + { "category": "travel_and_places", "char": "🚔", "name": "oncoming_police_car", "keywords": ["vehicle", "law", "legal", "enforcement", "911"] }, + { "category": "travel_and_places", "char": "🚍", "name": "oncoming_bus", "keywords": ["vehicle", "transportation"] }, + { "category": "travel_and_places", "char": "🚘", "name": "oncoming_automobile", "keywords": ["car", "vehicle", "transportation"] }, + { "category": "travel_and_places", "char": "🚖", "name": "oncoming_taxi", "keywords": ["vehicle", "cars", "uber"] }, + { "category": "travel_and_places", "char": "🚡", "name": "aerial_tramway", "keywords": ["transportation", "vehicle", "ski"] }, + { "category": "travel_and_places", "char": "🚠", "name": "mountain_cableway", "keywords": ["transportation", "vehicle", "ski"] }, + { "category": "travel_and_places", "char": "🚟", "name": "suspension_railway", "keywords": ["vehicle", "transportation"] }, + { "category": "travel_and_places", "char": "🚃", "name": "railway_car", "keywords": ["transportation", "vehicle", "train"] }, + { "category": "travel_and_places", "char": "🚋", "name": "train", "keywords": ["transportation", "vehicle", "carriage", "public", "travel"] }, + { "category": "travel_and_places", "char": "🚝", "name": "monorail", "keywords": ["transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🚄", "name": "bullettrain_side", "keywords": ["transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🚅", "name": "bullettrain_front", "keywords": ["transportation", "vehicle", "speed", "fast", "public", "travel"] }, + { "category": "travel_and_places", "char": "🚈", "name": "light_rail", "keywords": ["transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🚞", "name": "mountain_railway", "keywords": ["transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🚂", "name": "steam_locomotive", "keywords": ["transportation", "vehicle", "train"] }, + { "category": "travel_and_places", "char": "🚆", "name": "train2", "keywords": ["transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🚇", "name": "metro", "keywords": ["transportation", "blue-square", "mrt", "underground", "tube"] }, + { "category": "travel_and_places", "char": "🚊", "name": "tram", "keywords": ["transportation", "vehicle"] }, + { "category": "travel_and_places", "char": "🚉", "name": "station", "keywords": ["transportation", "vehicle", "public"] }, + { "category": "travel_and_places", "char": "🛸", "name": "flying_saucer", "keywords": ["transportation", "vehicle", "ufo"] }, + { "category": "travel_and_places", "char": "🚁", "name": "helicopter", "keywords": ["transportation", "vehicle", "fly"] }, + { "category": "travel_and_places", "char": "🛩", "name": "small_airplane", "keywords": ["flight", "transportation", "fly", "vehicle"] }, + { "category": "travel_and_places", "char": "✈️", "name": "airplane", "keywords": ["vehicle", "transportation", "flight", "fly"] }, + { "category": "travel_and_places", "char": "🛫", "name": "flight_departure", "keywords": ["airport", "flight", "landing"] }, + { "category": "travel_and_places", "char": "🛬", "name": "flight_arrival", "keywords": ["airport", "flight", "boarding"] }, + { "category": "travel_and_places", "char": "⛵", "name": "sailboat", "keywords": ["ship", "summer", "transportation", "water", "sailing"] }, + { "category": "travel_and_places", "char": "🛥", "name": "motor_boat", "keywords": ["ship"] }, + { "category": "travel_and_places", "char": "🚤", "name": "speedboat", "keywords": ["ship", "transportation", "vehicle", "summer"] }, + { "category": "travel_and_places", "char": "⛴", "name": "ferry", "keywords": ["boat", "ship", "yacht"] }, + { "category": "travel_and_places", "char": "🛳", "name": "passenger_ship", "keywords": ["yacht", "cruise", "ferry"] }, + { "category": "travel_and_places", "char": "🚀", "name": "rocket", "keywords": ["launch", "ship", "staffmode", "NASA", "outer space", "outer_space", "fly"] }, + { "category": "travel_and_places", "char": "🛰", "name": "artificial_satellite", "keywords": ["communication", "gps", "orbit", "spaceflight", "NASA", "ISS"] }, + { "category": "travel_and_places", "char": "🛻", "name": "pickup_truck", "keywords": ["car"] }, + { "category": "travel_and_places", "char": "🛼", "name": "roller_skate", "keywords": [] }, + { "category": "travel_and_places", "char": "💺", "name": "seat", "keywords": ["sit", "airplane", "transport", "bus", "flight", "fly"] }, + { "category": "travel_and_places", "char": "🛶", "name": "canoe", "keywords": ["boat", "paddle", "water", "ship"] }, + { "category": "travel_and_places", "char": "⚓", "name": "anchor", "keywords": ["ship", "ferry", "sea", "boat"] }, + { "category": "travel_and_places", "char": "🚧", "name": "construction", "keywords": ["wip", "progress", "caution", "warning"] }, + { "category": "travel_and_places", "char": "⛽", "name": "fuelpump", "keywords": ["gas station", "petroleum"] }, + { "category": "travel_and_places", "char": "🚏", "name": "busstop", "keywords": ["transportation", "wait"] }, + { "category": "travel_and_places", "char": "🚦", "name": "vertical_traffic_light", "keywords": ["transportation", "driving"] }, + { "category": "travel_and_places", "char": "🚥", "name": "traffic_light", "keywords": ["transportation", "signal"] }, + { "category": "travel_and_places", "char": "🏁", "name": "checkered_flag", "keywords": ["contest", "finishline", "race", "gokart"] }, + { "category": "travel_and_places", "char": "🚢", "name": "ship", "keywords": ["transportation", "titanic", "deploy"] }, + { "category": "travel_and_places", "char": "🎡", "name": "ferris_wheel", "keywords": ["photo", "carnival", "londoneye"] }, + { "category": "travel_and_places", "char": "🎢", "name": "roller_coaster", "keywords": ["carnival", "playground", "photo", "fun"] }, + { "category": "travel_and_places", "char": "🎠", "name": "carousel_horse", "keywords": ["photo", "carnival"] }, + { "category": "travel_and_places", "char": "🏗", "name": "building_construction", "keywords": ["wip", "working", "progress"] }, + { "category": "travel_and_places", "char": "🌁", "name": "foggy", "keywords": ["photo", "mountain"] }, + { "category": "travel_and_places", "char": "🏭", "name": "factory", "keywords": ["building", "industry", "pollution", "smoke"] }, + { "category": "travel_and_places", "char": "⛲", "name": "fountain", "keywords": ["photo", "summer", "water", "fresh"] }, + { "category": "travel_and_places", "char": "🎑", "name": "rice_scene", "keywords": ["photo", "japan", "asia", "tsukimi"] }, + { "category": "travel_and_places", "char": "⛰", "name": "mountain", "keywords": ["photo", "nature", "environment"] }, + { "category": "travel_and_places", "char": "🏔", "name": "mountain_snow", "keywords": ["photo", "nature", "environment", "winter", "cold"] }, + { "category": "travel_and_places", "char": "🗻", "name": "mount_fuji", "keywords": ["photo", "mountain", "nature", "japanese"] }, + { "category": "travel_and_places", "char": "🌋", "name": "volcano", "keywords": ["photo", "nature", "disaster"] }, + { "category": "travel_and_places", "char": "🗾", "name": "japan", "keywords": ["nation", "country", "japanese", "asia"] }, + { "category": "travel_and_places", "char": "🏕", "name": "camping", "keywords": ["photo", "outdoors", "tent"] }, + { "category": "travel_and_places", "char": "⛺", "name": "tent", "keywords": ["photo", "camping", "outdoors"] }, + { "category": "travel_and_places", "char": "🏞", "name": "national_park", "keywords": ["photo", "environment", "nature"] }, + { "category": "travel_and_places", "char": "🛣", "name": "motorway", "keywords": ["road", "cupertino", "interstate", "highway"] }, + { "category": "travel_and_places", "char": "🛤", "name": "railway_track", "keywords": ["train", "transportation"] }, + { "category": "travel_and_places", "char": "🌅", "name": "sunrise", "keywords": ["morning", "view", "vacation", "photo"] }, + { "category": "travel_and_places", "char": "🌄", "name": "sunrise_over_mountains", "keywords": ["view", "vacation", "photo"] }, + { "category": "travel_and_places", "char": "🏜", "name": "desert", "keywords": ["photo", "warm", "saharah"] }, + { "category": "travel_and_places", "char": "🏖", "name": "beach_umbrella", "keywords": ["weather", "summer", "sunny", "sand", "mojito"] }, + { "category": "travel_and_places", "char": "🏝", "name": "desert_island", "keywords": ["photo", "tropical", "mojito"] }, + { "category": "travel_and_places", "char": "🌇", "name": "city_sunrise", "keywords": ["photo", "good morning", "dawn"] }, + { "category": "travel_and_places", "char": "🌆", "name": "city_sunset", "keywords": ["photo", "evening", "sky", "buildings"] }, + { "category": "travel_and_places", "char": "🏙", "name": "cityscape", "keywords": ["photo", "night life", "urban"] }, + { "category": "travel_and_places", "char": "🌃", "name": "night_with_stars", "keywords": ["evening", "city", "downtown"] }, + { "category": "travel_and_places", "char": "🌉", "name": "bridge_at_night", "keywords": ["photo", "sanfrancisco"] }, + { "category": "travel_and_places", "char": "🌌", "name": "milky_way", "keywords": ["photo", "space", "stars"] }, + { "category": "travel_and_places", "char": "🌠", "name": "stars", "keywords": ["night", "photo"] }, + { "category": "travel_and_places", "char": "🎇", "name": "sparkler", "keywords": ["stars", "night", "shine"] }, + { "category": "travel_and_places", "char": "🎆", "name": "fireworks", "keywords": ["photo", "festival", "carnival", "congratulations"] }, + { "category": "travel_and_places", "char": "🌈", "name": "rainbow", "keywords": ["nature", "happy", "unicorn_face", "photo", "sky", "spring"] }, + { "category": "travel_and_places", "char": "🏘", "name": "houses", "keywords": ["buildings", "photo"] }, + { "category": "travel_and_places", "char": "🏰", "name": "european_castle", "keywords": ["building", "royalty", "history"] }, + { "category": "travel_and_places", "char": "🏯", "name": "japanese_castle", "keywords": ["photo", "building"] }, + { "category": "travel_and_places", "char": "🗼", "name": "tokyo_tower", "keywords": ["photo", "japanese"] }, + { "category": "travel_and_places", "char": "", "name": "shibuya_109", "keywords": ["photo", "japanese"] }, + { "category": "travel_and_places", "char": "🏟", "name": "stadium", "keywords": ["photo", "place", "sports", "concert", "venue"] }, + { "category": "travel_and_places", "char": "🗽", "name": "statue_of_liberty", "keywords": ["american", "newyork"] }, + { "category": "travel_and_places", "char": "🏠", "name": "house", "keywords": ["building", "home"] }, + { "category": "travel_and_places", "char": "🏡", "name": "house_with_garden", "keywords": ["home", "plant", "nature"] }, + { "category": "travel_and_places", "char": "🏚", "name": "derelict_house", "keywords": ["abandon", "evict", "broken", "building"] }, + { "category": "travel_and_places", "char": "🏢", "name": "office", "keywords": ["building", "bureau", "work"] }, + { "category": "travel_and_places", "char": "🏬", "name": "department_store", "keywords": ["building", "shopping", "mall"] }, + { "category": "travel_and_places", "char": "🏣", "name": "post_office", "keywords": ["building", "envelope", "communication"] }, + { "category": "travel_and_places", "char": "🏤", "name": "european_post_office", "keywords": ["building", "email"] }, + { "category": "travel_and_places", "char": "🏥", "name": "hospital", "keywords": ["building", "health", "surgery", "doctor"] }, + { "category": "travel_and_places", "char": "🏦", "name": "bank", "keywords": ["building", "money", "sales", "cash", "business", "enterprise"] }, + { "category": "travel_and_places", "char": "🏨", "name": "hotel", "keywords": ["building", "accomodation", "checkin"] }, + { "category": "travel_and_places", "char": "🏪", "name": "convenience_store", "keywords": ["building", "shopping", "groceries"] }, + { "category": "travel_and_places", "char": "🏫", "name": "school", "keywords": ["building", "student", "education", "learn", "teach"] }, + { "category": "travel_and_places", "char": "🏩", "name": "love_hotel", "keywords": ["like", "affection", "dating"] }, + { "category": "travel_and_places", "char": "💒", "name": "wedding", "keywords": ["love", "like", "affection", "couple", "marriage", "bride", "groom"] }, + { "category": "travel_and_places", "char": "🏛", "name": "classical_building", "keywords": ["art", "culture", "history"] }, + { "category": "travel_and_places", "char": "⛪", "name": "church", "keywords": ["building", "religion", "christ"] }, + { "category": "travel_and_places", "char": "🕌", "name": "mosque", "keywords": ["islam", "worship", "minaret"] }, + { "category": "travel_and_places", "char": "🕍", "name": "synagogue", "keywords": ["judaism", "worship", "temple", "jewish"] }, + { "category": "travel_and_places", "char": "🕋", "name": "kaaba", "keywords": ["mecca", "mosque", "islam"] }, + { "category": "travel_and_places", "char": "⛩", "name": "shinto_shrine", "keywords": ["temple", "japan", "kyoto"] }, + { "category": "travel_and_places", "char": "🛕", "name": "hindu_temple", "keywords": ["temple"] }, + { "category": "travel_and_places", "char": "🪨", "name": "rock", "keywords": [] }, + { "category": "travel_and_places", "char": "🪵", "name": "wood", "keywords": [] }, + { "category": "travel_and_places", "char": "🛖", "name": "hut", "keywords": [] }, + { "category": "travel_and_places", "char": "\uD83D\uDEDD", "name": "playground_slide", "keywords": [] }, + { "category": "travel_and_places", "char": "\uD83D\uDEDE", "name": "wheel", "keywords": [] }, + { "category": "travel_and_places", "char": "\uD83D\uDEDF", "name": "ring_buoy", "keywords": [] }, + { "category": "objects", "char": "⌚", "name": "watch", "keywords": ["time", "accessories"] }, + { "category": "objects", "char": "📱", "name": "iphone", "keywords": ["technology", "apple", "gadgets", "dial"] }, + { "category": "objects", "char": "📲", "name": "calling", "keywords": ["iphone", "incoming"] }, + { "category": "objects", "char": "💻", "name": "computer", "keywords": ["technology", "laptop", "screen", "display", "monitor"] }, + { "category": "objects", "char": "⌨", "name": "keyboard", "keywords": ["technology", "computer", "type", "input", "text"] }, + { "category": "objects", "char": "🖥", "name": "desktop_computer", "keywords": ["technology", "computing", "screen"] }, + { "category": "objects", "char": "🖨", "name": "printer", "keywords": ["paper", "ink"] }, + { "category": "objects", "char": "🖱", "name": "computer_mouse", "keywords": ["click"] }, + { "category": "objects", "char": "🖲", "name": "trackball", "keywords": ["technology", "trackpad"] }, + { "category": "objects", "char": "🕹", "name": "joystick", "keywords": ["game", "play"] }, + { "category": "objects", "char": "🗜", "name": "clamp", "keywords": ["tool"] }, + { "category": "objects", "char": "💽", "name": "minidisc", "keywords": ["technology", "record", "data", "disk", "90s"] }, + { "category": "objects", "char": "💾", "name": "floppy_disk", "keywords": ["oldschool", "technology", "save", "90s", "80s"] }, + { "category": "objects", "char": "💿", "name": "cd", "keywords": ["technology", "dvd", "disk", "disc", "90s"] }, + { "category": "objects", "char": "📀", "name": "dvd", "keywords": ["cd", "disk", "disc"] }, + { "category": "objects", "char": "📼", "name": "vhs", "keywords": ["record", "video", "oldschool", "90s", "80s"] }, + { "category": "objects", "char": "📷", "name": "camera", "keywords": ["gadgets", "photography"] }, + { "category": "objects", "char": "📸", "name": "camera_flash", "keywords": ["photography", "gadgets"] }, + { "category": "objects", "char": "📹", "name": "video_camera", "keywords": ["film", "record"] }, + { "category": "objects", "char": "🎥", "name": "movie_camera", "keywords": ["film", "record"] }, + { "category": "objects", "char": "📽", "name": "film_projector", "keywords": ["video", "tape", "record", "movie"] }, + { "category": "objects", "char": "🎞", "name": "film_strip", "keywords": ["movie"] }, + { "category": "objects", "char": "📞", "name": "telephone_receiver", "keywords": ["technology", "communication", "dial"] }, + { "category": "objects", "char": "☎️", "name": "phone", "keywords": ["technology", "communication", "dial", "telephone"] }, + { "category": "objects", "char": "📟", "name": "pager", "keywords": ["bbcall", "oldschool", "90s"] }, + { "category": "objects", "char": "📠", "name": "fax", "keywords": ["communication", "technology"] }, + { "category": "objects", "char": "📺", "name": "tv", "keywords": ["technology", "program", "oldschool", "show", "television"] }, + { "category": "objects", "char": "📻", "name": "radio", "keywords": ["communication", "music", "podcast", "program"] }, + { "category": "objects", "char": "🎙", "name": "studio_microphone", "keywords": ["sing", "recording", "artist", "talkshow"] }, + { "category": "objects", "char": "🎚", "name": "level_slider", "keywords": ["scale"] }, + { "category": "objects", "char": "🎛", "name": "control_knobs", "keywords": ["dial"] }, + { "category": "objects", "char": "🧭", "name": "compass", "keywords": ["magnetic", "navigation", "orienteering"] }, + { "category": "objects", "char": "⏱", "name": "stopwatch", "keywords": ["time", "deadline"] }, + { "category": "objects", "char": "⏲", "name": "timer_clock", "keywords": ["alarm"] }, + { "category": "objects", "char": "⏰", "name": "alarm_clock", "keywords": ["time", "wake"] }, + { "category": "objects", "char": "🕰", "name": "mantelpiece_clock", "keywords": ["time"] }, + { "category": "objects", "char": "⏳", "name": "hourglass_flowing_sand", "keywords": ["oldschool", "time", "countdown"] }, + { "category": "objects", "char": "⌛", "name": "hourglass", "keywords": ["time", "clock", "oldschool", "limit", "exam", "quiz", "test"] }, + { "category": "objects", "char": "📡", "name": "satellite", "keywords": ["communication", "future", "radio", "space"] }, + { "category": "objects", "char": "🔋", "name": "battery", "keywords": ["power", "energy", "sustain"] }, + { "category": "objects", "char": "\uD83E\uDEAB", "name": "battery", "keywords": [] }, + { "category": "objects", "char": "🔌", "name": "electric_plug", "keywords": ["charger", "power"] }, + { "category": "objects", "char": "💡", "name": "bulb", "keywords": ["light", "electricity", "idea"] }, + { "category": "objects", "char": "🔦", "name": "flashlight", "keywords": ["dark", "camping", "sight", "night"] }, + { "category": "objects", "char": "🕯", "name": "candle", "keywords": ["fire", "wax"] }, + { "category": "objects", "char": "🧯", "name": "fire_extinguisher", "keywords": ["quench"] }, + { "category": "objects", "char": "🗑", "name": "wastebasket", "keywords": ["bin", "trash", "rubbish", "garbage", "toss"] }, + { "category": "objects", "char": "🛢", "name": "oil_drum", "keywords": ["barrell"] }, + { "category": "objects", "char": "💸", "name": "money_with_wings", "keywords": ["dollar", "bills", "payment", "sale"] }, + { "category": "objects", "char": "💵", "name": "dollar", "keywords": ["money", "sales", "bill", "currency"] }, + { "category": "objects", "char": "💴", "name": "yen", "keywords": ["money", "sales", "japanese", "dollar", "currency"] }, + { "category": "objects", "char": "💶", "name": "euro", "keywords": ["money", "sales", "dollar", "currency"] }, + { "category": "objects", "char": "💷", "name": "pound", "keywords": ["british", "sterling", "money", "sales", "bills", "uk", "england", "currency"] }, + { "category": "objects", "char": "💰", "name": "moneybag", "keywords": ["dollar", "payment", "coins", "sale"] }, + { "category": "objects", "char": "🪙", "name": "coin", "keywords": ["dollar", "payment", "coins", "sale"] }, + { "category": "objects", "char": "💳", "name": "credit_card", "keywords": ["money", "sales", "dollar", "bill", "payment", "shopping"] }, + { "category": "objects", "char": "\uD83E\uDEAB", "name": "identification_card", "keywords": [] }, + { "category": "objects", "char": "💎", "name": "gem", "keywords": ["blue", "ruby", "diamond", "jewelry"] }, + { "category": "objects", "char": "⚖", "name": "balance_scale", "keywords": ["law", "fairness", "weight"] }, + { "category": "objects", "char": "🧰", "name": "toolbox", "keywords": ["tools", "diy", "fix", "maintainer", "mechanic"] }, + { "category": "objects", "char": "🔧", "name": "wrench", "keywords": ["tools", "diy", "ikea", "fix", "maintainer"] }, + { "category": "objects", "char": "🔨", "name": "hammer", "keywords": ["tools", "build", "create"] }, + { "category": "objects", "char": "⚒", "name": "hammer_and_pick", "keywords": ["tools", "build", "create"] }, + { "category": "objects", "char": "🛠", "name": "hammer_and_wrench", "keywords": ["tools", "build", "create"] }, + { "category": "objects", "char": "⛏", "name": "pick", "keywords": ["tools", "dig"] }, + { "category": "objects", "char": "🪓", "name": "axe", "keywords": ["tools"] }, + { "category": "objects", "char": "🦯", "name": "probing_cane", "keywords": ["tools"] }, + { "category": "objects", "char": "🔩", "name": "nut_and_bolt", "keywords": ["handy", "tools", "fix"] }, + { "category": "objects", "char": "⚙", "name": "gear", "keywords": ["cog"] }, + { "category": "objects", "char": "🪃", "name": "boomerang", "keywords": ["tool"] }, + { "category": "objects", "char": "🪚", "name": "carpentry_saw", "keywords": ["tool"] }, + { "category": "objects", "char": "🪛", "name": "screwdriver", "keywords": ["tool"] }, + { "category": "objects", "char": "🪝", "name": "hook", "keywords": ["tool"] }, + { "category": "objects", "char": "🪜", "name": "ladder", "keywords": ["tool"] }, + { "category": "objects", "char": "🧱", "name": "brick", "keywords": ["bricks"] }, + { "category": "objects", "char": "⛓", "name": "chains", "keywords": ["lock", "arrest"] }, + { "category": "objects", "char": "🧲", "name": "magnet", "keywords": ["attraction", "magnetic"] }, + { "category": "objects", "char": "🔫", "name": "gun", "keywords": ["violence", "weapon", "pistol", "revolver"] }, + { "category": "objects", "char": "💣", "name": "bomb", "keywords": ["boom", "explode", "explosion", "terrorism"] }, + { "category": "objects", "char": "🧨", "name": "firecracker", "keywords": ["dynamite", "boom", "explode", "explosion", "explosive"] }, + { "category": "objects", "char": "🔪", "name": "hocho", "keywords": ["knife", "blade", "cutlery", "kitchen", "weapon"] }, + { "category": "objects", "char": "🗡", "name": "dagger", "keywords": ["weapon"] }, + { "category": "objects", "char": "⚔", "name": "crossed_swords", "keywords": ["weapon"] }, + { "category": "objects", "char": "🛡", "name": "shield", "keywords": ["protection", "security"] }, + { "category": "objects", "char": "🚬", "name": "smoking", "keywords": ["kills", "tobacco", "cigarette", "joint", "smoke"] }, + { "category": "objects", "char": "☠", "name": "skull_and_crossbones", "keywords": ["poison", "danger", "deadly", "scary", "death", "pirate", "evil"] }, + { "category": "objects", "char": "⚰", "name": "coffin", "keywords": ["vampire", "dead", "die", "death", "rip", "graveyard", "cemetery", "casket", "funeral", "box"] }, + { "category": "objects", "char": "⚱", "name": "funeral_urn", "keywords": ["dead", "die", "death", "rip", "ashes"] }, + { "category": "objects", "char": "🏺", "name": "amphora", "keywords": ["vase", "jar"] }, + { "category": "objects", "char": "🔮", "name": "crystal_ball", "keywords": ["disco", "party", "magic", "circus", "fortune_teller"] }, + { "category": "objects", "char": "📿", "name": "prayer_beads", "keywords": ["dhikr", "religious"] }, + { "category": "objects", "char": "🧿", "name": "nazar_amulet", "keywords": ["bead", "charm"] }, + { "category": "objects", "char": "💈", "name": "barber", "keywords": ["hair", "salon", "style"] }, + { "category": "objects", "char": "⚗", "name": "alembic", "keywords": ["distilling", "science", "experiment", "chemistry"] }, + { "category": "objects", "char": "🔭", "name": "telescope", "keywords": ["stars", "space", "zoom", "science", "astronomy"] }, + { "category": "objects", "char": "🔬", "name": "microscope", "keywords": ["laboratory", "experiment", "zoomin", "science", "study"] }, + { "category": "objects", "char": "🕳", "name": "hole", "keywords": ["embarrassing"] }, + { "category": "objects", "char": "💊", "name": "pill", "keywords": ["health", "medicine", "doctor", "pharmacy", "drug"] }, + { "category": "objects", "char": "💉", "name": "syringe", "keywords": ["health", "hospital", "drugs", "blood", "medicine", "needle", "doctor", "nurse"] }, + { "category": "objects", "char": "🩸", "name": "drop_of_blood", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, + { "category": "objects", "char": "🩹", "name": "adhesive_bandage", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, + { "category": "objects", "char": "🩺", "name": "stethoscope", "keywords": ["health", "hospital", "medicine", "needle", "doctor", "nurse"] }, + { "category": "objects", "char": "🪒", "name": "razor", "keywords": ["health"] }, + { "category": "objects", "char": "\uD83E\uDE7B", "name": "xray", "keywords": [] }, + { "category": "objects", "char": "\uD83E\uDE7C", "name": "crutch", "keywords": [] }, + { "category": "objects", "char": "🧬", "name": "dna", "keywords": ["biologist", "genetics", "life"] }, + { "category": "objects", "char": "🧫", "name": "petri_dish", "keywords": ["bacteria", "biology", "culture", "lab"] }, + { "category": "objects", "char": "🧪", "name": "test_tube", "keywords": ["chemistry", "experiment", "lab", "science"] }, + { "category": "objects", "char": "🌡", "name": "thermometer", "keywords": ["weather", "temperature", "hot", "cold"] }, + { "category": "objects", "char": "🧹", "name": "broom", "keywords": ["cleaning", "sweeping", "witch"] }, + { "category": "objects", "char": "🧺", "name": "basket", "keywords": ["laundry"] }, + { "category": "objects", "char": "🧻", "name": "toilet_paper", "keywords": ["roll"] }, + { "category": "objects", "char": "🏷", "name": "label", "keywords": ["sale", "tag"] }, + { "category": "objects", "char": "🔖", "name": "bookmark", "keywords": ["favorite", "label", "save"] }, + { "category": "objects", "char": "🚽", "name": "toilet", "keywords": ["restroom", "wc", "washroom", "bathroom", "potty"] }, + { "category": "objects", "char": "🚿", "name": "shower", "keywords": ["clean", "water", "bathroom"] }, + { "category": "objects", "char": "🛁", "name": "bathtub", "keywords": ["clean", "shower", "bathroom"] }, + { "category": "objects", "char": "🧼", "name": "soap", "keywords": ["bar", "bathing", "cleaning", "lather"] }, + { "category": "objects", "char": "🧽", "name": "sponge", "keywords": ["absorbing", "cleaning", "porous"] }, + { "category": "objects", "char": "🧴", "name": "lotion_bottle", "keywords": ["moisturizer", "sunscreen"] }, + { "category": "objects", "char": "🔑", "name": "key", "keywords": ["lock", "door", "password"] }, + { "category": "objects", "char": "🗝", "name": "old_key", "keywords": ["lock", "door", "password"] }, + { "category": "objects", "char": "🛋", "name": "couch_and_lamp", "keywords": ["read", "chill"] }, + { "category": "objects", "char": "🪔", "name": "diya_Lamp", "keywords": ["light", "oil"] }, + { "category": "objects", "char": "🛌", "name": "sleeping_bed", "keywords": ["bed", "rest"] }, + { "category": "objects", "char": "🛏", "name": "bed", "keywords": ["sleep", "rest"] }, + { "category": "objects", "char": "🚪", "name": "door", "keywords": ["house", "entry", "exit"] }, + { "category": "objects", "char": "🪑", "name": "chair", "keywords": ["house", "desk"] }, + { "category": "objects", "char": "🛎", "name": "bellhop_bell", "keywords": ["service"] }, + { "category": "objects", "char": "🧸", "name": "teddy_bear", "keywords": ["plush", "stuffed"] }, + { "category": "objects", "char": "🖼", "name": "framed_picture", "keywords": ["photography"] }, + { "category": "objects", "char": "🗺", "name": "world_map", "keywords": ["location", "direction"] }, + { "category": "objects", "char": "🛗", "name": "elevator", "keywords": ["household"] }, + { "category": "objects", "char": "🪞", "name": "mirror", "keywords": ["household"] }, + { "category": "objects", "char": "🪟", "name": "window", "keywords": ["household"] }, + { "category": "objects", "char": "🪠", "name": "plunger", "keywords": ["household"] }, + { "category": "objects", "char": "🪤", "name": "mouse_trap", "keywords": ["household"] }, + { "category": "objects", "char": "🪣", "name": "bucket", "keywords": ["household"] }, + { "category": "objects", "char": "🪥", "name": "toothbrush", "keywords": ["household"] }, + { "category": "objects", "char": "\uD83E\uDEE7", "name": "bubbles", "keywords": [] }, + { "category": "objects", "char": "⛱", "name": "parasol_on_ground", "keywords": ["weather", "summer"] }, + { "category": "objects", "char": "🗿", "name": "moyai", "keywords": ["rock", "easter island", "moai"] }, + { "category": "objects", "char": "🛍", "name": "shopping", "keywords": ["mall", "buy", "purchase"] }, + { "category": "objects", "char": "🛒", "name": "shopping_cart", "keywords": ["trolley"] }, + { "category": "objects", "char": "🎈", "name": "balloon", "keywords": ["party", "celebration", "birthday", "circus"] }, + { "category": "objects", "char": "🎏", "name": "flags", "keywords": ["fish", "japanese", "koinobori", "carp", "banner"] }, + { "category": "objects", "char": "🎀", "name": "ribbon", "keywords": ["decoration", "pink", "girl", "bowtie"] }, + { "category": "objects", "char": "🎁", "name": "gift", "keywords": ["present", "birthday", "christmas", "xmas"] }, + { "category": "objects", "char": "🎊", "name": "confetti_ball", "keywords": ["festival", "party", "birthday", "circus"] }, + { "category": "objects", "char": "🎉", "name": "tada", "keywords": ["party", "congratulations", "birthday", "magic", "circus", "celebration"] }, + { "category": "objects", "char": "🎎", "name": "dolls", "keywords": ["japanese", "toy", "kimono"] }, + { "category": "objects", "char": "🎐", "name": "wind_chime", "keywords": ["nature", "ding", "spring", "bell"] }, + { "category": "objects", "char": "🎌", "name": "crossed_flags", "keywords": ["japanese", "nation", "country", "border"] }, + { "category": "objects", "char": "🏮", "name": "izakaya_lantern", "keywords": ["light", "paper", "halloween", "spooky"] }, + { "category": "objects", "char": "🧧", "name": "red_envelope", "keywords": ["gift"] }, + { "category": "objects", "char": "✉️", "name": "email", "keywords": ["letter", "postal", "inbox", "communication"] }, + { "category": "objects", "char": "📩", "name": "envelope_with_arrow", "keywords": ["email", "communication"] }, + { "category": "objects", "char": "📨", "name": "incoming_envelope", "keywords": ["email", "inbox"] }, + { "category": "objects", "char": "📧", "name": "e-mail", "keywords": ["communication", "inbox"] }, + { "category": "objects", "char": "💌", "name": "love_letter", "keywords": ["email", "like", "affection", "envelope", "valentines"] }, + { "category": "objects", "char": "📮", "name": "postbox", "keywords": ["email", "letter", "envelope"] }, + { "category": "objects", "char": "📪", "name": "mailbox_closed", "keywords": ["email", "communication", "inbox"] }, + { "category": "objects", "char": "📫", "name": "mailbox", "keywords": ["email", "inbox", "communication"] }, + { "category": "objects", "char": "📬", "name": "mailbox_with_mail", "keywords": ["email", "inbox", "communication"] }, + { "category": "objects", "char": "📭", "name": "mailbox_with_no_mail", "keywords": ["email", "inbox"] }, + { "category": "objects", "char": "📦", "name": "package", "keywords": ["mail", "gift", "cardboard", "box", "moving"] }, + { "category": "objects", "char": "📯", "name": "postal_horn", "keywords": ["instrument", "music"] }, + { "category": "objects", "char": "📥", "name": "inbox_tray", "keywords": ["email", "documents"] }, + { "category": "objects", "char": "📤", "name": "outbox_tray", "keywords": ["inbox", "email"] }, + { "category": "objects", "char": "📜", "name": "scroll", "keywords": ["documents", "ancient", "history", "paper"] }, + { "category": "objects", "char": "📃", "name": "page_with_curl", "keywords": ["documents", "office", "paper"] }, + { "category": "objects", "char": "📑", "name": "bookmark_tabs", "keywords": ["favorite", "save", "order", "tidy"] }, + { "category": "objects", "char": "🧾", "name": "receipt", "keywords": ["accounting", "expenses"] }, + { "category": "objects", "char": "📊", "name": "bar_chart", "keywords": ["graph", "presentation", "stats"] }, + { "category": "objects", "char": "📈", "name": "chart_with_upwards_trend", "keywords": ["graph", "presentation", "stats", "recovery", "business", "economics", "money", "sales", "good", "success"] }, + { "category": "objects", "char": "📉", "name": "chart_with_downwards_trend", "keywords": ["graph", "presentation", "stats", "recession", "business", "economics", "money", "sales", "bad", "failure"] }, + { "category": "objects", "char": "📄", "name": "page_facing_up", "keywords": ["documents", "office", "paper", "information"] }, + { "category": "objects", "char": "📅", "name": "date", "keywords": ["calendar", "schedule"] }, + { "category": "objects", "char": "📆", "name": "calendar", "keywords": ["schedule", "date", "planning"] }, + { "category": "objects", "char": "🗓", "name": "spiral_calendar", "keywords": ["date", "schedule", "planning"] }, + { "category": "objects", "char": "📇", "name": "card_index", "keywords": ["business", "stationery"] }, + { "category": "objects", "char": "🗃", "name": "card_file_box", "keywords": ["business", "stationery"] }, + { "category": "objects", "char": "🗳", "name": "ballot_box", "keywords": ["election", "vote"] }, + { "category": "objects", "char": "🗄", "name": "file_cabinet", "keywords": ["filing", "organizing"] }, + { "category": "objects", "char": "📋", "name": "clipboard", "keywords": ["stationery", "documents"] }, + { "category": "objects", "char": "🗒", "name": "spiral_notepad", "keywords": ["memo", "stationery"] }, + { "category": "objects", "char": "📁", "name": "file_folder", "keywords": ["documents", "business", "office"] }, + { "category": "objects", "char": "📂", "name": "open_file_folder", "keywords": ["documents", "load"] }, + { "category": "objects", "char": "🗂", "name": "card_index_dividers", "keywords": ["organizing", "business", "stationery"] }, + { "category": "objects", "char": "🗞", "name": "newspaper_roll", "keywords": ["press", "headline"] }, + { "category": "objects", "char": "📰", "name": "newspaper", "keywords": ["press", "headline"] }, + { "category": "objects", "char": "📓", "name": "notebook", "keywords": ["stationery", "record", "notes", "paper", "study"] }, + { "category": "objects", "char": "📕", "name": "closed_book", "keywords": ["read", "library", "knowledge", "textbook", "learn"] }, + { "category": "objects", "char": "📗", "name": "green_book", "keywords": ["read", "library", "knowledge", "study"] }, + { "category": "objects", "char": "📘", "name": "blue_book", "keywords": ["read", "library", "knowledge", "learn", "study"] }, + { "category": "objects", "char": "📙", "name": "orange_book", "keywords": ["read", "library", "knowledge", "textbook", "study"] }, + { "category": "objects", "char": "📔", "name": "notebook_with_decorative_cover", "keywords": ["classroom", "notes", "record", "paper", "study"] }, + { "category": "objects", "char": "📒", "name": "ledger", "keywords": ["notes", "paper"] }, + { "category": "objects", "char": "📚", "name": "books", "keywords": ["literature", "library", "study"] }, + { "category": "objects", "char": "📖", "name": "open_book", "keywords": ["book", "read", "library", "knowledge", "literature", "learn", "study"] }, + { "category": "objects", "char": "🧷", "name": "safety_pin", "keywords": ["diaper"] }, + { "category": "objects", "char": "🔗", "name": "link", "keywords": ["rings", "url"] }, + { "category": "objects", "char": "📎", "name": "paperclip", "keywords": ["documents", "stationery"] }, + { "category": "objects", "char": "🖇", "name": "paperclips", "keywords": ["documents", "stationery"] }, + { "category": "objects", "char": "✂️", "name": "scissors", "keywords": ["stationery", "cut"] }, + { "category": "objects", "char": "📐", "name": "triangular_ruler", "keywords": ["stationery", "math", "architect", "sketch"] }, + { "category": "objects", "char": "📏", "name": "straight_ruler", "keywords": ["stationery", "calculate", "length", "math", "school", "drawing", "architect", "sketch"] }, + { "category": "objects", "char": "🧮", "name": "abacus", "keywords": ["calculation"] }, + { "category": "objects", "char": "📌", "name": "pushpin", "keywords": ["stationery", "mark", "here"] }, + { "category": "objects", "char": "📍", "name": "round_pushpin", "keywords": ["stationery", "location", "map", "here"] }, + { "category": "objects", "char": "🚩", "name": "triangular_flag_on_post", "keywords": ["mark", "milestone", "place"] }, + { "category": "objects", "char": "🏳", "name": "white_flag", "keywords": ["losing", "loser", "lost", "surrender", "give up", "fail"] }, + { "category": "objects", "char": "🏴", "name": "black_flag", "keywords": ["pirate"] }, + { "category": "objects", "char": "🏳️‍🌈", "name": "rainbow_flag", "keywords": ["flag", "rainbow", "pride", "gay", "lgbt", "glbt", "queer", "homosexual", "lesbian", "bisexual", "transgender"] }, + { "category": "objects", "char": "🏳️‍⚧️", "name": "transgender_flag", "keywords": ["flag", "transgender"] }, + { "category": "objects", "char": "🔐", "name": "closed_lock_with_key", "keywords": ["security", "privacy"] }, + { "category": "objects", "char": "🔒", "name": "lock", "keywords": ["security", "password", "padlock"] }, + { "category": "objects", "char": "🔓", "name": "unlock", "keywords": ["privacy", "security"] }, + { "category": "objects", "char": "🔏", "name": "lock_with_ink_pen", "keywords": ["security", "secret"] }, + { "category": "objects", "char": "🖊", "name": "pen", "keywords": ["stationery", "writing", "write"] }, + { "category": "objects", "char": "🖋", "name": "fountain_pen", "keywords": ["stationery", "writing", "write"] }, + { "category": "objects", "char": "✒️", "name": "black_nib", "keywords": ["pen", "stationery", "writing", "write"] }, + { "category": "objects", "char": "📝", "name": "memo", "keywords": ["write", "documents", "stationery", "pencil", "paper", "writing", "legal", "exam", "quiz", "test", "study", "compose"] }, + { "category": "objects", "char": "✏️", "name": "pencil2", "keywords": ["stationery", "write", "paper", "writing", "school", "study"] }, + { "category": "objects", "char": "🖍", "name": "crayon", "keywords": ["drawing", "creativity"] }, + { "category": "objects", "char": "🖌", "name": "paintbrush", "keywords": ["drawing", "creativity", "art"] }, + { "category": "objects", "char": "🔍", "name": "mag", "keywords": ["search", "zoom", "find", "detective"] }, + { "category": "objects", "char": "🔎", "name": "mag_right", "keywords": ["search", "zoom", "find", "detective"] }, + { "category": "objects", "char": "🪦", "name": "headstone", "keywords": [] }, + { "category": "objects", "char": "🪧", "name": "placard", "keywords": [] }, + { "category": "symbols", "char": "💯", "name": "100", "keywords": ["score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass", "hundred"] }, + { "category": "symbols", "char": "🔢", "name": "1234", "keywords": ["numbers", "blue-square"] }, + { "category": "symbols", "char": "❤️", "name": "heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "🧡", "name": "orange_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "💛", "name": "yellow_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "💚", "name": "green_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "💙", "name": "blue_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "💜", "name": "purple_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "🤎", "name": "brown_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "🖤", "name": "black_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "🤍", "name": "white_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "💔", "name": "broken_heart", "keywords": ["sad", "sorry", "break", "heart", "heartbreak"] }, + { "category": "symbols", "char": "❣", "name": "heavy_heart_exclamation", "keywords": ["decoration", "love"] }, + { "category": "symbols", "char": "💕", "name": "two_hearts", "keywords": ["love", "like", "affection", "valentines", "heart"] }, + { "category": "symbols", "char": "💞", "name": "revolving_hearts", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "💓", "name": "heartbeat", "keywords": ["love", "like", "affection", "valentines", "pink", "heart"] }, + { "category": "symbols", "char": "💗", "name": "heartpulse", "keywords": ["like", "love", "affection", "valentines", "pink"] }, + { "category": "symbols", "char": "💖", "name": "sparkling_heart", "keywords": ["love", "like", "affection", "valentines"] }, + { "category": "symbols", "char": "💘", "name": "cupid", "keywords": ["love", "like", "heart", "affection", "valentines"] }, + { "category": "symbols", "char": "💝", "name": "gift_heart", "keywords": ["love", "valentines"] }, + { "category": "symbols", "char": "💟", "name": "heart_decoration", "keywords": ["purple-square", "love", "like"] }, + { "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83D\uDD25", "name": "heart_on_fire", "keywords": [] }, + { "category": "symbols", "char": "\u2764\uFE0F\u200D\uD83E\uDE79", "name": "mending_heart", "keywords": [] }, + { "category": "symbols", "char": "☮", "name": "peace_symbol", "keywords": ["hippie"] }, + { "category": "symbols", "char": "✝", "name": "latin_cross", "keywords": ["christianity"] }, + { "category": "symbols", "char": "☪", "name": "star_and_crescent", "keywords": ["islam"] }, + { "category": "symbols", "char": "🕉", "name": "om", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] }, + { "category": "symbols", "char": "☸", "name": "wheel_of_dharma", "keywords": ["hinduism", "buddhism", "sikhism", "jainism"] }, + { "category": "symbols", "char": "✡", "name": "star_of_david", "keywords": ["judaism"] }, + { "category": "symbols", "char": "🔯", "name": "six_pointed_star", "keywords": ["purple-square", "religion", "jewish", "hexagram"] }, + { "category": "symbols", "char": "🕎", "name": "menorah", "keywords": ["hanukkah", "candles", "jewish"] }, + { "category": "symbols", "char": "☯", "name": "yin_yang", "keywords": ["balance"] }, + { "category": "symbols", "char": "☦", "name": "orthodox_cross", "keywords": ["suppedaneum", "religion"] }, + { "category": "symbols", "char": "🛐", "name": "place_of_worship", "keywords": ["religion", "church", "temple", "prayer"] }, + { "category": "symbols", "char": "⛎", "name": "ophiuchus", "keywords": ["sign", "purple-square", "constellation", "astrology"] }, + { "category": "symbols", "char": "♈", "name": "aries", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, + { "category": "symbols", "char": "♉", "name": "taurus", "keywords": ["purple-square", "sign", "zodiac", "astrology"] }, + { "category": "symbols", "char": "♊", "name": "gemini", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, + { "category": "symbols", "char": "♋", "name": "cancer", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, + { "category": "symbols", "char": "♌", "name": "leo", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, + { "category": "symbols", "char": "♍", "name": "virgo", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, + { "category": "symbols", "char": "♎", "name": "libra", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, + { "category": "symbols", "char": "♏", "name": "scorpius", "keywords": ["sign", "zodiac", "purple-square", "astrology", "scorpio"] }, + { "category": "symbols", "char": "♐", "name": "sagittarius", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, + { "category": "symbols", "char": "♑", "name": "capricorn", "keywords": ["sign", "zodiac", "purple-square", "astrology"] }, + { "category": "symbols", "char": "♒", "name": "aquarius", "keywords": ["sign", "purple-square", "zodiac", "astrology"] }, + { "category": "symbols", "char": "♓", "name": "pisces", "keywords": ["purple-square", "sign", "zodiac", "astrology"] }, + { "category": "symbols", "char": "🆔", "name": "id", "keywords": ["purple-square", "words"] }, + { "category": "symbols", "char": "⚛", "name": "atom_symbol", "keywords": ["science", "physics", "chemistry"] }, + { "category": "symbols", "char": "⚧️", "name": "transgender_symbol", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] }, + { "category": "symbols", "char": "🈳", "name": "u7a7a", "keywords": ["kanji", "japanese", "chinese", "empty", "sky", "blue-square", "aki"] }, + { "category": "symbols", "char": "🈹", "name": "u5272", "keywords": ["cut", "divide", "chinese", "kanji", "pink-square", "waribiki"] }, + { "category": "symbols", "char": "☢", "name": "radioactive", "keywords": ["nuclear", "danger"] }, + { "category": "symbols", "char": "☣", "name": "biohazard", "keywords": ["danger"] }, + { "category": "symbols", "char": "📴", "name": "mobile_phone_off", "keywords": ["mute", "orange-square", "silence", "quiet"] }, + { "category": "symbols", "char": "📳", "name": "vibration_mode", "keywords": ["orange-square", "phone"] }, + { "category": "symbols", "char": "🈶", "name": "u6709", "keywords": ["orange-square", "chinese", "have", "kanji", "ari"] }, + { "category": "symbols", "char": "🈚", "name": "u7121", "keywords": ["nothing", "chinese", "kanji", "japanese", "orange-square", "nashi"] }, + { "category": "symbols", "char": "🈸", "name": "u7533", "keywords": ["chinese", "japanese", "kanji", "orange-square", "moushikomi"] }, + { "category": "symbols", "char": "🈺", "name": "u55b6", "keywords": ["japanese", "opening hours", "orange-square", "eigyo"] }, + { "category": "symbols", "char": "🈷️", "name": "u6708", "keywords": ["chinese", "month", "moon", "japanese", "orange-square", "kanji", "tsuki", "tsukigime", "getsugaku"] }, + { "category": "symbols", "char": "✴️", "name": "eight_pointed_black_star", "keywords": ["orange-square", "shape", "polygon"] }, + { "category": "symbols", "char": "🆚", "name": "vs", "keywords": ["words", "orange-square"] }, + { "category": "symbols", "char": "🉑", "name": "accept", "keywords": ["ok", "good", "chinese", "kanji", "agree", "yes", "orange-circle"] }, + { "category": "symbols", "char": "💮", "name": "white_flower", "keywords": ["japanese", "spring"] }, + { "category": "symbols", "char": "🉐", "name": "ideograph_advantage", "keywords": ["chinese", "kanji", "obtain", "get", "circle"] }, + { "category": "symbols", "char": "㊙️", "name": "secret", "keywords": ["privacy", "chinese", "sshh", "kanji", "red-circle"] }, + { "category": "symbols", "char": "㊗️", "name": "congratulations", "keywords": ["chinese", "kanji", "japanese", "red-circle"] }, + { "category": "symbols", "char": "🈴", "name": "u5408", "keywords": ["japanese", "chinese", "join", "kanji", "red-square", "goukaku", "pass"] }, + { "category": "symbols", "char": "🈵", "name": "u6e80", "keywords": ["full", "chinese", "japanese", "red-square", "kanji", "man"] }, + { "category": "symbols", "char": "🈲", "name": "u7981", "keywords": ["kanji", "japanese", "chinese", "forbidden", "limit", "restricted", "red-square", "kinshi"] }, + { "category": "symbols", "char": "🅰️", "name": "a", "keywords": ["red-square", "alphabet", "letter"] }, + { "category": "symbols", "char": "🅱️", "name": "b", "keywords": ["red-square", "alphabet", "letter"] }, + { "category": "symbols", "char": "🆎", "name": "ab", "keywords": ["red-square", "alphabet"] }, + { "category": "symbols", "char": "🆑", "name": "cl", "keywords": ["alphabet", "words", "red-square"] }, + { "category": "symbols", "char": "🅾️", "name": "o2", "keywords": ["alphabet", "red-square", "letter"] }, + { "category": "symbols", "char": "🆘", "name": "sos", "keywords": ["help", "red-square", "words", "emergency", "911"] }, + { "category": "symbols", "char": "⛔", "name": "no_entry", "keywords": ["limit", "security", "privacy", "bad", "denied", "stop", "circle"] }, + { "category": "symbols", "char": "📛", "name": "name_badge", "keywords": ["fire", "forbid"] }, + { "category": "symbols", "char": "🚫", "name": "no_entry_sign", "keywords": ["forbid", "stop", "limit", "denied", "disallow", "circle"] }, + { "category": "symbols", "char": "❌", "name": "x", "keywords": ["no", "delete", "remove", "cancel", "red"] }, + { "category": "symbols", "char": "⭕", "name": "o", "keywords": ["circle", "round"] }, + { "category": "symbols", "char": "🛑", "name": "stop_sign", "keywords": ["stop"] }, + { "category": "symbols", "char": "💢", "name": "anger", "keywords": ["angry", "mad"] }, + { "category": "symbols", "char": "♨️", "name": "hotsprings", "keywords": ["bath", "warm", "relax"] }, + { "category": "symbols", "char": "🚷", "name": "no_pedestrians", "keywords": ["rules", "crossing", "walking", "circle"] }, + { "category": "symbols", "char": "🚯", "name": "do_not_litter", "keywords": ["trash", "bin", "garbage", "circle"] }, + { "category": "symbols", "char": "🚳", "name": "no_bicycles", "keywords": ["cyclist", "prohibited", "circle"] }, + { "category": "symbols", "char": "🚱", "name": "non-potable_water", "keywords": ["drink", "faucet", "tap", "circle"] }, + { "category": "symbols", "char": "🔞", "name": "underage", "keywords": ["18", "drink", "pub", "night", "minor", "circle"] }, + { "category": "symbols", "char": "📵", "name": "no_mobile_phones", "keywords": ["iphone", "mute", "circle"] }, + { "category": "symbols", "char": "❗", "name": "exclamation", "keywords": ["heavy_exclamation_mark", "danger", "surprise", "punctuation", "wow", "warning"] }, + { "category": "symbols", "char": "❕", "name": "grey_exclamation", "keywords": ["surprise", "punctuation", "gray", "wow", "warning"] }, + { "category": "symbols", "char": "❓", "name": "question", "keywords": ["doubt", "confused"] }, + { "category": "symbols", "char": "❔", "name": "grey_question", "keywords": ["doubts", "gray", "huh", "confused"] }, + { "category": "symbols", "char": "‼️", "name": "bangbang", "keywords": ["exclamation", "surprise"] }, + { "category": "symbols", "char": "⁉️", "name": "interrobang", "keywords": ["wat", "punctuation", "surprise"] }, + { "category": "symbols", "char": "🔅", "name": "low_brightness", "keywords": ["sun", "afternoon", "warm", "summer"] }, + { "category": "symbols", "char": "🔆", "name": "high_brightness", "keywords": ["sun", "light"] }, + { "category": "symbols", "char": "🔱", "name": "trident", "keywords": ["weapon", "spear"] }, + { "category": "symbols", "char": "⚜", "name": "fleur_de_lis", "keywords": ["decorative", "scout"] }, + { "category": "symbols", "char": "〽️", "name": "part_alternation_mark", "keywords": ["graph", "presentation", "stats", "business", "economics", "bad"] }, + { "category": "symbols", "char": "⚠️", "name": "warning", "keywords": ["exclamation", "wip", "alert", "error", "problem", "issue"] }, + { "category": "symbols", "char": "🚸", "name": "children_crossing", "keywords": ["school", "warning", "danger", "sign", "driving", "yellow-diamond"] }, + { "category": "symbols", "char": "🔰", "name": "beginner", "keywords": ["badge", "shield"] }, + { "category": "symbols", "char": "♻️", "name": "recycle", "keywords": ["arrow", "environment", "garbage", "trash"] }, + { "category": "symbols", "char": "🈯", "name": "u6307", "keywords": ["chinese", "point", "green-square", "kanji", "reserved", "shiteiseki"] }, + { "category": "symbols", "char": "💹", "name": "chart", "keywords": ["green-square", "graph", "presentation", "stats"] }, + { "category": "symbols", "char": "❇️", "name": "sparkle", "keywords": ["stars", "green-square", "awesome", "good", "fireworks"] }, + { "category": "symbols", "char": "✳️", "name": "eight_spoked_asterisk", "keywords": ["star", "sparkle", "green-square"] }, + { "category": "symbols", "char": "❎", "name": "negative_squared_cross_mark", "keywords": ["x", "green-square", "no", "deny"] }, + { "category": "symbols", "char": "✅", "name": "white_check_mark", "keywords": ["green-square", "ok", "agree", "vote", "election", "answer", "tick"] }, + { "category": "symbols", "char": "💠", "name": "diamond_shape_with_a_dot_inside", "keywords": ["jewel", "blue", "gem", "crystal", "fancy"] }, + { "category": "symbols", "char": "🌀", "name": "cyclone", "keywords": ["weather", "swirl", "blue", "cloud", "vortex", "spiral", "whirlpool", "spin", "tornado", "hurricane", "typhoon"] }, + { "category": "symbols", "char": "➿", "name": "loop", "keywords": ["tape", "cassette"] }, + { "category": "symbols", "char": "🌐", "name": "globe_with_meridians", "keywords": ["earth", "international", "world", "internet", "interweb", "i18n"] }, + { "category": "symbols", "char": "Ⓜ️", "name": "m", "keywords": ["alphabet", "blue-circle", "letter"] }, + { "category": "symbols", "char": "🏧", "name": "atm", "keywords": ["money", "sales", "cash", "blue-square", "payment", "bank"] }, + { "category": "symbols", "char": "🈂️", "name": "sa", "keywords": ["japanese", "blue-square", "katakana"] }, + { "category": "symbols", "char": "🛂", "name": "passport_control", "keywords": ["custom", "blue-square"] }, + { "category": "symbols", "char": "🛃", "name": "customs", "keywords": ["passport", "border", "blue-square"] }, + { "category": "symbols", "char": "🛄", "name": "baggage_claim", "keywords": ["blue-square", "airport", "transport"] }, + { "category": "symbols", "char": "🛅", "name": "left_luggage", "keywords": ["blue-square", "travel"] }, + { "category": "symbols", "char": "♿", "name": "wheelchair", "keywords": ["blue-square", "disabled", "a11y", "accessibility"] }, + { "category": "symbols", "char": "🚭", "name": "no_smoking", "keywords": ["cigarette", "blue-square", "smell", "smoke"] }, + { "category": "symbols", "char": "🚾", "name": "wc", "keywords": ["toilet", "restroom", "blue-square"] }, + { "category": "symbols", "char": "🅿️", "name": "parking", "keywords": ["cars", "blue-square", "alphabet", "letter"] }, + { "category": "symbols", "char": "🚰", "name": "potable_water", "keywords": ["blue-square", "liquid", "restroom", "cleaning", "faucet"] }, + { "category": "symbols", "char": "🚹", "name": "mens", "keywords": ["toilet", "restroom", "wc", "blue-square", "gender", "male"] }, + { "category": "symbols", "char": "🚺", "name": "womens", "keywords": ["purple-square", "woman", "female", "toilet", "loo", "restroom", "gender"] }, + { "category": "symbols", "char": "🚼", "name": "baby_symbol", "keywords": ["orange-square", "child"] }, + { "category": "symbols", "char": "🚻", "name": "restroom", "keywords": ["blue-square", "toilet", "refresh", "wc", "gender"] }, + { "category": "symbols", "char": "🚮", "name": "put_litter_in_its_place", "keywords": ["blue-square", "sign", "human", "info"] }, + { "category": "symbols", "char": "🎦", "name": "cinema", "keywords": ["blue-square", "record", "film", "movie", "curtain", "stage", "theater"] }, + { "category": "symbols", "char": "📶", "name": "signal_strength", "keywords": ["blue-square", "reception", "phone", "internet", "connection", "wifi", "bluetooth", "bars"] }, + { "category": "symbols", "char": "🈁", "name": "koko", "keywords": ["blue-square", "here", "katakana", "japanese", "destination"] }, + { "category": "symbols", "char": "🆖", "name": "ng", "keywords": ["blue-square", "words", "shape", "icon"] }, + { "category": "symbols", "char": "🆗", "name": "ok", "keywords": ["good", "agree", "yes", "blue-square"] }, + { "category": "symbols", "char": "🆙", "name": "up", "keywords": ["blue-square", "above", "high"] }, + { "category": "symbols", "char": "🆒", "name": "cool", "keywords": ["words", "blue-square"] }, + { "category": "symbols", "char": "🆕", "name": "new", "keywords": ["blue-square", "words", "start"] }, + { "category": "symbols", "char": "🆓", "name": "free", "keywords": ["blue-square", "words"] }, + { "category": "symbols", "char": "0️⃣", "name": "zero", "keywords": ["0", "numbers", "blue-square", "null"] }, + { "category": "symbols", "char": "1️⃣", "name": "one", "keywords": ["blue-square", "numbers", "1"] }, + { "category": "symbols", "char": "2️⃣", "name": "two", "keywords": ["numbers", "2", "prime", "blue-square"] }, + { "category": "symbols", "char": "3️⃣", "name": "three", "keywords": ["3", "numbers", "prime", "blue-square"] }, + { "category": "symbols", "char": "4️⃣", "name": "four", "keywords": ["4", "numbers", "blue-square"] }, + { "category": "symbols", "char": "5️⃣", "name": "five", "keywords": ["5", "numbers", "blue-square", "prime"] }, + { "category": "symbols", "char": "6️⃣", "name": "six", "keywords": ["6", "numbers", "blue-square"] }, + { "category": "symbols", "char": "7️⃣", "name": "seven", "keywords": ["7", "numbers", "blue-square", "prime"] }, + { "category": "symbols", "char": "8️⃣", "name": "eight", "keywords": ["8", "blue-square", "numbers"] }, + { "category": "symbols", "char": "9️⃣", "name": "nine", "keywords": ["blue-square", "numbers", "9"] }, + { "category": "symbols", "char": "🔟", "name": "keycap_ten", "keywords": ["numbers", "10", "blue-square"] }, + { "category": "symbols", "char": "*⃣", "name": "asterisk", "keywords": ["star", "keycap"] }, + { "category": "symbols", "char": "⏏️", "name": "eject_button", "keywords": ["blue-square"] }, + { "category": "symbols", "char": "▶️", "name": "arrow_forward", "keywords": ["blue-square", "right", "direction", "play"] }, + { "category": "symbols", "char": "⏸", "name": "pause_button", "keywords": ["pause", "blue-square"] }, + { "category": "symbols", "char": "⏭", "name": "next_track_button", "keywords": ["forward", "next", "blue-square"] }, + { "category": "symbols", "char": "⏹", "name": "stop_button", "keywords": ["blue-square"] }, + { "category": "symbols", "char": "⏺", "name": "record_button", "keywords": ["blue-square"] }, + { "category": "symbols", "char": "⏯", "name": "play_or_pause_button", "keywords": ["blue-square", "play", "pause"] }, + { "category": "symbols", "char": "⏮", "name": "previous_track_button", "keywords": ["backward"] }, + { "category": "symbols", "char": "⏩", "name": "fast_forward", "keywords": ["blue-square", "play", "speed", "continue"] }, + { "category": "symbols", "char": "⏪", "name": "rewind", "keywords": ["play", "blue-square"] }, + { "category": "symbols", "char": "🔀", "name": "twisted_rightwards_arrows", "keywords": ["blue-square", "shuffle", "music", "random"] }, + { "category": "symbols", "char": "🔁", "name": "repeat", "keywords": ["loop", "record"] }, + { "category": "symbols", "char": "🔂", "name": "repeat_one", "keywords": ["blue-square", "loop"] }, + { "category": "symbols", "char": "◀️", "name": "arrow_backward", "keywords": ["blue-square", "left", "direction"] }, + { "category": "symbols", "char": "🔼", "name": "arrow_up_small", "keywords": ["blue-square", "triangle", "direction", "point", "forward", "top"] }, + { "category": "symbols", "char": "🔽", "name": "arrow_down_small", "keywords": ["blue-square", "direction", "bottom"] }, + { "category": "symbols", "char": "⏫", "name": "arrow_double_up", "keywords": ["blue-square", "direction", "top"] }, + { "category": "symbols", "char": "⏬", "name": "arrow_double_down", "keywords": ["blue-square", "direction", "bottom"] }, + { "category": "symbols", "char": "➡️", "name": "arrow_right", "keywords": ["blue-square", "next"] }, + { "category": "symbols", "char": "⬅️", "name": "arrow_left", "keywords": ["blue-square", "previous", "back"] }, + { "category": "symbols", "char": "⬆️", "name": "arrow_up", "keywords": ["blue-square", "continue", "top", "direction"] }, + { "category": "symbols", "char": "⬇️", "name": "arrow_down", "keywords": ["blue-square", "direction", "bottom"] }, + { "category": "symbols", "char": "↗️", "name": "arrow_upper_right", "keywords": ["blue-square", "point", "direction", "diagonal", "northeast"] }, + { "category": "symbols", "char": "↘️", "name": "arrow_lower_right", "keywords": ["blue-square", "direction", "diagonal", "southeast"] }, + { "category": "symbols", "char": "↙️", "name": "arrow_lower_left", "keywords": ["blue-square", "direction", "diagonal", "southwest"] }, + { "category": "symbols", "char": "↖️", "name": "arrow_upper_left", "keywords": ["blue-square", "point", "direction", "diagonal", "northwest"] }, + { "category": "symbols", "char": "↕️", "name": "arrow_up_down", "keywords": ["blue-square", "direction", "way", "vertical"] }, + { "category": "symbols", "char": "↔️", "name": "left_right_arrow", "keywords": ["shape", "direction", "horizontal", "sideways"] }, + { "category": "symbols", "char": "🔄", "name": "arrows_counterclockwise", "keywords": ["blue-square", "sync", "cycle"] }, + { "category": "symbols", "char": "↪️", "name": "arrow_right_hook", "keywords": ["blue-square", "return", "rotate", "direction"] }, + { "category": "symbols", "char": "↩️", "name": "leftwards_arrow_with_hook", "keywords": ["back", "return", "blue-square", "undo", "enter"] }, + { "category": "symbols", "char": "⤴️", "name": "arrow_heading_up", "keywords": ["blue-square", "direction", "top"] }, + { "category": "symbols", "char": "⤵️", "name": "arrow_heading_down", "keywords": ["blue-square", "direction", "bottom"] }, + { "category": "symbols", "char": "#️⃣", "name": "hash", "keywords": ["symbol", "blue-square", "twitter"] }, + { "category": "symbols", "char": "ℹ️", "name": "information_source", "keywords": ["blue-square", "alphabet", "letter"] }, + { "category": "symbols", "char": "🔤", "name": "abc", "keywords": ["blue-square", "alphabet"] }, + { "category": "symbols", "char": "🔡", "name": "abcd", "keywords": ["blue-square", "alphabet"] }, + { "category": "symbols", "char": "🔠", "name": "capital_abcd", "keywords": ["alphabet", "words", "blue-square"] }, + { "category": "symbols", "char": "🔣", "name": "symbols", "keywords": ["blue-square", "music", "note", "ampersand", "percent", "glyphs", "characters"] }, + { "category": "symbols", "char": "🎵", "name": "musical_note", "keywords": ["score", "tone", "sound"] }, + { "category": "symbols", "char": "🎶", "name": "notes", "keywords": ["music", "score"] }, + { "category": "symbols", "char": "〰️", "name": "wavy_dash", "keywords": ["draw", "line", "moustache", "mustache", "squiggle", "scribble"] }, + { "category": "symbols", "char": "➰", "name": "curly_loop", "keywords": ["scribble", "draw", "shape", "squiggle"] }, + { "category": "symbols", "char": "✔️", "name": "heavy_check_mark", "keywords": ["ok", "nike", "answer", "yes", "tick"] }, + { "category": "symbols", "char": "🔃", "name": "arrows_clockwise", "keywords": ["sync", "cycle", "round", "repeat"] }, + { "category": "symbols", "char": "➕", "name": "heavy_plus_sign", "keywords": ["math", "calculation", "addition", "more", "increase"] }, + { "category": "symbols", "char": "➖", "name": "heavy_minus_sign", "keywords": ["math", "calculation", "subtract", "less"] }, + { "category": "symbols", "char": "➗", "name": "heavy_division_sign", "keywords": ["divide", "math", "calculation"] }, + { "category": "symbols", "char": "✖️", "name": "heavy_multiplication_x", "keywords": ["math", "calculation"] }, + { "category": "symbols", "char": "\uD83D\uDFF0", "name": "heavy_equals_sign", "keywords": [] }, + { "category": "symbols", "char": "♾", "name": "infinity", "keywords": ["forever"] }, + { "category": "symbols", "char": "💲", "name": "heavy_dollar_sign", "keywords": ["money", "sales", "payment", "currency", "buck"] }, + { "category": "symbols", "char": "💱", "name": "currency_exchange", "keywords": ["money", "sales", "dollar", "travel"] }, + { "category": "symbols", "char": "©️", "name": "copyright", "keywords": ["ip", "license", "circle", "law", "legal"] }, + { "category": "symbols", "char": "®️", "name": "registered", "keywords": ["alphabet", "circle"] }, + { "category": "symbols", "char": "™️", "name": "tm", "keywords": ["trademark", "brand", "law", "legal"] }, + { "category": "symbols", "char": "🔚", "name": "end", "keywords": ["words", "arrow"] }, + { "category": "symbols", "char": "🔙", "name": "back", "keywords": ["arrow", "words", "return"] }, + { "category": "symbols", "char": "🔛", "name": "on", "keywords": ["arrow", "words"] }, + { "category": "symbols", "char": "🔝", "name": "top", "keywords": ["words", "blue-square"] }, + { "category": "symbols", "char": "🔜", "name": "soon", "keywords": ["arrow", "words"] }, + { "category": "symbols", "char": "☑️", "name": "ballot_box_with_check", "keywords": ["ok", "agree", "confirm", "black-square", "vote", "election", "yes", "tick"] }, + { "category": "symbols", "char": "🔘", "name": "radio_button", "keywords": ["input", "old", "music", "circle"] }, + { "category": "symbols", "char": "⚫", "name": "black_circle", "keywords": ["shape", "button", "round"] }, + { "category": "symbols", "char": "⚪", "name": "white_circle", "keywords": ["shape", "round"] }, + { "category": "symbols", "char": "🔴", "name": "red_circle", "keywords": ["shape", "error", "danger"] }, + { "category": "symbols", "char": "🟠", "name": "orange_circle", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟡", "name": "yellow_circle", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟢", "name": "green_circle", "keywords": ["shape"] }, + { "category": "symbols", "char": "🔵", "name": "large_blue_circle", "keywords": ["shape", "icon", "button"] }, + { "category": "symbols", "char": "🟣", "name": "purple_circle", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟤", "name": "brown_circle", "keywords": ["shape"] }, + { "category": "symbols", "char": "🔸", "name": "small_orange_diamond", "keywords": ["shape", "jewel", "gem"] }, + { "category": "symbols", "char": "🔹", "name": "small_blue_diamond", "keywords": ["shape", "jewel", "gem"] }, + { "category": "symbols", "char": "🔶", "name": "large_orange_diamond", "keywords": ["shape", "jewel", "gem"] }, + { "category": "symbols", "char": "🔷", "name": "large_blue_diamond", "keywords": ["shape", "jewel", "gem"] }, + { "category": "symbols", "char": "🔺", "name": "small_red_triangle", "keywords": ["shape", "direction", "up", "top"] }, + { "category": "symbols", "char": "▪️", "name": "black_small_square", "keywords": ["shape", "icon"] }, + { "category": "symbols", "char": "▫️", "name": "white_small_square", "keywords": ["shape", "icon"] }, + { "category": "symbols", "char": "⬛", "name": "black_large_square", "keywords": ["shape", "icon", "button"] }, + { "category": "symbols", "char": "⬜", "name": "white_large_square", "keywords": ["shape", "icon", "stone", "button"] }, + { "category": "symbols", "char": "🟥", "name": "red_square", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟧", "name": "orange_square", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟨", "name": "yellow_square", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟩", "name": "green_square", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟦", "name": "blue_square", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟪", "name": "purple_square", "keywords": ["shape"] }, + { "category": "symbols", "char": "🟫", "name": "brown_square", "keywords": ["shape"] }, + { "category": "symbols", "char": "🔻", "name": "small_red_triangle_down", "keywords": ["shape", "direction", "bottom"] }, + { "category": "symbols", "char": "◼️", "name": "black_medium_square", "keywords": ["shape", "button", "icon"] }, + { "category": "symbols", "char": "◻️", "name": "white_medium_square", "keywords": ["shape", "stone", "icon"] }, + { "category": "symbols", "char": "◾", "name": "black_medium_small_square", "keywords": ["icon", "shape", "button"] }, + { "category": "symbols", "char": "◽", "name": "white_medium_small_square", "keywords": ["shape", "stone", "icon", "button"] }, + { "category": "symbols", "char": "🔲", "name": "black_square_button", "keywords": ["shape", "input", "frame"] }, + { "category": "symbols", "char": "🔳", "name": "white_square_button", "keywords": ["shape", "input"] }, + { "category": "symbols", "char": "🔈", "name": "speaker", "keywords": ["sound", "volume", "silence", "broadcast"] }, + { "category": "symbols", "char": "🔉", "name": "sound", "keywords": ["volume", "speaker", "broadcast"] }, + { "category": "symbols", "char": "🔊", "name": "loud_sound", "keywords": ["volume", "noise", "noisy", "speaker", "broadcast"] }, + { "category": "symbols", "char": "🔇", "name": "mute", "keywords": ["sound", "volume", "silence", "quiet"] }, + { "category": "symbols", "char": "📣", "name": "mega", "keywords": ["sound", "speaker", "volume"] }, + { "category": "symbols", "char": "📢", "name": "loudspeaker", "keywords": ["volume", "sound"] }, + { "category": "symbols", "char": "🔔", "name": "bell", "keywords": ["sound", "notification", "christmas", "xmas", "chime"] }, + { "category": "symbols", "char": "🔕", "name": "no_bell", "keywords": ["sound", "volume", "mute", "quiet", "silent"] }, + { "category": "symbols", "char": "🃏", "name": "black_joker", "keywords": ["poker", "cards", "game", "play", "magic"] }, + { "category": "symbols", "char": "🀄", "name": "mahjong", "keywords": ["game", "play", "chinese", "kanji"] }, + { "category": "symbols", "char": "♠️", "name": "spades", "keywords": ["poker", "cards", "suits", "magic"] }, + { "category": "symbols", "char": "♣️", "name": "clubs", "keywords": ["poker", "cards", "magic", "suits"] }, + { "category": "symbols", "char": "♥️", "name": "hearts", "keywords": ["poker", "cards", "magic", "suits"] }, + { "category": "symbols", "char": "♦️", "name": "diamonds", "keywords": ["poker", "cards", "magic", "suits"] }, + { "category": "symbols", "char": "🎴", "name": "flower_playing_cards", "keywords": ["game", "sunset", "red"] }, + { "category": "symbols", "char": "💭", "name": "thought_balloon", "keywords": ["bubble", "cloud", "speech", "thinking", "dream"] }, + { "category": "symbols", "char": "🗯", "name": "right_anger_bubble", "keywords": ["caption", "speech", "thinking", "mad"] }, + { "category": "symbols", "char": "💬", "name": "speech_balloon", "keywords": ["bubble", "words", "message", "talk", "chatting"] }, + { "category": "symbols", "char": "🗨", "name": "left_speech_bubble", "keywords": ["words", "message", "talk", "chatting"] }, + { "category": "symbols", "char": "🕐", "name": "clock1", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕑", "name": "clock2", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕒", "name": "clock3", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕓", "name": "clock4", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕔", "name": "clock5", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕕", "name": "clock6", "keywords": ["time", "late", "early", "schedule", "dawn", "dusk"] }, + { "category": "symbols", "char": "🕖", "name": "clock7", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕗", "name": "clock8", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕘", "name": "clock9", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕙", "name": "clock10", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕚", "name": "clock11", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕛", "name": "clock12", "keywords": ["time", "noon", "midnight", "midday", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕜", "name": "clock130", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕝", "name": "clock230", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕞", "name": "clock330", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕟", "name": "clock430", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕠", "name": "clock530", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕡", "name": "clock630", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕢", "name": "clock730", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕣", "name": "clock830", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕤", "name": "clock930", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕥", "name": "clock1030", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕦", "name": "clock1130", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "symbols", "char": "🕧", "name": "clock1230", "keywords": ["time", "late", "early", "schedule"] }, + { "category": "flags", "char": "🇦🇫", "name": "afghanistan", "keywords": ["af", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇽", "name": "aland_islands", "keywords": ["Åland", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇱", "name": "albania", "keywords": ["al", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇩🇿", "name": "algeria", "keywords": ["dz", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇸", "name": "american_samoa", "keywords": ["american", "ws", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇩", "name": "andorra", "keywords": ["ad", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇴", "name": "angola", "keywords": ["ao", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇮", "name": "anguilla", "keywords": ["ai", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇶", "name": "antarctica", "keywords": ["aq", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇬", "name": "antigua_barbuda", "keywords": ["antigua", "barbuda", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇷", "name": "argentina", "keywords": ["ar", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇲", "name": "armenia", "keywords": ["am", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇼", "name": "aruba", "keywords": ["aw", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇨", "name": "ascension_island", "keywords": ["flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇺", "name": "australia", "keywords": ["au", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇹", "name": "austria", "keywords": ["at", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇿", "name": "azerbaijan", "keywords": ["az", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇸", "name": "bahamas", "keywords": ["bs", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇭", "name": "bahrain", "keywords": ["bh", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇩", "name": "bangladesh", "keywords": ["bd", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇧", "name": "barbados", "keywords": ["bb", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇾", "name": "belarus", "keywords": ["by", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇪", "name": "belgium", "keywords": ["be", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇿", "name": "belize", "keywords": ["bz", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇯", "name": "benin", "keywords": ["bj", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇲", "name": "bermuda", "keywords": ["bm", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇹", "name": "bhutan", "keywords": ["bt", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇴", "name": "bolivia", "keywords": ["bo", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇶", "name": "caribbean_netherlands", "keywords": ["bonaire", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇦", "name": "bosnia_herzegovina", "keywords": ["bosnia", "herzegovina", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇼", "name": "botswana", "keywords": ["bw", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇷", "name": "brazil", "keywords": ["br", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇴", "name": "british_indian_ocean_territory", "keywords": ["british", "indian", "ocean", "territory", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇻🇬", "name": "british_virgin_islands", "keywords": ["british", "virgin", "islands", "bvi", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇳", "name": "brunei", "keywords": ["bn", "darussalam", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇬", "name": "bulgaria", "keywords": ["bg", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇫", "name": "burkina_faso", "keywords": ["burkina", "faso", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇮", "name": "burundi", "keywords": ["bi", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇻", "name": "cape_verde", "keywords": ["cabo", "verde", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇭", "name": "cambodia", "keywords": ["kh", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇲", "name": "cameroon", "keywords": ["cm", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇦", "name": "canada", "keywords": ["ca", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇨", "name": "canary_islands", "keywords": ["canary", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇾", "name": "cayman_islands", "keywords": ["cayman", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇫", "name": "central_african_republic", "keywords": ["central", "african", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇩", "name": "chad", "keywords": ["td", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇱", "name": "chile", "keywords": ["flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇳", "name": "cn", "keywords": ["china", "chinese", "prc", "flag", "country", "nation", "banner"] }, + { "category": "flags", "char": "🇨🇽", "name": "christmas_island", "keywords": ["christmas", "island", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇨", "name": "cocos_islands", "keywords": ["cocos", "keeling", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇴", "name": "colombia", "keywords": ["co", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇲", "name": "comoros", "keywords": ["km", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇬", "name": "congo_brazzaville", "keywords": ["congo", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇩", "name": "congo_kinshasa", "keywords": ["congo", "democratic", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇰", "name": "cook_islands", "keywords": ["cook", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇷", "name": "costa_rica", "keywords": ["costa", "rica", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇭🇷", "name": "croatia", "keywords": ["hr", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇺", "name": "cuba", "keywords": ["cu", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇼", "name": "curacao", "keywords": ["curaçao", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇾", "name": "cyprus", "keywords": ["cy", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇿", "name": "czech_republic", "keywords": ["cz", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇩🇰", "name": "denmark", "keywords": ["dk", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇩🇯", "name": "djibouti", "keywords": ["dj", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇩🇲", "name": "dominica", "keywords": ["dm", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇩🇴", "name": "dominican_republic", "keywords": ["dominican", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇪🇨", "name": "ecuador", "keywords": ["ec", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇪🇬", "name": "egypt", "keywords": ["eg", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇻", "name": "el_salvador", "keywords": ["el", "salvador", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇶", "name": "equatorial_guinea", "keywords": ["equatorial", "gn", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇪🇷", "name": "eritrea", "keywords": ["er", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇪🇪", "name": "estonia", "keywords": ["ee", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇪🇹", "name": "ethiopia", "keywords": ["et", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇪🇺", "name": "eu", "keywords": ["european", "union", "flag", "banner"] }, + { "category": "flags", "char": "🇫🇰", "name": "falkland_islands", "keywords": ["falkland", "islands", "malvinas", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇫🇴", "name": "faroe_islands", "keywords": ["faroe", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇫🇯", "name": "fiji", "keywords": ["fj", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇫🇮", "name": "finland", "keywords": ["fi", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇫🇷", "name": "fr", "keywords": ["banner", "flag", "nation", "france", "french", "country"] }, + { "category": "flags", "char": "🇬🇫", "name": "french_guiana", "keywords": ["french", "guiana", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇫", "name": "french_polynesia", "keywords": ["french", "polynesia", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇫", "name": "french_southern_territories", "keywords": ["french", "southern", "territories", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇦", "name": "gabon", "keywords": ["ga", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇲", "name": "gambia", "keywords": ["gm", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇪", "name": "georgia", "keywords": ["ge", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇩🇪", "name": "de", "keywords": ["german", "nation", "flag", "country", "banner"] }, + { "category": "flags", "char": "🇬🇭", "name": "ghana", "keywords": ["gh", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇮", "name": "gibraltar", "keywords": ["gi", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇷", "name": "greece", "keywords": ["gr", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇱", "name": "greenland", "keywords": ["gl", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇩", "name": "grenada", "keywords": ["gd", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇵", "name": "guadeloupe", "keywords": ["gp", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇺", "name": "guam", "keywords": ["gu", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇹", "name": "guatemala", "keywords": ["gt", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇬", "name": "guernsey", "keywords": ["gg", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇳", "name": "guinea", "keywords": ["gn", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇼", "name": "guinea_bissau", "keywords": ["gw", "bissau", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇾", "name": "guyana", "keywords": ["gy", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇭🇹", "name": "haiti", "keywords": ["ht", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇭🇳", "name": "honduras", "keywords": ["hn", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇭🇰", "name": "hong_kong", "keywords": ["hong", "kong", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇭🇺", "name": "hungary", "keywords": ["hu", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇸", "name": "iceland", "keywords": ["is", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇳", "name": "india", "keywords": ["in", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇩", "name": "indonesia", "keywords": ["flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇷", "name": "iran", "keywords": ["iran, ", "islamic", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇶", "name": "iraq", "keywords": ["iq", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇪", "name": "ireland", "keywords": ["ie", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇲", "name": "isle_of_man", "keywords": ["isle", "man", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇱", "name": "israel", "keywords": ["il", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇮🇹", "name": "it", "keywords": ["italy", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇮", "name": "cote_divoire", "keywords": ["ivory", "coast", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇯🇲", "name": "jamaica", "keywords": ["jm", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇯🇵", "name": "jp", "keywords": ["japanese", "nation", "flag", "country", "banner"] }, + { "category": "flags", "char": "🇯🇪", "name": "jersey", "keywords": ["je", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇯🇴", "name": "jordan", "keywords": ["jo", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇿", "name": "kazakhstan", "keywords": ["kz", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇪", "name": "kenya", "keywords": ["ke", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇮", "name": "kiribati", "keywords": ["ki", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇽🇰", "name": "kosovo", "keywords": ["xk", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇼", "name": "kuwait", "keywords": ["kw", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇬", "name": "kyrgyzstan", "keywords": ["kg", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇦", "name": "laos", "keywords": ["lao", "democratic", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇻", "name": "latvia", "keywords": ["lv", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇧", "name": "lebanon", "keywords": ["lb", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇸", "name": "lesotho", "keywords": ["ls", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇷", "name": "liberia", "keywords": ["lr", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇾", "name": "libya", "keywords": ["ly", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇮", "name": "liechtenstein", "keywords": ["li", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇹", "name": "lithuania", "keywords": ["lt", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇺", "name": "luxembourg", "keywords": ["lu", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇴", "name": "macau", "keywords": ["macao", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇰", "name": "macedonia", "keywords": ["macedonia, ", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇬", "name": "madagascar", "keywords": ["mg", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇼", "name": "malawi", "keywords": ["mw", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇾", "name": "malaysia", "keywords": ["my", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇻", "name": "maldives", "keywords": ["mv", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇱", "name": "mali", "keywords": ["ml", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇹", "name": "malta", "keywords": ["mt", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇭", "name": "marshall_islands", "keywords": ["marshall", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇶", "name": "martinique", "keywords": ["mq", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇷", "name": "mauritania", "keywords": ["mr", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇺", "name": "mauritius", "keywords": ["mu", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇾🇹", "name": "mayotte", "keywords": ["yt", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇽", "name": "mexico", "keywords": ["mx", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇫🇲", "name": "micronesia", "keywords": ["micronesia, ", "federated", "states", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇩", "name": "moldova", "keywords": ["moldova, ", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇨", "name": "monaco", "keywords": ["mc", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇳", "name": "mongolia", "keywords": ["mn", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇪", "name": "montenegro", "keywords": ["me", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇸", "name": "montserrat", "keywords": ["ms", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇦", "name": "morocco", "keywords": ["ma", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇿", "name": "mozambique", "keywords": ["mz", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇲", "name": "myanmar", "keywords": ["mm", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇦", "name": "namibia", "keywords": ["na", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇷", "name": "nauru", "keywords": ["nr", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇵", "name": "nepal", "keywords": ["np", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇱", "name": "netherlands", "keywords": ["nl", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇨", "name": "new_caledonia", "keywords": ["new", "caledonia", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇿", "name": "new_zealand", "keywords": ["new", "zealand", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇮", "name": "nicaragua", "keywords": ["ni", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇪", "name": "niger", "keywords": ["ne", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇬", "name": "nigeria", "keywords": ["flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇺", "name": "niue", "keywords": ["nu", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇳🇫", "name": "norfolk_island", "keywords": ["norfolk", "island", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇲🇵", "name": "northern_mariana_islands", "keywords": ["northern", "mariana", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇵", "name": "north_korea", "keywords": ["north", "korea", "nation", "flag", "country", "banner"] }, + { "category": "flags", "char": "🇳🇴", "name": "norway", "keywords": ["no", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇴🇲", "name": "oman", "keywords": ["om_symbol", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇰", "name": "pakistan", "keywords": ["pk", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇼", "name": "palau", "keywords": ["pw", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇸", "name": "palestinian_territories", "keywords": ["palestine", "palestinian", "territories", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇦", "name": "panama", "keywords": ["pa", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇬", "name": "papua_new_guinea", "keywords": ["papua", "new", "guinea", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇾", "name": "paraguay", "keywords": ["py", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇪", "name": "peru", "keywords": ["pe", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇭", "name": "philippines", "keywords": ["ph", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇳", "name": "pitcairn_islands", "keywords": ["pitcairn", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇱", "name": "poland", "keywords": ["pl", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇹", "name": "portugal", "keywords": ["pt", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇷", "name": "puerto_rico", "keywords": ["puerto", "rico", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇶🇦", "name": "qatar", "keywords": ["qa", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇷🇪", "name": "reunion", "keywords": ["réunion", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇷🇴", "name": "romania", "keywords": ["ro", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇷🇺", "name": "ru", "keywords": ["russian", "federation", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇷🇼", "name": "rwanda", "keywords": ["rw", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇧🇱", "name": "st_barthelemy", "keywords": ["saint", "barthélemy", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇭", "name": "st_helena", "keywords": ["saint", "helena", "ascension", "tristan", "cunha", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇳", "name": "st_kitts_nevis", "keywords": ["saint", "kitts", "nevis", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇨", "name": "st_lucia", "keywords": ["saint", "lucia", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇵🇲", "name": "st_pierre_miquelon", "keywords": ["saint", "pierre", "miquelon", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇻🇨", "name": "st_vincent_grenadines", "keywords": ["saint", "vincent", "grenadines", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇼🇸", "name": "samoa", "keywords": ["ws", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇲", "name": "san_marino", "keywords": ["san", "marino", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇹", "name": "sao_tome_principe", "keywords": ["sao", "tome", "principe", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇦", "name": "saudi_arabia", "keywords": ["flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇳", "name": "senegal", "keywords": ["sn", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇷🇸", "name": "serbia", "keywords": ["rs", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇨", "name": "seychelles", "keywords": ["sc", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇱", "name": "sierra_leone", "keywords": ["sierra", "leone", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇬", "name": "singapore", "keywords": ["sg", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇽", "name": "sint_maarten", "keywords": ["sint", "maarten", "dutch", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇰", "name": "slovakia", "keywords": ["sk", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇮", "name": "slovenia", "keywords": ["si", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇧", "name": "solomon_islands", "keywords": ["solomon", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇴", "name": "somalia", "keywords": ["so", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇿🇦", "name": "south_africa", "keywords": ["south", "africa", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇸", "name": "south_georgia_south_sandwich_islands", "keywords": ["south", "georgia", "sandwich", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇰🇷", "name": "kr", "keywords": ["south", "korea", "nation", "flag", "country", "banner"] }, + { "category": "flags", "char": "🇸🇸", "name": "south_sudan", "keywords": ["south", "sd", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇪🇸", "name": "es", "keywords": ["spain", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇱🇰", "name": "sri_lanka", "keywords": ["sri", "lanka", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇩", "name": "sudan", "keywords": ["sd", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇷", "name": "suriname", "keywords": ["sr", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇿", "name": "swaziland", "keywords": ["sz", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇪", "name": "sweden", "keywords": ["se", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇨🇭", "name": "switzerland", "keywords": ["ch", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇸🇾", "name": "syria", "keywords": ["syrian", "arab", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇼", "name": "taiwan", "keywords": ["tw", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇯", "name": "tajikistan", "keywords": ["tj", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇿", "name": "tanzania", "keywords": ["tanzania, ", "united", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇭", "name": "thailand", "keywords": ["th", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇱", "name": "timor_leste", "keywords": ["timor", "leste", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇬", "name": "togo", "keywords": ["tg", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇰", "name": "tokelau", "keywords": ["tk", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇴", "name": "tonga", "keywords": ["to", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇹", "name": "trinidad_tobago", "keywords": ["trinidad", "tobago", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇦", "name": "tristan_da_cunha", "keywords": ["flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇳", "name": "tunisia", "keywords": ["tn", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇷", "name": "tr", "keywords": ["turkey", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇲", "name": "turkmenistan", "keywords": ["flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇨", "name": "turks_caicos_islands", "keywords": ["turks", "caicos", "islands", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇹🇻", "name": "tuvalu", "keywords": ["flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇺🇬", "name": "uganda", "keywords": ["ug", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇺🇦", "name": "ukraine", "keywords": ["ua", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇦🇪", "name": "united_arab_emirates", "keywords": ["united", "arab", "emirates", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇬🇧", "name": "uk", "keywords": ["united", "kingdom", "great", "britain", "northern", "ireland", "flag", "nation", "country", "banner", "british", "UK", "english", "england", "union jack"] }, + { "category": "flags", "char": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", "name": "england", "keywords": ["flag", "english"] }, + { "category": "flags", "char": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "name": "scotland", "keywords": ["flag", "scottish"] }, + { "category": "flags", "char": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", "name": "wales", "keywords": ["flag", "welsh"] }, + { "category": "flags", "char": "🇺🇸", "name": "us", "keywords": ["united", "states", "america", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇻🇮", "name": "us_virgin_islands", "keywords": ["virgin", "islands", "us", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇺🇾", "name": "uruguay", "keywords": ["uy", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇺🇿", "name": "uzbekistan", "keywords": ["uz", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇻🇺", "name": "vanuatu", "keywords": ["vu", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇻🇦", "name": "vatican_city", "keywords": ["vatican", "city", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇻🇪", "name": "venezuela", "keywords": ["ve", "bolivarian", "republic", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇻🇳", "name": "vietnam", "keywords": ["viet", "nam", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇼🇫", "name": "wallis_futuna", "keywords": ["wallis", "futuna", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇪🇭", "name": "western_sahara", "keywords": ["western", "sahara", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇾🇪", "name": "yemen", "keywords": ["ye", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇿🇲", "name": "zambia", "keywords": ["zm", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇿🇼", "name": "zimbabwe", "keywords": ["zw", "flag", "nation", "country", "banner"] }, + { "category": "flags", "char": "🇺🇳", "name": "united_nations", "keywords": ["un", "flag", "banner"] }, + { "category": "flags", "char": "🏴‍☠️", "name": "pirate_flag", "keywords": ["skull", "crossbones", "flag", "banner"] } +] + diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts new file mode 100644 index 0000000000..dbbd908b8f --- /dev/null +++ b/packages/frontend/src/events.ts @@ -0,0 +1,4 @@ +import { EventEmitter } from 'eventemitter3'; + +// TODO: 型付け +export const globalEvents = new EventEmitter(); diff --git a/packages/frontend/src/filters/bytes.ts b/packages/frontend/src/filters/bytes.ts new file mode 100644 index 0000000000..c80f2f0ed2 --- /dev/null +++ b/packages/frontend/src/filters/bytes.ts @@ -0,0 +1,9 @@ +export default (v, digits = 0) => { + if (v == null) return '?'; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + if (v === 0) return '0'; + const isMinus = v < 0; + if (isMinus) v = -v; + const i = Math.floor(Math.log(v) / Math.log(1024)); + return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; +}; diff --git a/packages/frontend/src/filters/note.ts b/packages/frontend/src/filters/note.ts new file mode 100644 index 0000000000..cd9b7d98d2 --- /dev/null +++ b/packages/frontend/src/filters/note.ts @@ -0,0 +1,3 @@ +export const notePage = note => { + return `/notes/${note.id}`; +}; diff --git a/packages/frontend/src/filters/number.ts b/packages/frontend/src/filters/number.ts new file mode 100644 index 0000000000..880a848ca4 --- /dev/null +++ b/packages/frontend/src/filters/number.ts @@ -0,0 +1 @@ +export default n => n == null ? 'N/A' : n.toLocaleString(); diff --git a/packages/frontend/src/filters/user.ts b/packages/frontend/src/filters/user.ts new file mode 100644 index 0000000000..ff2f7e2dae --- /dev/null +++ b/packages/frontend/src/filters/user.ts @@ -0,0 +1,15 @@ +import * as misskey from 'misskey-js'; +import * as Acct from 'misskey-js/built/acct'; +import { url } from '@/config'; + +export const acct = (user: misskey.Acct) => { + return Acct.toString(user); +}; + +export const userName = (user: misskey.entities.User) => { + return user.name || user.username; +}; + +export const userPage = (user: misskey.Acct, path?, absolute = false) => { + return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; +}; diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts new file mode 100644 index 0000000000..31e066960d --- /dev/null +++ b/packages/frontend/src/i18n.ts @@ -0,0 +1,5 @@ +import { markRaw } from 'vue'; +import { locale } from '@/config'; +import { I18n } from '@/scripts/i18n'; + +export const i18n = markRaw(new I18n(locale)); diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts new file mode 100644 index 0000000000..508d3262b3 --- /dev/null +++ b/packages/frontend/src/init.ts @@ -0,0 +1,433 @@ +/** + * Client entry point + */ +// https://vitejs.dev/config/build-options.html#build-modulepreload +import 'vite/modulepreload-polyfill'; + +import '@/style.scss'; + +//#region account indexedDB migration +import { set } from '@/scripts/idb-proxy'; + +if (localStorage.getItem('accounts') != null) { + set('accounts', JSON.parse(localStorage.getItem('accounts'))); + localStorage.removeItem('accounts'); +} +//#endregion + +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { compareVersions } from 'compare-versions'; +import JSON5 from 'json5'; + +import widgets from '@/widgets'; +import directives from '@/directives'; +import components from '@/components'; +import { version, ui, lang, host } from '@/config'; +import { applyTheme } from '@/scripts/theme'; +import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; +import { i18n } from '@/i18n'; +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'; +import { fetchInstance, instance } from '@/instance'; +import { makeHotkey } from '@/scripts/hotkey'; +import { search } from '@/scripts/search'; +import { deviceKind } from '@/scripts/device-kind'; +import { initializeSw } from '@/scripts/initialize-sw'; +import { reloadChannel } from '@/scripts/unison-reload'; +import { reactionPicker } from '@/scripts/reaction-picker'; +import { getUrlWithoutLoginId } from '@/scripts/login-id'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; + +(async () => { + console.info(`Misskey v${version}`); + + if (_DEV_) { + console.warn('Development mode!!!'); + + console.info(`vue ${vueVersion}`); + + (window as any).$i = $i; + (window as any).$store = defaultStore; + + window.addEventListener('error', event => { + console.error(event); + /* + alert({ + type: 'error', + title: 'DEV: Unhandled error', + text: event.message + }); + */ + }); + + window.addEventListener('unhandledrejection', event => { + console.error(event); + /* + alert({ + type: 'error', + title: 'DEV: Unhandled promise rejection', + text: event.reason + }); + */ + }); + } + + // タッチデバイスでCSSの:hoverを機能させる + document.addEventListener('touchend', () => {}, { passive: true }); + + // 一斉リロード + reloadChannel.addEventListener('message', path => { + if (path !== null) location.href = path; + else location.reload(); + }); + + // If mobile, insert the viewport meta tag + if (['smartphone', 'tablet'].includes(deviceKind)) { + const viewport = document.getElementsByName('viewport').item(0); + viewport.setAttribute('content', + `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); + } + + //#region Set lang attr + const html = document.documentElement; + html.setAttribute('lang', lang); + //#endregion + + //#region loginId + const params = new URLSearchParams(location.search); + const loginId = params.get('loginId'); + + if (loginId) { + const target = getUrlWithoutLoginId(location.href); + + if (!$i || $i.id !== loginId) { + const account = await getAccountFromId(loginId); + if (account) { + await login(account.token, target); + } + } + + history.replaceState({ misskey: 'loginId' }, '', target); + } + + //#endregion + + //#region Fetch user + if ($i && $i.token) { + if (_DEV_) { + console.log('account cache found. refreshing...'); + } + + refreshAccount(); + } else { + if (_DEV_) { + console.log('no account cache found.'); + } + + // 連携ログインの場合用にCookieを参照する + const i = (document.cookie.match(/igi=(\w+)/) ?? [null, null])[1]; + + if (i != null && i !== 'null') { + if (_DEV_) { + console.log('signing...'); + } + + try { + document.body.innerHTML = '

Please wait...
'; + await login(i); + } catch (err) { + // Render the error screen + // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) + document.body.innerHTML = '
Oops!
'; + } + } else { + if (_DEV_) { + console.log('not signed in'); + } + } + } + //#endregion + + const fetchInstanceMetaPromise = fetchInstance(); + + fetchInstanceMetaPromise.then(() => { + localStorage.setItem('v', instance.version); + + // Init service worker + initializeSw(); + }); + + const app = createApp( + window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) : + !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : + ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : + ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : + defineAsyncComponent(() => import('@/ui/universal.vue')), + ); + + if (_DEV_) { + app.config.performance = true; + } + + app.config.globalProperties = { + $i, + $store: defaultStore, + $instance: instance, + $t: i18n.t, + $ts: i18n.ts, + }; + + widgets(app); + directives(app); + components(app); + + const splash = document.getElementById('splash'); + // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) + if (splash) splash.addEventListener('transitionend', () => { + splash.remove(); + }); + + // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 + // なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する + const rootEl = (() => { + const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; + + const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID); + + if (currentEl) { + console.warn('multiple import detected'); + return currentEl; + } + + const rootEl = document.createElement('div'); + rootEl.id = MISSKEY_MOUNT_DIV_ID; + document.body.appendChild(rootEl); + return rootEl; + })(); + + app.mount(rootEl); + + // boot.jsのやつを解除 + window.onerror = null; + window.onunhandledrejection = null; + + reactionPicker.init(); + + if (splash) { + splash.style.opacity = '0'; + splash.style.pointerEvents = 'none'; + } + + // クライアントが更新されたか? + const lastVersion = localStorage.getItem('lastVersion'); + if (lastVersion !== version) { + localStorage.setItem('lastVersion', version); + + // テーマリビルドするため + localStorage.removeItem('theme'); + + try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため + if (lastVersion != null && compareVersions(version, lastVersion) === 1) { + // ログインしてる場合だけ + if ($i) { + popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); + } + } + } catch (err) { + } + } + + // NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため) + watch(defaultStore.reactiveState.darkMode, (darkMode) => { + applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); + }, { immediate: localStorage.theme == null }); + + const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme')); + const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme')); + + watch(darkTheme, (theme) => { + if (defaultStore.state.darkMode) { + applyTheme(theme); + } + }); + + watch(lightTheme, (theme) => { + if (!defaultStore.state.darkMode) { + applyTheme(theme); + } + }); + + //#region Sync dark mode + if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + defaultStore.set('darkMode', isDeviceDarkmode()); + } + + window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { + if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + defaultStore.set('darkMode', mql.matches); + } + }); + //#endregion + + fetchInstanceMetaPromise.then(() => { + if (defaultStore.state.themeInitial) { + if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); + if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.parse(instance.defaultDarkTheme)); + defaultStore.set('themeInitial', false); + } + }); + + watch(defaultStore.reactiveState.useBlurEffectForModal, v => { + document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); + }, { immediate: true }); + + watch(defaultStore.reactiveState.useBlurEffect, v => { + if (v) { + document.documentElement.style.removeProperty('--blur'); + } else { + document.documentElement.style.setProperty('--blur', 'none'); + } + }, { immediate: true }); + + let reloadDialogShowing = false; + stream.on('_disconnected_', async () => { + if (defaultStore.state.serverDisconnectedBehavior === 'reload') { + location.reload(); + } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { + if (reloadDialogShowing) return; + reloadDialogShowing = true; + const { canceled } = await confirm({ + type: 'warning', + title: i18n.ts.disconnectedFromServer, + text: i18n.ts.reloadConfirm, + }); + reloadDialogShowing = false; + if (!canceled) { + location.reload(); + } + } + }); + + stream.on('emojiAdded', emojiData => { + // TODO + //store.commit('instance/set', ); + }); + + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { + import('./plugin').then(({ install }) => { + install(plugin); + }); + } + + const hotkeys = { + 'd': (): void => { + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 's': search, + }; + + if ($i) { + // only add post shortcuts if logged in + hotkeys['p|n'] = post; + + if ($i.isDeleted) { + alert({ + type: 'warning', + text: i18n.ts.accountDeletionInProgress, + }); + } + + const lastUsed = localStorage.getItem('lastUsed'); + if (lastUsed) { + const lastUsedDate = parseInt(lastUsed, 10); + // 二時間以上前なら + if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { + toast(i18n.t('welcomeBackWithName', { + name: $i.name || $i.username, + })); + } + } + localStorage.setItem('lastUsed', Date.now().toString()); + + if ('Notification' in window) { + // 許可を得ていなかったらリクエスト + if (Notification.permission === 'default') { + Notification.requestPermission(); + } + } + + const main = markRaw(stream.useChannel('main', null, 'System')); + + // 自分の情報が更新されたとき + main.on('meUpdated', i => { + updateAccount(i); + }); + + main.on('readAllNotifications', () => { + updateAccount({ hasUnreadNotification: false }); + }); + + main.on('unreadNotification', () => { + updateAccount({ hasUnreadNotification: true }); + }); + + main.on('unreadMention', () => { + updateAccount({ hasUnreadMentions: true }); + }); + + main.on('readAllUnreadMentions', () => { + updateAccount({ hasUnreadMentions: false }); + }); + + main.on('unreadSpecifiedNote', () => { + updateAccount({ hasUnreadSpecifiedNotes: true }); + }); + + main.on('readAllUnreadSpecifiedNotes', () => { + updateAccount({ hasUnreadSpecifiedNotes: false }); + }); + + main.on('readAllMessagingMessages', () => { + updateAccount({ hasUnreadMessagingMessage: false }); + }); + + main.on('unreadMessagingMessage', () => { + updateAccount({ hasUnreadMessagingMessage: true }); + sound.play('chatBg'); + }); + + main.on('readAllAntennas', () => { + updateAccount({ hasUnreadAntenna: false }); + }); + + main.on('unreadAntenna', () => { + updateAccount({ hasUnreadAntenna: true }); + sound.play('antenna'); + }); + + main.on('readAllAnnouncements', () => { + updateAccount({ hasUnreadAnnouncement: false }); + }); + + main.on('readAllChannels', () => { + updateAccount({ hasUnreadChannel: false }); + }); + + main.on('unreadChannel', () => { + updateAccount({ hasUnreadChannel: true }); + sound.play('channel'); + }); + + // トークンが再生成されたとき + // このままではMisskeyが利用できないので強制的にサインアウトさせる + main.on('myTokenRegenerated', () => { + signout(); + }); + } + + // shortcut + document.addEventListener('keydown', makeHotkey(hotkeys)); +})(); diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts new file mode 100644 index 0000000000..51464f32fb --- /dev/null +++ b/packages/frontend/src/instance.ts @@ -0,0 +1,45 @@ +import { computed, reactive } from 'vue'; +import * as Misskey from 'misskey-js'; +import { api } from './os'; + +// TODO: 他のタブと永続化されたstateを同期 + +const instanceData = localStorage.getItem('instance'); + +// TODO: instanceをリアクティブにするかは再考の余地あり + +export const instance: Misskey.entities.InstanceMetadata = reactive(instanceData ? JSON.parse(instanceData) : { + // TODO: set default values +}); + +export async function fetchInstance() { + const meta = await api('meta', { + detail: false, + }); + + for (const [k, v] of Object.entries(meta)) { + instance[k] = v; + } + + localStorage.setItem('instance', JSON.stringify(instance)); +} + +export const emojiCategories = computed(() => { + if (instance.emojis == null) return []; + const categories = new Set(); + for (const emoji of instance.emojis) { + categories.add(emoji.category); + } + return Array.from(categories); +}); + +export const emojiTags = computed(() => { + if (instance.emojis == null) return []; + const tags = new Set(); + for (const emoji of instance.emojis) { + for (const tag of emoji.aliases) { + tags.add(tag); + } + } + return Array.from(tags); +}); diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts new file mode 100644 index 0000000000..31e6cd64a4 --- /dev/null +++ b/packages/frontend/src/navbar.ts @@ -0,0 +1,135 @@ +import { computed, ref, reactive } from 'vue'; +import { $i } from './account'; +import { search } from '@/scripts/search'; +import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { ui } from '@/config'; +import { unisonReload } from '@/scripts/unison-reload'; + +export const navbarItemDef = reactive({ + notifications: { + title: 'notifications', + icon: 'ti ti-bell', + show: computed(() => $i != null), + indicated: computed(() => $i != null && $i.hasUnreadNotification), + to: '/my/notifications', + }, + messaging: { + title: 'messaging', + icon: 'ti ti-messages', + show: computed(() => $i != null), + indicated: computed(() => $i != null && $i.hasUnreadMessagingMessage), + to: '/my/messaging', + }, + drive: { + title: 'drive', + icon: 'ti ti-cloud', + show: computed(() => $i != null), + to: '/my/drive', + }, + followRequests: { + title: 'followRequests', + icon: 'ti ti-user-plus', + show: computed(() => $i != null && $i.isLocked), + indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest), + to: '/my/follow-requests', + }, + explore: { + title: 'explore', + icon: 'ti ti-hash', + to: '/explore', + }, + announcements: { + title: 'announcements', + icon: 'ti ti-speakerphone', + indicated: computed(() => $i != null && $i.hasUnreadAnnouncement), + to: '/announcements', + }, + search: { + title: 'search', + icon: 'ti ti-search', + action: () => search(), + }, + lists: { + title: 'lists', + icon: 'ti ti-list', + show: computed(() => $i != null), + to: '/my/lists', + }, + /* + groups: { + title: 'groups', + icon: 'ti ti-users', + show: computed(() => $i != null), + to: '/my/groups', + }, + */ + antennas: { + title: 'antennas', + icon: 'ti ti-antenna', + show: computed(() => $i != null), + to: '/my/antennas', + }, + favorites: { + title: 'favorites', + icon: 'ti ti-star', + show: computed(() => $i != null), + to: '/my/favorites', + }, + pages: { + title: 'pages', + icon: 'ti ti-news', + to: '/pages', + }, + gallery: { + title: 'gallery', + icon: 'ti ti-icons', + to: '/gallery', + }, + clips: { + title: 'clip', + icon: 'ti ti-paperclip', + show: computed(() => $i != null), + to: '/my/clips', + }, + channels: { + title: 'channel', + icon: 'ti ti-device-tv', + to: '/channels', + }, + ui: { + title: 'switchUi', + icon: 'ti ti-devices', + action: (ev) => { + os.popupMenu([{ + text: i18n.ts.default, + active: ui === 'default' || ui === null, + action: () => { + localStorage.setItem('ui', 'default'); + unisonReload(); + }, + }, { + text: i18n.ts.deck, + active: ui === 'deck', + action: () => { + localStorage.setItem('ui', 'deck'); + unisonReload(); + }, + }, { + text: i18n.ts.classic, + active: ui === 'classic', + action: () => { + localStorage.setItem('ui', 'classic'); + unisonReload(); + }, + }], ev.currentTarget ?? ev.target); + }, + }, + reload: { + title: 'reload', + icon: 'ti ti-refresh', + action: (ev) => { + location.reload(); + }, + }, +}); diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts new file mode 100644 index 0000000000..53e73a8d48 --- /dev/null +++ b/packages/frontend/src/nirax.ts @@ -0,0 +1,275 @@ +// NIRAX --- A lightweight router + +import { EventEmitter } from 'eventemitter3'; +import { Ref, Component, ref, shallowRef, ShallowRef } from 'vue'; +import { pleaseLogin } from '@/scripts/please-login'; +import { safeURIDecode } from '@/scripts/safe-uri-decode'; + +type RouteDef = { + path: string; + component: Component; + query?: Record; + loginRequired?: boolean; + name?: string; + hash?: string; + globalCacheKey?: string; + children?: RouteDef[]; +}; + +type ParsedPath = (string | { + name: string; + startsWith?: string; + wildcard?: boolean; + optional?: boolean; +})[]; + +export type Resolved = { route: RouteDef; props: Map; child?: Resolved; }; + +function parsePath(path: string): ParsedPath { + const res = [] as ParsedPath; + + path = path.substring(1); + + for (const part of path.split('/')) { + if (part.includes(':')) { + const prefix = part.substring(0, part.indexOf(':')); + const placeholder = part.substring(part.indexOf(':') + 1); + const wildcard = placeholder.includes('(*)'); + const optional = placeholder.endsWith('?'); + res.push({ + name: placeholder.replace('(*)', '').replace('?', ''), + startsWith: prefix !== '' ? prefix : undefined, + wildcard, + optional, + }); + } else if (part.length !== 0) { + res.push(part); + } + } + + return res; +} + +export class Router extends EventEmitter<{ + change: (ctx: { + beforePath: string; + path: string; + resolved: Resolved; + key: string; + }) => void; + replace: (ctx: { + path: string; + key: string; + }) => void; + push: (ctx: { + beforePath: string; + path: string; + route: RouteDef | null; + props: Map | null; + key: string; + }) => void; + same: () => void; +}> { + private routes: RouteDef[]; + public current: Resolved; + public currentRef: ShallowRef = shallowRef(); + public currentRoute: ShallowRef = shallowRef(); + private currentPath: string; + private currentKey = Date.now().toString(); + + public navHook: ((path: string, flag?: any) => boolean) | null = null; + + constructor(routes: Router['routes'], currentPath: Router['currentPath']) { + super(); + + this.routes = routes; + this.currentPath = currentPath; + this.navigate(currentPath, null, false); + } + + public resolve(path: string): Resolved | null { + let queryString: string | null = null; + let hash: string | null = null; + if (path[0] === '/') path = path.substring(1); + if (path.includes('#')) { + hash = path.substring(path.indexOf('#') + 1); + path = path.substring(0, path.indexOf('#')); + } + if (path.includes('?')) { + queryString = path.substring(path.indexOf('?') + 1); + path = path.substring(0, path.indexOf('?')); + } + + if (_DEV_) console.log('Routing: ', path, queryString); + + function check(routes: RouteDef[], _parts: string[]): Resolved | null { + forEachRouteLoop: + for (const route of routes) { + let parts = [..._parts]; + const props = new Map(); + + pathMatchLoop: + for (const p of parsePath(route.path)) { + if (typeof p === 'string') { + if (p === parts[0]) { + parts.shift(); + } else { + continue forEachRouteLoop; + } + } else { + if (parts[0] == null && !p.optional) { + continue forEachRouteLoop; + } + if (p.wildcard) { + if (parts.length !== 0) { + props.set(p.name, safeURIDecode(parts.join('/'))); + parts = []; + } + break pathMatchLoop; + } else { + if (p.startsWith) { + if (parts[0] == null || !parts[0].startsWith(p.startsWith)) continue forEachRouteLoop; + + props.set(p.name, safeURIDecode(parts[0].substring(p.startsWith.length))); + parts.shift(); + } else { + if (parts[0]) { + props.set(p.name, safeURIDecode(parts[0])); + } + parts.shift(); + } + } + } + } + + if (parts.length === 0) { + if (route.children) { + const child = check(route.children, []); + if (child) { + return { + route, + props, + child, + }; + } else { + continue forEachRouteLoop; + } + } + + if (route.hash != null && hash != null) { + props.set(route.hash, safeURIDecode(hash)); + } + + if (route.query != null && queryString != null) { + const queryObject = [...new URLSearchParams(queryString).entries()] + .reduce((obj, entry) => ({ ...obj, [entry[0]]: entry[1] }), {}); + + for (const q in route.query) { + const as = route.query[q]; + if (queryObject[q]) { + props.set(as, safeURIDecode(queryObject[q])); + } + } + } + + return { + route, + props, + }; + } else { + if (route.children) { + const child = check(route.children, parts); + if (child) { + return { + route, + props, + child, + }; + } else { + continue forEachRouteLoop; + } + } else { + continue forEachRouteLoop; + } + } + } + + return null; + } + + const _parts = path.split('/').filter(part => part.length !== 0); + + return check(this.routes, _parts); + } + + private navigate(path: string, key: string | null | undefined, emitChange = true) { + const beforePath = this.currentPath; + this.currentPath = path; + + const res = this.resolve(this.currentPath); + + if (res == null) { + throw new Error('no route found for: ' + path); + } + + if (res.route.loginRequired) { + pleaseLogin('/'); + } + + const isSamePath = beforePath === path; + if (isSamePath && key == null) key = this.currentKey; + this.current = res; + this.currentRef.value = res; + this.currentRoute.value = res.route; + this.currentKey = res.route.globalCacheKey ?? key ?? path; + + if (emitChange) { + this.emit('change', { + beforePath, + path, + resolved: res, + key: this.currentKey, + }); + } + + return res; + } + + public getCurrentPath() { + return this.currentPath; + } + + public getCurrentKey() { + return this.currentKey; + } + + public push(path: string, flag?: any) { + const beforePath = this.currentPath; + if (path === beforePath) { + this.emit('same'); + return; + } + if (this.navHook) { + const cancel = this.navHook(path, flag); + if (cancel) return; + } + const res = this.navigate(path, null); + this.emit('push', { + beforePath, + path, + route: res.route, + props: res.props, + key: this.currentKey, + }); + } + + public replace(path: string, key?: string | null, emitEvent = true) { + this.navigate(path, key); + if (emitEvent) { + this.emit('replace', { + path, + key: this.currentKey, + }); + } + } +} diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts new file mode 100644 index 0000000000..7e57dcb4af --- /dev/null +++ b/packages/frontend/src/os.ts @@ -0,0 +1,588 @@ +// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する + +import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue'; +import { EventEmitter } from 'eventemitter3'; +import insertTextAtCursor from 'insert-text-at-cursor'; +import * as Misskey from 'misskey-js'; +import { apiUrl, url } from '@/config'; +import MkPostFormDialog from '@/components/MkPostFormDialog.vue'; +import MkWaitingDialog from '@/components/MkWaitingDialog.vue'; +import { MenuItem } from '@/types/menu'; +import { $i } from '@/account'; + +export const pendingApiRequestsCount = ref(0); + +const apiClient = new Misskey.api.APIClient({ + origin: url, +}); + +export const api = ((endpoint: string, data: Record = {}, token?: string | null | undefined) => { + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const promise = new Promise((resolve, reject) => { + // Append a credential + if ($i) (data as any).i = $i.token; + if (token !== undefined) (data as any).i = token; + + // Send request + window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: 'omit', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +}) as typeof apiClient.request; + +export const apiGet = ((endpoint: string, data: Record = {}) => { + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const query = new URLSearchParams(data); + + const promise = new Promise((resolve, reject) => { + // Send request + window.fetch(`${apiUrl}/${endpoint}?${query}`, { + method: 'GET', + credentials: 'omit', + cache: 'default', + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +}) as typeof apiClient.request; + +export const apiWithDialog = (( + endpoint: string, + data: Record = {}, + token?: string | null | undefined, +) => { + const promise = api(endpoint, data, token); + promiseDialog(promise, null, (err) => { + alert({ + type: 'error', + text: err.message + '\n' + (err as any).id, + }); + }); + + return promise; +}) as typeof api; + +export function promiseDialog>( + promise: T, + onSuccess?: ((res: any) => void) | null, + onFailure?: ((err: Error) => void) | null, + text?: string, +): T { + const showing = ref(true); + const success = ref(false); + + promise.then(res => { + if (onSuccess) { + showing.value = false; + onSuccess(res); + } else { + success.value = true; + window.setTimeout(() => { + showing.value = false; + }, 1000); + } + }).catch(err => { + showing.value = false; + if (onFailure) { + onFailure(err); + } else { + alert({ + type: 'error', + text: err, + }); + } + }); + + // NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない) + popup(MkWaitingDialog, { + success: success, + showing: showing, + text: text, + }, {}, 'closed'); + + return promise; +} + +let popupIdCount = 0; +export const popups = ref([]) as Ref<{ + id: any; + component: any; + props: Record; +}[]>; + +const zIndexes = { + low: 1000000, + middle: 2000000, + high: 3000000, +}; +export function claimZIndex(priority: 'low' | 'middle' | 'high' = 'low'): number { + zIndexes[priority] += 100; + return zIndexes[priority]; +} + +export async function popup(component: Component, props: Record, events = {}, disposeEvent?: string) { + markRaw(component); + + const id = ++popupIdCount; + const dispose = () => { + // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ? + window.setTimeout(() => { + popups.value = popups.value.filter(popup => popup.id !== id); + }, 0); + }; + const state = { + component, + props, + events: disposeEvent ? { + ...events, + [disposeEvent]: dispose, + } : events, + id, + }; + + popups.value.push(state); + + return { + dispose, + }; +} + +export function pageWindow(path: string) { + popup(defineAsyncComponent(() => import('@/components/MkPageWindow.vue')), { + initialPath: path, + }, {}, 'closed'); +} + +export function modalPageWindow(path: string) { + popup(defineAsyncComponent(() => import('@/components/MkModalPageWindow.vue')), { + initialPath: path, + }, {}, 'closed'); +} + +export function toast(message: string) { + popup(defineAsyncComponent(() => import('@/components/MkToast.vue')), { + message, + }, {}, 'closed'); +} + +export function alert(props: { + type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; + title?: string | null; + text?: string | null; +}): Promise { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), props, { + done: result => { + resolve(); + }, + }, 'closed'); + }); +} + +export function confirm(props: { + type: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; + title?: string | null; + text?: string | null; +}): Promise<{ canceled: boolean }> { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { + ...props, + showCancelButton: true, + }, { + done: result => { + resolve(result ? result : { canceled: true }); + }, + }, 'closed'); + }); +} + +export function inputText(props: { + type?: 'text' | 'email' | 'password' | 'url'; + title?: string | null; + text?: string | null; + placeholder?: string | null; + default?: string | null; +}): Promise<{ canceled: true; result: undefined; } | { + canceled: false; result: string; +}> { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { + title: props.title, + text: props.text, + input: { + type: props.type, + placeholder: props.placeholder, + default: props.default, + }, + }, { + done: result => { + resolve(result ? result : { canceled: true }); + }, + }, 'closed'); + }); +} + +export function inputNumber(props: { + title?: string | null; + text?: string | null; + placeholder?: string | null; + default?: number | null; +}): Promise<{ canceled: true; result: undefined; } | { + canceled: false; result: number; +}> { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { + title: props.title, + text: props.text, + input: { + type: 'number', + placeholder: props.placeholder, + default: props.default, + }, + }, { + done: result => { + resolve(result ? result : { canceled: true }); + }, + }, 'closed'); + }); +} + +export function inputDate(props: { + title?: string | null; + text?: string | null; + placeholder?: string | null; + default?: Date | null; +}): Promise<{ canceled: true; result: undefined; } | { + canceled: false; result: Date; +}> { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { + title: props.title, + text: props.text, + input: { + type: 'date', + placeholder: props.placeholder, + default: props.default, + }, + }, { + done: result => { + resolve(result ? { result: new Date(result.result), canceled: false } : { canceled: true }); + }, + }, 'closed'); + }); +} + +export function select(props: { + title?: string | null; + text?: string | null; + default?: string | null; +} & ({ + items: { + value: C; + text: string; + }[]; +} | { + groupedItems: { + label: string; + items: { + value: C; + text: string; + }[]; + }[]; +})): Promise<{ canceled: true; result: undefined; } | { + canceled: false; result: C; +}> { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkDialog.vue')), { + title: props.title, + text: props.text, + select: { + items: props.items, + groupedItems: props.groupedItems, + default: props.default, + }, + }, { + done: result => { + resolve(result ? result : { canceled: true }); + }, + }, 'closed'); + }); +} + +export function success() { + return new Promise((resolve, reject) => { + const showing = ref(true); + window.setTimeout(() => { + showing.value = false; + }, 1000); + popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { + success: true, + showing: showing, + }, { + done: () => resolve(), + }, 'closed'); + }); +} + +export function waiting() { + return new Promise((resolve, reject) => { + const showing = ref(true); + popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { + success: false, + showing: showing, + }, { + done: () => resolve(), + }, 'closed'); + }); +} + +export function form(title, form) { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form }, { + done: result => { + resolve(result); + }, + }, 'closed'); + }); +} + +export async function selectUser() { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {}, { + ok: user => { + resolve(user); + }, + }, 'closed'); + }); +} + +export async function selectDriveFile(multiple: boolean) { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { + type: 'file', + multiple, + }, { + done: files => { + if (files) { + resolve(multiple ? files : files[0]); + } + }, + }, 'closed'); + }); +} + +export async function selectDriveFolder(multiple: boolean) { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { + type: 'folder', + multiple, + }, { + done: folders => { + if (folders) { + resolve(multiple ? folders : folders[0]); + } + }, + }, 'closed'); + }); +} + +export async function pickEmoji(src: HTMLElement | null, opts) { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { + src, + ...opts, + }, { + done: emoji => { + resolve(emoji); + }, + }, 'closed'); + }); +} + +export async function cropImage(image: Misskey.entities.DriveFile, options: { + aspectRatio: number; +}): Promise { + return new Promise((resolve, reject) => { + popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { + file: image, + aspectRatio: options.aspectRatio, + }, { + ok: x => { + resolve(x); + }, + }, 'closed'); + }); +} + +type AwaitType = + T extends Promise ? U : + T extends (...args: any[]) => Promise ? V : + T; +let openingEmojiPicker: AwaitType> | null = null; +let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null; +export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) { + if (openingEmojiPicker) return; + + activeTextarea = initialTextarea; + + const textareas = document.querySelectorAll('textarea, input'); + for (const textarea of Array.from(textareas)) { + textarea.addEventListener('focus', () => { + activeTextarea = textarea; + }); + } + + const observer = new MutationObserver(records => { + for (const record of records) { + for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) { + const textareas = node.querySelectorAll('textarea, input') as NodeListOf>; + for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) { + if (document.activeElement === textarea) activeTextarea = textarea; + textarea.addEventListener('focus', () => { + activeTextarea = textarea; + }); + } + } + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false, + characterData: false, + }); + + openingEmojiPicker = await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerWindow.vue')), { + src, + ...opts, + }, { + chosen: emoji => { + insertTextAtCursor(activeTextarea, emoji); + }, + closed: () => { + openingEmojiPicker!.dispose(); + openingEmojiPicker = null; + observer.disconnect(); + }, + }); +} + +export function popupMenu(items: MenuItem[] | Ref, src?: HTMLElement, options?: { + align?: string; + width?: number; + viaKeyboard?: boolean; +}) { + return new Promise((resolve, reject) => { + let dispose; + popup(defineAsyncComponent(() => import('@/components/MkPopupMenu.vue')), { + items, + src, + width: options?.width, + align: options?.align, + viaKeyboard: options?.viaKeyboard, + }, { + closed: () => { + resolve(); + dispose(); + }, + }).then(res => { + dispose = res.dispose; + }); + }); +} + +export function contextMenu(items: MenuItem[] | Ref, ev: MouseEvent) { + ev.preventDefault(); + return new Promise((resolve, reject) => { + let dispose; + popup(defineAsyncComponent(() => import('@/components/MkContextMenu.vue')), { + items, + ev, + }, { + closed: () => { + resolve(); + dispose(); + }, + }).then(res => { + dispose = res.dispose; + }); + }); +} + +export function post(props: Record = {}) { + return new Promise((resolve, reject) => { + // NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない + // NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、 + // Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、 + // 複数のpost formを開いたときに場合によってはエラーになる + // もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが + let dispose; + popup(MkPostFormDialog, props, { + closed: () => { + resolve(); + dispose(); + }, + }).then(res => { + dispose = res.dispose; + }); + }); +} + +export const deckGlobalEvents = new EventEmitter(); + +/* +export function checkExistence(fileData: ArrayBuffer): Promise { + return new Promise((resolve, reject) => { + const data = new FormData(); + data.append('md5', getMD5(fileData)); + + os.api('drive/files/find-by-hash', { + md5: getMD5(fileData) + }).then(resp => { + resolve(resp.length > 0 ? resp[0] : null); + }); + }); +}*/ diff --git a/packages/frontend/src/pages/_empty_.vue b/packages/frontend/src/pages/_empty_.vue new file mode 100644 index 0000000000..000b6decc9 --- /dev/null +++ b/packages/frontend/src/pages/_empty_.vue @@ -0,0 +1,7 @@ + + + diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue new file mode 100644 index 0000000000..232d525347 --- /dev/null +++ b/packages/frontend/src/pages/_error_.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/frontend/src/pages/_loading_.vue b/packages/frontend/src/pages/_loading_.vue new file mode 100644 index 0000000000..1dd2e46e10 --- /dev/null +++ b/packages/frontend/src/pages/_loading_.vue @@ -0,0 +1,6 @@ + + + diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue new file mode 100644 index 0000000000..3ec972bcda --- /dev/null +++ b/packages/frontend/src/pages/about-misskey.vue @@ -0,0 +1,264 @@ + + + + + diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue new file mode 100644 index 0000000000..53ce1e4b75 --- /dev/null +++ b/packages/frontend/src/pages/about.emojis.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue new file mode 100644 index 0000000000..6c92ab1264 --- /dev/null +++ b/packages/frontend/src/pages/about.federation.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue new file mode 100644 index 0000000000..0ed692c5c5 --- /dev/null +++ b/packages/frontend/src/pages/about.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue new file mode 100644 index 0000000000..a11249e75d --- /dev/null +++ b/packages/frontend/src/pages/admin-file.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue new file mode 100644 index 0000000000..bdb41b2d2c --- /dev/null +++ b/packages/frontend/src/pages/admin/_header_.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue new file mode 100644 index 0000000000..973ec871ab --- /dev/null +++ b/packages/frontend/src/pages/admin/abuses.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue new file mode 100644 index 0000000000..2ec926c65c --- /dev/null +++ b/packages/frontend/src/pages/admin/ads.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue new file mode 100644 index 0000000000..607ad8aa02 --- /dev/null +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue new file mode 100644 index 0000000000..d03961cf95 --- /dev/null +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -0,0 +1,109 @@ + + + diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue new file mode 100644 index 0000000000..5a0d3d5e51 --- /dev/null +++ b/packages/frontend/src/pages/admin/database.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue new file mode 100644 index 0000000000..6c9dee1704 --- /dev/null +++ b/packages/frontend/src/pages/admin/email-settings.vue @@ -0,0 +1,126 @@ + + + diff --git a/packages/frontend/src/pages/admin/emoji-edit-dialog.vue b/packages/frontend/src/pages/admin/emoji-edit-dialog.vue new file mode 100644 index 0000000000..bd601cb1de --- /dev/null +++ b/packages/frontend/src/pages/admin/emoji-edit-dialog.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/emojis.vue b/packages/frontend/src/pages/admin/emojis.vue new file mode 100644 index 0000000000..14c8466d73 --- /dev/null +++ b/packages/frontend/src/pages/admin/emojis.vue @@ -0,0 +1,398 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue new file mode 100644 index 0000000000..8ad6bd4fc0 --- /dev/null +++ b/packages/frontend/src/pages/admin/files.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue new file mode 100644 index 0000000000..6c07a87eeb --- /dev/null +++ b/packages/frontend/src/pages/admin/index.vue @@ -0,0 +1,316 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue new file mode 100644 index 0000000000..1bdd174de4 --- /dev/null +++ b/packages/frontend/src/pages/admin/instance-block.vue @@ -0,0 +1,51 @@ + + + diff --git a/packages/frontend/src/pages/admin/integrations.discord.vue b/packages/frontend/src/pages/admin/integrations.discord.vue new file mode 100644 index 0000000000..0a69c44c93 --- /dev/null +++ b/packages/frontend/src/pages/admin/integrations.discord.vue @@ -0,0 +1,60 @@ + + + diff --git a/packages/frontend/src/pages/admin/integrations.github.vue b/packages/frontend/src/pages/admin/integrations.github.vue new file mode 100644 index 0000000000..66419d5891 --- /dev/null +++ b/packages/frontend/src/pages/admin/integrations.github.vue @@ -0,0 +1,60 @@ + + + diff --git a/packages/frontend/src/pages/admin/integrations.twitter.vue b/packages/frontend/src/pages/admin/integrations.twitter.vue new file mode 100644 index 0000000000..1e8d882b9c --- /dev/null +++ b/packages/frontend/src/pages/admin/integrations.twitter.vue @@ -0,0 +1,60 @@ + + + diff --git a/packages/frontend/src/pages/admin/integrations.vue b/packages/frontend/src/pages/admin/integrations.vue new file mode 100644 index 0000000000..9cc35baefd --- /dev/null +++ b/packages/frontend/src/pages/admin/integrations.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/frontend/src/pages/admin/metrics.vue b/packages/frontend/src/pages/admin/metrics.vue new file mode 100644 index 0000000000..db8e448639 --- /dev/null +++ b/packages/frontend/src/pages/admin/metrics.vue @@ -0,0 +1,472 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue new file mode 100644 index 0000000000..f2ab30eaa5 --- /dev/null +++ b/packages/frontend/src/pages/admin/object-storage.vue @@ -0,0 +1,148 @@ + + + diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue new file mode 100644 index 0000000000..62dff6ce7f --- /dev/null +++ b/packages/frontend/src/pages/admin/other-settings.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue new file mode 100644 index 0000000000..c3ce5ac901 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.active-users.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue new file mode 100644 index 0000000000..024ffdc245 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue @@ -0,0 +1,346 @@ + + + + + + diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue new file mode 100644 index 0000000000..71f5a054b4 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.federation.vue @@ -0,0 +1,185 @@ + + + + + + diff --git a/packages/frontend/src/pages/admin/overview.heatmap.vue b/packages/frontend/src/pages/admin/overview.heatmap.vue new file mode 100644 index 0000000000..16d1c83b9f --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.heatmap.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue new file mode 100644 index 0000000000..29848bf03b --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.instances.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue new file mode 100644 index 0000000000..a1f63c8711 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.moderators.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue new file mode 100644 index 0000000000..94509cf006 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.pie.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue new file mode 100644 index 0000000000..1e095bddaa --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue new file mode 100644 index 0000000000..72ebddc72f --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.retention.vue b/packages/frontend/src/pages/admin/overview.retention.vue new file mode 100644 index 0000000000..feac6f8118 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.retention.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue new file mode 100644 index 0000000000..4dcf7e751a --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.stats.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue new file mode 100644 index 0000000000..5d4be11742 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue new file mode 100644 index 0000000000..d656e55200 --- /dev/null +++ b/packages/frontend/src/pages/admin/overview.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue new file mode 100644 index 0000000000..5d0d67980e --- /dev/null +++ b/packages/frontend/src/pages/admin/proxy-account.vue @@ -0,0 +1,62 @@ + + + diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue new file mode 100644 index 0000000000..5777674ae3 --- /dev/null +++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue new file mode 100644 index 0000000000..186a22c43e --- /dev/null +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue new file mode 100644 index 0000000000..8d19b49fc5 --- /dev/null +++ b/packages/frontend/src/pages/admin/queue.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue new file mode 100644 index 0000000000..4768ae67b1 --- /dev/null +++ b/packages/frontend/src/pages/admin/relays.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue new file mode 100644 index 0000000000..2682bda337 --- /dev/null +++ b/packages/frontend/src/pages/admin/security.vue @@ -0,0 +1,179 @@ + + + diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue new file mode 100644 index 0000000000..460eb92694 --- /dev/null +++ b/packages/frontend/src/pages/admin/settings.vue @@ -0,0 +1,262 @@ + + + diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue new file mode 100644 index 0000000000..d466e21907 --- /dev/null +++ b/packages/frontend/src/pages/admin/users.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue new file mode 100644 index 0000000000..6a93b3b9fa --- /dev/null +++ b/packages/frontend/src/pages/announcements.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue new file mode 100644 index 0000000000..0b2c284c99 --- /dev/null +++ b/packages/frontend/src/pages/antenna-timeline.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue new file mode 100644 index 0000000000..1d5339b44c --- /dev/null +++ b/packages/frontend/src/pages/api-console.vue @@ -0,0 +1,89 @@ + + + diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue new file mode 100644 index 0000000000..1546735266 --- /dev/null +++ b/packages/frontend/src/pages/auth.form.vue @@ -0,0 +1,60 @@ + + + diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue new file mode 100644 index 0000000000..bb55881a22 --- /dev/null +++ b/packages/frontend/src/pages/auth.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue new file mode 100644 index 0000000000..5ae7e63f99 --- /dev/null +++ b/packages/frontend/src/pages/channel-editor.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue new file mode 100644 index 0000000000..f271bb270f --- /dev/null +++ b/packages/frontend/src/pages/channel.vue @@ -0,0 +1,184 @@ + + + + + diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue new file mode 100644 index 0000000000..34e9dac196 --- /dev/null +++ b/packages/frontend/src/pages/channels.vue @@ -0,0 +1,79 @@ + + + diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue new file mode 100644 index 0000000000..e0fbcb6bed --- /dev/null +++ b/packages/frontend/src/pages/clip.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue new file mode 100644 index 0000000000..04ade5c207 --- /dev/null +++ b/packages/frontend/src/pages/drive.vue @@ -0,0 +1,25 @@ + + + diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue new file mode 100644 index 0000000000..40fe496520 --- /dev/null +++ b/packages/frontend/src/pages/emojis.emoji.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue new file mode 100644 index 0000000000..18a371a086 --- /dev/null +++ b/packages/frontend/src/pages/explore.featured.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue new file mode 100644 index 0000000000..bfee0a6c07 --- /dev/null +++ b/packages/frontend/src/pages/explore.users.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue new file mode 100644 index 0000000000..6b0bcdaf62 --- /dev/null +++ b/packages/frontend/src/pages/explore.vue @@ -0,0 +1,87 @@ + + + diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue new file mode 100644 index 0000000000..ab47efec71 --- /dev/null +++ b/packages/frontend/src/pages/favorites.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue new file mode 100644 index 0000000000..b20679ccc1 --- /dev/null +++ b/packages/frontend/src/pages/follow-requests.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue new file mode 100644 index 0000000000..828246d678 --- /dev/null +++ b/packages/frontend/src/pages/follow.vue @@ -0,0 +1,62 @@ + + + diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue new file mode 100644 index 0000000000..c8111d7890 --- /dev/null +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue new file mode 100644 index 0000000000..24a634bab5 --- /dev/null +++ b/packages/frontend/src/pages/gallery/index.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue new file mode 100644 index 0000000000..85ab1048be --- /dev/null +++ b/packages/frontend/src/pages/gallery/post.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue new file mode 100644 index 0000000000..a2a1254360 --- /dev/null +++ b/packages/frontend/src/pages/instance-info.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/packages/frontend/src/pages/messaging/index.vue b/packages/frontend/src/pages/messaging/index.vue new file mode 100644 index 0000000000..0d30998330 --- /dev/null +++ b/packages/frontend/src/pages/messaging/index.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/packages/frontend/src/pages/messaging/messaging-room.form.vue b/packages/frontend/src/pages/messaging/messaging-room.form.vue new file mode 100644 index 0000000000..84572815c0 --- /dev/null +++ b/packages/frontend/src/pages/messaging/messaging-room.form.vue @@ -0,0 +1,364 @@ + + + + + diff --git a/packages/frontend/src/pages/messaging/messaging-room.message.vue b/packages/frontend/src/pages/messaging/messaging-room.message.vue new file mode 100644 index 0000000000..dbf0e37b73 --- /dev/null +++ b/packages/frontend/src/pages/messaging/messaging-room.message.vue @@ -0,0 +1,367 @@ + + + + + diff --git a/packages/frontend/src/pages/messaging/messaging-room.vue b/packages/frontend/src/pages/messaging/messaging-room.vue new file mode 100644 index 0000000000..b6eeb9260e --- /dev/null +++ b/packages/frontend/src/pages/messaging/messaging-room.vue @@ -0,0 +1,411 @@ + + + + + diff --git a/packages/frontend/src/pages/mfm-cheat-sheet.vue b/packages/frontend/src/pages/mfm-cheat-sheet.vue new file mode 100644 index 0000000000..7c85dfb7ad --- /dev/null +++ b/packages/frontend/src/pages/mfm-cheat-sheet.vue @@ -0,0 +1,387 @@ + + + + + diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue new file mode 100644 index 0000000000..5de072cbfa --- /dev/null +++ b/packages/frontend/src/pages/miauth.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue new file mode 100644 index 0000000000..005b036696 --- /dev/null +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue new file mode 100644 index 0000000000..cb583faaeb --- /dev/null +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue new file mode 100644 index 0000000000..a409a734b5 --- /dev/null +++ b/packages/frontend/src/pages/my-antennas/editor.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue new file mode 100644 index 0000000000..9daf23f9b5 --- /dev/null +++ b/packages/frontend/src/pages/my-antennas/index.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue new file mode 100644 index 0000000000..dd6b5b3a37 --- /dev/null +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -0,0 +1,100 @@ + + + + + diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue new file mode 100644 index 0000000000..3476436b27 --- /dev/null +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue new file mode 100644 index 0000000000..f6234ffe44 --- /dev/null +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue new file mode 100644 index 0000000000..e58e44ef79 --- /dev/null +++ b/packages/frontend/src/pages/not-found.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue new file mode 100644 index 0000000000..ba2bb91239 --- /dev/null +++ b/packages/frontend/src/pages/note.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue new file mode 100644 index 0000000000..7106951de2 --- /dev/null +++ b/packages/frontend/src/pages/notifications.vue @@ -0,0 +1,95 @@ + + + diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue new file mode 100644 index 0000000000..a84cb1e80e --- /dev/null +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue new file mode 100644 index 0000000000..dc2a620c09 --- /dev/null +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue @@ -0,0 +1,57 @@ + + + diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue new file mode 100644 index 0000000000..27324bdaef --- /dev/null +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue new file mode 100644 index 0000000000..6f11e2a08b --- /dev/null +++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue new file mode 100644 index 0000000000..f99fcb202f --- /dev/null +++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue new file mode 100644 index 0000000000..15cdda5efb --- /dev/null +++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue new file mode 100644 index 0000000000..968aa12de2 --- /dev/null +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -0,0 +1,394 @@ + + + + + + + diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue new file mode 100644 index 0000000000..a95bfe485c --- /dev/null +++ b/packages/frontend/src/pages/page.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue new file mode 100644 index 0000000000..b077180df8 --- /dev/null +++ b/packages/frontend/src/pages/pages.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/packages/frontend/src/pages/preview.vue b/packages/frontend/src/pages/preview.vue new file mode 100644 index 0000000000..354f686e46 --- /dev/null +++ b/packages/frontend/src/pages/preview.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue new file mode 100644 index 0000000000..f179fbe957 --- /dev/null +++ b/packages/frontend/src/pages/registry.keys.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue new file mode 100644 index 0000000000..378420b1ba --- /dev/null +++ b/packages/frontend/src/pages/registry.value.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue new file mode 100644 index 0000000000..a2c65294fc --- /dev/null +++ b/packages/frontend/src/pages/registry.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue new file mode 100644 index 0000000000..8ec15f6425 --- /dev/null +++ b/packages/frontend/src/pages/reset-password.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue new file mode 100644 index 0000000000..edb2d8e18c --- /dev/null +++ b/packages/frontend/src/pages/scratchpad.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue new file mode 100644 index 0000000000..c080b763bb --- /dev/null +++ b/packages/frontend/src/pages/search.vue @@ -0,0 +1,38 @@ + + + diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue new file mode 100644 index 0000000000..1803129aaa --- /dev/null +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -0,0 +1,216 @@ + + + diff --git a/packages/frontend/src/pages/settings/account-info.vue b/packages/frontend/src/pages/settings/account-info.vue new file mode 100644 index 0000000000..ccd99c162a --- /dev/null +++ b/packages/frontend/src/pages/settings/account-info.vue @@ -0,0 +1,158 @@ + + + diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue new file mode 100644 index 0000000000..493d3b2618 --- /dev/null +++ b/packages/frontend/src/pages/settings/accounts.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue new file mode 100644 index 0000000000..8d7291cd10 --- /dev/null +++ b/packages/frontend/src/pages/settings/api.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue new file mode 100644 index 0000000000..05abadff23 --- /dev/null +++ b/packages/frontend/src/pages/settings/apps.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue new file mode 100644 index 0000000000..2caad22b7b --- /dev/null +++ b/packages/frontend/src/pages/settings/custom-css.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue new file mode 100644 index 0000000000..82cefe05d5 --- /dev/null +++ b/packages/frontend/src/pages/settings/deck.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/frontend/src/pages/settings/delete-account.vue b/packages/frontend/src/pages/settings/delete-account.vue new file mode 100644 index 0000000000..8a25ff39f0 --- /dev/null +++ b/packages/frontend/src/pages/settings/delete-account.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue new file mode 100644 index 0000000000..2d45b1add8 --- /dev/null +++ b/packages/frontend/src/pages/settings/drive.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue new file mode 100644 index 0000000000..3fff8c6b1d --- /dev/null +++ b/packages/frontend/src/pages/settings/email.vue @@ -0,0 +1,111 @@ + + + diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue new file mode 100644 index 0000000000..84d99d2fd7 --- /dev/null +++ b/packages/frontend/src/pages/settings/general.vue @@ -0,0 +1,196 @@ + + + diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue new file mode 100644 index 0000000000..7db267c142 --- /dev/null +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue new file mode 100644 index 0000000000..01436cd554 --- /dev/null +++ b/packages/frontend/src/pages/settings/index.vue @@ -0,0 +1,291 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/instance-mute.vue b/packages/frontend/src/pages/settings/instance-mute.vue new file mode 100644 index 0000000000..54504de188 --- /dev/null +++ b/packages/frontend/src/pages/settings/instance-mute.vue @@ -0,0 +1,53 @@ + + + diff --git a/packages/frontend/src/pages/settings/integration.vue b/packages/frontend/src/pages/settings/integration.vue new file mode 100644 index 0000000000..557fe778e6 --- /dev/null +++ b/packages/frontend/src/pages/settings/integration.vue @@ -0,0 +1,99 @@ + + + diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue new file mode 100644 index 0000000000..1cf33d34db --- /dev/null +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -0,0 +1,61 @@ + + + diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue new file mode 100644 index 0000000000..0b2776ec90 --- /dev/null +++ b/packages/frontend/src/pages/settings/navbar.vue @@ -0,0 +1,87 @@ + + + diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue new file mode 100644 index 0000000000..e85fede157 --- /dev/null +++ b/packages/frontend/src/pages/settings/notifications.vue @@ -0,0 +1,90 @@ + + + diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue new file mode 100644 index 0000000000..40bb202789 --- /dev/null +++ b/packages/frontend/src/pages/settings/other.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue new file mode 100644 index 0000000000..550bba242e --- /dev/null +++ b/packages/frontend/src/pages/settings/plugin.install.vue @@ -0,0 +1,124 @@ + + + diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue new file mode 100644 index 0000000000..905efd833d --- /dev/null +++ b/packages/frontend/src/pages/settings/plugin.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue new file mode 100644 index 0000000000..f427a170c4 --- /dev/null +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -0,0 +1,444 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue new file mode 100644 index 0000000000..915ca05767 --- /dev/null +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -0,0 +1,100 @@ + + + diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue new file mode 100644 index 0000000000..14eeeaaa11 --- /dev/null +++ b/packages/frontend/src/pages/settings/profile.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue new file mode 100644 index 0000000000..2748cd7d4e --- /dev/null +++ b/packages/frontend/src/pages/settings/reaction.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue new file mode 100644 index 0000000000..33f49eb3ef --- /dev/null +++ b/packages/frontend/src/pages/settings/security.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue new file mode 100644 index 0000000000..62627c6333 --- /dev/null +++ b/packages/frontend/src/pages/settings/sounds.sound.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue new file mode 100644 index 0000000000..ef60b2c3c9 --- /dev/null +++ b/packages/frontend/src/pages/settings/sounds.vue @@ -0,0 +1,82 @@ + + + diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue new file mode 100644 index 0000000000..608222386e --- /dev/null +++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue @@ -0,0 +1,140 @@ + + + diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue new file mode 100644 index 0000000000..86c69fa2c3 --- /dev/null +++ b/packages/frontend/src/pages/settings/statusbar.vue @@ -0,0 +1,54 @@ + + + diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue new file mode 100644 index 0000000000..52a436e18d --- /dev/null +++ b/packages/frontend/src/pages/settings/theme.install.vue @@ -0,0 +1,80 @@ + + + diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue new file mode 100644 index 0000000000..409f0af650 --- /dev/null +++ b/packages/frontend/src/pages/settings/theme.manage.vue @@ -0,0 +1,78 @@ + + + diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue new file mode 100644 index 0000000000..f37c213b06 --- /dev/null +++ b/packages/frontend/src/pages/settings/theme.vue @@ -0,0 +1,409 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue new file mode 100644 index 0000000000..c8ec1ea586 --- /dev/null +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -0,0 +1,95 @@ + + + diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue new file mode 100644 index 0000000000..00a547da69 --- /dev/null +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -0,0 +1,82 @@ + + + diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue new file mode 100644 index 0000000000..9be23ee4f0 --- /dev/null +++ b/packages/frontend/src/pages/settings/webhook.vue @@ -0,0 +1,53 @@ + + + diff --git a/packages/frontend/src/pages/settings/word-mute.vue b/packages/frontend/src/pages/settings/word-mute.vue new file mode 100644 index 0000000000..6961d8151d --- /dev/null +++ b/packages/frontend/src/pages/settings/word-mute.vue @@ -0,0 +1,128 @@ + + + diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue new file mode 100644 index 0000000000..a7e797eeab --- /dev/null +++ b/packages/frontend/src/pages/share.vue @@ -0,0 +1,169 @@ + + + + + diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue new file mode 100644 index 0000000000..5459532310 --- /dev/null +++ b/packages/frontend/src/pages/signup-complete.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue new file mode 100644 index 0000000000..72775ed5c9 --- /dev/null +++ b/packages/frontend/src/pages/tag.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue new file mode 100644 index 0000000000..d8ff170ca2 --- /dev/null +++ b/packages/frontend/src/pages/theme-editor.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/packages/frontend/src/pages/timeline.tutorial.vue b/packages/frontend/src/pages/timeline.tutorial.vue new file mode 100644 index 0000000000..ae7b098b90 --- /dev/null +++ b/packages/frontend/src/pages/timeline.tutorial.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue new file mode 100644 index 0000000000..1c9e389367 --- /dev/null +++ b/packages/frontend/src/pages/timeline.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue new file mode 100644 index 0000000000..addc8db9e6 --- /dev/null +++ b/packages/frontend/src/pages/user-info.vue @@ -0,0 +1,485 @@ + + + + + + + diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue new file mode 100644 index 0000000000..fdb3167375 --- /dev/null +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue new file mode 100644 index 0000000000..8c71aacb0c --- /dev/null +++ b/packages/frontend/src/pages/user/clips.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue new file mode 100644 index 0000000000..d42acd838f --- /dev/null +++ b/packages/frontend/src/pages/user/follow-list.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue new file mode 100644 index 0000000000..17c2843381 --- /dev/null +++ b/packages/frontend/src/pages/user/followers.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue new file mode 100644 index 0000000000..03892ec03d --- /dev/null +++ b/packages/frontend/src/pages/user/following.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue new file mode 100644 index 0000000000..b80e83fb11 --- /dev/null +++ b/packages/frontend/src/pages/user/gallery.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue new file mode 100644 index 0000000000..43c1b37e1d --- /dev/null +++ b/packages/frontend/src/pages/user/home.vue @@ -0,0 +1,530 @@ + + + + + diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue new file mode 100644 index 0000000000..523072d2e6 --- /dev/null +++ b/packages/frontend/src/pages/user/index.activity.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/frontend/src/pages/user/index.photos.vue b/packages/frontend/src/pages/user/index.photos.vue new file mode 100644 index 0000000000..b33979a79d --- /dev/null +++ b/packages/frontend/src/pages/user/index.photos.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue new file mode 100644 index 0000000000..41983a5ae8 --- /dev/null +++ b/packages/frontend/src/pages/user/index.timeline.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue new file mode 100644 index 0000000000..6e895cd8d7 --- /dev/null +++ b/packages/frontend/src/pages/user/index.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/packages/frontend/src/pages/user/pages.vue b/packages/frontend/src/pages/user/pages.vue new file mode 100644 index 0000000000..7833d6c42c --- /dev/null +++ b/packages/frontend/src/pages/user/pages.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue new file mode 100644 index 0000000000..ab3df34301 --- /dev/null +++ b/packages/frontend/src/pages/user/reactions.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue new file mode 100644 index 0000000000..bfa54d39f2 --- /dev/null +++ b/packages/frontend/src/pages/welcome.entrance.a.vue @@ -0,0 +1,309 @@ + + + + + + + diff --git a/packages/frontend/src/pages/welcome.entrance.b.vue b/packages/frontend/src/pages/welcome.entrance.b.vue new file mode 100644 index 0000000000..8230adaf1f --- /dev/null +++ b/packages/frontend/src/pages/welcome.entrance.b.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/packages/frontend/src/pages/welcome.entrance.c.vue b/packages/frontend/src/pages/welcome.entrance.c.vue new file mode 100644 index 0000000000..d2d07bb1f0 --- /dev/null +++ b/packages/frontend/src/pages/welcome.entrance.c.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue new file mode 100644 index 0000000000..2729d30d4b --- /dev/null +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue new file mode 100644 index 0000000000..d6a88540d1 --- /dev/null +++ b/packages/frontend/src/pages/welcome.timeline.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue new file mode 100644 index 0000000000..a1c3fc2abb --- /dev/null +++ b/packages/frontend/src/pages/welcome.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts new file mode 100644 index 0000000000..642e1f4f7f --- /dev/null +++ b/packages/frontend/src/pizzax.ts @@ -0,0 +1,169 @@ +// PIZZAX --- A lightweight store + +import { onUnmounted, Ref, ref, watch } from 'vue'; +import { $i } from './account'; +import { api } from './os'; +import { stream } from './stream'; + +type StateDef = Record; + +type ArrayElement
= A extends readonly (infer T)[] ? T : never; + +const connection = $i && stream.useChannel('main'); + +export class Storage { + public readonly key: string; + public readonly keyForLocalStorage: string; + + public readonly def: T; + + // TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487 + public readonly state: { [K in keyof T]: T[K]['default'] }; + public readonly reactiveState: { [K in keyof T]: Ref }; + + constructor(key: string, def: T) { + this.key = key; + this.keyForLocalStorage = 'pizzax::' + key; + this.def = def; + + // TODO: indexedDBにする + const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}'); + const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {}; + const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {}; + + const state = {}; + const reactiveState = {}; + for (const [k, v] of Object.entries(def)) { + if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) { + state[k] = deviceState[k]; + } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) { + state[k] = registryCache[k]; + } else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) { + state[k] = deviceAccountState[k]; + } else { + state[k] = v.default; + if (_DEV_) console.log('Use default value', k, v.default); + } + } + for (const [k, v] of Object.entries(state)) { + reactiveState[k] = ref(v); + } + this.state = state as any; + this.reactiveState = reactiveState as any; + + if ($i) { + // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) + window.setTimeout(() => { + api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => { + const cache = {}; + for (const [k, v] of Object.entries(def)) { + if (v.where === 'account') { + if (Object.prototype.hasOwnProperty.call(kvs, k)) { + state[k] = kvs[k]; + reactiveState[k].value = kvs[k]; + cache[k] = kvs[k]; + } else { + state[k] = v.default; + reactiveState[k].value = v.default; + } + } + } + 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; + + 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)); + } + }); + } + } + + public set(key: K, value: T[K]['default']): void { + if (_DEV_) console.log('set', key, value); + + this.state[key] = value; + this.reactiveState[key].value = value; + + switch (this.def[key].where) { + case 'device': { + const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}'); + deviceState[key] = value; + localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState)); + break; + } + case 'deviceAccount': { + if ($i == null) break; + const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}'); + deviceAccountState[key] = value; + localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState)); + break; + } + case 'account': { + if ($i == null) break; + const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}'); + cache[key] = value; + localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache)); + api('i/registry/set', { + scope: ['client', this.key], + key: key, + value: value, + }); + break; + } + } + } + + public push(key: K, value: ArrayElement): void { + const currentState = this.state[key]; + this.set(key, [...currentState, value]); + } + + public reset(key: keyof T) { + this.set(key, this.def[key].default); + } + + /** + * 特定のキーの、簡易的なgetter/setterを作ります + * 主にvue場で設定コントロールのmodelとして使う用 + */ + public makeGetterSetter(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) { + const valueRef = ref(this.state[key]); + + const stop = watch(this.reactiveState[key], val => { + valueRef.value = val; + }); + + // NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする + onUnmounted(() => { + stop(); + }); + + // TODO: VueのcustomRef使うと良い感じになるかも + return { + get: () => { + if (getter) { + return getter(valueRef.value); + } else { + return valueRef.value; + } + }, + set: (value: unknown) => { + const val = setter ? setter(value) : value; + this.set(key, val); + valueRef.value = val; + }, + }; + } +} diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts new file mode 100644 index 0000000000..3a00cd0455 --- /dev/null +++ b/packages/frontend/src/plugin.ts @@ -0,0 +1,123 @@ +import { AiScript, utils, values } from '@syuilo/aiscript'; +import { deserialize } from '@syuilo/aiscript/built/serializer'; +import { jsToVal } from '@syuilo/aiscript/built/interpreter/util'; +import { createAiScriptEnv } from '@/scripts/aiscript/api'; +import { inputText } from '@/os'; +import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions } from '@/store'; + +const pluginContexts = new Map(); + +export function install(plugin) { + console.info('Plugin installed:', plugin.name, 'v' + plugin.version); + + const aiscript = new AiScript(createPluginEnv({ + plugin: plugin, + storageKey: 'plugins:' + plugin.id, + }), { + in: (q) => { + return new Promise(ok => { + inputText({ + title: q, + }).then(({ canceled, result: a }) => { + ok(a); + }); + }); + }, + out: (value) => { + console.log(value); + }, + log: (type, params) => { + }, + }); + + initPlugin({ plugin, aiscript }); + + aiscript.exec(deserialize(plugin.ast)); +} + +function createPluginEnv(opts) { + const config = new Map(); + for (const [k, v] of Object.entries(opts.plugin.config || {})) { + config.set(k, jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default)); + } + + return { + ...createAiScriptEnv({ ...opts, token: opts.plugin.token }), + //#region Deprecated + 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { + registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => { + registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { + registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + //#endregion + 'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => { + registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => { + registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => { + registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler }); + }), + 'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => { + registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler }); + }), + 'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => { + registerNotePostInterruptor({ pluginId: opts.plugin.id, handler }); + }), + 'Plugin:open_url': values.FN_NATIVE(([url]) => { + window.open(url.value, '_blank'); + }), + 'Plugin:config': values.OBJ(config), + }; +} + +function initPlugin({ plugin, aiscript }) { + pluginContexts.set(plugin.id, aiscript); +} + +function registerPostFormAction({ pluginId, title, handler }) { + postFormActions.push({ + title, handler: (form, update) => { + pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => { + update(key.value, value.value); + })]); + }, + }); +} + +function registerUserAction({ pluginId, title, handler }) { + userActions.push({ + title, handler: (user) => { + pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]); + }, + }); +} + +function registerNoteAction({ pluginId, title, handler }) { + noteActions.push({ + title, handler: (note) => { + pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]); + }, + }); +} + +function registerNoteViewInterruptor({ pluginId, handler }) { + noteViewInterruptors.push({ + handler: async (note) => { + return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)])); + }, + }); +} + +function registerNotePostInterruptor({ pluginId, handler }) { + notePostInterruptors.push({ + handler: async (note) => { + return utils.valToJs(await pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)])); + }, + }); +} diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts new file mode 100644 index 0000000000..111b15e0a6 --- /dev/null +++ b/packages/frontend/src/router.ts @@ -0,0 +1,501 @@ +import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue'; +import { Router } from '@/nirax'; +import { $i, iAmModerator } from '@/account'; +import MkLoading from '@/pages/_loading_.vue'; +import MkError from '@/pages/_error_.vue'; +import { ui } from '@/config'; + +const page = (loader: AsyncComponentLoader) => defineAsyncComponent({ + loader: loader, + loadingComponent: MkLoading, + errorComponent: MkError, +}); + +export const routes = [{ + path: '/@:initUser/pages/:initPageName/view-source', + component: page(() => import('./pages/page-editor/page-editor.vue')), +}, { + path: '/@:username/pages/:pageName', + component: page(() => import('./pages/page.vue')), +}, { + path: '/@:acct/following', + component: page(() => import('./pages/user/following.vue')), +}, { + path: '/@:acct/followers', + component: page(() => import('./pages/user/followers.vue')), +}, { + name: 'user', + path: '/@:acct/:page?', + component: page(() => import('./pages/user/index.vue')), +}, { + name: 'note', + path: '/notes/:noteId', + component: page(() => import('./pages/note.vue')), +}, { + path: '/clips/:clipId', + component: page(() => import('./pages/clip.vue')), +}, { + path: '/user-info/:userId', + component: page(() => import('./pages/user-info.vue')), +}, { + path: '/instance-info/:host', + component: page(() => import('./pages/instance-info.vue')), +}, { + name: 'settings', + path: '/settings', + component: page(() => import('./pages/settings/index.vue')), + loginRequired: true, + children: [{ + path: '/profile', + name: 'profile', + component: page(() => import('./pages/settings/profile.vue')), + }, { + path: '/privacy', + name: 'privacy', + component: page(() => import('./pages/settings/privacy.vue')), + }, { + path: '/reaction', + name: 'reaction', + component: page(() => import('./pages/settings/reaction.vue')), + }, { + path: '/drive', + name: 'drive', + component: page(() => import('./pages/settings/drive.vue')), + }, { + path: '/notifications', + name: 'notifications', + component: page(() => import('./pages/settings/notifications.vue')), + }, { + path: '/email', + name: 'email', + component: page(() => import('./pages/settings/email.vue')), + }, { + path: '/integration', + name: 'integration', + component: page(() => import('./pages/settings/integration.vue')), + }, { + path: '/security', + name: 'security', + component: page(() => import('./pages/settings/security.vue')), + }, { + path: '/general', + name: 'general', + component: page(() => import('./pages/settings/general.vue')), + }, { + path: '/theme/install', + name: 'theme', + component: page(() => import('./pages/settings/theme.install.vue')), + }, { + path: '/theme/manage', + name: 'theme', + component: page(() => import('./pages/settings/theme.manage.vue')), + }, { + path: '/theme', + name: 'theme', + component: page(() => import('./pages/settings/theme.vue')), + }, { + path: '/navbar', + name: 'navbar', + component: page(() => import('./pages/settings/navbar.vue')), + }, { + path: '/statusbar', + name: 'statusbar', + component: page(() => import('./pages/settings/statusbar.vue')), + }, { + path: '/sounds', + name: 'sounds', + component: page(() => import('./pages/settings/sounds.vue')), + }, { + path: '/plugin/install', + name: 'plugin', + component: page(() => import('./pages/settings/plugin.install.vue')), + }, { + path: '/plugin', + name: 'plugin', + component: page(() => import('./pages/settings/plugin.vue')), + }, { + path: '/import-export', + name: 'import-export', + component: page(() => import('./pages/settings/import-export.vue')), + }, { + path: '/instance-mute', + name: 'instance-mute', + component: page(() => import('./pages/settings/instance-mute.vue')), + }, { + path: '/mute-block', + name: 'mute-block', + component: page(() => import('./pages/settings/mute-block.vue')), + }, { + path: '/word-mute', + name: 'word-mute', + component: page(() => import('./pages/settings/word-mute.vue')), + }, { + path: '/api', + name: 'api', + component: page(() => import('./pages/settings/api.vue')), + }, { + path: '/apps', + name: 'api', + component: page(() => import('./pages/settings/apps.vue')), + }, { + path: '/webhook/edit/:webhookId', + name: 'webhook', + component: page(() => import('./pages/settings/webhook.edit.vue')), + }, { + path: '/webhook/new', + name: 'webhook', + component: page(() => import('./pages/settings/webhook.new.vue')), + }, { + path: '/webhook', + name: 'webhook', + component: page(() => import('./pages/settings/webhook.vue')), + }, { + path: '/deck', + name: 'deck', + component: page(() => import('./pages/settings/deck.vue')), + }, { + path: '/preferences-backups', + name: 'preferences-backups', + component: page(() => import('./pages/settings/preferences-backups.vue')), + }, { + path: '/custom-css', + name: 'general', + component: page(() => import('./pages/settings/custom-css.vue')), + }, { + path: '/accounts', + name: 'profile', + component: page(() => import('./pages/settings/accounts.vue')), + }, { + path: '/account-info', + name: 'other', + component: page(() => import('./pages/settings/account-info.vue')), + }, { + path: '/delete-account', + name: 'other', + component: page(() => import('./pages/settings/delete-account.vue')), + }, { + path: '/other', + name: 'other', + component: page(() => import('./pages/settings/other.vue')), + }, { + path: '/', + component: page(() => import('./pages/_empty_.vue')), + }], +}, { + path: '/reset-password/:token?', + component: page(() => import('./pages/reset-password.vue')), +}, { + path: '/signup-complete/:code', + component: page(() => import('./pages/signup-complete.vue')), +}, { + path: '/announcements', + component: page(() => import('./pages/announcements.vue')), +}, { + path: '/about', + component: page(() => import('./pages/about.vue')), + hash: 'initialTab', +}, { + path: '/about-misskey', + component: page(() => import('./pages/about-misskey.vue')), +}, { + path: '/theme-editor', + component: page(() => import('./pages/theme-editor.vue')), + loginRequired: true, +}, { + path: '/explore/tags/:tag', + component: page(() => import('./pages/explore.vue')), +}, { + path: '/explore', + component: page(() => import('./pages/explore.vue')), +}, { + path: '/search', + component: page(() => import('./pages/search.vue')), + query: { + q: 'query', + channel: 'channel', + }, +}, { + path: '/authorize-follow', + component: page(() => import('./pages/follow.vue')), + loginRequired: true, +}, { + path: '/share', + component: page(() => import('./pages/share.vue')), + loginRequired: true, +}, { + path: '/api-console', + component: page(() => import('./pages/api-console.vue')), + loginRequired: true, +}, { + path: '/mfm-cheat-sheet', + component: page(() => import('./pages/mfm-cheat-sheet.vue')), +}, { + path: '/scratchpad', + component: page(() => import('./pages/scratchpad.vue')), +}, { + path: '/preview', + component: page(() => import('./pages/preview.vue')), +}, { + path: '/auth/:token', + component: page(() => import('./pages/auth.vue')), +}, { + path: '/miauth/:session', + component: page(() => import('./pages/miauth.vue')), + query: { + callback: 'callback', + name: 'name', + icon: 'icon', + permission: 'permission', + }, +}, { + path: '/tags/:tag', + component: page(() => import('./pages/tag.vue')), +}, { + path: '/pages/new', + component: page(() => import('./pages/page-editor/page-editor.vue')), + loginRequired: true, +}, { + path: '/pages/edit/:initPageId', + component: page(() => import('./pages/page-editor/page-editor.vue')), + loginRequired: true, +}, { + path: '/pages', + component: page(() => import('./pages/pages.vue')), +}, { + path: '/gallery/:postId/edit', + component: page(() => import('./pages/gallery/edit.vue')), + loginRequired: true, +}, { + path: '/gallery/new', + component: page(() => import('./pages/gallery/edit.vue')), + loginRequired: true, +}, { + path: '/gallery/:postId', + component: page(() => import('./pages/gallery/post.vue')), +}, { + path: '/gallery', + component: page(() => import('./pages/gallery/index.vue')), +}, { + path: '/channels/:channelId/edit', + component: page(() => import('./pages/channel-editor.vue')), + loginRequired: true, +}, { + path: '/channels/new', + component: page(() => import('./pages/channel-editor.vue')), + loginRequired: true, +}, { + path: '/channels/:channelId', + component: page(() => import('./pages/channel.vue')), +}, { + path: '/channels', + component: page(() => import('./pages/channels.vue')), +}, { + path: '/registry/keys/system/:path(*)?', + component: page(() => import('./pages/registry.keys.vue')), +}, { + path: '/registry/value/system/:path(*)?', + component: page(() => import('./pages/registry.value.vue')), +}, { + path: '/registry', + component: page(() => import('./pages/registry.vue')), +}, { + path: '/admin/file/:fileId', + component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), +}, { + path: '/admin', + component: iAmModerator ? page(() => import('./pages/admin/index.vue')) : page(() => import('./pages/not-found.vue')), + children: [{ + path: '/overview', + name: 'overview', + component: page(() => import('./pages/admin/overview.vue')), + }, { + path: '/users', + name: 'users', + component: page(() => import('./pages/admin/users.vue')), + }, { + path: '/emojis', + name: 'emojis', + component: page(() => import('./pages/admin/emojis.vue')), + }, { + path: '/queue', + name: 'queue', + component: page(() => import('./pages/admin/queue.vue')), + }, { + path: '/files', + name: 'files', + component: page(() => import('./pages/admin/files.vue')), + }, { + path: '/announcements', + name: 'announcements', + component: page(() => import('./pages/admin/announcements.vue')), + }, { + path: '/ads', + name: 'ads', + component: page(() => import('./pages/admin/ads.vue')), + }, { + path: '/database', + name: 'database', + component: page(() => import('./pages/admin/database.vue')), + }, { + path: '/abuses', + name: 'abuses', + component: page(() => import('./pages/admin/abuses.vue')), + }, { + path: '/settings', + name: 'settings', + component: page(() => import('./pages/admin/settings.vue')), + }, { + path: '/email-settings', + name: 'email-settings', + component: page(() => import('./pages/admin/email-settings.vue')), + }, { + path: '/object-storage', + name: 'object-storage', + component: page(() => import('./pages/admin/object-storage.vue')), + }, { + path: '/security', + name: 'security', + component: page(() => import('./pages/admin/security.vue')), + }, { + path: '/relays', + name: 'relays', + component: page(() => import('./pages/admin/relays.vue')), + }, { + path: '/integrations', + name: 'integrations', + component: page(() => import('./pages/admin/integrations.vue')), + }, { + path: '/instance-block', + name: 'instance-block', + component: page(() => import('./pages/admin/instance-block.vue')), + }, { + path: '/proxy-account', + name: 'proxy-account', + component: page(() => import('./pages/admin/proxy-account.vue')), + }, { + path: '/other-settings', + name: 'other-settings', + component: page(() => import('./pages/admin/other-settings.vue')), + }, { + path: '/', + component: page(() => import('./pages/_empty_.vue')), + }], +}, { + path: '/my/notifications', + component: page(() => import('./pages/notifications.vue')), + loginRequired: true, +}, { + path: '/my/favorites', + component: page(() => import('./pages/favorites.vue')), + loginRequired: true, +}, { + name: 'messaging', + path: '/my/messaging', + component: page(() => import('./pages/messaging/index.vue')), + loginRequired: true, +}, { + path: '/my/messaging/:userAcct', + component: page(() => import('./pages/messaging/messaging-room.vue')), + loginRequired: true, +}, { + path: '/my/messaging/group/:groupId', + component: page(() => import('./pages/messaging/messaging-room.vue')), + loginRequired: true, +}, { + path: '/my/drive/folder/:folder', + component: page(() => import('./pages/drive.vue')), + loginRequired: true, +}, { + path: '/my/drive', + component: page(() => import('./pages/drive.vue')), + loginRequired: true, +}, { + path: '/my/follow-requests', + component: page(() => import('./pages/follow-requests.vue')), + loginRequired: true, +}, { + path: '/my/lists/:listId', + component: page(() => import('./pages/my-lists/list.vue')), + loginRequired: true, +}, { + path: '/my/lists', + component: page(() => import('./pages/my-lists/index.vue')), + loginRequired: true, +}, { + path: '/my/clips', + component: page(() => import('./pages/my-clips/index.vue')), + loginRequired: true, +}, { + path: '/my/antennas/create', + component: page(() => import('./pages/my-antennas/create.vue')), + loginRequired: true, +}, { + path: '/my/antennas/:antennaId', + component: page(() => import('./pages/my-antennas/edit.vue')), + loginRequired: true, +}, { + path: '/my/antennas', + component: page(() => import('./pages/my-antennas/index.vue')), + loginRequired: true, +}, { + path: '/timeline/list/:listId', + component: page(() => import('./pages/user-list-timeline.vue')), + loginRequired: true, +}, { + path: '/timeline/antenna/:antennaId', + component: page(() => import('./pages/antenna-timeline.vue')), + loginRequired: true, +}, { + name: 'index', + path: '/', + component: $i ? page(() => import('./pages/timeline.vue')) : page(() => import('./pages/welcome.vue')), + globalCacheKey: 'index', +}, { + path: '/:(*)', + component: page(() => import('./pages/not-found.vue')), +}]; + +export const mainRouter = new Router(routes, location.pathname + location.search + location.hash); + +window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); + +// TODO: このファイルでスクロール位置も管理する設計だとdeckに対応できないのでなんとかする +// スクロール位置取得+スクロール位置設定関数をprovideする感じでも良いかも + +const scrollPosStore = new Map(); + +window.setInterval(() => { + scrollPosStore.set(window.history.state?.key, window.scrollY); +}, 1000); + +mainRouter.addListener('push', ctx => { + window.history.pushState({ key: ctx.key }, '', ctx.path); + const scrollPos = scrollPosStore.get(ctx.key) ?? 0; + window.scroll({ top: scrollPos, behavior: 'instant' }); + if (scrollPos !== 0) { + window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール + window.scroll({ top: scrollPos, behavior: 'instant' }); + }, 100); + } +}); + +mainRouter.addListener('replace', ctx => { + window.history.replaceState({ key: ctx.key }, '', ctx.path); +}); + +mainRouter.addListener('same', () => { + window.scroll({ top: 0, behavior: 'smooth' }); +}); + +window.addEventListener('popstate', (event) => { + mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key, false); + const scrollPos = scrollPosStore.get(event.state?.key) ?? 0; + window.scroll({ top: scrollPos, behavior: 'instant' }); + window.setTimeout(() => { // 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール + window.scroll({ top: scrollPos, behavior: 'instant' }); + }, 100); +}); + +export function useRouter(): Router { + return inject('router', null) ?? mainRouter; +} diff --git a/packages/frontend/src/scripts/2fa.ts b/packages/frontend/src/scripts/2fa.ts new file mode 100644 index 0000000000..62a38ff02a --- /dev/null +++ b/packages/frontend/src/scripts/2fa.ts @@ -0,0 +1,33 @@ +export function byteify(string: string, encoding: 'ascii' | 'base64' | 'hex') { + switch (encoding) { + case 'ascii': + return Uint8Array.from(string, c => c.charCodeAt(0)); + case 'base64': + return Uint8Array.from( + atob( + string + .replace(/-/g, '+') + .replace(/_/g, '/'), + ), + c => c.charCodeAt(0), + ); + case 'hex': + return new Uint8Array( + string + .match(/.{1,2}/g) + .map(byte => parseInt(byte, 16)), + ); + } +} + +export function hexify(buffer: ArrayBuffer) { + return Array.from(new Uint8Array(buffer)) + .reduce( + (str, byte) => str + byte.toString(16).padStart(2, '0'), + '', + ); +} + +export function stringify(buffer: ArrayBuffer) { + return String.fromCharCode(... new Uint8Array(buffer)); +} diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts new file mode 100644 index 0000000000..6debcb8a13 --- /dev/null +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -0,0 +1,43 @@ +import { utils, values } from '@syuilo/aiscript'; +import * as os from '@/os'; +import { $i } from '@/account'; + +export function createAiScriptEnv(opts) { + let apiRequests = 0; + return { + USER_ID: $i ? values.STR($i.id) : values.NULL, + USER_NAME: $i ? values.STR($i.name) : values.NULL, + USER_USERNAME: $i ? values.STR($i.username) : values.NULL, + 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { + await os.alert({ + type: type ? type.value : 'info', + title: title.value, + text: text.value, + }); + }), + 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { + const confirm = await os.confirm({ + type: type ? type.value : 'question', + title: title.value, + text: text.value, + }); + return confirm.canceled ? values.FALSE : values.TRUE; + }), + 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { + if (token) utils.assertString(token); + apiRequests++; + if (apiRequests > 16) return values.NULL; + const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null)); + return utils.jsToVal(res); + }), + 'Mk:save': values.FN_NATIVE(([key, value]) => { + utils.assertString(key); + localStorage.setItem('aiscript:' + opts.storageKey + ':' + key.value, JSON.stringify(utils.valToJs(value))); + return values.NULL; + }), + 'Mk:load': values.FN_NATIVE(([key]) => { + utils.assertString(key); + return utils.jsToVal(JSON.parse(localStorage.getItem('aiscript:' + opts.storageKey + ':' + key.value))); + }), + }; +} diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts new file mode 100644 index 0000000000..4620c8b735 --- /dev/null +++ b/packages/frontend/src/scripts/array.ts @@ -0,0 +1,149 @@ +import { EndoRelation, Predicate } from './relation'; + +/** + * Count the number of elements that satisfy the predicate + */ + +export function countIf(f: Predicate, xs: T[]): number { + return xs.filter(f).length; +} + +/** + * Count the number of elements that is equal to the element + */ +export function count(a: T, xs: T[]): number { + return countIf(x => x === a, xs); +} + +/** + * Concatenate an array of arrays + */ +export function concat(xss: T[][]): T[] { + return ([] as T[]).concat(...xss); +} + +/** + * Intersperse the element between the elements of the array + * @param sep The element to be interspersed + */ +export function intersperse(sep: T, xs: T[]): T[] { + return concat(xs.map(x => [sep, x])).slice(1); +} + +/** + * Returns the array of elements that is not equal to the element + */ +export function erase(a: T, xs: T[]): T[] { + return xs.filter(x => x !== a); +} + +/** + * Finds the array of all elements in the first array not contained in the second array. + * The order of result values are determined by the first array. + */ +export function difference(xs: T[], ys: T[]): T[] { + return xs.filter(x => !ys.includes(x)); +} + +/** + * Remove all but the first element from every group of equivalent elements + */ +export function unique(xs: T[]): T[] { + return [...new Set(xs)]; +} + +export function uniqueBy(values: TValue[], keySelector: (value: TValue) => TKey): TValue[] { + const map = new Map(); + + for (const value of values) { + const key = keySelector(value); + if (!map.has(key)) map.set(key, value); + } + + return [...map.values()]; +} + +export function sum(xs: number[]): number { + return xs.reduce((a, b) => a + b, 0); +} + +export function maximum(xs: number[]): number { + return Math.max(...xs); +} + +/** + * Splits an array based on the equivalence relation. + * The concatenation of the result is equal to the argument. + */ +export function groupBy(f: EndoRelation, xs: T[]): T[][] { + const groups = [] as T[][]; + for (const x of xs) { + if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) { + groups[groups.length - 1].push(x); + } else { + groups.push([x]); + } + } + return groups; +} + +/** + * Splits an array based on the equivalence relation induced by the function. + * The concatenation of the result is equal to the argument. + */ +export function groupOn(f: (x: T) => S, xs: T[]): T[][] { + return groupBy((a, b) => f(a) === f(b), xs); +} + +export function groupByX(collections: T[], keySelector: (x: T) => string) { + return collections.reduce((obj: Record, item: T) => { + const key = keySelector(item); + if (typeof obj[key] === 'undefined') { + obj[key] = []; + } + + obj[key].push(item); + + return obj; + }, {}); +} + +/** + * Compare two arrays by lexicographical order + */ +export function lessThan(xs: number[], ys: number[]): boolean { + for (let i = 0; i < Math.min(xs.length, ys.length); i++) { + if (xs[i] < ys[i]) return true; + if (xs[i] > ys[i]) return false; + } + return xs.length < ys.length; +} + +/** + * Returns the longest prefix of elements that satisfy the predicate + */ +export function takeWhile(f: Predicate, xs: T[]): T[] { + const ys: T[] = []; + for (const x of xs) { + if (f(x)) { + ys.push(x); + } else { + break; + } + } + return ys; +} + +export function cumulativeSum(xs: number[]): number[] { + const ys = Array.from(xs); // deep copy + for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1]; + return ys; +} + +export function toArray(x: T | T[] | undefined): T[] { + return Array.isArray(x) ? x : x != null ? [x] : []; +} + +export function toSingle(x: T | T[] | undefined): T | undefined { + return Array.isArray(x) ? x[0] : x; +} diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts new file mode 100644 index 0000000000..1bae3790f5 --- /dev/null +++ b/packages/frontend/src/scripts/autocomplete.ts @@ -0,0 +1,276 @@ +import { nextTick, Ref, ref, defineAsyncComponent } from 'vue'; +import getCaretCoordinates from 'textarea-caret'; +import { toASCII } from 'punycode/'; +import { popup } from '@/os'; + +export class Autocomplete { + private suggestion: { + x: Ref; + y: Ref; + q: Ref; + close: () => void; + } | null; + private textarea: HTMLInputElement | HTMLTextAreaElement; + private currentType: string; + private textRef: Ref; + private opening: boolean; + + private get text(): string { + // Use raw .value to get the latest value + // (Because v-model does not update while composition) + return this.textarea.value; + } + + private set text(text: string) { + // Use ref value to notify other watchers + // (Because .value setter never fires input/change events) + this.textRef.value = text; + } + + /** + * 対象のテキストエリアを与えてインスタンスを初期化します。 + */ + constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref) { + //#region BIND + this.onInput = this.onInput.bind(this); + this.complete = this.complete.bind(this); + this.close = this.close.bind(this); + //#endregion + + this.suggestion = null; + this.textarea = textarea; + this.textRef = textRef; + this.opening = false; + + this.attach(); + } + + /** + * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 + */ + public attach() { + this.textarea.addEventListener('input', this.onInput); + } + + /** + * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 + */ + public detach() { + this.textarea.removeEventListener('input', this.onInput); + this.close(); + } + + /** + * テキスト入力時 + */ + private onInput() { + const caretPos = this.textarea.selectionStart; + const text = this.text.substr(0, caretPos).split('\n').pop()!; + + const mentionIndex = text.lastIndexOf('@'); + const hashtagIndex = text.lastIndexOf('#'); + const emojiIndex = text.lastIndexOf(':'); + const mfmTagIndex = text.lastIndexOf('$'); + + const max = Math.max( + mentionIndex, + hashtagIndex, + emojiIndex, + mfmTagIndex); + + if (max === -1) { + this.close(); + return; + } + + const isMention = mentionIndex !== -1; + const isHashtag = hashtagIndex !== -1; + const isMfmTag = mfmTagIndex !== -1; + const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); + + let opened = false; + + if (isMention) { + const username = text.substr(mentionIndex + 1); + if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) { + this.open('user', username); + opened = true; + } else if (username === '') { + this.open('user', null); + opened = true; + } + } + + if (isHashtag && !opened) { + const hashtag = text.substr(hashtagIndex + 1); + if (!hashtag.includes(' ')) { + this.open('hashtag', hashtag); + opened = true; + } + } + + if (isEmoji && !opened) { + const emoji = text.substr(emojiIndex + 1); + if (!emoji.includes(' ')) { + this.open('emoji', emoji); + opened = true; + } + } + + if (isMfmTag && !opened) { + const mfmTag = text.substr(mfmTagIndex + 1); + if (!mfmTag.includes(' ')) { + this.open('mfmTag', mfmTag.replace('[', '')); + opened = true; + } + } + + if (!opened) { + this.close(); + } + } + + /** + * サジェストを提示します。 + */ + private async open(type: string, q: string | null) { + if (type !== this.currentType) { + this.close(); + } + if (this.opening) return; + this.opening = true; + this.currentType = type; + + //#region サジェストを表示すべき位置を計算 + const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); + + const rect = this.textarea.getBoundingClientRect(); + + const x = rect.left + caretPosition.left - this.textarea.scrollLeft; + const y = rect.top + caretPosition.top - this.textarea.scrollTop; + //#endregion + + if (this.suggestion) { + this.suggestion.x.value = x; + this.suggestion.y.value = y; + this.suggestion.q.value = q; + + this.opening = false; + } else { + const _x = ref(x); + const _y = ref(y); + const _q = ref(q); + + const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), { + textarea: this.textarea, + close: this.close, + type: type, + q: _q, + x: _x, + y: _y, + }, { + done: (res) => { + this.complete(res); + }, + }); + + this.suggestion = { + q: _q, + x: _x, + y: _y, + close: () => dispose(), + }; + + this.opening = false; + } + } + + /** + * サジェストを閉じます。 + */ + private close() { + if (this.suggestion == null) return; + + this.suggestion.close(); + this.suggestion = null; + + this.textarea.focus(); + } + + /** + * オートコンプリートする + */ + private complete({ type, value }) { + this.close(); + + const caret = this.textarea.selectionStart; + + if (type === 'user') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('@')); + const after = source.substr(caret); + + const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; + + // 挿入 + this.text = `${trimmedBefore}@${acct} ${after}`; + + // キャレットを戻す + nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (acct.length + 2); + this.textarea.setSelectionRange(pos, pos); + }); + } else if (type === 'hashtag') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('#')); + const after = source.substr(caret); + + // 挿入 + this.text = `${trimmedBefore}#${value} ${after}`; + + // キャレットを戻す + nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (value.length + 2); + this.textarea.setSelectionRange(pos, pos); + }); + } else if (type === 'emoji') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf(':')); + const after = source.substr(caret); + + // 挿入 + this.text = trimmedBefore + value + after; + + // キャレットを戻す + nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + value.length; + this.textarea.setSelectionRange(pos, pos); + }); + } else if (type === 'mfmTag') { + const source = this.text; + + const before = source.substr(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('$')); + const after = source.substr(caret); + + // 挿入 + this.text = `${trimmedBefore}$[${value} ]${after}`; + + // キャレットを戻す + nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (value.length + 3); + this.textarea.setSelectionRange(pos, pos); + }); + } + } +} diff --git a/packages/frontend/src/scripts/chart-vline.ts b/packages/frontend/src/scripts/chart-vline.ts new file mode 100644 index 0000000000..8e3c4436b2 --- /dev/null +++ b/packages/frontend/src/scripts/chart-vline.ts @@ -0,0 +1,21 @@ +export const chartVLine = (vLineColor: string) => ({ + id: 'vLine', + beforeDraw(chart, args, options) { + if (chart.tooltip?._active?.length) { + const activePoint = chart.tooltip._active[0]; + const ctx = chart.ctx; + const x = activePoint.element.x; + const topY = chart.scales.y.top; + const bottomY = chart.scales.y.bottom; + + ctx.save(); + ctx.beginPath(); + ctx.moveTo(x, bottomY); + ctx.lineTo(x, topY); + ctx.lineWidth = 1; + ctx.strokeStyle = vLineColor; + ctx.stroke(); + ctx.restore(); + } + }, +}); diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts new file mode 100644 index 0000000000..35d40a6e08 --- /dev/null +++ b/packages/frontend/src/scripts/check-word-mute.ts @@ -0,0 +1,37 @@ +export function checkWordMute(note: Record, me: Record | null | undefined, mutedWords: Array): boolean { + // 自分自身 + if (me && (note.userId === me.id)) return false; + + if (mutedWords.length > 0) { + const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); + + if (text === '') return false; + + const matched = mutedWords.some(filter => { + if (Array.isArray(filter)) { + // Clean up + const filteredFilter = filter.filter(keyword => keyword !== ''); + if (filteredFilter.length === 0) return false; + + return filteredFilter.every(keyword => text.includes(keyword)); + } else { + // represents RegExp + const regexp = filter.match(/^\/(.+)\/(.*)$/); + + // This should never happen due to input sanitisation. + if (!regexp) return false; + + try { + return new RegExp(regexp[1], regexp[2]).test(text); + } catch (err) { + // This should never happen due to input sanitisation. + return false; + } + } + }); + + if (matched) return true; + } + + return false; +} diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts new file mode 100644 index 0000000000..16fad24129 --- /dev/null +++ b/packages/frontend/src/scripts/clone.ts @@ -0,0 +1,18 @@ +// structredCloneが遅いため +// SEE: http://var.blog.jp/archives/86038606.html + +type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; + +export function deepClone(x: T): T { + if (typeof x === 'object') { + if (x === null) return x; + if (Array.isArray(x)) return x.map(deepClone) as T; + const obj = {} as Record; + for (const [k, v] of Object.entries(x)) { + obj[k] = deepClone(v); + } + return obj as T; + } else { + return x; + } +} diff --git a/packages/frontend/src/scripts/collect-page-vars.ts b/packages/frontend/src/scripts/collect-page-vars.ts new file mode 100644 index 0000000000..76b68beaf6 --- /dev/null +++ b/packages/frontend/src/scripts/collect-page-vars.ts @@ -0,0 +1,68 @@ +interface StringPageVar { + name: string, + type: 'string', + value: string +} + +interface NumberPageVar { + name: string, + type: 'number', + value: number +} + +interface BooleanPageVar { + name: string, + type: 'boolean', + value: boolean +} + +type PageVar = StringPageVar | NumberPageVar | BooleanPageVar; + +export function collectPageVars(content): PageVar[] { + const pageVars: PageVar[] = []; + const collect = (xs: any[]): void => { + for (const x of xs) { + if (x.type === 'textInput') { + pageVars.push({ + name: x.name, + type: 'string', + value: x.default || '', + }); + } else if (x.type === 'textareaInput') { + pageVars.push({ + name: x.name, + type: 'string', + value: x.default || '', + }); + } else if (x.type === 'numberInput') { + pageVars.push({ + name: x.name, + type: 'number', + value: x.default || 0, + }); + } else if (x.type === 'switch') { + pageVars.push({ + name: x.name, + type: 'boolean', + value: x.default || false, + }); + } else if (x.type === 'counter') { + pageVars.push({ + name: x.name, + type: 'number', + value: 0, + }); + } else if (x.type === 'radioButton') { + pageVars.push({ + name: x.name, + type: 'string', + value: x.default || '', + }); + } else if (x.children) { + collect(x.children); + } + } + }; + collect(content); + return pageVars; +} diff --git a/packages/frontend/src/scripts/contains.ts b/packages/frontend/src/scripts/contains.ts new file mode 100644 index 0000000000..256e09d293 --- /dev/null +++ b/packages/frontend/src/scripts/contains.ts @@ -0,0 +1,9 @@ +export default (parent, child, checkSame = true) => { + if (checkSame && parent === child) return true; + let node = child.parentNode; + while (node) { + if (node === parent) return true; + node = node.parentNode; + } + return false; +}; diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts new file mode 100644 index 0000000000..ab13cab970 --- /dev/null +++ b/packages/frontend/src/scripts/copy-to-clipboard.ts @@ -0,0 +1,33 @@ +/** + * Clipboardに値をコピー(TODO: 文字列以外も対応) + */ +export default val => { + // 空div 生成 + const tmp = document.createElement('div'); + // 選択用のタグ生成 + const pre = document.createElement('pre'); + + // 親要素のCSSで user-select: none だとコピーできないので書き換える + pre.style.webkitUserSelect = 'auto'; + pre.style.userSelect = 'auto'; + + tmp.appendChild(pre).textContent = val; + + // 要素を画面外へ + const s = tmp.style; + s.position = 'fixed'; + s.right = '200%'; + + // body に追加 + document.body.appendChild(tmp); + // 要素を選択 + document.getSelection().selectAllChildren(tmp); + + // クリップボードにコピー + const result = document.execCommand('copy'); + + // 要素削除 + document.body.removeChild(tmp); + + return result; +}; diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/scripts/device-kind.ts new file mode 100644 index 0000000000..544cac0604 --- /dev/null +++ b/packages/frontend/src/scripts/device-kind.ts @@ -0,0 +1,10 @@ +import { defaultStore } from '@/store'; + +const ua = navigator.userAgent.toLowerCase(); +const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); +const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); + +export const deviceKind = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind + : isSmartphone ? 'smartphone' + : isTablet ? 'tablet' + : 'desktop'; diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts new file mode 100644 index 0000000000..3f05642d57 --- /dev/null +++ b/packages/frontend/src/scripts/emoji-base.ts @@ -0,0 +1,20 @@ +const twemojiSvgBase = '/twemoji'; +const fluentEmojiPngBase = '/fluent-emoji'; + +export function char2twemojiFilePath(char: string): string { + 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); + const fileName = codes.join('-'); + return `${twemojiSvgBase}/${fileName}.svg`; +} + +export function char2fluentEmojiFilePath(char: string): string { + let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); + // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 + if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); + if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); + codes = codes.filter(x => x && x.length); + const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); + return `${fluentEmojiPngBase}/${fileName}.png`; +} diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts new file mode 100644 index 0000000000..bc52fa7a43 --- /dev/null +++ b/packages/frontend/src/scripts/emojilist.ts @@ -0,0 +1,17 @@ +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: typeof unicodeEmojiCategories[number]; +} + +// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb +import _emojilist from '../emojilist.json'; + +export const emojilist = _emojilist as UnicodeEmojiDef[]; + +export function getEmojiName(char: string): string | undefined { + return emojilist.find(emo => emo.char === char)?.name; +} diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts new file mode 100644 index 0000000000..af517f2672 --- /dev/null +++ b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts @@ -0,0 +1,9 @@ +export function extractAvgColorFromBlurhash(hash: string) { + return typeof hash === 'string' + ? '#' + [...hash.slice(2, 6)] + .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) + .reduce((a, c) => a * 83 + c, 0) + .toString(16) + .padStart(6, '0') + : undefined; +} diff --git a/packages/frontend/src/scripts/extract-mentions.ts b/packages/frontend/src/scripts/extract-mentions.ts new file mode 100644 index 0000000000..cc19b161a8 --- /dev/null +++ b/packages/frontend/src/scripts/extract-mentions.ts @@ -0,0 +1,11 @@ +// test is located in test/extract-mentions + +import * as mfm from 'mfm-js'; + +export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { + // TODO: 重複を削除 + const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention'); + const mentions = mentionNodes.map(x => x.props); + + return mentions; +} diff --git a/packages/frontend/src/scripts/extract-url-from-mfm.ts b/packages/frontend/src/scripts/extract-url-from-mfm.ts new file mode 100644 index 0000000000..34e3eb6c19 --- /dev/null +++ b/packages/frontend/src/scripts/extract-url-from-mfm.ts @@ -0,0 +1,19 @@ +import * as mfm from 'mfm-js'; +import { unique } from '@/scripts/array'; + +// unique without hash +// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] +const removeHash = (x: string) => x.replace(/#[^#]*$/, ''); + +export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] { + const urlNodes = mfm.extract(nodes, (node) => { + return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent)); + }); + const urls: string[] = unique(urlNodes.map(x => x.props.url)); + + return urls.reduce((array, url) => { + const urlWithoutHash = removeHash(url); + if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url); + return array; + }, [] as string[]); +} diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts new file mode 100644 index 0000000000..d6802fa322 --- /dev/null +++ b/packages/frontend/src/scripts/focus.ts @@ -0,0 +1,27 @@ +export function focusPrev(el: Element | null, self = false, scroll = true) { + if (el == null) return; + if (!self) el = el.previousElementSibling; + if (el) { + if (el.hasAttribute('tabindex')) { + (el as HTMLElement).focus({ + preventScroll: !scroll, + }); + } else { + focusPrev(el.previousElementSibling, true); + } + } +} + +export function focusNext(el: Element | null, self = false, scroll = true) { + if (el == null) return; + if (!self) el = el.nextElementSibling; + if (el) { + if (el.hasAttribute('tabindex')) { + (el as HTMLElement).focus({ + preventScroll: !scroll, + }); + } else { + focusPrev(el.nextElementSibling, true); + } + } +} diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts new file mode 100644 index 0000000000..7f321cc0ae --- /dev/null +++ b/packages/frontend/src/scripts/form.ts @@ -0,0 +1,59 @@ +export type FormItem = { + label?: string; + type: 'string'; + default: string | null; + hidden?: boolean; + multiline?: boolean; +} | { + label?: string; + type: 'number'; + default: number | null; + hidden?: boolean; + step?: number; +} | { + label?: string; + type: 'boolean'; + default: boolean | null; + hidden?: boolean; +} | { + label?: string; + type: 'enum'; + default: string | null; + hidden?: boolean; + enum: string[]; +} | { + label?: string; + type: 'radio'; + default: unknown | null; + hidden?: boolean; + options: { + label: string; + value: unknown; + }[]; +} | { + label?: string; + type: 'object'; + default: Record | null; + hidden: true; +} | { + label?: string; + type: 'array'; + default: unknown[] | null; + hidden: true; +}; + +export type Form = Record; + +type GetItemType = + 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 + : never; + +export type GetFormResultType = { + [P in keyof F]: GetItemType; +}; diff --git a/packages/frontend/src/scripts/format-time-string.ts b/packages/frontend/src/scripts/format-time-string.ts new file mode 100644 index 0000000000..c20db5e827 --- /dev/null +++ b/packages/frontend/src/scripts/format-time-string.ts @@ -0,0 +1,50 @@ +const defaultLocaleStringFormats: {[index: string]: string} = { + 'weekday': 'narrow', + 'era': 'narrow', + 'year': 'numeric', + 'month': 'numeric', + 'day': 'numeric', + 'hour': 'numeric', + 'minute': 'numeric', + 'second': 'numeric', + 'timeZoneName': 'short', +}; + +function formatLocaleString(date: Date, format: string): string { + return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => { + if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) { + return date.toLocaleString(window.navigator.language, { [kind]: option ? option : defaultLocaleStringFormats[kind] }); + } else { + return match; + } + }); +} + +export function formatDateTimeString(date: Date, format: string): string { + return format + .replace(/yyyy/g, date.getFullYear().toString()) + .replace(/yy/g, date.getFullYear().toString().slice(-2)) + .replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long' })) + .replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short' })) + .replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2)) + .replace(/M/g, (date.getMonth() + 1).toString()) + .replace(/dd/g, (`0${date.getDate()}`).slice(-2)) + .replace(/d/g, date.getDate().toString()) + .replace(/HH/g, (`0${date.getHours()}`).slice(-2)) + .replace(/H/g, date.getHours().toString()) + .replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2)) + .replace(/h/g, ((date.getHours() % 12) || 12).toString()) + .replace(/mm/g, (`0${date.getMinutes()}`).slice(-2)) + .replace(/m/g, date.getMinutes().toString()) + .replace(/ss/g, (`0${date.getSeconds()}`).slice(-2)) + .replace(/s/g, date.getSeconds().toString()) + .replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM'); +} + +export function formatTimeString(date: Date, format: string): string { + return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => { + if (localeformat) return formatLocaleString(date, localeformat); + if (datetimeformat) return formatDateTimeString(date, datetimeformat); + return match; + }); +} diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts new file mode 100644 index 0000000000..da7d622632 --- /dev/null +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -0,0 +1,30 @@ +import * as Acct from 'misskey-js/built/acct'; +import { host as localHost } from '@/config'; + +export async function genSearchQuery(v: any, q: string) { + let host: string; + let userId: string; + if (q.split(' ').some(x => x.startsWith('@'))) { + for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) { + if (at.includes('.')) { + if (at === localHost || at === '.') { + host = null; + } else { + host = at; + } + } else { + const user = await v.os.api('users/show', Acct.parse(at)).catch(x => null); + if (user) { + userId = user.id; + } else { + // todo: show error + } + } + } + } + return { + query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '), + host: host, + userId: userId, + }; +} diff --git a/packages/frontend/src/scripts/get-account-from-id.ts b/packages/frontend/src/scripts/get-account-from-id.ts new file mode 100644 index 0000000000..1da897f176 --- /dev/null +++ b/packages/frontend/src/scripts/get-account-from-id.ts @@ -0,0 +1,7 @@ +import { get } from '@/scripts/idb-proxy'; + +export async function getAccountFromId(id: string) { + const accounts = await get('accounts') as { token: string; id: string; }[]; + if (!accounts) console.log('Accounts are not recorded'); + return accounts.find(account => account.id === id); +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts new file mode 100644 index 0000000000..7656770894 --- /dev/null +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -0,0 +1,341 @@ +import { defineAsyncComponent, Ref, inject } from 'vue'; +import * as misskey from 'misskey-js'; +import { pleaseLogin } from './please-login'; +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 { notePage } from '@/filters/note'; + +export function getNoteMenu(props: { + note: misskey.entities.Note; + menuButton: Ref; + translation: Ref; + translating: Ref; + isDeleted: Ref; + currentClipPage?: Ref; +}) { + const isRenote = ( + props.note.renote != null && + props.note.text == null && + props.note.fileIds.length === 0 && + props.note.poll == null + ); + + const appearNote = isRenote ? props.note.renote as misskey.entities.Note : props.note; + + function del(): void { + os.confirm({ + type: 'warning', + text: i18n.ts.noteDeleteConfirm, + }).then(({ canceled }) => { + if (canceled) return; + + os.api('notes/delete', { + noteId: appearNote.id, + }); + }); + } + + function delEdit(): void { + os.confirm({ + type: 'warning', + text: i18n.ts.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 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, res => { + if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { + os.alert({ + type: 'error', + text: i18n.ts.pinLimitExceeded, + }); + } + }); + } + + async function clip(): Promise { + const clips = await os.api('clips/list'); + os.popupMenu([{ + icon: 'ti ti-plus', + text: i18n.ts.createNew, + action: async () => { + const { canceled, result } = await os.form(i18n.ts.createNewClip, { + name: { + type: 'string', + label: i18n.ts.name, + }, + description: { + type: 'string', + required: false, + multiline: true, + label: i18n.ts.description, + }, + isPublic: { + type: 'boolean', + label: i18n.ts.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.promiseDialog( + os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), + null, + async (err) => { + if (err.id === '734806c4-542c-463a-9311-15c512803965') { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), + }); + if (!confirm.canceled) { + os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); + if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true; + } + } else { + os.alert({ + type: 'error', + text: err.message + '\n' + err.id, + }); + } + }, + ); + }, + }))], props.menuButton.value, { + }).then(focus); + } + + async function unclip(): Promise { + os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id }); + props.isDeleted.value = true; + } + + async function promote(): Promise { + const { canceled, result: days } = await os.inputNumber({ + title: i18n.ts.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}`, + }); + } + function notedetails(): void { + os.pageWindow(`/notes/${appearNote.id}`); + } + async function translate(): Promise { + 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 = [ + ...( + props.currentClipPage?.value.userId === $i.id ? [{ + icon: 'ti ti-backspace', + text: i18n.ts.unclip, + danger: true, + action: unclip, + }, null] : [] + ), { + icon: 'ti ti-external-link', + text: i18n.ts.details, + action: notedetails, + }, { + icon: 'ti ti-copy', + text: i18n.ts.copyContent, + action: copyContent, + }, { + icon: 'ti ti-link', + text: i18n.ts.copyLink, + action: copyLink, + }, (appearNote.url || appearNote.uri) ? { + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + window.open(appearNote.url || appearNote.uri, '_blank'); + }, + } : undefined, + { + icon: 'ti ti-share', + text: i18n.ts.share, + action: share, + }, + instance.translatorAvailable ? { + icon: 'ti ti-language-hiragana', + text: i18n.ts.translate, + action: translate, + } : undefined, + null, + statePromise.then(state => state.isFavorited ? { + icon: 'ti ti-star-off', + text: i18n.ts.unfavorite, + action: () => toggleFavorite(false), + } : { + icon: 'ti ti-star', + text: i18n.ts.favorite, + action: () => toggleFavorite(true), + }), + { + icon: 'ti ti-paperclip', + text: i18n.ts.clip, + action: () => clip(), + }, + statePromise.then(state => state.isMutedThread ? { + icon: 'ti ti-message-off', + text: i18n.ts.unmuteThread, + action: () => toggleThreadMute(false), + } : { + icon: 'ti ti-message-off', + text: i18n.ts.muteThread, + action: () => toggleThreadMute(true), + }), + appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => togglePin(false), + } : { + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => togglePin(true), + } : undefined, + /* + ...($i.isModerator || $i.isAdmin ? [ + null, + { + icon: 'fas fa-bullhorn', + text: i18n.ts.promote, + action: promote + }] + : [] + ),*/ + ...(appearNote.userId !== $i.id ? [ + null, + { + icon: 'ti ti-exclamation-circle', + text: i18n.ts.reportAbuse, + action: () => { + const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; + os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + user: appearNote.user, + initialComment: `Note: ${u}\n-----\n`, + }, {}, 'closed'); + }, + }] + : [] + ), + ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ + null, + appearNote.userId === $i.id ? { + icon: 'ti ti-edit', + text: i18n.ts.deleteAndEdit, + action: delEdit, + } : undefined, + { + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: del, + }] + : [] + )] + .filter(x => x !== undefined); + } else { + menu = [{ + icon: 'ti ti-external-link', + text: i18n.ts.detailed, + action: openDetail, + }, { + icon: 'ti ti-copy', + text: i18n.ts.copyContent, + action: copyContent, + }, { + icon: 'ti ti-link', + text: i18n.ts.copyLink, + action: copyLink, + }, (appearNote.url || appearNote.uri) ? { + icon: 'ti ti-external-link', + text: i18n.ts.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: 'ti ti-plug', + text: action.title, + action: () => { + action.handler(appearNote); + }, + }))]); + } + + return menu; +} diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts new file mode 100644 index 0000000000..d57e1c3029 --- /dev/null +++ b/packages/frontend/src/scripts/get-note-summary.ts @@ -0,0 +1,55 @@ +import * as misskey from 'misskey-js'; +import { i18n } from '@/i18n'; + +/** + * 投稿を表す文字列を取得します。 + * @param {*} note (packされた)投稿 + */ +export const getNoteSummary = (note: misskey.entities.Note): string => { + if (note.deletedAt) { + return `(${i18n.ts.deletedNote})`; + } + + if (note.isHidden) { + return `(${i18n.ts.invisibleNote})`; + } + + let summary = ''; + + // 本文 + if (note.cw != null) { + summary += note.cw; + } else { + summary += note.text ? note.text : ''; + } + + // ファイルが添付されているとき + if ((note.files || []).length !== 0) { + summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`; + } + + // 投票が添付されているとき + if (note.poll) { + summary += ` (${i18n.ts.poll})`; + } + + // 返信のとき + if (note.replyId) { + if (note.reply) { + summary += `\n\nRE: ${getNoteSummary(note.reply)}`; + } else { + summary += '\n\nRE: ...'; + } + } + + // Renoteのとき + if (note.renoteId) { + if (note.renote) { + summary += `\n\nRN: ${getNoteSummary(note.renote)}`; + } else { + summary += '\n\nRN: ...'; + } + } + + return summary.trim(); +}; diff --git a/packages/frontend/src/scripts/get-static-image-url.ts b/packages/frontend/src/scripts/get-static-image-url.ts new file mode 100644 index 0000000000..cbd1761983 --- /dev/null +++ b/packages/frontend/src/scripts/get-static-image-url.ts @@ -0,0 +1,19 @@ +import { url as instanceUrl } from '@/config'; +import * as url from '@/scripts/url'; + +export function getStaticImageUrl(baseUrl: string): string { + const u = new URL(baseUrl); + if (u.href.startsWith(`${instanceUrl}/proxy/`)) { + // もう既にproxyっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; + } + + // 拡張子がないとキャッシュしてくれないCDNがあるのでダミーの名前を指定する + const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.webp`; + + return `${instanceUrl}/proxy/${dummy}?${url.query({ + url: u.href, + static: '1', + })}`; +} diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts new file mode 100644 index 0000000000..2faacffdfc --- /dev/null +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -0,0 +1,253 @@ +import * as Acct from 'misskey-js/built/acct'; +import { defineAsyncComponent } from 'vue'; +import { i18n } from '@/i18n'; +import copyToClipboard from '@/scripts/copy-to-clipboard'; +import { host } from '@/config'; +import * as os from '@/os'; +import { userActions } from '@/store'; +import { $i, iAmModerator } from '@/account'; +import { mainRouter } from '@/router'; +import { Router } from '@/nirax'; + +export function getUserMenu(user, router: Router = mainRouter) { + const meId = $i ? $i.id : null; + + async function pushList() { + const t = i18n.ts.selectList; // なぜか後で参照すると null になるので最初にメモリに確保しておく + const lists = await os.api('users/lists/list'); + if (lists.length === 0) { + os.alert({ + type: 'error', + text: i18n.ts.youHaveNoLists, + }); + return; + } + const { canceled, result: listId } = await os.select({ + title: t, + items: lists.map(list => ({ + value: list.id, text: list.name, + })), + }); + if (canceled) return; + os.apiWithDialog('users/lists/push', { + listId: listId, + userId: user.id, + }); + } + + async function inviteGroup() { + const groups = await os.api('users/groups/owned'); + if (groups.length === 0) { + os.alert({ + type: 'error', + text: i18n.ts.youHaveNoGroups, + }); + return; + } + const { canceled, result: groupId } = await os.select({ + title: i18n.ts.group, + items: groups.map(group => ({ + value: group.id, text: group.name, + })), + }); + if (canceled) return; + os.apiWithDialog('users/groups/invite', { + groupId: groupId, + userId: user.id, + }); + } + + async function toggleMute() { + if (user.isMuted) { + os.apiWithDialog('mute/delete', { + userId: user.id, + }).then(() => { + user.isMuted = false; + }); + } else { + const { canceled, result: period } = await os.select({ + title: i18n.ts.mutePeriod, + items: [{ + value: 'indefinitely', text: i18n.ts.indefinitely, + }, { + value: 'tenMinutes', text: i18n.ts.tenMinutes, + }, { + value: 'oneHour', text: i18n.ts.oneHour, + }, { + value: 'oneDay', text: i18n.ts.oneDay, + }, { + value: 'oneWeek', text: i18n.ts.oneWeek, + }], + default: 'indefinitely', + }); + if (canceled) return; + + const expiresAt = period === 'indefinitely' ? null + : period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10) + : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) + : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) + : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) + : null; + + os.apiWithDialog('mute/create', { + userId: user.id, + expiresAt, + }).then(() => { + user.isMuted = true; + }); + } + } + + async function toggleBlock() { + if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return; + + os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { + userId: user.id, + }).then(() => { + user.isBlocking = !user.isBlocking; + }); + } + + async function toggleSilence() { + if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return; + + os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { + userId: user.id, + }).then(() => { + user.isSilenced = !user.isSilenced; + }); + } + + async function toggleSuspend() { + if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; + + os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { + userId: user.id, + }).then(() => { + user.isSuspended = !user.isSuspended; + }); + } + + function reportAbuse() { + os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + user: user, + }, {}, 'closed'); + } + + async function getConfirmed(text: string): Promise { + const confirm = await os.confirm({ + type: 'warning', + title: 'confirm', + text, + }); + + return !confirm.canceled; + } + + async function invalidateFollow() { + os.apiWithDialog('following/invalidate', { + userId: user.id, + }).then(() => { + user.isFollowed = !user.isFollowed; + }); + } + + let menu = [{ + icon: 'ti ti-at', + text: i18n.ts.copyUsername, + action: () => { + copyToClipboard(`@${user.username}@${user.host || host}`); + }, + }, { + icon: 'ti ti-rss', + text: i18n.ts.copyRSS, + action: () => { + copyToClipboard(`${user.host || host}/@${user.username}.atom`); + } + }, { + icon: 'ti ti-info-circle', + text: i18n.ts.info, + action: () => { + router.push(`/user-info/${user.id}`); + }, + }, { + icon: 'ti ti-mail', + text: i18n.ts.sendMessage, + action: () => { + os.post({ specified: user }); + }, + }, meId !== user.id ? { + type: 'link', + icon: 'ti ti-messages', + text: i18n.ts.startMessaging, + to: '/my/messaging/' + Acct.toString(user), + } : undefined, null, { + icon: 'ti ti-list', + text: i18n.ts.addToList, + action: pushList, + }, meId !== user.id ? { + icon: 'ti ti-users', + text: i18n.ts.inviteToGroup, + action: inviteGroup, + } : undefined] as any; + + if ($i && meId !== user.id) { + menu = menu.concat([null, { + icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', + text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, + action: toggleMute, + }, { + icon: 'ti ti-ban', + text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, + action: toggleBlock, + }]); + + if (user.isFollowed) { + menu = menu.concat([{ + icon: 'ti ti-link-off', + text: i18n.ts.breakFollow, + action: invalidateFollow, + }]); + } + + menu = menu.concat([null, { + icon: 'ti ti-exclamation-circle', + text: i18n.ts.reportAbuse, + action: reportAbuse, + }]); + + if (iAmModerator) { + menu = menu.concat([null, { + icon: 'ti ti-microphone-2-off', + text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence, + action: toggleSilence, + }, { + icon: 'ti ti-snowflake', + text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend, + action: toggleSuspend, + }]); + } + } + + if ($i && meId === user.id) { + menu = menu.concat([null, { + icon: 'ti ti-pencil', + text: i18n.ts.editProfile, + action: () => { + router.push('/settings/profile'); + }, + }]); + } + + if (userActions.length > 0) { + menu = menu.concat([null, ...userActions.map(action => ({ + icon: 'ti ti-plug', + text: action.title, + action: () => { + action.handler(user); + }, + }))]); + } + + return menu; +} diff --git a/packages/frontend/src/scripts/get-user-name.ts b/packages/frontend/src/scripts/get-user-name.ts new file mode 100644 index 0000000000..d499ea0203 --- /dev/null +++ b/packages/frontend/src/scripts/get-user-name.ts @@ -0,0 +1,3 @@ +export default function(user: { name?: string | null, username: string }): string { + return user.name || user.username; +} diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts new file mode 100644 index 0000000000..4a0ded637d --- /dev/null +++ b/packages/frontend/src/scripts/hotkey.ts @@ -0,0 +1,90 @@ +import keyCode from './keycode'; + +type Callback = (ev: KeyboardEvent) => void; + +type Keymap = Record; + +type Pattern = { + which: string[]; + ctrl?: boolean; + shift?: boolean; + alt?: boolean; +}; + +type Action = { + patterns: Pattern[]; + callback: Callback; + allowRepeat: boolean; +}; + +const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { + const result = { + patterns: [], + callback, + allowRepeat: true, + } as Action; + + if (patterns.match(/^\(.*\)$/) !== null) { + result.allowRepeat = false; + patterns = patterns.slice(1, -1); + } + + result.patterns = patterns.split('|').map(part => { + const pattern = { + which: [], + ctrl: false, + alt: false, + shift: false, + } as Pattern; + + const keys = part.trim().split('+').map(x => x.trim().toLowerCase()); + for (const key of keys) { + switch (key) { + case 'ctrl': pattern.ctrl = true; break; + case 'alt': pattern.alt = true; break; + case 'shift': pattern.shift = true; break; + default: pattern.which = keyCode(key).map(k => k.toLowerCase()); + } + } + + return pattern; + }); + + return result; +}); + +const ignoreElemens = ['input', 'textarea']; + +function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean { + const key = ev.code.toLowerCase(); + return patterns.some(pattern => pattern.which.includes(key) && + pattern.ctrl === ev.ctrlKey && + pattern.shift === ev.shiftKey && + pattern.alt === ev.altKey && + !ev.metaKey, + ); +} + +export const makeHotkey = (keymap: Keymap) => { + const actions = parseKeymap(keymap); + + return (ev: KeyboardEvent) => { + if (document.activeElement) { + if (ignoreElemens.some(el => document.activeElement!.matches(el))) return; + if (document.activeElement.attributes['contenteditable']) return; + } + + for (const action of actions) { + const matched = match(ev, action.patterns); + + if (matched) { + if (!action.allowRepeat && ev.repeat) return; + + ev.preventDefault(); + ev.stopPropagation(); + action.callback(ev); + break; + } + } + }; +}; diff --git a/packages/frontend/src/scripts/hpml/block.ts b/packages/frontend/src/scripts/hpml/block.ts new file mode 100644 index 0000000000..804c5c1124 --- /dev/null +++ b/packages/frontend/src/scripts/hpml/block.ts @@ -0,0 +1,109 @@ +// blocks + +export type BlockBase = { + id: string; + type: string; +}; + +export type TextBlock = BlockBase & { + type: 'text'; + text: string; +}; + +export type SectionBlock = BlockBase & { + type: 'section'; + title: string; + children: (Block | VarBlock)[]; +}; + +export type ImageBlock = BlockBase & { + type: 'image'; + fileId: string | null; +}; + +export type ButtonBlock = BlockBase & { + type: 'button'; + text: any; + primary: boolean; + action: string; + content: string; + event: string; + message: string; + var: string; + fn: string; +}; + +export type IfBlock = BlockBase & { + type: 'if'; + var: string; + children: Block[]; +}; + +export type TextareaBlock = BlockBase & { + type: 'textarea'; + text: string; +}; + +export type PostBlock = BlockBase & { + type: 'post'; + text: string; + attachCanvasImage: boolean; + canvasId: string; +}; + +export type CanvasBlock = BlockBase & { + type: 'canvas'; + name: string; // canvas id + width: number; + height: number; +}; + +export type NoteBlock = BlockBase & { + type: 'note'; + detailed: boolean; + note: string | null; +}; + +export type Block = + TextBlock | SectionBlock | ImageBlock | ButtonBlock | IfBlock | TextareaBlock | PostBlock | CanvasBlock | NoteBlock | VarBlock; + +// variable blocks + +export type VarBlockBase = BlockBase & { + name: string; +}; + +export type NumberInputVarBlock = VarBlockBase & { + type: 'numberInput'; + text: string; +}; + +export type TextInputVarBlock = VarBlockBase & { + type: 'textInput'; + text: string; +}; + +export type SwitchVarBlock = VarBlockBase & { + type: 'switch'; + text: string; +}; + +export type RadioButtonVarBlock = VarBlockBase & { + type: 'radioButton'; + title: string; + values: string[]; +}; + +export type CounterVarBlock = VarBlockBase & { + type: 'counter'; + text: string; + inc: number; +}; + +export type VarBlock = + NumberInputVarBlock | TextInputVarBlock | SwitchVarBlock | RadioButtonVarBlock | CounterVarBlock; + +const varBlock = ['numberInput', 'textInput', 'switch', 'radioButton', 'counter']; +export function isVarBlock(block: Block): block is VarBlock { + return varBlock.includes(block.type); +} diff --git a/packages/frontend/src/scripts/hpml/evaluator.ts b/packages/frontend/src/scripts/hpml/evaluator.ts new file mode 100644 index 0000000000..196b3142a1 --- /dev/null +++ b/packages/frontend/src/scripts/hpml/evaluator.ts @@ -0,0 +1,232 @@ +import autobind from 'autobind-decorator'; +import { PageVar, envVarsDef, Fn, HpmlScope, HpmlError } from '.'; +import { version } from '@/config'; +import { AiScript, utils, values } from '@syuilo/aiscript'; +import { createAiScriptEnv } from '../aiscript/api'; +import { collectPageVars } from '../collect-page-vars'; +import { initHpmlLib, initAiLib } from './lib'; +import * as os from '@/os'; +import { markRaw, ref, Ref, unref } from 'vue'; +import { Expr, isLiteralValue, Variable } from './expr'; + +/** + * Hpml evaluator + */ +export class Hpml { + private variables: Variable[]; + private pageVars: PageVar[]; + private envVars: Record; + public aiscript?: AiScript; + public pageVarUpdatedCallback?: values.VFn; + public canvases: Record = {}; + public vars: Ref> = ref({}); + public page: Record; + + private opts: { + randomSeed: string; visitor?: any; url?: string; + enableAiScript: boolean; + }; + + constructor(page: Hpml['page'], opts: Hpml['opts']) { + this.page = page; + this.variables = this.page.variables; + this.pageVars = collectPageVars(this.page.content); + this.opts = opts; + + if (this.opts.enableAiScript) { + this.aiscript = markRaw(new AiScript({ ...createAiScriptEnv({ + storageKey: 'pages:' + this.page.id, + }), ...initAiLib(this) }, { + in: (q) => { + return new Promise(ok => { + os.inputText({ + title: q, + }).then(({ canceled, result: a }) => { + ok(a); + }); + }); + }, + out: (value) => { + console.log(value); + }, + log: (type, params) => { + }, + })); + + this.aiscript.scope.opts.onUpdated = (name, value) => { + this.eval(); + }; + } + + const date = new Date(); + + this.envVars = { + AI: 'kawaii', + VERSION: version, + URL: this.page ? `${opts.url}/@${this.page.user.username}/pages/${this.page.name}` : '', + LOGIN: opts.visitor != null, + NAME: opts.visitor ? opts.visitor.name || opts.visitor.username : '', + USERNAME: opts.visitor ? opts.visitor.username : '', + USERID: opts.visitor ? opts.visitor.id : '', + NOTES_COUNT: opts.visitor ? opts.visitor.notesCount : 0, + FOLLOWERS_COUNT: opts.visitor ? opts.visitor.followersCount : 0, + FOLLOWING_COUNT: opts.visitor ? opts.visitor.followingCount : 0, + IS_CAT: opts.visitor ? opts.visitor.isCat : false, + SEED: opts.randomSeed ? opts.randomSeed : '', + YMD: `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`, + AISCRIPT_DISABLED: !this.opts.enableAiScript, + NULL: null, + }; + + this.eval(); + } + + @autobind + public eval() { + try { + this.vars.value = this.evaluateVars(); + } catch (err) { + //this.onError(e); + } + } + + @autobind + public interpolate(str: string) { + if (str == null) return null; + return str.replace(/{(.+?)}/g, match => { + const v = unref(this.vars)[match.slice(1, -1).trim()]; + return v == null ? 'NULL' : v.toString(); + }); + } + + @autobind + public callAiScript(fn: string) { + try { + if (this.aiscript) this.aiscript.execFn(this.aiscript.scope.get(fn), []); + } catch (err) {} + } + + @autobind + public registerCanvas(id: string, canvas: any) { + this.canvases[id] = canvas; + } + + @autobind + public updatePageVar(name: string, value: any) { + const pageVar = this.pageVars.find(v => v.name === name); + if (pageVar !== undefined) { + pageVar.value = value; + if (this.pageVarUpdatedCallback) { + if (this.aiscript) this.aiscript.execFn(this.pageVarUpdatedCallback, [values.STR(name), utils.jsToVal(value)]); + } + } else { + throw new HpmlError(`No such page var '${name}'`); + } + } + + @autobind + public updateRandomSeed(seed: string) { + this.opts.randomSeed = seed; + this.envVars.SEED = seed; + } + + @autobind + private _interpolateScope(str: string, scope: HpmlScope) { + return str.replace(/{(.+?)}/g, match => { + const v = scope.getState(match.slice(1, -1).trim()); + return v == null ? 'NULL' : v.toString(); + }); + } + + @autobind + public evaluateVars(): Record { + const values: Record = {}; + + for (const [k, v] of Object.entries(this.envVars)) { + values[k] = v; + } + + for (const v of this.pageVars) { + values[v.name] = v.value; + } + + for (const v of this.variables) { + values[v.name] = this.evaluate(v, new HpmlScope([values])); + } + + return values; + } + + @autobind + private evaluate(expr: Expr, scope: HpmlScope): any { + if (isLiteralValue(expr)) { + if (expr.type === null) { + return null; + } + + if (expr.type === 'number') { + return parseInt((expr.value as any), 10); + } + + if (expr.type === 'text' || expr.type === 'multiLineText') { + return this._interpolateScope(expr.value || '', scope); + } + + if (expr.type === 'textList') { + return this._interpolateScope(expr.value || '', scope).trim().split('\n'); + } + + if (expr.type === 'ref') { + return scope.getState(expr.value); + } + + if (expr.type === 'aiScriptVar') { + if (this.aiscript) { + try { + return utils.valToJs(this.aiscript.scope.get(expr.value)); + } catch (err) { + return null; + } + } else { + return null; + } + } + + // Define user function + if (expr.type === 'fn') { + return { + slots: expr.value.slots.map(x => x.name), + exec: (slotArg: Record) => { + return this.evaluate(expr.value.expression, scope.createChildScope(slotArg, expr.id)); + }, + } as Fn; + } + return; + } + + // Call user function + if (expr.type.startsWith('fn:')) { + const fnName = expr.type.split(':')[1]; + const fn = scope.getState(fnName); + const args = {} as Record; + for (let i = 0; i < fn.slots.length; i++) { + const name = fn.slots[i]; + args[name] = this.evaluate(expr.args[i], scope); + } + return fn.exec(args); + } + + if (expr.args === undefined) return null; + + const funcs = initHpmlLib(expr, scope, this.opts.randomSeed, this.opts.visitor); + + // Call function + const fnName = expr.type; + const fn = (funcs as any)[fnName]; + if (fn == null) { + throw new HpmlError(`No such function '${fnName}'`); + } else { + return fn(...expr.args.map(x => this.evaluate(x, scope))); + } + } +} diff --git a/packages/frontend/src/scripts/hpml/expr.ts b/packages/frontend/src/scripts/hpml/expr.ts new file mode 100644 index 0000000000..18c7c2a14b --- /dev/null +++ b/packages/frontend/src/scripts/hpml/expr.ts @@ -0,0 +1,79 @@ +import { literalDefs, Type } from '.'; + +export type ExprBase = { + id: string; +}; + +// value + +export type EmptyValue = ExprBase & { + type: null; + value: null; +}; + +export type TextValue = ExprBase & { + type: 'text'; + value: string; +}; + +export type MultiLineTextValue = ExprBase & { + type: 'multiLineText'; + value: string; +}; + +export type TextListValue = ExprBase & { + type: 'textList'; + value: string; +}; + +export type NumberValue = ExprBase & { + type: 'number'; + value: number; +}; + +export type RefValue = ExprBase & { + type: 'ref'; + value: string; // value is variable name +}; + +export type AiScriptRefValue = ExprBase & { + type: 'aiScriptVar'; + value: string; // value is variable name +}; + +export type UserFnValue = ExprBase & { + type: 'fn'; + value: UserFnInnerValue; +}; +type UserFnInnerValue = { + slots: { + name: string; + type: Type; + }[]; + expression: Expr; +}; + +export type Value = + EmptyValue | TextValue | MultiLineTextValue | TextListValue | NumberValue | RefValue | AiScriptRefValue | UserFnValue; + +export function isLiteralValue(expr: Expr): expr is Value { + if (expr.type == null) return true; + if (literalDefs[expr.type]) return true; + return false; +} + +// call function + +export type CallFn = ExprBase & { // "fn:hoge" or string + type: string; + args: Expr[]; + value: null; +}; + +// variable +export type Variable = (Value | CallFn) & { + name: string; +}; + +// expression +export type Expr = Variable | Value | CallFn; diff --git a/packages/frontend/src/scripts/hpml/index.ts b/packages/frontend/src/scripts/hpml/index.ts new file mode 100644 index 0000000000..9a55a5c286 --- /dev/null +++ b/packages/frontend/src/scripts/hpml/index.ts @@ -0,0 +1,103 @@ +/** + * Hpml + */ + +import autobind from 'autobind-decorator'; +import { Hpml } from './evaluator'; +import { funcDefs } from './lib'; + +export type Fn = { + slots: string[]; + exec: (args: Record) => ReturnType; +}; + +export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null; + +export const literalDefs: Record = { + text: { out: 'string', category: 'value', icon: 'ti ti-quote' }, + multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left' }, + textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list' }, + number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up' }, + ref: { out: null, category: 'value', icon: 'fas fa-magic' }, + aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic' }, + fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt' }, +}; + +export const blockDefs = [ + ...Object.entries(literalDefs).map(([k, v]) => ({ + type: k, out: v.out, category: v.category, icon: v.icon, + })), + ...Object.entries(funcDefs).map(([k, v]) => ({ + type: k, out: v.out, category: v.category, icon: v.icon, + })), +]; + +export type PageVar = { name: string; value: any; type: Type; }; + +export const envVarsDef: Record = { + AI: 'string', + URL: 'string', + VERSION: 'string', + LOGIN: 'boolean', + NAME: 'string', + USERNAME: 'string', + USERID: 'string', + NOTES_COUNT: 'number', + FOLLOWERS_COUNT: 'number', + FOLLOWING_COUNT: 'number', + IS_CAT: 'boolean', + SEED: null, + YMD: 'string', + AISCRIPT_DISABLED: 'boolean', + NULL: null, +}; + +export class HpmlScope { + private layerdStates: Record[]; + public name: string; + + constructor(layerdStates: HpmlScope['layerdStates'], name?: HpmlScope['name']) { + this.layerdStates = layerdStates; + this.name = name || 'anonymous'; + } + + @autobind + public createChildScope(states: Record, name?: HpmlScope['name']): HpmlScope { + const layer = [states, ...this.layerdStates]; + return new HpmlScope(layer, name); + } + + /** + * 指定した名前の変数の値を取得します + * @param name 変数名 + */ + @autobind + public getState(name: string): any { + for (const later of this.layerdStates) { + const state = later[name]; + if (state !== undefined) { + return state; + } + } + + throw new HpmlError( + `No such variable '${name}' in scope '${this.name}'`, { + scope: this.layerdStates, + }); + } +} + +export class HpmlError extends Error { + public info?: any; + + constructor(message: string, info?: any) { + super(message); + + this.info = info; + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, HpmlError); + } + } +} diff --git a/packages/frontend/src/scripts/hpml/lib.ts b/packages/frontend/src/scripts/hpml/lib.ts new file mode 100644 index 0000000000..b684876a7f --- /dev/null +++ b/packages/frontend/src/scripts/hpml/lib.ts @@ -0,0 +1,247 @@ +import tinycolor from 'tinycolor2'; +import { Hpml } from './evaluator'; +import { values, utils } from '@syuilo/aiscript'; +import { Fn, HpmlScope } from '.'; +import { Expr } from './expr'; +import seedrandom from 'seedrandom'; + +/* TODO: https://www.chartjs.org/docs/latest/configuration/canvas-background.html#color +// https://stackoverflow.com/questions/38493564/chart-area-background-color-chartjs +Chart.pluginService.register({ + beforeDraw: (chart, easing) => { + if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) { + const ctx = chart.chart.ctx; + ctx.save(); + ctx.fillStyle = chart.config.options.chartArea.backgroundColor; + ctx.fillRect(0, 0, chart.chart.width, chart.chart.height); + ctx.restore(); + } + } +}); +*/ + +export function initAiLib(hpml: Hpml) { + return { + 'MkPages:updated': values.FN_NATIVE(([callback]) => { + hpml.pageVarUpdatedCallback = (callback as values.VFn); + }), + 'MkPages:get_canvas': values.FN_NATIVE(([id]) => { + utils.assertString(id); + const canvas = hpml.canvases[id.value]; + const ctx = canvas.getContext('2d'); + return values.OBJ(new Map([ + ['clear_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.clearRect(x.value, y.value, width.value, height.value); })], + ['fill_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.fillRect(x.value, y.value, width.value, height.value); })], + ['stroke_rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.strokeRect(x.value, y.value, width.value, height.value); })], + ['fill_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.fillText(text.value, x.value, y.value, width ? width.value : undefined); })], + ['stroke_text', values.FN_NATIVE(([text, x, y, width]) => { ctx.strokeText(text.value, x.value, y.value, width ? width.value : undefined); })], + ['set_line_width', values.FN_NATIVE(([width]) => { ctx.lineWidth = width.value; })], + ['set_font', values.FN_NATIVE(([font]) => { ctx.font = font.value; })], + ['set_fill_style', values.FN_NATIVE(([style]) => { ctx.fillStyle = style.value; })], + ['set_stroke_style', values.FN_NATIVE(([style]) => { ctx.strokeStyle = style.value; })], + ['begin_path', values.FN_NATIVE(() => { ctx.beginPath(); })], + ['close_path', values.FN_NATIVE(() => { ctx.closePath(); })], + ['move_to', values.FN_NATIVE(([x, y]) => { ctx.moveTo(x.value, y.value); })], + ['line_to', values.FN_NATIVE(([x, y]) => { ctx.lineTo(x.value, y.value); })], + ['arc', values.FN_NATIVE(([x, y, radius, startAngle, endAngle]) => { ctx.arc(x.value, y.value, radius.value, startAngle.value, endAngle.value); })], + ['rect', values.FN_NATIVE(([x, y, width, height]) => { ctx.rect(x.value, y.value, width.value, height.value); })], + ['fill', values.FN_NATIVE(() => { ctx.fill(); })], + ['stroke', values.FN_NATIVE(() => { ctx.stroke(); })], + ])); + }), + 'MkPages:chart': values.FN_NATIVE(([id, opts]) => { + /* TODO + utils.assertString(id); + utils.assertObject(opts); + const canvas = hpml.canvases[id.value]; + const color = getComputedStyle(document.documentElement).getPropertyValue('--accent'); + Chart.defaults.color = '#555'; + const chart = new Chart(canvas, { + type: opts.value.get('type').value, + data: { + labels: opts.value.get('labels').value.map(x => x.value), + datasets: opts.value.get('datasets').value.map(x => ({ + label: x.value.has('label') ? x.value.get('label').value : '', + data: x.value.get('data').value.map(x => x.value), + pointRadius: 0, + lineTension: 0, + borderWidth: 2, + borderColor: x.value.has('color') ? x.value.get('color') : color, + backgroundColor: tinycolor(x.value.has('color') ? x.value.get('color') : color).setAlpha(0.1).toRgbString(), + })) + }, + options: { + responsive: false, + devicePixelRatio: 1.5, + title: { + display: opts.value.has('title'), + text: opts.value.has('title') ? opts.value.get('title').value : '', + fontSize: 14, + }, + layout: { + padding: { + left: 32, + right: 32, + top: opts.value.has('title') ? 16 : 32, + bottom: 16 + } + }, + legend: { + display: opts.value.get('datasets').value.filter(x => x.value.has('label') && x.value.get('label').value).length === 0 ? false : true, + position: 'bottom', + labels: { + boxWidth: 16, + } + }, + tooltips: { + enabled: false, + }, + chartArea: { + backgroundColor: '#fff' + }, + ...(opts.value.get('type').value === 'radar' ? { + scale: { + ticks: { + display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : false, + min: opts.value.has('min') ? opts.value.get('min').value : undefined, + max: opts.value.has('max') ? opts.value.get('max').value : undefined, + maxTicksLimit: 8, + }, + pointLabels: { + fontSize: 12 + } + } + } : { + scales: { + yAxes: [{ + ticks: { + display: opts.value.has('show_tick_label') ? opts.value.get('show_tick_label').value : true, + min: opts.value.has('min') ? opts.value.get('min').value : undefined, + max: opts.value.has('max') ? opts.value.get('max').value : undefined, + } + }] + } + }) + } + }); + */ + }), + }; +} + +export const funcDefs: Record = { + if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'ti ti-share' }, + for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle' }, + not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, + or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, + and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, + add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-plus' }, + subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-minus' }, + multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'ti ti-x' }, + divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' }, + mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' }, + round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator' }, + eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals' }, + notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal' }, + gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than' }, + lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than' }, + gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal' }, + ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal' }, + strLen: { in: ['string'], out: 'number', category: 'text', icon: 'ti ti-quote' }, + strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'ti ti-quote' }, + strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, + strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, + join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'ti ti-quote' }, + stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt' }, + numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt' }, + splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt' }, + pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent' }, + listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent' }, + rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, + dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, + seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, + random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, + dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, + seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, + randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' }, + dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' }, + seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice' }, + DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice' }, // dailyRandomPickWithProbabilityMapping +}; + +export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) { + const date = new Date(); + const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; + + // SHOULD be fine to ignore since it's intended + function shape isn't defined + // eslint-disable-next-line @typescript-eslint/ban-types + const funcs: Record = { + not: (a: boolean) => !a, + or: (a: boolean, b: boolean) => a || b, + and: (a: boolean, b: boolean) => a && b, + eq: (a: any, b: any) => a === b, + notEq: (a: any, b: any) => a !== b, + gt: (a: number, b: number) => a > b, + lt: (a: number, b: number) => a < b, + gtEq: (a: number, b: number) => a >= b, + ltEq: (a: number, b: number) => a <= b, + if: (bool: boolean, a: any, b: any) => bool ? a : b, + for: (times: number, fn: Fn) => { + const result: any[] = []; + for (let i = 0; i < times; i++) { + result.push(fn.exec({ + [fn.slots[0]]: i + 1, + })); + } + return result; + }, + add: (a: number, b: number) => a + b, + subtract: (a: number, b: number) => a - b, + multiply: (a: number, b: number) => a * b, + divide: (a: number, b: number) => a / b, + mod: (a: number, b: number) => a % b, + round: (a: number) => Math.round(a), + strLen: (a: string) => a.length, + strPick: (a: string, b: number) => a[b - 1], + strReplace: (a: string, b: string, c: string) => a.split(b).join(c), + strReverse: (a: string) => a.split('').reverse().join(''), + join: (texts: string[], separator: string) => texts.join(separator || ''), + stringToNumber: (a: string) => parseInt(a), + numberToString: (a: number) => a.toString(), + splitStrByLine: (a: string) => a.split('\n'), + pick: (list: any[], i: number) => list[i - 1], + listLen: (list: any[]) => list.length, + random: (probability: number) => Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * 100) < probability, + rannum: (min: number, max: number) => min + Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * (max - min + 1)), + randomPick: (list: any[]) => list[Math.floor(seedrandom(`${randomSeed}:${expr.id}`)() * list.length)], + dailyRandom: (probability: number) => Math.floor(seedrandom(`${day}:${expr.id}`)() * 100) < probability, + dailyRannum: (min: number, max: number) => min + Math.floor(seedrandom(`${day}:${expr.id}`)() * (max - min + 1)), + dailyRandomPick: (list: any[]) => list[Math.floor(seedrandom(`${day}:${expr.id}`)() * list.length)], + seedRandom: (seed: any, probability: number) => Math.floor(seedrandom(seed)() * 100) < probability, + seedRannum: (seed: any, min: number, max: number) => min + Math.floor(seedrandom(seed)() * (max - min + 1)), + seedRandomPick: (seed: any, list: any[]) => list[Math.floor(seedrandom(seed)() * list.length)], + DRPWPM: (list: string[]) => { + const xs: any[] = []; + let totalFactor = 0; + for (const x of list) { + const parts = x.split(' '); + const factor = parseInt(parts.pop()!, 10); + const text = parts.join(' '); + totalFactor += factor; + xs.push({ factor, text }); + } + const r = seedrandom(`${day}:${expr.id}`)() * totalFactor; + let stackedFactor = 0; + for (const x of xs) { + if (r >= stackedFactor && r <= stackedFactor + x.factor) { + return x.text; + } else { + stackedFactor += x.factor; + } + } + return xs[0].text; + }, + }; + + return funcs; +} diff --git a/packages/frontend/src/scripts/hpml/type-checker.ts b/packages/frontend/src/scripts/hpml/type-checker.ts new file mode 100644 index 0000000000..24c9ed8bcb --- /dev/null +++ b/packages/frontend/src/scripts/hpml/type-checker.ts @@ -0,0 +1,191 @@ +import autobind from 'autobind-decorator'; +import { isLiteralValue } from './expr'; +import { funcDefs } from './lib'; +import { envVarsDef } from '.'; +import type { Type, PageVar } from '.'; +import type { Expr, Variable } from './expr'; + +type TypeError = { + arg: number; + expect: Type; + actual: Type; +}; + +/** + * Hpml type checker + */ +export class HpmlTypeChecker { + public variables: Variable[]; + public pageVars: PageVar[]; + + constructor(variables: HpmlTypeChecker['variables'] = [], pageVars: HpmlTypeChecker['pageVars'] = []) { + this.variables = variables; + this.pageVars = pageVars; + } + + @autobind + public typeCheck(v: Expr): TypeError | null { + if (isLiteralValue(v)) return null; + + const def = funcDefs[v.type || '']; + if (def == null) { + throw new Error('Unknown type: ' + v.type); + } + + const generic: Type[] = []; + + for (let i = 0; i < def.in.length; i++) { + const arg = def.in[i]; + const type = this.infer(v.args[i]); + if (type === null) continue; + + if (typeof arg === 'number') { + if (generic[arg] === undefined) { + generic[arg] = type; + } else if (type !== generic[arg]) { + return { + arg: i, + expect: generic[arg], + actual: type, + }; + } + } else if (type !== arg) { + return { + arg: i, + expect: arg, + actual: type, + }; + } + } + + return null; + } + + @autobind + public getExpectedType(v: Expr, slot: number): Type { + const def = funcDefs[v.type || '']; + if (def == null) { + throw new Error('Unknown type: ' + v.type); + } + + const generic: Type[] = []; + + for (let i = 0; i < def.in.length; i++) { + const arg = def.in[i]; + const type = this.infer(v.args[i]); + if (type === null) continue; + + if (typeof arg === 'number') { + if (generic[arg] === undefined) { + generic[arg] = type; + } + } + } + + if (typeof def.in[slot] === 'number') { + return generic[def.in[slot]] ?? null; + } else { + return def.in[slot]; + } + } + + @autobind + public infer(v: Expr): Type { + if (v.type === null) return null; + if (v.type === 'text') return 'string'; + if (v.type === 'multiLineText') return 'string'; + if (v.type === 'textList') return 'stringArray'; + if (v.type === 'number') return 'number'; + if (v.type === 'ref') { + const variable = this.variables.find(va => va.name === v.value); + if (variable) { + return this.infer(variable); + } + + const pageVar = this.pageVars.find(va => va.name === v.value); + if (pageVar) { + return pageVar.type; + } + + const envVar = envVarsDef[v.value || '']; + if (envVar !== undefined) { + return envVar; + } + + return null; + } + if (v.type === 'aiScriptVar') return null; + if (v.type === 'fn') return null; // todo + if (v.type.startsWith('fn:')) return null; // todo + + const generic: Type[] = []; + + const def = funcDefs[v.type]; + + for (let i = 0; i < def.in.length; i++) { + const arg = def.in[i]; + if (typeof arg === 'number') { + const type = this.infer(v.args[i]); + + if (generic[arg] === undefined) { + generic[arg] = type; + } else { + if (type !== generic[arg]) { + generic[arg] = null; + } + } + } + } + + if (typeof def.out === 'number') { + return generic[def.out]; + } else { + return def.out; + } + } + + @autobind + public getVarByName(name: string): Variable { + const v = this.variables.find(x => x.name === name); + if (v !== undefined) { + return v; + } else { + throw new Error(`No such variable '${name}'`); + } + } + + @autobind + public getVarsByType(type: Type): Variable[] { + if (type == null) return this.variables; + return this.variables.filter(x => (this.infer(x) === null) || (this.infer(x) === type)); + } + + @autobind + public getEnvVarsByType(type: Type): string[] { + if (type == null) return Object.keys(envVarsDef); + return Object.entries(envVarsDef).filter(([k, v]) => v === null || type === v).map(([k, v]) => k); + } + + @autobind + public getPageVarsByType(type: Type): string[] { + if (type == null) return this.pageVars.map(v => v.name); + return this.pageVars.filter(v => type === v.type).map(v => v.name); + } + + @autobind + public isUsedName(name: string) { + if (this.variables.some(v => v.name === name)) { + return true; + } + + if (this.pageVars.some(v => v.name === name)) { + return true; + } + + if (envVarsDef[name]) { + return true; + } + + return false; + } +} diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts new file mode 100644 index 0000000000..54184386da --- /dev/null +++ b/packages/frontend/src/scripts/i18n.ts @@ -0,0 +1,29 @@ +export class I18n> { + public ts: T; + + constructor(locale: T) { + this.ts = locale; + + //#region BIND + this.t = this.t.bind(this); + //#endregion + } + + // string にしているのは、ドット区切りでのパス指定を許可するため + // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも + public t(key: string, args?: Record): string { + try { + let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; + + if (args) { + for (const [k, v] of Object.entries(args)) { + str = str.replace(`{${k}}`, v.toString()); + } + } + return str; + } catch (err) { + console.warn(`missing localization '${key}'`); + return key; + } + } +} diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts new file mode 100644 index 0000000000..77bb84463c --- /dev/null +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -0,0 +1,36 @@ +// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、 +// indexedDBが使えない環境ではlocalStorageを使う +import { + get as iget, + set as iset, + del as idel, +} from 'idb-keyval'; + +const fallbackName = (key: string) => `idbfallback::${key}`; + +let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true; + +if (idbAvailable) { + iset('idb-test', 'test').catch(err => { + console.error('idb error', err); + console.error('indexedDB is unavailable. It will use localStorage.'); + idbAvailable = false; + }); +} else { + console.error('indexedDB is unavailable. It will use localStorage.'); +} + +export async function get(key: string) { + if (idbAvailable) return iget(key); + return JSON.parse(localStorage.getItem(fallbackName(key))); +} + +export async function set(key: string, val: any) { + if (idbAvailable) return iset(key, val); + return localStorage.setItem(fallbackName(key), JSON.stringify(val)); +} + +export async function del(key: string) { + if (idbAvailable) return idel(key); + return localStorage.removeItem(fallbackName(key)); +} diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts new file mode 100644 index 0000000000..de52f30523 --- /dev/null +++ b/packages/frontend/src/scripts/initialize-sw.ts @@ -0,0 +1,13 @@ +import { lang } from '@/config'; + +export async function initializeSw() { + if (!('serviceWorker' in navigator)) return; + + navigator.serviceWorker.register(`/sw.js`, { scope: '/', type: 'classic' }); + navigator.serviceWorker.ready.then(registration => { + registration.active?.postMessage({ + msg: 'initialize', + lang, + }); + }); +} diff --git a/packages/frontend/src/scripts/is-device-darkmode.ts b/packages/frontend/src/scripts/is-device-darkmode.ts new file mode 100644 index 0000000000..854f38e517 --- /dev/null +++ b/packages/frontend/src/scripts/is-device-darkmode.ts @@ -0,0 +1,3 @@ +export function isDeviceDarkmode() { + return window.matchMedia('(prefers-color-scheme: dark)').matches; +} diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts new file mode 100644 index 0000000000..69f6a82803 --- /dev/null +++ b/packages/frontend/src/scripts/keycode.ts @@ -0,0 +1,33 @@ +export default (input: string): string[] => { + if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) { + const codes = aliases[input]; + return Array.isArray(codes) ? codes : [codes]; + } else { + return [input]; + } +}; + +export const aliases = { + 'esc': 'Escape', + 'enter': ['Enter', 'NumpadEnter'], + 'up': 'ArrowUp', + 'down': 'ArrowDown', + 'left': 'ArrowLeft', + 'right': 'ArrowRight', + 'plus': ['NumpadAdd', 'Semicolon'], +}; + +/*! +* Programmatically add the following +*/ + +// lower case chars +for (let i = 97; i < 123; i++) { + const char = String.fromCharCode(i); + aliases[char] = `Key${char.toUpperCase()}`; +} + +// numbers +for (let i = 0; i < 10; i++) { + aliases[i] = [`Numpad${i}`, `Digit${i}`]; +} diff --git a/packages/frontend/src/scripts/langmap.ts b/packages/frontend/src/scripts/langmap.ts new file mode 100644 index 0000000000..25f5b366c8 --- /dev/null +++ b/packages/frontend/src/scripts/langmap.ts @@ -0,0 +1,666 @@ +// TODO: sharedに置いてバックエンドのと統合したい +export const langmap = { + 'ach': { + nativeName: 'Lwo', + }, + 'ady': { + nativeName: 'Адыгэбзэ', + }, + 'af': { + nativeName: 'Afrikaans', + }, + 'af-NA': { + nativeName: 'Afrikaans (Namibia)', + }, + 'af-ZA': { + nativeName: 'Afrikaans (South Africa)', + }, + 'ak': { + nativeName: 'Tɕɥi', + }, + 'ar': { + nativeName: 'العربية', + }, + 'ar-AR': { + nativeName: 'العربية', + }, + 'ar-MA': { + nativeName: 'العربية', + }, + 'ar-SA': { + nativeName: 'العربية (السعودية)', + }, + 'ay-BO': { + nativeName: 'Aymar aru', + }, + 'az': { + nativeName: 'Azərbaycan dili', + }, + 'az-AZ': { + nativeName: 'Azərbaycan dili', + }, + 'be-BY': { + nativeName: 'Беларуская', + }, + 'bg': { + nativeName: 'Български', + }, + 'bg-BG': { + nativeName: 'Български', + }, + 'bn': { + nativeName: 'বাংলা', + }, + 'bn-IN': { + nativeName: 'বাংলা (ভারত)', + }, + 'bn-BD': { + nativeName: 'বাংলা(বাংলাদেশ)', + }, + 'br': { + nativeName: 'Brezhoneg', + }, + 'bs-BA': { + nativeName: 'Bosanski', + }, + 'ca': { + nativeName: 'Català', + }, + 'ca-ES': { + nativeName: 'Català', + }, + 'cak': { + nativeName: 'Maya Kaqchikel', + }, + 'ck-US': { + nativeName: 'ᏣᎳᎩ (tsalagi)', + }, + 'cs': { + nativeName: 'Čeština', + }, + 'cs-CZ': { + nativeName: 'Čeština', + }, + 'cy': { + nativeName: 'Cymraeg', + }, + 'cy-GB': { + nativeName: 'Cymraeg', + }, + 'da': { + nativeName: 'Dansk', + }, + 'da-DK': { + nativeName: 'Dansk', + }, + 'de': { + nativeName: 'Deutsch', + }, + 'de-AT': { + nativeName: 'Deutsch (Österreich)', + }, + 'de-DE': { + nativeName: 'Deutsch (Deutschland)', + }, + 'de-CH': { + nativeName: 'Deutsch (Schweiz)', + }, + 'dsb': { + nativeName: 'Dolnoserbšćina', + }, + 'el': { + nativeName: 'Ελληνικά', + }, + 'el-GR': { + nativeName: 'Ελληνικά', + }, + 'en': { + nativeName: 'English', + }, + 'en-GB': { + nativeName: 'English (UK)', + }, + 'en-AU': { + nativeName: 'English (Australia)', + }, + 'en-CA': { + nativeName: 'English (Canada)', + }, + 'en-IE': { + nativeName: 'English (Ireland)', + }, + 'en-IN': { + nativeName: 'English (India)', + }, + 'en-PI': { + nativeName: 'English (Pirate)', + }, + 'en-SG': { + nativeName: 'English (Singapore)', + }, + 'en-UD': { + nativeName: 'English (Upside Down)', + }, + 'en-US': { + nativeName: 'English (US)', + }, + 'en-ZA': { + nativeName: 'English (South Africa)', + }, + 'en@pirate': { + nativeName: 'English (Pirate)', + }, + 'eo': { + nativeName: 'Esperanto', + }, + 'eo-EO': { + nativeName: 'Esperanto', + }, + 'es': { + nativeName: 'Español', + }, + 'es-AR': { + nativeName: 'Español (Argentine)', + }, + 'es-419': { + nativeName: 'Español (Latinoamérica)', + }, + 'es-CL': { + nativeName: 'Español (Chile)', + }, + 'es-CO': { + nativeName: 'Español (Colombia)', + }, + 'es-EC': { + nativeName: 'Español (Ecuador)', + }, + 'es-ES': { + nativeName: 'Español (España)', + }, + 'es-LA': { + nativeName: 'Español (Latinoamérica)', + }, + 'es-NI': { + nativeName: 'Español (Nicaragua)', + }, + 'es-MX': { + nativeName: 'Español (México)', + }, + 'es-US': { + nativeName: 'Español (Estados Unidos)', + }, + 'es-VE': { + nativeName: 'Español (Venezuela)', + }, + 'et': { + nativeName: 'eesti keel', + }, + 'et-EE': { + nativeName: 'Eesti (Estonia)', + }, + 'eu': { + nativeName: 'Euskara', + }, + 'eu-ES': { + nativeName: 'Euskara', + }, + 'fa': { + nativeName: 'فارسی', + }, + 'fa-IR': { + nativeName: 'فارسی', + }, + 'fb-LT': { + nativeName: 'Leet Speak', + }, + 'ff': { + nativeName: 'Fulah', + }, + 'fi': { + nativeName: 'Suomi', + }, + 'fi-FI': { + nativeName: 'Suomi', + }, + 'fo': { + nativeName: 'Føroyskt', + }, + 'fo-FO': { + nativeName: 'Føroyskt (Færeyjar)', + }, + 'fr': { + nativeName: 'Français', + }, + 'fr-CA': { + nativeName: 'Français (Canada)', + }, + 'fr-FR': { + nativeName: 'Français (France)', + }, + 'fr-BE': { + nativeName: 'Français (Belgique)', + }, + 'fr-CH': { + nativeName: 'Français (Suisse)', + }, + 'fy-NL': { + nativeName: 'Frysk', + }, + 'ga': { + nativeName: 'Gaeilge', + }, + 'ga-IE': { + nativeName: 'Gaeilge', + }, + 'gd': { + nativeName: 'Gàidhlig', + }, + 'gl': { + nativeName: 'Galego', + }, + 'gl-ES': { + nativeName: 'Galego', + }, + 'gn-PY': { + nativeName: 'Avañe\'ẽ', + }, + 'gu-IN': { + nativeName: 'ગુજરાતી', + }, + 'gv': { + nativeName: 'Gaelg', + }, + 'gx-GR': { + nativeName: 'Ἑλληνική ἀρχαία', + }, + 'he': { + nativeName: 'עברית‏', + }, + 'he-IL': { + nativeName: 'עברית‏', + }, + 'hi': { + nativeName: 'हिन्दी', + }, + 'hi-IN': { + nativeName: 'हिन्दी', + }, + 'hr': { + nativeName: 'Hrvatski', + }, + 'hr-HR': { + nativeName: 'Hrvatski', + }, + 'hsb': { + nativeName: 'Hornjoserbšćina', + }, + 'ht': { + nativeName: 'Kreyòl', + }, + 'hu': { + nativeName: 'Magyar', + }, + 'hu-HU': { + nativeName: 'Magyar', + }, + 'hy': { + nativeName: 'Հայերեն', + }, + 'hy-AM': { + nativeName: 'Հայերեն (Հայաստան)', + }, + 'id': { + nativeName: 'Bahasa Indonesia', + }, + 'id-ID': { + nativeName: 'Bahasa Indonesia', + }, + 'is': { + nativeName: 'Íslenska', + }, + 'is-IS': { + nativeName: 'Íslenska (Iceland)', + }, + 'it': { + nativeName: 'Italiano', + }, + 'it-IT': { + nativeName: 'Italiano', + }, + 'ja': { + nativeName: '日本語', + }, + 'ja-JP': { + nativeName: '日本語 (日本)', + }, + 'jv-ID': { + nativeName: 'Basa Jawa', + }, + 'ka-GE': { + nativeName: 'ქართული', + }, + 'kk-KZ': { + nativeName: 'Қазақша', + }, + 'km': { + nativeName: 'ភាសាខ្មែរ', + }, + 'kl': { + nativeName: 'kalaallisut', + }, + 'km-KH': { + nativeName: 'ភាសាខ្មែរ', + }, + 'kab': { + nativeName: 'Taqbaylit', + }, + 'kn': { + nativeName: 'ಕನ್ನಡ', + }, + 'kn-IN': { + nativeName: 'ಕನ್ನಡ (India)', + }, + 'ko': { + nativeName: '한국어', + }, + 'ko-KR': { + nativeName: '한국어 (한국)', + }, + 'ku-TR': { + nativeName: 'Kurdî', + }, + 'kw': { + nativeName: 'Kernewek', + }, + 'la': { + nativeName: 'Latin', + }, + 'la-VA': { + nativeName: 'Latin', + }, + 'lb': { + nativeName: 'Lëtzebuergesch', + }, + 'li-NL': { + nativeName: 'Lèmbörgs', + }, + 'lt': { + nativeName: 'Lietuvių', + }, + 'lt-LT': { + nativeName: 'Lietuvių', + }, + 'lv': { + nativeName: 'Latviešu', + }, + 'lv-LV': { + nativeName: 'Latviešu', + }, + 'mai': { + nativeName: 'मैथिली, মৈথিলী', + }, + 'mg-MG': { + nativeName: 'Malagasy', + }, + 'mk': { + nativeName: 'Македонски', + }, + 'mk-MK': { + nativeName: 'Македонски (Македонски)', + }, + 'ml': { + nativeName: 'മലയാളം', + }, + 'ml-IN': { + nativeName: 'മലയാളം', + }, + 'mn-MN': { + nativeName: 'Монгол', + }, + 'mr': { + nativeName: 'मराठी', + }, + 'mr-IN': { + nativeName: 'मराठी', + }, + 'ms': { + nativeName: 'Bahasa Melayu', + }, + 'ms-MY': { + nativeName: 'Bahasa Melayu', + }, + 'mt': { + nativeName: 'Malti', + }, + 'mt-MT': { + nativeName: 'Malti', + }, + 'my': { + nativeName: 'ဗမာစကာ', + }, + 'no': { + nativeName: 'Norsk', + }, + 'nb': { + nativeName: 'Norsk (bokmål)', + }, + 'nb-NO': { + nativeName: 'Norsk (bokmål)', + }, + 'ne': { + nativeName: 'नेपाली', + }, + 'ne-NP': { + nativeName: 'नेपाली', + }, + 'nl': { + nativeName: 'Nederlands', + }, + 'nl-BE': { + nativeName: 'Nederlands (België)', + }, + 'nl-NL': { + nativeName: 'Nederlands (Nederland)', + }, + 'nn-NO': { + nativeName: 'Norsk (nynorsk)', + }, + 'oc': { + nativeName: 'Occitan', + }, + 'or-IN': { + nativeName: 'ଓଡ଼ିଆ', + }, + 'pa': { + nativeName: 'ਪੰਜਾਬੀ', + }, + 'pa-IN': { + nativeName: 'ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)', + }, + 'pl': { + nativeName: 'Polski', + }, + 'pl-PL': { + nativeName: 'Polski', + }, + 'ps-AF': { + nativeName: 'پښتو', + }, + 'pt': { + nativeName: 'Português', + }, + 'pt-BR': { + nativeName: 'Português (Brasil)', + }, + 'pt-PT': { + nativeName: 'Português (Portugal)', + }, + 'qu-PE': { + nativeName: 'Qhichwa', + }, + 'rm-CH': { + nativeName: 'Rumantsch', + }, + 'ro': { + nativeName: 'Română', + }, + 'ro-RO': { + nativeName: 'Română', + }, + 'ru': { + nativeName: 'Русский', + }, + 'ru-RU': { + nativeName: 'Русский', + }, + 'sa-IN': { + nativeName: 'संस्कृतम्', + }, + 'se-NO': { + nativeName: 'Davvisámegiella', + }, + 'sh': { + nativeName: 'српскохрватски', + }, + 'si-LK': { + nativeName: 'සිංහල', + }, + 'sk': { + nativeName: 'Slovenčina', + }, + 'sk-SK': { + nativeName: 'Slovenčina (Slovakia)', + }, + 'sl': { + nativeName: 'Slovenščina', + }, + 'sl-SI': { + nativeName: 'Slovenščina', + }, + 'so-SO': { + nativeName: 'Soomaaliga', + }, + 'sq': { + nativeName: 'Shqip', + }, + 'sq-AL': { + nativeName: 'Shqip', + }, + 'sr': { + nativeName: 'Српски', + }, + 'sr-RS': { + nativeName: 'Српски (Serbia)', + }, + 'su': { + nativeName: 'Basa Sunda', + }, + 'sv': { + nativeName: 'Svenska', + }, + 'sv-SE': { + nativeName: 'Svenska', + }, + 'sw': { + nativeName: 'Kiswahili', + }, + 'sw-KE': { + nativeName: 'Kiswahili', + }, + 'ta': { + nativeName: 'தமிழ்', + }, + 'ta-IN': { + nativeName: 'தமிழ்', + }, + 'te': { + nativeName: 'తెలుగు', + }, + 'te-IN': { + nativeName: 'తెలుగు', + }, + 'tg': { + nativeName: 'забо́ни тоҷикӣ́', + }, + 'tg-TJ': { + nativeName: 'тоҷикӣ', + }, + 'th': { + nativeName: 'ภาษาไทย', + }, + 'th-TH': { + nativeName: 'ภาษาไทย (ประเทศไทย)', + }, + 'fil': { + nativeName: 'Filipino', + }, + 'tlh': { + nativeName: 'tlhIngan-Hol', + }, + 'tr': { + nativeName: 'Türkçe', + }, + 'tr-TR': { + nativeName: 'Türkçe', + }, + 'tt-RU': { + nativeName: 'татарча', + }, + 'uk': { + nativeName: 'Українська', + }, + 'uk-UA': { + nativeName: 'Українська', + }, + 'ur': { + nativeName: 'اردو', + }, + 'ur-PK': { + nativeName: 'اردو', + }, + 'uz': { + nativeName: 'O\'zbek', + }, + 'uz-UZ': { + nativeName: 'O\'zbek', + }, + 'vi': { + nativeName: 'Tiếng Việt', + }, + 'vi-VN': { + nativeName: 'Tiếng Việt', + }, + 'xh-ZA': { + nativeName: 'isiXhosa', + }, + 'yi': { + nativeName: 'ייִדיש', + }, + 'yi-DE': { + nativeName: 'ייִדיש (German)', + }, + 'zh': { + nativeName: '中文', + }, + 'zh-Hans': { + nativeName: '中文简体', + }, + 'zh-Hant': { + nativeName: '中文繁體', + }, + 'zh-CN': { + nativeName: '中文(中国大陆)', + }, + 'zh-HK': { + nativeName: '中文(香港)', + }, + 'zh-SG': { + nativeName: '中文(新加坡)', + }, + 'zh-TW': { + nativeName: '中文(台灣)', + }, + 'zu-ZA': { + nativeName: 'isiZulu', + }, +}; diff --git a/packages/frontend/src/scripts/login-id.ts b/packages/frontend/src/scripts/login-id.ts new file mode 100644 index 0000000000..0f9c6be4a9 --- /dev/null +++ b/packages/frontend/src/scripts/login-id.ts @@ -0,0 +1,11 @@ +export function getUrlWithLoginId(url: string, loginId: string) { + const u = new URL(url, origin); + u.searchParams.append('loginId', loginId); + return u.toString(); +} + +export function getUrlWithoutLoginId(url: string) { + const u = new URL(url); + u.searchParams.delete('loginId'); + return u.toString(); +} diff --git a/packages/frontend/src/scripts/lookup-user.ts b/packages/frontend/src/scripts/lookup-user.ts new file mode 100644 index 0000000000..3ab9d55300 --- /dev/null +++ b/packages/frontend/src/scripts/lookup-user.ts @@ -0,0 +1,36 @@ +import * as Acct from 'misskey-js/built/acct'; +import { i18n } from '@/i18n'; +import * as os from '@/os'; + +export async function lookupUser() { + const { canceled, result } = await os.inputText({ + title: i18n.ts.usernameOrUserId, + }); + if (canceled) return; + + const show = (user) => { + os.pageWindow(`/user-info/${user.id}`); + }; + + const usernamePromise = os.api('users/show', Acct.parse(result)); + const idPromise = os.api('users/show', { userId: result }); + let _notFound = false; + const notFound = () => { + if (_notFound) { + os.alert({ + type: 'error', + text: i18n.ts.noSuchUser, + }); + } else { + _notFound = true; + } + }; + usernamePromise.then(show).catch(err => { + if (err.code === 'NO_SUCH_USER') { + notFound(); + } + }); + idPromise.then(show).catch(err => { + notFound(); + }); +} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts new file mode 100644 index 0000000000..aaf7f9e610 --- /dev/null +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -0,0 +1,15 @@ +import { query } from '@/scripts/url'; +import { url } from '@/config'; + +export function getProxiedImageUrl(imageUrl: string, type?: 'preview'): string { + return `${url}/proxy/image.webp?${query({ + url: imageUrl, + fallback: '1', + ...(type ? { [type]: '1' } : {}), + })}`; +} + +export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { + if (imageUrl == null) return null; + return getProxiedImageUrl(imageUrl, type); +} diff --git a/packages/frontend/src/scripts/mfm-tags.ts b/packages/frontend/src/scripts/mfm-tags.ts new file mode 100644 index 0000000000..18e8d7038a --- /dev/null +++ b/packages/frontend/src/scripts/mfm-tags.ts @@ -0,0 +1 @@ +export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'sparkle', 'rotate']; diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts new file mode 100644 index 0000000000..0db8369f9d --- /dev/null +++ b/packages/frontend/src/scripts/page-metadata.ts @@ -0,0 +1,41 @@ +import * as misskey from 'misskey-js'; +import { ComputedRef, inject, isRef, onActivated, onMounted, provide, ref, Ref } from 'vue'; + +export const setPageMetadata = Symbol('setPageMetadata'); +export const pageMetadataProvider = Symbol('pageMetadataProvider'); + +export type PageMetadata = { + title: string; + subtitle?: string; + icon?: string | null; + avatar?: misskey.entities.User | null; + userName?: misskey.entities.User | null; + bg?: string; +}; + +export function definePageMetadata(metadata: PageMetadata | null | Ref | ComputedRef): void { + const _metadata = isRef(metadata) ? metadata : ref(metadata); + + provide(pageMetadataProvider, _metadata); + + const set = inject(setPageMetadata) as any; + if (set) { + set(_metadata); + + onMounted(() => { + set(_metadata); + }); + + onActivated(() => { + set(_metadata); + }); + } +} + +export function provideMetadataReceiver(callback: (info: ComputedRef) => void): void { + provide(setPageMetadata, callback); +} + +export function injectPageMetadata(): PageMetadata | undefined { + return inject(pageMetadataProvider); +} diff --git a/packages/frontend/src/scripts/physics.ts b/packages/frontend/src/scripts/physics.ts new file mode 100644 index 0000000000..efda80f074 --- /dev/null +++ b/packages/frontend/src/scripts/physics.ts @@ -0,0 +1,152 @@ +import * as Matter from 'matter-js'; + +export function physics(container: HTMLElement) { + const containerWidth = container.offsetWidth; + const containerHeight = container.offsetHeight; + const containerCenterX = containerWidth / 2; + + // サイズ固定化(要らないかも?) + container.style.position = 'relative'; + container.style.boxSizing = 'border-box'; + container.style.width = `${containerWidth}px`; + container.style.height = `${containerHeight}px`; + + // create engine + const engine = Matter.Engine.create({ + constraintIterations: 4, + positionIterations: 8, + velocityIterations: 8, + }); + + const world = engine.world; + + // create renderer + const render = Matter.Render.create({ + engine: engine, + //element: document.getElementById('debug'), + options: { + width: containerWidth, + height: containerHeight, + background: 'transparent', // transparent to hide + wireframeBackground: 'transparent', // transparent to hide + }, + }); + + // Disable to hide debug + Matter.Render.run(render); + + // create runner + const runner = Matter.Runner.create(); + Matter.Runner.run(runner, engine); + + const groundThickness = 1024; + const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, { + isStatic: true, + restitution: 0.1, + friction: 2, + }); + + //const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts); + //const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts); + + Matter.World.add(world, [ + ground, + //wallRight, + //wallLeft, + ]); + + const objEls = Array.from(container.children) as HTMLElement[]; + const objs: Matter.Body[] = []; + for (const objEl of objEls) { + const left = objEl.dataset.physicsX ? parseInt(objEl.dataset.physicsX) : objEl.offsetLeft; + const top = objEl.dataset.physicsY ? parseInt(objEl.dataset.physicsY) : objEl.offsetTop; + + let obj: Matter.Body; + if (objEl.classList.contains('_physics_circle_')) { + obj = Matter.Bodies.circle( + left + (objEl.offsetWidth / 2), + top + (objEl.offsetHeight / 2), + Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2, + { + restitution: 0.5, + }, + ); + } else { + const style = window.getComputedStyle(objEl); + obj = Matter.Bodies.rectangle( + left + (objEl.offsetWidth / 2), + top + (objEl.offsetHeight / 2), + objEl.offsetWidth, + objEl.offsetHeight, + { + chamfer: { radius: parseInt(style.borderRadius || '0', 10) }, + restitution: 0.5, + }, + ); + } + objEl.id = obj.id.toString(); + objs.push(obj); + } + + Matter.World.add(engine.world, objs); + + // Add mouse control + + const mouse = Matter.Mouse.create(container); + const mouseConstraint = Matter.MouseConstraint.create(engine, { + mouse: mouse, + constraint: { + stiffness: 0.1, + render: { + visible: false, + }, + }, + }); + + Matter.World.add(engine.world, mouseConstraint); + + // keep the mouse in sync with rendering + render.mouse = mouse; + + for (const objEl of objEls) { + objEl.style.position = 'absolute'; + objEl.style.top = '0'; + objEl.style.left = '0'; + objEl.style.margin = '0'; + } + + window.requestAnimationFrame(update); + + let stop = false; + + function update() { + for (const objEl of objEls) { + const obj = objs.find(obj => obj.id.toString() === objEl.id.toString()); + if (obj == null) continue; + + const x = (obj.position.x - objEl.offsetWidth / 2); + const y = (obj.position.y - objEl.offsetHeight / 2); + const angle = obj.angle; + objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; + } + + if (!stop) { + window.requestAnimationFrame(update); + } + } + + // 奈落に落ちたオブジェクトは消す + const intervalId = window.setInterval(() => { + for (const obj of objs) { + if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj); + } + }, 1000 * 10); + + return { + stop: () => { + stop = true; + Matter.Runner.stop(runner); + window.clearInterval(intervalId); + }, + }; +} diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts new file mode 100644 index 0000000000..b8fb853cc1 --- /dev/null +++ b/packages/frontend/src/scripts/please-login.ts @@ -0,0 +1,21 @@ +import { defineAsyncComponent } from 'vue'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; +import { popup } from '@/os'; + +export function pleaseLogin(path?: string) { + if ($i) return; + + popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { + autoSet: true, + message: i18n.ts.signinRequired, + }, { + cancelled: () => { + if (path) { + window.location.href = path; + } + }, + }, 'closed'); + + if (!path) throw new Error('signin required'); +} diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts new file mode 100644 index 0000000000..580031d0a3 --- /dev/null +++ b/packages/frontend/src/scripts/popout.ts @@ -0,0 +1,23 @@ +import * as config from '@/config'; +import { appendQuery } from './url'; + +export function popout(path: string, w?: HTMLElement) { + let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; + url = appendQuery(url, 'zen'); + if (w) { + const position = w.getBoundingClientRect(); + const width = parseInt(getComputedStyle(w, '').width, 10); + const height = parseInt(getComputedStyle(w, '').height, 10); + const x = window.screenX + position.left; + const y = window.screenY + position.top; + window.open(url, url, + `width=${width}, height=${height}, top=${y}, left=${x}`); + } else { + const width = 400; + const height = 500; + const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2); + const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2); + window.open(url, url, + `width=${width}, height=${height}, top=${x}, left=${y}`); + } +} diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts new file mode 100644 index 0000000000..e84eebf103 --- /dev/null +++ b/packages/frontend/src/scripts/popup-position.ts @@ -0,0 +1,158 @@ +import { Ref } from 'vue'; + +export function calcPopupPosition(el: HTMLElement, props: { + anchorElement: HTMLElement | null; + innerMargin: number; + direction: 'top' | 'bottom' | 'left' | 'right'; + align: 'top' | 'bottom' | 'left' | 'right' | 'center'; + alignOffset?: number; + x?: number; + y?: number; +}): { top: number; left: number; transformOrigin: string; } { + const contentWidth = el.offsetWidth; + const contentHeight = el.offsetHeight; + + let rect: DOMRect; + + if (props.anchorElement) { + rect = props.anchorElement.getBoundingClientRect(); + } + + const calcPosWhenTop = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; + } else { + left = props.x; + top = (props.y - contentHeight) - props.innerMargin; + } + + left -= (el.offsetWidth / 2); + + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenBottom = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin; + } else { + left = props.x; + top = (props.y) + props.innerMargin; + } + + left -= (el.offsetWidth / 2); + + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenLeft = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; + top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); + } else { + left = (props.x - contentWidth) - props.innerMargin; + top = props.y; + } + + top -= (el.offsetHeight / 2); + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenRight = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin; + + if (props.align === 'top') { + top = rect.top + window.pageYOffset; + if (props.alignOffset != null) top += props.alignOffset; + } else if (props.align === 'bottom') { + // TODO + } else { // center + top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); + top -= (el.offsetHeight / 2); + } + } else { + left = props.x + props.innerMargin; + top = props.y; + top -= (el.offsetHeight / 2); + } + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + }; + + const calc = (): { + left: number; + top: number; + transformOrigin: string; + } => { + switch (props.direction) { + case 'top': { + const [left, top] = calcPosWhenTop(); + + // ツールチップを上に向かって表示するスペースがなければ下に向かって出す + if (top - window.pageYOffset < 0) { + const [left, top] = calcPosWhenBottom(); + return { left, top, transformOrigin: 'center top' }; + } + + return { left, top, transformOrigin: 'center bottom' }; + } + + case 'bottom': { + const [left, top] = calcPosWhenBottom(); + // TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す + return { left, top, transformOrigin: 'center top' }; + } + + case 'left': { + const [left, top] = calcPosWhenLeft(); + + // ツールチップを左に向かって表示するスペースがなければ右に向かって出す + if (left - window.pageXOffset < 0) { + const [left, top] = calcPosWhenRight(); + return { left, top, transformOrigin: 'left center' }; + } + + return { left, top, transformOrigin: 'right center' }; + } + + case 'right': { + const [left, top] = calcPosWhenRight(); + // TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す + return { left, top, transformOrigin: 'left center' }; + } + } + }; + + return calc(); +} diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts new file mode 100644 index 0000000000..fe32e719da --- /dev/null +++ b/packages/frontend/src/scripts/reaction-picker.ts @@ -0,0 +1,41 @@ +import { defineAsyncComponent, Ref, ref } from 'vue'; +import { popup } from '@/os'; + +class ReactionPicker { + private src: Ref = ref(null); + private manualShowing = ref(false); + private onChosen?: (reaction: string) => void; + private onClosed?: () => void; + + constructor() { + // nop + } + + public async init() { + await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { + src: this.src, + asReactionPicker: true, + manualShowing: this.manualShowing, + }, { + done: reaction => { + this.onChosen!(reaction); + }, + close: () => { + this.manualShowing.value = false; + }, + closed: () => { + this.src.value = null; + this.onClosed!(); + }, + }); + } + + public show(src: HTMLElement, onChosen: ReactionPicker['onChosen'], onClosed: ReactionPicker['onClosed']) { + this.src.value = src; + this.manualShowing.value = true; + this.onChosen = onChosen; + this.onClosed = onClosed; + } +} + +export const reactionPicker = new ReactionPicker(); diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts new file mode 100644 index 0000000000..301b56d7fd --- /dev/null +++ b/packages/frontend/src/scripts/safe-uri-decode.ts @@ -0,0 +1,7 @@ +export function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts new file mode 100644 index 0000000000..f5bc6bf9ce --- /dev/null +++ b/packages/frontend/src/scripts/scroll.ts @@ -0,0 +1,85 @@ +type ScrollBehavior = 'auto' | 'smooth' | 'instant'; + +export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { + if (el == null || el.tagName === 'HTML') return null; + const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y'); + if (overflow === 'scroll' || overflow === 'auto') { + return el; + } else { + return getScrollContainer(el.parentElement); + } +} + +export function getScrollPosition(el: Element | null): number { + const container = getScrollContainer(el); + return container == null ? window.scrollY : container.scrollTop; +} + +export function isTopVisible(el: Element | null): boolean { + const scrollTop = getScrollPosition(el); + const topPosition = el.offsetTop; // TODO: container内でのelの相対位置を取得できればより正確になる + + return scrollTop <= topPosition; +} + +export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) { + if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance; + return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance; +} + +export function onScrollTop(el: Element, cb) { + const container = getScrollContainer(el) || window; + const onScroll = ev => { + if (!document.body.contains(el)) return; + if (isTopVisible(el)) { + cb(); + container.removeEventListener('scroll', onScroll); + } + }; + container.addEventListener('scroll', onScroll, { passive: true }); +} + +export function onScrollBottom(el: Element, cb) { + const container = getScrollContainer(el) || window; + const onScroll = ev => { + if (!document.body.contains(el)) return; + const pos = getScrollPosition(el); + if (pos + el.clientHeight > el.scrollHeight - 1) { + cb(); + container.removeEventListener('scroll', onScroll); + } + }; + container.addEventListener('scroll', onScroll, { passive: true }); +} + +export function scroll(el: Element, options: { + top?: number; + left?: number; + behavior?: ScrollBehavior; +}) { + const container = getScrollContainer(el); + if (container == null) { + window.scroll(options); + } else { + container.scroll(options); + } +} + +export function scrollToTop(el: Element, options: { behavior?: ScrollBehavior; } = {}) { + scroll(el, { top: 0, ...options }); +} + +export function scrollToBottom(el: Element, options: { behavior?: ScrollBehavior; } = {}) { + scroll(el, { top: 99999, ...options }); // TODO: ちゃんと計算する +} + +export function isBottom(el: Element, asobi = 0) { + const container = getScrollContainer(el); + const current = container + ? el.scrollTop + el.offsetHeight + : window.scrollY + window.innerHeight; + const max = container + ? el.scrollHeight + : document.body.offsetHeight; + return current >= (max - asobi); +} diff --git a/packages/frontend/src/scripts/search.ts b/packages/frontend/src/scripts/search.ts new file mode 100644 index 0000000000..64914d3d65 --- /dev/null +++ b/packages/frontend/src/scripts/search.ts @@ -0,0 +1,63 @@ +import * as os from '@/os'; +import { i18n } from '@/i18n'; +import { mainRouter } from '@/router'; + +export async function search() { + const { canceled, result: query } = await os.inputText({ + title: i18n.ts.search, + }); + if (canceled || query == null || query === '') return; + + const q = query.trim(); + + if (q.startsWith('@') && !q.includes(' ')) { + mainRouter.push(`/${q}`); + return; + } + + if (q.startsWith('#')) { + mainRouter.push(`/tags/${encodeURIComponent(q.substr(1))}`); + return; + } + + // like 2018/03/12 + if (/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}/.test(q.replace(/-/g, '/'))) { + const date = new Date(q.replace(/-/g, '/')); + + // 日付しか指定されてない場合、例えば 2018/03/12 ならユーザーは + // 2018/03/12 のコンテンツを「含む」結果になることを期待するはずなので + // 23時間59分進める(そのままだと 2018/03/12 00:00:00 「まで」の + // 結果になってしまい、2018/03/12 のコンテンツは含まれない) + if (q.replace(/-/g, '/').match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{2}$/)) { + date.setHours(23, 59, 59, 999); + } + + // TODO + //v.$root.$emit('warp', date); + os.alert({ + icon: 'fas fa-history', + iconOnly: true, autoClose: true, + }); + return; + } + + if (q.startsWith('https://')) { + const promise = os.api('ap/show', { + uri: q, + }); + + os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); + + const res = await promise; + + if (res.type === 'User') { + mainRouter.push(`/@${res.object.username}@${res.object.host}`); + } else if (res.type === 'Note') { + mainRouter.push(`/notes/${res.object.id}`); + } + + return; + } + + mainRouter.push(`/search?q=${encodeURIComponent(q)}`); +} diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts new file mode 100644 index 0000000000..ec5f8f65e9 --- /dev/null +++ b/packages/frontend/src/scripts/select-file.ts @@ -0,0 +1,103 @@ +import { ref } from 'vue'; +import { DriveFile } from 'misskey-js/built/entities'; +import * as os from '@/os'; +import { stream } from '@/stream'; +import { i18n } from '@/i18n'; +import { defaultStore } from '@/store'; +import { uploadFile } from '@/scripts/upload'; + +function select(src: any, label: string | null, multiple: boolean): Promise { + return new Promise((res, rej) => { + const keepOriginal = ref(defaultStore.state.keepOriginalUploading); + + const chooseFileFromPc = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = multiple; + input.onchange = () => { + const promises = Array.from(input.files).map(file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); + + Promise.all(promises).then(driveFiles => { + res(multiple ? driveFiles : driveFiles[0]); + }).catch(err => { + // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない + }); + + // 一応廃棄 + (window as any).__misskey_input_ref__ = null; + }; + + // https://qiita.com/fukasawah/items/b9dc732d95d99551013d + // iOS Safari で正常に動かす為のおまじない + (window as any).__misskey_input_ref__ = input; + + input.click(); + }; + + const chooseFileFromDrive = () => { + os.selectDriveFile(multiple).then(files => { + res(files); + }); + }; + + const chooseFileFromUrl = () => { + os.inputText({ + title: i18n.ts.uploadFromUrl, + type: 'url', + placeholder: i18n.ts.uploadFromUrlDescription, + }).then(({ canceled, result: url }) => { + if (canceled) return; + + const marker = Math.random().toString(); // TODO: UUIDとか使う + + const connection = stream.useChannel('main'); + connection.on('urlUploadFinished', urlResponse => { + if (urlResponse.marker === marker) { + res(multiple ? [urlResponse.file] : urlResponse.file); + connection.dispose(); + } + }); + + os.api('drive/files/upload-from-url', { + url: url, + folderId: defaultStore.state.uploadFolder, + marker, + }); + + os.alert({ + title: i18n.ts.uploadFromUrlRequested, + text: i18n.ts.uploadFromUrlMayTakeTime, + }); + }); + }; + + os.popupMenu([label ? { + text: label, + type: 'label', + } : undefined, { + type: 'switch', + text: i18n.ts.keepOriginalUploading, + ref: keepOriginal, + }, { + text: i18n.ts.upload, + icon: 'ti ti-upload', + action: chooseFileFromPc, + }, { + text: i18n.ts.fromDrive, + icon: 'ti ti-cloud', + action: chooseFileFromDrive, + }, { + text: i18n.ts.fromUrl, + icon: 'ti ti-link', + action: chooseFileFromUrl, + }], src); + }); +} + +export function selectFile(src: any, label: string | null = null): Promise { + return select(src, label, false) as Promise; +} + +export function selectFiles(src: any, label: string | null = null): Promise { + return select(src, label, true) as Promise; +} diff --git a/packages/frontend/src/scripts/show-suspended-dialog.ts b/packages/frontend/src/scripts/show-suspended-dialog.ts new file mode 100644 index 0000000000..e11569ecd4 --- /dev/null +++ b/packages/frontend/src/scripts/show-suspended-dialog.ts @@ -0,0 +1,10 @@ +import * as os from '@/os'; +import { i18n } from '@/i18n'; + +export function showSuspendedDialog() { + return os.alert({ + type: 'error', + title: i18n.ts.yourAccountSuspendedTitle, + text: i18n.ts.yourAccountSuspendedDescription, + }); +} diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts new file mode 100644 index 0000000000..05e6cdfbcf --- /dev/null +++ b/packages/frontend/src/scripts/shuffle.ts @@ -0,0 +1,19 @@ +/** + * 配列をシャッフル (破壊的) + */ +export function shuffle(array: T): T { + let currentIndex = array.length, randomIndex; + + // While there remain elements to shuffle. + while (currentIndex !== 0) { + // Pick a remaining element. + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], array[currentIndex]]; + } + + return array; +} diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts new file mode 100644 index 0000000000..9d1f603235 --- /dev/null +++ b/packages/frontend/src/scripts/sound.ts @@ -0,0 +1,66 @@ +import { ColdDeviceStorage } from '@/store'; + +const cache = new Map(); + +export const soundsTypes = [ + null, + 'syuilo/up', + 'syuilo/down', + 'syuilo/pope1', + 'syuilo/pope2', + 'syuilo/waon', + 'syuilo/popo', + 'syuilo/triple', + 'syuilo/poi1', + 'syuilo/poi2', + 'syuilo/pirori', + 'syuilo/pirori-wet', + 'syuilo/pirori-square-wet', + 'syuilo/square-pico', + 'syuilo/reverved', + 'syuilo/ryukyu', + 'syuilo/kick', + 'syuilo/snare', + 'syuilo/queue-jammed', + 'aisha/1', + 'aisha/2', + 'aisha/3', + 'noizenecio/kick_gaba1', + 'noizenecio/kick_gaba2', + 'noizenecio/kick_gaba3', + 'noizenecio/kick_gaba4', + 'noizenecio/kick_gaba5', + 'noizenecio/kick_gaba6', + 'noizenecio/kick_gaba7', +] as const; + +export function getAudio(file: string, useCache = true): HTMLAudioElement { + let audio: HTMLAudioElement; + if (useCache && cache.has(file)) { + audio = cache.get(file); + } else { + audio = new Audio(`/client-assets/sounds/${file}.mp3`); + if (useCache) cache.set(file, audio); + } + return audio; +} + +export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement { + const masterVolume = ColdDeviceStorage.get('sound_masterVolume'); + audio.volume = masterVolume - ((1 - volume) * masterVolume); + return audio; +} + +export function play(type: string) { + const sound = ColdDeviceStorage.get('sound_' + type as any); + if (sound.type == null) return; + playFile(sound.type, sound.volume); +} + +export function playFile(file: string, volume: number) { + const masterVolume = ColdDeviceStorage.get('sound_masterVolume'); + if (masterVolume === 0) return; + + const audio = setVolume(getAudio(file), volume); + audio.play(); +} diff --git a/packages/frontend/src/scripts/sticky-sidebar.ts b/packages/frontend/src/scripts/sticky-sidebar.ts new file mode 100644 index 0000000000..c67b8f37ac --- /dev/null +++ b/packages/frontend/src/scripts/sticky-sidebar.ts @@ -0,0 +1,50 @@ +export class StickySidebar { + private lastScrollTop = 0; + private container: HTMLElement; + private el: HTMLElement; + private spacer: HTMLElement; + private marginTop: number; + private isTop = false; + private isBottom = false; + private offsetTop: number; + private globalHeaderHeight: number = 59; + + constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) { + this.container = container; + this.el = this.container.children[0] as HTMLElement; + this.el.style.position = 'sticky'; + this.spacer = document.createElement('div'); + this.container.prepend(this.spacer); + this.marginTop = marginTop; + this.offsetTop = this.container.getBoundingClientRect().top; + this.globalHeaderHeight = globalHeaderHeight; + } + + public calc(scrollTop: number) { + if (scrollTop > this.lastScrollTop) { // downscroll + const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight); + this.el.style.bottom = null; + this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`; + + this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight); + + if (this.isTop) { + this.isTop = false; + this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`; + } + } else { // upscroll + const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight; + this.el.style.top = null; + this.el.style.bottom = `${-overflow}px`; + + this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop; + + if (this.isBottom) { + this.isBottom = false; + this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`; + } + } + + this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop; + } +} diff --git a/packages/frontend/src/scripts/theme-editor.ts b/packages/frontend/src/scripts/theme-editor.ts new file mode 100644 index 0000000000..944875ff15 --- /dev/null +++ b/packages/frontend/src/scripts/theme-editor.ts @@ -0,0 +1,81 @@ +import { v4 as uuid } from 'uuid'; + +import { themeProps, Theme } from './theme'; + +export type Default = null; +export type Color = string; +export type FuncName = 'alpha' | 'darken' | 'lighten'; +export type Func = { type: 'func'; name: FuncName; arg: number; value: string; }; +export type RefProp = { type: 'refProp'; key: string; }; +export type RefConst = { type: 'refConst'; key: string; }; +export type Css = { type: 'css'; value: string; }; + +export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default; + +export type ThemeViewModel = [ string, ThemeValue ][]; + +export const fromThemeString = (str?: string) : ThemeValue => { + if (!str) return null; + if (str.startsWith(':')) { + const parts = str.slice(1).split('<'); + const name = parts[0] as FuncName; + const arg = parseFloat(parts[1]); + const value = parts[2].startsWith('@') ? parts[2].slice(1) : ''; + return { type: 'func', name, arg, value }; + } else if (str.startsWith('@')) { + return { + type: 'refProp', + key: str.slice(1), + }; + } else if (str.startsWith('$')) { + return { + type: 'refConst', + key: str.slice(1), + }; + } else if (str.startsWith('"')) { + return { + type: 'css', + value: str.substr(1).trim(), + }; + } else { + return str; + } +}; + +export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => { + if (typeof value === 'string') return value; + switch (value.type) { + case 'func': return `:${value.name}<${value.arg}<@${value.value}`; + case 'refProp': return `@${value.key}`; + case 'refConst': return `$${value.key}`; + case 'css': return `" ${value.value}`; + } +}; + +export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => { + const props = { } as { [key: string]: string }; + for (const [key, value] of vm) { + if (value === null) continue; + props[key] = toThemeString(value); + } + + return { + id: uuid(), + name, desc, author, props, base, + }; +}; + +export const convertToViewModel = (theme: Theme): ThemeViewModel => { + const vm: ThemeViewModel = []; + // プロパティの登録 + vm.push(...themeProps.map(key => [key, fromThemeString(theme.props[key])] as [ string, ThemeValue ])); + + // 定数の登録 + const consts = Object + .keys(theme.props) + .filter(k => k.startsWith('$')) + .map(k => [k, fromThemeString(theme.props[k])] as [ string, ThemeValue ]); + + vm.push(...consts); + return vm; +}; diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts new file mode 100644 index 0000000000..62a2b9459a --- /dev/null +++ b/packages/frontend/src/scripts/theme.ts @@ -0,0 +1,148 @@ +import { ref } from 'vue'; +import tinycolor from 'tinycolor2'; +import { globalEvents } from '@/events'; + +export type Theme = { + id: string; + name: string; + author: string; + desc?: string; + base?: 'dark' | 'light'; + props: Record; +}; + +import lightTheme from '@/themes/_light.json5'; +import darkTheme from '@/themes/_dark.json5'; +import { deepClone } from './clone'; + +export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); + +export const getBuiltinThemes = () => Promise.all( + [ + 'l-light', + 'l-coffee', + 'l-apricot', + 'l-rainy', + 'l-vivid', + 'l-cherry', + 'l-sushi', + 'l-u0', + + 'd-dark', + 'd-persimmon', + 'd-astro', + 'd-future', + 'd-botanical', + 'd-green-lime', + 'd-green-orange', + 'd-cherry', + 'd-ice', + 'd-u0', + ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)), +); + +export const getBuiltinThemesRef = () => { + const builtinThemes = ref([]); + getBuiltinThemes().then(themes => builtinThemes.value = themes); + return builtinThemes; +}; + +let timeout = null; + +export function applyTheme(theme: Theme, persist = true) { + if (timeout) window.clearTimeout(timeout); + + document.documentElement.classList.add('_themeChanging_'); + + timeout = window.setTimeout(() => { + document.documentElement.classList.remove('_themeChanging_'); + }, 1000); + + const colorSchema = theme.base === 'dark' ? 'dark' : 'light'; + + // Deep copy + const _theme = deepClone(theme); + + if (_theme.base) { + const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); + if (base) _theme.props = Object.assign({}, base.props, _theme.props); + } + + const props = compile(_theme); + + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', props['htmlThemeColor']); + break; + } + } + + for (const [k, v] of Object.entries(props)) { + document.documentElement.style.setProperty(`--${k}`, v.toString()); + } + + document.documentElement.style.setProperty('color-schema', colorSchema); + + if (persist) { + localStorage.setItem('theme', JSON.stringify(props)); + localStorage.setItem('colorSchema', colorSchema); + } + + // 色計算など再度行えるようにクライアント全体に通知 + globalEvents.emit('themeChanged'); +} + +function compile(theme: Theme): Record { + function getColor(val: string): tinycolor.Instance { + // ref (prop) + if (val[0] === '@') { + return getColor(theme.props[val.substr(1)]); + } + + // ref (const) + else if (val[0] === '$') { + return getColor(theme.props[val]); + } + + // func + else if (val[0] === ':') { + const parts = val.split('<'); + const func = parts.shift().substr(1); + const arg = parseFloat(parts.shift()); + const color = getColor(parts.join('<')); + + switch (func) { + case 'darken': return color.darken(arg); + case 'lighten': return color.lighten(arg); + case 'alpha': return color.setAlpha(arg); + case 'hue': return color.spin(arg); + case 'saturate': return color.saturate(arg); + } + } + + // other case + return tinycolor(val); + } + + const props = {}; + + for (const [k, v] of Object.entries(theme.props)) { + if (k.startsWith('$')) continue; // ignore const + + props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); + } + + return props; +} + +function genValue(c: tinycolor.Instance): string { + return c.toRgbString(); +} + +export function validateTheme(theme: Record): boolean { + if (theme.id == null || typeof theme.id !== 'string') return false; + if (theme.name == null || typeof theme.name !== 'string') return false; + if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false; + if (theme.props == null || typeof theme.props !== 'object') return false; + return true; +} diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/scripts/time.ts new file mode 100644 index 0000000000..34e8b6b17c --- /dev/null +++ b/packages/frontend/src/scripts/time.ts @@ -0,0 +1,39 @@ +const dateTimeIntervals = { + 'day': 86400000, + 'hour': 3600000, + 'ms': 1, +}; + +export function dateUTC(time: number[]): Date { + const d = time.length === 2 ? Date.UTC(time[0], time[1]) + : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) + : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) + : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) + : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) + : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) + : null; + + if (!d) throw 'wrong number of arguments'; + + return new Date(d); +} + +export function isTimeSame(a: Date, b: Date): boolean { + return a.getTime() === b.getTime(); +} + +export function isTimeBefore(a: Date, b: Date): boolean { + return (a.getTime() - b.getTime()) < 0; +} + +export function isTimeAfter(a: Date, b: Date): boolean { + return (a.getTime() - b.getTime()) > 0; +} + +export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { + return new Date(x.getTime() + (value * dateTimeIntervals[span])); +} + +export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { + return new Date(x.getTime() - (value * dateTimeIntervals[span])); +} diff --git a/packages/frontend/src/scripts/timezones.ts b/packages/frontend/src/scripts/timezones.ts new file mode 100644 index 0000000000..8ce07323f6 --- /dev/null +++ b/packages/frontend/src/scripts/timezones.ts @@ -0,0 +1,49 @@ +export const timezones = [{ + name: 'UTC', + abbrev: 'UTC', + offset: 0, +}, { + name: 'Europe/Berlin', + abbrev: 'CET', + offset: 60, +}, { + name: 'Asia/Tokyo', + abbrev: 'JST', + offset: 540, +}, { + name: 'Asia/Seoul', + abbrev: 'KST', + offset: 540, +}, { + name: 'Asia/Shanghai', + abbrev: 'CST', + offset: 480, +}, { + name: 'Australia/Sydney', + abbrev: 'AEST', + offset: 600, +}, { + name: 'Australia/Darwin', + abbrev: 'ACST', + offset: 570, +}, { + name: 'Australia/Perth', + abbrev: 'AWST', + offset: 480, +}, { + name: 'America/New_York', + abbrev: 'EST', + offset: -300, +}, { + name: 'America/Mexico_City', + abbrev: 'CST', + offset: -360, +}, { + name: 'America/Phoenix', + abbrev: 'MST', + offset: -420, +}, { + name: 'America/Los_Angeles', + abbrev: 'PST', + offset: -480, +}]; diff --git a/packages/frontend/src/scripts/touch.ts b/packages/frontend/src/scripts/touch.ts new file mode 100644 index 0000000000..5251bc2e27 --- /dev/null +++ b/packages/frontend/src/scripts/touch.ts @@ -0,0 +1,23 @@ +const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; + +export let isTouchUsing = false; + +export let isScreenTouching = false; + +if (isTouchSupported) { + window.addEventListener('touchstart', () => { + // maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも + // タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする + isTouchUsing = true; + + isScreenTouching = true; + }, { passive: true }); + + window.addEventListener('touchend', () => { + // 子要素のtouchstartイベントでstopPropagation()が呼ばれると親要素に伝搬されずタッチされたと判定されないため、 + // touchendイベントでもtouchstartイベントと同様にtrueにする + isTouchUsing = true; + + isScreenTouching = false; + }, { passive: true }); +} diff --git a/packages/frontend/src/scripts/unison-reload.ts b/packages/frontend/src/scripts/unison-reload.ts new file mode 100644 index 0000000000..59af584c1b --- /dev/null +++ b/packages/frontend/src/scripts/unison-reload.ts @@ -0,0 +1,15 @@ +// SafariがBroadcastChannel未実装なのでライブラリを使う +import { BroadcastChannel } from 'broadcast-channel'; + +export const reloadChannel = new BroadcastChannel('reload'); + +// BroadcastChannelを用いて、クライアントが一斉にreloadするようにします。 +export function unisonReload(path?: string) { + if (path !== undefined) { + reloadChannel.postMessage(path); + location.href = path; + } else { + reloadChannel.postMessage(null); + location.reload(); + } +} diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts new file mode 100644 index 0000000000..9a39652ef5 --- /dev/null +++ b/packages/frontend/src/scripts/upload.ts @@ -0,0 +1,137 @@ +import { reactive, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import { readAndCompressImage } from 'browser-image-resizer'; +import { getCompressionConfig } from './upload/compress-config'; +import { defaultStore } from '@/store'; +import { apiUrl } from '@/config'; +import { $i } from '@/account'; +import { alert } from '@/os'; +import { i18n } from '@/i18n'; + +type Uploading = { + id: string; + name: string; + progressMax: number | undefined; + progressValue: number | undefined; + img: string; +}; +export const uploads = ref([]); + +const mimeTypeMap = { + 'image/webp': 'webp', + 'image/jpeg': 'jpg', + 'image/png': 'png', +} as const; + +export function uploadFile( + file: File, + folder?: any, + name?: string, + keepOriginal: boolean = defaultStore.state.keepOriginalUploading, +): Promise { + if ($i == null) throw new Error('Not logged in'); + + if (folder && typeof folder === 'object') folder = folder.id; + + return new Promise((resolve, reject) => { + const id = Math.random().toString(); + + const reader = new FileReader(); + reader.onload = async (): Promise => { + const ctx = reactive({ + id: id, + name: name ?? file.name ?? 'untitled', + progressMax: undefined, + progressValue: undefined, + img: window.URL.createObjectURL(file), + }); + + uploads.value.push(ctx); + + const config = !keepOriginal ? await getCompressionConfig(file) : undefined; + let resizedImage: Blob | undefined; + if (config) { + try { + const resized = await readAndCompressImage(file, config); + if (resized.size < file.size || file.type === 'image/webp') { + // The compression may not always reduce the file size + // (and WebP is not browser safe yet) + resizedImage = resized; + } + if (_DEV_) { + const saved = ((1 - resized.size / file.size) * 100).toFixed(2); + console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`); + } + + ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name; + } catch (err) { + console.error('Failed to resize image', err); + } + } + + const formData = new FormData(); + formData.append('i', $i.token); + formData.append('force', 'true'); + formData.append('file', resizedImage ?? file); + formData.append('name', ctx.name); + if (folder) formData.append('folderId', folder); + + const xhr = new XMLHttpRequest(); + xhr.open('POST', apiUrl + '/drive/files/create', true); + xhr.onload = ((ev: ProgressEvent) => { + if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { + // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい + uploads.value = uploads.value.filter(x => x.id !== id); + + if (ev.target?.response) { + const res = JSON.parse(ev.target.response); + if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseInappropriate, + }); + } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseNoFreeSpace, + }); + } else { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, + }); + } + } else { + alert({ + type: 'error', + title: 'Failed to upload', + text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, + }); + } + + reject(); + return; + } + + const driveFile = JSON.parse(ev.target.response); + + resolve(driveFile); + + uploads.value = uploads.value.filter(x => x.id !== id); + }) as (ev: ProgressEvent) => any; + + xhr.upload.onprogress = ev => { + if (ev.lengthComputable) { + ctx.progressMax = ev.total; + ctx.progressValue = ev.loaded; + } + }; + + xhr.send(formData); + }; + reader.readAsArrayBuffer(file); + }); +} diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts new file mode 100644 index 0000000000..793c78ad20 --- /dev/null +++ b/packages/frontend/src/scripts/upload/compress-config.ts @@ -0,0 +1,23 @@ +import isAnimated from 'is-file-animated'; +import type { BrowserImageResizerConfig } from 'browser-image-resizer'; + +const compressTypeMap = { + 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, + 'image/png': { quality: 1, mimeType: 'image/png' }, + 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, + 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, +} as const; + +export async function getCompressionConfig(file: File): Promise { + const imgConfig = compressTypeMap[file.type]; + if (!imgConfig || await isAnimated(file)) { + return; + } + + return { + maxWidth: 2048, + maxHeight: 2048, + debug: true, + ...imgConfig, + }; +} diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts new file mode 100644 index 0000000000..86735de9f0 --- /dev/null +++ b/packages/frontend/src/scripts/url.ts @@ -0,0 +1,13 @@ +export function query(obj: Record): string { + const params = Object.entries(obj) + .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) + .reduce((a, [k, v]) => (a[k] = v, a), {} as Record); + + return Object.entries(params) + .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) + .join('&'); +} + +export function appendQuery(url: string, query: string): string { + return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; +} diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts new file mode 100644 index 0000000000..881e5e9ad5 --- /dev/null +++ b/packages/frontend/src/scripts/use-chart-tooltip.ts @@ -0,0 +1,54 @@ +import { onUnmounted, ref } from 'vue'; +import * as os from '@/os'; +import MkChartTooltip from '@/components/MkChartTooltip.vue'; + +export function useChartTooltip(opts: { position: 'top' | 'middle' } = { position: 'top' }) { + const tooltipShowing = ref(false); + const tooltipX = ref(0); + const tooltipY = ref(0); + const tooltipTitle = ref(null); + const tooltipSeries = ref(null); + let disposeTooltipComponent; + + os.popup(MkChartTooltip, { + showing: tooltipShowing, + x: tooltipX, + y: tooltipY, + title: tooltipTitle, + series: tooltipSeries, + }, {}).then(({ dispose }) => { + disposeTooltipComponent = dispose; + }); + + onUnmounted(() => { + if (disposeTooltipComponent) disposeTooltipComponent(); + }); + + function handler(context) { + if (context.tooltip.opacity === 0) { + tooltipShowing.value = false; + return; + } + + tooltipTitle.value = context.tooltip.title[0]; + tooltipSeries.value = context.tooltip.body.map((b, i) => ({ + backgroundColor: context.tooltip.labelColors[i].backgroundColor, + borderColor: context.tooltip.labelColors[i].borderColor, + text: b.lines[0], + })); + + const rect = context.chart.canvas.getBoundingClientRect(); + + tooltipShowing.value = true; + tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; + if (opts.position === 'top') { + tooltipY.value = rect.top + window.pageYOffset; + } else if (opts.position === 'middle') { + tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; + } + } + + return { + handler, + }; +} diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts new file mode 100644 index 0000000000..201ba417ef --- /dev/null +++ b/packages/frontend/src/scripts/use-interval.ts @@ -0,0 +1,24 @@ +import { onMounted, onUnmounted } from 'vue'; + +export function useInterval(fn: () => void, interval: number, options: { + immediate: boolean; + afterMounted: boolean; +}): void { + if (Number.isNaN(interval)) return; + + let intervalId: number | null = null; + + if (options.afterMounted) { + onMounted(() => { + if (options.immediate) fn(); + intervalId = window.setInterval(fn, interval); + }); + } else { + if (options.immediate) fn(); + intervalId = window.setInterval(fn, interval); + } + + onUnmounted(() => { + if (intervalId) window.clearInterval(intervalId); + }); +} diff --git a/packages/frontend/src/scripts/use-leave-guard.ts b/packages/frontend/src/scripts/use-leave-guard.ts new file mode 100644 index 0000000000..a93b84d1fe --- /dev/null +++ b/packages/frontend/src/scripts/use-leave-guard.ts @@ -0,0 +1,47 @@ +import { inject, onUnmounted, Ref } from 'vue'; +import { i18n } from '@/i18n'; +import * as os from '@/os'; + +export function useLeaveGuard(enabled: Ref) { + /* TODO + const setLeaveGuard = inject('setLeaveGuard'); + + if (setLeaveGuard) { + setLeaveGuard(async () => { + if (!enabled.value) return false; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.leaveConfirm, + }); + + return canceled; + }); + } else { + onBeforeRouteLeave(async (to, from) => { + if (!enabled.value) return true; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts.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/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts new file mode 100644 index 0000000000..e6bdb345c4 --- /dev/null +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -0,0 +1,110 @@ +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; + note: Ref; + isDeletedRef: Ref; +}) { + const note = props.note; + const connection = $i ? stream : null; + + function onStreamNoteUpdated(noteData): void { + const { type, id, body } = noteData; + + if (id !== note.value.id) return; + + switch (type) { + case 'reacted': { + const reaction = body.reaction; + + if (body.emoji) { + const emojis = note.value.emojis || []; + if (!emojis.includes(body.emoji)) { + note.value.emojis = [...emojis, body.emoji]; + } + } + + // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる + const currentCount = (note.value.reactions || {})[reaction] || 0; + + note.value.reactions[reaction] = currentCount + 1; + + if ($i && (body.userId === $i.id)) { + note.value.myReaction = reaction; + } + break; + } + + case 'unreacted': { + const reaction = body.reaction; + + // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる + const currentCount = (note.value.reactions || {})[reaction] || 0; + + note.value.reactions[reaction] = Math.max(0, currentCount - 1); + + if ($i && (body.userId === $i.id)) { + note.value.myReaction = null; + } + break; + } + + case 'pollVoted': { + const choice = body.choice; + + const choices = [...note.value.poll.choices]; + choices[choice] = { + ...choices[choice], + votes: choices[choice].votes + 1, + ...($i && (body.userId === $i.id) ? { + isVoted: true, + } : {}), + }; + + note.value.poll.choices = choices; + break; + } + + case 'deleted': { + props.isDeletedRef.value = true; + break; + } + } + } + + function capture(withHandler = false): void { + if (connection) { + // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する + connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id }); + if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); + } + } + + function decapture(withHandler = false): void { + if (connection) { + connection.send('un', { + id: note.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/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/scripts/use-tooltip.ts new file mode 100644 index 0000000000..1f6e0fb6ce --- /dev/null +++ b/packages/frontend/src/scripts/use-tooltip.ts @@ -0,0 +1,86 @@ +import { Ref, ref, watch, onUnmounted } from 'vue'; + +export function useTooltip( + elRef: Ref, + onShow: (showing: Ref) => void, + delay = 300, +): void { + let isHovering = false; + + // iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ + // 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる + // TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...? + let shouldIgnoreMouseover = false; + + let timeoutId: number; + + let changeShowingState: (() => void) | null; + + const open = () => { + close(); + if (!isHovering) return; + if (elRef.value == null) return; + const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; + if (!document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため + + const showing = ref(true); + onShow(showing); + changeShowingState = () => { + showing.value = false; + }; + }; + + const close = () => { + if (changeShowingState != null) { + changeShowingState(); + changeShowingState = null; + } + }; + + const onMouseover = () => { + if (isHovering) return; + if (shouldIgnoreMouseover) return; + isHovering = true; + timeoutId = window.setTimeout(open, delay); + }; + + const onMouseleave = () => { + if (!isHovering) return; + isHovering = false; + window.clearTimeout(timeoutId); + close(); + }; + + const onTouchstart = () => { + shouldIgnoreMouseover = true; + if (isHovering) return; + isHovering = true; + timeoutId = window.setTimeout(open, delay); + }; + + const onTouchend = () => { + if (!isHovering) return; + isHovering = false; + window.clearTimeout(timeoutId); + close(); + }; + + const stop = watch(elRef, () => { + if (elRef.value) { + stop(); + const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; + el.addEventListener('mouseover', onMouseover, { passive: true }); + el.addEventListener('mouseleave', onMouseleave, { passive: true }); + el.addEventListener('touchstart', onTouchstart, { passive: true }); + el.addEventListener('touchend', onTouchend, { passive: true }); + el.addEventListener('click', close, { passive: true }); + } + }, { + immediate: true, + flush: 'post', + }); + + onUnmounted(() => { + close(); + }); +} diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts new file mode 100644 index 0000000000..1bedab5fad --- /dev/null +++ b/packages/frontend/src/store.ts @@ -0,0 +1,383 @@ +import { markRaw, ref } from 'vue'; +import { Storage } from './pizzax'; +import { Theme } from './scripts/theme'; + +interface PostFormAction { + title: string, + handler: (form: T, update: (key: unknown, value: unknown) => void) => void; +} + +interface UserAction { + title: string, + handler: (user: UserDetailed) => void; +} + +interface NoteAction { + title: string, + handler: (note: Note) => void; +} + +interface NoteViewInterruptor { + handler: (note: Note) => unknown; +} + +interface NotePostInterruptor { + handler: (note: FIXME) => unknown; +} + +export const postFormActions: PostFormAction[] = []; +export const userActions: UserAction[] = []; +export const noteActions: NoteAction[] = []; +export const noteViewInterruptors: NoteViewInterruptor[] = []; +export const notePostInterruptors: NotePostInterruptor[] = []; + +// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう) +// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない +export const defaultStore = markRaw(new Storage('base', { + tutorial: { + where: 'account', + default: 0, + }, + keepCw: { + where: 'account', + default: true, + }, + showFullAcct: { + where: 'account', + default: false, + }, + rememberNoteVisibility: { + where: 'account', + default: false, + }, + defaultNoteVisibility: { + where: 'account', + default: 'public', + }, + defaultNoteLocalOnly: { + where: 'account', + default: false, + }, + uploadFolder: { + where: 'account', + default: null as string | null, + }, + pastedFileName: { + where: 'account', + default: 'yyyy-MM-dd HH-mm-ss [{{number}}]', + }, + keepOriginalUploading: { + where: 'account', + default: false, + }, + memo: { + where: 'account', + default: null, + }, + reactions: { + where: 'account', + default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'], + }, + mutedWords: { + where: 'account', + default: [], + }, + mutedAds: { + where: 'account', + default: [] as string[], + }, + + menu: { + where: 'deviceAccount', + default: [ + 'notifications', + 'favorites', + 'drive', + 'followRequests', + '-', + 'explore', + 'announcements', + 'search', + '-', + 'ui', + ], + }, + visibility: { + where: 'deviceAccount', + default: 'public' as 'public' | 'home' | 'followers' | 'specified', + }, + localOnly: { + where: 'deviceAccount', + default: false, + }, + statusbars: { + where: 'deviceAccount', + default: [] as { + name: string; + id: string; + type: string; + size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge'; + black: boolean; + props: Record; + }[], + }, + widgets: { + where: 'deviceAccount', + default: [] as { + name: string; + id: string; + place: string | null; + data: Record; + }[], + }, + tl: { + where: 'deviceAccount', + default: { + src: 'home' as 'home' | 'local' | 'social' | 'global', + arg: null, + }, + }, + + overridedDeviceKind: { + where: 'device', + default: null as null | 'smartphone' | 'tablet' | 'desktop', + }, + serverDisconnectedBehavior: { + where: 'device', + default: 'quiet' as 'quiet' | 'reload' | 'dialog', + }, + nsfw: { + where: 'device', + default: 'respect' as 'respect' | 'force' | 'ignore', + }, + animation: { + where: 'device', + default: true, + }, + animatedMfm: { + where: 'device', + default: false, + }, + loadRawImages: { + where: 'device', + default: false, + }, + imageNewTab: { + where: 'device', + default: false, + }, + disableShowingAnimatedImages: { + where: 'device', + default: false, + }, + disablePagesScript: { + where: 'device', + default: false, + }, + emojiStyle: { + where: 'device', + default: 'twemoji', // twemoji / fluentEmoji / native + }, + disableDrawer: { + where: 'device', + default: false, + }, + useBlurEffectForModal: { + where: 'device', + default: true, + }, + useBlurEffect: { + where: 'device', + default: true, + }, + showFixedPostForm: { + where: 'device', + default: false, + }, + enableInfiniteScroll: { + where: 'device', + default: true, + }, + useReactionPickerForContextMenu: { + where: 'device', + default: false, + }, + showGapBetweenNotesInTimeline: { + where: 'device', + default: false, + }, + darkMode: { + where: 'device', + default: false, + }, + instanceTicker: { + where: 'device', + default: 'remote' as 'none' | 'remote' | 'always', + }, + reactionPickerSize: { + where: 'device', + default: 1, + }, + reactionPickerWidth: { + where: 'device', + default: 1, + }, + reactionPickerHeight: { + where: 'device', + default: 2, + }, + reactionPickerUseDrawerForMobile: { + where: 'device', + default: true, + }, + recentlyUsedEmojis: { + where: 'device', + default: [] as string[], + }, + recentlyUsedUsers: { + where: 'device', + default: [] as string[], + }, + defaultSideView: { + where: 'device', + default: false, + }, + menuDisplay: { + where: 'device', + default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top', + }, + reportError: { + where: 'device', + default: false, + }, + squareAvatars: { + where: 'device', + default: false, + }, + postFormWithHashtags: { + where: 'device', + default: false, + }, + postFormHashtags: { + where: 'device', + default: '', + }, + themeInitial: { + where: 'device', + default: true, + }, + numberOfPageCache: { + where: 'device', + default: 5, + }, + aiChanMode: { + where: 'device', + default: false, + }, +})); + +// TODO: 他のタブと永続化されたstateを同期 + +const PREFIX = 'miux:'; + +type Plugin = { + id: string; + name: string; + active: boolean; + configData: Record; + token: string; + ast: any[]; +}; + +interface Watcher { + key: string; + callback: (value: unknown) => void; +} + +/** + * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) + */ +import lightTheme from '@/themes/l-light.json5'; +import darkTheme from '@/themes/d-green-lime.json5'; +import { Note, UserDetailed } from 'misskey-js/built/entities'; + +export class ColdDeviceStorage { + public static default = { + lightTheme, + darkTheme, + syncDeviceDarkMode: true, + plugins: [] as Plugin[], + mediaVolume: 0.5, + sound_masterVolume: 0.3, + sound_note: { type: 'syuilo/down', volume: 1 }, + sound_noteMy: { type: 'syuilo/up', volume: 1 }, + sound_notification: { type: 'syuilo/pope2', volume: 1 }, + sound_chat: { type: 'syuilo/pope1', volume: 1 }, + sound_chatBg: { type: 'syuilo/waon', volume: 1 }, + sound_antenna: { type: 'syuilo/triple', volume: 1 }, + sound_channel: { type: 'syuilo/square-pico', volume: 1 }, + }; + + public static watchers: Watcher[] = []; + + public static get(key: T): typeof ColdDeviceStorage.default[T] { + // TODO: indexedDBにする + // ただしその際はnullチェックではなくキー存在チェックにしないとダメ + // (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある) + const value = localStorage.getItem(PREFIX + key); + if (value == null) { + return ColdDeviceStorage.default[key]; + } else { + return JSON.parse(value); + } + } + + public static set(key: T, value: typeof ColdDeviceStorage.default[T]): void { + // 呼び出し側のバグ等で undefined が来ることがある + // undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (value === undefined) { + console.error(`attempt to store undefined value for key '${key}'`); + return; + } + + localStorage.setItem(PREFIX + key, JSON.stringify(value)); + + for (const watcher of this.watchers) { + if (watcher.key === key) watcher.callback(value); + } + } + + public static watch(key, callback) { + this.watchers.push({ key, callback }); + } + + // TODO: VueのcustomRef使うと良い感じになるかも + public static ref(key: T) { + const v = ColdDeviceStorage.get(key); + const r = ref(v); + // TODO: このままではwatcherがリークするので開放する方法を考える + this.watch(key, v => { + r.value = v; + }); + return r; + } + + /** + * 特定のキーの、簡易的なgetter/setterを作ります + * 主にvue場で設定コントロールのmodelとして使う用 + */ + public static makeGetterSetter(key: K) { + // TODO: VueのcustomRef使うと良い感じになるかも + const valueRef = ColdDeviceStorage.ref(key); + return { + get: () => { + return valueRef.value; + }, + set: (value: unknown) => { + const val = value; + ColdDeviceStorage.set(key, val); + }, + }; + } +} diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts new file mode 100644 index 0000000000..dea3459b86 --- /dev/null +++ b/packages/frontend/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/frontend/src/style.scss b/packages/frontend/src/style.scss new file mode 100644 index 0000000000..8b7a846863 --- /dev/null +++ b/packages/frontend/src/style.scss @@ -0,0 +1,584 @@ +@charset "utf-8"; + +:root { + --radius: 12px; + --marginFull: 16px; + --marginHalf: 10px; + + --margin: var(--marginFull); + + @media (max-width: 500px) { + --margin: var(--marginHalf); + } + + //--ad: rgb(255 169 0 / 10%); +} + +::selection { + color: #fff; + background-color: var(--accent); +} + +html { + touch-action: manipulation; + background-color: var(--bg); + background-attachment: fixed; + background-size: cover; + background-position: center; + color: var(--fg); + accent-color: var(--accent); + overflow: auto; + overflow-wrap: break-word; + font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; + font-size: 14px; + line-height: 1.35; + text-size-adjust: 100%; + tab-size: 2; + + &, * { + scrollbar-color: var(--scrollbarHandle) inherit; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-track { + background: inherit; + } + + &::-webkit-scrollbar-thumb { + background: var(--scrollbarHandle); + + &:hover { + background: var(--scrollbarHandleHover); + } + + &:active { + background: var(--accent); + } + } + } + + &.f-1 { + font-size: 15px; + } + + &.f-2 { + font-size: 16px; + } + + &.f-3 { + font-size: 17px; + } + + &.useSystemFont { + font-family: 'Hiragino Maru Gothic Pro', sans-serif; + } +} + +html._themeChanging_ { + &, * { + transition: background 1s ease, border 1s ease !important; + } +} + +html, body { + margin: 0; + padding: 0; + scroll-behavior: smooth; +} + +a { + text-decoration: none; + cursor: pointer; + color: inherit; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; + + &:hover { + text-decoration: underline; + } +} + +textarea, input { + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; +} + +optgroup, option { + background: var(--panel); + color: var(--fg); +} + +hr { + margin: var(--margin) 0 var(--margin) 0; + border: none; + height: 1px; + background: var(--divider); +} + +.ti { + vertical-align: -10%; + line-height: 0.9em; + + &:before { + font-size: 130%; + } +} + +.ti-fw { + display: inline-block; + text-align: center; +} + +._indicatorCircle { + display: inline-block; + width: 1em; + height: 1em; + border-radius: 100%; + background: currentColor; +} + +._noSelect { + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; +} + +._ghost { + &, * { + @extend ._noSelect; + pointer-events: none; + } +} + +._modalBg { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--modalBg); + -webkit-backdrop-filter: var(--modalBgFilter); + backdrop-filter: var(--modalBgFilter); +} + +._shadow { + box-shadow: 0px 4px 32px var(--shadow) !important; +} + +._button { + appearance: none; + display: inline-block; + padding: 0; + margin: 0; // for Safari + background: none; + border: none; + cursor: pointer; + color: inherit; + touch-action: manipulation; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; + font-size: 1em; + font-family: inherit; + line-height: inherit; + max-width: 100%; + + &, * { + @extend ._noSelect; + } + + * { + pointer-events: none; + } + + &:focus-visible { + outline: none; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } +} + +._buttonPrimary { + @extend ._button; + color: var(--fgOnAccent); + background: var(--accent); + + &:not(:disabled):hover { + background: var(--X8); + } + + &:not(:disabled):active { + background: var(--X9); + } +} + +._buttonGradate { + @extend ._buttonPrimary; + color: var(--fgOnAccent); + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + + &:not(:disabled):hover { + background: linear-gradient(90deg, var(--X8), var(--X8)); + } + + &:not(:disabled):active { + background: linear-gradient(90deg, var(--X8), var(--X8)); + } +} + +._help { + color: var(--accent); + cursor: help +} + +._textButton { + @extend ._button; + color: var(--accent); + + &:not(:disabled):hover { + text-decoration: underline; + } +} + +._inputs { + display: flex; + margin: 32px 0; + + &:first-child { + margin-top: 8px; + } + + &:last-child { + margin-bottom: 8px; + } + + > * { + flex: 1; + margin: 0 !important; + + &:not(:first-child) { + margin-left: 8px !important; + } + + &:not(:last-child) { + margin-right: 8px !important; + } + } +} + +._panel { + background: var(--panel); + border-radius: var(--radius); + overflow: clip; +} + +._block { + @extend ._panel; + + & + ._block { + margin-top: var(--margin); + } +} + +._gap { + margin: var(--margin) 0; +} + +// TODO: 廃止 +._card { + @extend ._panel; + + // TODO: _cardTitle に + > ._title { + margin: 0; + padding: 22px 32px; + font-size: 1em; + border-bottom: solid 1px var(--panelHeaderDivider); + font-weight: bold; + background: var(--panelHeaderBg); + color: var(--panelHeaderFg); + + @media (max-width: 500px) { + padding: 16px; + font-size: 1em; + } + } + + // TODO: _cardContent に + > ._content { + padding: 32px; + + @media (max-width: 500px) { + padding: 16px; + } + + &._noPad { + padding: 0 !important; + } + + & + ._content { + border-top: solid 0.5px var(--divider); + } + } + + // TODO: _cardFooter に + > ._footer { + border-top: solid 0.5px var(--divider); + padding: 24px 32px; + + @media (max-width: 500px) { + padding: 16px; + } + } +} + +._borderButton { + @extend ._button; + display: block; + width: 100%; + padding: 10px; + box-sizing: border-box; + text-align: center; + border: solid 0.5px var(--divider); + border-radius: var(--radius); + + &:active { + border-color: var(--accent); + } +} + +._popup { + background: var(--popup); + border-radius: var(--radius); + contain: content; +} + +// TODO: 廃止 +._monolithic_ { + ._section:not(:empty) { + box-sizing: border-box; + padding: var(--root-margin, 32px); + + @media (max-width: 500px) { + --root-margin: 10px; + } + + & + ._section:not(:empty) { + border-top: solid 0.5px var(--divider); + } + } +} + +._narrow_ ._card { + > ._title { + padding: 16px; + font-size: 1em; + } + + > ._content { + padding: 16px; + } + + > ._footer { + padding: 16px; + } +} + +._acrylic { + background: var(--acrylicPanel); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} + +._formBlock { + margin: 1.5em 0; +} + +._formRoot { + > ._formBlock:first-child { + margin-top: 0; + } + + > ._formBlock:last-child { + margin-bottom: 0; + } +} + +._formLinksGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-gap: 12px; +} + +._formLinks { + > *:not(:last-child) { + margin-bottom: 8px; + } +} + +._beta { + margin-left: 0.7em; + font-size: 65%; + padding: 2px 3px; + color: var(--accent); + border: solid 1px var(--accent); + border-radius: 4px; + vertical-align: top; +} + +._table { + > ._row { + display: flex; + + &:not(:last-child) { + margin-bottom: 16px; + + @media (max-width: 500px) { + margin-bottom: 8px; + } + } + + > ._cell { + flex: 1; + + > ._label { + font-size: 80%; + opacity: 0.7; + + > ._icon { + margin-right: 4px; + display: none; + } + } + } + } +} + +._fullinfo { + padding: 64px 32px; + text-align: center; + + > img { + vertical-align: bottom; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; + } +} + +._keyValue { + display: flex; + + > * { + flex: 1; + } +} + +._link { + color: var(--link); +} + +._caption { + font-size: 0.8em; + opacity: 0.7; +} + +._monospace { + font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; +} + +._code { + @extend ._monospace; + background: #2d2d2d; + color: #ccc; + font-size: 14px; + line-height: 1.5; + padding: 5px; +} + +.prism-editor__textarea:focus { + outline: none; +} + +._zoom { + transition-duration: 0.5s, 0.5s; + transition-property: opacity, transform; + transition-timing-function: cubic-bezier(0,.5,.5,1); +} + +.zoom-enter-active, .zoom-leave-active { + transition: opacity 0.5s, transform 0.5s !important; +} +.zoom-enter-from, .zoom-leave-to { + opacity: 0; + transform: scale(0.9); +} + +@keyframes blink { + 0% { opacity: 1; transform: scale(1); } + 30% { opacity: 1; transform: scale(1); } + 90% { opacity: 0; transform: scale(0.5); } +} + +@keyframes tada { + from { + transform: scale3d(1, 1, 1); + } + + 10%, + 20% { + transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); + } + + 30%, + 50%, + 70%, + 90% { + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); + } + + 40%, + 60%, + 80% { + transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); + } + + to { + transform: scale3d(1, 1, 1); + } +} + +._anime_bounce { + will-change: transform; + animation: bounce ease 0.7s; + animation-iteration-count: 1; + transform-origin: 50% 50%; +} +._anime_bounce_ready { + will-change: transform; + transform: scaleX(0.90) scaleY(0.90) ; +} +._anime_bounce_standBy { + transition: transform 0.1s ease; +} + +@keyframes bounce{ + 0% { + transform: scaleX(0.90) scaleY(0.90) ; + } + 19% { + transform: scaleX(1.10) scaleY(1.10) ; + } + 48% { + transform: scaleX(0.95) scaleY(0.95) ; + } + 100% { + transform: scaleX(1.00) scaleY(1.00) ; + } +} diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts new file mode 100644 index 0000000000..fdc92ed793 --- /dev/null +++ b/packages/frontend/src/theme-store.ts @@ -0,0 +1,34 @@ +import { api } from '@/os'; +import { $i } from '@/account'; +import { Theme } from './scripts/theme'; + +const lsCacheKey = $i ? `themes:${$i.id}` : ''; + +export function getThemes(): Theme[] { + return JSON.parse(localStorage.getItem(lsCacheKey) || '[]'); +} + +export async function fetchThemes(): Promise { + if ($i == null) return; + + try { + const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' }); + localStorage.setItem(lsCacheKey, JSON.stringify(themes)); + } catch (err) { + if (err.code === 'NO_SUCH_KEY') return; + throw err; + } +} + +export async function addTheme(theme: Theme): Promise { + await fetchThemes(); + const themes = getThemes().concat(theme); + await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); + localStorage.setItem(lsCacheKey, JSON.stringify(themes)); +} + +export async function removeTheme(theme: Theme): Promise { + const themes = getThemes().filter(t => t.id !== theme.id); + await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes }); + localStorage.setItem(lsCacheKey, JSON.stringify(themes)); +} diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5 new file mode 100644 index 0000000000..88ec8a5459 --- /dev/null +++ b/packages/frontend/src/themes/_dark.json5 @@ -0,0 +1,99 @@ +// ダークテーマのベーステーマ +// このテーマが直接使われることは無い +{ + id: 'dark', + + name: 'Dark', + author: 'syuilo', + desc: 'Default dark theme', + kind: 'dark', + + props: { + accent: '#86b300', + accentDarken: ':darken<10<@accent', + accentLighten: ':lighten<10<@accent', + accentedBg: ':alpha<0.15<@accent', + focus: ':alpha<0.3<@accent', + bg: '#000', + acrylicBg: ':alpha<0.5<@bg', + fg: '#dadada', + fgTransparentWeak: ':alpha<0.75<@fg', + fgTransparent: ':alpha<0.5<@fg', + fgHighlighted: ':lighten<3<@fg', + fgOnAccent: '#fff', + divider: 'rgba(255, 255, 255, 0.1)', + indicator: '@accent', + panel: ':lighten<3<@bg', + panelHighlight: ':lighten<3<@panel', + panelHeaderBg: ':lighten<3<@panel', + panelHeaderFg: '@fg', + panelHeaderDivider: 'rgba(0, 0, 0, 0)', + panelBorder: '" solid 1px var(--divider)', + acrylicPanel: ':alpha<0.5<@panel', + windowHeader: ':alpha<0.85<@panel', + popup: ':lighten<3<@panel', + shadow: 'rgba(0, 0, 0, 0.3)', + header: ':alpha<0.7<@panel', + navBg: '@panel', + navFg: '@fg', + navHoverFg: ':lighten<17<@fg', + navActive: '@accent', + navIndicator: '@indicator', + link: '#44a4c1', + hashtag: '#ff9156', + mention: '@accent', + mentionMe: '@mention', + renote: '#229e82', + modalBg: 'rgba(0, 0, 0, 0.5)', + scrollbarHandle: 'rgba(255, 255, 255, 0.2)', + scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', + dateLabelFg: '@fg', + infoBg: '#253142', + infoFg: '#fff', + infoWarnBg: '#42321c', + infoWarnFg: '#ffbd3e', + switchBg: 'rgba(255, 255, 255, 0.15)', + cwBg: '#687390', + cwFg: '#393f4f', + cwHoverBg: '#707b97', + buttonBg: 'rgba(255, 255, 255, 0.05)', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', + buttonGradateA: '@accent', + buttonGradateB: ':hue<20<@accent', + swutchOffBg: 'rgba(255, 255, 255, 0.1)', + swutchOffFg: '@fg', + swutchOnBg: '@accentedBg', + swutchOnFg: '@accent', + inputBorder: 'rgba(255, 255, 255, 0.1)', + inputBorderHover: 'rgba(255, 255, 255, 0.2)', + listItemHoverBg: 'rgba(255, 255, 255, 0.03)', + driveFolderBg: ':alpha<0.3<@accent', + wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', + badge: '#31b1ce', + messageBg: '@bg', + success: '#86b300', + error: '#ec4137', + warn: '#ecb637', + codeString: '#ffb675', + codeNumber: '#cfff9e', + codeBoolean: '#c59eff', + deckDivider: '#000', + htmlThemeColor: '@bg', + X2: ':darken<2<@panel', + X3: 'rgba(255, 255, 255, 0.05)', + X4: 'rgba(255, 255, 255, 0.1)', + X5: 'rgba(255, 255, 255, 0.05)', + X6: 'rgba(255, 255, 255, 0.15)', + X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', + X11: 'rgba(0, 0, 0, 0.3)', + X12: 'rgba(255, 255, 255, 0.1)', + X13: 'rgba(255, 255, 255, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', + }, +} diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5 new file mode 100644 index 0000000000..bad1291c83 --- /dev/null +++ b/packages/frontend/src/themes/_light.json5 @@ -0,0 +1,99 @@ +// ライトテーマのベーステーマ +// このテーマが直接使われることは無い +{ + id: 'light', + + name: 'Light', + author: 'syuilo', + desc: 'Default light theme', + kind: 'light', + + props: { + accent: '#86b300', + accentDarken: ':darken<10<@accent', + accentLighten: ':lighten<10<@accent', + accentedBg: ':alpha<0.15<@accent', + focus: ':alpha<0.3<@accent', + bg: '#fff', + acrylicBg: ':alpha<0.5<@bg', + fg: '#5f5f5f', + fgTransparentWeak: ':alpha<0.75<@fg', + fgTransparent: ':alpha<0.5<@fg', + fgHighlighted: ':darken<3<@fg', + fgOnAccent: '#fff', + divider: 'rgba(0, 0, 0, 0.1)', + indicator: '@accent', + panel: ':lighten<3<@bg', + panelHighlight: ':darken<3<@panel', + panelHeaderBg: ':lighten<3<@panel', + panelHeaderFg: '@fg', + panelHeaderDivider: 'rgba(0, 0, 0, 0)', + panelBorder: '" solid 1px var(--divider)', + acrylicPanel: ':alpha<0.5<@panel', + windowHeader: ':alpha<0.85<@panel', + popup: ':lighten<3<@panel', + shadow: 'rgba(0, 0, 0, 0.1)', + header: ':alpha<0.7<@panel', + navBg: '@panel', + navFg: '@fg', + navHoverFg: ':darken<17<@fg', + navActive: '@accent', + navIndicator: '@indicator', + link: '#44a4c1', + hashtag: '#ff9156', + mention: '@accent', + mentionMe: '@mention', + renote: '#229e82', + modalBg: 'rgba(0, 0, 0, 0.3)', + scrollbarHandle: 'rgba(0, 0, 0, 0.2)', + scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', + dateLabelFg: '@fg', + infoBg: '#e5f5ff', + infoFg: '#72818a', + infoWarnBg: '#fff0db', + infoWarnFg: '#8f6e31', + switchBg: 'rgba(0, 0, 0, 0.15)', + cwBg: '#b1b9c1', + cwFg: '#fff', + cwHoverBg: '#bbc4ce', + buttonBg: 'rgba(0, 0, 0, 0.05)', + buttonHoverBg: 'rgba(0, 0, 0, 0.1)', + buttonGradateA: '@accent', + buttonGradateB: ':hue<20<@accent', + swutchOffBg: 'rgba(0, 0, 0, 0.1)', + swutchOffFg: '@panel', + swutchOnBg: '@accent', + swutchOnFg: '@fgOnAccent', + inputBorder: 'rgba(0, 0, 0, 0.1)', + inputBorderHover: 'rgba(0, 0, 0, 0.2)', + listItemHoverBg: 'rgba(0, 0, 0, 0.03)', + driveFolderBg: ':alpha<0.3<@accent', + wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', + badge: '#31b1ce', + messageBg: '@bg', + success: '#86b300', + error: '#ec4137', + warn: '#ecb637', + codeString: '#b98710', + codeNumber: '#0fbbbb', + codeBoolean: '#62b70c', + deckDivider: ':darken<3<@bg', + htmlThemeColor: '@bg', + X2: ':darken<2<@panel', + X3: 'rgba(0, 0, 0, 0.05)', + X4: 'rgba(0, 0, 0, 0.1)', + X5: 'rgba(0, 0, 0, 0.05)', + X6: 'rgba(0, 0, 0, 0.25)', + X7: 'rgba(0, 0, 0, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', + X11: 'rgba(0, 0, 0, 0.1)', + X12: 'rgba(0, 0, 0, 0.1)', + X13: 'rgba(0, 0, 0, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', + }, +} diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend/src/themes/d-astro.json5 new file mode 100644 index 0000000000..c6a927ec3a --- /dev/null +++ b/packages/frontend/src/themes/d-astro.json5 @@ -0,0 +1,78 @@ +{ + id: '080a01c5-377d-4fbb-88cc-6bb5d04977ea', + base: 'dark', + name: 'Mi Astro Dark', + author: 'syuilo', + props: { + bg: '#232125', + fg: '#efdab9', + cwBg: '#687390', + cwFg: '#393f4f', + link: '#78b0a0', + warn: '#ecb637', + badge: '#31b1ce', + error: '#ec4137', + focus: ':alpha<0.3<@accent', + navBg: '@panel', + navFg: '@fg', + panel: '#2a272b', + accent: '#81c08b', + header: ':alpha<0.7<@bg', + infoBg: '#253142', + infoFg: '#fff', + renote: '#659CC8', + shadow: 'rgba(0, 0, 0, 0.3)', + divider: 'rgba(255, 255, 255, 0.1)', + hashtag: '#ff9156', + mention: '#ffd152', + modalBg: 'rgba(0, 0, 0, 0.5)', + success: '#86b300', + buttonBg: 'rgba(255, 255, 255, 0.05)', + acrylicBg: ':alpha<0.5<@bg', + cwHoverBg: '#707b97', + indicator: '@accent', + mentionMe: '#fb5d38', + messageBg: '@bg', + navActive: '@accent', + infoWarnBg: '#42321c', + infoWarnFg: '#ffbd3e', + navHoverFg: ':lighten<17<@fg', + dateLabelFg: '@fg', + inputBorder: 'rgba(255, 255, 255, 0.1)', + inputBorderHover: 'rgba(255, 255, 255, 0.2)', + panelBorder: '" solid 1px var(--divider)', + accentDarken: ':darken<10<@accent', + acrylicPanel: ':alpha<0.5<@panel', + navIndicator: '@accent', + accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', + buttonGradateA: '@accent', + buttonGradateB: ':hue<-20<@accent', + driveFolderBg: ':alpha<0.3<@accent', + fgHighlighted: ':lighten<3<@fg', + panelHeaderBg: ':lighten<3<@panel', + panelHeaderFg: '@fg', + htmlThemeColor: '@bg', + panelHighlight: ':lighten<3<@panel', + listItemHoverBg: 'rgba(255, 255, 255, 0.03)', + scrollbarHandle: 'rgba(255, 255, 255, 0.2)', + wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', + panelHeaderDivider: 'rgba(0, 0, 0, 0)', + scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', + X2: ':darken<2<@panel', + X3: 'rgba(255, 255, 255, 0.05)', + X4: 'rgba(255, 255, 255, 0.1)', + X5: 'rgba(255, 255, 255, 0.05)', + X6: 'rgba(255, 255, 255, 0.15)', + X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', + X11: 'rgba(0, 0, 0, 0.3)', + X12: 'rgba(255, 255, 255, 0.1)', + X13: 'rgba(255, 255, 255, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + }, +} diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend/src/themes/d-botanical.json5 new file mode 100644 index 0000000000..c03b95e2d7 --- /dev/null +++ b/packages/frontend/src/themes/d-botanical.json5 @@ -0,0 +1,26 @@ +{ + id: '504debaf-4912-6a4c-5059-1db08a76b737', + + name: 'Mi Botanical Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: 'rgb(148, 179, 0)', + bg: 'rgb(37, 38, 36)', + fg: 'rgb(216, 212, 199)', + fgHighlighted: '#fff', + divider: 'rgba(255, 255, 255, 0.14)', + panel: 'rgb(47, 47, 44)', + panelHeaderBg: '@panel', + panelHeaderDivider: '@divider', + header: ':alpha<0.7<@panel', + navBg: '#363636', + renote: '@accent', + mention: 'rgb(212, 153, 76)', + mentionMe: 'rgb(212, 210, 76)', + hashtag: '#5bcbb0', + link: '@accent', + }, +} diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend/src/themes/d-cherry.json5 new file mode 100644 index 0000000000..a7e1ad1c80 --- /dev/null +++ b/packages/frontend/src/themes/d-cherry.json5 @@ -0,0 +1,20 @@ +{ + id: '679b3b87-a4e9-4789-8696-b56c15cc33b0', + + name: 'Mi Cherry Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: 'rgb(255, 89, 117)', + bg: 'rgb(28, 28, 37)', + fg: 'rgb(236, 239, 244)', + panel: 'rgb(35, 35, 47)', + renote: '@accent', + link: '@accent', + mention: '@accent', + hashtag: '@accent', + divider: 'rgb(63, 63, 80)', + }, +} diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend/src/themes/d-dark.json5 new file mode 100644 index 0000000000..d24ce4df69 --- /dev/null +++ b/packages/frontend/src/themes/d-dark.json5 @@ -0,0 +1,26 @@ +{ + id: '8050783a-7f63-445a-b270-36d0f6ba1677', + + name: 'Mi Dark', + author: 'syuilo', + desc: 'Default light theme', + + base: 'dark', + + props: { + bg: '#232323', + fg: 'rgb(199, 209, 216)', + fgHighlighted: '#fff', + divider: 'rgba(255, 255, 255, 0.14)', + panel: '#2d2d2d', + panelHeaderBg: '@panel', + panelHeaderDivider: '@divider', + header: ':alpha<0.7<@panel', + navBg: '#363636', + renote: '@accent', + mention: '#da6d35', + mentionMe: '#d44c4c', + hashtag: '#4cb8d4', + link: '@accent', + }, +} diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend/src/themes/d-future.json5 new file mode 100644 index 0000000000..b6fa1ab0c1 --- /dev/null +++ b/packages/frontend/src/themes/d-future.json5 @@ -0,0 +1,27 @@ +{ + id: '32a637ef-b47a-4775-bb7b-bacbb823f865', + + name: 'Mi Future Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: '#63e2b7', + bg: '#101014', + fg: '#D5D5D6', + fgHighlighted: '#fff', + fgOnAccent: '#000', + divider: 'rgba(255, 255, 255, 0.1)', + panel: '#18181c', + panelHeaderBg: '@panel', + panelHeaderDivider: '@divider', + renote: '@accent', + mention: '#f2c97d', + mentionMe: '@accent', + hashtag: '#70c0e8', + link: '#e88080', + buttonGradateA: '@accent', + buttonGradateB: ':saturate<30<:hue<30<@accent', + }, +} diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend/src/themes/d-green-lime.json5 new file mode 100644 index 0000000000..a6983b9ac2 --- /dev/null +++ b/packages/frontend/src/themes/d-green-lime.json5 @@ -0,0 +1,24 @@ +{ + id: '02816013-8107-440f-877e-865083ffe194', + + name: 'Mi Green+Lime Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: '#b4e900', + bg: '#0C1210', + fg: '#dee7e4', + fgHighlighted: '#fff', + fgOnAccent: '#192320', + divider: '#e7fffb24', + panel: '#192320', + panelHeaderBg: '@panel', + panelHeaderDivider: '@divider', + popup: '#293330', + renote: '@accent', + mentionMe: '#ffaa00', + link: '#24d7ce', + }, +} diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend/src/themes/d-green-orange.json5 new file mode 100644 index 0000000000..62adc39e29 --- /dev/null +++ b/packages/frontend/src/themes/d-green-orange.json5 @@ -0,0 +1,24 @@ +{ + id: 'dc489603-27b5-424a-9b25-1ff6aec9824a', + + name: 'Mi Green+Orange Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: '#e97f00', + bg: '#0C1210', + fg: '#dee7e4', + fgHighlighted: '#fff', + fgOnAccent: '#192320', + divider: '#e7fffb24', + panel: '#192320', + panelHeaderBg: '@panel', + panelHeaderDivider: '@divider', + popup: '#293330', + renote: '@accent', + mentionMe: '#b4e900', + link: '#24d7ce', + }, +} diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend/src/themes/d-ice.json5 new file mode 100644 index 0000000000..179b060dcf --- /dev/null +++ b/packages/frontend/src/themes/d-ice.json5 @@ -0,0 +1,13 @@ +{ + id: '66e7e5a9-cd43-42cd-837d-12f47841fa34', + + name: 'Mi Ice Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: '#47BFE8', + bg: '#212526', + }, +} diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend/src/themes/d-persimmon.json5 new file mode 100644 index 0000000000..e36265ff10 --- /dev/null +++ b/packages/frontend/src/themes/d-persimmon.json5 @@ -0,0 +1,25 @@ +{ + id: 'c503d768-7c70-4db2-a4e6-08264304bc8d', + + name: 'Mi Persimmon Dark', + author: 'syuilo', + + base: 'dark', + + props: { + accent: 'rgb(206, 102, 65)', + bg: 'rgb(31, 33, 31)', + fg: '#cdd8c7', + fgHighlighted: '#fff', + divider: 'rgba(255, 255, 255, 0.14)', + panel: 'rgb(41, 43, 41)', + infoFg: '@fg', + infoBg: '#333c3b', + navBg: '#141714', + renote: '@accent', + mention: '@accent', + mentionMe: '#de6161', + hashtag: '#68bad0', + link: '#a1c758', + }, +} diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend/src/themes/d-u0.json5 new file mode 100644 index 0000000000..b270f809ac --- /dev/null +++ b/packages/frontend/src/themes/d-u0.json5 @@ -0,0 +1,88 @@ +{ + id: '7a5bc13b-df8f-4d44-8e94-4452f0c634bb', + base: 'dark', + name: 'Mi U0 Dark', + props: { + X2: ':darken<2<@panel', + X3: 'rgba(255, 255, 255, 0.05)', + X4: 'rgba(255, 255, 255, 0.1)', + X5: 'rgba(255, 255, 255, 0.05)', + X6: 'rgba(255, 255, 255, 0.15)', + X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + bg: '#172426', + fg: '#dadada', + X10: ':alpha<0.4<@accent', + X11: 'rgba(0, 0, 0, 0.3)', + X12: 'rgba(255, 255, 255, 0.1)', + X13: 'rgba(255, 255, 255, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', + cwBg: '#687390', + cwFg: '#393f4f', + link: '@accent', + warn: '#ecb637', + badge: '#31b1ce', + error: '#ec4137', + focus: ':alpha<0.3<@accent', + navBg: '@panel', + navFg: '@fg', + panel: ':lighten<3<@bg', + popup: ':lighten<3<@panel', + accent: '#00a497', + header: ':alpha<0.7<@panel', + infoBg: '#253142', + infoFg: '#fff', + renote: '@accent', + shadow: 'rgba(0, 0, 0, 0.3)', + divider: 'rgba(255, 255, 255, 0.1)', + hashtag: '#e6b422', + mention: '@accent', + modalBg: 'rgba(0, 0, 0, 0.5)', + success: '#86b300', + buttonBg: 'rgba(255, 255, 255, 0.05)', + switchBg: 'rgba(255, 255, 255, 0.15)', + acrylicBg: ':alpha<0.5<@bg', + cwHoverBg: '#707b97', + indicator: '@accent', + mentionMe: '@mention', + messageBg: '@bg', + navActive: '@accent', + accentedBg: ':alpha<0.15<@accent', + codeNumber: '#cfff9e', + codeString: '#ffb675', + fgOnAccent: '#fff', + infoWarnBg: '#42321c', + infoWarnFg: '#ffbd3e', + navHoverFg: ':lighten<17<@fg', + codeBoolean: '#c59eff', + dateLabelFg: '@fg', + inputBorder: 'rgba(255, 255, 255, 0.1)', + panelBorder: '" solid 1px var(--divider)', + accentDarken: ':darken<10<@accent', + acrylicPanel: ':alpha<0.5<@panel', + navIndicator: '@indicator', + accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(255, 255, 255, 0.1)', + driveFolderBg: ':alpha<0.3<@accent', + fgHighlighted: ':lighten<3<@fg', + fgTransparent: ':alpha<0.5<@fg', + panelHeaderBg: ':lighten<3<@panel', + panelHeaderFg: '@fg', + buttonGradateA: '@accent', + buttonGradateB: ':hue<20<@accent', + htmlThemeColor: '@bg', + panelHighlight: ':lighten<3<@panel', + listItemHoverBg: 'rgba(255, 255, 255, 0.03)', + scrollbarHandle: 'rgba(255, 255, 255, 0.2)', + inputBorderHover: 'rgba(255, 255, 255, 0.2)', + wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', + fgTransparentWeak: ':alpha<0.75<@fg', + panelHeaderDivider: 'rgba(0, 0, 0, 0)', + scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', + deckDivider: '#142022', + }, +} diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend/src/themes/l-apricot.json5 new file mode 100644 index 0000000000..1ed5525575 --- /dev/null +++ b/packages/frontend/src/themes/l-apricot.json5 @@ -0,0 +1,22 @@ +{ + id: '0ff48d43-aab3-46e7-ab12-8492110d2e2b', + + name: 'Mi Apricot Light', + author: 'syuilo', + + base: 'light', + + props: { + accent: 'rgb(234, 154, 82)', + bg: '#e6e5e2', + fg: 'rgb(149, 143, 139)', + panel: '#EEECE8', + renote: '@accent', + link: '@accent', + mention: '@accent', + hashtag: '@accent', + inputBorder: 'rgba(0, 0, 0, 0.1)', + inputBorderHover: 'rgba(0, 0, 0, 0.2)', + infoBg: 'rgb(226, 235, 241)', + }, +} diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend/src/themes/l-cherry.json5 new file mode 100644 index 0000000000..5ad240241e --- /dev/null +++ b/packages/frontend/src/themes/l-cherry.json5 @@ -0,0 +1,21 @@ +{ + id: 'ac168876-f737-4074-a3fc-a370c732ef48', + + name: 'Mi Cherry Light', + author: 'syuilo', + + base: 'light', + + props: { + accent: 'rgb(219, 96, 114)', + bg: 'rgb(254, 248, 249)', + fg: 'rgb(152, 13, 26)', + panel: 'rgb(255, 255, 255)', + renote: '@accent', + link: 'rgb(156, 187, 5)', + mention: '@accent', + hashtag: '@accent', + divider: 'rgba(134, 51, 51, 0.1)', + inputBorderHover: 'rgb(238, 221, 222)', + }, +} diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend/src/themes/l-coffee.json5 new file mode 100644 index 0000000000..fbcd4fa9ef --- /dev/null +++ b/packages/frontend/src/themes/l-coffee.json5 @@ -0,0 +1,21 @@ +{ + id: '6ed80faa-74f0-42c2-98e4-a64d9e138eab', + + name: 'Mi Coffee Light', + author: 'syuilo', + + base: 'light', + + props: { + accent: '#9f8989', + bg: '#f5f3f3', + fg: '#7f6666', + panel: '#fff', + divider: 'rgba(87, 68, 68, 0.1)', + renote: 'rgb(160, 172, 125)', + link: 'rgb(137, 151, 159)', + mention: '@accent', + mentionMe: 'rgb(170, 149, 98)', + hashtag: '@accent', + }, +} diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend/src/themes/l-light.json5 new file mode 100644 index 0000000000..248355c945 --- /dev/null +++ b/packages/frontend/src/themes/l-light.json5 @@ -0,0 +1,20 @@ +{ + id: '4eea646f-7afa-4645-83e9-83af0333cd37', + + name: 'Mi Light', + author: 'syuilo', + desc: 'Default light theme', + + base: 'light', + + props: { + bg: '#f9f9f9', + fg: '#676767', + divider: '#e8e8e8', + header: ':alpha<0.7<@panel', + navBg: '#fff', + panel: '#fff', + panelHeaderDivider: '@divider', + mentionMe: 'rgb(0, 179, 70)', + }, +} diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend/src/themes/l-rainy.json5 new file mode 100644 index 0000000000..283dd74c6c --- /dev/null +++ b/packages/frontend/src/themes/l-rainy.json5 @@ -0,0 +1,21 @@ +{ + id: 'a58a0abb-ff8c-476a-8dec-0ad7837e7e96', + + name: 'Mi Rainy Light', + author: 'syuilo', + + base: 'light', + + props: { + accent: '#5db0da', + bg: 'rgb(246 248 249)', + fg: '#636b71', + panel: '#fff', + divider: 'rgb(230 233 234)', + panelHeaderDivider: '@divider', + renote: '@accent', + link: '@accent', + mention: '@accent', + hashtag: '@accent', + }, +} diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend/src/themes/l-sushi.json5 new file mode 100644 index 0000000000..5846927d65 --- /dev/null +++ b/packages/frontend/src/themes/l-sushi.json5 @@ -0,0 +1,18 @@ +{ + id: '213273e5-7d20-d5f0-6e36-1b6a4f67115c', + + name: 'Mi Sushi Light', + author: 'syuilo', + + base: 'light', + + props: { + accent: '#e36749', + bg: '#f0eee9', + fg: '#5f5f5f', + renote: '@accent', + link: '@accent', + mention: '@accent', + hashtag: '#229e82', + }, +} diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend/src/themes/l-u0.json5 new file mode 100644 index 0000000000..03b114ba39 --- /dev/null +++ b/packages/frontend/src/themes/l-u0.json5 @@ -0,0 +1,87 @@ +{ + id: 'e2c940b5-6e9a-4c03-b738-261c720c426d', + base: 'light', + name: 'Mi U0 Light', + props: { + X2: ':darken<2<@panel', + X3: 'rgba(255, 255, 255, 0.05)', + X4: 'rgba(255, 255, 255, 0.1)', + X5: 'rgba(255, 255, 255, 0.05)', + X6: 'rgba(255, 255, 255, 0.15)', + X7: 'rgba(255, 255, 255, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + bg: '#e7e7eb', + fg: '#5f5f5f', + X10: ':alpha<0.4<@accent', + X11: 'rgba(0, 0, 0, 0.3)', + X12: 'rgba(255, 255, 255, 0.1)', + X13: 'rgba(255, 255, 255, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', + cwBg: '#687390', + cwFg: '#393f4f', + link: '@accent', + warn: '#ecb637', + badge: '#31b1ce', + error: '#ec4137', + focus: ':alpha<0.3<@accent', + navBg: '@panel', + navFg: '@fg', + panel: ':lighten<3<@bg', + popup: ':lighten<3<@panel', + accent: '#478384', + header: ':alpha<0.7<@panel', + infoBg: '#253142', + infoFg: '#fff', + renote: '@accent', + shadow: 'rgba(0, 0, 0, 0.3)', + divider: '#4646461a', + hashtag: '#1f3134', + mention: '@accent', + modalBg: 'rgba(0, 0, 0, 0.5)', + success: '#86b300', + buttonBg: '#0000000d', + switchBg: 'rgba(255, 255, 255, 0.15)', + acrylicBg: ':alpha<0.5<@bg', + cwHoverBg: '#707b97', + indicator: '@accent', + mentionMe: '@mention', + messageBg: '@bg', + navActive: '@accent', + accentedBg: ':alpha<0.15<@accent', + codeNumber: '#cfff9e', + codeString: '#ffb675', + fgOnAccent: '#fff', + infoWarnBg: '#42321c', + infoWarnFg: '#ffbd3e', + navHoverFg: ':lighten<17<@fg', + codeBoolean: '#c59eff', + dateLabelFg: '@fg', + inputBorder: 'rgba(255, 255, 255, 0.1)', + panelBorder: '" solid 1px var(--divider)', + accentDarken: ':darken<10<@accent', + acrylicPanel: ':alpha<0.5<@panel', + navIndicator: '@indicator', + accentLighten: ':lighten<10<@accent', + buttonHoverBg: '#0000001a', + driveFolderBg: ':alpha<0.3<@accent', + fgHighlighted: ':lighten<3<@fg', + fgTransparent: ':alpha<0.5<@fg', + panelHeaderBg: ':lighten<3<@panel', + panelHeaderFg: '@fg', + buttonGradateA: '@accent', + buttonGradateB: ':hue<20<@accent', + htmlThemeColor: '@bg', + panelHighlight: ':lighten<3<@panel', + listItemHoverBg: 'rgba(255, 255, 255, 0.03)', + scrollbarHandle: '#74747433', + inputBorderHover: 'rgba(255, 255, 255, 0.2)', + wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', + fgTransparentWeak: ':alpha<0.75<@fg', + panelHeaderDivider: 'rgba(0, 0, 0, 0)', + scrollbarHandleHover: 'rgba(255, 255, 255, 0.4)', + }, +} diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend/src/themes/l-vivid.json5 new file mode 100644 index 0000000000..b3c08f38ae --- /dev/null +++ b/packages/frontend/src/themes/l-vivid.json5 @@ -0,0 +1,82 @@ +{ + id: '6128c2a9-5c54-43fe-a47d-17942356470b', + + name: 'Mi Vivid Light', + author: 'syuilo', + + base: 'light', + + props: { + bg: '#fafafa', + fg: '#444', + cwBg: '#b1b9c1', + cwFg: '#fff', + link: '#ff9400', + warn: '#ecb637', + badge: '#31b1ce', + error: '#ec4137', + focus: ':alpha<0.3<@accent', + navBg: '@panel', + navFg: '@fg', + panel: '#fff', + accent: '#008cff', + header: ':alpha<0.7<@panel', + infoBg: '#e5f5ff', + infoFg: '#72818a', + renote: '@accent', + shadow: 'rgba(0, 0, 0, 0.1)', + divider: 'rgba(0, 0, 0, 0.08)', + hashtag: '#92d400', + mention: '@accent', + modalBg: 'rgba(0, 0, 0, 0.3)', + success: '#86b300', + buttonBg: 'rgba(0, 0, 0, 0.05)', + acrylicBg: ':alpha<0.5<@bg', + cwHoverBg: '#bbc4ce', + indicator: '@accent', + mentionMe: '@mention', + messageBg: '@bg', + navActive: '@accent', + infoWarnBg: '#fff0db', + infoWarnFg: '#8f6e31', + navHoverFg: ':darken<17<@fg', + dateLabelFg: '@fg', + inputBorder: 'rgba(0, 0, 0, 0.1)', + inputBorderHover: 'rgba(0, 0, 0, 0.2)', + panelBorder: '" solid 1px var(--divider)', + accentDarken: ':darken<10<@accent', + acrylicPanel: ':alpha<0.5<@panel', + navIndicator: '@accent', + accentLighten: ':lighten<10<@accent', + buttonHoverBg: 'rgba(0, 0, 0, 0.1)', + driveFolderBg: ':alpha<0.3<@accent', + fgHighlighted: ':darken<3<@fg', + fgTransparent: ':alpha<0.5<@fg', + panelHeaderBg: ':lighten<3<@panel', + panelHeaderFg: '@fg', + htmlThemeColor: '@bg', + panelHighlight: ':darken<3<@panel', + listItemHoverBg: 'rgba(0, 0, 0, 0.03)', + scrollbarHandle: 'rgba(0, 0, 0, 0.2)', + wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', + fgTransparentWeak: ':alpha<0.75<@fg', + panelHeaderDivider: '@divider', + scrollbarHandleHover: 'rgba(0, 0, 0, 0.4)', + X2: ':darken<2<@panel', + X3: 'rgba(0, 0, 0, 0.05)', + X4: 'rgba(0, 0, 0, 0.1)', + X5: 'rgba(0, 0, 0, 0.05)', + X6: 'rgba(0, 0, 0, 0.25)', + X7: 'rgba(0, 0, 0, 0.05)', + X8: ':lighten<5<@accent', + X9: ':darken<5<@accent', + X10: ':alpha<0.4<@accent', + X11: 'rgba(0, 0, 0, 0.1)', + X12: 'rgba(0, 0, 0, 0.1)', + X13: 'rgba(0, 0, 0, 0.15)', + X14: ':alpha<0.5<@navBg', + X15: ':alpha<0<@panel', + X16: ':alpha<0.7<@panel', + X17: ':alpha<0.8<@bg', + }, +} diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts new file mode 100644 index 0000000000..972f6db214 --- /dev/null +++ b/packages/frontend/src/types/menu.ts @@ -0,0 +1,21 @@ +import * as Misskey from 'misskey-js'; +import { Ref } from 'vue'; + +export type MenuAction = (ev: MouseEvent) => void; + +export type MenuDivider = null; +export type MenuNull = undefined; +export type MenuLabel = { type: 'label', text: string }; +export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; +export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; +export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; +export type MenuSwitch = { type: 'switch', ref: Ref, text: string, disabled?: boolean }; +export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; +export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] }; + +export type MenuPending = { type: 'pending' }; + +type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent; +type OuterPromiseMenuItem = Promise; +export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; +export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent; diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue new file mode 100644 index 0000000000..7f3fc0e4af --- /dev/null +++ b/packages/frontend/src/ui/_common_/common.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue new file mode 100644 index 0000000000..50b28de063 --- /dev/null +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -0,0 +1,314 @@ + + + + + diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue new file mode 100644 index 0000000000..b82da15f13 --- /dev/null +++ b/packages/frontend/src/ui/_common_/navbar.vue @@ -0,0 +1,521 @@ + + + + + diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue new file mode 100644 index 0000000000..24fc4f6f6d --- /dev/null +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue new file mode 100644 index 0000000000..e7f88e4984 --- /dev/null +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue new file mode 100644 index 0000000000..f4d989c387 --- /dev/null +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue new file mode 100644 index 0000000000..114ca5be8c --- /dev/null +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue new file mode 100644 index 0000000000..a855de8ab9 --- /dev/null +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts new file mode 100644 index 0000000000..8676d2d48d --- /dev/null +++ b/packages/frontend/src/ui/_common_/sw-inject.ts @@ -0,0 +1,35 @@ +import { inject } from 'vue'; +import { post } from '@/os'; +import { $i, login } from '@/account'; +import { defaultStore } from '@/store'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { mainRouter } from '@/router'; + +export function swInject() { + navigator.serviceWorker.addEventListener('message', ev => { + if (_DEV_) { + console.log('sw msg', ev.data); + } + + if (ev.data.type !== 'order') return; + + if (ev.data.loginId !== $i?.id) { + return getAccountFromId(ev.data.loginId).then(account => { + if (!account) return; + return login(account.token, ev.data.url); + }); + } + + switch (ev.data.order) { + case 'post': + return post(ev.data.options); + case 'push': + if (mainRouter.currentRoute.value.path === ev.data.url) { + return window.scroll({ top: 0, behavior: 'smooth' }); + } + return mainRouter.push(ev.data.url); + default: + return; + } + }); +} diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue new file mode 100644 index 0000000000..70882bd251 --- /dev/null +++ b/packages/frontend/src/ui/_common_/upload.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue new file mode 100644 index 0000000000..46d79e6355 --- /dev/null +++ b/packages/frontend/src/ui/classic.header.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue new file mode 100644 index 0000000000..dac09ea703 --- /dev/null +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue new file mode 100644 index 0000000000..0e726c11ed --- /dev/null +++ b/packages/frontend/src/ui/classic.vue @@ -0,0 +1,320 @@ + + + + + diff --git a/packages/frontend/src/ui/classic.widgets.vue b/packages/frontend/src/ui/classic.widgets.vue new file mode 100644 index 0000000000..163ec982ce --- /dev/null +++ b/packages/frontend/src/ui/classic.widgets.vue @@ -0,0 +1,84 @@ + + + + + diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue new file mode 100644 index 0000000000..f3415cfd09 --- /dev/null +++ b/packages/frontend/src/ui/deck.vue @@ -0,0 +1,435 @@ + + + + + diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue new file mode 100644 index 0000000000..ba14530662 --- /dev/null +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/packages/frontend/src/ui/deck/column-core.vue b/packages/frontend/src/ui/deck/column-core.vue new file mode 100644 index 0000000000..30c0dc5e1c --- /dev/null +++ b/packages/frontend/src/ui/deck/column-core.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue new file mode 100644 index 0000000000..2a99b621e6 --- /dev/null +++ b/packages/frontend/src/ui/deck/column.vue @@ -0,0 +1,398 @@ + + + + + diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts new file mode 100644 index 0000000000..56db7398e5 --- /dev/null +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -0,0 +1,296 @@ +import { throttle } from 'throttle-debounce'; +import { markRaw } from 'vue'; +import { notificationTypes } from 'misskey-js'; +import { Storage } from '../../pizzax'; +import { i18n } from '@/i18n'; +import { api } from '@/os'; +import { deepClone } from '@/scripts/clone'; + +type ColumnWidget = { + name: string; + id: string; + data: Record; +}; + +export type Column = { + id: string; + type: 'main' | 'widgets' | 'notifications' | 'tl' | 'antenna' | 'list' | 'mentions' | 'direct'; + name: string | null; + width: number; + widgets?: ColumnWidget[]; + active?: boolean; + flexible?: boolean; + antennaId?: string; + listId?: string; + includingTypes?: typeof notificationTypes[number][]; + tl?: 'home' | 'local' | 'social' | 'global'; +}; + +export const deckStore = markRaw(new Storage('deck', { + profile: { + where: 'deviceAccount', + default: 'default', + }, + columns: { + where: 'deviceAccount', + default: [] as Column[], + }, + layout: { + where: 'deviceAccount', + default: [] as Column['id'][][], + }, + columnAlign: { + where: 'deviceAccount', + default: 'left' as 'left' | 'right' | 'center', + }, + alwaysShowMainColumn: { + where: 'deviceAccount', + default: true, + }, + navWindow: { + where: 'deviceAccount', + default: true, + }, +})); + +export const loadDeck = async () => { + let deck; + + try { + deck = await api('i/registry/get', { + scope: ['client', 'deck', 'profiles'], + key: deckStore.state.profile, + }); + } catch (err) { + if (err.code === 'NO_SUCH_KEY') { + // 後方互換性のため + if (deckStore.state.profile === 'default') { + saveDeck(); + return; + } + + deckStore.set('columns', []); + deckStore.set('layout', []); + return; + } + throw err; + } + + deckStore.set('columns', deck.columns); + deckStore.set('layout', deck.layout); +}; + +// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する +export const saveDeck = throttle(1000, () => { + api('i/registry/set', { + scope: ['client', 'deck', 'profiles'], + key: deckStore.state.profile, + value: { + columns: deckStore.reactiveState.columns.value, + layout: deckStore.reactiveState.layout.value, + }, + }); +}); + +export async function getProfiles(): Promise { + return await api('i/registry/keys', { + scope: ['client', 'deck', 'profiles'], + }); +} + +export async function deleteProfile(key: string): Promise { + return await api('i/registry/remove', { + scope: ['client', 'deck', 'profiles'], + key: key, + }); +} + +export function addColumn(column: Column) { + if (column.name === undefined) column.name = null; + deckStore.push('columns', column); + deckStore.push('layout', [column.id]); + saveDeck(); +} + +export function removeColumn(id: Column['id']) { + deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id)); + deckStore.set('layout', deckStore.state.layout + .map(ids => ids.filter(_id => _id !== id)) + .filter(ids => ids.length > 0)); + saveDeck(); +} + +export function swapColumn(a: Column['id'], b: Column['id']) { + const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1); + const aY = deckStore.state.layout[aX].findIndex(id => id === a); + const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1); + const bY = deckStore.state.layout[bX].findIndex(id => id === b); + const layout = deepClone(deckStore.state.layout); + layout[aX][aY] = b; + layout[bX][bY] = a; + deckStore.set('layout', layout); + saveDeck(); +} + +export function swapLeftColumn(id: Column['id']) { + const layout = deepClone(deckStore.state.layout); + deckStore.state.layout.some((ids, i) => { + if (ids.includes(id)) { + const left = deckStore.state.layout[i - 1]; + if (left) { + layout[i - 1] = deckStore.state.layout[i]; + layout[i] = left; + deckStore.set('layout', layout); + } + return true; + } + }); + saveDeck(); +} + +export function swapRightColumn(id: Column['id']) { + const layout = deepClone(deckStore.state.layout); + deckStore.state.layout.some((ids, i) => { + if (ids.includes(id)) { + const right = deckStore.state.layout[i + 1]; + if (right) { + layout[i + 1] = deckStore.state.layout[i]; + layout[i] = right; + deckStore.set('layout', layout); + } + return true; + } + }); + saveDeck(); +} + +export function swapUpColumn(id: Column['id']) { + const layout = deepClone(deckStore.state.layout); + const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); + const ids = deepClone(deckStore.state.layout[idsIndex]); + ids.some((x, i) => { + if (x === id) { + const up = ids[i - 1]; + if (up) { + ids[i - 1] = id; + ids[i] = up; + + layout[idsIndex] = ids; + deckStore.set('layout', layout); + } + return true; + } + }); + saveDeck(); +} + +export function swapDownColumn(id: Column['id']) { + const layout = deepClone(deckStore.state.layout); + const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id)); + const ids = deepClone(deckStore.state.layout[idsIndex]); + ids.some((x, i) => { + if (x === id) { + const down = ids[i + 1]; + if (down) { + ids[i + 1] = id; + ids[i] = down; + + layout[idsIndex] = ids; + deckStore.set('layout', layout); + } + return true; + } + }); + saveDeck(); +} + +export function stackLeftColumn(id: Column['id']) { + let layout = deepClone(deckStore.state.layout); + const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); + layout = layout.map(ids => ids.filter(_id => _id !== id)); + layout[i - 1].push(id); + layout = layout.filter(ids => ids.length > 0); + deckStore.set('layout', layout); + saveDeck(); +} + +export function popRightColumn(id: Column['id']) { + let layout = deepClone(deckStore.state.layout); + const i = deckStore.state.layout.findIndex(ids => ids.includes(id)); + const affected = layout[i]; + layout = layout.map(ids => ids.filter(_id => _id !== id)); + layout.splice(i + 1, 0, [id]); + layout = layout.filter(ids => ids.length > 0); + deckStore.set('layout', layout); + + const columns = deepClone(deckStore.state.columns); + for (const column of columns) { + if (affected.includes(column.id)) { + column.active = true; + } + } + deckStore.set('columns', columns); + + saveDeck(); +} + +export function addColumnWidget(id: Column['id'], widget: ColumnWidget) { + const columns = deepClone(deckStore.state.columns); + const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); + const column = deepClone(deckStore.state.columns[columnIndex]); + if (column == null) return; + if (column.widgets == null) column.widgets = []; + column.widgets.unshift(widget); + columns[columnIndex] = column; + deckStore.set('columns', columns); + saveDeck(); +} + +export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) { + const columns = deepClone(deckStore.state.columns); + const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); + const column = deepClone(deckStore.state.columns[columnIndex]); + if (column == null) return; + column.widgets = column.widgets.filter(w => w.id !== widget.id); + columns[columnIndex] = column; + deckStore.set('columns', columns); + saveDeck(); +} + +export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) { + const columns = deepClone(deckStore.state.columns); + const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); + const column = deepClone(deckStore.state.columns[columnIndex]); + if (column == null) return; + column.widgets = widgets; + columns[columnIndex] = column; + deckStore.set('columns', columns); + saveDeck(); +} + +export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) { + const columns = deepClone(deckStore.state.columns); + const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); + const column = deepClone(deckStore.state.columns[columnIndex]); + if (column == null) return; + column.widgets = column.widgets.map(w => w.id === widgetId ? { + ...w, + data: widgetData, + } : w); + columns[columnIndex] = column; + deckStore.set('columns', columns); + saveDeck(); +} + +export function updateColumn(id: Column['id'], column: Partial) { + const columns = deepClone(deckStore.state.columns); + const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); + const currentColumn = deepClone(deckStore.state.columns[columnIndex]); + if (currentColumn == null) return; + for (const [k, v] of Object.entries(column)) { + currentColumn[k] = v; + } + columns[columnIndex] = currentColumn; + deckStore.set('columns', columns); + saveDeck(); +} diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue new file mode 100644 index 0000000000..75b018cacd --- /dev/null +++ b/packages/frontend/src/ui/deck/direct-column.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue new file mode 100644 index 0000000000..d9f3f7b4e7 --- /dev/null +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue new file mode 100644 index 0000000000..0c66172397 --- /dev/null +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -0,0 +1,68 @@ + + + diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue new file mode 100644 index 0000000000..16962956a0 --- /dev/null +++ b/packages/frontend/src/ui/deck/mentions-column.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue new file mode 100644 index 0000000000..9d133035fe --- /dev/null +++ b/packages/frontend/src/ui/deck/notifications-column.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue new file mode 100644 index 0000000000..49b29145ff --- /dev/null +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue new file mode 100644 index 0000000000..fc61d18ff6 --- /dev/null +++ b/packages/frontend/src/ui/deck/widgets-column.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue new file mode 100644 index 0000000000..b91bf476e8 --- /dev/null +++ b/packages/frontend/src/ui/universal.vue @@ -0,0 +1,390 @@ + + + + + + + diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue new file mode 100644 index 0000000000..33fb492836 --- /dev/null +++ b/packages/frontend/src/ui/universal.widgets.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue new file mode 100644 index 0000000000..ec9150d346 --- /dev/null +++ b/packages/frontend/src/ui/visitor.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/frontend/src/ui/visitor/a.vue b/packages/frontend/src/ui/visitor/a.vue new file mode 100644 index 0000000000..f8db7a9d09 --- /dev/null +++ b/packages/frontend/src/ui/visitor/a.vue @@ -0,0 +1,259 @@ + + + + + + + diff --git a/packages/frontend/src/ui/visitor/b.vue b/packages/frontend/src/ui/visitor/b.vue new file mode 100644 index 0000000000..275008a8f8 --- /dev/null +++ b/packages/frontend/src/ui/visitor/b.vue @@ -0,0 +1,248 @@ + + + + + + + diff --git a/packages/frontend/src/ui/visitor/header.vue b/packages/frontend/src/ui/visitor/header.vue new file mode 100644 index 0000000000..7300b12a75 --- /dev/null +++ b/packages/frontend/src/ui/visitor/header.vue @@ -0,0 +1,228 @@ + + + + + diff --git a/packages/frontend/src/ui/visitor/kanban.vue b/packages/frontend/src/ui/visitor/kanban.vue new file mode 100644 index 0000000000..51e47f277d --- /dev/null +++ b/packages/frontend/src/ui/visitor/kanban.vue @@ -0,0 +1,257 @@ + + + + + + diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue new file mode 100644 index 0000000000..84c96a1dae --- /dev/null +++ b/packages/frontend/src/ui/zen.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/frontend/src/widgets/activity.calendar.vue b/packages/frontend/src/widgets/activity.calendar.vue new file mode 100644 index 0000000000..84f6af1c13 --- /dev/null +++ b/packages/frontend/src/widgets/activity.calendar.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/packages/frontend/src/widgets/activity.chart.vue b/packages/frontend/src/widgets/activity.chart.vue new file mode 100644 index 0000000000..b61e419f94 --- /dev/null +++ b/packages/frontend/src/widgets/activity.chart.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/packages/frontend/src/widgets/activity.vue b/packages/frontend/src/widgets/activity.vue new file mode 100644 index 0000000000..238a05ca09 --- /dev/null +++ b/packages/frontend/src/widgets/activity.vue @@ -0,0 +1,90 @@ + + + diff --git a/packages/frontend/src/widgets/aichan.vue b/packages/frontend/src/widgets/aichan.vue new file mode 100644 index 0000000000..828490fd9c --- /dev/null +++ b/packages/frontend/src/widgets/aichan.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/frontend/src/widgets/aiscript.vue b/packages/frontend/src/widgets/aiscript.vue new file mode 100644 index 0000000000..4009edb8b8 --- /dev/null +++ b/packages/frontend/src/widgets/aiscript.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/packages/frontend/src/widgets/button.vue b/packages/frontend/src/widgets/button.vue new file mode 100644 index 0000000000..f0148d7f4e --- /dev/null +++ b/packages/frontend/src/widgets/button.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/packages/frontend/src/widgets/calendar.vue b/packages/frontend/src/widgets/calendar.vue new file mode 100644 index 0000000000..99bd36e2fc --- /dev/null +++ b/packages/frontend/src/widgets/calendar.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/packages/frontend/src/widgets/clock.vue b/packages/frontend/src/widgets/clock.vue new file mode 100644 index 0000000000..dc99b6631e --- /dev/null +++ b/packages/frontend/src/widgets/clock.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/packages/frontend/src/widgets/digital-clock.vue b/packages/frontend/src/widgets/digital-clock.vue new file mode 100644 index 0000000000..d2bfd523f3 --- /dev/null +++ b/packages/frontend/src/widgets/digital-clock.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/packages/frontend/src/widgets/federation.vue b/packages/frontend/src/widgets/federation.vue new file mode 100644 index 0000000000..3374783b0c --- /dev/null +++ b/packages/frontend/src/widgets/federation.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts new file mode 100644 index 0000000000..39826f13c8 --- /dev/null +++ b/packages/frontend/src/widgets/index.ts @@ -0,0 +1,53 @@ +import { App, defineAsyncComponent } from 'vue'; + +export default function(app: App) { + app.component('MkwMemo', defineAsyncComponent(() => import('./memo.vue'))); + app.component('MkwNotifications', defineAsyncComponent(() => import('./notifications.vue'))); + app.component('MkwTimeline', defineAsyncComponent(() => import('./timeline.vue'))); + app.component('MkwCalendar', defineAsyncComponent(() => import('./calendar.vue'))); + app.component('MkwRss', defineAsyncComponent(() => import('./rss.vue'))); + app.component('MkwRssTicker', defineAsyncComponent(() => import('./rss-ticker.vue'))); + app.component('MkwTrends', defineAsyncComponent(() => import('./trends.vue'))); + app.component('MkwClock', defineAsyncComponent(() => import('./clock.vue'))); + app.component('MkwActivity', defineAsyncComponent(() => import('./activity.vue'))); + app.component('MkwPhotos', defineAsyncComponent(() => import('./photos.vue'))); + app.component('MkwDigitalClock', defineAsyncComponent(() => import('./digital-clock.vue'))); + app.component('MkwUnixClock', defineAsyncComponent(() => import('./unix-clock.vue'))); + app.component('MkwFederation', defineAsyncComponent(() => import('./federation.vue'))); + app.component('MkwPostForm', defineAsyncComponent(() => import('./post-form.vue'))); + app.component('MkwSlideshow', defineAsyncComponent(() => import('./slideshow.vue'))); + app.component('MkwServerMetric', defineAsyncComponent(() => import('./server-metric/index.vue'))); + app.component('MkwOnlineUsers', defineAsyncComponent(() => import('./online-users.vue'))); + app.component('MkwJobQueue', defineAsyncComponent(() => import('./job-queue.vue'))); + app.component('MkwInstanceCloud', defineAsyncComponent(() => import('./instance-cloud.vue'))); + app.component('MkwButton', defineAsyncComponent(() => import('./button.vue'))); + app.component('MkwAiscript', defineAsyncComponent(() => import('./aiscript.vue'))); + app.component('MkwAichan', defineAsyncComponent(() => import('./aichan.vue'))); + app.component('MkwUserList', defineAsyncComponent(() => import('./user-list.vue'))); +} + +export const widgets = [ + 'memo', + 'notifications', + 'timeline', + 'calendar', + 'rss', + 'rssTicker', + 'trends', + 'clock', + 'activity', + 'photos', + 'digitalClock', + 'unixClock', + 'federation', + 'instanceCloud', + 'postForm', + 'slideshow', + 'serverMetric', + 'onlineUsers', + 'jobQueue', + 'button', + 'aiscript', + 'aichan', + 'userList', +]; diff --git a/packages/frontend/src/widgets/instance-cloud.vue b/packages/frontend/src/widgets/instance-cloud.vue new file mode 100644 index 0000000000..4965616995 --- /dev/null +++ b/packages/frontend/src/widgets/instance-cloud.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/packages/frontend/src/widgets/job-queue.vue b/packages/frontend/src/widgets/job-queue.vue new file mode 100644 index 0000000000..9f19c51825 --- /dev/null +++ b/packages/frontend/src/widgets/job-queue.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/packages/frontend/src/widgets/memo.vue b/packages/frontend/src/widgets/memo.vue new file mode 100644 index 0000000000..1cc0e10bba --- /dev/null +++ b/packages/frontend/src/widgets/memo.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/packages/frontend/src/widgets/notifications.vue b/packages/frontend/src/widgets/notifications.vue new file mode 100644 index 0000000000..e697209444 --- /dev/null +++ b/packages/frontend/src/widgets/notifications.vue @@ -0,0 +1,70 @@ + + + diff --git a/packages/frontend/src/widgets/online-users.vue b/packages/frontend/src/widgets/online-users.vue new file mode 100644 index 0000000000..e9ab79b111 --- /dev/null +++ b/packages/frontend/src/widgets/online-users.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/packages/frontend/src/widgets/photos.vue b/packages/frontend/src/widgets/photos.vue new file mode 100644 index 0000000000..4ad5324053 --- /dev/null +++ b/packages/frontend/src/widgets/photos.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/frontend/src/widgets/post-form.vue b/packages/frontend/src/widgets/post-form.vue new file mode 100644 index 0000000000..f1708775ba --- /dev/null +++ b/packages/frontend/src/widgets/post-form.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/frontend/src/widgets/rss-ticker.vue b/packages/frontend/src/widgets/rss-ticker.vue new file mode 100644 index 0000000000..44c21d1836 --- /dev/null +++ b/packages/frontend/src/widgets/rss-ticker.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/packages/frontend/src/widgets/rss.vue b/packages/frontend/src/widgets/rss.vue new file mode 100644 index 0000000000..c0338c8e47 --- /dev/null +++ b/packages/frontend/src/widgets/rss.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue new file mode 100644 index 0000000000..80a8e427e1 --- /dev/null +++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue new file mode 100644 index 0000000000..e7b2226d1f --- /dev/null +++ b/packages/frontend/src/widgets/server-metric/cpu.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/packages/frontend/src/widgets/server-metric/disk.vue b/packages/frontend/src/widgets/server-metric/disk.vue new file mode 100644 index 0000000000..3d22d05383 --- /dev/null +++ b/packages/frontend/src/widgets/server-metric/disk.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue new file mode 100644 index 0000000000..bc3fca6fc1 --- /dev/null +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -0,0 +1,87 @@ + + + diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue new file mode 100644 index 0000000000..6018eb4265 --- /dev/null +++ b/packages/frontend/src/widgets/server-metric/mem.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue new file mode 100644 index 0000000000..ab8b0fe471 --- /dev/null +++ b/packages/frontend/src/widgets/server-metric/net.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/packages/frontend/src/widgets/server-metric/pie.vue b/packages/frontend/src/widgets/server-metric/pie.vue new file mode 100644 index 0000000000..868dbc0484 --- /dev/null +++ b/packages/frontend/src/widgets/server-metric/pie.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/packages/frontend/src/widgets/slideshow.vue b/packages/frontend/src/widgets/slideshow.vue new file mode 100644 index 0000000000..e317b8ab94 --- /dev/null +++ b/packages/frontend/src/widgets/slideshow.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/packages/frontend/src/widgets/timeline.vue b/packages/frontend/src/widgets/timeline.vue new file mode 100644 index 0000000000..e48444d33f --- /dev/null +++ b/packages/frontend/src/widgets/timeline.vue @@ -0,0 +1,129 @@ + + + diff --git a/packages/frontend/src/widgets/trends.vue b/packages/frontend/src/widgets/trends.vue new file mode 100644 index 0000000000..02eec0431e --- /dev/null +++ b/packages/frontend/src/widgets/trends.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/packages/frontend/src/widgets/unix-clock.vue b/packages/frontend/src/widgets/unix-clock.vue new file mode 100644 index 0000000000..cf85ac782c --- /dev/null +++ b/packages/frontend/src/widgets/unix-clock.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/packages/frontend/src/widgets/user-list.vue b/packages/frontend/src/widgets/user-list.vue new file mode 100644 index 0000000000..9ffbf0d8e3 --- /dev/null +++ b/packages/frontend/src/widgets/user-list.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts new file mode 100644 index 0000000000..8bd56a5966 --- /dev/null +++ b/packages/frontend/src/widgets/widget.ts @@ -0,0 +1,73 @@ +import { reactive, watch } from 'vue'; +import { throttle } from 'throttle-debounce'; +import { Form, GetFormResultType } from '@/scripts/form'; +import * as os from '@/os'; +import { deepClone } from '@/scripts/clone'; + +export type Widget

> = { + id: string; + data: Partial

; +}; + +export type WidgetComponentProps

> = { + widget?: Widget

; +}; + +export type WidgetComponentEmits

> = { + (ev: 'updateProps', props: P); +}; + +export type WidgetComponentExpose = { + name: string; + id: string | null; + configure: () => void; +}; + +export const useWidgetPropsManager = >( + name: string, + propsDef: F, + props: Readonly>>, + emit: WidgetComponentEmits>, +): { + widgetProps: GetFormResultType; + save: () => void; + configure: () => void; +} => { + const widgetProps = reactive(props.widget ? deepClone(props.widget.data) : {}); + + const mergeProps = () => { + for (const prop of Object.keys(propsDef)) { + if (typeof widgetProps[prop] === 'undefined') { + widgetProps[prop] = propsDef[prop].default; + } + } + }; + watch(widgetProps, () => { + mergeProps(); + }, { deep: true, immediate: true }); + + const save = throttle(3000, () => { + emit('updateProps', widgetProps); + }); + + const configure = async () => { + const form = deepClone(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, + }; +}; diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json new file mode 100644 index 0000000000..86109f600a --- /dev/null +++ b/packages/frontend/tsconfig.json @@ -0,0 +1,47 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmitOnError": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": false, + "target": "es2017", + "module": "esnext", + "moduleResolution": "node", + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + }, + "typeRoots": [ + "node_modules/@types", + "@types", + ], + "types": [ + "vite/client", + ], + "lib": [ + "esnext", + "dom" + ], + "jsx": "preserve" + }, + "compileOnSave": false, + "include": [ + ".eslintrc.js", + "./**/*.ts", + "./**/*.vue" + ] +} diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts new file mode 100644 index 0000000000..1acf5301b7 --- /dev/null +++ b/packages/frontend/vite.config.ts @@ -0,0 +1,70 @@ +import pluginVue from '@vitejs/plugin-vue'; +import { defineConfig } from 'vite'; + +import locales from '../../locales'; +import meta from '../../package.json'; +import pluginJson5 from './vite.json5'; + +const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; + +export default defineConfig(({ command, mode }) => { + + return { + base: '/vite/', + + plugins: [ + pluginVue({ + reactivityTransform: true, + }), + pluginJson5(), + ], + + resolve: { + extensions, + alias: { + '@/': __dirname + '/src/', + '/client-assets/': __dirname + '/assets/', + '/static-assets/': __dirname + '/../backend/assets/', + }, + }, + + define: { + _VERSION_: JSON.stringify(meta.version), + _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _ENV_: JSON.stringify(process.env.NODE_ENV), + _DEV_: process.env.NODE_ENV !== 'production', + _PERF_PREFIX_: JSON.stringify('Misskey:'), + _DATA_TRANSFER_DRIVE_FILE_: JSON.stringify('mk_drive_file'), + _DATA_TRANSFER_DRIVE_FOLDER_: JSON.stringify('mk_drive_folder'), + _DATA_TRANSFER_DECK_COLUMN_: JSON.stringify('mk_deck_column'), + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + }, + + build: { + target: [ + 'chrome100', + 'firefox100', + 'safari15', + 'es2017', // TODO: そのうち消す + ], + manifest: 'manifest.json', + rollupOptions: { + input: { + app: './src/init.ts', + }, + output: { + manualChunks: { + vue: ['vue'], + }, + }, + }, + cssCodeSplit: true, + outDir: __dirname + '/../../built/_vite_', + assetsDir: '.', + emptyOutDir: false, + sourcemap: process.env.NODE_ENV === 'development', + reportCompressedSize: false, + }, + }; +}); diff --git a/packages/frontend/vite.json5.ts b/packages/frontend/vite.json5.ts new file mode 100644 index 0000000000..0a37fbff44 --- /dev/null +++ b/packages/frontend/vite.json5.ts @@ -0,0 +1,38 @@ +// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json + +import JSON5 from 'json5'; +import { Plugin } from 'rollup'; +import { createFilter, dataToEsm } from '@rollup/pluginutils'; +import { RollupJsonOptions } from '@rollup/plugin-json'; + +export default function json5(options: RollupJsonOptions = {}): Plugin { + const filter = createFilter(options.include, options.exclude); + const indent = 'indent' in options ? options.indent : '\t'; + + return { + name: 'json5', + + // eslint-disable-next-line no-shadow + transform(json, id) { + if (id.slice(-6) !== '.json5' || !filter(id)) return null; + + try { + const parsed = JSON5.parse(json); + return { + code: dataToEsm(parsed, { + preferConst: options.preferConst, + compact: options.compact, + namedExports: options.namedExports, + indent, + }), + map: { mappings: '' }, + }; + } catch (err) { + const message = 'Could not parse JSON file'; + const position = parseInt(/[\d]/.exec(err.message)[0], 10); + this.warn({ message, id, position }); + return null; + } + }, + }; +} -- cgit v1.2.3-freya