summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend')
-rw-r--r--packages/frontend/.storybook/changes.ts2
-rw-r--r--packages/frontend/.storybook/fakes.ts2
-rw-r--r--packages/frontend/.storybook/generate.tsx2
-rw-r--r--packages/frontend/.storybook/main.ts36
-rw-r--r--packages/frontend/.storybook/manager.ts2
-rw-r--r--packages/frontend/.storybook/mocks.ts34
-rw-r--r--packages/frontend/.storybook/preload-locale.ts2
-rw-r--r--packages/frontend/.storybook/preload-theme.ts2
-rw-r--r--packages/frontend/.storybook/preview.ts4
-rw-r--r--packages/frontend/@types/global.d.ts7
-rw-r--r--packages/frontend/@types/theme.d.ts2
-rw-r--r--packages/frontend/assets/drop-and-fusion/bgm_1.mp3bin0 -> 1637271 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/click.mp3bin0 -> 26496 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/collision.mp3bin0 -> 18240 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/collision_yen.mp3bin0 -> 7807 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/drop-arrow.svg6
-rw-r--r--packages/frontend/assets/drop-and-fusion/drop.mp3bin0 -> 18240 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/drop_yen.mp3bin0 -> 5850 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/dropper.pngbin0 -> 32415 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/frame-dark.svg28
-rw-r--r--packages/frontend/assets/drop-and-fusion/frame-light.svg28
-rw-r--r--packages/frontend/assets/drop-and-fusion/fusion.mp3bin0 -> 19328 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/fusion_yen.mp3bin0 -> 7807 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/gameover.mp3bin0 -> 31346 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/gameover.pngbin0 -> 67156 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/gameover_yen.mp3bin0 -> 46392 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/go.pngbin0 -> 31115 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/hold.mp3bin0 -> 21941 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/logo.pngbin0 -> 254016 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/cold_face.pngbin0 -> 40776 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/exploding_head.pngbin0 -> 47230 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/face_with_open_mouth.pngbin0 -> 36399 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.pngbin0 -> 40322 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/grinning_squinting_face.pngbin0 -> 41020 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/heart_suit.pngbin0 -> 22437 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/pleading_face.pngbin0 -> 44074 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_hearts.pngbin0 -> 52432 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_sunglasses.pngbin0 -> 47859 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/normal_monos/zany_face.pngbin0 -> 44995 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/ready.pngbin0 -> 34674 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_1.pngbin0 -> 29193 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_10.pngbin0 -> 33717 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_2.pngbin0 -> 32324 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_3.pngbin0 -> 33127 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_4.pngbin0 -> 31182 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_5.pngbin0 -> 32745 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_6.pngbin0 -> 32100 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_7.pngbin0 -> 31318 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_8.pngbin0 -> 32886 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/square_monos/keycap_9.pngbin0 -> 32483 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/candy_color.svg86
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg316
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/cookie_color.svg116
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/custard_color.svg22
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/doughnut_color.svg272
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/lollipop_color.svg112
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/pancakes_color.svg92
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg161
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/shortcake_color.svg48
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg140
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/candy_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/chocolate_bar_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/custard_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/lollipop_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/pancakes_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shaved_ice_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shortcake_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/sweets_monos/verts/soft_ice_cream_color.svg5
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/10000yen.pngbin0 -> 93558 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/1000yen.pngbin0 -> 98316 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/100yen.pngbin0 -> 55276 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/10yen.pngbin0 -> 67485 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/1yen.pngbin0 -> 57534 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/2000yen.pngbin0 -> 88045 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/5000yen.pngbin0 -> 94334 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/500yen.pngbin0 -> 67547 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/50yen.pngbin0 -> 41915 bytes
-rw-r--r--packages/frontend/assets/drop-and-fusion/yen_monos/5yen.pngbin0 -> 60516 bytes
-rw-r--r--packages/frontend/assets/reversi/logo.pngbin0 -> 185387 bytes
-rw-r--r--packages/frontend/assets/reversi/lose.mp3bin0 -> 9565 bytes
-rw-r--r--packages/frontend/assets/reversi/matched.mp3bin0 -> 43884 bytes
-rw-r--r--packages/frontend/assets/reversi/put.mp3bin0 -> 5014 bytes
-rw-r--r--packages/frontend/assets/reversi/stone_b.pngbin0 -> 10794 bytes
-rw-r--r--packages/frontend/assets/reversi/stone_w.pngbin0 -> 11546 bytes
-rw-r--r--packages/frontend/assets/reversi/win.mp3bin0 -> 25703 bytes
-rw-r--r--packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts2
-rw-r--r--packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts2
-rw-r--r--packages/frontend/package.json106
-rw-r--r--packages/frontend/public/mockServiceWorker.js2
-rw-r--r--packages/frontend/src/_boot_.ts2
-rw-r--r--packages/frontend/src/_dev_boot_.ts2
-rw-r--r--packages/frontend/src/account.ts14
-rw-r--r--packages/frontend/src/boot/common.ts16
-rw-r--r--packages/frontend/src/boot/main-boot.ts44
-rw-r--r--packages/frontend/src/boot/sub-boot.ts2
-rw-r--r--packages/frontend/src/cache.ts12
-rw-r--r--packages/frontend/src/components/MkAbuseReport.stories.impl.ts10
-rw-r--r--packages/frontend/src/components/MkAbuseReport.vue6
-rw-r--r--packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts10
-rw-r--r--packages/frontend/src/components/MkAbuseReportWindow.vue4
-rw-r--r--packages/frontend/src/components/MkAccountMoved.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkAccountMoved.vue6
-rw-r--r--packages/frontend/src/components/MkAchievements.stories.impl.ts12
-rw-r--r--packages/frontend/src/components/MkAchievements.vue7
-rw-r--r--packages/frontend/src/components/MkAnalogClock.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkAnalogClock.vue2
-rw-r--r--packages/frontend/src/components/MkAnimBg.vue2
-rw-r--r--packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkAnnouncementDialog.vue11
-rw-r--r--packages/frontend/src/components/MkAsUi.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkAsUi.vue44
-rw-r--r--packages/frontend/src/components/MkAutocomplete.stories.impl.ts19
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue43
-rw-r--r--packages/frontend/src/components/MkAvatars.stories.impl.ts10
-rw-r--r--packages/frontend/src/components/MkAvatars.vue6
-rw-r--r--packages/frontend/src/components/MkButton.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkButton.vue8
-rw-r--r--packages/frontend/src/components/MkCaptcha.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkCaptcha.vue46
-rw-r--r--packages/frontend/src/components/MkChannelFollowButton.vue8
-rw-r--r--packages/frontend/src/components/MkChannelList.vue2
-rw-r--r--packages/frontend/src/components/MkChannelPreview.vue2
-rw-r--r--packages/frontend/src/components/MkChart.vue69
-rw-r--r--packages/frontend/src/components/MkChartLegend.vue16
-rw-r--r--packages/frontend/src/components/MkChartTooltip.vue2
-rw-r--r--packages/frontend/src/components/MkClickerGame.vue2
-rw-r--r--packages/frontend/src/components/MkClipPreview.vue2
-rw-r--r--packages/frontend/src/components/MkCode.core.vue76
-rw-r--r--packages/frontend/src/components/MkCode.vue70
-rw-r--r--packages/frontend/src/components/MkCodeEditor.vue22
-rw-r--r--packages/frontend/src/components/MkCodeInline.vue25
-rw-r--r--packages/frontend/src/components/MkColorInput.vue6
-rw-r--r--packages/frontend/src/components/MkContainer.vue2
-rw-r--r--packages/frontend/src/components/MkContextMenu.vue12
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue19
-rw-r--r--packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue104
-rw-r--r--packages/frontend/src/components/MkCwButton.vue26
-rw-r--r--packages/frontend/src/components/MkDateSeparatedList.vue46
-rw-r--r--packages/frontend/src/components/MkDialog.vue8
-rw-r--r--packages/frontend/src/components/MkDigitalClock.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkDigitalClock.vue2
-rw-r--r--packages/frontend/src/components/MkDonation.vue2
-rw-r--r--packages/frontend/src/components/MkDrive.file.vue4
-rw-r--r--packages/frontend/src/components/MkDrive.folder.vue13
-rw-r--r--packages/frontend/src/components/MkDrive.navFolder.vue8
-rw-r--r--packages/frontend/src/components/MkDrive.vue38
-rw-r--r--packages/frontend/src/components/MkDriveFileThumbnail.vue2
-rw-r--r--packages/frontend/src/components/MkDriveSelectDialog.vue2
-rw-r--r--packages/frontend/src/components/MkDriveWindow.vue2
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.section.vue8
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.vue21
-rw-r--r--packages/frontend/src/components/MkEmojiPickerDialog.vue5
-rw-r--r--packages/frontend/src/components/MkEmojiPickerWindow.vue6
-rw-r--r--packages/frontend/src/components/MkFeaturedPhotos.vue6
-rw-r--r--packages/frontend/src/components/MkFileCaptionEditWindow.vue6
-rw-r--r--packages/frontend/src/components/MkFileListForAdmin.vue4
-rw-r--r--packages/frontend/src/components/MkFlashPreview.vue12
-rw-r--r--packages/frontend/src/components/MkFoldableSection.vue51
-rw-r--r--packages/frontend/src/components/MkFolder.vue6
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue15
-rw-r--r--packages/frontend/src/components/MkForgotPassword.vue6
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue18
-rw-r--r--packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts5
-rw-r--r--packages/frontend/src/components/MkGalleryPostPreview.vue6
-rw-r--r--packages/frontend/src/components/MkGoogle.vue2
-rw-r--r--packages/frontend/src/components/MkHeatmap.vue69
-rw-r--r--packages/frontend/src/components/MkHorizontalSwipe.vue239
-rw-r--r--packages/frontend/src/components/MkImgWithBlurhash.vue4
-rw-r--r--packages/frontend/src/components/MkInfo.vue2
-rw-r--r--packages/frontend/src/components/MkInput.vue31
-rw-r--r--packages/frontend/src/components/MkInstanceCardMini.vue6
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue45
-rw-r--r--packages/frontend/src/components/MkInstanceTicker.vue10
-rw-r--r--packages/frontend/src/components/MkInviteCode.stories.impl.ts8
-rw-r--r--packages/frontend/src/components/MkInviteCode.vue2
-rw-r--r--packages/frontend/src/components/MkKeyValue.vue2
-rw-r--r--packages/frontend/src/components/MkLaunchPad.vue9
-rw-r--r--packages/frontend/src/components/MkLink.vue2
-rw-r--r--packages/frontend/src/components/MkMarquee.vue3
-rw-r--r--packages/frontend/src/components/MkMediaAudio.vue361
-rw-r--r--packages/frontend/src/components/MkMediaBanner.vue15
-rw-r--r--packages/frontend/src/components/MkMediaImage.vue2
-rw-r--r--packages/frontend/src/components/MkMediaList.vue19
-rw-r--r--packages/frontend/src/components/MkMediaRange.vue152
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue530
-rw-r--r--packages/frontend/src/components/MkMention.vue2
-rw-r--r--packages/frontend/src/components/MkMenu.child.vue7
-rw-r--r--packages/frontend/src/components/MkMenu.vue32
-rw-r--r--packages/frontend/src/components/MkMiniChart.vue6
-rw-r--r--packages/frontend/src/components/MkModal.vue2
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue6
-rw-r--r--packages/frontend/src/components/MkNote.vue124
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue82
-rw-r--r--packages/frontend/src/components/MkNoteHeader.vue4
-rw-r--r--packages/frontend/src/components/MkNotePreview.vue12
-rw-r--r--packages/frontend/src/components/MkNoteSimple.vue2
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue6
-rw-r--r--packages/frontend/src/components/MkNotes.vue2
-rw-r--r--packages/frontend/src/components/MkNotification.vue51
-rw-r--r--packages/frontend/src/components/MkNotificationSelectWindow.vue4
-rw-r--r--packages/frontend/src/components/MkNotifications.vue6
-rw-r--r--packages/frontend/src/components/MkNumber.vue23
-rw-r--r--packages/frontend/src/components/MkNumberDiff.vue2
-rw-r--r--packages/frontend/src/components/MkObjectView.value.vue2
-rw-r--r--packages/frontend/src/components/MkObjectView.vue2
-rw-r--r--packages/frontend/src/components/MkOmit.vue6
-rw-r--r--packages/frontend/src/components/MkPagePreview.vue4
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue68
-rw-r--r--packages/frontend/src/components/MkPagination.vue9
-rw-r--r--packages/frontend/src/components/MkPasswordDialog.vue6
-rw-r--r--packages/frontend/src/components/MkPlusOneEffect.vue7
-rw-r--r--packages/frontend/src/components/MkPoll.vue47
-rw-r--r--packages/frontend/src/components/MkPollEditor.vue46
-rw-r--r--packages/frontend/src/components/MkPopupMenu.vue2
-rw-r--r--packages/frontend/src/components/MkPostForm.vue116
-rw-r--r--packages/frontend/src/components/MkPostFormAttaches.vue35
-rw-r--r--packages/frontend/src/components/MkPostFormDialog.vue14
-rw-r--r--packages/frontend/src/components/MkPullToRefresh.vue9
-rw-r--r--packages/frontend/src/components/MkPushNotificationAllowButton.vue11
-rw-r--r--packages/frontend/src/components/MkRadio.vue2
-rw-r--r--packages/frontend/src/components/MkRadios.vue7
-rw-r--r--packages/frontend/src/components/MkRange.vue8
-rw-r--r--packages/frontend/src/components/MkReactionEffect.vue2
-rw-r--r--packages/frontend/src/components/MkReactionIcon.vue2
-rw-r--r--packages/frontend/src/components/MkReactionTooltip.vue2
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.details.vue2
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.reaction.vue50
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.vue2
-rw-r--r--packages/frontend/src/components/MkRemoteCaution.vue2
-rw-r--r--packages/frontend/src/components/MkRetentionHeatmap.vue36
-rw-r--r--packages/frontend/src/components/MkRetentionLineChart.vue18
-rw-r--r--packages/frontend/src/components/MkRippleEffect.vue11
-rw-r--r--packages/frontend/src/components/MkRolePreview.vue2
-rw-r--r--packages/frontend/src/components/MkSelect.vue45
-rw-r--r--packages/frontend/src/components/MkSignin.vue12
-rw-r--r--packages/frontend/src/components/MkSigninDialog.vue2
-rw-r--r--packages/frontend/src/components/MkSignupDialog.form.vue20
-rw-r--r--packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts5
-rw-r--r--packages/frontend/src/components/MkSignupDialog.rules.vue14
-rw-r--r--packages/frontend/src/components/MkSignupDialog.vue16
-rw-r--r--packages/frontend/src/components/MkSourceCodeAvailablePopup.vue112
-rw-r--r--packages/frontend/src/components/MkSparkle.vue9
-rw-r--r--packages/frontend/src/components/MkSubNoteContent.vue10
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue2
-rw-r--r--packages/frontend/src/components/MkSwitch.button.vue4
-rw-r--r--packages/frontend/src/components/MkSwitch.vue2
-rw-r--r--packages/frontend/src/components/MkTab.vue14
-rw-r--r--packages/frontend/src/components/MkTagCloud.vue4
-rw-r--r--packages/frontend/src/components/MkTextarea.vue22
-rw-r--r--packages/frontend/src/components/MkTimeline.vue42
-rw-r--r--packages/frontend/src/components/MkToast.vue2
-rw-r--r--packages/frontend/src/components/MkTokenGenerateWindow.vue70
-rw-r--r--packages/frontend/src/components/MkTooltip.vue9
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.Note.vue7
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.PostNote.vue4
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.Sensitive.vue4
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.Timeline.vue2
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.vue4
-rw-r--r--packages/frontend/src/components/MkUpdated.vue12
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue12
-rw-r--r--packages/frontend/src/components/MkUrlPreviewPopup.vue2
-rw-r--r--packages/frontend/src/components/MkUserAnnouncementEditDialog.vue39
-rw-r--r--packages/frontend/src/components/MkUserCardMini.vue110
-rw-r--r--packages/frontend/src/components/MkUserInfo.vue2
-rw-r--r--packages/frontend/src/components/MkUserList.vue2
-rw-r--r--packages/frontend/src/components/MkUserOnlineIndicator.vue2
-rw-r--r--packages/frontend/src/components/MkUserPopup.vue6
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue84
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts16
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue30
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Privacy.vue6
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.vue8
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.User.vue6
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts16
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.vue6
-rw-r--r--packages/frontend/src/components/MkUsersTooltip.vue2
-rw-r--r--packages/frontend/src/components/MkVisibilityPicker.vue4
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue23
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.vue15
-rw-r--r--packages/frontend/src/components/MkWaitingDialog.vue4
-rw-r--r--packages/frontend/src/components/MkWidgets.vue14
-rw-r--r--packages/frontend/src/components/MkWindow.vue83
-rw-r--r--packages/frontend/src/components/MkYouTubePlayer.vue4
-rw-r--r--packages/frontend/src/components/form/link.vue2
-rw-r--r--packages/frontend/src/components/form/section.vue2
-rw-r--r--packages/frontend/src/components/form/slot.vue2
-rw-r--r--packages/frontend/src/components/form/split.vue2
-rw-r--r--packages/frontend/src/components/form/suspense.vue2
-rw-r--r--packages/frontend/src/components/global/I18n.vue46
-rw-r--r--packages/frontend/src/components/global/MkA.stories.impl.ts5
-rw-r--r--packages/frontend/src/components/global/MkA.vue4
-rw-r--r--packages/frontend/src/components/global/MkAcct.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkAcct.vue4
-rw-r--r--packages/frontend/src/components/global/MkAd.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkAd.vue2
-rw-r--r--packages/frontend/src/components/global/MkAvatar.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue18
-rw-r--r--packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkCondensedLine.vue2
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkCustomEmoji.vue22
-rw-r--r--packages/frontend/src/components/global/MkEllipsis.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkEllipsis.vue2
-rw-r--r--packages/frontend/src/components/global/MkEmoji.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkEmoji.vue14
-rw-r--r--packages/frontend/src/components/global/MkError.stories.impl.ts5
-rw-r--r--packages/frontend/src/components/global/MkError.stories.meta.ts2
-rw-r--r--packages/frontend/src/components/global/MkError.vue2
-rw-r--r--packages/frontend/src/components/global/MkFooterSpacer.vue2
-rw-r--r--packages/frontend/src/components/global/MkLazy.vue2
-rw-r--r--packages/frontend/src/components/global/MkLoading.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkLoading.vue2
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts5
-rw-r--r--packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts61
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.stories.impl.ts4
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue14
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue22
-rw-r--r--packages/frontend/src/components/global/MkSpacer.vue2
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.vue25
-rw-r--r--packages/frontend/src/components/global/MkTime.stories.impl.ts14
-rw-r--r--packages/frontend/src/components/global/MkTime.vue32
-rw-r--r--packages/frontend/src/components/global/MkUrl.stories.impl.ts13
-rw-r--r--packages/frontend/src/components/global/MkUrl.vue2
-rw-r--r--packages/frontend/src/components/global/MkUserName.stories.impl.ts4
-rw-r--r--packages/frontend/src/components/global/MkUserName.vue2
-rw-r--r--packages/frontend/src/components/global/RouterView.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/global/RouterView.vue46
-rw-r--r--packages/frontend/src/components/global/i18n.ts29
-rw-r--r--packages/frontend/src/components/index.ts4
-rw-r--r--packages/frontend/src/components/page/block.type.ts34
-rw-r--r--packages/frontend/src/components/page/page.block.vue5
-rw-r--r--packages/frontend/src/components/page/page.image.vue14
-rw-r--r--packages/frontend/src/components/page/page.note.vue10
-rw-r--r--packages/frontend/src/components/page/page.section.vue5
-rw-r--r--packages/frontend/src/components/page/page.text.vue7
-rw-r--r--packages/frontend/src/components/page/page.vue2
-rw-r--r--packages/frontend/src/config.ts4
-rw-r--r--packages/frontend/src/const.ts28
-rw-r--r--packages/frontend/src/custom-emojis.ts8
-rw-r--r--packages/frontend/src/debug.ts2
-rw-r--r--packages/frontend/src/directives/adaptive-bg.ts2
-rw-r--r--packages/frontend/src/directives/adaptive-border.ts2
-rw-r--r--packages/frontend/src/directives/anim.ts2
-rw-r--r--packages/frontend/src/directives/appear.ts2
-rw-r--r--packages/frontend/src/directives/click-anime.ts2
-rw-r--r--packages/frontend/src/directives/follow-append.ts2
-rw-r--r--packages/frontend/src/directives/get-size.ts2
-rw-r--r--packages/frontend/src/directives/hotkey.ts2
-rw-r--r--packages/frontend/src/directives/index.ts2
-rw-r--r--packages/frontend/src/directives/panel.ts2
-rw-r--r--packages/frontend/src/directives/ripple.ts2
-rw-r--r--packages/frontend/src/directives/tooltip.ts2
-rw-r--r--packages/frontend/src/directives/user-preview.ts2
-rw-r--r--packages/frontend/src/events.ts10
-rw-r--r--packages/frontend/src/filters/bytes.ts6
-rw-r--r--packages/frontend/src/filters/date.ts2
-rw-r--r--packages/frontend/src/filters/hms.ts65
-rw-r--r--packages/frontend/src/filters/kmg.ts9
-rw-r--r--packages/frontend/src/filters/note.ts2
-rw-r--r--packages/frontend/src/filters/number.ts2
-rw-r--r--packages/frontend/src/filters/user.ts2
-rw-r--r--packages/frontend/src/i18n.ts7
-rw-r--r--packages/frontend/src/index.html11
-rw-r--r--packages/frontend/src/instance.ts6
-rw-r--r--packages/frontend/src/local-storage.ts3
-rw-r--r--packages/frontend/src/navbar.ts9
-rw-r--r--packages/frontend/src/nirax.ts218
-rw-r--r--packages/frontend/src/os.ts33
-rw-r--r--packages/frontend/src/pages/_empty_.vue2
-rw-r--r--packages/frontend/src/pages/_error_.vue13
-rw-r--r--packages/frontend/src/pages/_loading_.vue2
-rw-r--r--packages/frontend/src/pages/about-misskey.vue56
-rw-r--r--packages/frontend/src/pages/about.emojis.vue2
-rw-r--r--packages/frontend/src/pages/about.federation.vue2
-rw-r--r--packages/frontend/src/pages/about.vue202
-rw-r--r--packages/frontend/src/pages/achievements.vue6
-rw-r--r--packages/frontend/src/pages/admin-file.vue17
-rw-r--r--packages/frontend/src/pages/admin-user.vue39
-rw-r--r--packages/frontend/src/pages/admin/RolesEditorFormula.vue2
-rw-r--r--packages/frontend/src/pages/admin/_header_.vue14
-rw-r--r--packages/frontend/src/pages/admin/abuses.vue6
-rw-r--r--packages/frontend/src/pages/admin/ads.vue19
-rw-r--r--packages/frontend/src/pages/admin/announcements.vue19
-rw-r--r--packages/frontend/src/pages/admin/bot-protection.vue39
-rw-r--r--packages/frontend/src/pages/admin/branding.vue39
-rw-r--r--packages/frontend/src/pages/admin/database.vue10
-rw-r--r--packages/frontend/src/pages/admin/email-settings.vue9
-rw-r--r--packages/frontend/src/pages/admin/external-services.vue9
-rw-r--r--packages/frontend/src/pages/admin/federation.vue6
-rw-r--r--packages/frontend/src/pages/admin/files.vue11
-rw-r--r--packages/frontend/src/pages/admin/index.vue23
-rw-r--r--packages/frontend/src/pages/admin/instance-block.vue9
-rw-r--r--packages/frontend/src/pages/admin/invites.vue13
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue17
-rw-r--r--packages/frontend/src/pages/admin/modlog.ModLog.vue2
-rw-r--r--packages/frontend/src/pages/admin/modlog.vue6
-rw-r--r--packages/frontend/src/pages/admin/object-storage.vue9
-rw-r--r--packages/frontend/src/pages/admin/other-settings.vue9
-rw-r--r--packages/frontend/src/pages/admin/overview.active-users.vue6
-rw-r--r--packages/frontend/src/pages/admin/overview.ap-requests.vue6
-rw-r--r--packages/frontend/src/pages/admin/overview.federation.vue7
-rw-r--r--packages/frontend/src/pages/admin/overview.heatmap.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.instances.vue6
-rw-r--r--packages/frontend/src/pages/admin/overview.moderators.vue6
-rw-r--r--packages/frontend/src/pages/admin/overview.pie.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.queue.chart.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.queue.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.retention.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.stats.vue12
-rw-r--r--packages/frontend/src/pages/admin/overview.users.vue6
-rw-r--r--packages/frontend/src/pages/admin/overview.vue17
-rw-r--r--packages/frontend/src/pages/admin/proxy-account.vue13
-rw-r--r--packages/frontend/src/pages/admin/queue.chart.chart.vue2
-rw-r--r--packages/frontend/src/pages/admin/queue.chart.vue6
-rw-r--r--packages/frontend/src/pages/admin/queue.vue6
-rw-r--r--packages/frontend/src/pages/admin/relays.vue15
-rw-r--r--packages/frontend/src/pages/admin/roles.edit.vue14
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue2
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue19
-rw-r--r--packages/frontend/src/pages/admin/roles.vue11
-rw-r--r--packages/frontend/src/pages/admin/security.vue41
-rw-r--r--packages/frontend/src/pages/admin/server-rules.vue6
-rw-r--r--packages/frontend/src/pages/admin/settings.vue24
-rw-r--r--packages/frontend/src/pages/admin/users.vue8
-rw-r--r--packages/frontend/src/pages/ads.vue6
-rw-r--r--packages/frontend/src/pages/announcements.vue68
-rw-r--r--packages/frontend/src/pages/antenna-timeline.vue13
-rw-r--r--packages/frontend/src/pages/api-console.vue14
-rw-r--r--packages/frontend/src/pages/auth.form.vue14
-rw-r--r--packages/frontend/src/pages/auth.vue14
-rw-r--r--packages/frontend/src/pages/avatar-decorations.vue13
-rw-r--r--packages/frontend/src/pages/channel-editor.vue20
-rw-r--r--packages/frontend/src/pages/channel.vue98
-rw-r--r--packages/frontend/src/pages/channels.vue85
-rw-r--r--packages/frontend/src/pages/clicker.vue6
-rw-r--r--packages/frontend/src/pages/clip.vue13
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue11
-rw-r--r--packages/frontend/src/pages/drive.file.info.vue9
-rw-r--r--packages/frontend/src/pages/drive.file.notes.vue2
-rw-r--r--packages/frontend/src/pages/drive.file.vue21
-rw-r--r--packages/frontend/src/pages/drive.vue6
-rw-r--r--packages/frontend/src/pages/drop-and-fusion.game.vue1517
-rw-r--r--packages/frontend/src/pages/drop-and-fusion.vue192
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue40
-rw-r--r--packages/frontend/src/pages/emojis.emoji.vue26
-rw-r--r--packages/frontend/src/pages/explore.featured.vue2
-rw-r--r--packages/frontend/src/pages/explore.roles.vue6
-rw-r--r--packages/frontend/src/pages/explore.users.vue8
-rw-r--r--packages/frontend/src/pages/explore.vue17
-rw-r--r--packages/frontend/src/pages/favorites.vue6
-rw-r--r--packages/frontend/src/pages/flash/flash-edit.vue17
-rw-r--r--packages/frontend/src/pages/flash/flash-index.vue49
-rw-r--r--packages/frontend/src/pages/flash/flash.vue41
-rw-r--r--packages/frontend/src/pages/follow-requests.vue12
-rw-r--r--packages/frontend/src/pages/follow.vue13
-rw-r--r--packages/frontend/src/pages/gallery/edit.vue14
-rw-r--r--packages/frontend/src/pages/gallery/index.vue19
-rw-r--r--packages/frontend/src/pages/gallery/post.vue17
-rw-r--r--packages/frontend/src/pages/games.vue34
-rw-r--r--packages/frontend/src/pages/install-extensions.vue (renamed from packages/frontend/src/pages/install-extentions.vue)9
-rw-r--r--packages/frontend/src/pages/instance-info.vue219
-rw-r--r--packages/frontend/src/pages/invite.vue15
-rw-r--r--packages/frontend/src/pages/list.vue13
-rw-r--r--packages/frontend/src/pages/miauth.vue16
-rw-r--r--packages/frontend/src/pages/my-antennas/create.vue8
-rw-r--r--packages/frontend/src/pages/my-antennas/edit.vue12
-rw-r--r--packages/frontend/src/pages/my-antennas/editor.vue11
-rw-r--r--packages/frontend/src/pages/my-antennas/index.vue6
-rw-r--r--packages/frontend/src/pages/my-clips/index.vue39
-rw-r--r--packages/frontend/src/pages/my-lists/index.vue12
-rw-r--r--packages/frontend/src/pages/my-lists/list.vue25
-rw-r--r--packages/frontend/src/pages/not-found.vue6
-rw-r--r--packages/frontend/src/pages/note.vue86
-rw-r--r--packages/frontend/src/pages/notifications.vue36
-rw-r--r--packages/frontend/src/pages/oauth.vue12
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue5
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue6
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue2
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue2
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.blocks.vue2
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.container.vue2
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.vue37
-rw-r--r--packages/frontend/src/pages/page.vue25
-rw-r--r--packages/frontend/src/pages/pages.vue55
-rw-r--r--packages/frontend/src/pages/registry.keys.vue9
-rw-r--r--packages/frontend/src/pages/registry.value.vue9
-rw-r--r--packages/frontend/src/pages/registry.vue9
-rw-r--r--packages/frontend/src/pages/reset-password.vue8
-rw-r--r--packages/frontend/src/pages/reversi/game.board.vue633
-rw-r--r--packages/frontend/src/pages/reversi/game.setting.vue298
-rw-r--r--packages/frontend/src/pages/reversi/game.vue120
-rw-r--r--packages/frontend/src/pages/reversi/index.vue353
-rw-r--r--packages/frontend/src/pages/role.vue12
-rw-r--r--packages/frontend/src/pages/scratchpad.vue22
-rw-r--r--packages/frontend/src/pages/search.note.vue11
-rw-r--r--packages/frontend/src/pages/search.user.vue7
-rw-r--r--packages/frontend/src/pages/search.vue31
-rw-r--r--packages/frontend/src/pages/settings/2fa.qrdialog.vue8
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue10
-rw-r--r--packages/frontend/src/pages/settings/accounts.vue9
-rw-r--r--packages/frontend/src/pages/settings/api.vue9
-rw-r--r--packages/frontend/src/pages/settings/apps.vue12
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.decoration.vue6
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.dialog.vue6
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.vue15
-rw-r--r--packages/frontend/src/pages/settings/custom-css.vue6
-rw-r--r--packages/frontend/src/pages/settings/deck.vue6
-rw-r--r--packages/frontend/src/pages/settings/drive-cleaner.vue9
-rw-r--r--packages/frontend/src/pages/settings/drive.vue20
-rw-r--r--packages/frontend/src/pages/settings/email.vue27
-rw-r--r--packages/frontend/src/pages/settings/emoji-picker.vue8
-rw-r--r--packages/frontend/src/pages/settings/general.vue52
-rw-r--r--packages/frontend/src/pages/settings/import-export.vue45
-rw-r--r--packages/frontend/src/pages/settings/index.vue18
-rw-r--r--packages/frontend/src/pages/settings/migration.vue25
-rw-r--r--packages/frontend/src/pages/settings/mute-block.instance-mute.vue12
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue19
-rw-r--r--packages/frontend/src/pages/settings/mute-block.word-mute.vue4
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue6
-rw-r--r--packages/frontend/src/pages/settings/notifications.notification-config.vue2
-rw-r--r--packages/frontend/src/pages/settings/notifications.vue21
-rw-r--r--packages/frontend/src/pages/settings/other.vue21
-rw-r--r--packages/frontend/src/pages/settings/plugin.install.vue6
-rw-r--r--packages/frontend/src/pages/settings/plugin.vue6
-rw-r--r--packages/frontend/src/pages/settings/preferences-backups.vue11
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue18
-rw-r--r--packages/frontend/src/pages/settings/profile.vue25
-rw-r--r--packages/frontend/src/pages/settings/roles.vue14
-rw-r--r--packages/frontend/src/pages/settings/security.vue9
-rw-r--r--packages/frontend/src/pages/settings/sounds.sound.vue9
-rw-r--r--packages/frontend/src/pages/settings/sounds.vue10
-rw-r--r--packages/frontend/src/pages/settings/statusbar.statusbar.vue2
-rw-r--r--packages/frontend/src/pages/settings/statusbar.vue10
-rw-r--r--packages/frontend/src/pages/settings/theme.install.vue8
-rw-r--r--packages/frontend/src/pages/settings/theme.manage.vue6
-rw-r--r--packages/frontend/src/pages/settings/theme.vue21
-rw-r--r--packages/frontend/src/pages/settings/webhook.edit.vue13
-rw-r--r--packages/frontend/src/pages/settings/webhook.new.vue6
-rw-r--r--packages/frontend/src/pages/settings/webhook.vue6
-rw-r--r--packages/frontend/src/pages/share.vue21
-rw-r--r--packages/frontend/src/pages/signup-complete.vue7
-rw-r--r--packages/frontend/src/pages/tag.vue9
-rw-r--r--packages/frontend/src/pages/theme-editor.vue10
-rw-r--r--packages/frontend/src/pages/timeline.vue144
-rw-r--r--packages/frontend/src/pages/user-list-timeline.vue14
-rw-r--r--packages/frontend/src/pages/user-tag.vue6
-rw-r--r--packages/frontend/src/pages/user/achievements.vue2
-rw-r--r--packages/frontend/src/pages/user/activity.following.vue6
-rw-r--r--packages/frontend/src/pages/user/activity.heatmap.vue219
-rw-r--r--packages/frontend/src/pages/user/activity.notes.vue6
-rw-r--r--packages/frontend/src/pages/user/activity.pv.vue6
-rw-r--r--packages/frontend/src/pages/user/activity.vue6
-rw-r--r--packages/frontend/src/pages/user/clips.vue2
-rw-r--r--packages/frontend/src/pages/user/flashs.vue2
-rw-r--r--packages/frontend/src/pages/user/follow-list.vue2
-rw-r--r--packages/frontend/src/pages/user/followers.vue21
-rw-r--r--packages/frontend/src/pages/user/following.vue21
-rw-r--r--packages/frontend/src/pages/user/gallery.vue2
-rw-r--r--packages/frontend/src/pages/user/home.stories.impl.ts26
-rw-r--r--packages/frontend/src/pages/user/home.vue12
-rw-r--r--packages/frontend/src/pages/user/index.activity.vue2
-rw-r--r--packages/frontend/src/pages/user/index.files.vue6
-rw-r--r--packages/frontend/src/pages/user/index.timeline.vue2
-rw-r--r--packages/frontend/src/pages/user/index.vue61
-rw-r--r--packages/frontend/src/pages/user/lists.vue2
-rw-r--r--packages/frontend/src/pages/user/pages.vue2
-rw-r--r--packages/frontend/src/pages/user/raw.vue2
-rw-r--r--packages/frontend/src/pages/user/reactions.vue2
-rw-r--r--packages/frontend/src/pages/welcome.entrance.a.vue8
-rw-r--r--packages/frontend/src/pages/welcome.setup.vue5
-rw-r--r--packages/frontend/src/pages/welcome.timeline.vue8
-rw-r--r--packages/frontend/src/pages/welcome.vue10
-rw-r--r--packages/frontend/src/pizzax.ts41
-rw-r--r--packages/frontend/src/plugin.ts18
-rw-r--r--packages/frontend/src/router.ts557
-rw-r--r--packages/frontend/src/router/definition.ts598
-rw-r--r--packages/frontend/src/router/main.ts167
-rw-r--r--packages/frontend/src/router/supplier.ts30
-rw-r--r--packages/frontend/src/scripts/achievements.ts18
-rw-r--r--packages/frontend/src/scripts/aiscript/api.ts15
-rw-r--r--packages/frontend/src/scripts/aiscript/ui.ts6
-rw-r--r--packages/frontend/src/scripts/array.ts2
-rw-r--r--packages/frontend/src/scripts/autocomplete.ts45
-rw-r--r--packages/frontend/src/scripts/cache.ts2
-rw-r--r--packages/frontend/src/scripts/chart-legend.ts2
-rw-r--r--packages/frontend/src/scripts/chart-vline.ts2
-rw-r--r--packages/frontend/src/scripts/check-reaction-permissions.ts8
-rw-r--r--packages/frontend/src/scripts/check-word-mute.ts2
-rw-r--r--packages/frontend/src/scripts/clicker-game.ts8
-rw-r--r--packages/frontend/src/scripts/clone.ts8
-rw-r--r--packages/frontend/src/scripts/code-highlighter.ts85
-rw-r--r--packages/frontend/src/scripts/collapsed.ts2
-rw-r--r--packages/frontend/src/scripts/collect-page-vars.ts2
-rw-r--r--packages/frontend/src/scripts/color.ts2
-rw-r--r--packages/frontend/src/scripts/confetti.ts2
-rw-r--r--packages/frontend/src/scripts/contains.ts2
-rw-r--r--packages/frontend/src/scripts/copy-to-clipboard.ts2
-rw-r--r--packages/frontend/src/scripts/device-kind.ts9
-rw-r--r--packages/frontend/src/scripts/emoji-base.ts2
-rw-r--r--packages/frontend/src/scripts/emoji-picker.ts2
-rw-r--r--packages/frontend/src/scripts/emojilist.ts9
-rw-r--r--packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts2
-rw-r--r--packages/frontend/src/scripts/extract-mentions.ts2
-rw-r--r--packages/frontend/src/scripts/extract-url-from-mfm.ts2
-rw-r--r--packages/frontend/src/scripts/focus.ts2
-rw-r--r--packages/frontend/src/scripts/form.ts20
-rw-r--r--packages/frontend/src/scripts/format-time-string.ts2
-rw-r--r--packages/frontend/src/scripts/gen-search-query.ts4
-rw-r--r--packages/frontend/src/scripts/get-account-from-id.ts2
-rw-r--r--packages/frontend/src/scripts/get-drive-file-menu.ts13
-rw-r--r--packages/frontend/src/scripts/get-note-menu.ts85
-rw-r--r--packages/frontend/src/scripts/get-note-summary.ts10
-rw-r--r--packages/frontend/src/scripts/get-user-menu.ts20
-rw-r--r--packages/frontend/src/scripts/get-user-name.ts2
-rw-r--r--packages/frontend/src/scripts/hotkey.ts2
-rw-r--r--packages/frontend/src/scripts/i18n.ts294
-rw-r--r--packages/frontend/src/scripts/idb-proxy.ts2
-rw-r--r--packages/frontend/src/scripts/idle-render.ts2
-rw-r--r--packages/frontend/src/scripts/init-chart.ts2
-rw-r--r--packages/frontend/src/scripts/initialize-sw.ts2
-rw-r--r--packages/frontend/src/scripts/install-plugin.ts5
-rw-r--r--packages/frontend/src/scripts/install-theme.ts2
-rw-r--r--packages/frontend/src/scripts/intl-const.ts6
-rw-r--r--packages/frontend/src/scripts/is-device-darkmode.ts2
-rw-r--r--packages/frontend/src/scripts/isFfVisibleForMe.ts2
-rw-r--r--packages/frontend/src/scripts/keycode.ts2
-rw-r--r--packages/frontend/src/scripts/langmap.ts2
-rw-r--r--packages/frontend/src/scripts/login-id.ts2
-rw-r--r--packages/frontend/src/scripts/lookup-user.ts7
-rw-r--r--packages/frontend/src/scripts/lookup.ts7
-rw-r--r--packages/frontend/src/scripts/media-proxy.ts2
-rw-r--r--packages/frontend/src/scripts/merge.ts35
-rw-r--r--packages/frontend/src/scripts/mfm-function-picker.ts2
-rw-r--r--packages/frontend/src/scripts/misskey-api.ts (renamed from packages/frontend/src/scripts/api.ts)28
-rw-r--r--packages/frontend/src/scripts/navigator.ts2
-rw-r--r--packages/frontend/src/scripts/nyaize.ts2
-rw-r--r--packages/frontend/src/scripts/page-metadata.ts74
-rw-r--r--packages/frontend/src/scripts/physics.ts2
-rw-r--r--packages/frontend/src/scripts/please-login.ts2
-rw-r--r--packages/frontend/src/scripts/popout.ts2
-rw-r--r--packages/frontend/src/scripts/popup-position.ts4
-rw-r--r--packages/frontend/src/scripts/post-message.ts2
-rw-r--r--packages/frontend/src/scripts/reaction-picker.ts8
-rw-r--r--packages/frontend/src/scripts/safe-parse.ts11
-rw-r--r--packages/frontend/src/scripts/safe-uri-decode.ts2
-rw-r--r--packages/frontend/src/scripts/scroll.ts2
-rw-r--r--packages/frontend/src/scripts/select-file.ts5
-rw-r--r--packages/frontend/src/scripts/show-moved-dialog.ts2
-rw-r--r--packages/frontend/src/scripts/show-suspended-dialog.ts2
-rw-r--r--packages/frontend/src/scripts/shuffle.ts2
-rw-r--r--packages/frontend/src/scripts/snowfall-effect.ts26
-rw-r--r--packages/frontend/src/scripts/sound.ts109
-rw-r--r--packages/frontend/src/scripts/sticky-sidebar.ts2
-rw-r--r--packages/frontend/src/scripts/test-utils.ts2
-rw-r--r--packages/frontend/src/scripts/theme-editor.ts2
-rw-r--r--packages/frontend/src/scripts/theme.ts12
-rw-r--r--packages/frontend/src/scripts/time.ts2
-rw-r--r--packages/frontend/src/scripts/timezones.ts2
-rw-r--r--packages/frontend/src/scripts/touch.ts6
-rw-r--r--packages/frontend/src/scripts/unison-reload.ts2
-rw-r--r--packages/frontend/src/scripts/upload.ts4
-rw-r--r--packages/frontend/src/scripts/upload/compress-config.ts6
-rw-r--r--packages/frontend/src/scripts/upload/isWebpSupported.ts2
-rw-r--r--packages/frontend/src/scripts/url.ts2
-rw-r--r--packages/frontend/src/scripts/use-chart-tooltip.ts2
-rw-r--r--packages/frontend/src/scripts/use-document-visibility.ts2
-rw-r--r--packages/frontend/src/scripts/use-interval.ts14
-rw-r--r--packages/frontend/src/scripts/use-leave-guard.ts2
-rw-r--r--packages/frontend/src/scripts/use-note-capture.ts8
-rw-r--r--packages/frontend/src/scripts/use-tooltip.ts2
-rw-r--r--packages/frontend/src/scripts/worker-multi-dispatch.ts2
-rw-r--r--packages/frontend/src/store.ts29
-rw-r--r--packages/frontend/src/stream.ts2
-rw-r--r--packages/frontend/src/style.scss10
-rw-r--r--packages/frontend/src/theme-store.ts10
-rw-r--r--packages/frontend/src/themes/_dark.json54
-rw-r--r--packages/frontend/src/themes/_light.json54
-rw-r--r--packages/frontend/src/type.ts3
-rw-r--r--packages/frontend/src/types/date-separated-list.ts2
-rw-r--r--packages/frontend/src/types/menu.ts6
-rw-r--r--packages/frontend/src/types/page-header.ts2
-rw-r--r--packages/frontend/src/ui/_common_/announcements.vue2
-rw-r--r--packages/frontend/src/ui/_common_/common.ts2
-rw-r--r--packages/frontend/src/ui/_common_/common.vue7
-rw-r--r--packages/frontend/src/ui/_common_/navbar-for-mobile.vue4
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue6
-rw-r--r--packages/frontend/src/ui/_common_/notification.vue2
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-federation.vue6
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-rss.vue2
-rw-r--r--packages/frontend/src/ui/_common_/statusbar-user-list.vue6
-rw-r--r--packages/frontend/src/ui/_common_/statusbars.vue2
-rw-r--r--packages/frontend/src/ui/_common_/stream-indicator.vue2
-rw-r--r--packages/frontend/src/ui/_common_/sw-inject.ts11
-rw-r--r--packages/frontend/src/ui/_common_/upload.vue2
-rw-r--r--packages/frontend/src/ui/classic.header.vue4
-rw-r--r--packages/frontend/src/ui/classic.sidebar.vue4
-rw-r--r--packages/frontend/src/ui/classic.vue22
-rw-r--r--packages/frontend/src/ui/deck.vue14
-rw-r--r--packages/frontend/src/ui/deck/antenna-column.vue5
-rw-r--r--packages/frontend/src/ui/deck/channel-column.vue7
-rw-r--r--packages/frontend/src/ui/deck/column.vue2
-rw-r--r--packages/frontend/src/ui/deck/deck-store.ts12
-rw-r--r--packages/frontend/src/ui/deck/direct-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/list-column.vue5
-rw-r--r--packages/frontend/src/ui/deck/main-column.vue20
-rw-r--r--packages/frontend/src/ui/deck/mentions-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/notifications-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/role-timeline-column.vue5
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue2
-rw-r--r--packages/frontend/src/ui/deck/widgets-column.vue2
-rw-r--r--packages/frontend/src/ui/minimum.vue24
-rw-r--r--packages/frontend/src/ui/universal.vue26
-rw-r--r--packages/frontend/src/ui/universal.widgets.vue2
-rw-r--r--packages/frontend/src/ui/visitor.vue37
-rw-r--r--packages/frontend/src/ui/zen.vue24
-rw-r--r--packages/frontend/src/unicode-emoji-indexes/ja-JP.json1866
-rw-r--r--packages/frontend/src/unicode-emoji-indexes/ja-JP_hira.json1866
-rw-r--r--packages/frontend/src/widgets/WidgetActivity.calendar.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetActivity.chart.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetActivity.vue6
-rw-r--r--packages/frontend/src/widgets/WidgetAichan.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetAiscript.vue18
-rw-r--r--packages/frontend/src/widgets/WidgetAiscriptApp.vue18
-rw-r--r--packages/frontend/src/widgets/WidgetBirthdayFollowings.vue6
-rw-r--r--packages/frontend/src/widgets/WidgetButton.vue18
-rw-r--r--packages/frontend/src/widgets/WidgetCalendar.vue10
-rw-r--r--packages/frontend/src/widgets/WidgetClicker.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetClock.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetDigitalClock.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetFederation.vue8
-rw-r--r--packages/frontend/src/widgets/WidgetInstanceCloud.vue5
-rw-r--r--packages/frontend/src/widgets/WidgetInstanceInfo.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetJobQueue.vue27
-rw-r--r--packages/frontend/src/widgets/WidgetMemo.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetNotifications.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetOnlineUsers.vue6
-rw-r--r--packages/frontend/src/widgets/WidgetPhotos.vue6
-rw-r--r--packages/frontend/src/widgets/WidgetPostForm.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetProfile.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetRss.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetRssTicker.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetSlideshow.vue7
-rw-r--r--packages/frontend/src/widgets/WidgetTimeline.vue9
-rw-r--r--packages/frontend/src/widgets/WidgetTrends.vue8
-rw-r--r--packages/frontend/src/widgets/WidgetUnixClock.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetUserList.vue9
-rw-r--r--packages/frontend/src/widgets/index.ts2
-rw-r--r--packages/frontend/src/widgets/server-metric/cpu-mem.vue13
-rw-r--r--packages/frontend/src/widgets/server-metric/cpu.vue6
-rw-r--r--packages/frontend/src/widgets/server-metric/disk.vue2
-rw-r--r--packages/frontend/src/widgets/server-metric/index.vue15
-rw-r--r--packages/frontend/src/widgets/server-metric/mem.vue6
-rw-r--r--packages/frontend/src/widgets/server-metric/net.vue13
-rw-r--r--packages/frontend/src/widgets/server-metric/pie.vue2
-rw-r--r--packages/frontend/src/widgets/widget.ts2
-rw-r--r--packages/frontend/src/workers/draw-blurhash.ts2
-rw-r--r--packages/frontend/src/workers/test-webgl2.ts2
-rw-r--r--packages/frontend/test/emoji.test.ts41
-rw-r--r--packages/frontend/test/home.test.ts2
-rw-r--r--packages/frontend/test/init.ts26
-rw-r--r--packages/frontend/test/note.test.ts2
-rw-r--r--packages/frontend/test/scroll.test.ts2
-rw-r--r--packages/frontend/test/url-preview.test.ts32
-rw-r--r--packages/frontend/tsconfig.json1
-rw-r--r--packages/frontend/vite.config.local-dev.ts33
-rw-r--r--packages/frontend/vite.config.ts8
772 files changed, 16345 insertions, 4525 deletions
diff --git a/packages/frontend/.storybook/changes.ts b/packages/frontend/.storybook/changes.ts
index 0cc648fbae..7c70972e1e 100644
--- a/packages/frontend/.storybook/changes.ts
+++ b/packages/frontend/.storybook/changes.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts
index 2960489c77..48c9e0261d 100644
--- a/packages/frontend/.storybook/fakes.ts
+++ b/packages/frontend/.storybook/fakes.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index d61df9e7be..76c5b6be4b 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/.storybook/main.ts b/packages/frontend/.storybook/main.ts
index a450f8b46b..0a87488573 100644
--- a/packages/frontend/.storybook/main.ts
+++ b/packages/frontend/.storybook/main.ts
@@ -1,27 +1,30 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { resolve } from 'node:path';
+import { createRequire } from 'node:module';
+import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { StorybookConfig } from '@storybook/vue3-vite';
import { type Plugin, mergeConfig } from 'vite';
import turbosnap from 'vite-plugin-turbosnap';
-const dirname = fileURLToPath(new URL('.', import.meta.url));
+const require = createRequire(import.meta.url);
+const _dirname = fileURLToPath(new URL('.', import.meta.url));
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
- '@storybook/addon-essentials',
- '@storybook/addon-interactions',
- '@storybook/addon-links',
- '@storybook/addon-storysource',
- resolve(dirname, '../node_modules/storybook-addon-misskey-theme'),
+ getAbsolutePath('@storybook/addon-essentials'),
+ getAbsolutePath('@storybook/addon-interactions'),
+ getAbsolutePath('@storybook/addon-links'),
+ getAbsolutePath('@storybook/addon-storysource'),
+ getAbsolutePath('@storybook/addon-mdx-gfm'),
+ resolve(_dirname, '../node_modules/storybook-addon-misskey-theme'),
],
framework: {
- name: '@storybook/vue3-vite',
+ name: getAbsolutePath('@storybook/vue3-vite') as '@storybook/vue3-vite',
options: {},
},
docs: {
@@ -37,10 +40,13 @@ const config = {
}
return mergeConfig(config, {
plugins: [
- // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8
- (turbosnap as any as typeof turbosnap['default'])({
- rootDir: config.root ?? process.cwd(),
- }),
+ {
+ // XXX: https://github.com/IanVS/vite-plugin-turbosnap/issues/8
+ ...(turbosnap as any as typeof turbosnap['default'])({
+ rootDir: config.root ?? process.cwd(),
+ }),
+ name: 'fake-turbosnap',
+ },
],
build: {
target: [
@@ -53,3 +59,7 @@ const config = {
},
} satisfies StorybookConfig;
export default config;
+
+function getAbsolutePath(value: string): string {
+ return dirname(require.resolve(join(value, 'package.json')));
+}
diff --git a/packages/frontend/.storybook/manager.ts b/packages/frontend/.storybook/manager.ts
index 8f501111d0..7375a1f2a9 100644
--- a/packages/frontend/.storybook/manager.ts
+++ b/packages/frontend/.storybook/manager.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/.storybook/mocks.ts b/packages/frontend/.storybook/mocks.ts
index 80e5157c5a..817b0125e7 100644
--- a/packages/frontend/.storybook/mocks.ts
+++ b/packages/frontend/.storybook/mocks.ts
@@ -1,9 +1,9 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { type SharedOptions, rest } from 'msw';
+import { type SharedOptions, http, HttpResponse } from 'msw';
export const onUnhandledRequest = ((req, print) => {
if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) {
@@ -13,19 +13,31 @@ export const onUnhandledRequest = ((req, print) => {
}) satisfies SharedOptions['onUnhandledRequest'];
export const commonHandlers = [
- rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => {
- const { codepoints } = req.params;
+ http.get('/fluent-emoji/:codepoints.png', async ({ params }) => {
+ const { codepoints } = params;
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
- return res(ctx.set('Content-Type', 'image/png'), ctx.body(value));
+ return new HttpResponse(value, {
+ headers: {
+ 'Content-Type': 'image/png',
+ },
+ });
}),
- rest.get('/fluent-emojis/:codepoints.png', async (req, res, ctx) => {
- const { codepoints } = req.params;
+ http.get('/fluent-emojis/:codepoints.png', async ({ params }) => {
+ const { codepoints } = params;
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
- return res(ctx.set('Content-Type', 'image/png'), ctx.body(value));
+ return new HttpResponse(value, {
+ headers: {
+ 'Content-Type': 'image/png',
+ },
+ });
}),
- rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => {
- const { codepoints } = req.params;
+ http.get('/twemoji/:codepoints.svg', async ({ params }) => {
+ const { codepoints } = params;
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
- return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value));
+ return new HttpResponse(value, {
+ headers: {
+ 'Content-Type': 'image/svg+xml',
+ },
+ });
}),
];
diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts
index 349cc13508..c823ff9bee 100644
--- a/packages/frontend/.storybook/preload-locale.ts
+++ b/packages/frontend/.storybook/preload-locale.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts
index ad2cf18a35..fb93d7be13 100644
--- a/packages/frontend/.storybook/preload-theme.ts
+++ b/packages/frontend/.storybook/preload-theme.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index 9860b60c67..982a2979ac 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { addons } from '@storybook/addons';
import { FORCE_REMOUNT } from '@storybook/core-events';
+import { addons } from '@storybook/preview-api';
import { type Preview, setup } from '@storybook/vue3';
import isChromatic from 'chromatic/isChromatic';
import { initialize, mswDecorator } from 'msw-storybook-addon';
diff --git a/packages/frontend/@types/global.d.ts b/packages/frontend/@types/global.d.ts
index 7d9335cc52..1025d1bedb 100644
--- a/packages/frontend/@types/global.d.ts
+++ b/packages/frontend/@types/global.d.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -16,3 +16,8 @@ declare const _DATA_TRANSFER_DECK_COLUMN_: string;
// for dev-mode
declare const _LANGS_FULL_: string[][];
+
+// TagCanvas
+interface Window {
+ TagCanvas: any;
+}
diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts
index 376bbb0e9c..0a7281898d 100644
--- a/packages/frontend/@types/theme.d.ts
+++ b/packages/frontend/@types/theme.d.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/assets/drop-and-fusion/bgm_1.mp3 b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3
new file mode 100644
index 0000000000..cafc34ad9c
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/bgm_1.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/click.mp3 b/packages/frontend/assets/drop-and-fusion/click.mp3
new file mode 100644
index 0000000000..ef03e60f61
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/click.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/collision.mp3 b/packages/frontend/assets/drop-and-fusion/collision.mp3
new file mode 100644
index 0000000000..59dae90965
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/collision.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/collision_yen.mp3 b/packages/frontend/assets/drop-and-fusion/collision_yen.mp3
new file mode 100644
index 0000000000..6737357f62
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/collision_yen.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/drop-arrow.svg b/packages/frontend/assets/drop-and-fusion/drop-arrow.svg
new file mode 100644
index 0000000000..f98bb8a1ac
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/drop-arrow.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M0,0L128,0L64,64L0,0Z" style="fill:rgb(255,61,0);"/>
+ <path d="M0,0L128,0L64,64L0,0ZM28.971,12L64,47.029C64,47.029 99.029,12 99.029,12L28.971,12Z" style="fill:rgb(255,122,0);"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/drop.mp3 b/packages/frontend/assets/drop-and-fusion/drop.mp3
new file mode 100644
index 0000000000..a65c653891
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/drop.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/drop_yen.mp3 b/packages/frontend/assets/drop-and-fusion/drop_yen.mp3
new file mode 100644
index 0000000000..bbf385f15a
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/drop_yen.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/dropper.png b/packages/frontend/assets/drop-and-fusion/dropper.png
new file mode 100644
index 0000000000..f4300aa5c0
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/dropper.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/frame-dark.svg b/packages/frontend/assets/drop-and-fusion/frame-dark.svg
new file mode 100644
index 0000000000..3fa7c0da81
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/frame-dark.svg
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 450 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+ <g>
+ <g transform="matrix(0.944444,0,0,0.8125,12.5,100)">
+ <rect x="0" y="0" width="450" height="600"/>
+ </g>
+ <g transform="matrix(0.944444,0,0,0.8125,12.5,100)">
+ <rect x="0" y="0" width="450" height="600" style="fill:rgb(255,147,2);fill-opacity:0.15;"/>
+ </g>
+ <use xlink:href="#_Image1" x="0" y="49.048" width="450px" height="551px"/>
+ </g>
+ <g transform="matrix(0.755719,0.654896,-0.654896,0.755719,383.517,-217.265)">
+ <g transform="matrix(0.755719,-0.654896,0.654896,0.755719,-147.545,415.355)">
+ <use xlink:href="#_Image2" x="0" y="49" width="450px" height="551px"/>
+ </g>
+ </g>
+ <use xlink:href="#_Image3" x="25" y="99.5" width="400px" height="475px"/>
+ <g transform="matrix(1,0,0,2,1.13687e-13,25)">
+ <rect x="25" y="37.5" width="400" height="12.5" style="fill:url(#_Linear4);"/>
+ </g>
+ <defs>
+ <image id="_Image1" width="450px" height="551px" xlink:href="data:image/png;base64,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CSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCp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"/>
+ <image id="_Image2" width="450px" height="551px" xlink:href="data:image/png;base64,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"/>
+ <image id="_Image3" width="400px" height="475px" xlink:href="data:image/png;base64,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"/>
+ <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(7.65404e-16,12.5,-0.390625,2.39189e-17,225,37.5)"><stop offset="0" style="stop-color:rgb(255,14,0);stop-opacity:0.5"/><stop offset="1" style="stop-color:rgb(255,13,0);stop-opacity:0"/></linearGradient>
+ </defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/frame-light.svg b/packages/frontend/assets/drop-and-fusion/frame-light.svg
new file mode 100644
index 0000000000..6052ccbaa0
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/frame-light.svg
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 450 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
+ <g>
+ <g transform="matrix(0.944444,0,0,0.8125,12.5,100)">
+ <rect x="0" y="0" width="450" height="600" style="fill:white;"/>
+ </g>
+ <g transform="matrix(0.944444,0,0,0.8125,12.5,100)">
+ <rect x="0" y="0" width="450" height="600" style="fill:rgb(255,147,2);fill-opacity:0.15;"/>
+ </g>
+ <use xlink:href="#_Image1" x="0" y="49.048" width="450px" height="551px"/>
+ </g>
+ <g transform="matrix(0.755719,0.654896,-0.654896,0.755719,383.517,-217.265)">
+ <g transform="matrix(0.755719,-0.654896,0.654896,0.755719,-147.545,415.355)">
+ <use xlink:href="#_Image2" x="0" y="49" width="450px" height="551px"/>
+ </g>
+ </g>
+ <use xlink:href="#_Image3" x="25" y="99.5" width="400px" height="475px"/>
+ <g transform="matrix(1,0,0,2,1.13687e-13,25)">
+ <rect x="25" y="37.5" width="400" height="12.5" style="fill:url(#_Linear4);"/>
+ </g>
+ <defs>
+ <image id="_Image1" width="450px" height="551px" xlink:href="data:image/png;base64,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CSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCpCSEAqQkhAKkJIQCp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"/>
+ <image id="_Image2" width="450px" height="551px" xlink:href="data:image/png;base64,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"/>
+ <image id="_Image3" width="400px" height="475px" xlink:href="data:image/png;base64,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"/>
+ <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(7.65404e-16,12.5,-0.390625,2.39189e-17,225,37.5)"><stop offset="0" style="stop-color:rgb(255,14,0);stop-opacity:0.5"/><stop offset="1" style="stop-color:rgb(255,13,0);stop-opacity:0"/></linearGradient>
+ </defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/fusion.mp3 b/packages/frontend/assets/drop-and-fusion/fusion.mp3
new file mode 100644
index 0000000000..8b4f8df6e9
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/fusion.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/fusion_yen.mp3 b/packages/frontend/assets/drop-and-fusion/fusion_yen.mp3
new file mode 100644
index 0000000000..e8d203fb5d
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/fusion_yen.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/gameover.mp3 b/packages/frontend/assets/drop-and-fusion/gameover.mp3
new file mode 100644
index 0000000000..23b41c5699
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/gameover.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/gameover.png b/packages/frontend/assets/drop-and-fusion/gameover.png
new file mode 100644
index 0000000000..8b622577ca
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/gameover.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3 b/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3
new file mode 100644
index 0000000000..c7fdcb5c8f
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/go.png b/packages/frontend/assets/drop-and-fusion/go.png
new file mode 100644
index 0000000000..37468f1395
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/go.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/hold.mp3 b/packages/frontend/assets/drop-and-fusion/hold.mp3
new file mode 100644
index 0000000000..f064c976d3
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/hold.mp3
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/logo.png b/packages/frontend/assets/drop-and-fusion/logo.png
new file mode 100644
index 0000000000..c6725bea88
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/logo.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/cold_face.png b/packages/frontend/assets/drop-and-fusion/normal_monos/cold_face.png
new file mode 100644
index 0000000000..f5f53e9efc
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/cold_face.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/exploding_head.png b/packages/frontend/assets/drop-and-fusion/normal_monos/exploding_head.png
new file mode 100644
index 0000000000..e8ec5182c8
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/exploding_head.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/face_with_open_mouth.png b/packages/frontend/assets/drop-and-fusion/normal_monos/face_with_open_mouth.png
new file mode 100644
index 0000000000..c523020f62
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/face_with_open_mouth.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png b/packages/frontend/assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png
new file mode 100644
index 0000000000..db9e839c84
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/grinning_squinting_face.png b/packages/frontend/assets/drop-and-fusion/normal_monos/grinning_squinting_face.png
new file mode 100644
index 0000000000..fd72d749a1
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/grinning_squinting_face.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/heart_suit.png b/packages/frontend/assets/drop-and-fusion/normal_monos/heart_suit.png
new file mode 100644
index 0000000000..b0105f8582
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/heart_suit.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/pleading_face.png b/packages/frontend/assets/drop-and-fusion/normal_monos/pleading_face.png
new file mode 100644
index 0000000000..42f58d411c
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/pleading_face.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_hearts.png b/packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_hearts.png
new file mode 100644
index 0000000000..416ef0410a
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_hearts.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_sunglasses.png b/packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_sunglasses.png
new file mode 100644
index 0000000000..c0f72254c2
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_sunglasses.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/normal_monos/zany_face.png b/packages/frontend/assets/drop-and-fusion/normal_monos/zany_face.png
new file mode 100644
index 0000000000..f14f9db20b
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/normal_monos/zany_face.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/ready.png b/packages/frontend/assets/drop-and-fusion/ready.png
new file mode 100644
index 0000000000..10a87fcf58
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/ready.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_1.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_1.png
new file mode 100644
index 0000000000..d672f2854a
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_1.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_10.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_10.png
new file mode 100644
index 0000000000..32cf193540
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_10.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_2.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_2.png
new file mode 100644
index 0000000000..81c3f58e6e
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_2.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_3.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_3.png
new file mode 100644
index 0000000000..424d8c123d
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_3.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_4.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_4.png
new file mode 100644
index 0000000000..ea6ae50531
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_4.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_5.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_5.png
new file mode 100644
index 0000000000..ad435da69a
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_5.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_6.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_6.png
new file mode 100644
index 0000000000..70c9522b43
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_6.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_7.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_7.png
new file mode 100644
index 0000000000..5a24307487
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_7.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_8.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_8.png
new file mode 100644
index 0000000000..9689d8ecfb
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_8.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/square_monos/keycap_9.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_9.png
new file mode 100644
index 0000000000..ac3f638841
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_9.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/candy_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/candy_color.svg
new file mode 100644
index 0000000000..6eab3ca49b
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/candy_color.svg
@@ -0,0 +1,86 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13 3.99998C13 2.89998 12.2135 2.08134 11.0625 1.9375C9.875 1.78909 9.10938 2.49997 8.51562 3.57811C8.20282 4.14611 7.69531 4.34368 7.13281 4.15621C6.07292 3.80295 5.07886 3.24214 4.07812 4.38277C3.35156 5.21089 3.46006 6.16444 4.07812 7.07811C4.4375 7.60936 4.46599 8.50491 3.85938 8.85154C2.98438 9.35154 2.01562 9.89998 2.01562 11C2.01562 12.5937 3.22812 13.375 4.32812 13.375L13 13V3.99998Z" fill="url(#paint0_linear_18_32303)"/>
+<path d="M13 3.99998C13 2.89998 12.2135 2.08134 11.0625 1.9375C9.875 1.78909 9.10938 2.49997 8.51562 3.57811C8.20282 4.14611 7.69531 4.34368 7.13281 4.15621C6.07292 3.80295 5.07886 3.24214 4.07812 4.38277C3.35156 5.21089 3.46006 6.16444 4.07812 7.07811C4.4375 7.60936 4.46599 8.50491 3.85938 8.85154C2.98438 9.35154 2.01562 9.89998 2.01562 11C2.01562 12.5937 3.22812 13.375 4.32812 13.375L13 13V3.99998Z" fill="url(#paint1_radial_18_32303)"/>
+<path d="M13 3.99998C13 2.89998 12.2135 2.08134 11.0625 1.9375C9.875 1.78909 9.10938 2.49997 8.51562 3.57811C8.20282 4.14611 7.69531 4.34368 7.13281 4.15621C6.07292 3.80295 5.07886 3.24214 4.07812 4.38277C3.35156 5.21089 3.46006 6.16444 4.07812 7.07811C4.4375 7.60936 4.46599 8.50491 3.85938 8.85154C2.98438 9.35154 2.01562 9.89998 2.01562 11C2.01562 12.5937 3.22812 13.375 4.32812 13.375L13 13V3.99998Z" fill="url(#paint2_radial_18_32303)"/>
+<path d="M16 26C21.5228 26 26 21.5228 26 16C26 10.4772 21.5228 6 16 6C10.4772 6 6 10.4772 6 16C6 21.5228 10.4772 26 16 26Z" fill="url(#paint3_radial_18_32303)"/>
+<path d="M16 26C21.5228 26 26 21.5228 26 16C26 10.4772 21.5228 6 16 6C10.4772 6 6 10.4772 6 16C6 21.5228 10.4772 26 16 26Z" fill="url(#paint4_radial_18_32303)"/>
+<path d="M16 26C21.5228 26 26 21.5228 26 16C26 10.4772 21.5228 6 16 6C10.4772 6 6 10.4772 6 16C6 21.5228 10.4772 26 16 26Z" fill="url(#paint5_radial_18_32303)"/>
+<path d="M13.2344 13.0156C16.5007 9.6733 23.0156 8.23438 25.625 13.2695C25.3867 12.4766 24.7656 10.5391 23.0156 8.87496C19.5156 5.98433 13.8416 7.95125 10.5937 11.5937C8.46307 13.9833 5.57031 19.7031 9.61719 23.6992C10.4609 24.3555 11.8437 25.4844 14.1719 25.8281C6.98438 22.6875 10.3465 15.9707 13.2344 13.0156Z" fill="url(#paint6_linear_18_32303)"/>
+<path d="M19.1094 24.1719C15.2031 24 15.2701 20.2321 17.2812 17.9766C20.6387 14.211 24.2891 16.0156 24.2187 19.0703H25.1808C26.4349 14.6766 23.3913 12.9922 20.9375 12.9922C17.3586 12.9922 14.1562 16.1094 13.2266 19.4609C12.4375 22.6406 14.2266 26.375 19.1094 25.125V24.1719Z" fill="#C62561"/>
+<path d="M19.1094 24.1719C15.2031 24 15.2701 20.2321 17.2812 17.9766C20.6387 14.211 24.2891 16.0156 24.2187 19.0703H25.1808C26.4349 14.6766 23.3913 12.9922 20.9375 12.9922C17.3586 12.9922 14.1562 16.1094 13.2266 19.4609C12.4375 22.6406 14.2266 26.375 19.1094 25.125V24.1719Z" fill="url(#paint7_radial_18_32303)"/>
+<path d="M19 28C19 29.1 19.9 30 21 30C22.1 30 23 29.1 23 28C23 27.2 23.97 26.8 24.54 27.36L24.59 27.41C25.37 28.19 26.64 28.19 27.42 27.41C28.2 26.63 28.2 25.36 27.42 24.58L27.37 24.53C26.8 23.97 27.2 23 28 23C29.1 23 30 22.1 30 21C30 19.9 29.1 19 28 19H22.5C20.57 19 19 20.57 19 22.5V28Z" fill="url(#paint8_linear_18_32303)"/>
+<path d="M8.85156 9.00391C6.21406 11.6414 5.88281 14.8047 6.03906 16.8672C6.03906 16.8672 5.95968 12.9153 9.29687 9.57814C12.6341 6.24095 16.4219 6.01561 16.4219 6.01561C14.3125 5.89846 11.4891 6.36641 8.85156 9.00391Z" fill="#A72D36"/>
+<g filter="url(#filter0_f_18_32303)">
+<path d="M26.6641 24.0547C27.1367 24.0039 26.6094 23.2812 27.211 22.1406C26.4844 22.6484 26.1914 24.1055 26.6641 24.0547Z" fill="#E75372"/>
+</g>
+<g filter="url(#filter1_f_18_32303)">
+<path d="M27.7196 25.3724C27.9826 25.8229 27.9149 26.8411 26.9618 27.263C25.7243 27.6943 26.9514 26.1823 27.7196 25.3724Z" fill="url(#paint9_radial_18_32303)"/>
+</g>
+<g filter="url(#filter2_f_18_32303)">
+<path d="M29.5156 19.9688C29.8385 20.4434 30.125 21.5352 28.75 22.1618C27.925 22.5377 28.6927 20.9656 29.5156 19.9688Z" fill="url(#paint10_radial_18_32303)"/>
+</g>
+<defs>
+<filter id="filter0_f_18_32303" x="26.0343" y="21.7406" width="1.57663" height="2.71534" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.2" result="effect1_foregroundBlur_18_32303"/>
+</filter>
+<filter id="filter1_f_18_32303" x="26.1843" y="25.1224" width="1.91805" height="2.46649" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_32303"/>
+</filter>
+<filter id="filter2_f_18_32303" x="27.996" y="19.5687" width="2.18687" height="3.04994" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.2" result="effect1_foregroundBlur_18_32303"/>
+</filter>
+<linearGradient id="paint0_linear_18_32303" x1="2.01562" y1="7.64644" x2="13" y2="7.64644" gradientUnits="userSpaceOnUse">
+<stop stop-color="#AA1C3D"/>
+<stop offset="1" stop-color="#C31D45"/>
+</linearGradient>
+<radialGradient id="paint1_radial_18_32303" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8 11.1875) rotate(123.69) scale(5.18298 4.96914)">
+<stop stop-color="#951731"/>
+<stop offset="1" stop-color="#9D1934" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint2_radial_18_32303" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.2187 4.40625) rotate(90) scale(5.0625 1.3908)">
+<stop stop-color="#EC516B"/>
+<stop offset="1" stop-color="#EB506C" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint3_radial_18_32303" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21.8125 11.8125) rotate(161.633) scale(16.6613)">
+<stop stop-color="#FFD95A"/>
+<stop offset="0.423359" stop-color="#EEB53D"/>
+<stop offset="0.787547" stop-color="#CA8631"/>
+<stop offset="1" stop-color="#B28341"/>
+</radialGradient>
+<radialGradient id="paint4_radial_18_32303" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.625 26.6875) rotate(104.903) scale(8.01975 13.7205)">
+<stop stop-color="#CD7677"/>
+<stop offset="1" stop-color="#CE7A85" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint5_radial_18_32303" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(20.0625 22.0625) rotate(151.844) scale(5.03309 3.80247)">
+<stop stop-color="#CF771E"/>
+<stop offset="1" stop-color="#C96D2E" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint6_linear_18_32303" x1="24.0625" y1="9.8125" x2="9.6875" y2="23.875" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F54353"/>
+<stop offset="0.485245" stop-color="#C01C47"/>
+<stop offset="1" stop-color="#C2355A"/>
+</linearGradient>
+<radialGradient id="paint7_radial_18_32303" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21.625 12.5625) rotate(73.1132) scale(7.31544 7.34222)">
+<stop offset="0.341752" stop-color="#F25271"/>
+<stop offset="1" stop-color="#F15372" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint8_linear_18_32303" x1="20.3125" y1="20.3125" x2="27.5" y2="27.75" gradientUnits="userSpaceOnUse">
+<stop stop-color="#BF242E"/>
+<stop offset="1" stop-color="#CF1E51"/>
+</linearGradient>
+<radialGradient id="paint9_radial_18_32303" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27.8524 26.1536) rotate(118.896) scale(1.8218 1.06121)">
+<stop stop-color="#ED5372"/>
+<stop offset="1" stop-color="#ED5372" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint10_radial_18_32303" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(29.7828 21.2031) rotate(120.115) scale(1.71599 0.933717)">
+<stop stop-color="#ED5372"/>
+<stop offset="1" stop-color="#ED5372" stop-opacity="0"/>
+</radialGradient>
+</defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg
new file mode 100644
index 0000000000..eea5fec186
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg
@@ -0,0 +1,316 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.9844 22.625L2.875 14.4531C1.5547 13.1328 2.04687 11.8125 2.62501 11.2344L11.1875 2.71876C12.211 1.69532 13.375 1.89065 14.3125 2.82815L22.375 10.8281L10.9844 22.625Z" fill="#8A584C"/>
+<path d="M10.9844 22.625L2.875 14.4531C1.5547 13.1328 2.04687 11.8125 2.62501 11.2344L11.1875 2.71876C12.211 1.69532 13.375 1.89065 14.3125 2.82815L22.375 10.8281L10.9844 22.625Z" fill="url(#paint0_radial_18_32062)"/>
+<path d="M10.9844 22.625L2.875 14.4531C1.5547 13.1328 2.04687 11.8125 2.62501 11.2344L11.1875 2.71876C12.211 1.69532 13.375 1.89065 14.3125 2.82815L22.375 10.8281L10.9844 22.625Z" fill="url(#paint1_radial_18_32062)"/>
+<path d="M10.9844 22.625L2.875 14.4531C1.5547 13.1328 2.04687 11.8125 2.62501 11.2344L11.1875 2.71876C12.211 1.69532 13.375 1.89065 14.3125 2.82815L22.375 10.8281L10.9844 22.625Z" fill="url(#paint2_radial_18_32062)"/>
+<g filter="url(#filter0_f_18_32062)">
+<path d="M6.46309 9.72343C6.47658 9.71056 6.49039 9.6979 6.50466 9.6859C7.03753 9.23812 7.71518 9.24807 8.23887 9.69368C8.24699 9.70059 8.25463 9.70742 8.26249 9.71463C8.36319 9.80688 9.10895 10.4917 9.67969 11.0625C10.2582 11.641 10.9331 12.3785 11.0077 12.4602C11.0125 12.4654 11.0169 12.4703 11.0215 12.4756C11.4733 12.9952 11.5049 13.6871 11.0259 14.2228C11.0184 14.2312 11.0113 14.2392 11.0039 14.2477C10.9204 14.3435 10.357 14.9867 9.67969 15.664C9.02564 16.3181 8.40257 16.8978 8.2741 17.0168C8.25775 17.0319 8.24131 17.0464 8.22388 17.0602C7.64411 17.5221 7.00112 17.4542 6.48458 17C6.48373 16.9993 6.48346 16.999 6.48261 16.9983C6.45576 16.9747 5.65368 16.2708 5.04688 15.664C4.44065 15.0578 3.74501 14.2642 3.72049 14.2362C3.71971 14.2353 3.71942 14.235 3.71865 14.2341C3.25359 13.6982 3.26579 12.9793 3.70954 12.4791C3.71632 12.4715 3.72307 12.4642 3.73012 12.4568C3.82368 12.3587 4.53588 11.6125 5.08595 11.0625C5.62082 10.5276 6.33466 9.84586 6.46309 9.72343Z" fill="#824534"/>
+<path d="M6.46309 9.72343C6.47658 9.71056 6.49039 9.6979 6.50466 9.6859C7.03753 9.23812 7.71518 9.24807 8.23887 9.69368C8.24699 9.70059 8.25463 9.70742 8.26249 9.71463C8.36319 9.80688 9.10895 10.4917 9.67969 11.0625C10.2582 11.641 10.9331 12.3785 11.0077 12.4602C11.0125 12.4654 11.0169 12.4703 11.0215 12.4756C11.4733 12.9952 11.5049 13.6871 11.0259 14.2228C11.0184 14.2312 11.0113 14.2392 11.0039 14.2477C10.9204 14.3435 10.357 14.9867 9.67969 15.664C9.02564 16.3181 8.40257 16.8978 8.2741 17.0168C8.25775 17.0319 8.24131 17.0464 8.22388 17.0602C7.64411 17.5221 7.00112 17.4542 6.48458 17C6.48373 16.9993 6.48346 16.999 6.48261 16.9983C6.45576 16.9747 5.65368 16.2708 5.04688 15.664C4.44065 15.0578 3.74501 14.2642 3.72049 14.2362C3.71971 14.2353 3.71942 14.235 3.71865 14.2341C3.25359 13.6982 3.26579 12.9793 3.70954 12.4791C3.71632 12.4715 3.72307 12.4642 3.73012 12.4568C3.82368 12.3587 4.53588 11.6125 5.08595 11.0625C5.62082 10.5276 6.33466 9.84586 6.46309 9.72343Z" fill="url(#paint3_linear_18_32062)"/>
+<path d="M6.46309 9.72343C6.47658 9.71056 6.49039 9.6979 6.50466 9.6859C7.03753 9.23812 7.71518 9.24807 8.23887 9.69368C8.24699 9.70059 8.25463 9.70742 8.26249 9.71463C8.36319 9.80688 9.10895 10.4917 9.67969 11.0625C10.2582 11.641 10.9331 12.3785 11.0077 12.4602C11.0125 12.4654 11.0169 12.4703 11.0215 12.4756C11.4733 12.9952 11.5049 13.6871 11.0259 14.2228C11.0184 14.2312 11.0113 14.2392 11.0039 14.2477C10.9204 14.3435 10.357 14.9867 9.67969 15.664C9.02564 16.3181 8.40257 16.8978 8.2741 17.0168C8.25775 17.0319 8.24131 17.0464 8.22388 17.0602C7.64411 17.5221 7.00112 17.4542 6.48458 17C6.48373 16.9993 6.48346 16.999 6.48261 16.9983C6.45576 16.9747 5.65368 16.2708 5.04688 15.664C4.44065 15.0578 3.74501 14.2642 3.72049 14.2362C3.71971 14.2353 3.71942 14.235 3.71865 14.2341C3.25359 13.6982 3.26579 12.9793 3.70954 12.4791C3.71632 12.4715 3.72307 12.4642 3.73012 12.4568C3.82368 12.3587 4.53588 11.6125 5.08595 11.0625C5.62082 10.5276 6.33466 9.84586 6.46309 9.72343Z" fill="#7B3E41"/>
+</g>
+<path d="M6.77559 9.16093C6.78908 9.14806 6.80289 9.1354 6.81716 9.1234C7.35003 8.67562 8.02768 8.68557 8.55137 9.13118C8.55949 9.13809 8.56713 9.14492 8.57499 9.15213C8.67569 9.24438 9.42145 9.92923 9.99219 10.5C10.5707 11.0785 11.2456 11.816 11.3202 11.8977C11.325 11.9029 11.3294 11.9078 11.334 11.9131C11.7858 12.4327 11.8174 13.1246 11.3384 13.6603C11.3309 13.6687 11.3238 13.6767 11.3164 13.6852C11.2329 13.781 10.6695 14.4242 9.99219 15.1015C9.33814 15.7556 8.71507 16.3353 8.5866 16.4543C8.57025 16.4694 8.55381 16.4839 8.53638 16.4977C7.95661 16.9596 7.31362 16.8917 6.79708 16.4375C6.79623 16.4368 6.79596 16.4365 6.79511 16.4358C6.76826 16.4122 5.96618 15.7083 5.35938 15.1015C4.75315 14.4953 4.05751 13.7017 4.03299 13.6737C4.03221 13.6728 4.03192 13.6725 4.03115 13.6716C3.56609 13.1357 3.57829 12.4168 4.02204 11.9166C4.02882 11.909 4.03557 11.9017 4.04262 11.8943C4.13618 11.7962 4.84838 11.05 5.39845 10.5C5.93332 9.96511 6.64716 9.28336 6.77559 9.16093Z" fill="#824534"/>
+<path d="M6.77559 9.16093C6.78908 9.14806 6.80289 9.1354 6.81716 9.1234C7.35003 8.67562 8.02768 8.68557 8.55137 9.13118C8.55949 9.13809 8.56713 9.14492 8.57499 9.15213C8.67569 9.24438 9.42145 9.92923 9.99219 10.5C10.5707 11.0785 11.2456 11.816 11.3202 11.8977C11.325 11.9029 11.3294 11.9078 11.334 11.9131C11.7858 12.4327 11.8174 13.1246 11.3384 13.6603C11.3309 13.6687 11.3238 13.6767 11.3164 13.6852C11.2329 13.781 10.6695 14.4242 9.99219 15.1015C9.33814 15.7556 8.71507 16.3353 8.5866 16.4543C8.57025 16.4694 8.55381 16.4839 8.53638 16.4977C7.95661 16.9596 7.31362 16.8917 6.79708 16.4375C6.79623 16.4368 6.79596 16.4365 6.79511 16.4358C6.76826 16.4122 5.96618 15.7083 5.35938 15.1015C4.75315 14.4953 4.05751 13.7017 4.03299 13.6737C4.03221 13.6728 4.03192 13.6725 4.03115 13.6716C3.56609 13.1357 3.57829 12.4168 4.02204 11.9166C4.02882 11.909 4.03557 11.9017 4.04262 11.8943C4.13618 11.7962 4.84838 11.05 5.39845 10.5C5.93332 9.96511 6.64716 9.28336 6.77559 9.16093Z" fill="url(#paint4_linear_18_32062)"/>
+<path d="M6.77559 9.16093C6.78908 9.14806 6.80289 9.1354 6.81716 9.1234C7.35003 8.67562 8.02768 8.68557 8.55137 9.13118C8.55949 9.13809 8.56713 9.14492 8.57499 9.15213C8.67569 9.24438 9.42145 9.92923 9.99219 10.5C10.5707 11.0785 11.2456 11.816 11.3202 11.8977C11.325 11.9029 11.3294 11.9078 11.334 11.9131C11.7858 12.4327 11.8174 13.1246 11.3384 13.6603C11.3309 13.6687 11.3238 13.6767 11.3164 13.6852C11.2329 13.781 10.6695 14.4242 9.99219 15.1015C9.33814 15.7556 8.71507 16.3353 8.5866 16.4543C8.57025 16.4694 8.55381 16.4839 8.53638 16.4977C7.95661 16.9596 7.31362 16.8917 6.79708 16.4375C6.79623 16.4368 6.79596 16.4365 6.79511 16.4358C6.76826 16.4122 5.96618 15.7083 5.35938 15.1015C4.75315 14.4953 4.05751 13.7017 4.03299 13.6737C4.03221 13.6728 4.03192 13.6725 4.03115 13.6716C3.56609 13.1357 3.57829 12.4168 4.02204 11.9166C4.02882 11.909 4.03557 11.9017 4.04262 11.8943C4.13618 11.7962 4.84838 11.05 5.39845 10.5C5.93332 9.96511 6.64716 9.28336 6.77559 9.16093Z" fill="url(#paint5_linear_18_32062)"/>
+<path d="M6.77559 9.16093C6.78908 9.14806 6.80289 9.1354 6.81716 9.1234C7.35003 8.67562 8.02768 8.68557 8.55137 9.13118C8.55949 9.13809 8.56713 9.14492 8.57499 9.15213C8.67569 9.24438 9.42145 9.92923 9.99219 10.5C10.5707 11.0785 11.2456 11.816 11.3202 11.8977C11.325 11.9029 11.3294 11.9078 11.334 11.9131C11.7858 12.4327 11.8174 13.1246 11.3384 13.6603C11.3309 13.6687 11.3238 13.6767 11.3164 13.6852C11.2329 13.781 10.6695 14.4242 9.99219 15.1015C9.33814 15.7556 8.71507 16.3353 8.5866 16.4543C8.57025 16.4694 8.55381 16.4839 8.53638 16.4977C7.95661 16.9596 7.31362 16.8917 6.79708 16.4375C6.79623 16.4368 6.79596 16.4365 6.79511 16.4358C6.76826 16.4122 5.96618 15.7083 5.35938 15.1015C4.75315 14.4953 4.05751 13.7017 4.03299 13.6737C4.03221 13.6728 4.03192 13.6725 4.03115 13.6716C3.56609 13.1357 3.57829 12.4168 4.02204 11.9166C4.02882 11.909 4.03557 11.9017 4.04262 11.8943C4.13618 11.7962 4.84838 11.05 5.39845 10.5C5.93332 9.96511 6.64716 9.28336 6.77559 9.16093Z" fill="url(#paint6_linear_18_32062)"/>
+<path d="M6.77559 9.16093C6.78908 9.14806 6.80289 9.1354 6.81716 9.1234C7.35003 8.67562 8.02768 8.68557 8.55137 9.13118C8.55949 9.13809 8.56713 9.14492 8.57499 9.15213C8.67569 9.24438 9.42145 9.92923 9.99219 10.5C10.5707 11.0785 11.2456 11.816 11.3202 11.8977C11.325 11.9029 11.3294 11.9078 11.334 11.9131C11.7858 12.4327 11.8174 13.1246 11.3384 13.6603C11.3309 13.6687 11.3238 13.6767 11.3164 13.6852C11.2329 13.781 10.6695 14.4242 9.99219 15.1015C9.33814 15.7556 8.71507 16.3353 8.5866 16.4543C8.57025 16.4694 8.55381 16.4839 8.53638 16.4977C7.95661 16.9596 7.31362 16.8917 6.79708 16.4375C6.79623 16.4368 6.79596 16.4365 6.79511 16.4358C6.76826 16.4122 5.96618 15.7083 5.35938 15.1015C4.75315 14.4953 4.05751 13.7017 4.03299 13.6737C4.03221 13.6728 4.03192 13.6725 4.03115 13.6716C3.56609 13.1357 3.57829 12.4168 4.02204 11.9166C4.02882 11.909 4.03557 11.9017 4.04262 11.8943C4.13618 11.7962 4.84838 11.05 5.39845 10.5C5.93332 9.96511 6.64716 9.28336 6.77559 9.16093Z" fill="url(#paint7_linear_18_32062)"/>
+<g filter="url(#filter1_f_18_32062)">
+<path d="M4.39003 13.1845C4.2339 13.0047 4.22852 12.7406 4.38087 12.5576C4.67465 12.2047 5.19322 11.6037 5.80061 10.9963C6.40743 10.3895 7.00484 9.87436 7.35569 9.58263C7.53818 9.43089 7.80117 9.43597 7.98065 9.59126C8.38765 9.94341 9.10397 10.5727 9.55267 11.0214C9.99768 11.4664 10.6204 12.1747 10.9741 12.5834C11.1334 12.7674 11.1344 13.0384 10.9761 13.2233C10.6339 13.6226 10.0351 14.3086 9.57032 14.7734C9.1059 15.2379 8.42064 15.836 8.02119 16.1783C7.8359 16.3371 7.56387 16.3355 7.37965 16.1754C6.96194 15.8126 6.23492 15.1724 5.8183 14.7558C5.39836 14.3359 4.7513 13.6006 4.39003 13.1845Z" fill="url(#paint8_linear_18_32062)"/>
+<path d="M4.39003 13.1845C4.2339 13.0047 4.22852 12.7406 4.38087 12.5576C4.67465 12.2047 5.19322 11.6037 5.80061 10.9963C6.40743 10.3895 7.00484 9.87436 7.35569 9.58263C7.53818 9.43089 7.80117 9.43597 7.98065 9.59126C8.38765 9.94341 9.10397 10.5727 9.55267 11.0214C9.99768 11.4664 10.6204 12.1747 10.9741 12.5834C11.1334 12.7674 11.1344 13.0384 10.9761 13.2233C10.6339 13.6226 10.0351 14.3086 9.57032 14.7734C9.1059 15.2379 8.42064 15.836 8.02119 16.1783C7.8359 16.3371 7.56387 16.3355 7.37965 16.1754C6.96194 15.8126 6.23492 15.1724 5.8183 14.7558C5.39836 14.3359 4.7513 13.6006 4.39003 13.1845Z" fill="url(#paint9_linear_18_32062)"/>
+<path d="M4.39003 13.1845C4.2339 13.0047 4.22852 12.7406 4.38087 12.5576C4.67465 12.2047 5.19322 11.6037 5.80061 10.9963C6.40743 10.3895 7.00484 9.87436 7.35569 9.58263C7.53818 9.43089 7.80117 9.43597 7.98065 9.59126C8.38765 9.94341 9.10397 10.5727 9.55267 11.0214C9.99768 11.4664 10.6204 12.1747 10.9741 12.5834C11.1334 12.7674 11.1344 13.0384 10.9761 13.2233C10.6339 13.6226 10.0351 14.3086 9.57032 14.7734C9.1059 15.2379 8.42064 15.836 8.02119 16.1783C7.8359 16.3371 7.56387 16.3355 7.37965 16.1754C6.96194 15.8126 6.23492 15.1724 5.8183 14.7558C5.39836 14.3359 4.7513 13.6006 4.39003 13.1845Z" fill="url(#paint10_linear_18_32062)"/>
+</g>
+<path d="M11.9631 4.00468C11.9766 3.99181 11.9904 3.97915 12.0047 3.96715C12.5375 3.51937 13.2152 3.52932 13.7389 3.97493C13.747 3.98184 13.7546 3.98867 13.7625 3.99588C13.8632 4.08813 14.6089 4.77298 15.1797 5.34373C15.7582 5.92224 16.4331 6.6597 16.5077 6.74144C16.5125 6.74663 16.5169 6.75157 16.5215 6.75687C16.9733 7.27646 17.0049 7.96838 16.5259 8.50405C16.5184 8.51244 16.5113 8.52045 16.5039 8.52893C16.4204 8.62477 15.857 9.26797 15.1797 9.94529C14.5256 10.5993 13.9026 11.179 13.7741 11.298C13.7577 11.3132 13.7413 11.3276 13.7239 11.3415C13.1441 11.8034 12.5011 11.7354 11.9846 11.2813C11.9837 11.2805 11.9835 11.2803 11.9826 11.2795C11.9558 11.256 11.1537 10.5521 10.5469 9.94529C9.94065 9.33906 9.24501 8.54543 9.22049 8.51743C9.21971 8.51654 9.21942 8.51621 9.21865 8.51531C8.75359 7.97944 8.76579 7.26059 9.20954 6.76036C9.21632 6.75271 9.22307 6.74549 9.23012 6.73809C9.32368 6.63994 10.0359 5.89379 10.5859 5.34373C11.1208 4.80886 11.8347 4.12711 11.9631 4.00468Z" fill="#824534"/>
+<path d="M11.9631 4.00468C11.9766 3.99181 11.9904 3.97915 12.0047 3.96715C12.5375 3.51937 13.2152 3.52932 13.7389 3.97493C13.747 3.98184 13.7546 3.98867 13.7625 3.99588C13.8632 4.08813 14.6089 4.77298 15.1797 5.34373C15.7582 5.92224 16.4331 6.6597 16.5077 6.74144C16.5125 6.74663 16.5169 6.75157 16.5215 6.75687C16.9733 7.27646 17.0049 7.96838 16.5259 8.50405C16.5184 8.51244 16.5113 8.52045 16.5039 8.52893C16.4204 8.62477 15.857 9.26797 15.1797 9.94529C14.5256 10.5993 13.9026 11.179 13.7741 11.298C13.7577 11.3132 13.7413 11.3276 13.7239 11.3415C13.1441 11.8034 12.5011 11.7354 11.9846 11.2813C11.9837 11.2805 11.9835 11.2803 11.9826 11.2795C11.9558 11.256 11.1537 10.5521 10.5469 9.94529C9.94065 9.33906 9.24501 8.54543 9.22049 8.51743C9.21971 8.51654 9.21942 8.51621 9.21865 8.51531C8.75359 7.97944 8.76579 7.26059 9.20954 6.76036C9.21632 6.75271 9.22307 6.74549 9.23012 6.73809C9.32368 6.63994 10.0359 5.89379 10.5859 5.34373C11.1208 4.80886 11.8347 4.12711 11.9631 4.00468Z" fill="url(#paint11_linear_18_32062)"/>
+<path d="M11.9631 4.00468C11.9766 3.99181 11.9904 3.97915 12.0047 3.96715C12.5375 3.51937 13.2152 3.52932 13.7389 3.97493C13.747 3.98184 13.7546 3.98867 13.7625 3.99588C13.8632 4.08813 14.6089 4.77298 15.1797 5.34373C15.7582 5.92224 16.4331 6.6597 16.5077 6.74144C16.5125 6.74663 16.5169 6.75157 16.5215 6.75687C16.9733 7.27646 17.0049 7.96838 16.5259 8.50405C16.5184 8.51244 16.5113 8.52045 16.5039 8.52893C16.4204 8.62477 15.857 9.26797 15.1797 9.94529C14.5256 10.5993 13.9026 11.179 13.7741 11.298C13.7577 11.3132 13.7413 11.3276 13.7239 11.3415C13.1441 11.8034 12.5011 11.7354 11.9846 11.2813C11.9837 11.2805 11.9835 11.2803 11.9826 11.2795C11.9558 11.256 11.1537 10.5521 10.5469 9.94529C9.94065 9.33906 9.24501 8.54543 9.22049 8.51743C9.21971 8.51654 9.21942 8.51621 9.21865 8.51531C8.75359 7.97944 8.76579 7.26059 9.20954 6.76036C9.21632 6.75271 9.22307 6.74549 9.23012 6.73809C9.32368 6.63994 10.0359 5.89379 10.5859 5.34373C11.1208 4.80886 11.8347 4.12711 11.9631 4.00468Z" fill="url(#paint12_linear_18_32062)"/>
+<path d="M11.9631 4.00468C11.9766 3.99181 11.9904 3.97915 12.0047 3.96715C12.5375 3.51937 13.2152 3.52932 13.7389 3.97493C13.747 3.98184 13.7546 3.98867 13.7625 3.99588C13.8632 4.08813 14.6089 4.77298 15.1797 5.34373C15.7582 5.92224 16.4331 6.6597 16.5077 6.74144C16.5125 6.74663 16.5169 6.75157 16.5215 6.75687C16.9733 7.27646 17.0049 7.96838 16.5259 8.50405C16.5184 8.51244 16.5113 8.52045 16.5039 8.52893C16.4204 8.62477 15.857 9.26797 15.1797 9.94529C14.5256 10.5993 13.9026 11.179 13.7741 11.298C13.7577 11.3132 13.7413 11.3276 13.7239 11.3415C13.1441 11.8034 12.5011 11.7354 11.9846 11.2813C11.9837 11.2805 11.9835 11.2803 11.9826 11.2795C11.9558 11.256 11.1537 10.5521 10.5469 9.94529C9.94065 9.33906 9.24501 8.54543 9.22049 8.51743C9.21971 8.51654 9.21942 8.51621 9.21865 8.51531C8.75359 7.97944 8.76579 7.26059 9.20954 6.76036C9.21632 6.75271 9.22307 6.74549 9.23012 6.73809C9.32368 6.63994 10.0359 5.89379 10.5859 5.34373C11.1208 4.80886 11.8347 4.12711 11.9631 4.00468Z" fill="url(#paint13_linear_18_32062)"/>
+<path d="M11.9631 4.00468C11.9766 3.99181 11.9904 3.97915 12.0047 3.96715C12.5375 3.51937 13.2152 3.52932 13.7389 3.97493C13.747 3.98184 13.7546 3.98867 13.7625 3.99588C13.8632 4.08813 14.6089 4.77298 15.1797 5.34373C15.7582 5.92224 16.4331 6.6597 16.5077 6.74144C16.5125 6.74663 16.5169 6.75157 16.5215 6.75687C16.9733 7.27646 17.0049 7.96838 16.5259 8.50405C16.5184 8.51244 16.5113 8.52045 16.5039 8.52893C16.4204 8.62477 15.857 9.26797 15.1797 9.94529C14.5256 10.5993 13.9026 11.179 13.7741 11.298C13.7577 11.3132 13.7413 11.3276 13.7239 11.3415C13.1441 11.8034 12.5011 11.7354 11.9846 11.2813C11.9837 11.2805 11.9835 11.2803 11.9826 11.2795C11.9558 11.256 11.1537 10.5521 10.5469 9.94529C9.94065 9.33906 9.24501 8.54543 9.22049 8.51743C9.21971 8.51654 9.21942 8.51621 9.21865 8.51531C8.75359 7.97944 8.76579 7.26059 9.20954 6.76036C9.21632 6.75271 9.22307 6.74549 9.23012 6.73809C9.32368 6.63994 10.0359 5.89379 10.5859 5.34373C11.1208 4.80886 11.8347 4.12711 11.9631 4.00468Z" fill="url(#paint14_linear_18_32062)"/>
+<g filter="url(#filter2_f_18_32062)">
+<path d="M9.58023 8.02622C9.42436 7.84587 9.41986 7.5814 9.5728 7.39856C9.87588 7.03625 10.4173 6.41088 11.0507 5.7774C11.6836 5.1445 12.3052 4.60674 12.6654 4.30578C12.8477 4.15346 13.1111 4.15764 13.2911 4.31264C13.699 4.66392 14.4166 5.29158 14.8652 5.74017C15.3102 6.18519 15.9315 6.89491 16.2843 7.30448C16.4432 7.48892 16.4434 7.76006 16.2846 7.94456C15.9316 8.35465 15.3048 9.0702 14.8203 9.55468C14.3362 10.0388 13.6215 10.6649 13.2112 11.018C13.0263 11.1772 12.7541 11.1764 12.5695 11.0168C12.1509 10.6549 11.4224 10.0161 11.0058 9.59953C10.586 9.17972 9.94063 8.44325 9.58023 8.02622Z" fill="url(#paint15_linear_18_32062)"/>
+<path d="M9.58023 8.02622C9.42436 7.84587 9.41986 7.5814 9.5728 7.39856C9.87588 7.03625 10.4173 6.41088 11.0507 5.7774C11.6836 5.1445 12.3052 4.60674 12.6654 4.30578C12.8477 4.15346 13.1111 4.15764 13.2911 4.31264C13.699 4.66392 14.4166 5.29158 14.8652 5.74017C15.3102 6.18519 15.9315 6.89491 16.2843 7.30448C16.4432 7.48892 16.4434 7.76006 16.2846 7.94456C15.9316 8.35465 15.3048 9.0702 14.8203 9.55468C14.3362 10.0388 13.6215 10.6649 13.2112 11.018C13.0263 11.1772 12.7541 11.1764 12.5695 11.0168C12.1509 10.6549 11.4224 10.0161 11.0058 9.59953C10.586 9.17972 9.94063 8.44325 9.58023 8.02622Z" fill="url(#paint16_linear_18_32062)"/>
+<path d="M9.58023 8.02622C9.42436 7.84587 9.41986 7.5814 9.5728 7.39856C9.87588 7.03625 10.4173 6.41088 11.0507 5.7774C11.6836 5.1445 12.3052 4.60674 12.6654 4.30578C12.8477 4.15346 13.1111 4.15764 13.2911 4.31264C13.699 4.66392 14.4166 5.29158 14.8652 5.74017C15.3102 6.18519 15.9315 6.89491 16.2843 7.30448C16.4432 7.48892 16.4434 7.76006 16.2846 7.94456C15.9316 8.35465 15.3048 9.0702 14.8203 9.55468C14.3362 10.0388 13.6215 10.6649 13.2112 11.018C13.0263 11.1772 12.7541 11.1764 12.5695 11.0168C12.1509 10.6549 11.4224 10.0161 11.0058 9.59953C10.586 9.17972 9.94063 8.44325 9.58023 8.02622Z" fill="url(#paint17_linear_18_32062)"/>
+</g>
+<g filter="url(#filter3_f_18_32062)">
+<path d="M17.0902 9.92832C17.1037 9.91546 17.1175 9.90279 17.1318 9.8908C17.6646 9.44301 18.3423 9.45297 18.866 9.89858C18.8741 9.90549 18.8818 9.91232 18.8896 9.91952C18.9903 10.0118 19.7361 10.6966 20.3068 11.2674C20.8853 11.8459 21.5602 12.5833 21.6349 12.6651C21.6396 12.6703 21.644 12.6752 21.6486 12.6805C22.1004 13.2001 22.132 13.892 21.6531 14.4277C21.6456 14.4361 21.6384 14.4441 21.631 14.4526C21.5476 14.5484 20.9841 15.1916 20.3068 15.8689C19.6528 16.523 19.0297 17.1027 18.9012 17.2217C18.8849 17.2368 18.8684 17.2513 18.851 17.2651C18.2712 17.727 17.6282 17.6591 17.1117 17.2049C17.1109 17.2042 17.1106 17.2039 17.1097 17.2032C17.0829 17.1796 16.2808 16.4757 15.674 15.8689C15.0678 15.2627 14.3721 14.4691 14.3476 14.4411C14.3468 14.4402 14.3465 14.4399 14.3458 14.439C13.8807 13.9031 13.8929 13.1842 14.3367 12.684C14.3434 12.6764 14.3502 12.6691 14.3572 12.6617C14.4508 12.5636 15.163 11.8174 15.7131 11.2674C16.2479 10.7325 16.9618 10.0508 17.0902 9.92832Z" fill="#824534"/>
+<path d="M17.0902 9.92832C17.1037 9.91546 17.1175 9.90279 17.1318 9.8908C17.6646 9.44301 18.3423 9.45297 18.866 9.89858C18.8741 9.90549 18.8818 9.91232 18.8896 9.91952C18.9903 10.0118 19.7361 10.6966 20.3068 11.2674C20.8853 11.8459 21.5602 12.5833 21.6349 12.6651C21.6396 12.6703 21.644 12.6752 21.6486 12.6805C22.1004 13.2001 22.132 13.892 21.6531 14.4277C21.6456 14.4361 21.6384 14.4441 21.631 14.4526C21.5476 14.5484 20.9841 15.1916 20.3068 15.8689C19.6528 16.523 19.0297 17.1027 18.9012 17.2217C18.8849 17.2368 18.8684 17.2513 18.851 17.2651C18.2712 17.727 17.6282 17.6591 17.1117 17.2049C17.1109 17.2042 17.1106 17.2039 17.1097 17.2032C17.0829 17.1796 16.2808 16.4757 15.674 15.8689C15.0678 15.2627 14.3721 14.4691 14.3476 14.4411C14.3468 14.4402 14.3465 14.4399 14.3458 14.439C13.8807 13.9031 13.8929 13.1842 14.3367 12.684C14.3434 12.6764 14.3502 12.6691 14.3572 12.6617C14.4508 12.5636 15.163 11.8174 15.7131 11.2674C16.2479 10.7325 16.9618 10.0508 17.0902 9.92832Z" fill="url(#paint18_linear_18_32062)"/>
+<path d="M17.0902 9.92832C17.1037 9.91546 17.1175 9.90279 17.1318 9.8908C17.6646 9.44301 18.3423 9.45297 18.866 9.89858C18.8741 9.90549 18.8818 9.91232 18.8896 9.91952C18.9903 10.0118 19.7361 10.6966 20.3068 11.2674C20.8853 11.8459 21.5602 12.5833 21.6349 12.6651C21.6396 12.6703 21.644 12.6752 21.6486 12.6805C22.1004 13.2001 22.132 13.892 21.6531 14.4277C21.6456 14.4361 21.6384 14.4441 21.631 14.4526C21.5476 14.5484 20.9841 15.1916 20.3068 15.8689C19.6528 16.523 19.0297 17.1027 18.9012 17.2217C18.8849 17.2368 18.8684 17.2513 18.851 17.2651C18.2712 17.727 17.6282 17.6591 17.1117 17.2049C17.1109 17.2042 17.1106 17.2039 17.1097 17.2032C17.0829 17.1796 16.2808 16.4757 15.674 15.8689C15.0678 15.2627 14.3721 14.4691 14.3476 14.4411C14.3468 14.4402 14.3465 14.4399 14.3458 14.439C13.8807 13.9031 13.8929 13.1842 14.3367 12.684C14.3434 12.6764 14.3502 12.6691 14.3572 12.6617C14.4508 12.5636 15.163 11.8174 15.7131 11.2674C16.2479 10.7325 16.9618 10.0508 17.0902 9.92832Z" fill="url(#paint19_linear_18_32062)"/>
+<path d="M17.0902 9.92832C17.1037 9.91546 17.1175 9.90279 17.1318 9.8908C17.6646 9.44301 18.3423 9.45297 18.866 9.89858C18.8741 9.90549 18.8818 9.91232 18.8896 9.91952C18.9903 10.0118 19.7361 10.6966 20.3068 11.2674C20.8853 11.8459 21.5602 12.5833 21.6349 12.6651C21.6396 12.6703 21.644 12.6752 21.6486 12.6805C22.1004 13.2001 22.132 13.892 21.6531 14.4277C21.6456 14.4361 21.6384 14.4441 21.631 14.4526C21.5476 14.5484 20.9841 15.1916 20.3068 15.8689C19.6528 16.523 19.0297 17.1027 18.9012 17.2217C18.8849 17.2368 18.8684 17.2513 18.851 17.2651C18.2712 17.727 17.6282 17.6591 17.1117 17.2049C17.1109 17.2042 17.1106 17.2039 17.1097 17.2032C17.0829 17.1796 16.2808 16.4757 15.674 15.8689C15.0678 15.2627 14.3721 14.4691 14.3476 14.4411C14.3468 14.4402 14.3465 14.4399 14.3458 14.439C13.8807 13.9031 13.8929 13.1842 14.3367 12.684C14.3434 12.6764 14.3502 12.6691 14.3572 12.6617C14.4508 12.5636 15.163 11.8174 15.7131 11.2674C16.2479 10.7325 16.9618 10.0508 17.0902 9.92832Z" fill="url(#paint20_linear_18_32062)"/>
+<path d="M17.0902 9.92832C17.1037 9.91546 17.1175 9.90279 17.1318 9.8908C17.6646 9.44301 18.3423 9.45297 18.866 9.89858C18.8741 9.90549 18.8818 9.91232 18.8896 9.91952C18.9903 10.0118 19.7361 10.6966 20.3068 11.2674C20.8853 11.8459 21.5602 12.5833 21.6349 12.6651C21.6396 12.6703 21.644 12.6752 21.6486 12.6805C22.1004 13.2001 22.132 13.892 21.6531 14.4277C21.6456 14.4361 21.6384 14.4441 21.631 14.4526C21.5476 14.5484 20.9841 15.1916 20.3068 15.8689C19.6528 16.523 19.0297 17.1027 18.9012 17.2217C18.8849 17.2368 18.8684 17.2513 18.851 17.2651C18.2712 17.727 17.6282 17.6591 17.1117 17.2049C17.1109 17.2042 17.1106 17.2039 17.1097 17.2032C17.0829 17.1796 16.2808 16.4757 15.674 15.8689C15.0678 15.2627 14.3721 14.4691 14.3476 14.4411C14.3468 14.4402 14.3465 14.4399 14.3458 14.439C13.8807 13.9031 13.8929 13.1842 14.3367 12.684C14.3434 12.6764 14.3502 12.6691 14.3572 12.6617C14.4508 12.5636 15.163 11.8174 15.7131 11.2674C16.2479 10.7325 16.9618 10.0508 17.0902 9.92832Z" fill="#613534"/>
+</g>
+<path d="M17.4943 9.47343C17.5078 9.46056 17.5216 9.4479 17.5359 9.4359C18.0688 8.98812 18.7464 8.99807 19.2701 9.44368C19.2782 9.45059 19.2859 9.45742 19.2937 9.46463C19.3944 9.55688 20.1402 10.2417 20.7109 10.8125C21.2895 11.391 21.9643 12.1285 22.039 12.2102C22.0437 12.2154 22.0482 12.2203 22.0528 12.2256C22.5046 12.7452 22.5361 13.4371 22.0572 13.9728C22.0497 13.9812 22.0425 13.9892 22.0351 13.9977C21.9517 14.0935 21.3883 14.7367 20.7109 15.414C20.0569 16.0681 19.4338 16.6478 19.3053 16.7668C19.289 16.7819 19.2726 16.7964 19.2551 16.8102C18.6754 17.2721 18.0324 17.2042 17.5158 16.75C17.515 16.7493 17.5147 16.749 17.5139 16.7483C17.487 16.7247 16.6849 16.0208 16.0781 15.414C15.4719 14.8078 14.7763 14.0142 14.7517 13.9862C14.751 13.9853 14.7507 13.985 14.7499 13.9841C14.2848 13.4482 14.297 12.7293 14.7408 12.2291C14.7476 12.2215 14.7543 12.2142 14.7614 12.2068C14.8549 12.1087 15.5671 11.3625 16.1172 10.8125C16.6521 10.2776 17.3659 9.59586 17.4943 9.47343Z" fill="#824534"/>
+<path d="M17.4943 9.47343C17.5078 9.46056 17.5216 9.4479 17.5359 9.4359C18.0688 8.98812 18.7464 8.99807 19.2701 9.44368C19.2782 9.45059 19.2859 9.45742 19.2937 9.46463C19.3944 9.55688 20.1402 10.2417 20.7109 10.8125C21.2895 11.391 21.9643 12.1285 22.039 12.2102C22.0437 12.2154 22.0482 12.2203 22.0528 12.2256C22.5046 12.7452 22.5361 13.4371 22.0572 13.9728C22.0497 13.9812 22.0425 13.9892 22.0351 13.9977C21.9517 14.0935 21.3883 14.7367 20.7109 15.414C20.0569 16.0681 19.4338 16.6478 19.3053 16.7668C19.289 16.7819 19.2726 16.7964 19.2551 16.8102C18.6754 17.2721 18.0324 17.2042 17.5158 16.75C17.515 16.7493 17.5147 16.749 17.5139 16.7483C17.487 16.7247 16.6849 16.0208 16.0781 15.414C15.4719 14.8078 14.7763 14.0142 14.7517 13.9862C14.751 13.9853 14.7507 13.985 14.7499 13.9841C14.2848 13.4482 14.297 12.7293 14.7408 12.2291C14.7476 12.2215 14.7543 12.2142 14.7614 12.2068C14.8549 12.1087 15.5671 11.3625 16.1172 10.8125C16.6521 10.2776 17.3659 9.59586 17.4943 9.47343Z" fill="url(#paint21_linear_18_32062)"/>
+<path d="M17.4943 9.47343C17.5078 9.46056 17.5216 9.4479 17.5359 9.4359C18.0688 8.98812 18.7464 8.99807 19.2701 9.44368C19.2782 9.45059 19.2859 9.45742 19.2937 9.46463C19.3944 9.55688 20.1402 10.2417 20.7109 10.8125C21.2895 11.391 21.9643 12.1285 22.039 12.2102C22.0437 12.2154 22.0482 12.2203 22.0528 12.2256C22.5046 12.7452 22.5361 13.4371 22.0572 13.9728C22.0497 13.9812 22.0425 13.9892 22.0351 13.9977C21.9517 14.0935 21.3883 14.7367 20.7109 15.414C20.0569 16.0681 19.4338 16.6478 19.3053 16.7668C19.289 16.7819 19.2726 16.7964 19.2551 16.8102C18.6754 17.2721 18.0324 17.2042 17.5158 16.75C17.515 16.7493 17.5147 16.749 17.5139 16.7483C17.487 16.7247 16.6849 16.0208 16.0781 15.414C15.4719 14.8078 14.7763 14.0142 14.7517 13.9862C14.751 13.9853 14.7507 13.985 14.7499 13.9841C14.2848 13.4482 14.297 12.7293 14.7408 12.2291C14.7476 12.2215 14.7543 12.2142 14.7614 12.2068C14.8549 12.1087 15.5671 11.3625 16.1172 10.8125C16.6521 10.2776 17.3659 9.59586 17.4943 9.47343Z" fill="url(#paint22_linear_18_32062)"/>
+<path d="M17.4943 9.47343C17.5078 9.46056 17.5216 9.4479 17.5359 9.4359C18.0688 8.98812 18.7464 8.99807 19.2701 9.44368C19.2782 9.45059 19.2859 9.45742 19.2937 9.46463C19.3944 9.55688 20.1402 10.2417 20.7109 10.8125C21.2895 11.391 21.9643 12.1285 22.039 12.2102C22.0437 12.2154 22.0482 12.2203 22.0528 12.2256C22.5046 12.7452 22.5361 13.4371 22.0572 13.9728C22.0497 13.9812 22.0425 13.9892 22.0351 13.9977C21.9517 14.0935 21.3883 14.7367 20.7109 15.414C20.0569 16.0681 19.4338 16.6478 19.3053 16.7668C19.289 16.7819 19.2726 16.7964 19.2551 16.8102C18.6754 17.2721 18.0324 17.2042 17.5158 16.75C17.515 16.7493 17.5147 16.749 17.5139 16.7483C17.487 16.7247 16.6849 16.0208 16.0781 15.414C15.4719 14.8078 14.7763 14.0142 14.7517 13.9862C14.751 13.9853 14.7507 13.985 14.7499 13.9841C14.2848 13.4482 14.297 12.7293 14.7408 12.2291C14.7476 12.2215 14.7543 12.2142 14.7614 12.2068C14.8549 12.1087 15.5671 11.3625 16.1172 10.8125C16.6521 10.2776 17.3659 9.59586 17.4943 9.47343Z" fill="url(#paint23_linear_18_32062)"/>
+<path d="M17.4943 9.47343C17.5078 9.46056 17.5216 9.4479 17.5359 9.4359C18.0688 8.98812 18.7464 8.99807 19.2701 9.44368C19.2782 9.45059 19.2859 9.45742 19.2937 9.46463C19.3944 9.55688 20.1402 10.2417 20.7109 10.8125C21.2895 11.391 21.9643 12.1285 22.039 12.2102C22.0437 12.2154 22.0482 12.2203 22.0528 12.2256C22.5046 12.7452 22.5361 13.4371 22.0572 13.9728C22.0497 13.9812 22.0425 13.9892 22.0351 13.9977C21.9517 14.0935 21.3883 14.7367 20.7109 15.414C20.0569 16.0681 19.4338 16.6478 19.3053 16.7668C19.289 16.7819 19.2726 16.7964 19.2551 16.8102C18.6754 17.2721 18.0324 17.2042 17.5158 16.75C17.515 16.7493 17.5147 16.749 17.5139 16.7483C17.487 16.7247 16.6849 16.0208 16.0781 15.414C15.4719 14.8078 14.7763 14.0142 14.7517 13.9862C14.751 13.9853 14.7507 13.985 14.7499 13.9841C14.2848 13.4482 14.297 12.7293 14.7408 12.2291C14.7476 12.2215 14.7543 12.2142 14.7614 12.2068C14.8549 12.1087 15.5671 11.3625 16.1172 10.8125C16.6521 10.2776 17.3659 9.59586 17.4943 9.47343Z" fill="url(#paint24_linear_18_32062)"/>
+<g filter="url(#filter4_f_18_32062)">
+<path d="M15.1088 13.497C14.9527 13.3172 14.9473 13.0531 15.0996 12.8701C15.3934 12.5172 15.912 11.9162 16.5194 11.3088C17.1262 10.702 17.7236 10.1869 18.0744 9.89513C18.2569 9.74339 18.5199 9.74847 18.6994 9.90376C19.1064 10.2559 19.8227 10.8852 20.2714 11.3339C20.7164 11.7789 21.3391 12.4872 21.6929 12.8959C21.8522 13.0799 21.8532 13.3509 21.6948 13.5358C21.3526 13.9351 20.7539 14.6211 20.2891 15.0859C19.8246 15.5504 19.1394 16.1485 18.7399 16.4908C18.5546 16.6496 18.2826 16.648 18.0984 16.4879C17.6807 16.1251 16.9537 15.4849 16.537 15.0683C16.1171 14.6484 15.4701 13.9131 15.1088 13.497Z" fill="url(#paint25_linear_18_32062)"/>
+<path d="M15.1088 13.497C14.9527 13.3172 14.9473 13.0531 15.0996 12.8701C15.3934 12.5172 15.912 11.9162 16.5194 11.3088C17.1262 10.702 17.7236 10.1869 18.0744 9.89513C18.2569 9.74339 18.5199 9.74847 18.6994 9.90376C19.1064 10.2559 19.8227 10.8852 20.2714 11.3339C20.7164 11.7789 21.3391 12.4872 21.6929 12.8959C21.8522 13.0799 21.8532 13.3509 21.6948 13.5358C21.3526 13.9351 20.7539 14.6211 20.2891 15.0859C19.8246 15.5504 19.1394 16.1485 18.7399 16.4908C18.5546 16.6496 18.2826 16.648 18.0984 16.4879C17.6807 16.1251 16.9537 15.4849 16.537 15.0683C16.1171 14.6484 15.4701 13.9131 15.1088 13.497Z" fill="url(#paint26_linear_18_32062)"/>
+<path d="M15.1088 13.497C14.9527 13.3172 14.9473 13.0531 15.0996 12.8701C15.3934 12.5172 15.912 11.9162 16.5194 11.3088C17.1262 10.702 17.7236 10.1869 18.0744 9.89513C18.2569 9.74339 18.5199 9.74847 18.6994 9.90376C19.1064 10.2559 19.8227 10.8852 20.2714 11.3339C20.7164 11.7789 21.3391 12.4872 21.6929 12.8959C21.8522 13.0799 21.8532 13.3509 21.6948 13.5358C21.3526 13.9351 20.7539 14.6211 20.2891 15.0859C19.8246 15.5504 19.1394 16.1485 18.7399 16.4908C18.5546 16.6496 18.2826 16.648 18.0984 16.4879C17.6807 16.1251 16.9537 15.4849 16.537 15.0683C16.1171 14.6484 15.4701 13.9131 15.1088 13.497Z" fill="url(#paint27_linear_18_32062)"/>
+</g>
+<g filter="url(#filter5_f_18_32062)">
+<path d="M11.9631 15.1578C11.9766 15.1449 11.9904 15.1322 12.0047 15.1202C12.5375 14.6724 13.2152 14.6824 13.7389 15.128C13.747 15.1349 13.7546 15.1418 13.7625 15.149C13.8632 15.2412 14.6089 15.9261 15.1797 16.4968C15.7582 17.0753 16.4331 17.8128 16.5077 17.8945C16.5125 17.8997 16.5169 17.9046 16.5215 17.91C16.9733 18.4295 17.0049 19.1215 16.5259 19.6571C16.5184 19.6655 16.5113 19.6735 16.5039 19.682C16.4204 19.7779 15.857 20.421 15.1797 21.0984C14.5256 21.7524 13.9026 22.3321 13.7741 22.4511C13.7577 22.4662 13.7413 22.4807 13.7239 22.4946C13.1441 22.9565 12.5011 22.8885 11.9846 22.4344C11.9837 22.4336 11.9835 22.4334 11.9826 22.4326C11.9558 22.4091 11.1537 21.7052 10.5469 21.0984C9.94065 20.4921 9.24501 19.6985 9.22049 19.6705C9.21971 19.6696 9.21942 19.6693 9.21865 19.6684C8.75359 19.1325 8.76579 18.4137 9.20954 17.9134C9.21632 17.9058 9.22307 17.8986 9.23012 17.8912C9.32368 17.793 10.0359 17.0469 10.5859 16.4968C11.1208 15.9619 11.8347 15.2802 11.9631 15.1578Z" fill="url(#paint28_linear_18_32062)"/>
+</g>
+<path d="M12.3225 14.6766C12.336 14.6637 12.3498 14.651 12.364 14.639C12.8969 14.1912 13.5746 14.2012 14.0982 14.6468C14.1064 14.6537 14.114 14.6605 14.1219 14.6678C14.2226 14.76 14.9683 15.4449 15.5391 16.0156C16.1176 16.5941 16.7925 17.3316 16.8671 17.4133C16.8719 17.4185 16.8763 17.4234 16.8809 17.4287C17.3327 17.9483 17.3642 18.6403 16.8853 19.1759C16.8778 19.1843 16.8707 19.1923 16.8633 19.2008C16.7798 19.2966 16.2164 19.9398 15.5391 20.6172C14.885 21.2712 14.2619 21.8509 14.1335 21.9699C14.1171 21.985 14.1007 21.9995 14.0833 22.0134C13.5035 22.4753 12.8605 22.4073 12.344 21.9531C12.3431 21.9524 12.3428 21.9522 12.342 21.9514C12.3151 21.9279 11.5131 21.224 10.9063 20.6172C10.3 20.0109 9.60438 19.2173 9.57986 19.1893C9.57908 19.1884 9.5788 19.1881 9.57802 19.1872C9.11297 18.6513 9.12517 17.9325 9.56892 17.4322C9.5757 17.4246 9.58244 17.4174 9.58949 17.41C9.68306 17.3118 10.3953 16.5657 10.9453 16.0156C11.4802 15.4807 12.194 14.799 12.3225 14.6766Z" fill="#824534"/>
+<path d="M12.3225 14.6766C12.336 14.6637 12.3498 14.651 12.364 14.639C12.8969 14.1912 13.5746 14.2012 14.0982 14.6468C14.1064 14.6537 14.114 14.6605 14.1219 14.6678C14.2226 14.76 14.9683 15.4449 15.5391 16.0156C16.1176 16.5941 16.7925 17.3316 16.8671 17.4133C16.8719 17.4185 16.8763 17.4234 16.8809 17.4287C17.3327 17.9483 17.3642 18.6403 16.8853 19.1759C16.8778 19.1843 16.8707 19.1923 16.8633 19.2008C16.7798 19.2966 16.2164 19.9398 15.5391 20.6172C14.885 21.2712 14.2619 21.8509 14.1335 21.9699C14.1171 21.985 14.1007 21.9995 14.0833 22.0134C13.5035 22.4753 12.8605 22.4073 12.344 21.9531C12.3431 21.9524 12.3428 21.9522 12.342 21.9514C12.3151 21.9279 11.5131 21.224 10.9063 20.6172C10.3 20.0109 9.60438 19.2173 9.57986 19.1893C9.57908 19.1884 9.5788 19.1881 9.57802 19.1872C9.11297 18.6513 9.12517 17.9325 9.56892 17.4322C9.5757 17.4246 9.58244 17.4174 9.58949 17.41C9.68306 17.3118 10.3953 16.5657 10.9453 16.0156C11.4802 15.4807 12.194 14.799 12.3225 14.6766Z" fill="url(#paint29_linear_18_32062)"/>
+<path d="M12.3225 14.6766C12.336 14.6637 12.3498 14.651 12.364 14.639C12.8969 14.1912 13.5746 14.2012 14.0982 14.6468C14.1064 14.6537 14.114 14.6605 14.1219 14.6678C14.2226 14.76 14.9683 15.4449 15.5391 16.0156C16.1176 16.5941 16.7925 17.3316 16.8671 17.4133C16.8719 17.4185 16.8763 17.4234 16.8809 17.4287C17.3327 17.9483 17.3642 18.6403 16.8853 19.1759C16.8778 19.1843 16.8707 19.1923 16.8633 19.2008C16.7798 19.2966 16.2164 19.9398 15.5391 20.6172C14.885 21.2712 14.2619 21.8509 14.1335 21.9699C14.1171 21.985 14.1007 21.9995 14.0833 22.0134C13.5035 22.4753 12.8605 22.4073 12.344 21.9531C12.3431 21.9524 12.3428 21.9522 12.342 21.9514C12.3151 21.9279 11.5131 21.224 10.9063 20.6172C10.3 20.0109 9.60438 19.2173 9.57986 19.1893C9.57908 19.1884 9.5788 19.1881 9.57802 19.1872C9.11297 18.6513 9.12517 17.9325 9.56892 17.4322C9.5757 17.4246 9.58244 17.4174 9.58949 17.41C9.68306 17.3118 10.3953 16.5657 10.9453 16.0156C11.4802 15.4807 12.194 14.799 12.3225 14.6766Z" fill="url(#paint30_linear_18_32062)"/>
+<path d="M12.3225 14.6766C12.336 14.6637 12.3498 14.651 12.364 14.639C12.8969 14.1912 13.5746 14.2012 14.0982 14.6468C14.1064 14.6537 14.114 14.6605 14.1219 14.6678C14.2226 14.76 14.9683 15.4449 15.5391 16.0156C16.1176 16.5941 16.7925 17.3316 16.8671 17.4133C16.8719 17.4185 16.8763 17.4234 16.8809 17.4287C17.3327 17.9483 17.3642 18.6403 16.8853 19.1759C16.8778 19.1843 16.8707 19.1923 16.8633 19.2008C16.7798 19.2966 16.2164 19.9398 15.5391 20.6172C14.885 21.2712 14.2619 21.8509 14.1335 21.9699C14.1171 21.985 14.1007 21.9995 14.0833 22.0134C13.5035 22.4753 12.8605 22.4073 12.344 21.9531C12.3431 21.9524 12.3428 21.9522 12.342 21.9514C12.3151 21.9279 11.5131 21.224 10.9063 20.6172C10.3 20.0109 9.60438 19.2173 9.57986 19.1893C9.57908 19.1884 9.5788 19.1881 9.57802 19.1872C9.11297 18.6513 9.12517 17.9325 9.56892 17.4322C9.5757 17.4246 9.58244 17.4174 9.58949 17.41C9.68306 17.3118 10.3953 16.5657 10.9453 16.0156C11.4802 15.4807 12.194 14.799 12.3225 14.6766Z" fill="url(#paint31_linear_18_32062)"/>
+<path d="M12.3225 14.6766C12.336 14.6637 12.3498 14.651 12.364 14.639C12.8969 14.1912 13.5746 14.2012 14.0982 14.6468C14.1064 14.6537 14.114 14.6605 14.1219 14.6678C14.2226 14.76 14.9683 15.4449 15.5391 16.0156C16.1176 16.5941 16.7925 17.3316 16.8671 17.4133C16.8719 17.4185 16.8763 17.4234 16.8809 17.4287C17.3327 17.9483 17.3642 18.6403 16.8853 19.1759C16.8778 19.1843 16.8707 19.1923 16.8633 19.2008C16.7798 19.2966 16.2164 19.9398 15.5391 20.6172C14.885 21.2712 14.2619 21.8509 14.1335 21.9699C14.1171 21.985 14.1007 21.9995 14.0833 22.0134C13.5035 22.4753 12.8605 22.4073 12.344 21.9531C12.3431 21.9524 12.3428 21.9522 12.342 21.9514C12.3151 21.9279 11.5131 21.224 10.9063 20.6172C10.3 20.0109 9.60438 19.2173 9.57986 19.1893C9.57908 19.1884 9.5788 19.1881 9.57802 19.1872C9.11297 18.6513 9.12517 17.9325 9.56892 17.4322C9.5757 17.4246 9.58244 17.4174 9.58949 17.41C9.68306 17.3118 10.3953 16.5657 10.9453 16.0156C11.4802 15.4807 12.194 14.799 12.3225 14.6766Z" fill="url(#paint32_linear_18_32062)"/>
+<g filter="url(#filter6_f_18_32062)">
+<path d="M9.93691 18.7001C9.78078 18.5203 9.77539 18.2563 9.92775 18.0732C10.2215 17.7203 10.7401 17.1193 11.3475 16.5119C11.9543 15.9051 12.5517 15.39 12.9026 15.0983C13.0851 14.9465 13.348 14.9516 13.5275 15.1069C13.9345 15.459 14.6508 16.0883 15.0995 16.537C15.5446 16.9821 16.1672 17.6903 16.521 18.099C16.6803 18.283 16.6813 18.5541 16.5229 18.7389C16.1808 19.1382 15.582 19.8242 15.1172 20.2891C14.6528 20.7535 13.9675 21.3516 13.5681 21.6939C13.3828 21.8527 13.1107 21.8511 12.9265 21.6911C12.5088 21.3282 11.7818 20.688 11.3652 20.2714C10.9452 19.8515 10.2982 19.1162 9.93691 18.7001Z" fill="url(#paint33_linear_18_32062)"/>
+<path d="M9.93691 18.7001C9.78078 18.5203 9.77539 18.2563 9.92775 18.0732C10.2215 17.7203 10.7401 17.1193 11.3475 16.5119C11.9543 15.9051 12.5517 15.39 12.9026 15.0983C13.0851 14.9465 13.348 14.9516 13.5275 15.1069C13.9345 15.459 14.6508 16.0883 15.0995 16.537C15.5446 16.9821 16.1672 17.6903 16.521 18.099C16.6803 18.283 16.6813 18.5541 16.5229 18.7389C16.1808 19.1382 15.582 19.8242 15.1172 20.2891C14.6528 20.7535 13.9675 21.3516 13.5681 21.6939C13.3828 21.8527 13.1107 21.8511 12.9265 21.6911C12.5088 21.3282 11.7818 20.688 11.3652 20.2714C10.9452 19.8515 10.2982 19.1162 9.93691 18.7001Z" fill="url(#paint34_linear_18_32062)"/>
+<path d="M9.93691 18.7001C9.78078 18.5203 9.77539 18.2563 9.92775 18.0732C10.2215 17.7203 10.7401 17.1193 11.3475 16.5119C11.9543 15.9051 12.5517 15.39 12.9026 15.0983C13.0851 14.9465 13.348 14.9516 13.5275 15.1069C13.9345 15.459 14.6508 16.0883 15.0995 16.537C15.5446 16.9821 16.1672 17.6903 16.521 18.099C16.6803 18.283 16.6813 18.5541 16.5229 18.7389C16.1808 19.1382 15.582 19.8242 15.1172 20.2891C14.6528 20.7535 13.9675 21.3516 13.5681 21.6939C13.3828 21.8527 13.1107 21.8511 12.9265 21.6911C12.5088 21.3282 11.7818 20.688 11.3652 20.2714C10.9452 19.8515 10.2982 19.1162 9.93691 18.7001Z" fill="url(#paint35_linear_18_32062)"/>
+</g>
+<path d="M10.2578 21.7831C10.2578 21.7831 16.5546 28.3456 17.664 29.33C18.7734 30.3144 19.9921 30.1269 20.9453 29.33C21.8984 28.5331 28.3203 22.0956 29.3359 21.0331C30.3515 19.9706 30.0703 18.5488 29.1796 17.6581C28.289 16.7675 21.7109 10.2519 21.7109 10.2519L10.2578 21.7831Z" fill="url(#paint36_linear_18_32062)"/>
+<path d="M10.2578 21.7831C10.2578 21.7831 16.5546 28.3456 17.664 29.33C18.7734 30.3144 19.9921 30.1269 20.9453 29.33C21.8984 28.5331 28.3203 22.0956 29.3359 21.0331C30.3515 19.9706 30.0703 18.5488 29.1796 17.6581C28.289 16.7675 21.7109 10.2519 21.7109 10.2519L10.2578 21.7831Z" fill="url(#paint37_linear_18_32062)"/>
+<g filter="url(#filter7_f_18_32062)">
+<path d="M12.1329 21.5234C12.1329 21.5234 16.8586 26.498 17.8978 27.4201C18.937 28.3421 19.376 28.869 20.2688 28.1226C21.1616 27.3762 27.1771 21.3461 28.1284 20.3508C29.0798 19.3556 28.6577 18.7408 27.8235 17.9066C26.9892 17.0723 22 12.1416 22 12.1416L12.1329 21.5234Z" fill="#D3245A"/>
+<path d="M12.1329 21.5234C12.1329 21.5234 16.8586 26.498 17.8978 27.4201C18.937 28.3421 19.376 28.869 20.2688 28.1226C21.1616 27.3762 27.1771 21.3461 28.1284 20.3508C29.0798 19.3556 28.6577 18.7408 27.8235 17.9066C26.9892 17.0723 22 12.1416 22 12.1416L12.1329 21.5234Z" fill="url(#paint38_linear_18_32062)"/>
+<path d="M12.1329 21.5234C12.1329 21.5234 16.8586 26.498 17.8978 27.4201C18.937 28.3421 19.376 28.869 20.2688 28.1226C21.1616 27.3762 27.1771 21.3461 28.1284 20.3508C29.0798 19.3556 28.6577 18.7408 27.8235 17.9066C26.9892 17.0723 22 12.1416 22 12.1416L12.1329 21.5234Z" fill="url(#paint39_linear_18_32062)"/>
+</g>
+<g filter="url(#filter8_f_18_32062)">
+<path d="M22.4907 12.0862C22.4907 12.0862 14.132 23.4293 13.8188 23.7425C13.5057 24.0557 13.2494 24.1126 12.8223 23.8422C12.3952 23.5717 11.0855 22.0342 10.872 21.7922C10.6584 21.5501 10.6653 21.2495 10.9466 20.987C11.2279 20.7244 21.1525 10.8406 21.1525 10.8406C21.3127 10.6804 21.6015 10.7222 21.7291 10.8406C21.7291 10.8406 22.3128 11.3388 22.4907 11.5168C22.6687 11.6948 22.5818 11.9587 22.4907 12.0862Z" fill="#572916"/>
+</g>
+<path d="M23.2266 11.5469C23.2266 11.5469 13.7188 24.4531 13.375 24.7969C13.0312 25.1406 12.75 25.2031 12.2812 24.9063C11.8125 24.6094 10.375 22.9219 10.1406 22.6563C9.90625 22.3906 9.91374 22.0606 10.2225 21.7725C10.5312 21.4844 21.7578 10.1797 21.7578 10.1797C21.9336 10.0039 22.2506 10.0497 22.3906 10.1797C22.3906 10.1797 23.0312 10.7266 23.2266 10.9219C23.4219 11.1172 23.3266 11.4069 23.2266 11.5469Z" fill="url(#paint40_linear_18_32062)"/>
+<g filter="url(#filter9_f_18_32062)">
+<path d="M22.875 11.2812C22.875 10.8281 22.375 10.3438 21.9219 10.5C21.4688 10.6562 16.5 16.2812 16.5 16.2812L18.0781 17.5468C19.6615 15.651 22.875 11.7422 22.875 11.2812Z" fill="url(#paint41_linear_18_32062)"/>
+</g>
+<defs>
+<filter id="filter0_f_18_32062" x="2.37329" y="8.35474" width="9.99959" height="10.0206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter1_f_18_32062" x="4.0197" y="9.22174" width="7.32452" height="7.32471" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter2_f_18_32062" x="9.21066" y="3.94391" width="7.44289" height="7.44305" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter3_f_18_32062" x="13.0004" y="8.55963" width="9.99959" height="10.0206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter4_f_18_32062" x="14.7384" y="9.53424" width="7.32452" height="7.32471" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter5_f_18_32062" x="7.87329" y="13.7891" width="9.99959" height="10.0206" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter6_f_18_32062" x="9.56657" y="14.7374" width="7.32452" height="7.32471" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter7_f_18_32062" x="11.1329" y="11.1417" width="18.5395" height="18.3354" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter8_f_18_32062" x="9.72244" y="9.73553" width="13.8717" height="15.2796" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<filter id="filter9_f_18_32062" x="16" y="9.97003" width="7.375" height="8.07678" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_32062"/>
+</filter>
+<radialGradient id="paint0_radial_18_32062" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.375 5.875) rotate(135) scale(12.3302 6.13734)">
+<stop stop-color="#735040"/>
+<stop offset="1" stop-color="#724A3A" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint1_radial_18_32062" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(4.03125 18.9062) rotate(45) scale(11.5596 8.97142)">
+<stop stop-color="#834D4F"/>
+<stop offset="1" stop-color="#834B4D" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint2_radial_18_32062" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.5 6.6875) rotate(45) scale(10.1647 3.13779)">
+<stop stop-color="#AC7A64"/>
+<stop offset="1" stop-color="#AC7A63" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint3_linear_18_32062" x1="6.59375" y1="14.8125" x2="5.3125" y2="16.0937" gradientUnits="userSpaceOnUse">
+<stop stop-color="#835355" stop-opacity="0"/>
+<stop offset="0.542683" stop-color="#825254"/>
+</linearGradient>
+<linearGradient id="paint4_linear_18_32062" x1="6.90625" y1="14.25" x2="5.625" y2="15.5312" gradientUnits="userSpaceOnUse">
+<stop stop-color="#835355" stop-opacity="0"/>
+<stop offset="0.542683" stop-color="#825254"/>
+</linearGradient>
+<linearGradient id="paint5_linear_18_32062" x1="9.55469" y1="11.6016" x2="10.375" y2="10.7813" gradientUnits="userSpaceOnUse">
+<stop stop-color="#91634B" stop-opacity="0"/>
+<stop offset="0.257143" stop-color="#8E5E45"/>
+<stop offset="0.971429" stop-color="#79452B"/>
+</linearGradient>
+<linearGradient id="paint6_linear_18_32062" x1="5.3125" y1="10.5625" x2="6.25781" y2="11.5078" gradientUnits="userSpaceOnUse">
+<stop stop-color="#754A38"/>
+<stop offset="0.198347" stop-color="#764B38"/>
+<stop offset="1" stop-color="#8A5648" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint7_linear_18_32062" x1="10.4375" y1="15.0313" x2="9.54687" y2="14.1406" gradientUnits="userSpaceOnUse">
+<stop offset="0.192983" stop-color="#7F4131"/>
+<stop offset="0.508772" stop-color="#884B3F"/>
+<stop offset="1" stop-color="#905452" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint8_linear_18_32062" x1="4.71562" y1="13.599" x2="10.6952" y2="13.5756" gradientUnits="userSpaceOnUse">
+<stop stop-color="#865345"/>
+<stop offset="1" stop-color="#945C4E"/>
+</linearGradient>
+<linearGradient id="paint9_linear_18_32062" x1="5.9333" y1="14.7546" x2="9.4925" y2="11.2617" gradientUnits="userSpaceOnUse">
+<stop offset="0.0276698" stop-color="#7F4A40"/>
+<stop offset="0.183829" stop-color="#844A45" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint10_linear_18_32062" x1="9.61" y1="11.1453" x2="6.4587" y2="14.2966" gradientUnits="userSpaceOnUse">
+<stop offset="0.0194806" stop-color="#9B705C"/>
+<stop offset="0.204546" stop-color="#9B705C" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint11_linear_18_32062" x1="12.0937" y1="9.09375" x2="10.8125" y2="10.375" gradientUnits="userSpaceOnUse">
+<stop offset="0.402439" stop-color="#7A3C35" stop-opacity="0"/>
+<stop offset="0.865854" stop-color="#783930"/>
+<stop offset="1" stop-color="#713126"/>
+</linearGradient>
+<linearGradient id="paint12_linear_18_32062" x1="14.6562" y1="6.46875" x2="15.5625" y2="5.625" gradientUnits="userSpaceOnUse">
+<stop stop-color="#A87760" stop-opacity="0"/>
+<stop offset="0.342675" stop-color="#A8775F"/>
+<stop offset="1" stop-color="#A3735A"/>
+</linearGradient>
+<linearGradient id="paint13_linear_18_32062" x1="10.5" y1="5.40625" x2="11.4453" y2="6.35156" gradientUnits="userSpaceOnUse">
+<stop stop-color="#754A38"/>
+<stop offset="0.198347" stop-color="#764B38"/>
+<stop offset="1" stop-color="#8A5648" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint14_linear_18_32062" x1="15.625" y1="9.875" x2="14.7344" y2="8.98438" gradientUnits="userSpaceOnUse">
+<stop offset="0.192983" stop-color="#7F4131"/>
+<stop offset="0.508772" stop-color="#884B3F"/>
+<stop offset="1" stop-color="#905452" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint15_linear_18_32062" x1="9.90402" y1="8.44186" x2="15.9816" y2="8.5181" gradientUnits="userSpaceOnUse">
+<stop stop-color="#8B5A4A"/>
+<stop offset="1" stop-color="#9B6351"/>
+</linearGradient>
+<linearGradient id="paint16_linear_18_32062" x1="11.1227" y1="9.59642" x2="13.8559" y2="6.86243" gradientUnits="userSpaceOnUse">
+<stop offset="0.0276698" stop-color="#804643"/>
+<stop offset="0.183829" stop-color="#874B4F" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint17_linear_18_32062" x1="14.9214" y1="5.86515" x2="12.6745" y2="8.00623" gradientUnits="userSpaceOnUse">
+<stop offset="0.0194806" stop-color="#9B705C"/>
+<stop offset="0.204546" stop-color="#9B705C" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint18_linear_18_32062" x1="17.2209" y1="15.0174" x2="15.9396" y2="16.2986" gradientUnits="userSpaceOnUse">
+<stop offset="0.109756" stop-color="#6D3F3D" stop-opacity="0"/>
+<stop offset="0.670732" stop-color="#643026"/>
+<stop offset="1" stop-color="#5D2A1F"/>
+</linearGradient>
+<linearGradient id="paint19_linear_18_32062" x1="19.8693" y1="12.369" x2="20.6896" y2="11.5486" gradientUnits="userSpaceOnUse">
+<stop stop-color="#91634B" stop-opacity="0"/>
+<stop offset="0.257143" stop-color="#8E5E45"/>
+<stop offset="0.971429" stop-color="#79452B"/>
+</linearGradient>
+<linearGradient id="paint20_linear_18_32062" x1="15.6271" y1="11.3299" x2="16.5724" y2="12.2752" gradientUnits="userSpaceOnUse">
+<stop stop-color="#733821"/>
+<stop offset="0.198347" stop-color="#773D25"/>
+<stop offset="1" stop-color="#7F4B35" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint21_linear_18_32062" x1="17.625" y1="14.5625" x2="16.3437" y2="15.8437" gradientUnits="userSpaceOnUse">
+<stop offset="0.109756" stop-color="#6D3F3D" stop-opacity="0"/>
+<stop offset="0.670732" stop-color="#643026"/>
+<stop offset="1" stop-color="#5D2A1F"/>
+</linearGradient>
+<linearGradient id="paint22_linear_18_32062" x1="20.2734" y1="11.9141" x2="21.0938" y2="11.0938" gradientUnits="userSpaceOnUse">
+<stop stop-color="#91634B" stop-opacity="0"/>
+<stop offset="0.257143" stop-color="#8E5E45"/>
+<stop offset="0.971429" stop-color="#79452B"/>
+</linearGradient>
+<linearGradient id="paint23_linear_18_32062" x1="16.0312" y1="10.875" x2="16.9766" y2="11.8203" gradientUnits="userSpaceOnUse">
+<stop stop-color="#733821"/>
+<stop offset="0.198347" stop-color="#773D25"/>
+<stop offset="1" stop-color="#7F4B35" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint24_linear_18_32062" x1="21.1562" y1="15.3438" x2="20.2656" y2="14.4531" gradientUnits="userSpaceOnUse">
+<stop offset="0.192983" stop-color="#7F4131"/>
+<stop offset="0.508772" stop-color="#884B3F"/>
+<stop offset="1" stop-color="#905452" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint25_linear_18_32062" x1="15.4344" y1="13.9115" x2="21.414" y2="13.8881" gradientUnits="userSpaceOnUse">
+<stop stop-color="#865345"/>
+<stop offset="1" stop-color="#945C4E"/>
+</linearGradient>
+<linearGradient id="paint26_linear_18_32062" x1="16.652" y1="15.0671" x2="20.2113" y2="11.5742" gradientUnits="userSpaceOnUse">
+<stop offset="0.0276698" stop-color="#7F4A40"/>
+<stop offset="0.183829" stop-color="#844A45" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint27_linear_18_32062" x1="20.3288" y1="11.4578" x2="17.1775" y2="14.6091" gradientUnits="userSpaceOnUse">
+<stop offset="0.0194806" stop-color="#9B705C"/>
+<stop offset="0.204546" stop-color="#9B705C" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint28_linear_18_32062" x1="11.375" y1="21.375" x2="9.21875" y2="19.2188" gradientUnits="userSpaceOnUse">
+<stop stop-color="#64342F"/>
+<stop offset="1" stop-color="#773C41"/>
+</linearGradient>
+<linearGradient id="paint29_linear_18_32062" x1="12.4531" y1="19.7656" x2="11.1719" y2="21.0469" gradientUnits="userSpaceOnUse">
+<stop stop-color="#835355" stop-opacity="0"/>
+<stop offset="0.542683" stop-color="#825254"/>
+</linearGradient>
+<linearGradient id="paint30_linear_18_32062" x1="15.1016" y1="17.1172" x2="15.9219" y2="16.2969" gradientUnits="userSpaceOnUse">
+<stop stop-color="#91634B" stop-opacity="0"/>
+<stop offset="0.257143" stop-color="#8E5E45"/>
+<stop offset="0.971429" stop-color="#79452B"/>
+</linearGradient>
+<linearGradient id="paint31_linear_18_32062" x1="10.8594" y1="16.0781" x2="11.8047" y2="17.0234" gradientUnits="userSpaceOnUse">
+<stop stop-color="#733821"/>
+<stop offset="0.198347" stop-color="#783F28"/>
+<stop offset="1" stop-color="#7E4934" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint32_linear_18_32062" x1="15.9844" y1="20.5469" x2="15.0937" y2="19.6563" gradientUnits="userSpaceOnUse">
+<stop offset="0.192983" stop-color="#7F4131"/>
+<stop offset="0.508772" stop-color="#884B3F"/>
+<stop offset="1" stop-color="#905452" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint33_linear_18_32062" x1="10.2625" y1="19.1146" x2="16.2421" y2="19.0912" gradientUnits="userSpaceOnUse">
+<stop stop-color="#865345"/>
+<stop offset="1" stop-color="#945C4E"/>
+</linearGradient>
+<linearGradient id="paint34_linear_18_32062" x1="11.4802" y1="20.2702" x2="15.0394" y2="16.7774" gradientUnits="userSpaceOnUse">
+<stop offset="0.0276698" stop-color="#7F4A40"/>
+<stop offset="0.183829" stop-color="#844A45" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint35_linear_18_32062" x1="15.1569" y1="16.6609" x2="12.0056" y2="19.8122" gradientUnits="userSpaceOnUse">
+<stop offset="0.0194806" stop-color="#9B705C"/>
+<stop offset="0.204546" stop-color="#9B705C" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint36_linear_18_32062" x1="26.9375" y1="16.3125" x2="15.75" y2="27.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="#ED4253"/>
+<stop offset="1" stop-color="#C41959"/>
+</linearGradient>
+<linearGradient id="paint37_linear_18_32062" x1="25.9375" y1="25.125" x2="22.6875" y2="21.875" gradientUnits="userSpaceOnUse">
+<stop stop-color="#D61E58"/>
+<stop offset="0.932692" stop-color="#D71D59" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint38_linear_18_32062" x1="25.75" y1="15.125" x2="22.8437" y2="18.0312" gradientUnits="userSpaceOnUse">
+<stop offset="0.0860215" stop-color="#EA4B6C"/>
+<stop offset="0.693548" stop-color="#EC4B6C" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint39_linear_18_32062" x1="17.4375" y1="18.5" x2="18.8438" y2="19.5313" gradientUnits="userSpaceOnUse">
+<stop offset="0.384393" stop-color="#B3154F"/>
+<stop offset="0.766859" stop-color="#B71550" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint40_linear_18_32062" x1="23.3401" y1="11.4724" x2="11.25" y2="23.625" gradientUnits="userSpaceOnUse">
+<stop stop-color="#C1C1C2"/>
+<stop offset="0.0875295" stop-color="#ADABB2"/>
+<stop offset="0.198407" stop-color="#B1A4C2"/>
+<stop offset="0.757899" stop-color="#B3A5C6"/>
+<stop offset="0.879116" stop-color="#A892C1"/>
+<stop offset="1" stop-color="#A485C3"/>
+</linearGradient>
+<linearGradient id="paint41_linear_18_32062" x1="22.25" y1="10.7831" x2="16.5" y2="16.5331" gradientUnits="userSpaceOnUse">
+<stop stop-color="#D4D1DB"/>
+<stop offset="1" stop-color="#B3A6C5" stop-opacity="0"/>
+</linearGradient>
+</defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/cookie_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/cookie_color.svg
new file mode 100644
index 0000000000..42b628cca1
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/cookie_color.svg
@@ -0,0 +1,116 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M2 16C2 25.29 8.27 30 16 30C23.73 30 30 25.26 30 16C30 6.57 23.73 2 16 2C8.27 2 2 6.43 2 16Z" fill="#DDB78F"/>
+<path d="M2 16C2 25.29 8.27 30 16 30C23.73 30 30 25.26 30 16C30 6.57 23.73 2 16 2C8.27 2 2 6.43 2 16Z" fill="url(#paint0_radial_18_31720)"/>
+<path d="M2 16C2 25.29 8.27 30 16 30C23.73 30 30 25.26 30 16C30 6.57 23.73 2 16 2C8.27 2 2 6.43 2 16Z" fill="url(#paint1_radial_18_31720)"/>
+<path d="M2 16C2 25.29 8.27 30 16 30C23.73 30 30 25.26 30 16C30 6.57 23.73 2 16 2C8.27 2 2 6.43 2 16Z" fill="url(#paint2_radial_18_31720)"/>
+<path d="M26.92 14.61L26.96 13.99C27 13.42 26.52 12.94 25.95 12.98L25.34 13.02C25.1 13.03 24.85 13.11 24.63 13.24C24.01 13.63 23.76 14.42 24.04 15.09C24.38 15.89 25.32 16.23 26.09 15.85C26.59 15.62 26.88 15.13 26.92 14.61Z" fill="#6F434A"/>
+<g filter="url(#filter0_f_18_31720)">
+<path d="M26.7344 13.875C26.7688 13.3847 26.4531 13.25 26.0156 13.25C25.5781 13.25 25.1653 13.4633 24.8125 13.6718C24.2792 14.0073 23.9955 14.43 24.2364 15.0063C24.5288 15.6944 25.0877 15.7019 25.75 15.375C26.3626 15.0932 26.5801 14.4399 26.7344 13.875Z" fill="url(#paint3_radial_18_31720)"/>
+</g>
+<path d="M19.89 8.32001L20.51 8.36001C21.08 8.40001 21.56 7.92001 21.52 7.35001L21.48 6.74001C21.47 6.50001 21.39 6.25001 21.26 6.03001C20.87 5.41001 20.08 5.16001 19.41 5.44001C18.61 5.78001 18.27 6.72001 18.65 7.49001C18.88 7.99001 19.37 8.28001 19.89 8.32001Z" fill="#6F434A"/>
+<g filter="url(#filter1_f_18_31720)">
+<path d="M20.0001 7.68832L20.4808 7.71707C20.9227 7.74582 21.2948 7.40083 21.2638 6.99115L21.2328 6.55273C21.2251 6.38023 21.163 6.20055 21.0622 6.04243C20.7599 5.59681 20.1474 5.41713 19.6279 5.61837C19.0077 5.86274 18.7441 6.53835 19.0387 7.09178C19.217 7.45114 19.5969 7.65957 20.0001 7.68832Z" fill="url(#paint4_radial_18_31720)"/>
+</g>
+<path d="M10.67 23.75L10.62 24.52C10.57 25.23 11.16 25.82 11.87 25.77L12.63 25.72C12.93 25.7 13.23 25.61 13.51 25.44C14.28 24.96 14.59 23.98 14.24 23.14C13.82 22.14 12.65 21.72 11.7 22.2C11.08 22.51 10.71 23.11 10.67 23.75Z" fill="#6F434A"/>
+<g filter="url(#filter2_f_18_31720)">
+<path d="M10.9819 23.765L10.9402 24.4071C10.8985 24.9991 11.3905 25.4911 11.9825 25.4494L12.6162 25.4077C12.8664 25.3911 13.1165 25.316 13.35 25.1743C13.9921 24.774 14.2506 23.9568 13.9587 23.2564C13.6085 22.4225 12.6329 22.0723 11.8407 22.4726C11.3238 22.7311 11.0152 23.2314 10.9819 23.765Z" fill="url(#paint5_radial_18_31720)"/>
+</g>
+<path d="M20.4 15.19L20.43 14.73C20.53 13.22 19.28 11.97 17.76 12.06L17.31 12.09C16.86 12.11 16.41 12.24 15.99 12.49C14.75 13.22 14.22 14.78 14.77 16.11C15.42 17.7 17.27 18.37 18.78 17.62C19.76 17.15 20.34 16.2 20.4 15.19Z" fill="#6F434A"/>
+<g filter="url(#filter3_f_18_31720)">
+<path d="M20.024 14.9836L20.0483 14.6099C20.1296 13.3833 19.1142 12.368 17.8795 12.4411L17.514 12.4655C17.1485 12.4817 16.7829 12.5873 16.4418 12.7904C15.4345 13.3833 15.004 14.6505 15.4508 15.7309C15.9788 17.0224 17.4815 17.5666 18.7081 16.9574C19.5041 16.5756 19.9752 15.804 20.024 14.9836Z" fill="url(#paint6_radial_18_31720)"/>
+</g>
+<path d="M7.68 9.41994L7.65 8.99994C7.57 7.62994 8.7 6.48994 10.07 6.57994L10.48 6.60994C10.89 6.62994 11.3 6.74994 11.68 6.96994C12.81 7.62994 13.28 9.04994 12.79 10.2599C12.2 11.6999 10.52 12.3099 9.15 11.6299C8.27 11.1899 7.74 10.3299 7.68 9.41994Z" fill="url(#paint7_radial_18_31720)"/>
+<g filter="url(#filter4_f_18_31720)">
+<path d="M8.15663 9.38764L8.13206 9.04363C8.06654 7.92149 8.99209 6.98774 10.1142 7.06146L10.45 7.08603C10.7859 7.10241 11.2825 7.0698 11.5938 7.25C12.5193 7.79059 12.8076 8.94642 12.4062 9.9375C11.923 11.117 10.4828 11.7548 9.36068 11.1978C8.63989 10.8374 8.20578 10.133 8.15663 9.38764Z" fill="url(#paint8_radial_18_31720)"/>
+</g>
+<path d="M24.26 22.82L24.28 23.18C24.36 24.35 23.38 25.33 22.21 25.25L21.86 25.23C21.51 25.21 21.16 25.11 20.83 24.92C19.86 24.35 19.46 23.14 19.88 22.11C20.39 20.88 21.82 20.35 23 20.94C23.76 21.3 24.21 22.03 24.26 22.82Z" fill="#6F434A"/>
+<g filter="url(#filter5_f_18_31720)">
+<path d="M24.0262 22.7197L24.043 23.0224C24.1103 24.0064 23.2861 24.8306 22.3021 24.7633L22.0078 24.7465C21.7134 24.7297 21.4191 24.6456 21.1415 24.4858C20.3258 24.0064 19.9893 22.9888 20.3426 22.1226C20.7715 21.0881 21.9741 20.6424 22.9665 21.1386C23.6057 21.4413 23.9842 22.0553 24.0262 22.7197Z" fill="url(#paint9_radial_18_31720)"/>
+</g>
+<path d="M5.90999 17.54L5.86999 16.92C5.82999 16.35 6.30999 15.87 6.87999 15.91L7.48999 15.95C7.72999 15.96 7.97999 16.04 8.19999 16.17C8.81999 16.56 9.06999 17.35 8.78999 18.02C8.44999 18.82 7.50999 19.16 6.73999 18.78C6.23999 18.55 5.93999 18.06 5.90999 17.54Z" fill="#6F434A"/>
+<g filter="url(#filter6_f_18_31720)">
+<path d="M6.21167 17.5158L6.17985 17.0226C6.14803 16.5691 6.52991 16.1872 6.9834 16.219L7.46872 16.2508C7.65966 16.2588 7.85856 16.3224 8.03359 16.4259C8.52686 16.7362 8.72576 17.3647 8.50299 17.8977C8.23249 18.5342 7.48463 18.8047 6.87202 18.5024C6.47422 18.3194 6.23554 17.9296 6.21167 17.5158Z" fill="url(#paint10_radial_18_31720)"/>
+</g>
+<defs>
+<filter id="filter0_f_18_31720" x="23.6498" y="12.75" width="3.58717" height="3.33459" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_31720"/>
+</filter>
+<filter id="filter1_f_18_31720" x="18.4181" y="5.03638" width="3.34752" height="3.18237" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_31720"/>
+</filter>
+<filter id="filter2_f_18_31720" x="10.4377" y="21.807" width="4.14041" height="4.1449" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_31720"/>
+</filter>
+<filter id="filter3_f_18_31720" x="14.7748" y="11.9374" width="5.77806" height="5.76941" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_31720"/>
+</filter>
+<filter id="filter4_f_18_31720" x="7.62878" y="6.55737" width="5.45396" height="5.34436" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_31720"/>
+</filter>
+<filter id="filter5_f_18_31720" x="19.7031" y="20.4355" width="4.84375" height="4.83167" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_31720"/>
+</filter>
+<filter id="filter6_f_18_31720" x="5.67799" y="15.7172" width="3.41576" height="3.40894" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_31720"/>
+</filter>
+<radialGradient id="paint0_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.4375 7.8125) rotate(119.554) scale(20.9077)">
+<stop stop-color="#FFDAAE"/>
+<stop offset="1" stop-color="#D59077" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint1_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.375 16) rotate(83.2902) scale(17.1172 16.2696)">
+<stop offset="0.772068" stop-color="#BF9E7A" stop-opacity="0"/>
+<stop offset="1" stop-color="#C4A47E"/>
+</radialGradient>
+<radialGradient id="paint2_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(19.375 6.5) rotate(92.6808) scale(37.4159)">
+<stop offset="0.324983" stop-color="#E9AB8B" stop-opacity="0"/>
+<stop offset="0.505034" stop-color="#DE9A80"/>
+<stop offset="0.656015" stop-color="#D07067"/>
+</radialGradient>
+<radialGradient id="paint3_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(26.6119 13.3594) rotate(140.104) scale(2.75277 2.61177)">
+<stop offset="0.175443" stop-color="#886562"/>
+<stop offset="1" stop-color="#8E6C67" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint4_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21.2656 7.41039) rotate(-149.903) scale(2.90043 2.79159)">
+<stop offset="0.175443" stop-color="#886562"/>
+<stop offset="1" stop-color="#8E6C67" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint5_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11.1292 25.2582) rotate(-41.0159) scale(3.90831 4.00044)">
+<stop offset="0.469854" stop-color="#896764" stop-opacity="0"/>
+<stop offset="0.935135" stop-color="#896763"/>
+</radialGradient>
+<radialGradient id="paint6_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.2748 16) rotate(-21.7768) scale(5.55942 5.69235)">
+<stop offset="0.388269" stop-color="#896764" stop-opacity="0"/>
+<stop offset="0.935135" stop-color="#896763"/>
+</radialGradient>
+<radialGradient id="paint7_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11.125 8.875) rotate(123.403) scale(3.63294 3.63565)">
+<stop stop-color="#7D5755"/>
+<stop offset="1" stop-color="#60383B"/>
+</radialGradient>
+<radialGradient id="paint8_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.12878 9.5625) rotate(-3.96087) scale(4.82023 4.82383)">
+<stop offset="0.596528" stop-color="#896764" stop-opacity="0"/>
+<stop offset="0.935135" stop-color="#896763"/>
+</radialGradient>
+<radialGradient id="paint9_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(20.4375 24.5313) rotate(-40.886) scale(4.77422 4.88362)">
+<stop offset="0.469854" stop-color="#896764" stop-opacity="0"/>
+<stop offset="0.935135" stop-color="#896763"/>
+</radialGradient>
+<radialGradient id="paint10_radial_18_31720" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.3253 18.4778) rotate(-40.8959) scale(3.00101 3.06992)">
+<stop offset="0.469854" stop-color="#896764" stop-opacity="0"/>
+<stop offset="0.935135" stop-color="#896763"/>
+</radialGradient>
+</defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/custard_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/custard_color.svg
new file mode 100644
index 0000000000..d967fec7fe
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/custard_color.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <g transform="matrix(1,0,0,1,0,-6.5)">
+ <path d="M7.97,16.47L5.61,26L26.38,26L24.08,16.48C23.88,15.61 23.1,15 22.21,15L9.84,15C8.96,15 8.18,15.61 7.97,16.47Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
+ <path d="M9.84,15C8.96,15 8.18,15.61 7.97,16.47L7.34,19L24.7,19L24.09,16.48C23.88,15.61 23.1,15 22.21,15L9.84,15Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
+ <path d="M4,28C5.28,29.28 7.02,30 8.83,30L23.17,30C24.98,30 26.72,29.28 28,28L4,28Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
+ <path d="M4,28C5.28,29.28 7.02,30 8.83,30L23.17,30C24.98,30 26.72,29.28 28,28L4,28Z" style="fill:url(#_Linear4);fill-rule:nonzero;"/>
+ <path d="M29,28L3,28C2.45,28 2,27.55 2,27C2,26.45 2.45,26 3,26L29,26C29.55,26 30,26.45 30,27C30,27.55 29.55,28 29,28Z" style="fill:url(#_Linear5);fill-rule:nonzero;"/>
+ <path d="M29,28L3,28C2.45,28 2,27.55 2,27C2,26.45 2.45,26 3,26L29,26C29.55,26 30,26.45 30,27C30,27.55 29.55,28 29,28Z" style="fill:url(#_Radial6);fill-rule:nonzero;"/>
+ <path d="M29,28L3,28C2.45,28 2,27.55 2,27C2,26.45 2.45,26 3,26L29,26C29.55,26 30,26.45 30,27C30,27.55 29.55,28 29,28Z" style="fill:url(#_Radial7);fill-rule:nonzero;"/>
+ </g>
+ <defs>
+ <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(20.77,0,0,20.77,5.61,26)"><stop offset="0" style="stop-color:rgb(148,107,83);stop-opacity:1"/><stop offset="0.25" style="stop-color:rgb(166,108,58);stop-opacity:1"/><stop offset="0.54" style="stop-color:rgb(204,139,83);stop-opacity:1"/><stop offset="0.75" style="stop-color:rgb(224,165,108);stop-opacity:1"/><stop offset="0.86" style="stop-color:rgb(230,165,103);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(204,153,104);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(16.8875,0,0,16.8875,7.8125,17.3125)"><stop offset="0" style="stop-color:rgb(113,76,64);stop-opacity:1"/><stop offset="0.25" style="stop-color:rgb(122,74,57);stop-opacity:1"/><stop offset="0.49" style="stop-color:rgb(149,95,75);stop-opacity:1"/><stop offset="0.78" style="stop-color:rgb(180,128,107);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(172,121,98);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(23.625,0,0,23.625,4.375,30)"><stop offset="0" style="stop-color:rgb(173,153,193);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(173,150,195);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.0313,1.9219,-1.9219,-0.0313,16.2813,26.5469)"><stop offset="0" style="stop-color:rgb(152,131,172);stop-opacity:1"/><stop offset="0.73" style="stop-color:rgb(152,131,172);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(156,132,180);stop-opacity:0"/></linearGradient>
+ <linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(28.3125,0,0,28.3125,2,27)"><stop offset="0" style="stop-color:rgb(134,133,137);stop-opacity:1"/><stop offset="0.51" style="stop-color:rgb(172,170,172);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(188,185,194);stop-opacity:1"/></linearGradient>
+ <radialGradient id="_Radial6" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.99107e-16,0.78125,-14.5,-5.55142e-15,26.1875,26.7187)"><stop offset="0" style="stop-color:rgb(221,218,228);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(222,219,228);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-5.98214e-16,1.5625,-60.1813,-2.30408e-14,11.1875,28)"><stop offset="0" style="stop-color:rgb(175,152,197);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(174,152,197);stop-opacity:0"/></radialGradient>
+ </defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/doughnut_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/doughnut_color.svg
new file mode 100644
index 0000000000..e8e225bc0a
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/doughnut_color.svg
@@ -0,0 +1,272 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M27.32 14C25.67 9.34 21.22 6 16 6C10.78 6 6.33 9.34 4.68 14H4V18C4 24.63 9.37 30 16 30C22.63 30 28 24.63 28 18V14H27.32ZM20.5 17.64C20.5 19.85 18.71 21.64 16.5 21.64H15.5C13.29 21.64 11.5 19.85 11.5 17.64C11.5 15.43 13.29 14 15.5 14H16.5C18.71 14 20.5 15.43 20.5 17.64Z" fill="url(#paint0_linear_18_31224)"/>
+<path d="M27.32 14C25.67 9.34 21.22 6 16 6C10.78 6 6.33 9.34 4.68 14H4V18C4 24.63 9.37 30 16 30C22.63 30 28 24.63 28 18V14H27.32ZM20.5 17.64C20.5 19.85 18.71 21.64 16.5 21.64H15.5C13.29 21.64 11.5 19.85 11.5 17.64C11.5 15.43 13.29 14 15.5 14H16.5C18.71 14 20.5 15.43 20.5 17.64Z" fill="url(#paint1_radial_18_31224)"/>
+<path d="M27.32 14C25.67 9.34 21.22 6 16 6C10.78 6 6.33 9.34 4.68 14H4V18C4 24.63 9.37 30 16 30C22.63 30 28 24.63 28 18V14H27.32ZM20.5 17.64C20.5 19.85 18.71 21.64 16.5 21.64H15.5C13.29 21.64 11.5 19.85 11.5 17.64C11.5 15.43 13.29 14 15.5 14H16.5C18.71 14 20.5 15.43 20.5 17.64Z" fill="url(#paint2_radial_18_31224)"/>
+<path d="M16 2C9.37 2 3.8125 7.37 3.8125 14C3.8125 20.63 8.96875 26.3438 16 26.3438C23.0312 26.3438 28.25 20.63 28.25 14C28.25 7.37 22.63 2 16 2ZM20.5 14C20.5 16.21 18.71 18 16.5 18H15.5C13.29 18 11.5 16.21 11.5 14C11.5 12 13.29 10.2812 15.5 10.2812H16.5C18.71 10.2812 20.5 11.9688 20.5 14Z" fill="url(#paint3_radial_18_31224)"/>
+<path d="M16 2C9.37 2 3.8125 7.37 3.8125 14C3.8125 20.63 8.96875 26.3438 16 26.3438C23.0312 26.3438 28.25 20.63 28.25 14C28.25 7.37 22.63 2 16 2ZM20.5 14C20.5 16.21 18.71 18 16.5 18H15.5C13.29 18 11.5 16.21 11.5 14C11.5 12 13.29 10.2812 15.5 10.2812H16.5C18.71 10.2812 20.5 11.9688 20.5 14Z" fill="url(#paint4_radial_18_31224)"/>
+<path d="M16 2C9.37 2 3.8125 7.37 3.8125 14C3.8125 20.63 8.96875 26.3438 16 26.3438C23.0312 26.3438 28.25 20.63 28.25 14C28.25 7.37 22.63 2 16 2ZM20.5 14C20.5 16.21 18.71 18 16.5 18H15.5C13.29 18 11.5 16.21 11.5 14C11.5 12 13.29 10.2812 15.5 10.2812H16.5C18.71 10.2812 20.5 11.9688 20.5 14Z" fill="url(#paint5_radial_18_31224)"/>
+<g filter="url(#filter0_f_18_31224)">
+<path d="M24.4825 11.3693C24.6825 11.1693 24.6825 10.8293 24.4825 10.6293L23.7025 9.84934C23.5025 9.64934 23.1625 9.64934 22.9625 9.84934C22.7625 10.0493 22.7625 10.3893 22.9625 10.5893L23.7425 11.3693C23.9425 11.5793 24.2825 11.5793 24.4825 11.3693Z" fill="#783C43"/>
+</g>
+<g filter="url(#filter1_f_18_31224)">
+<path d="M23.0563 16.19C22.8563 15.99 22.8563 15.65 23.0563 15.45L23.8363 14.67C24.0363 14.47 24.3763 14.47 24.5763 14.67C24.7763 14.87 24.7763 15.21 24.5763 15.41L23.7963 16.19C23.5963 16.4 23.2563 16.4 23.0563 16.19Z" fill="#733D42"/>
+</g>
+<g filter="url(#filter2_f_18_31224)">
+<path d="M21.7019 20.335C21.9019 20.135 21.9019 19.795 21.7019 19.595L20.9219 18.815C20.7219 18.615 20.3819 18.615 20.1819 18.815C19.9819 19.015 19.9819 19.355 20.1819 19.555L20.9619 20.335C21.1619 20.545 21.5019 20.545 21.7019 20.335Z" fill="#7E3B46"/>
+</g>
+<g filter="url(#filter3_f_18_31224)">
+<path d="M14.0331 23.2882C13.8331 23.0882 13.8331 22.7482 14.0331 22.5482L14.8131 21.7682C15.0131 21.5682 15.3531 21.5682 15.5531 21.7682C15.7531 21.9682 15.7531 22.3082 15.5531 22.5082L14.7731 23.2882C14.5731 23.4982 14.2331 23.4982 14.0331 23.2882Z" fill="#823C46"/>
+</g>
+<path d="M24.7525 11.2912C24.5525 11.4912 24.2125 11.4912 24.0125 11.2912L23.2325 10.5112C23.0325 10.3112 23.0325 9.97122 23.2325 9.77122C23.4325 9.57122 23.7725 9.57122 23.9725 9.77122L24.7525 10.5512C24.9625 10.7512 24.9625 11.0912 24.7525 11.2912Z" fill="url(#paint6_linear_18_31224)"/>
+<path d="M24.7525 11.2912C24.5525 11.4912 24.2125 11.4912 24.0125 11.2912L23.2325 10.5112C23.0325 10.3112 23.0325 9.97122 23.2325 9.77122C23.4325 9.57122 23.7725 9.57122 23.9725 9.77122L24.7525 10.5512C24.9625 10.7512 24.9625 11.0912 24.7525 11.2912Z" fill="url(#paint7_radial_18_31224)"/>
+<g filter="url(#filter4_f_18_31224)">
+<path d="M15.9156 6.40181C15.7156 6.20181 15.7156 5.86181 15.9156 5.66181L16.6956 4.88181C16.8956 4.68181 17.2356 4.68181 17.4356 4.88181C17.6356 5.08181 17.6356 5.42181 17.4356 5.62181L16.6556 6.40181C16.4556 6.61181 16.1156 6.61181 15.9156 6.40181Z" fill="#67383D"/>
+</g>
+<g filter="url(#filter5_f_18_31224)">
+<path d="M20.9781 8.42061C20.7781 8.22061 20.7781 7.88061 20.9781 7.68061L21.7581 6.90061C21.9581 6.70061 22.2981 6.70061 22.4981 6.90061C22.6981 7.10061 22.6981 7.44061 22.4981 7.64061L21.7181 8.42061C21.5181 8.63061 21.1781 8.63061 20.9781 8.42061Z" fill="#6B3C40"/>
+</g>
+<path d="M16.15 6.27999C15.95 6.07999 15.95 5.73999 16.15 5.53999L16.93 4.75999C17.13 4.55999 17.47 4.55999 17.67 4.75999C17.87 4.95999 17.87 5.29999 17.67 5.49999L16.89 6.27999C16.69 6.48999 16.35 6.48999 16.15 6.27999Z" fill="url(#paint8_linear_18_31224)"/>
+<path d="M16.15 6.27999C15.95 6.07999 15.95 5.73999 16.15 5.53999L16.93 4.75999C17.13 4.55999 17.47 4.55999 17.67 4.75999C17.87 4.95999 17.87 5.29999 17.67 5.49999L16.89 6.27999C16.69 6.48999 16.35 6.48999 16.15 6.27999Z" fill="url(#paint9_radial_18_31224)"/>
+<g filter="url(#filter6_f_18_31224)">
+<path d="M8.06003 9.36556C7.86003 9.16556 7.86003 8.82556 8.06003 8.62556L8.84003 7.84556C9.04003 7.64556 9.38003 7.64556 9.58003 7.84556C9.78003 8.04556 9.78003 8.38556 9.58003 8.58556L8.80003 9.36556C8.60003 9.56556 8.26003 9.56556 8.06003 9.36556Z" fill="#62393D"/>
+</g>
+<path d="M8.33378 9.25618C8.13378 9.05618 8.13378 8.71618 8.33378 8.51618L9.11378 7.73618C9.31378 7.53618 9.65378 7.53618 9.85378 7.73618C10.0538 7.93618 10.0538 8.27618 9.85378 8.47618L9.07378 9.25618C8.87378 9.45618 8.53378 9.45618 8.33378 9.25618Z" fill="url(#paint10_linear_18_31224)"/>
+<path d="M8.33378 9.25618C8.13378 9.05618 8.13378 8.71618 8.33378 8.51618L9.11378 7.73618C9.31378 7.53618 9.65378 7.53618 9.85378 7.73618C10.0538 7.93618 10.0538 8.27618 9.85378 8.47618L9.07378 9.25618C8.87378 9.45618 8.53378 9.45618 8.33378 9.25618Z" fill="url(#paint11_radial_18_31224)"/>
+<path d="M14.33 23.1975C14.13 22.9975 14.13 22.6575 14.33 22.4575L15.11 21.6775C15.31 21.4775 15.65 21.4775 15.85 21.6775C16.05 21.8775 16.05 22.2175 15.85 22.4175L15.07 23.1975C14.87 23.4075 14.53 23.4075 14.33 23.1975Z" fill="url(#paint12_linear_18_31224)"/>
+<path d="M14.33 23.1975C14.13 22.9975 14.13 22.6575 14.33 22.4575L15.11 21.6775C15.31 21.4775 15.65 21.4775 15.85 21.6775C16.05 21.8775 16.05 22.2175 15.85 22.4175L15.07 23.1975C14.87 23.4075 14.53 23.4075 14.33 23.1975Z" fill="url(#paint13_radial_18_31224)"/>
+<path d="M21.8425 20.1624C21.6425 20.3624 21.3025 20.3624 21.1025 20.1624L20.3225 19.3824C20.1225 19.1824 20.1225 18.8424 20.3225 18.6424C20.5225 18.4424 20.8625 18.4424 21.0625 18.6424L21.8425 19.4224C22.0525 19.6224 22.0525 19.9624 21.8425 20.1624Z" fill="url(#paint14_linear_18_31224)"/>
+<path d="M21.8425 20.1624C21.6425 20.3624 21.3025 20.3624 21.1025 20.1624L20.3225 19.3824C20.1225 19.1824 20.1225 18.8424 20.3225 18.6424C20.5225 18.4424 20.8625 18.4424 21.0625 18.6424L21.8425 19.4224C22.0525 19.6224 22.0525 19.9624 21.8425 20.1624Z" fill="url(#paint15_radial_18_31224)"/>
+<path d="M23.24 16.1037C23.04 15.9037 23.04 15.5637 23.24 15.3637L24.02 14.5837C24.22 14.3837 24.56 14.3837 24.76 14.5837C24.96 14.7837 24.96 15.1237 24.76 15.3237L23.98 16.1037C23.78 16.3137 23.44 16.3137 23.24 16.1037Z" fill="url(#paint16_linear_18_31224)"/>
+<path d="M23.24 16.1037C23.04 15.9037 23.04 15.5637 23.24 15.3637L24.02 14.5837C24.22 14.3837 24.56 14.3837 24.76 14.5837C24.96 14.7837 24.96 15.1237 24.76 15.3237L23.98 16.1037C23.78 16.3137 23.44 16.3137 23.24 16.1037Z" fill="url(#paint17_radial_18_31224)"/>
+<g filter="url(#filter7_f_18_31224)">
+<path d="M11.618 21.2525C11.418 21.4525 11.078 21.4525 10.878 21.2525L10.098 20.4725C9.89801 20.2725 9.89801 19.9325 10.098 19.7325C10.298 19.5325 10.638 19.5325 10.838 19.7325L11.618 20.5125C11.828 20.7125 11.828 21.0525 11.618 21.2525Z" fill="#693D49"/>
+</g>
+<path d="M11.8375 21.0725C11.6375 21.2725 11.2975 21.2725 11.0975 21.0725L10.3175 20.2925C10.1175 20.0925 10.1175 19.7525 10.3175 19.5525C10.5175 19.3525 10.8575 19.3525 11.0575 19.5525L11.8375 20.3325C12.0475 20.5325 12.0475 20.8725 11.8375 21.0725Z" fill="url(#paint18_linear_18_31224)"/>
+<path d="M11.8375 21.0725C11.6375 21.2725 11.2975 21.2725 11.0975 21.0725L10.3175 20.2925C10.1175 20.0925 10.1175 19.7525 10.3175 19.5525C10.5175 19.3525 10.8575 19.3525 11.0575 19.5525L11.8375 20.3325C12.0475 20.5325 12.0475 20.8725 11.8375 21.0725Z" fill="url(#paint19_radial_18_31224)"/>
+<g filter="url(#filter8_f_18_31224)">
+<path d="M6.05543 14.1662C5.85543 13.9662 5.85543 13.6262 6.05543 13.4262L6.83543 12.6462C7.03543 12.4462 7.37543 12.4462 7.57543 12.6462C7.77543 12.8462 7.77543 13.1862 7.57543 13.3862L6.79543 14.1662C6.59543 14.3762 6.25543 14.3762 6.05543 14.1662Z" fill="#5A3840"/>
+</g>
+<g filter="url(#filter9_f_18_31224)">
+<path d="M12.5852 7.31929C12.3852 7.51929 12.0452 7.51929 11.8452 7.31929L11.0552 6.53929C10.8552 6.33929 10.8552 5.99929 11.0552 5.79929C11.2552 5.59929 11.5952 5.59929 11.7952 5.79929L12.5752 6.57929C12.7852 6.77929 12.7852 7.11929 12.5852 7.31929Z" fill="#5A3840"/>
+</g>
+<path d="M12.7613 7.19747C12.5613 7.39747 12.2213 7.39747 12.0213 7.19747L11.2313 6.41747C11.0313 6.21747 11.0313 5.87747 11.2313 5.67747C11.4313 5.47747 11.7713 5.47747 11.9713 5.67747L12.7513 6.45747C12.9613 6.65747 12.9613 6.99747 12.7613 7.19747Z" fill="url(#paint20_linear_18_31224)"/>
+<path d="M12.7613 7.19747C12.5613 7.39747 12.2213 7.39747 12.0213 7.19747L11.2313 6.41747C11.0313 6.21747 11.0313 5.87747 11.2313 5.67747C11.4313 5.47747 11.7713 5.47747 11.9713 5.67747L12.7513 6.45747C12.9613 6.65747 12.9613 6.99747 12.7613 7.19747Z" fill="url(#paint21_radial_18_31224)"/>
+<path d="M6.33379 14.0725C6.13379 13.8725 6.13379 13.5325 6.33379 13.3325L7.11379 12.5525C7.31379 12.3525 7.65379 12.3525 7.85379 12.5525C8.05379 12.7525 8.05379 13.0925 7.85379 13.2925L7.07379 14.0725C6.87379 14.2825 6.53379 14.2825 6.33379 14.0725Z" fill="url(#paint22_linear_18_31224)"/>
+<path d="M6.33379 14.0725C6.13379 13.8725 6.13379 13.5325 6.33379 13.3325L7.11379 12.5525C7.31379 12.3525 7.65379 12.3525 7.85379 12.5525C8.05379 12.7525 8.05379 13.0925 7.85379 13.2925L7.07379 14.0725C6.87379 14.2825 6.53379 14.2825 6.33379 14.0725Z" fill="url(#paint23_radial_18_31224)"/>
+<path d="M21.2363 8.3387C21.0363 8.1387 21.0363 7.7987 21.2363 7.5987L22.0163 6.8187C22.2163 6.6187 22.5563 6.6187 22.7563 6.8187C22.9563 7.0187 22.9563 7.3587 22.7563 7.5587L21.9763 8.3387C21.7763 8.5487 21.4363 8.5487 21.2363 8.3387Z" fill="url(#paint24_linear_18_31224)"/>
+<path d="M21.2363 8.3387C21.0363 8.1387 21.0363 7.7987 21.2363 7.5987L22.0163 6.8187C22.2163 6.6187 22.5563 6.6187 22.7563 6.8187C22.9563 7.0187 22.9563 7.3587 22.7563 7.5587L21.9763 8.3387C21.7763 8.5487 21.4363 8.5487 21.2363 8.3387Z" fill="url(#paint25_radial_18_31224)"/>
+<g filter="url(#filter10_f_18_31224)">
+<path d="M10.5019 17.4825C10.3019 17.6825 9.96191 17.6825 9.76191 17.4825L8.98191 16.7025C8.78191 16.5025 8.78191 16.1625 8.98191 15.9625C9.18191 15.7625 9.52191 15.7625 9.72191 15.9625L10.5019 16.7425C10.7119 16.9425 10.7119 17.2725 10.5019 17.4825Z" fill="#6F3A43"/>
+</g>
+<path d="M10.76 17.2638C10.56 17.4638 10.22 17.4638 10.02 17.2638L9.24003 16.4837C9.04003 16.2837 9.04003 15.9438 9.24003 15.7438C9.44003 15.5438 9.78003 15.5438 9.98003 15.7438L10.76 16.5237C10.97 16.7237 10.97 17.0538 10.76 17.2638Z" fill="url(#paint26_linear_18_31224)"/>
+<path d="M10.76 17.2638C10.56 17.4638 10.22 17.4638 10.02 17.2638L9.24003 16.4837C9.04003 16.2837 9.04003 15.9438 9.24003 15.7438C9.44003 15.5438 9.78003 15.5438 9.98003 15.7438L10.76 16.5237C10.97 16.7237 10.97 17.0538 10.76 17.2638Z" fill="url(#paint27_radial_18_31224)"/>
+<g filter="url(#filter11_f_18_31224)">
+<path d="M10.5781 10.4844C8.93209 13.1078 11.0312 15.5938 11.0312 15.5938C10.5298 14.3629 10.6853 12.5943 11.2031 11.5156C12.1052 9.63666 13.4219 8.45318 16.3125 8.25C13.5781 7.85943 11.7593 8.60176 10.5781 10.4844Z" fill="url(#paint28_radial_18_31224)"/>
+</g>
+<g filter="url(#filter12_f_18_31224)">
+<path d="M25.375 20.1875C20.625 25.75 12.6875 23.6874 12.6875 23.6874C19.2953 23.6874 22.2286 22.608 24.75 19.1249C27.2714 15.6417 26.9375 9.6874 24.125 5.65623C27.375 8.99994 28.6902 16.3051 25.375 20.1875Z" fill="url(#paint29_radial_18_31224)"/>
+</g>
+<defs>
+<filter id="filter0_f_18_31224" x="22.5625" y="9.44934" width="2.32001" height="2.32751" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter1_f_18_31224" x="22.6563" y="14.27" width="2.32001" height="2.32751" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter2_f_18_31224" x="19.7819" y="18.415" width="2.32001" height="2.32751" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter3_f_18_31224" x="13.6331" y="21.3682" width="2.32001" height="2.32751" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter4_f_18_31224" x="15.5156" y="4.48181" width="2.32001" height="2.32751" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter5_f_18_31224" x="20.5781" y="6.50061" width="2.32001" height="2.32751" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter6_f_18_31224" x="7.66003" y="7.44556" width="2.32001" height="2.31995" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter7_f_18_31224" x="9.69801" y="19.3325" width="2.3275" height="2.31995" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter8_f_18_31224" x="5.65543" y="12.2462" width="2.32001" height="2.32751" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter9_f_18_31224" x="10.6552" y="5.39929" width="2.32877" height="2.31995" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter10_f_18_31224" x="8.58191" y="15.5625" width="2.3275" height="2.31995" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter11_f_18_31224" x="8.94801" y="7.14905" width="8.36449" height="9.4447" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<filter id="filter12_f_18_31224" x="11.6875" y="4.65625" width="16.6161" height="20.4052" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_31224"/>
+</filter>
+<linearGradient id="paint0_linear_18_31224" x1="3.5625" y1="20.5625" x2="28.375" y2="20.5625" gradientUnits="userSpaceOnUse">
+<stop stop-color="#BE8A63"/>
+<stop offset="0.183879" stop-color="#DF8777"/>
+<stop offset="0.463476" stop-color="#F28886"/>
+<stop offset="0.803526" stop-color="#F0AE9C"/>
+<stop offset="1" stop-color="#EAC891"/>
+</linearGradient>
+<radialGradient id="paint1_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(16.75 11.6875) rotate(97.5709) scale(9.96184 9.16466)">
+<stop offset="0.405405" stop-color="#F48F75"/>
+<stop offset="0.598905" stop-color="#FFA85B"/>
+<stop offset="0.954955" stop-color="#FFA85B" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint2_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.6875 16.6875) rotate(90) scale(5.1875 5.88997)">
+<stop offset="0.53012" stop-color="#CD777A"/>
+<stop offset="1" stop-color="#D47A70" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint3_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.875 20.125) rotate(-135.234) scale(21.6553 18.6519)">
+<stop offset="0.393247" stop-color="#AB6B59"/>
+<stop offset="0.912271" stop-color="#7E4C42"/>
+<stop offset="1" stop-color="#664946"/>
+</radialGradient>
+<radialGradient id="paint4_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(16.875 13) rotate(88.698) scale(5.50142 5.67515)">
+<stop offset="0.686112" stop-color="#854D42"/>
+<stop offset="1" stop-color="#844C43" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint5_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(16.3125 9.75) rotate(90) scale(17.9375 19.5)">
+<stop offset="0.790941" stop-color="#A15B79" stop-opacity="0"/>
+<stop offset="0.9144" stop-color="#A15B75"/>
+</radialGradient>
+<linearGradient id="paint6_linear_18_31224" x1="23.5634" y1="10.9375" x2="24.3447" y2="10.1568" gradientUnits="userSpaceOnUse">
+<stop stop-color="#D256BC"/>
+<stop offset="0.501103" stop-color="#FF73E1"/>
+<stop offset="1" stop-color="#FF82E8"/>
+</linearGradient>
+<radialGradient id="paint7_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.7111 10.8047) rotate(138.898) scale(0.404057 0.258873)">
+<stop stop-color="#FF94FF"/>
+<stop offset="1" stop-color="#FF94FF" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint8_linear_18_31224" x1="16.5937" y1="5.125" x2="17.2812" y2="5.90625" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F66EC9"/>
+<stop offset="0.498647" stop-color="#FF8AF5"/>
+<stop offset="1" stop-color="#FF72DB"/>
+</linearGradient>
+<radialGradient id="paint9_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.3281 5.03125) rotate(135) scale(1.5026 0.85995)">
+<stop offset="0.636927" stop-color="#E55DC8" stop-opacity="0"/>
+<stop offset="1" stop-color="#DB5BC1"/>
+</radialGradient>
+<linearGradient id="paint10_linear_18_31224" x1="8.77753" y1="8.09908" x2="9.46184" y2="8.87992" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F66EC9"/>
+<stop offset="0.498647" stop-color="#FF8AF5"/>
+<stop offset="1" stop-color="#FF72DB"/>
+</linearGradient>
+<radialGradient id="paint11_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.5119 8.00572) rotate(135.118) scale(1.49952 0.85818)">
+<stop offset="0.636927" stop-color="#E55DC8" stop-opacity="0"/>
+<stop offset="1" stop-color="#DB5BC1"/>
+</radialGradient>
+<linearGradient id="paint12_linear_18_31224" x1="14.7737" y1="22.0425" x2="15.4612" y2="22.8237" gradientUnits="userSpaceOnUse">
+<stop stop-color="#F66EC9"/>
+<stop offset="0.498647" stop-color="#FF8AF5"/>
+<stop offset="1" stop-color="#FF72DB"/>
+</linearGradient>
+<radialGradient id="paint13_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.5081 21.9487) rotate(135) scale(1.5026 0.85995)">
+<stop offset="0.636927" stop-color="#E55DC8" stop-opacity="0"/>
+<stop offset="1" stop-color="#DB5BC1"/>
+</radialGradient>
+<linearGradient id="paint14_linear_18_31224" x1="20.6534" y1="19.8087" x2="21.4347" y2="19.028" gradientUnits="userSpaceOnUse">
+<stop stop-color="#D256BC"/>
+<stop offset="0.501103" stop-color="#FF73E1"/>
+<stop offset="1" stop-color="#FF82E8"/>
+</linearGradient>
+<radialGradient id="paint15_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21.8011 19.6759) rotate(138.898) scale(0.404057 0.258873)">
+<stop stop-color="#FF94FF"/>
+<stop offset="1" stop-color="#FF94FF" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint16_linear_18_31224" x1="23.6838" y1="14.9487" x2="24.3713" y2="15.73" gradientUnits="userSpaceOnUse">
+<stop stop-color="#22A4FA"/>
+<stop offset="0.498647" stop-color="#49C0FF"/>
+<stop offset="1" stop-color="#2AB1FF"/>
+</linearGradient>
+<radialGradient id="paint17_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.4182 14.855) rotate(133.986) scale(1.69913 1.74052)">
+<stop offset="0.658529" stop-color="#2A77DD" stop-opacity="0"/>
+<stop offset="1" stop-color="#2B73DA"/>
+</radialGradient>
+<linearGradient id="paint18_linear_18_31224" x1="10.6484" y1="20.7187" x2="11.4297" y2="19.938" gradientUnits="userSpaceOnUse">
+<stop stop-color="#2A61B8"/>
+<stop offset="0.501103" stop-color="#25A1FF"/>
+<stop offset="1" stop-color="#2DBFFF"/>
+</linearGradient>
+<radialGradient id="paint19_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11.796 20.5859) rotate(138.898) scale(0.404057 0.258873)">
+<stop stop-color="#4FC9FF"/>
+<stop offset="1" stop-color="#50CBFF" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint20_linear_18_31224" x1="11.5625" y1="6.84375" x2="12.3438" y2="6.0625" gradientUnits="userSpaceOnUse">
+<stop stop-color="#2A61B8"/>
+<stop offset="0.501103" stop-color="#25A1FF"/>
+<stop offset="1" stop-color="#2DBFFF"/>
+</linearGradient>
+<radialGradient id="paint21_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12.7109 6.71094) rotate(138.918) scale(0.404217 0.258951)">
+<stop stop-color="#4FC9FF"/>
+<stop offset="1" stop-color="#50CBFF" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint22_linear_18_31224" x1="6.77754" y1="12.9175" x2="7.46504" y2="13.6987" gradientUnits="userSpaceOnUse">
+<stop stop-color="#22A4FA"/>
+<stop offset="0.498647" stop-color="#49C0FF"/>
+<stop offset="1" stop-color="#2AB1FF"/>
+</linearGradient>
+<radialGradient id="paint23_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7.51192 12.8237) rotate(133.986) scale(1.69913 1.74052)">
+<stop offset="0.658529" stop-color="#2A77DD" stop-opacity="0"/>
+<stop offset="1" stop-color="#2B73DA"/>
+</radialGradient>
+<linearGradient id="paint24_linear_18_31224" x1="21.68" y1="7.18372" x2="22.3675" y2="7.96497" gradientUnits="userSpaceOnUse">
+<stop stop-color="#22A4FA"/>
+<stop offset="0.498647" stop-color="#49C0FF"/>
+<stop offset="1" stop-color="#2AB1FF"/>
+</linearGradient>
+<radialGradient id="paint25_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(22.4144 7.08997) rotate(133.986) scale(1.69913 1.74052)">
+<stop offset="0.658529" stop-color="#2A77DD" stop-opacity="0"/>
+<stop offset="1" stop-color="#2B73DA"/>
+</radialGradient>
+<linearGradient id="paint26_linear_18_31224" x1="9.57092" y1="16.91" x2="10.3522" y2="16.1293" gradientUnits="userSpaceOnUse">
+<stop stop-color="#D256BC"/>
+<stop offset="0.501103" stop-color="#FF73E1"/>
+<stop offset="1" stop-color="#FF82E8"/>
+</linearGradient>
+<radialGradient id="paint27_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.7186 16.7772) rotate(138.898) scale(0.404057 0.258873)">
+<stop stop-color="#FF94FF"/>
+<stop offset="1" stop-color="#FF94FF" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint28_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(12.1875 10.4375) rotate(47.0901) scale(6.05863 29.5218)">
+<stop stop-color="#C79A91"/>
+<stop offset="1" stop-color="#C79A91" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint29_radial_18_31224" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(25.25 19.0625) rotate(-157.529) scale(15.083 71.364)">
+<stop stop-color="#CF9689"/>
+<stop offset="1" stop-color="#D1988A" stop-opacity="0"/>
+</radialGradient>
+</defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/lollipop_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/lollipop_color.svg
new file mode 100644
index 0000000000..ad90ac6f52
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/lollipop_color.svg
@@ -0,0 +1,112 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M28.25 28.25L16.5 17" stroke="url(#paint0_linear_18_29825)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
+<mask id="mask0_18_29825" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="14" y="15" width="16" height="15">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15.236 15.7898C15.9044 15.0916 17.0121 15.0676 17.7103 15.736L29.4603 26.986C30.1584 27.6544 30.1824 28.7621 29.514 29.4603C28.8456 30.1584 27.7379 30.1824 27.0398 29.514L15.2898 18.264C14.5916 17.5956 14.5676 16.4879 15.236 15.7898Z" fill="#212121"/>
+</mask>
+<g mask="url(#mask0_18_29825)">
+<g filter="url(#filter0_f_18_29825)">
+<path d="M29 27.5L17.25 16.25" stroke="#FFE5C1" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</g>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15.236 15.7898C15.9044 15.0916 17.0121 15.0676 17.7103 15.736L29.4603 26.986C30.1584 27.6544 30.1824 28.7621 29.514 29.4603C28.8456 30.1584 27.7379 30.1824 27.0398 29.514L15.2898 18.264C14.5916 17.5956 14.5676 16.4879 15.236 15.7898Z" fill="url(#paint1_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.38559 6.91832C4.28822 5.39149 5.58521 4.12544 7.13636 3.26038C13.3511 6.72017 12 12 12 12C9.95937 8.79099 8.01466 7.26385 3.38559 6.91832Z" fill="url(#paint2_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M2.00043 11.9068C2.01725 10.0646 2.53218 8.34146 3.41704 6.86548C10.2965 6.96669 12 12 12 12C8.66694 10.2518 6.29 9.90635 2.00043 11.9068Z" fill="url(#paint3_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.29277 16.9213C2.4698 15.4683 2 13.789 2 12C2 11.9479 2.0004 11.8959 2.00119 11.844C8.00872 8.49337 12 12 12 12C8.21727 12.0949 5.9829 13.1457 3.29277 16.9213Z" fill="url(#paint4_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.92097 20.6159C5.39375 19.7137 4.12726 18.417 3.26172 16.866C6.78909 10.9586 12 12 12 12C8.7792 13.9043 7.41959 16.0271 6.92097 20.6159Z" fill="url(#paint5_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M6.86548 20.583C6.96669 13.7035 12 12 12 12C10.173 15.2975 9.97217 17.7746 11.9074 21.9996C10.065 21.9829 8.34163 21.4679 6.86548 20.583Z" fill="url(#paint6_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.844 21.9988C8.49337 15.9913 12 12 12 12C12.1732 15.8368 13.1795 18.105 16.9204 20.7077C15.4676 21.5304 13.7887 22 12 22C11.9479 22 11.8959 21.9996 11.844 21.9988Z" fill="url(#paint7_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16.8661 20.7383C10.9586 17.2109 12 12 12 12C14.0949 15.3202 15.9729 16.7474 20.6143 17.0819C19.7121 18.6078 18.4161 19.8733 16.8661 20.7383Z" fill="url(#paint8_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M20.583 17.1345C13.7035 17.0333 12 12 12 12C15.3417 13.8027 17.8524 14.0929 21.9996 12.0944C21.9825 13.9361 21.4676 15.6589 20.583 17.1345Z" fill="url(#paint9_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9988 12.156C21.9996 12.1041 22 12.0521 22 12C22 10.2115 21.5305 8.53271 20.708 7.08008C18.0379 10.9644 15.7923 11.8814 12 12C12 12 15.9913 15.5066 21.9988 12.156Z" fill="url(#paint10_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M17.0816 3.3855C18.6076 4.28766 19.8732 5.58378 20.7383 7.13389C17.2109 13.0414 12 12 12 12C15.1071 10.0716 16.7119 8.22757 17.0816 3.3855Z" fill="url(#paint11_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M17.1345 3.41708C15.6593 2.53265 13.9371 2.0178 12.096 2.00049C14.2371 6.27017 13.7353 8.83597 12 12C12 12 17.0333 10.2965 17.1345 3.41708Z" fill="url(#paint12_linear_18_29825)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12.156 2.00119C12.1041 2.0004 12.0521 2 12 2C10.213 2 8.53548 2.46873 7.08368 3.28996C11.2283 5.87922 12.1157 8.21834 12 12C12 12 15.5066 8.00872 12.156 2.00119Z" fill="url(#paint13_linear_18_29825)"/>
+<mask id="mask1_18_29825" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="2" width="20" height="20">
+<circle cx="12" cy="12" r="10" fill="black"/>
+</mask>
+<g mask="url(#mask1_18_29825)">
+<g filter="url(#filter1_f_18_29825)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM2.125 12C2.125 6.54619 6.54619 2.125 12 2.125C17.4538 2.125 21.875 6.54619 21.875 12C21.875 17.4538 17.4538 21.875 12 21.875C6.54619 21.875 2.125 17.4538 2.125 12Z" fill="black" fill-opacity="0.32"/>
+</g>
+</g>
+<defs>
+<filter id="filter0_f_18_29825" x="15.75" y="14.75" width="14.75" height="14.25" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_29825"/>
+</filter>
+<filter id="filter1_f_18_29825" x="1.5" y="1.5" width="21" height="21" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_29825"/>
+</filter>
+<linearGradient id="paint0_linear_18_29825" x1="25.0221" y1="22.7615" x2="22.5" y2="25.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FDDAB2"/>
+<stop offset="0.442708" stop-color="#EA9BB3"/>
+<stop offset="0.677083" stop-color="#E37DC3"/>
+<stop offset="1" stop-color="#C969AB"/>
+</linearGradient>
+<linearGradient id="paint1_linear_18_29825" x1="14.5" y1="15.5" x2="22.5" y2="23.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="#B05C92"/>
+<stop offset="0.765625" stop-color="#B05C92" stop-opacity="0"/>
+</linearGradient>
+<linearGradient id="paint2_linear_18_29825" x1="5.5" y1="4.50003" x2="12" y2="12" gradientUnits="userSpaceOnUse">
+<stop stop-color="#5E9BEB"/>
+<stop offset="0.526042" stop-color="#6FA0F3"/>
+<stop offset="1" stop-color="#7EA4F4"/>
+</linearGradient>
+<linearGradient id="paint3_linear_18_29825" x1="12" y1="12" x2="2.5" y2="9" gradientUnits="userSpaceOnUse">
+<stop stop-color="#7DC3A2"/>
+<stop offset="0.515625" stop-color="#71C398"/>
+<stop offset="1" stop-color="#74D099"/>
+</linearGradient>
+<linearGradient id="paint4_linear_18_29825" x1="12" y1="12" x2="2" y2="14" gradientUnits="userSpaceOnUse">
+<stop stop-color="#EAC27C"/>
+<stop offset="0.489583" stop-color="#EBC16A"/>
+<stop offset="1" stop-color="#FFE885"/>
+</linearGradient>
+<linearGradient id="paint5_linear_18_29825" x1="12" y1="12" x2="4.5" y2="18.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="#ED8876"/>
+<stop offset="0.442708" stop-color="#FE8765"/>
+<stop offset="1" stop-color="#FF916D"/>
+</linearGradient>
+<linearGradient id="paint6_linear_18_29825" x1="12" y1="12" x2="9" y2="21.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="#BD4C97"/>
+<stop offset="0.515625" stop-color="#B93A90"/>
+<stop offset="1" stop-color="#B83A8B"/>
+</linearGradient>
+<linearGradient id="paint7_linear_18_29825" x1="12" y1="12" x2="13.6573" y2="22" gradientUnits="userSpaceOnUse">
+<stop stop-color="#B976DB"/>
+<stop offset="0.510417" stop-color="#AF64D6"/>
+<stop offset="1" stop-color="#9B4BC5"/>
+</linearGradient>
+<linearGradient id="paint8_linear_18_29825" x1="19" y1="19.5" x2="12" y2="12" gradientUnits="userSpaceOnUse">
+<stop stop-color="#5E92F8"/>
+<stop offset="0.5" stop-color="#6D95F1"/>
+<stop offset="1" stop-color="#7CA0F2"/>
+</linearGradient>
+<linearGradient id="paint9_linear_18_29825" x1="21.5" y1="15" x2="12" y2="12" gradientUnits="userSpaceOnUse">
+<stop stop-color="#90F7BA"/>
+<stop offset="0.526042" stop-color="#76C5A0"/>
+<stop offset="1" stop-color="#7EC1A4"/>
+</linearGradient>
+<linearGradient id="paint10_linear_18_29825" x1="21.5" y1="10" x2="12" y2="12" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFFF90"/>
+<stop offset="0.515625" stop-color="#FFD677"/>
+<stop offset="1" stop-color="#EDC47E"/>
+</linearGradient>
+<linearGradient id="paint11_linear_18_29825" x1="19.5" y1="5.49997" x2="12" y2="12" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FFB281"/>
+<stop offset="0.5" stop-color="#FF9372"/>
+<stop offset="1" stop-color="#F18C79"/>
+</linearGradient>
+<linearGradient id="paint12_linear_18_29825" x1="15" y1="2.50003" x2="12" y2="12" gradientUnits="userSpaceOnUse">
+<stop stop-color="#FC70BD"/>
+<stop offset="1" stop-color="#C2509A"/>
+</linearGradient>
+<linearGradient id="paint13_linear_18_29825" x1="9.5" y1="2.5" x2="12" y2="12" gradientUnits="userSpaceOnUse">
+<stop stop-color="#DB8BFB"/>
+<stop offset="1" stop-color="#BC79DD"/>
+</linearGradient>
+</defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/pancakes_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/pancakes_color.svg
new file mode 100644
index 0000000000..94b2ab69b5
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/pancakes_color.svg
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;">
+ <g transform="matrix(1,0,0,1,0,0.3089)">
+ <path d="M21.357,12.066L10.653,12.066C5.871,12.066 2,15.947 2,20.729C2,25.511 5.871,29.382 10.653,29.382L21.347,29.382C26.129,29.382 30,25.511 30,20.729C30.01,15.947 26.139,12.066 21.357,12.066Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
+ <g>
+ <path d="M21.283,12.484L10.758,12.484C6.057,12.484 2.25,16.155 2.25,20.677C2.25,25.198 6.057,28.859 10.758,28.859L21.273,28.859C25.975,28.859 29.781,25.198 29.781,20.677C29.791,16.155 25.985,12.484 21.283,12.484Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
+ </g>
+ <g>
+ <path d="M21.417,14.823L10.593,14.823L4.011,16.714L4.011,19.275C4.011,22.916 6.962,25.857 10.593,25.857L21.407,25.857C25.048,25.857 27.989,22.906 27.989,19.275L27.989,16.714L21.417,14.823Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
+ </g>
+ <path d="M21.417,14.287L10.593,14.287L4.011,16.177L4.011,18.738C4.011,22.38 6.962,25.321 10.593,25.321L21.407,25.321C25.048,25.321 27.989,22.37 27.989,18.738L27.989,16.177L21.417,14.287Z" style="fill:url(#_Linear4);fill-rule:nonzero;"/>
+ <path d="M21.417,9.195L10.593,9.195C6.952,9.195 4.011,12.146 4.011,15.777C4.011,19.419 6.962,22.36 10.593,22.36L21.407,22.36C25.048,22.36 27.989,19.409 27.989,15.777C27.999,12.136 25.048,9.195 21.417,9.195Z" style="fill:url(#_Linear5);fill-rule:nonzero;"/>
+ <path d="M21.417,9.195L10.593,9.195C6.952,9.195 4.011,12.146 4.011,15.777C4.011,19.419 6.962,22.36 10.593,22.36L21.407,22.36C25.048,22.36 27.989,19.409 27.989,15.777C27.999,12.136 25.048,9.195 21.417,9.195Z" style="fill:url(#_Radial6);fill-rule:nonzero;"/>
+ <path d="M21.417,9.195L10.593,9.195C6.952,9.195 4.011,12.146 4.011,15.777C4.011,19.419 6.962,22.36 10.593,22.36L21.407,22.36C25.048,22.36 27.989,19.409 27.989,15.777C27.999,12.136 25.048,9.195 21.417,9.195Z" style="fill:url(#_Radial7);fill-rule:nonzero;"/>
+ <path d="M21.417,8.515L10.593,8.515L4.011,10.416L4.011,12.977C4.011,16.618 6.962,19.559 10.593,19.559L21.407,19.559C25.048,19.559 27.989,16.608 27.989,12.977L27.989,10.416L21.417,8.515Z" style="fill:url(#_Linear8);fill-rule:nonzero;"/>
+ <path d="M21.416,3.422L10.592,3.422C6.951,3.422 4.01,6.374 4.01,10.005C4.01,13.646 6.961,16.587 10.592,16.587L21.406,16.587C25.048,16.587 27.989,13.636 27.989,10.005C27.999,6.374 25.048,3.422 21.416,3.422Z" style="fill:url(#_Linear9);fill-rule:nonzero;"/>
+ <g>
+ <path d="M18.375,3.579L10.687,3.579C7.109,3.579 4.219,6.53 4.219,10.161C4.219,13.802 7.119,16.743 10.687,16.743L18.375,16.743L18.375,3.579Z" style="fill:url(#_Linear10);fill-rule:nonzero;"/>
+ <path d="M18.375,3.579L10.687,3.579C7.109,3.579 4.219,6.53 4.219,10.161C4.219,13.802 7.119,16.743 10.687,16.743L18.375,16.743L18.375,3.579Z" style="fill:url(#_Radial11);fill-rule:nonzero;"/>
+ <path d="M18.375,3.579L10.687,3.579C7.109,3.579 4.219,6.53 4.219,10.161C4.219,13.802 7.119,16.743 10.687,16.743L18.375,16.743L18.375,3.579Z" style="fill:url(#_Radial12);fill-rule:nonzero;"/>
+ </g>
+ <path d="M19.436,6.204L12.564,6.204C10.523,6.204 8.872,7.855 8.872,9.895C8.872,11.566 9.983,12.986 11.513,13.437C11.974,13.577 12.284,14.007 12.284,14.487L12.284,20.299C12.284,21.009 12.834,21.61 13.544,21.63C14.264,21.64 14.855,21.059 14.855,20.349L14.855,14.677C14.855,14.077 15.335,13.597 15.935,13.597C16.535,13.597 17.015,14.077 17.015,14.677L17.015,16.328C17.015,17.038 17.566,17.638 18.276,17.658C18.996,17.668 19.586,17.088 19.586,16.378L19.586,14.537C19.586,14.047 19.906,13.607 20.387,13.477C21.977,13.066 23.148,11.616 23.148,9.905C23.138,7.855 21.477,6.204 19.436,6.204Z" style="fill:url(#_Radial13);fill-rule:nonzero;"/>
+ <path d="M19.436,6.204L12.564,6.204C10.523,6.204 8.872,7.855 8.872,9.895C8.872,11.566 9.983,12.986 11.513,13.437C11.974,13.577 12.284,14.007 12.284,14.487L12.284,20.299C12.284,21.009 12.834,21.61 13.544,21.63C14.264,21.64 14.855,21.059 14.855,20.349L14.855,14.677C14.855,14.077 15.335,13.597 15.935,13.597C16.535,13.597 17.015,14.077 17.015,14.677L17.015,16.328C17.015,17.038 17.566,17.638 18.276,17.658C18.996,17.668 19.586,17.088 19.586,16.378L19.586,14.537C19.586,14.047 19.906,13.607 20.387,13.477C21.977,13.066 23.148,11.616 23.148,9.905C23.138,7.855 21.477,6.204 19.436,6.204Z" style="fill:rgb(136,71,52);fill-rule:nonzero;"/>
+ <path d="M19.436,6.204L12.564,6.204C10.523,6.204 8.872,7.855 8.872,9.895C8.872,11.566 9.983,12.986 11.513,13.437C11.974,13.577 12.284,14.007 12.284,14.487L12.284,20.299C12.284,21.009 12.834,21.61 13.544,21.63C14.264,21.64 14.855,21.059 14.855,20.349L14.855,14.677C14.855,14.077 15.335,13.597 15.935,13.597C16.535,13.597 17.015,14.077 17.015,14.677L17.015,16.328C17.015,17.038 17.566,17.638 18.276,17.658C18.996,17.668 19.586,17.088 19.586,16.378L19.586,14.537C19.586,14.047 19.906,13.607 20.387,13.477C21.977,13.066 23.148,11.616 23.148,9.905C23.138,7.855 21.477,6.204 19.436,6.204Z" style="fill:url(#_Linear14);fill-rule:nonzero;"/>
+ <g>
+ <path d="M8.953,9.805C8.953,11.4 10.107,12.552 11.606,12.982C12.057,13.115 12.361,13.526 12.361,13.985L12.361,19.964C12.361,20.642 12.9,21.2 13.595,21.219C14.3,21.228 14.75,20.85 14.75,20.172L14.75,14.079C14.75,13.505 15.349,13.047 15.937,13.047C16.524,13.047 17.156,13.505 17.156,14.079L17.156,16.219C17.156,16.897 17.533,17.497 18.229,17.516C18.934,17.525 19.512,16.897 19.512,16.219L19.512,14.059C19.512,13.591 19.826,13.171 20.296,13.047C21.854,12.655 23.094,11.439 23.094,9.805C23.094,3.859 8.953,3.947 8.953,9.805Z" style="fill:url(#_Radial15);fill-rule:nonzero;"/>
+ <path d="M8.953,9.805C8.953,11.4 10.107,12.552 11.606,12.982C12.057,13.115 12.361,13.526 12.361,13.985L12.361,19.964C12.361,20.642 12.9,21.2 13.595,21.219C14.3,21.228 14.75,20.85 14.75,20.172L14.75,14.079C14.75,13.505 15.349,13.047 15.937,13.047C16.524,13.047 17.156,13.505 17.156,14.079L17.156,16.219C17.156,16.897 17.533,17.497 18.229,17.516C18.934,17.525 19.512,16.897 19.512,16.219L19.512,14.059C19.512,13.591 19.826,13.171 20.296,13.047C21.854,12.655 23.094,11.439 23.094,9.805C23.094,3.859 8.953,3.947 8.953,9.805Z" style="fill:url(#_Radial16);fill-rule:nonzero;"/>
+ <path d="M8.953,9.805C8.953,11.4 10.107,12.552 11.606,12.982C12.057,13.115 12.361,13.526 12.361,13.985L12.361,19.964C12.361,20.642 12.9,21.2 13.595,21.219C14.3,21.228 14.75,20.85 14.75,20.172L14.75,14.079C14.75,13.505 15.349,13.047 15.937,13.047C16.524,13.047 17.156,13.505 17.156,14.079L17.156,16.219C17.156,16.897 17.533,17.497 18.229,17.516C18.934,17.525 19.512,16.897 19.512,16.219L19.512,14.059C19.512,13.591 19.826,13.171 20.296,13.047C21.854,12.655 23.094,11.439 23.094,9.805C23.094,3.859 8.953,3.947 8.953,9.805Z" style="fill:url(#_Radial17);fill-rule:nonzero;"/>
+ <path d="M8.953,9.805C8.953,11.4 10.107,12.552 11.606,12.982C12.057,13.115 12.361,13.526 12.361,13.985L12.361,19.964C12.361,20.642 12.9,21.2 13.595,21.219C14.3,21.228 14.75,20.85 14.75,20.172L14.75,14.079C14.75,13.505 15.349,13.047 15.937,13.047C16.524,13.047 17.156,13.505 17.156,14.079L17.156,16.219C17.156,16.897 17.533,17.497 18.229,17.516C18.934,17.525 19.512,16.897 19.512,16.219L19.512,14.059C19.512,13.591 19.826,13.171 20.296,13.047C21.854,12.655 23.094,11.439 23.094,9.805C23.094,3.859 8.953,3.947 8.953,9.805Z" style="fill:url(#_Radial18);fill-rule:nonzero;"/>
+ <path d="M8.953,9.805C8.953,11.4 10.107,12.552 11.606,12.982C12.057,13.115 12.361,13.526 12.361,13.985L12.361,19.964C12.361,20.642 12.9,21.2 13.595,21.219C14.3,21.228 14.75,20.85 14.75,20.172L14.75,14.079C14.75,13.505 15.349,13.047 15.937,13.047C16.524,13.047 17.156,13.505 17.156,14.079L17.156,16.219C17.156,16.897 17.533,17.497 18.229,17.516C18.934,17.525 19.512,16.897 19.512,16.219L19.512,14.059C19.512,13.591 19.826,13.171 20.296,13.047C21.854,12.655 23.094,11.439 23.094,9.805C23.094,3.859 8.953,3.947 8.953,9.805Z" style="fill:url(#_Radial19);fill-rule:nonzero;"/>
+ <path d="M8.953,9.805C8.953,11.4 10.107,12.552 11.606,12.982C12.057,13.115 12.361,13.526 12.361,13.985L12.361,19.964C12.361,20.642 12.9,21.2 13.595,21.219C14.3,21.228 14.75,20.85 14.75,20.172L14.75,14.079C14.75,13.505 15.349,13.047 15.937,13.047C16.524,13.047 17.156,13.505 17.156,14.079L17.156,16.219C17.156,16.897 17.533,17.497 18.229,17.516C18.934,17.525 19.512,16.897 19.512,16.219L19.512,14.059C19.512,13.591 19.826,13.171 20.296,13.047C21.854,12.655 23.094,11.439 23.094,9.805C23.094,3.859 8.953,3.947 8.953,9.805Z" style="fill:url(#_Radial20);fill-rule:nonzero;"/>
+ <path d="M8.953,9.805C8.953,11.4 10.107,12.552 11.606,12.982C12.057,13.115 12.361,13.526 12.361,13.985L12.361,19.964C12.361,20.642 12.9,21.2 13.595,21.219C14.3,21.228 14.75,20.85 14.75,20.172L14.75,14.079C14.75,13.505 15.349,13.047 15.937,13.047C16.524,13.047 17.156,13.505 17.156,14.079L17.156,16.219C17.156,16.897 17.533,17.497 18.229,17.516C18.934,17.525 19.512,16.897 19.512,16.219L19.512,14.059C19.512,13.591 19.826,13.171 20.296,13.047C21.854,12.655 23.094,11.439 23.094,9.805C23.094,3.859 8.953,3.947 8.953,9.805Z" style="fill:url(#_Radial21);fill-rule:nonzero;"/>
+ </g>
+ <g>
+ <path d="M11.479,7.266L11.479,7.641C11.479,8.062 11.671,8.484 12.055,8.721L15.092,10.511C15.659,10.871 16.361,10.871 16.918,10.511L19.955,8.721C20.339,8.474 20.531,8.052 20.531,7.641L20.531,7.266L11.479,7.266Z" style="fill:rgb(115,57,25);fill-rule:nonzero;"/>
+ </g>
+ <path d="M17.406,5.113L16.955,4.843C16.365,4.492 15.635,4.492 15.055,4.843L14.605,5.113L11.293,5.113L11.293,7.213C11.293,7.624 11.493,8.034 11.894,8.264L15.055,10.134C15.645,10.485 16.375,10.485 16.955,10.134L20.116,8.264C20.517,8.024 20.717,7.614 20.717,7.213L20.717,5.113L17.406,5.113Z" style="fill:url(#_Linear22);fill-rule:nonzero;"/>
+ <path d="M17.406,5.113L16.955,4.843C16.365,4.492 15.635,4.492 15.055,4.843L14.605,5.113L11.293,5.113L11.293,7.213C11.293,7.624 11.493,8.034 11.894,8.264L15.055,10.134C15.645,10.485 16.375,10.485 16.955,10.134L20.116,8.264C20.517,8.024 20.717,7.614 20.717,7.213L20.717,5.113L17.406,5.113Z" style="fill:url(#_Radial23);fill-rule:nonzero;"/>
+ <path d="M15.055,2.263L11.894,4.133C11.093,4.603 11.093,5.764 11.894,6.244L15.055,8.115C15.645,8.465 16.375,8.465 16.955,8.115L20.116,6.244C20.917,5.774 20.917,4.613 20.116,4.133L16.955,2.263C16.365,1.912 15.645,1.912 15.055,2.263Z" style="fill:url(#_Linear24);fill-rule:nonzero;"/>
+ <circle cx="22.492" cy="26.922" r="1.492" style="fill:url(#_Radial25);"/>
+ <circle cx="24.52" cy="25.857" r="0.536" style="fill:url(#_Radial26);"/>
+ <g>
+ <path d="M19.516,13.375C20.234,12.75 22.984,11.922 22.75,9.375C22.125,11.156 21.731,11.677 20.125,12.031C18.922,12.297 18.844,13.234 18.844,13.719L18.953,16.766L19.281,16.766C19.369,16.237 19.18,13.667 19.516,13.375Z" style="fill:url(#_Radial27);fill-rule:nonzero;"/>
+ </g>
+ <g>
+ <path d="M13.828,17.047C13.828,17.185 13.94,17.297 14.078,17.297C14.216,17.297 14.328,17.185 14.328,17.047L13.828,17.047ZM17.283,14.283C17.283,14.421 17.395,14.533 17.533,14.533C17.671,14.533 17.783,14.421 17.783,14.283L17.283,14.283ZM14.328,17.047L14.328,14.283L13.828,14.283L13.828,17.047L14.328,17.047ZM14.328,14.283C14.328,13.975 14.487,13.586 14.769,13.271C15.048,12.958 15.418,12.75 15.813,12.75L15.813,12.25C15.238,12.25 14.741,12.551 14.396,12.938C14.052,13.322 13.828,13.825 13.828,14.283L14.328,14.283ZM15.813,12.75C16.201,12.75 16.568,12.968 16.847,13.291C17.128,13.617 17.283,14.007 17.283,14.283L17.783,14.283C17.783,13.855 17.561,13.354 17.225,12.964C16.886,12.572 16.393,12.25 15.813,12.25L15.813,12.75Z" style="fill:url(#_Linear28);fill-rule:nonzero;"/>
+ </g>
+ <g>
+ <path d="M21.359,16.438C23.75,16.438 27.094,14.859 27.75,11.453" style="fill:none;fill-rule:nonzero;stroke:url(#_Radial29);stroke-width:0.3px;"/>
+ </g>
+ <g>
+ <path d="M23.782,28.281C26,27.688 28.781,25.406 29.625,21.469" style="fill:none;fill-rule:nonzero;stroke:url(#_Radial30);stroke-width:0.3px;"/>
+ </g>
+ <g>
+ <path d="M20.695,5.273C20.695,5.555 20.506,5.938 20.156,6.188C19.806,6.438 17.74,7.563 16.594,8.219C16.345,8.361 15.773,8.453 15.258,8.094" style="fill:none;fill-rule:nonzero;stroke:url(#_Radial31);stroke-width:0.3px;"/>
+ </g>
+ </g>
+ <defs>
+ <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(28,0,0,28,2,21.125)"><stop offset="0" style="stop-color:rgb(187,183,188);stop-opacity:1"/><stop offset="0.1" style="stop-color:rgb(173,149,194);stop-opacity:1"/><stop offset="0.23" style="stop-color:rgb(195,160,226);stop-opacity:1"/><stop offset="0.8" style="stop-color:rgb(201,166,231);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(216,208,223);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(27.3125,0,0,27.3125,2.8125,20.125)"><stop offset="0" style="stop-color:rgb(190,180,182);stop-opacity:1"/><stop offset="0.16" style="stop-color:rgb(219,206,213);stop-opacity:1"/><stop offset="0.49" style="stop-color:rgb(223,205,210);stop-opacity:1"/><stop offset="0.82" style="stop-color:rgb(223,209,214);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(227,216,205);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(7.18837e-16,11.7395,-11.7395,7.18837e-16,16,14.823)"><stop offset="0" style="stop-color:rgb(180,157,159);stop-opacity:1"/><stop offset="0.89" style="stop-color:rgb(180,157,159);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(180,157,159);stop-opacity:0"/></linearGradient>
+ <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(23.9786,0,0,23.9786,4.01074,19.1875)"><stop offset="0" style="stop-color:rgb(184,140,67);stop-opacity:1"/><stop offset="0.16" style="stop-color:rgb(198,144,57);stop-opacity:1"/><stop offset="0.33" style="stop-color:rgb(217,159,63);stop-opacity:1"/><stop offset="0.76" style="stop-color:rgb(217,167,52);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(236,211,76);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(23.4268,0,0,23.4268,4.5625,17.875)"><stop offset="0" style="stop-color:rgb(169,110,68);stop-opacity:1"/><stop offset="0.15" style="stop-color:rgb(184,120,76);stop-opacity:1"/><stop offset="0.34" style="stop-color:rgb(186,119,75);stop-opacity:1"/><stop offset="0.59" style="stop-color:rgb(214,140,86);stop-opacity:1"/><stop offset="0.82" style="stop-color:rgb(227,152,93);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(234,173,116);stop-opacity:1"/></linearGradient>
+ <radialGradient id="_Radial6" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.04736e-15,5.34759,-13.25,-5.07285e-15,16,15.7774)"><stop offset="0" style="stop-color:rgb(153,81,44);stop-opacity:1"/><stop offset="0.66" style="stop-color:rgb(153,81,44);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(166,87,47);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial7" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.875,0,0,2.85976,12.25,19.5)"><stop offset="0" style="stop-color:rgb(147,74,40);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(147,74,40);stop-opacity:0"/></radialGradient>
+ <linearGradient id="_Linear8" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(23.4268,0,0,23.4268,4.5625,13.625)"><stop offset="0" style="stop-color:rgb(178,140,76);stop-opacity:1"/><stop offset="0.13" style="stop-color:rgb(185,127,51);stop-opacity:1"/><stop offset="0.28" style="stop-color:rgb(189,118,40);stop-opacity:1"/><stop offset="0.49" style="stop-color:rgb(195,124,40);stop-opacity:1"/><stop offset="0.59" style="stop-color:rgb(208,135,38);stop-opacity:1"/><stop offset="0.7" style="stop-color:rgb(214,146,45);stop-opacity:1"/><stop offset="0.82" style="stop-color:rgb(222,169,51);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(229,206,83);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear9" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(23.3636,0,0,23.3636,4.625,8.8125)"><stop offset="0" style="stop-color:rgb(204,162,117);stop-opacity:1"/><stop offset="0.14" style="stop-color:rgb(214,168,122);stop-opacity:1"/><stop offset="0.72" style="stop-color:rgb(225,177,124);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(230,178,125);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear10" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(19.3405,0,0,19.3405,7.22204,7.96546)"><stop offset="0" style="stop-color:rgb(212,167,120);stop-opacity:1"/><stop offset="0.06" style="stop-color:rgb(212,167,120);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(231,178,125);stop-opacity:1"/></linearGradient>
+ <radialGradient id="_Radial11" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.5,4.3125,-3.88579,4.05475,9.875,9.8125)"><stop offset="0" style="stop-color:rgb(179,130,88);stop-opacity:1"/><stop offset="0.77" style="stop-color:rgb(187,143,101);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(187,143,101);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial12" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.375,0,0,4.25084,12.8125,15.4375)"><stop offset="0" style="stop-color:rgb(175,119,78);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(175,119,78);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial13" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-12.4374,5.12507,-4.74283,-11.5098,20.5,10.0625)"><stop offset="0" style="stop-color:rgb(157,98,60);stop-opacity:1"/><stop offset="0.03" style="stop-color:rgb(157,98,60);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(130,82,62);stop-opacity:1"/></radialGradient>
+ <linearGradient id="_Linear14" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(13.5225,0,0,13.5225,9.625,11.875)"><stop offset="0" style="stop-color:rgb(128,75,51);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(159,94,62);stop-opacity:1"/></linearGradient>
+ <radialGradient id="_Radial15" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-12.1805,4.89355,-4.44716,-11.0694,20.4072,9.95537)"><stop offset="0" style="stop-color:rgb(157,98,60);stop-opacity:1"/><stop offset="0.03" style="stop-color:rgb(157,98,60);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(130,82,62);stop-opacity:1"/></radialGradient>
+ <radialGradient id="_Radial16" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.45827,1.87983,-1.00439,1.84774,13.6437,9.00053)"><stop offset="0" style="stop-color:rgb(118,67,38);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(115,62,33);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial17" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-3.62501,4.18749,-4.06664,-3.5204,10.75,6.75)"><stop offset="0" style="stop-color:rgb(114,77,61);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(114,76,59);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial18" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.37499,-0.625018,0.413551,-1.57144,18.6563,17.6875)"><stop offset="0" style="stop-color:rgb(138,70,84);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(140,71,86);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial19" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.29214e-15,-3.375,1.27037,-4.8637e-16,13.1562,19.875)"><stop offset="0" style="stop-color:rgb(136,75,79);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(134,68,83);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial20" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.1875,1.09374,-0.673499,-0.115457,13.9063,16.0625)"><stop offset="0" style="stop-color:rgb(163,118,101);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(165,120,103);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.281246,0.937501,-0.651233,-0.195367,18.8906,15.7344)"><stop offset="0" style="stop-color:rgb(166,121,104);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(168,123,104);stop-opacity:0"/></radialGradient>
+ <linearGradient id="_Linear22" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(9.2792,0,0,9.2792,11.4375,7.48849)"><stop offset="0" style="stop-color:rgb(199,162,43);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(232,193,46);stop-opacity:1"/></linearGradient>
+ <radialGradient id="_Radial23" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.14857e-15,3,-3.44251,-1.31799e-15,16.005,10.9375)"><stop offset="0" style="stop-color:rgb(219,165,41);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(217,163,43);stop-opacity:0"/></radialGradient>
+ <linearGradient id="_Linear24" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.1641,5.02344,-5.02344,3.1641,14.3984,2.60156)"><stop offset="0" style="stop-color:rgb(207,186,58);stop-opacity:1"/><stop offset="0.01" style="stop-color:rgb(207,186,58);stop-opacity:1"/><stop offset="0.09" style="stop-color:rgb(213,193,55);stop-opacity:1"/><stop offset="0.93" style="stop-color:rgb(231,206,55);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(231,206,55);stop-opacity:1"/></linearGradient>
+ <radialGradient id="_Radial25" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.98437,2.28517e-15,-2.16435e-15,-2.82658,23.5774,26.9224)"><stop offset="0" style="stop-color:rgb(160,103,64);stop-opacity:1"/><stop offset="0.71" style="stop-color:rgb(142,81,46);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(142,81,46);stop-opacity:1"/></radialGradient>
+ <radialGradient id="_Radial26" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.07227,8.21052e-16,-7.77636e-16,-1.01557,24.9104,25.8569)"><stop offset="0" style="stop-color:rgb(160,103,64);stop-opacity:1"/><stop offset="0.71" style="stop-color:rgb(142,81,46);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(142,81,46);stop-opacity:1"/></radialGradient>
+ <radialGradient id="_Radial27" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.98808e-15,7.80469,-4.14003,-1.58504e-15,20.8039,13.0703)"><stop offset="0" style="stop-color:rgb(173,124,104);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(172,123,101);stop-opacity:0"/></radialGradient>
+ <linearGradient id="_Linear28" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.7188,0,0,3.7188,14.0312,13.5027)"><stop offset="0" style="stop-color:rgb(162,119,100);stop-opacity:1"/><stop offset="0.46" style="stop-color:rgb(156,111,94);stop-opacity:1"/><stop offset="0.62" style="stop-color:rgb(111,63,46);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(111,63,46);stop-opacity:1"/></linearGradient>
+ <radialGradient id="_Radial29" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.53125,-2.40625,1.27457,1.34078,25.7188,14.5)"><stop offset="0" style="stop-color:rgb(239,185,146);stop-opacity:1"/><stop offset="0.45" style="stop-color:rgb(241,189,146);stop-opacity:1"/><stop offset="0.99" style="stop-color:rgb(240,188,143);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(240,188,143);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(3.50413,-4.40625,2.33395,1.8561,26.9687,25.875)"><stop offset="0" style="stop-color:rgb(235,225,240);stop-opacity:1"/><stop offset="0.45" style="stop-color:rgb(235,226,240);stop-opacity:1"/><stop offset="0.99" style="stop-color:rgb(232,223,236);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(232,223,236);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial31" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(2.88719,-1.6592,0.823999,1.43385,18.1875,7.3125)"><stop offset="0" style="stop-color:rgb(240,209,81);stop-opacity:1"/><stop offset="0.45" style="stop-color:rgb(242,211,89);stop-opacity:1"/><stop offset="0.99" style="stop-color:rgb(243,211,87);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(243,211,87);stop-opacity:0"/></radialGradient>
+ </defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg
new file mode 100644
index 0000000000..64dfef8e05
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg
@@ -0,0 +1,161 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16 22C21.5228 22 26 17.5228 26 12C26 6.47715 21.5228 2 16 2C10.4772 2 6 6.47715 6 12C6 17.5228 10.4772 22 16 22Z" fill="#CDA3AF"/>
+<path d="M16 22C21.5228 22 26 17.5228 26 12C26 6.47715 21.5228 2 16 2C10.4772 2 6 6.47715 6 12C6 17.5228 10.4772 22 16 22Z" fill="url(#paint0_radial_18_28637)"/>
+<g filter="url(#filter0_f_18_28637)">
+<path d="M8.80999 13.595C9.03999 12.855 9.72999 12.375 10.49 12.375C11.25 12.375 12.3325 12.865 12.5625 13.585C13.0725 15.205 14.19 15.7188 15.98 15.7188C17.77 15.7188 18.9275 15.205 19.4375 13.595C19.6675 12.865 20.71 12.385 21.47 12.385H21.51C22.27 12.385 23.2387 12.4987 23.4688 13.2188C23.8088 14.2888 24.0838 14.6337 25.0938 15.0938C23.5238 2.90129 7.99999 6.46882 8.80999 13.595Z" fill="#E17CA3"/>
+<path d="M8.80999 13.595C9.03999 12.855 9.72999 12.375 10.49 12.375C11.25 12.375 12.3325 12.865 12.5625 13.585C13.0725 15.205 14.19 15.7188 15.98 15.7188C17.77 15.7188 18.9275 15.205 19.4375 13.595C19.6675 12.865 20.71 12.385 21.47 12.385H21.51C22.27 12.385 23.2387 12.4987 23.4688 13.2188C23.8088 14.2888 24.0838 14.6337 25.0938 15.0938C23.5238 2.90129 7.99999 6.46882 8.80999 13.595Z" fill="url(#paint1_radial_18_28637)"/>
+<path d="M8.80999 13.595C9.03999 12.855 9.72999 12.375 10.49 12.375C11.25 12.375 12.3325 12.865 12.5625 13.585C13.0725 15.205 14.19 15.7188 15.98 15.7188C17.77 15.7188 18.9275 15.205 19.4375 13.595C19.6675 12.865 20.71 12.385 21.47 12.385H21.51C22.27 12.385 23.2387 12.4987 23.4688 13.2188C23.8088 14.2888 24.0838 14.6337 25.0938 15.0938C23.5238 2.90129 7.99999 6.46882 8.80999 13.595Z" fill="url(#paint2_linear_18_28637)"/>
+<path d="M8.80999 13.595C9.03999 12.855 9.72999 12.375 10.49 12.375C11.25 12.375 12.3325 12.865 12.5625 13.585C13.0725 15.205 14.19 15.7188 15.98 15.7188C17.77 15.7188 18.9275 15.205 19.4375 13.595C19.6675 12.865 20.71 12.385 21.47 12.385H21.51C22.27 12.385 23.2387 12.4987 23.4688 13.2188C23.8088 14.2888 24.0838 14.6337 25.0938 15.0938C23.5238 2.90129 7.99999 6.46882 8.80999 13.595Z" fill="url(#paint3_radial_18_28637)"/>
+<path d="M8.80999 13.595C9.03999 12.855 9.72999 12.375 10.49 12.375C11.25 12.375 12.3325 12.865 12.5625 13.585C13.0725 15.205 14.19 15.7188 15.98 15.7188C17.77 15.7188 18.9275 15.205 19.4375 13.595C19.6675 12.865 20.71 12.385 21.47 12.385H21.51C22.27 12.385 23.2387 12.4987 23.4688 13.2188C23.8088 14.2888 24.0838 14.6337 25.0938 15.0938C23.5238 2.90129 7.99999 6.46882 8.80999 13.595Z" fill="url(#paint4_radial_18_28637)"/>
+<path d="M8.80999 13.595C9.03999 12.855 9.72999 12.375 10.49 12.375C11.25 12.375 12.3325 12.865 12.5625 13.585C13.0725 15.205 14.19 15.7188 15.98 15.7188C17.77 15.7188 18.9275 15.205 19.4375 13.595C19.6675 12.865 20.71 12.385 21.47 12.385H21.51C22.27 12.385 23.2387 12.4987 23.4688 13.2188C23.8088 14.2888 24.0838 14.6337 25.0938 15.0938C23.5238 2.90129 7.99999 6.46882 8.80999 13.595Z" fill="url(#paint5_linear_18_28637)"/>
+</g>
+<path d="M16 2C10.48 2 6 6.48 6 12C6 13.28 6.24 14.5 6.68 15.63C7.69 15.16 8.47 14.29 8.81 13.22C9.04 12.48 9.73 12 10.49 12C11.25 12 11.94 12.49 12.17 13.21C12.68 14.83 14.19 16 15.98 16C17.77 16 19.28 14.83 19.79 13.22C20.02 12.49 20.71 12.01 21.47 12.01H21.51C22.27 12.01 22.96 12.5 23.19 13.22C23.53 14.29 24.31 15.17 25.32 15.63C25.76 14.5 26 13.28 26 12C25.99 6.48 21.52 2 16 2Z" fill="#E17CA3"/>
+<path d="M16 2C10.48 2 6 6.48 6 12C6 13.28 6.24 14.5 6.68 15.63C7.69 15.16 8.47 14.29 8.81 13.22C9.04 12.48 9.73 12 10.49 12C11.25 12 11.94 12.49 12.17 13.21C12.68 14.83 14.19 16 15.98 16C17.77 16 19.28 14.83 19.79 13.22C20.02 12.49 20.71 12.01 21.47 12.01H21.51C22.27 12.01 22.96 12.5 23.19 13.22C23.53 14.29 24.31 15.17 25.32 15.63C25.76 14.5 26 13.28 26 12C25.99 6.48 21.52 2 16 2Z" fill="url(#paint6_radial_18_28637)"/>
+<path d="M16 2C10.48 2 6 6.48 6 12C6 13.28 6.24 14.5 6.68 15.63C7.69 15.16 8.47 14.29 8.81 13.22C9.04 12.48 9.73 12 10.49 12C11.25 12 11.94 12.49 12.17 13.21C12.68 14.83 14.19 16 15.98 16C17.77 16 19.28 14.83 19.79 13.22C20.02 12.49 20.71 12.01 21.47 12.01H21.51C22.27 12.01 22.96 12.5 23.19 13.22C23.53 14.29 24.31 15.17 25.32 15.63C25.76 14.5 26 13.28 26 12C25.99 6.48 21.52 2 16 2Z" fill="url(#paint7_linear_18_28637)"/>
+<path d="M16 2C10.48 2 6 6.48 6 12C6 13.28 6.24 14.5 6.68 15.63C7.69 15.16 8.47 14.29 8.81 13.22C9.04 12.48 9.73 12 10.49 12C11.25 12 11.94 12.49 12.17 13.21C12.68 14.83 14.19 16 15.98 16C17.77 16 19.28 14.83 19.79 13.22C20.02 12.49 20.71 12.01 21.47 12.01H21.51C22.27 12.01 22.96 12.5 23.19 13.22C23.53 14.29 24.31 15.17 25.32 15.63C25.76 14.5 26 13.28 26 12C25.99 6.48 21.52 2 16 2Z" fill="url(#paint8_radial_18_28637)"/>
+<path d="M16 2C10.48 2 6 6.48 6 12C6 13.28 6.24 14.5 6.68 15.63C7.69 15.16 8.47 14.29 8.81 13.22C9.04 12.48 9.73 12 10.49 12C11.25 12 11.94 12.49 12.17 13.21C12.68 14.83 14.19 16 15.98 16C17.77 16 19.28 14.83 19.79 13.22C20.02 12.49 20.71 12.01 21.47 12.01H21.51C22.27 12.01 22.96 12.5 23.19 13.22C23.53 14.29 24.31 15.17 25.32 15.63C25.76 14.5 26 13.28 26 12C25.99 6.48 21.52 2 16 2Z" fill="url(#paint9_radial_18_28637)"/>
+<path d="M16 2C10.48 2 6 6.48 6 12C6 13.28 6.24 14.5 6.68 15.63C7.69 15.16 8.47 14.29 8.81 13.22C9.04 12.48 9.73 12 10.49 12C11.25 12 11.94 12.49 12.17 13.21C12.68 14.83 14.19 16 15.98 16C17.77 16 19.28 14.83 19.79 13.22C20.02 12.49 20.71 12.01 21.47 12.01H21.51C22.27 12.01 22.96 12.5 23.19 13.22C23.53 14.29 24.31 15.17 25.32 15.63C25.76 14.5 26 13.28 26 12C25.99 6.48 21.52 2 16 2Z" fill="url(#paint10_radial_18_28637)"/>
+<path d="M25.1 8C25.76 8 26.19 8.69 25.9 9.28L21.65 18H19.67L24.3 8.5C24.45 8.19 24.77 8 25.1 8Z" fill="url(#paint11_linear_18_28637)"/>
+<g filter="url(#filter1_f_18_28637)">
+<path d="M20.5625 18.4688L25.2812 8.90625" stroke="url(#paint12_linear_18_28637)" stroke-width="0.75" stroke-linecap="round"/>
+</g>
+<path d="M8 18L15.41 29.68C15.68 30.11 16.32 30.11 16.59 29.68L24 18H8Z" fill="url(#paint13_radial_18_28637)"/>
+<path d="M8 18L15.41 29.68C15.68 30.11 16.32 30.11 16.59 29.68L24 18H8Z" fill="url(#paint14_linear_18_28637)"/>
+<g filter="url(#filter2_f_18_28637)">
+<path d="M13.2962 6.15625C13.591 6.15625 13.83 5.97477 13.83 5.67999C13.83 5.38521 13.591 5.07812 13.2962 5.07812C13.0015 5.07812 12.4219 5.59584 12.4219 5.89062C12.4219 6.18541 13.0015 6.15625 13.2962 6.15625Z" fill="url(#paint15_radial_18_28637)"/>
+</g>
+<g filter="url(#filter3_f_18_28637)">
+<path d="M11.2962 7.23523C11.591 7.23523 11.83 7.05375 11.83 6.75897C11.83 6.46419 11.591 6.1571 11.2962 6.1571C11.0015 6.1571 10.4219 6.67482 10.4219 6.9696C10.4219 7.26438 11.0015 7.23523 11.2962 7.23523Z" fill="url(#paint16_radial_18_28637)"/>
+</g>
+<g filter="url(#filter4_f_18_28637)">
+<path d="M13.2962 8.2179C13.591 8.2179 13.83 8.03641 13.83 7.74163C13.83 7.44685 13.591 7.13977 13.2962 7.13977C13.0015 7.13977 12.4219 7.65749 12.4219 7.95227C12.4219 8.24705 13.0015 8.2179 13.2962 8.2179Z" fill="url(#paint17_radial_18_28637)"/>
+</g>
+<path d="M13.4837 6.05748C13.7785 6.05748 14.0175 5.81852 14.0175 5.52374C14.0175 5.22896 13.7785 4.98999 13.4837 4.98999C13.189 4.98999 12.95 5.22896 12.95 5.52374C12.95 5.81852 13.189 6.05748 13.4837 6.05748Z" fill="url(#paint18_radial_18_28637)"/>
+<path d="M11.4994 7.04186C11.7941 7.04186 12.0331 6.80289 12.0331 6.50811C12.0331 6.21333 11.7941 5.97437 11.4994 5.97437C11.2046 5.97437 10.9656 6.21333 10.9656 6.50811C10.9656 6.80289 11.2046 7.04186 11.4994 7.04186Z" fill="#E5D6EB"/>
+<path d="M11.4994 7.04186C11.7941 7.04186 12.0331 6.80289 12.0331 6.50811C12.0331 6.21333 11.7941 5.97437 11.4994 5.97437C11.2046 5.97437 10.9656 6.21333 10.9656 6.50811C10.9656 6.80289 11.2046 7.04186 11.4994 7.04186Z" fill="url(#paint19_radial_18_28637)"/>
+<path d="M13.4837 8.10936C13.7785 8.10936 14.0175 7.8704 14.0175 7.57562C14.0175 7.28084 13.7785 7.04187 13.4837 7.04187C13.189 7.04187 12.95 7.28084 12.95 7.57562C12.95 7.8704 13.189 8.10936 13.4837 8.10936Z" fill="url(#paint20_radial_18_28637)"/>
+<g filter="url(#filter5_f_18_28637)">
+<ellipse cx="25.4492" cy="8.65625" rx="0.324167" ry="0.34375" fill="#74D8FF"/>
+</g>
+<defs>
+<filter id="filter0_f_18_28637" x="7.77939" y="6.07935" width="18.3144" height="10.6394" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_28637"/>
+</filter>
+<filter id="filter1_f_18_28637" x="19.4374" y="7.78113" width="6.9689" height="11.8127" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.375" result="effect1_foregroundBlur_18_28637"/>
+</filter>
+<filter id="filter2_f_18_28637" x="12.1719" y="4.82812" width="1.90811" height="1.57898" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_28637"/>
+</filter>
+<filter id="filter3_f_18_28637" x="10.1719" y="5.9071" width="1.90811" height="1.57898" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_28637"/>
+</filter>
+<filter id="filter4_f_18_28637" x="12.1719" y="6.88977" width="1.90811" height="1.57898" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.125" result="effect1_foregroundBlur_18_28637"/>
+</filter>
+<filter id="filter5_f_18_28637" x="24.625" y="7.8125" width="1.64833" height="1.6875" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.25" result="effect1_foregroundBlur_18_28637"/>
+</filter>
+<radialGradient id="paint0_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21 12) rotate(119.358) scale(11.4735)">
+<stop stop-color="#FFEDE4"/>
+<stop offset="0.451632" stop-color="#FFDBDF"/>
+<stop offset="1" stop-color="#D8AEBD" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint1_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21 7.75) rotate(129.523) scale(12.9639 14.3163)">
+<stop stop-color="#FFBAEA"/>
+<stop offset="0.451156" stop-color="#FF97E4"/>
+<stop offset="1" stop-color="#FF83E1" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint2_linear_18_28637" x1="16" y1="2.375" x2="10" y2="17.125" gradientUnits="userSpaceOnUse">
+<stop offset="0.399003" stop-color="#FF80E1" stop-opacity="0"/>
+<stop offset="0.953305" stop-color="#E153BB"/>
+</linearGradient>
+<radialGradient id="paint3_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(25.25 14.5) rotate(140.793) scale(6.1301 2.87771)">
+<stop stop-color="#FF74DE"/>
+<stop offset="1" stop-color="#FF73DB" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint4_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.5 16.375) scale(5.5 8.8125)">
+<stop offset="0.465909" stop-color="#DB62C1"/>
+<stop offset="0.545455" stop-color="#DE60C3" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint5_linear_18_28637" x1="22" y1="13.1875" x2="8.77939" y2="13.1875" gradientUnits="userSpaceOnUse">
+<stop stop-color="#E9B4AF"/>
+<stop offset="0.888764" stop-color="#C9939B"/>
+</linearGradient>
+<radialGradient id="paint6_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21 7.375) rotate(129.523) scale(12.9639 14.3163)">
+<stop stop-color="#FFBAEA"/>
+<stop offset="0.451156" stop-color="#FF97E4"/>
+<stop offset="1" stop-color="#FF83E1" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint7_linear_18_28637" x1="16" y1="2" x2="10" y2="16.75" gradientUnits="userSpaceOnUse">
+<stop offset="0.399003" stop-color="#FF80E1" stop-opacity="0"/>
+<stop offset="0.953305" stop-color="#E153BB"/>
+</linearGradient>
+<radialGradient id="paint8_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(25.25 14.125) rotate(140.793) scale(6.1301 2.87771)">
+<stop stop-color="#FF74DE"/>
+<stop offset="1" stop-color="#FF73DB" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint9_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.5 16) scale(5.5 8.8125)">
+<stop offset="0.465909" stop-color="#DB62C1"/>
+<stop offset="0.545455" stop-color="#DE60C3" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint10_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(21.6875 15.5625) scale(4.9375 8.07417)">
+<stop offset="0.414524" stop-color="#FF67CC"/>
+<stop offset="0.50886" stop-color="#DE60C3" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint11_linear_18_28637" x1="20.0625" y1="18" x2="24.875" y2="8" gradientUnits="userSpaceOnUse">
+<stop stop-color="#2E9CF8"/>
+<stop offset="1" stop-color="#42B6F8"/>
+</linearGradient>
+<linearGradient id="paint12_linear_18_28637" x1="25.1875" y1="9.4375" x2="20.4063" y2="18.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="#5AC6FF"/>
+<stop offset="1" stop-color="#3FACFF"/>
+</linearGradient>
+<radialGradient id="paint13_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.875 25.25) rotate(-31.6755) scale(13.8067 26.8563)">
+<stop offset="0.137131" stop-color="#518EF4"/>
+<stop offset="0.415574" stop-color="#60BCFF"/>
+<stop offset="0.680287" stop-color="#62D2FF"/>
+<stop offset="1" stop-color="#5FD4FF"/>
+</radialGradient>
+<linearGradient id="paint14_linear_18_28637" x1="20.8125" y1="23.3125" x2="19.875" y2="22.6875" gradientUnits="userSpaceOnUse">
+<stop offset="0.138462" stop-color="#64BDFF"/>
+<stop offset="1" stop-color="#64B6FF" stop-opacity="0"/>
+</linearGradient>
+<radialGradient id="paint15_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.2031 5.61759) rotate(153.418) scale(1.20556 1.11753)">
+<stop stop-color="#CD7BA2"/>
+<stop offset="1" stop-color="#CD7BA2" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint16_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11.2031 6.69656) rotate(153.418) scale(1.20556 1.11753)">
+<stop stop-color="#CD7BA2"/>
+<stop offset="1" stop-color="#CD7BA2" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint17_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.2031 7.67923) rotate(153.418) scale(1.20556 1.11753)">
+<stop offset="0.0843161" stop-color="#CB71AE"/>
+<stop offset="1" stop-color="#CD7BA2" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint18_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.7188 5.29688) rotate(123.32) scale(0.910235)">
+<stop offset="0.159787" stop-color="#FBEDFD"/>
+<stop offset="0.854934" stop-color="#E2C9E5"/>
+</radialGradient>
+<radialGradient id="paint19_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(11.7344 6.28125) rotate(123.32) scale(0.910235)">
+<stop offset="0.159787" stop-color="#FBEDFD"/>
+<stop offset="0.854934" stop-color="#E2C9E5"/>
+</radialGradient>
+<radialGradient id="paint20_radial_18_28637" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.7188 7.34875) rotate(123.32) scale(0.910235)">
+<stop offset="0.159787" stop-color="#FBEDFD"/>
+<stop offset="0.854934" stop-color="#E2C9E5"/>
+</radialGradient>
+</defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/shortcake_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/shortcake_color.svg
new file mode 100644
index 0000000000..66e8f91f19
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/shortcake_color.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <g transform="matrix(1,0,0,1,0,0.528489)">
+ <path d="M3.627,29.819L28.256,22.752L29.044,22.028L29.044,7.978L28.022,5.999L2.073,13.343L2.073,28.287C2.073,29.512 2.568,30.072 3.627,29.819Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
+ <path d="M3.627,29.819L28.256,22.752L29.044,22.028L29.044,7.978L28.022,5.999L2.073,13.343L2.073,28.287C2.073,29.512 2.568,30.072 3.627,29.819Z" style="fill:url(#_Linear2);fill-rule:nonzero;"/>
+ <path d="M3.627,29.819L28.256,22.752L29.044,22.028L29.044,7.978L28.022,5.999L2.073,13.343L2.073,28.287C2.073,29.512 2.568,30.072 3.627,29.819Z" style="fill:url(#_Linear3);fill-rule:nonzero;"/>
+ <path d="M3.627,29.819L28.256,22.752L29.044,22.028L29.044,7.978L28.022,5.999L2.073,13.343L2.073,28.287C2.073,29.512 2.568,30.072 3.627,29.819Z" style="fill:url(#_Linear4);fill-rule:nonzero;"/>
+ <path d="M3.627,29.819L28.256,22.752L29.044,22.028L29.044,7.978L28.022,5.999L2.073,13.343L2.073,28.287C2.073,29.512 2.568,30.072 3.627,29.819Z" style="fill:url(#_Linear5);fill-rule:nonzero;"/>
+ <path d="M13.034,2.508L3.064,11.731C1.118,13.393 2.129,14.542 3.998,14.13L27.024,7.524C27.616,7.383 27.99,7.338 27.99,8.023L27.99,12.042C27.99,12.73 28.054,13.088 27.616,13.226C27.616,13.226 5.411,19.92 4.933,20.05C4.455,20.18 3.393,20.378 3.064,20.33C2.734,20.282 2.316,20.05 2.129,20.05C1.942,20.05 1.942,20.158 1.942,20.673L1.942,22.84C1.942,23.003 2.083,23.108 2.083,23.108C2.61,23.445 3.335,23.59 3.998,23.415L27.243,16.685C27.616,16.591 27.99,16.841 27.99,17.308L27.99,22.606C27.99,22.928 28.185,22.945 28.322,22.928C29.314,22.642 30,22.055 30,21.047L30,8.023C30,7.17 29.383,5.745 27.99,5.312L16.96,2.726C15.913,2.508 13.888,1.847 13.034,2.508Z" style="fill:url(#_Linear6);fill-rule:nonzero;"/>
+ <path d="M13.034,2.508L3.064,11.731C1.118,13.393 2.129,14.542 3.998,14.13L27.024,7.524C27.616,7.383 27.99,7.338 27.99,8.023L27.99,12.042C27.99,12.73 28.054,13.088 27.616,13.226C27.616,13.226 5.411,19.92 4.933,20.05C4.455,20.18 3.393,20.378 3.064,20.33C2.734,20.282 2.316,20.05 2.129,20.05C1.942,20.05 1.942,20.158 1.942,20.673L1.942,22.84C1.942,23.003 2.083,23.108 2.083,23.108C2.61,23.445 3.335,23.59 3.998,23.415L27.243,16.685C27.616,16.591 27.99,16.841 27.99,17.308L27.99,22.606C27.99,22.928 28.185,22.945 28.322,22.928C29.314,22.642 30,22.055 30,21.047L30,8.023C30,7.17 29.383,5.745 27.99,5.312L16.96,2.726C15.913,2.508 13.888,1.847 13.034,2.508Z" style="fill:url(#_Linear7);fill-rule:nonzero;"/>
+ <path d="M13.034,2.508L3.064,11.731C1.118,13.393 2.129,14.542 3.998,14.13L27.024,7.524C27.616,7.383 27.99,7.338 27.99,8.023L27.99,12.042C27.99,12.73 28.054,13.088 27.616,13.226C27.616,13.226 5.411,19.92 4.933,20.05C4.455,20.18 3.393,20.378 3.064,20.33C2.734,20.282 2.316,20.05 2.129,20.05C1.942,20.05 1.942,20.158 1.942,20.673L1.942,22.84C1.942,23.003 2.083,23.108 2.083,23.108C2.61,23.445 3.335,23.59 3.998,23.415L27.243,16.685C27.616,16.591 27.99,16.841 27.99,17.308L27.99,22.606C27.99,22.928 28.185,22.945 28.322,22.928C29.314,22.642 30,22.055 30,21.047L30,8.023C30,7.17 29.383,5.745 27.99,5.312L16.96,2.726C15.913,2.508 13.888,1.847 13.034,2.508Z" style="fill:url(#_Linear8);fill-rule:nonzero;"/>
+ <path d="M13.034,2.508L3.064,11.731C1.118,13.393 2.129,14.542 3.998,14.13L27.024,7.524C27.616,7.383 27.99,7.338 27.99,8.023L27.99,12.042C27.99,12.73 28.054,13.088 27.616,13.226C27.616,13.226 5.411,19.92 4.933,20.05C4.455,20.18 3.393,20.378 3.064,20.33C2.734,20.282 2.316,20.05 2.129,20.05C1.942,20.05 1.942,20.158 1.942,20.673L1.942,22.84C1.942,23.003 2.083,23.108 2.083,23.108C2.61,23.445 3.335,23.59 3.998,23.415L27.243,16.685C27.616,16.591 27.99,16.841 27.99,17.308L27.99,22.606C27.99,22.928 28.185,22.945 28.322,22.928C29.314,22.642 30,22.055 30,21.047L30,8.023C30,7.17 29.383,5.745 27.99,5.312L16.96,2.726C15.913,2.508 13.888,1.847 13.034,2.508Z" style="fill:url(#_Linear9);fill-rule:nonzero;"/>
+ <path d="M13.034,2.508L3.064,11.731C1.118,13.393 2.129,14.542 3.998,14.13L27.024,7.524C27.616,7.383 27.99,7.338 27.99,8.023L27.99,12.042C27.99,12.73 28.054,13.088 27.616,13.226C27.616,13.226 5.411,19.92 4.933,20.05C4.455,20.18 3.393,20.378 3.064,20.33C2.734,20.282 2.316,20.05 2.129,20.05C1.942,20.05 1.942,20.158 1.942,20.673L1.942,22.84C1.942,23.003 2.083,23.108 2.083,23.108C2.61,23.445 3.335,23.59 3.998,23.415L27.243,16.685C27.616,16.591 27.99,16.841 27.99,17.308L27.99,22.606C27.99,22.928 28.185,22.945 28.322,22.928C29.314,22.642 30,22.055 30,21.047L30,8.023C30,7.17 29.383,5.745 27.99,5.312L16.96,2.726C15.913,2.508 13.888,1.847 13.034,2.508Z" style="fill:url(#_Radial10);fill-rule:nonzero;"/>
+ <path d="M13.034,2.508L3.064,11.731C1.118,13.393 2.129,14.542 3.998,14.13L27.024,7.524C27.616,7.383 27.99,7.338 27.99,8.023L27.99,12.042C27.99,12.73 28.054,13.088 27.616,13.226C27.616,13.226 5.411,19.92 4.933,20.05C4.455,20.18 3.393,20.378 3.064,20.33C2.734,20.282 2.316,20.05 2.129,20.05C1.942,20.05 1.942,20.158 1.942,20.673L1.942,22.84C1.942,23.003 2.083,23.108 2.083,23.108C2.61,23.445 3.335,23.59 3.998,23.415L27.243,16.685C27.616,16.591 27.99,16.841 27.99,17.308L27.99,22.606C27.99,22.928 28.185,22.945 28.322,22.928C29.314,22.642 30,22.055 30,21.047L30,8.023C30,7.17 29.383,5.745 27.99,5.312L16.96,2.726C15.913,2.508 13.888,1.847 13.034,2.508Z" style="fill:url(#_Linear11);fill-rule:nonzero;"/>
+ <path d="M13.034,2.508L3.064,11.731C1.118,13.393 2.129,14.542 3.998,14.13L27.024,7.524C27.616,7.383 27.99,7.338 27.99,8.023L27.99,12.042C27.99,12.73 28.054,13.088 27.616,13.226C27.616,13.226 5.411,19.92 4.933,20.05C4.455,20.18 3.393,20.378 3.064,20.33C2.734,20.282 2.316,20.05 2.129,20.05C1.942,20.05 1.942,20.158 1.942,20.673L1.942,22.84C1.942,23.003 2.083,23.108 2.083,23.108C2.61,23.445 3.335,23.59 3.998,23.415L27.243,16.685C27.616,16.591 27.99,16.841 27.99,17.308L27.99,22.606C27.99,22.928 28.185,22.945 28.322,22.928C29.314,22.642 30,22.055 30,21.047L30,8.023C30,7.17 29.383,5.745 27.99,5.312L16.96,2.726C15.913,2.508 13.888,1.847 13.034,2.508Z" style="fill:url(#_Linear12);fill-rule:nonzero;"/>
+ <path d="M13.034,2.508L3.064,11.731C1.118,13.393 2.129,14.542 3.998,14.13L27.024,7.524C27.616,7.383 27.99,7.338 27.99,8.023L27.99,12.042C27.99,12.73 28.054,13.088 27.616,13.226C27.616,13.226 5.411,19.92 4.933,20.05C4.455,20.18 3.393,20.378 3.064,20.33C2.734,20.282 2.316,20.05 2.129,20.05C1.942,20.05 1.942,20.158 1.942,20.673L1.942,22.84C1.942,23.003 2.083,23.108 2.083,23.108C2.61,23.445 3.335,23.59 3.998,23.415L27.243,16.685C27.616,16.591 27.99,16.841 27.99,17.308L27.99,22.606C27.99,22.928 28.185,22.945 28.322,22.928C29.314,22.642 30,22.055 30,21.047L30,8.023C30,7.17 29.383,5.745 27.99,5.312L16.96,2.726C15.913,2.508 13.888,1.847 13.034,2.508Z" style="fill:url(#_Radial13);fill-rule:nonzero;"/>
+ <g>
+ <path d="M12.664,7.578C12.024,7.578 11.803,7.217 11.891,6.554C12.556,-2.951 20.734,7.578 14.627,7.578L12.664,7.578Z" style="fill:rgb(213,45,38);fill-rule:nonzero;"/>
+ <path d="M12.664,7.578C12.024,7.578 11.803,7.217 11.891,6.554C12.556,-2.951 20.734,7.578 14.627,7.578L12.664,7.578Z" style="fill:rgb(162,56,40);fill-rule:nonzero;"/>
+ </g>
+ <path d="M12.024,4.517C12.15,3.889 12.359,2.115 14.54,1.681L16.896,2.888L17.754,5.463C16.95,7.624 15.291,7.47 14.627,7.47L12.664,7.47C12.024,7.47 11.66,7.106 11.748,6.437L12.024,4.517Z" style="fill:rgb(213,45,38);fill-rule:nonzero;"/>
+ <path d="M12.024,4.517C12.15,3.889 12.359,2.115 14.54,1.681L16.896,2.888L17.754,5.463C16.95,7.624 15.291,7.47 14.627,7.47L12.664,7.47C12.024,7.47 11.66,7.106 11.748,6.437L12.024,4.517Z" style="fill:url(#_Radial14);fill-rule:nonzero;"/>
+ <g>
+ <path d="M16.898,4.469C16.459,3.974 16.542,3.199 16.542,3.199C16.542,3.199 15.848,3.183 15.5,2.805C13.208,0.515 16.006,2.044 16.727,2.687C17.664,3.523 17.828,4.992 17.828,4.992C17.751,5.087 17.984,5.484 17.734,5.344C17.484,5.203 16.898,4.469 16.898,4.469Z" style="fill:rgb(141,197,39);fill-rule:nonzero;"/>
+ <path d="M16.898,4.469C16.459,3.974 16.542,3.199 16.542,3.199C16.542,3.199 15.848,3.183 15.5,2.805C13.208,0.515 16.006,2.044 16.727,2.687C17.664,3.523 17.828,4.992 17.828,4.992C17.751,5.087 17.984,5.484 17.734,5.344C17.484,5.203 16.898,4.469 16.898,4.469Z" style="fill:rgb(103,42,23);fill-rule:nonzero;"/>
+ </g>
+ <path d="M16.94,4.372C16.5,3.877 16.62,3.121 16.62,3.121C16.62,3.121 15.906,3.121 15.558,2.743L14.365,1.507C14.301,1.419 14.365,1.323 14.453,1.259C14.453,1.259 15.154,0.728 16.242,1.419C16.844,1.863 17.988,3.281 18.278,3.877C18.805,4.777 18.132,5.55 18.132,5.55C18.055,5.645 17.965,5.672 17.856,5.55L16.94,4.372Z" style="fill:rgb(141,197,39);fill-rule:nonzero;"/>
+ <path d="M16.94,4.372C16.5,3.877 16.62,3.121 16.62,3.121C16.62,3.121 15.906,3.121 15.558,2.743L14.365,1.507C14.301,1.419 14.365,1.323 14.453,1.259C14.453,1.259 15.154,0.728 16.242,1.419C16.844,1.863 17.988,3.281 18.278,3.877C18.805,4.777 18.132,5.55 18.132,5.55C18.055,5.645 17.965,5.672 17.856,5.55L16.94,4.372Z" style="fill:url(#_Radial15);fill-rule:nonzero;"/>
+ </g>
+ <defs>
+ <linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(24.25,-0.0312,0.0312,24.25,1.6875,18.4687)"><stop offset="0" style="stop-color:rgb(202,149,127);stop-opacity:1"/><stop offset="0.01" style="stop-color:rgb(202,149,127);stop-opacity:1"/><stop offset="0.05" style="stop-color:rgb(255,180,164);stop-opacity:1"/><stop offset="0.08" style="stop-color:rgb(255,214,176);stop-opacity:1"/><stop offset="0.14" style="stop-color:rgb(255,217,176);stop-opacity:1"/><stop offset="0.52" style="stop-color:rgb(255,193,138);stop-opacity:1"/><stop offset="0.92" style="stop-color:rgb(255,157,80);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(255,157,80);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.5583,5.125,-5.125,1.5583,14,21.6875)"><stop offset="0" style="stop-color:rgb(255,179,123);stop-opacity:0"/><stop offset="0.78" style="stop-color:rgb(255,179,123);stop-opacity:0"/><stop offset="0.89" style="stop-color:rgb(255,156,129);stop-opacity:1"/><stop offset="0.93" style="stop-color:rgb(246,127,157);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(246,127,157);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear3" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.125,4.0625,-4.0625,1.125,19.875,15.1875)"><stop offset="0" style="stop-color:rgb(240,132,86);stop-opacity:0"/><stop offset="0.26" style="stop-color:rgb(240,132,86);stop-opacity:0"/><stop offset="0.76" style="stop-color:rgb(228,121,75);stop-opacity:1"/><stop offset="0.92" style="stop-color:rgb(228,121,75);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(228,121,75);stop-opacity:0"/></linearGradient>
+ <linearGradient id="_Linear4" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.8792,3.125,-3.125,0.8792,15.5583,8.0625)"><stop offset="0" style="stop-color:rgb(240,132,86);stop-opacity:0"/><stop offset="0.26" style="stop-color:rgb(240,132,86);stop-opacity:0"/><stop offset="0.76" style="stop-color:rgb(228,121,75);stop-opacity:1"/><stop offset="0.92" style="stop-color:rgb(228,121,75);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(228,121,75);stop-opacity:0"/></linearGradient>
+ <linearGradient id="_Linear5" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.92737,0,0,1.92737,2.07263,20.5)"><stop offset="0" style="stop-color:rgb(194,150,119);stop-opacity:1"/><stop offset="0.58" style="stop-color:rgb(254,179,163);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(255,206,177);stop-opacity:0"/></linearGradient>
+ <linearGradient id="_Linear6" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(6.6562,22.5312,-22.5312,6.6562,10.9688,3.46875)"><stop offset="0" style="stop-color:rgb(243,207,162);stop-opacity:1"/><stop offset="0.26" style="stop-color:rgb(243,207,162);stop-opacity:1"/><stop offset="0.3" style="stop-color:rgb(255,240,203);stop-opacity:1"/><stop offset="0.32" style="stop-color:rgb(255,213,206);stop-opacity:1"/><stop offset="0.55" style="stop-color:rgb(255,214,207);stop-opacity:1"/><stop offset="0.6" style="stop-color:rgb(255,254,226);stop-opacity:1"/><stop offset="0.62" style="stop-color:rgb(255,228,229);stop-opacity:1"/><stop offset="0.97" style="stop-color:rgb(255,227,227);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(249,170,224);stop-opacity:1"/></linearGradient>
+ <linearGradient id="_Linear7" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.74547,0,0,1.74547,1.94203,21.625)"><stop offset="0" style="stop-color:rgb(198,156,155);stop-opacity:1"/><stop offset="0.14" style="stop-color:rgb(221,164,181);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(248,182,211);stop-opacity:0"/></linearGradient>
+ <linearGradient id="_Linear8" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.45313,0.48438,-0.48438,0.45313,8.5,6.54687)"><stop offset="0" style="stop-color:rgb(183,162,135);stop-opacity:1"/><stop offset="0.82" style="stop-color:rgb(207,177,140);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(207,177,140);stop-opacity:0"/></linearGradient>
+ <linearGradient id="_Linear9" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.0782,0.35156,-0.35156,-0.0782,21.2969,3.67188)"><stop offset="0" style="stop-color:rgb(183,162,135);stop-opacity:1"/><stop offset="0.82" style="stop-color:rgb(207,177,140);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(207,177,140);stop-opacity:0"/></linearGradient>
+ <radialGradient id="_Radial10" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.53191e-15,6.61319,-3.0625,-1.1725e-15,28.9375,12.8556)"><stop offset="0" style="stop-color:rgb(255,222,211);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(255,222,213);stop-opacity:0"/></radialGradient>
+ <linearGradient id="_Linear11" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-2.4375,2.98508e-16,-2.98508e-16,-2.4375,30,12.8556)"><stop offset="0" style="stop-color:rgb(214,192,170);stop-opacity:1"/><stop offset="0.04" style="stop-color:rgb(246,212,188);stop-opacity:1"/><stop offset="0.19" style="stop-color:rgb(255,222,212);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(255,222,211);stop-opacity:0"/></linearGradient>
+ <linearGradient id="_Linear12" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.9218,2.2343,-2.2343,0.9218,27.6094,20.9844)"><stop offset="0" style="stop-color:rgb(255,214,227);stop-opacity:0"/><stop offset="0.29" style="stop-color:rgb(255,214,227);stop-opacity:0"/><stop offset="0.59" style="stop-color:rgb(244,168,220);stop-opacity:0"/><stop offset="0.95" style="stop-color:rgb(229,150,221);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(229,150,221);stop-opacity:1"/></linearGradient>
+ <radialGradient id="_Radial13" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0.0624985,1.78125,-4.40342,0.154502,13.8281,6.6875)"><stop offset="0" style="stop-color:rgb(178,129,98);stop-opacity:1"/><stop offset="0.52" style="stop-color:rgb(178,129,98);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(194,154,121);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial14" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-1.81247,4.09702,-4.25876,-1.88402,16.1875,3.375)"><stop offset="0" style="stop-color:rgb(255,106,131);stop-opacity:1"/><stop offset="0.14" style="stop-color:rgb(255,106,131);stop-opacity:1"/><stop offset="0.52" style="stop-color:rgb(238,45,71);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(243,47,89);stop-opacity:0"/></radialGradient>
+ <radialGradient id="_Radial15" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-8.743e-16,2.28362,-2.07009,-7.92548e-16,16.4093,3.34783)"><stop offset="0" style="stop-color:rgb(177,235,103);stop-opacity:1"/><stop offset="0.9" style="stop-color:rgb(175,234,99);stop-opacity:0"/><stop offset="1" style="stop-color:rgb(175,234,99);stop-opacity:0"/></radialGradient>
+ </defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg
new file mode 100644
index 0000000000..37be9c0cb3
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg
@@ -0,0 +1,140 @@
+<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14.8021 18.0831H14.0721H12.0221H11.2921V20.1093C11.2921 20.1093 11.7321 20.0387 11.9421 20.2907C12.0521 20.4218 12.0321 20.7444 12.0321 20.7444C12.0321 21.3189 12.4921 21.7726 13.0521 21.7726C13.6121 21.7726 14.0721 21.3089 14.0721 20.7444C14.0721 20.7444 14.0621 20.3411 14.1821 20.1395C14.3321 19.8774 14.6121 19.8674 14.6121 19.8674L14.8021 18.0831Z" fill="url(#paint0_radial_18_28477)"/>
+<path d="M15.7121 22.6293H10.7021L11.5221 29.2319C11.5721 29.6654 11.9421 29.988 12.3721 29.988H15.7221H19.9821C20.4121 29.988 20.7721 29.6654 20.8321 29.2319L21.6521 22.6293H15.7121Z" fill="url(#paint1_linear_18_28477)"/>
+<path d="M15.7121 22.6293H10.7021L11.5221 29.2319C11.5721 29.6654 11.9421 29.988 12.3721 29.988H15.7221H19.9821C20.4121 29.988 20.7721 29.6654 20.8321 29.2319L21.6521 22.6293H15.7121Z" fill="url(#paint2_linear_18_28477)"/>
+<g opacity="0.5">
+<path d="M19.8421 22.6293H18.4421L16.2321 24.857L14.0221 22.6293H12.6221L10.9021 24.3631L11.2221 26.9739L12.6721 28.4356L11.5921 29.5243C11.7321 29.7964 12.0221 29.988 12.3521 29.988H12.4221L13.3121 29.0908L14.2021 29.988H15.4921L13.9821 28.4356L16.2421 26.1574L18.5021 28.4356L16.9621 29.988H18.2521L19.1421 29.0908L20.0321 29.988C20.3721 29.9678 20.6521 29.746 20.7721 29.4335L19.7821 28.4356L21.0821 27.1251L21.2621 25.6433L19.1321 27.7904L16.8721 25.5123L19.1321 23.2341L21.2921 25.4115L21.4321 24.2522L19.8421 22.6293ZM13.3321 27.7804L11.0721 25.5022L13.3321 23.224L15.5921 25.5022L13.3321 27.7804Z" fill="url(#paint3_linear_18_28477)"/>
+<path d="M19.8421 22.6293H18.4421L16.2321 24.857L14.0221 22.6293H12.6221L10.9021 24.3631L11.2221 26.9739L12.6721 28.4356L11.5921 29.5243C11.7321 29.7964 12.0221 29.988 12.3521 29.988H12.4221L13.3121 29.0908L14.2021 29.988H15.4921L13.9821 28.4356L16.2421 26.1574L18.5021 28.4356L16.9621 29.988H18.2521L19.1421 29.0908L20.0321 29.988C20.3721 29.9678 20.6521 29.746 20.7721 29.4335L19.7821 28.4356L21.0821 27.1251L21.2621 25.6433L19.1321 27.7904L16.8721 25.5123L19.1321 23.2341L21.2921 25.4115L21.4321 24.2522L19.8421 22.6293ZM13.3321 27.7804L11.0721 25.5022L13.3321 23.224L15.5921 25.5022L13.3321 27.7804Z" fill="url(#paint4_linear_18_28477)"/>
+</g>
+<path d="M21.8359 7.40625C21.8359 6.46094 20.5156 5.81254 19.7969 5.81254H18.6719C18.5019 5.82262 18 5.69535 18 5.31254C18 5.31254 17.9531 4.30926 17.9531 3.78129C17.9531 3.25331 17.6791 2.46623 16.7891 2.54688C16.0391 2.61744 15.7821 3.31528 15.7221 3.6177C15.6621 3.95035 15.4288 4.43069 14.9688 4.77342C14.0188 5.47905 11.1641 6.59373 11.1641 6.59373C10.3227 6.87643 9.64311 7.8143 9.71094 8.70311C9.71094 9.14752 10.0234 9.55505 10.0234 9.87498C10.0234 10.1949 9.41406 10.3672 9.41406 10.3672L22.0156 9.35938C22.0156 9.35938 21.5547 9.07812 21.5547 8.74219C21.5547 8.46094 21.8359 8.17188 21.8359 7.40625Z" fill="url(#paint5_linear_18_28477)"/>
+<path d="M21.8359 7.40625C21.8359 6.46094 20.5156 5.81254 19.7969 5.81254H18.6719C18.5019 5.82262 18 5.69535 18 5.31254C18 5.31254 17.9531 4.30926 17.9531 3.78129C17.9531 3.25331 17.6791 2.46623 16.7891 2.54688C16.0391 2.61744 15.7821 3.31528 15.7221 3.6177C15.6621 3.95035 15.4288 4.43069 14.9688 4.77342C14.0188 5.47905 11.1641 6.59373 11.1641 6.59373C10.3227 6.87643 9.64311 7.8143 9.71094 8.70311C9.71094 9.14752 10.0234 9.55505 10.0234 9.87498C10.0234 10.1949 9.41406 10.3672 9.41406 10.3672L22.0156 9.35938C22.0156 9.35938 21.5547 9.07812 21.5547 8.74219C21.5547 8.46094 21.8359 8.17188 21.8359 7.40625Z" fill="url(#paint6_radial_18_28477)"/>
+<path d="M21.8359 7.40625C21.8359 6.46094 20.5156 5.81254 19.7969 5.81254H18.6719C18.5019 5.82262 18 5.69535 18 5.31254C18 5.31254 17.9531 4.30926 17.9531 3.78129C17.9531 3.25331 17.6791 2.46623 16.7891 2.54688C16.0391 2.61744 15.7821 3.31528 15.7221 3.6177C15.6621 3.95035 15.4288 4.43069 14.9688 4.77342C14.0188 5.47905 11.1641 6.59373 11.1641 6.59373C10.3227 6.87643 9.64311 7.8143 9.71094 8.70311C9.71094 9.14752 10.0234 9.55505 10.0234 9.87498C10.0234 10.1949 9.41406 10.3672 9.41406 10.3672L22.0156 9.35938C22.0156 9.35938 21.5547 9.07812 21.5547 8.74219C21.5547 8.46094 21.8359 8.17188 21.8359 7.40625Z" fill="url(#paint7_radial_18_28477)"/>
+<path d="M21.8359 7.40625C21.8359 6.46094 20.5156 5.81254 19.7969 5.81254H18.6719C18.5019 5.82262 18 5.69535 18 5.31254C18 5.31254 17.9531 4.30926 17.9531 3.78129C17.9531 3.25331 17.6791 2.46623 16.7891 2.54688C16.0391 2.61744 15.7821 3.31528 15.7221 3.6177C15.6621 3.95035 15.4288 4.43069 14.9688 4.77342C14.0188 5.47905 11.1641 6.59373 11.1641 6.59373C10.3227 6.87643 9.64311 7.8143 9.71094 8.70311C9.71094 9.14752 10.0234 9.55505 10.0234 9.87498C10.0234 10.1949 9.41406 10.3672 9.41406 10.3672L22.0156 9.35938C22.0156 9.35938 21.5547 9.07812 21.5547 8.74219C21.5547 8.46094 21.8359 8.17188 21.8359 7.40625Z" fill="url(#paint8_radial_18_28477)"/>
+<path d="M21.3021 9.24249C19.7917 9.36788 9.8621 10.2505 9.8621 10.2505C8.5021 10.3715 7.59375 11.5468 7.59375 12.6484C7.59375 13.4453 7.99999 13.6406 7.99999 14.1171C7.99999 14.5937 7.59375 14.7265 7.59375 14.7265L24.1953 13.6406C24.1953 13.6406 23.7882 13.375 23.7882 12.7696C23.7882 12.3984 24.0625 12.2656 24.0625 11.5469C24.0625 10.2734 22.8125 9.11711 21.3021 9.24249Z" fill="url(#paint9_linear_18_28477)"/>
+<path d="M21.3021 9.24249C19.7917 9.36788 9.8621 10.2505 9.8621 10.2505C8.5021 10.3715 7.59375 11.5468 7.59375 12.6484C7.59375 13.4453 7.99999 13.6406 7.99999 14.1171C7.99999 14.5937 7.59375 14.7265 7.59375 14.7265L24.1953 13.6406C24.1953 13.6406 23.7882 13.375 23.7882 12.7696C23.7882 12.3984 24.0625 12.2656 24.0625 11.5469C24.0625 10.2734 22.8125 9.11711 21.3021 9.24249Z" fill="url(#paint10_radial_18_28477)"/>
+<path d="M21.3021 9.24249C19.7917 9.36788 9.8621 10.2505 9.8621 10.2505C8.5021 10.3715 7.59375 11.5468 7.59375 12.6484C7.59375 13.4453 7.99999 13.6406 7.99999 14.1171C7.99999 14.5937 7.59375 14.7265 7.59375 14.7265L24.1953 13.6406C24.1953 13.6406 23.7882 13.375 23.7882 12.7696C23.7882 12.3984 24.0625 12.2656 24.0625 11.5469C24.0625 10.2734 22.8125 9.11711 21.3021 9.24249Z" fill="url(#paint11_radial_18_28477)"/>
+<path d="M25.7721 15.956C25.6221 14.2827 24.1621 13.0428 22.5021 13.1839L8.54211 14.4137C6.88211 14.5649 5.65211 16.0367 5.79211 17.71C5.94211 19.3834 7.40211 20.6233 9.06211 20.4821L17.1021 19.7765C17.3721 19.7563 20.5221 19.4741 20.7621 19.4539L23.0221 19.2523C24.6821 19.1011 25.9121 17.6294 25.7721 15.956Z" fill="url(#paint12_radial_18_28477)"/>
+<path d="M22.2221 22.6294H10.2521C9.7821 22.6294 9.39211 22.2463 9.39211 21.7624V20.2605L23.0721 19.202V21.7624C23.0821 22.2463 22.6921 22.6294 22.2221 22.6294Z" fill="url(#paint13_linear_18_28477)"/>
+<path d="M22.2221 22.6294H10.2521C9.7821 22.6294 9.39211 22.2463 9.39211 21.7624V20.2605L23.0721 19.202V21.7624C23.0821 22.2463 22.6921 22.6294 22.2221 22.6294Z" fill="url(#paint14_linear_18_28477)"/>
+<g filter="url(#filter0_f_18_28477)">
+<path d="M22.5021 13.7943C19.496 14.0591 12.4038 17.4238 10.1094 19.4374C9.64133 19.8482 10.8122 20.1392 11.4175 20.4009C11.7187 20.5312 11.768 20.9875 11.8125 21.1249C11.9463 21.5384 12.3125 21.8749 12.8125 21.8749C13.3125 21.8749 13.8047 21.3327 13.8047 21.039C13.8047 20.8374 13.7109 20.4791 14.8437 20.4009C15.9766 20.3228 16.8518 20.2724 20.8125 19.9634C24.7732 19.6545 22.8858 16.4166 22.5021 13.7943Z" fill="url(#paint15_linear_18_28477)"/>
+<path d="M22.5021 13.7943C19.496 14.0591 12.4038 17.4238 10.1094 19.4374C9.64133 19.8482 10.8122 20.1392 11.4175 20.4009C11.7187 20.5312 11.768 20.9875 11.8125 21.1249C11.9463 21.5384 12.3125 21.8749 12.8125 21.8749C13.3125 21.8749 13.8047 21.3327 13.8047 21.039C13.8047 20.8374 13.7109 20.4791 14.8437 20.4009C15.9766 20.3228 16.8518 20.2724 20.8125 19.9634C24.7732 19.6545 22.8858 16.4166 22.5021 13.7943Z" fill="url(#paint16_radial_18_28477)"/>
+<path d="M22.5021 13.7943C19.496 14.0591 12.4038 17.4238 10.1094 19.4374C9.64133 19.8482 10.8122 20.1392 11.4175 20.4009C11.7187 20.5312 11.768 20.9875 11.8125 21.1249C11.9463 21.5384 12.3125 21.8749 12.8125 21.8749C13.3125 21.8749 13.8047 21.3327 13.8047 21.039C13.8047 20.8374 13.7109 20.4791 14.8437 20.4009C15.9766 20.3228 16.8518 20.2724 20.8125 19.9634C24.7732 19.6545 22.8858 16.4166 22.5021 13.7943Z" fill="#C67B4D"/>
+</g>
+<path d="M25.7721 15.956C25.6221 14.2827 24.1621 13.0428 22.5021 13.1839L8.54211 14.4137C6.88211 14.5649 5.65211 16.0367 5.79211 17.71C5.94211 19.3834 7.40211 20.6233 9.06211 20.4821L11.3921 20.2805C11.6121 20.2604 11.8021 20.4418 11.8021 20.6636L11.8321 20.8551C11.8321 21.3591 12.2421 21.7724 12.7421 21.7724C13.2421 21.7724 13.6521 21.3591 13.6521 20.8551L13.6221 20.4317C13.6221 20.2301 13.7721 20.0688 13.9721 20.0487L17.0921 19.7765C17.3621 19.7563 20.5121 19.4741 20.7521 19.4539L23.0121 19.2523C24.6821 19.1011 25.9121 17.6294 25.7721 15.956Z" fill="url(#paint17_linear_18_28477)"/>
+<path d="M25.7721 15.956C25.6221 14.2827 24.1621 13.0428 22.5021 13.1839L8.54211 14.4137C6.88211 14.5649 5.65211 16.0367 5.79211 17.71C5.94211 19.3834 7.40211 20.6233 9.06211 20.4821L11.3921 20.2805C11.6121 20.2604 11.8021 20.4418 11.8021 20.6636L11.8321 20.8551C11.8321 21.3591 12.2421 21.7724 12.7421 21.7724C13.2421 21.7724 13.6521 21.3591 13.6521 20.8551L13.6221 20.4317C13.6221 20.2301 13.7721 20.0688 13.9721 20.0487L17.0921 19.7765C17.3621 19.7563 20.5121 19.4741 20.7521 19.4539L23.0121 19.2523C24.6821 19.1011 25.9121 17.6294 25.7721 15.956Z" fill="url(#paint18_radial_18_28477)"/>
+<path d="M25.7721 15.956C25.6221 14.2827 24.1621 13.0428 22.5021 13.1839L8.54211 14.4137C6.88211 14.5649 5.65211 16.0367 5.79211 17.71C5.94211 19.3834 7.40211 20.6233 9.06211 20.4821L11.3921 20.2805C11.6121 20.2604 11.8021 20.4418 11.8021 20.6636L11.8321 20.8551C11.8321 21.3591 12.2421 21.7724 12.7421 21.7724C13.2421 21.7724 13.6521 21.3591 13.6521 20.8551L13.6221 20.4317C13.6221 20.2301 13.7721 20.0688 13.9721 20.0487L17.0921 19.7765C17.3621 19.7563 20.5121 19.4741 20.7521 19.4539L23.0121 19.2523C24.6821 19.1011 25.9121 17.6294 25.7721 15.956Z" fill="url(#paint19_radial_18_28477)"/>
+<path d="M25.7721 15.956C25.6221 14.2827 24.1621 13.0428 22.5021 13.1839L8.54211 14.4137C6.88211 14.5649 5.65211 16.0367 5.79211 17.71C5.94211 19.3834 7.40211 20.6233 9.06211 20.4821L11.3921 20.2805C11.6121 20.2604 11.8021 20.4418 11.8021 20.6636L11.8321 20.8551C11.8321 21.3591 12.2421 21.7724 12.7421 21.7724C13.2421 21.7724 13.6521 21.3591 13.6521 20.8551L13.6221 20.4317C13.6221 20.2301 13.7721 20.0688 13.9721 20.0487L17.0921 19.7765C17.3621 19.7563 20.5121 19.4741 20.7521 19.4539L23.0121 19.2523C24.6821 19.1011 25.9121 17.6294 25.7721 15.956Z" fill="url(#paint20_radial_18_28477)"/>
+<defs>
+<filter id="filter0_f_18_28477" x="9.00156" y="12.7943" width="15.2857" height="10.0806" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+<feGaussianBlur stdDeviation="0.5" result="effect1_foregroundBlur_18_28477"/>
+</filter>
+<radialGradient id="paint0_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.046 19.9264) scale(1.7953 1.80973)">
+<stop stop-color="#FFE0A8"/>
+<stop offset="1" stop-color="#FFDEA3"/>
+</radialGradient>
+<linearGradient id="paint1_linear_18_28477" x1="11.125" y1="26.75" x2="21.25" y2="26.75" gradientUnits="userSpaceOnUse">
+<stop stop-color="#C5987C"/>
+<stop offset="0.216049" stop-color="#CC8953"/>
+<stop offset="0.567901" stop-color="#EAA36E"/>
+<stop offset="1" stop-color="#FCBD73"/>
+</linearGradient>
+<linearGradient id="paint2_linear_18_28477" x1="16.1771" y1="22.6293" x2="16.1771" y2="29.988" gradientUnits="userSpaceOnUse">
+<stop offset="0.882728" stop-color="#C8746A" stop-opacity="0"/>
+<stop offset="1" stop-color="#BE6D82"/>
+</linearGradient>
+<linearGradient id="paint3_linear_18_28477" x1="10.9021" y1="25.75" x2="21.4321" y2="25.75" gradientUnits="userSpaceOnUse">
+<stop stop-color="#D4A789"/>
+<stop offset="0.424775" stop-color="#FAB88D"/>
+<stop offset="0.757159" stop-color="#FFF0C9"/>
+<stop offset="1" stop-color="#F9CF9A"/>
+</linearGradient>
+<linearGradient id="paint4_linear_18_28477" x1="16.1671" y1="22.6293" x2="16.1671" y2="29.988" gradientUnits="userSpaceOnUse">
+<stop offset="0.916702" stop-color="#E09A80" stop-opacity="0"/>
+<stop offset="1" stop-color="#CA7D95"/>
+</linearGradient>
+<linearGradient id="paint5_linear_18_28477" x1="16.25" y1="9.84375" x2="15.5977" y2="3.19204" gradientUnits="userSpaceOnUse">
+<stop offset="0.0275603" stop-color="#FFE4A8"/>
+<stop offset="0.143815" stop-color="#FFD59B"/>
+<stop offset="0.250725" stop-color="#FEDCAB"/>
+</linearGradient>
+<radialGradient id="paint6_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(20.9375 6.875) rotate(115.278) scale(2.48825 4.09612)">
+<stop stop-color="#FFF6BA"/>
+<stop offset="1" stop-color="#FFE5B1" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint7_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.1875 8.0625) rotate(82.6476) scale(5.86069 10.89)">
+<stop offset="0.45355" stop-color="#E0BB89" stop-opacity="0"/>
+<stop offset="0.662231" stop-color="#E6BE8D"/>
+</radialGradient>
+<radialGradient id="paint8_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.9063 4.46875) rotate(177.089) scale(3.69226 9.43834)">
+<stop offset="0.128265" stop-color="#FFF8BA"/>
+<stop offset="0.347135" stop-color="#FFE0AF"/>
+<stop offset="0.645989" stop-color="#FAD8A6" stop-opacity="0"/>
+</radialGradient>
+<linearGradient id="paint9_linear_18_28477" x1="16.9054" y1="13.754" x2="16.5214" y2="9.54913" gradientUnits="userSpaceOnUse">
+<stop offset="0.0571005" stop-color="#FFE4A8"/>
+<stop offset="0.157977" stop-color="#FFD59B"/>
+<stop offset="0.356286" stop-color="#FFDEA7"/>
+<stop offset="0.846352" stop-color="#FFE1A8"/>
+<stop offset="0.968133" stop-color="#FDD594"/>
+</linearGradient>
+<radialGradient id="paint10_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(20.4375 10.9375) rotate(174.189) scale(14.198 7.83563)">
+<stop offset="0.685205" stop-color="#FED29C" stop-opacity="0"/>
+<stop offset="0.780293" stop-color="#F9C994"/>
+<stop offset="0.880128" stop-color="#E4B783"/>
+</radialGradient>
+<radialGradient id="paint11_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(22.875 10.5625) rotate(123.275) scale(2.39221 3.65631)">
+<stop stop-color="#FFF8BD"/>
+<stop offset="1" stop-color="#FFE7B1" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint12_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.7821 16.8319) scale(7.523 7.58349)">
+<stop offset="0.00558659" stop-color="#FFF5D4"/>
+<stop offset="1" stop-color="#FFDEA3"/>
+</radialGradient>
+<linearGradient id="paint13_linear_18_28477" x1="9.39211" y1="21.625" x2="22.625" y2="21.625" gradientUnits="userSpaceOnUse">
+<stop stop-color="#D2AD8C"/>
+<stop offset="0.0884459" stop-color="#D19870"/>
+<stop offset="0.319877" stop-color="#EAA26B"/>
+<stop offset="0.744954" stop-color="#F8BA81"/>
+<stop offset="1" stop-color="#F8C589"/>
+</linearGradient>
+<linearGradient id="paint14_linear_18_28477" x1="16.2322" y1="19.202" x2="16.2322" y2="22.6294" gradientUnits="userSpaceOnUse">
+<stop offset="0.798134" stop-color="#DB9176" stop-opacity="0"/>
+<stop offset="0.89843" stop-color="#E19978"/>
+<stop offset="1" stop-color="#C97758"/>
+</linearGradient>
+<linearGradient id="paint15_linear_18_28477" x1="17" y1="20.9855" x2="16.5625" y2="14.2355" gradientUnits="userSpaceOnUse">
+<stop offset="0.029796" stop-color="#D89387"/>
+<stop offset="0.312982" stop-color="#F8C6A3"/>
+<stop offset="0.577315" stop-color="#FFDBA7"/>
+<stop offset="0.840851" stop-color="#FFE0A6"/>
+<stop offset="0.978024" stop-color="#FDD594"/>
+</linearGradient>
+<radialGradient id="paint16_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(18.5625 16.048) rotate(83.3255) scale(5.91509 13.7581)">
+<stop offset="0.720443" stop-color="#E1AE83" stop-opacity="0"/>
+<stop offset="1" stop-color="#E7B286"/>
+</radialGradient>
+<linearGradient id="paint17_linear_18_28477" x1="17" y1="20.375" x2="16.5625" y2="13.625" gradientUnits="userSpaceOnUse">
+<stop offset="0.029796" stop-color="#D89387"/>
+<stop offset="0.312982" stop-color="#F8C6A3"/>
+<stop offset="0.577315" stop-color="#FFDBA7"/>
+<stop offset="0.840851" stop-color="#FFE0A6"/>
+<stop offset="0.978024" stop-color="#FDD594"/>
+</linearGradient>
+<radialGradient id="paint18_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(18.5625 15.4375) rotate(83.3255) scale(5.91509 13.7581)">
+<stop offset="0.720443" stop-color="#E1AE83" stop-opacity="0"/>
+<stop offset="0.925198" stop-color="#E7B286"/>
+</radialGradient>
+<radialGradient id="paint19_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.3125 14.9375) rotate(113.929) scale(2.7736 3.64835)">
+<stop stop-color="#FFF8BD"/>
+<stop offset="1" stop-color="#FFEEB6" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint20_radial_18_28477" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.3125 20.8437) rotate(-5.0796) scale(1.41179 2.5081)">
+<stop offset="0.197452" stop-color="#FFD4A8"/>
+<stop offset="0.407382" stop-color="#FDC59F"/>
+<stop offset="0.831214" stop-color="#FBC49E" stop-opacity="0"/>
+</radialGradient>
+</defs>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/candy_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/candy_color.svg
new file mode 100644
index 0000000000..e673f430f5
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/candy_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M12,1.918L4,4L2.016,12L6,13.375L6,18L8,22L12,25.372L16.008,26L19,25.372L20,30L28,27L30,20L25.473,19L26,15L24,10L20,7L16.008,6L13,6L12,1.918Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/chocolate_bar_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/chocolate_bar_color.svg
new file mode 100644
index 0000000000..5cd39cc9e3
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/chocolate_bar_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M12,2.034L2.035,12L2.035,14L18,30L20,30L29.982,20L29.982,18L14,2.034L12,2.034Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/custard_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/custard_color.svg
new file mode 100644
index 0000000000..dd4870dd7d
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/custard_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M9,15L23,15L30,27L25.7,30L6.34,30L2,27L9,15Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg
new file mode 100644
index 0000000000..a8d5557f5c
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M15,2L11,3L8,5L7,6L5,9L4,12L4,20L5,23L7,26L11,29L14,30L18,30L21,29L25,26L27,23L28,20L28,12L27,9L25,6L24,5L21,3L17,2L15,2Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/lollipop_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/lollipop_color.svg
new file mode 100644
index 0000000000..d3778737a9
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/lollipop_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M12,2L7,3L3,7L2,12L3,16L5,19L8,21L12,22L18,21L27,30L30,30L30,27L21,18L22,14.25L22,11L21,8L19,5L16.5,3L12,2Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/pancakes_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/pancakes_color.svg
new file mode 100644
index 0000000000..b1a1a322e0
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/pancakes_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M15,2L14,3L8,4L6,5L4,8L4.011,15L2,19L2,22.36L3,25L5,28L10,30L22,30L27,28L29,25L30,22L30,19L27.989,15L27.989,8L26,5L24,4L18,3L17,2L15,2Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shaved_ice_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shaved_ice_color.svg
new file mode 100644
index 0000000000..00872c7a0c
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shaved_ice_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M15,2L11,3L8,6L7,8L6,11L6,13L7,16L8,18L15,30L17,30L24,18L25,16L26,13L26,11L25,8L24,6L21,3L17,2L15,2Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shortcake_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shortcake_color.svg
new file mode 100644
index 0000000000..e6ed1fbbf9
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shortcake_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M14,2L2,13L2,31L30,23L30,7L29,6L20,4L17,3L16,2L14,2Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/soft_ice_cream_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/soft_ice_cream_color.svg
new file mode 100644
index 0000000000..b77e0c3655
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/soft_ice_cream_color.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="32px" height="32px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
+ <path d="M17,2.541L14.078,5.402L10,7L10,10.367L8,11L8,14L5.781,16.265L6.594,19.627L9.414,20.916L12,29.988L21,29.988L22.016,22.629L23.072,21.772L23.072,19.202L25.783,17.473L25.783,14.727L24,13.173L24,10.367L22,9.233L22.016,6.454L18,5L17,2.541Z"/>
+</svg>
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/10000yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/10000yen.png
new file mode 100644
index 0000000000..bda777719d
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/10000yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/1000yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/1000yen.png
new file mode 100644
index 0000000000..4c462fb1f6
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/1000yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/100yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/100yen.png
new file mode 100644
index 0000000000..8911543af9
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/100yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/10yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/10yen.png
new file mode 100644
index 0000000000..041f773891
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/10yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/1yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/1yen.png
new file mode 100644
index 0000000000..cc6dcfd740
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/1yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/2000yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/2000yen.png
new file mode 100644
index 0000000000..6048b7c996
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/2000yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/5000yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/5000yen.png
new file mode 100644
index 0000000000..b0fe26db11
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/5000yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/500yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/500yen.png
new file mode 100644
index 0000000000..9e3d2b766b
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/500yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/50yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/50yen.png
new file mode 100644
index 0000000000..c8ef089972
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/50yen.png
Binary files differ
diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/5yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/5yen.png
new file mode 100644
index 0000000000..b120bdca36
--- /dev/null
+++ b/packages/frontend/assets/drop-and-fusion/yen_monos/5yen.png
Binary files differ
diff --git a/packages/frontend/assets/reversi/logo.png b/packages/frontend/assets/reversi/logo.png
new file mode 100644
index 0000000000..724a311ea1
--- /dev/null
+++ b/packages/frontend/assets/reversi/logo.png
Binary files differ
diff --git a/packages/frontend/assets/reversi/lose.mp3 b/packages/frontend/assets/reversi/lose.mp3
new file mode 100644
index 0000000000..b62d50baf7
--- /dev/null
+++ b/packages/frontend/assets/reversi/lose.mp3
Binary files differ
diff --git a/packages/frontend/assets/reversi/matched.mp3 b/packages/frontend/assets/reversi/matched.mp3
new file mode 100644
index 0000000000..f26d07614e
--- /dev/null
+++ b/packages/frontend/assets/reversi/matched.mp3
Binary files differ
diff --git a/packages/frontend/assets/reversi/put.mp3 b/packages/frontend/assets/reversi/put.mp3
new file mode 100644
index 0000000000..baa1b83195
--- /dev/null
+++ b/packages/frontend/assets/reversi/put.mp3
Binary files differ
diff --git a/packages/frontend/assets/reversi/stone_b.png b/packages/frontend/assets/reversi/stone_b.png
new file mode 100644
index 0000000000..9e98455a3e
--- /dev/null
+++ b/packages/frontend/assets/reversi/stone_b.png
Binary files differ
diff --git a/packages/frontend/assets/reversi/stone_w.png b/packages/frontend/assets/reversi/stone_w.png
new file mode 100644
index 0000000000..f2bee593dc
--- /dev/null
+++ b/packages/frontend/assets/reversi/stone_w.png
Binary files differ
diff --git a/packages/frontend/assets/reversi/win.mp3 b/packages/frontend/assets/reversi/win.mp3
new file mode 100644
index 0000000000..25402ce2a6
--- /dev/null
+++ b/packages/frontend/assets/reversi/win.mp3
Binary files differ
diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
index 535adc9c85..5d8cf05fff 100644
--- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
+++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts
index 68cdc0bc78..0ed2e14d2a 100644
--- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts
+++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 523fc281b3..91a391ac08 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -4,11 +4,11 @@
"type": "module",
"scripts": {
"watch": "vite",
- "dev": "vite --config vite.config.local-dev.ts",
+ "dev": "vite --config vite.config.local-dev.ts --debug hmr",
"build": "vite build",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
- "build-storybook": "pnpm build-storybook-pre && storybook build",
+ "build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
"chromatic": "chromatic",
"test": "vitest --run --globals",
"test-and-coverage": "vitest --run --coverage --globals",
@@ -19,18 +19,19 @@
"dependencies": {
"@discordapp/twemoji": "15.0.2",
"@github/webauthn-json": "2.1.1",
+ "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
+ "@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.5",
"@rollup/pluginutils": "5.1.0",
- "@syuilo/aiscript": "0.16.0",
+ "@syuilo/aiscript": "0.17.0",
"@tabler/icons-webfont": "2.44.0",
"@twemoji/parser": "15.0.0",
- "@vitejs/plugin-vue": "4.5.2",
- "@vue/compiler-sfc": "3.3.12",
- "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
+ "@vitejs/plugin-vue": "5.0.3",
+ "@vue/compiler-sfc": "3.4.18",
+ "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.2",
"astring": "1.8.6",
"broadcast-channel": "7.0.0",
- "browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
"buraha": "0.0.1",
"canvas-confetti": "1.6.1",
"chart.js": "4.4.1",
@@ -38,30 +39,31 @@
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
- "chromatic": "10.1.0",
+ "chromatic": "10.6.1",
"compare-versions": "6.1.0",
"cropperjs": "2.0.0-beta.4",
"date-fns": "2.30.0",
"escape-regexp": "0.0.1",
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
- "gsap": "3.12.4",
"idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.19.0",
"mfm-js": "0.24.0",
+ "misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
+ "misskey-reversi": "workspace:*",
"photoswipe": "5.4.3",
"punycode": "2.3.1",
- "rollup": "4.9.1",
+ "rollup": "4.9.6",
"sanitize-html": "2.11.0",
- "sass": "1.69.5",
- "shiki": "0.14.7",
+ "sass": "1.70.0",
+ "shiki": "1.0.0-beta.3",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
- "three": "0.159.0",
+ "three": "0.160.1",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.8",
@@ -69,69 +71,71 @@
"typescript": "5.3.3",
"uuid": "9.0.1",
"v-code-diff": "1.7.2",
- "vite": "5.0.10",
- "vue": "3.3.12",
+ "vite": "5.1.0",
+ "vue": "3.4.18",
"vuedraggable": "next"
},
"devDependencies": {
- "@storybook/addon-actions": "7.6.5",
- "@storybook/addon-essentials": "7.6.5",
- "@storybook/addon-interactions": "7.6.5",
- "@storybook/addon-links": "7.6.5",
- "@storybook/addon-storysource": "7.6.5",
- "@storybook/addons": "7.6.5",
- "@storybook/blocks": "7.6.5",
- "@storybook/core-events": "7.6.5",
- "@storybook/jest": "0.2.3",
- "@storybook/manager-api": "7.6.5",
- "@storybook/preview-api": "7.6.5",
- "@storybook/react": "7.6.5",
- "@storybook/react-vite": "7.6.5",
- "@storybook/testing-library": "0.2.2",
- "@storybook/theming": "7.6.5",
- "@storybook/types": "7.6.5",
- "@storybook/vue3": "7.6.5",
- "@storybook/vue3-vite": "7.6.5",
- "@testing-library/vue": "8.0.1",
+ "@misskey-dev/eslint-plugin": "1.0.0",
+ "@misskey-dev/summaly": "5.0.3",
+ "@storybook/addon-actions": "8.0.0-beta.2",
+ "@storybook/addon-essentials": "8.0.0-beta.2",
+ "@storybook/addon-interactions": "8.0.0-beta.2",
+ "@storybook/addon-links": "8.0.0-beta.2",
+ "@storybook/addon-mdx-gfm": "8.0.0-beta.2",
+ "@storybook/addon-storysource": "8.0.0-beta.2",
+ "@storybook/blocks": "8.0.0-beta.2",
+ "@storybook/components": "8.0.0-beta.2",
+ "@storybook/core-events": "8.0.0-beta.2",
+ "@storybook/manager-api": "8.0.0-beta.2",
+ "@storybook/preview-api": "8.0.0-beta.2",
+ "@storybook/react": "8.0.0-beta.2",
+ "@storybook/react-vite": "8.0.0-beta.2",
+ "@storybook/test": "8.0.0-beta.2",
+ "@storybook/theming": "8.0.0-beta.2",
+ "@storybook/types": "8.0.0-beta.2",
+ "@storybook/vue3": "8.0.0-beta.2",
+ "@storybook/vue3-vite": "8.0.0-beta.2",
+ "@testing-library/vue": "8.0.2",
"@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5",
- "@types/matter-js": "0.19.5",
+ "@types/matter-js": "0.19.6",
"@types/micromatch": "4.0.6",
- "@types/node": "20.10.5",
+ "@types/node": "20.11.17",
"@types/punycode": "2.1.3",
"@types/sanitize-html": "2.9.5",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
- "@types/uuid": "9.0.7",
+ "@types/uuid": "9.0.8",
"@types/ws": "8.5.10",
- "@typescript-eslint/eslint-plugin": "6.14.0",
- "@typescript-eslint/parser": "6.14.0",
+ "@typescript-eslint/eslint-plugin": "6.18.1",
+ "@typescript-eslint/parser": "6.18.1",
"@vitest/coverage-v8": "0.34.6",
- "@vue/runtime-core": "3.3.12",
- "acorn": "8.11.2",
+ "@vue/runtime-core": "3.4.18",
+ "acorn": "8.11.3",
"cross-env": "7.0.3",
- "cypress": "13.6.1",
+ "cypress": "13.6.4",
"eslint": "8.56.0",
"eslint-plugin-import": "2.29.1",
- "eslint-plugin-vue": "9.19.2",
+ "eslint-plugin-vue": "9.20.1",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.5",
- "msw": "1.3.2",
- "msw-storybook-addon": "1.10.0",
- "nodemon": "3.0.2",
- "prettier": "3.1.1",
+ "msw": "2.1.7",
+ "msw-storybook-addon": "2.0.0-beta.1",
+ "nodemon": "3.0.3",
+ "prettier": "3.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"start-server-and-test": "2.0.3",
- "storybook": "7.6.5",
+ "storybook": "8.0.0-beta.2",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
- "summaly": "github:misskey-dev/summaly",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.6",
"vitest-fetch-mock": "0.2.2",
- "vue-eslint-parser": "9.3.2",
- "vue-tsc": "1.8.25"
+ "vue-component-type-helpers": "1.8.27",
+ "vue-eslint-parser": "9.4.2",
+ "vue-tsc": "1.8.27"
}
}
diff --git a/packages/frontend/public/mockServiceWorker.js b/packages/frontend/public/mockServiceWorker.js
index 5384ce6b94..3bb1e66910 100644
--- a/packages/frontend/public/mockServiceWorker.js
+++ b/packages/frontend/public/mockServiceWorker.js
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts
index efb78fe447..875353f8a4 100644
--- a/packages/frontend/src/_boot_.ts
+++ b/packages/frontend/src/_boot_.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts
index d01a957048..eceec76c51 100644
--- a/packages/frontend/src/_dev_boot_.ts
+++ b/packages/frontend/src/_dev_boot_.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index f23fb804c5..e606fe368c 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -11,7 +11,8 @@ import { miLocalStorage } from '@/local-storage.js';
import { MenuButton } from '@/types/menu.js';
import { del, get, set } from '@/scripts/idb-proxy.js';
import { apiUrl } from '@/config.js';
-import { waiting, api, popup, popupMenu, success, alert } from '@/os.js';
+import { waiting, popup, popupMenu, success, alert } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
// TODO: 他のタブと永続化されたstateを同期
@@ -23,9 +24,14 @@ const accountData = miLocalStorage.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 iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
export const iAmAdmin = $i != null && $i.isAdmin;
+export function signinRequired() {
+ if ($i == null) throw new Error('signin required');
+ return $i;
+}
+
export let notesCount = $i == null ? 0 : $i.notesCount;
export function incNotesCount() {
notesCount++;
@@ -246,7 +252,7 @@ export async function openAccountMenu(opts: {
}
const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
- const accountsPromise = api('users/show', { userIds: storedAccounts.map(x => x.id) });
+ const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) });
function createItem(account: Misskey.entities.UserDetailed) {
return {
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index ef69eff764..681beaf00f 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -22,6 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
+import { setupRouter } from '@/router/definition.js';
export async function common(createVue: () => App<Element>) {
console.info(`Misskey v${version}`);
@@ -59,12 +60,6 @@ export async function common(createVue: () => App<Element>) {
});
}
- const splash = document.getElementById('splash');
- // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
- if (splash) splash.addEventListener('transitionend', () => {
- splash.remove();
- });
-
let isClientUpdated = false;
//#region クライアントが更新されたかチェック
@@ -241,6 +236,8 @@ export async function common(createVue: () => App<Element>) {
const app = createVue();
+ setupRouter(app);
+
if (_DEV_) {
app.config.performance = true;
}
@@ -286,5 +283,10 @@ function removeSplash() {
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
+
+ // transitionendイベントが発火しない場合があるため
+ window.setTimeout(() => {
+ splash.remove();
+ }, 1000);
}
}
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 0159d0c032..b19d45a35e 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -1,25 +1,26 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { createApp, markRaw, defineAsyncComponent } from 'vue';
+import { createApp, defineAsyncComponent, markRaw } from 'vue';
import { common } from './common.js';
import { ui } from '@/config.js';
import { i18n } from '@/i18n.js';
-import { confirm, alert, post, popup, toast } from '@/os.js';
+import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
-import { $i, updateAccount, signout } from '@/account.js';
-import { defaultStore, ColdDeviceStorage } from '@/store.js';
+import { $i, signout, updateAccount } from '@/account.js';
+import { fetchInstance, instance } from '@/instance.js';
+import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
-import { mainRouter } from '@/router.js';
import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
+import { mainRouter } from '@/router/main.js';
export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp(
@@ -77,9 +78,23 @@ export async function mainBoot() {
if (defaultStore.state.enableSeasonalScreenEffect) {
const month = new Date().getMonth() + 1;
- if (month === 12 || month === 1) {
- const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
- new SnowfallEffect().render();
+ if (defaultStore.state.hemisphere === 'S') {
+ // ▼南半球
+ if (month === 7 || month === 8) {
+ const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+ new SnowfallEffect({}).render();
+ }
+ } else {
+ // ▼北半球
+ if (month === 12 || month === 1) {
+ const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+ new SnowfallEffect({}).render();
+ } else if (month === 3 || month === 4) {
+ const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+ new SakuraEffect({
+ sakura: true,
+ }).render();
+ }
}
}
@@ -205,7 +220,7 @@ export async function mainBoot() {
const lastUsedDate = parseInt(lastUsed, 10);
// 二時間以上前なら
if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) {
- toast(i18n.t('welcomeBackWithName', {
+ toast(i18n.tsx.welcomeBackWithName({
name: $i.name || $i.username,
}));
}
@@ -220,6 +235,13 @@ export async function mainBoot() {
}
}
+ fetchInstance().then(() => {
+ const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
+ if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
+ popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
+ }
+ });
+
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト
if (Notification.permission === 'default') {
@@ -271,7 +293,7 @@ export async function mainBoot() {
main.on('unreadAntenna', () => {
updateAccount({ hasUnreadAntenna: true });
- sound.play('antenna');
+ sound.playMisskeySfx('antenna');
});
main.on('readAllAnnouncements', () => {
diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts
index 92ee074afb..017457822b 100644
--- a/packages/frontend/src/boot/sub-boot.ts
+++ b/packages/frontend/src/boot/sub-boot.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts
index 25d2b3c15f..b286528de6 100644
--- a/packages/frontend/src/cache.ts
+++ b/packages/frontend/src/cache.ts
@@ -1,13 +1,13 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
import { Cache } from '@/scripts/cache.js';
-import { api } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
-export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => api('clips/list'));
-export const rolesCache = new Cache(1000 * 60 * 30, () => api('admin/roles/list'));
-export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => api('users/lists/list'));
-export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => api('antennas/list'));
+export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
+export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
+export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list'));
+export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list'));
diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/packages/frontend/src/components/MkAbuseReport.stories.impl.ts
index 77e7c84d5c..cf09c96fd4 100644
--- a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts
+++ b/packages/frontend/src/components/MkAbuseReport.stories.impl.ts
@@ -1,12 +1,12 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue';
@@ -44,9 +44,9 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/admin/resolve-abuse-user-report', async (req, res, ctx) => {
- action('POST /api/admin/resolve-abuse-user-report')(await req.json());
- return res(ctx.json({}));
+ http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => {
+ action('POST /api/admin/resolve-abuse-user-report')(await request.json());
+ return HttpResponse.json({});
}),
],
},
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index ce7e134b70..271b94feaa 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -1,12 +1,12 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="bcekxzvu _margin _panel">
<div class="target">
- <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`">
+ <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'">
<MkAvatar class="avatar" :user="report.targetUser" indicator/>
<div class="names">
<MkUserName class="name" :user="report.targetUser"/>
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="report.comment"/>
</div>
<hr/>
- <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link">@{{ report.reporter.username }}</MkA></div>
+ <div>{{ i18n.ts.reporter }}: <MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></div>
<div v-if="report.assignee">
{{ i18n.ts.moderator }}:
<MkAcct :user="report.assignee"/>
diff --git a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts
index dc842b3d1b..9df957f3ec 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts
+++ b/packages/frontend/src/components/MkAbuseReportWindow.stories.impl.ts
@@ -1,12 +1,12 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
@@ -44,9 +44,9 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users/report-abuse', async (req, res, ctx) => {
- action('POST /api/users/report-abuse')(await req.json());
- return res(ctx.json({}));
+ http.post('/api/users/report-abuse', async ({ request }) => {
+ action('POST /api/users/report-abuse')(await request.json());
+ return HttpResponse.json({});
}),
],
},
diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index 7814681ea2..b09c7bb3fb 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -39,7 +39,7 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
- user: Misskey.entities.User;
+ user: Misskey.entities.UserDetailed;
initialComment?: string;
}>();
diff --git a/packages/frontend/src/components/MkAccountMoved.stories.impl.ts b/packages/frontend/src/components/MkAccountMoved.stories.impl.ts
index 33c6c24631..f1cfdc157a 100644
--- a/packages/frontend/src/components/MkAccountMoved.stories.impl.ts
+++ b/packages/frontend/src/components/MkAccountMoved.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index 155d9fe3a9..6c0774b634 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js';
import MkMention from './MkMention.vue';
import { i18n } from '@/i18n.js';
import { host as localHost } from '@/config.js';
-import { api } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
const user = ref<Misskey.entities.UserLite>();
@@ -25,7 +25,7 @@ const props = defineProps<{
movedTo: string; // user id
}>();
-api('users/show', { userId: props.movedTo }).then(u => user.value = u);
+misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkAchievements.stories.impl.ts b/packages/frontend/src/components/MkAchievements.stories.impl.ts
index 6d972467b1..7614da51da 100644
--- a/packages/frontend/src/components/MkAchievements.stories.impl.ts
+++ b/packages/frontend/src/components/MkAchievements.stories.impl.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAchievements from './MkAchievements.vue';
@@ -39,8 +39,8 @@ export const Empty = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users/achievements', (req, res, ctx) => {
- return res(ctx.json([]));
+ http.post('/api/users/achievements', () => {
+ return HttpResponse.json([]);
}),
],
},
@@ -52,8 +52,8 @@ export const All = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users/achievements', (req, res, ctx) => {
- return res(ctx.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 }))));
+ http.post('/api/users/achievements', () => {
+ return HttpResponse.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 })));
}),
],
},
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index d49eeb0329..5d103fa789 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -1,12 +1,12 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<div v-if="achievements" :class="$style.root">
- <div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
+ <div v-for="achievement in achievements" :key="achievement.name" :class="$style.achievement" class="_panel">
<div :class="$style.icon">
<div
:class="[$style.iconFrame, {
@@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'misskey-js';
import { onMounted, ref, computed } from 'vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
@@ -71,7 +72,7 @@ const achievements = ref<Misskey.entities.UsersAchievementsResponse | null>(null
const lockedAchievements = computed(() => ACHIEVEMENT_TYPES.filter(x => !(achievements.value ?? []).some(a => a.name === x)));
function fetch() {
- os.api('users/achievements', { userId: props.user.id }).then(res => {
+ misskeyApi('users/achievements', { userId: props.user.id }).then(res => {
achievements.value = [];
for (const t of ACHIEVEMENT_TYPES) {
const a = res.find(x => x.name === t);
diff --git a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts
index f87ad30f9b..270ca40825 100644
--- a/packages/frontend/src/components/MkAnalogClock.stories.impl.ts
+++ b/packages/frontend/src/components/MkAnalogClock.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index 0e252f7b1d..835efbd6cd 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue
index 284ee8f3f8..4bf6125af5 100644
--- a/packages/frontend/src/components/MkAnimBg.vue
+++ b/packages/frontend/src/components/MkAnimBg.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts b/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts
index 42cfb90f7c..ffa4e56f5f 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts
+++ b/packages/frontend/src/components/MkAnnouncementDialog.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index aaac3dd29b..f27694658e 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
@@ -43,20 +44,20 @@ async function ok() {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts._announcement.readConfirmTitle,
- text: i18n.t('_announcement.readConfirmText', { title: props.announcement.title }),
+ text: i18n.tsx._announcement.readConfirmText({ title: props.announcement.title }),
});
if (confirm.canceled) return;
}
- modal.value.close();
- os.api('i/read-announcement', { announcementId: props.announcement.id });
+ modal.value?.close();
+ misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
updateAccount({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
});
}
function onBgClick() {
- rootEl.value.animate([{
+ rootEl.value?.animate([{
offset: 0,
transform: 'scale(1)',
}, {
diff --git a/packages/frontend/src/components/MkAsUi.stories.impl.ts b/packages/frontend/src/components/MkAsUi.stories.impl.ts
index 564fa902ba..cf8d5483b9 100644
--- a/packages/frontend/src/components/MkAsUi.stories.impl.ts
+++ b/packages/frontend/src/components/MkAsUi.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 0ff5bd7036..5eb77740be 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
</template>
</div>
- <span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span>
- <Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }" :text="c.text" @clickEv="c.onClickEv"/>
+ <span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : undefined, fontWeight: c.bold ? 'bold' : undefined, color: c.color }">{{ c.text }}</span>
+ <Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }" :text="c.text ?? ''" @clickEv="c.onClickEv"/>
<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :disabled="c.disabled" :small="size === 'small'" inline @click="c.onClick">{{ c.text }}</MkButton>
<div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }">
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
@@ -20,19 +20,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkSwitch>
- <MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default" @update:modelValue="c.onInput">
+ <MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default ?? null" @update:modelValue="c.onInput">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkTextarea>
- <MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onInput">
+ <MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onInput">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
- <MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default" type="number" @update:modelValue="c.onInput">
+ <MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default ?? null" type="number" @update:modelValue="c.onInput">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput>
- <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onChange">
+ <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onChange">
<template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template>
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
@@ -42,8 +42,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPostForm
fixed
:instant="true"
- :initialText="c.form.text"
- :initialCw="c.form.cw"
+ :initialText="c.form?.text"
+ :initialCw="c.form?.cw"
/>
</div>
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
</template>
</MkFolder>
- <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align ?? null, backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
+ <div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
<template v-for="child in c.children" :key="child">
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
</template>
@@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSelect from '@/components/MkSelect.vue';
-import { AsUiComponent } from '@/scripts/aiscript/ui.js';
+import { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js';
import MkFolder from '@/components/MkFolder.vue';
import MkPostForm from '@/components/MkPostForm.vue';
@@ -85,20 +85,32 @@ const props = withDefaults(defineProps<{
const c = props.component;
function g(id) {
- return props.components.find(x => x.value.id === id).value;
+ const v = props.components.find(x => x.value.id === id)?.value;
+ if (v) return v;
+
+ return {
+ id: 'dummy',
+ type: 'root',
+ children: [],
+ } as AsUiRoot;
}
-const valueForSwitch = ref(c.default ?? false);
+const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
function onSwitchUpdate(v) {
valueForSwitch.value = v;
- if (c.onChange) c.onChange(v);
+ if ('onChange' in c && c.onChange) {
+ c.onChange(v as never);
+ }
}
function openPostForm() {
+ const form = (c as AsUiPostFormButton).form;
+ if (!form) return;
+
os.post({
- initialText: c.form.text,
- initialCw: c.form.cw,
+ initialText: form.text,
+ initialCw: form.cw,
instant: true,
});
}
diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
index 969519386f..ec24b8c240 100644
--- a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
+++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
@@ -1,14 +1,13 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
-import { expect } from '@storybook/jest';
-import { userEvent, waitFor, within } from '@storybook/testing-library';
+import { expect, userEvent, waitFor, within } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAutocomplete from './MkAutocomplete.vue';
@@ -99,11 +98,11 @@ export const User = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => {
- return res(ctx.json([
+ http.post('/api/users/search-by-username-and-host', () => {
+ return HttpResponse.json([
userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'),
userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'),
- ]));
+ ]);
}),
],
},
@@ -132,12 +131,12 @@ export const Hashtag = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/hashtags/search', (req, res, ctx) => {
- return res(ctx.json([
+ http.post('/api/hashtags/search', () => {
+ return HttpResponse.json([
'気象警報注意報',
'気象警報',
'気象情報',
- ]));
+ ]);
}),
],
},
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 494d120a93..412325bfee 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -35,6 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<span>{{ tag }}</span>
</li>
</ol>
+ <ol v-else-if="mfmParams.length > 0" ref="suggests" :class="$style.list">
+ <li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
+ <span>{{ param }}</span>
+ </li>
+ </ol>
</div>
</template>
@@ -45,12 +50,13 @@ import contains from '@/scripts/contains.js';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
import { acct } from '@/filters/user.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
-import { MFM_TAGS } from '@/const.js';
+import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
type EmojiDef = {
emoji: string;
@@ -129,7 +135,7 @@ export default {
<script lang="ts" setup>
const props = defineProps<{
type: string;
- q: string | null;
+ q: any;
textarea: HTMLTextAreaElement;
close: () => void;
x: number;
@@ -150,6 +156,7 @@ const hashtags = ref<any[]>([]);
const emojis = ref<(EmojiDef)[]>([]);
const items = ref<Element[] | HTMLCollection>([]);
const mfmTags = ref<string[]>([]);
+const mfmParams = ref<string[]>([]);
const select = ref(-1);
const zIndex = os.claimZIndex('high');
@@ -201,7 +208,7 @@ function exec() {
users.value = JSON.parse(cache);
fetching.value = false;
} else {
- os.api('users/search-by-username-and-host', {
+ misskeyApi('users/search-by-username-and-host', {
username: props.q,
limit: 10,
detail: false,
@@ -224,7 +231,7 @@ function exec() {
hashtags.value = hashtags;
fetching.value = false;
} else {
- os.api('hashtags/search', {
+ misskeyApi('hashtags/search', {
query: props.q,
limit: 30,
}).then(searchedHashtags => {
@@ -250,6 +257,13 @@ function exec() {
}
mfmTags.value = MFM_TAGS.filter(tag => tag.startsWith(props.q ?? ''));
+ } else if (props.type === 'mfmParam') {
+ if (props.q.params.at(-1) === '') {
+ mfmParams.value = MFM_PARAMS[props.q.tag] ?? [];
+ return;
+ }
+
+ mfmParams.value = MFM_PARAMS[props.q.tag].filter(param => param.startsWith(props.q.params.at(-1) ?? ''));
}
}
@@ -261,15 +275,24 @@ function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30):
}
const matched = new Map<string, EmojiScore>();
-
- // 前方一致(エイリアスなし)
+ // 完全一致(エイリアス込み)
emojiDb.some(x => {
- if (x.name.startsWith(query) && !x.aliasOf) {
- matched.set(x.name, { emoji: x, score: query.length + 1 });
+ if (x.name === query && !matched.has(x.aliasOf ?? x.name)) {
+ matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
}
return matched.size === max;
});
+ // 前方一致(エイリアスなし)
+ if (matched.size < max) {
+ emojiDb.some(x => {
+ if (x.name.startsWith(query) && !x.aliasOf) {
+ matched.set(x.name, { emoji: x, score: query.length + 1 });
+ }
+ return matched.size === max;
+ });
+ }
+
// 前方一致(エイリアス込み)
if (matched.size < max) {
emojiDb.some(x => {
@@ -408,7 +431,7 @@ function applySelect() {
function chooseUser() {
props.close();
- os.selectUser().then(user => {
+ os.selectUser({ includeSelf: true }).then(user => {
complete('user', user);
props.textarea.focus();
});
diff --git a/packages/frontend/src/components/MkAvatars.stories.impl.ts b/packages/frontend/src/components/MkAvatars.stories.impl.ts
index d41b64695f..d2a4a9f03b 100644
--- a/packages/frontend/src/components/MkAvatars.stories.impl.ts
+++ b/packages/frontend/src/components/MkAvatars.stories.impl.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAvatars from './MkAvatars.vue';
@@ -38,12 +38,12 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users/show', (req, res, ctx) => {
- return res(ctx.json([
+ http.post('/api/users/show', () => {
+ return HttpResponse.json([
userDetailed('17'),
userDetailed('20'),
userDetailed('18'),
- ]));
+ ]);
}),
],
},
diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue
index 5644a324cf..8236d0ddb9 100644
--- a/packages/frontend/src/components/MkAvatars.vue
+++ b/packages/frontend/src/components/MkAvatars.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
const props = withDefaults(defineProps<{
userIds: string[];
@@ -27,7 +27,7 @@ const props = withDefaults(defineProps<{
const users = ref<Misskey.entities.UserLite[]>([]);
onMounted(async () => {
- users.value = await os.api('users/show', {
+ users.value = await misskeyApi('users/show', {
userIds: props.userIds,
}) as unknown as Misskey.entities.UserLite[];
});
diff --git a/packages/frontend/src/components/MkButton.stories.impl.ts b/packages/frontend/src/components/MkButton.stories.impl.ts
index e852557b12..e8802e4f8f 100644
--- a/packages/frontend/src/components/MkButton.stories.impl.ts
+++ b/packages/frontend/src/components/MkButton.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 8b176eedaa..817f1aadf3 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA
v-else class="_button"
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
- :to="to"
+ :to="to ?? '#'"
@mousedown="onMousedown"
>
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
@@ -131,6 +131,10 @@ function onMousedown(evt: MouseEvent): void {
box-sizing: border-box;
transition: background 0.1s ease;
+ &:hover {
+ text-decoration: none;
+ }
+
&:not(:disabled):hover {
background: var(--buttonHoverBg);
}
diff --git a/packages/frontend/src/components/MkCaptcha.stories.impl.ts b/packages/frontend/src/components/MkCaptcha.stories.impl.ts
index fb50e50b18..475257cc45 100644
--- a/packages/frontend/src/components/MkCaptcha.stories.impl.ts
+++ b/packages/frontend/src/components/MkCaptcha.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index 40bca11e64..c64bb47e77 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -1,19 +1,22 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
- <span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span>
- <div ref="captchaEl"></div>
+ <span v-if="!available">Loading<MkEllipsis/></span>
+ <div v-if="props.provider == 'mcaptcha'">
+ <div id="mcaptcha__widget-container" class="m-captcha-style"></div>
+ <div ref="captchaEl"></div>
+ </div>
+ <div v-else ref="captchaEl"></div>
</div>
</template>
<script lang="ts" setup>
-import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch } from 'vue';
+import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
import { defaultStore } from '@/store.js';
-import { i18n } from '@/i18n.js';
// APIs provided by Captcha services
export type Captcha = {
@@ -26,7 +29,7 @@ export type Captcha = {
getResponse(id: string): string;
};
-export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile';
+export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha';
type CaptchaContainer = {
readonly [_ in CaptchaProvider]?: Captcha;
@@ -39,6 +42,7 @@ declare global {
const props = defineProps<{
provider: CaptchaProvider;
sitekey: string | null; // null will show error on request
+ instanceUrl?: string | null;
modelValue?: string | null;
}>();
@@ -55,6 +59,7 @@ const variable = computed(() => {
case 'hcaptcha': return 'hcaptcha';
case 'recaptcha': return 'grecaptcha';
case 'turnstile': return 'turnstile';
+ case 'mcaptcha': return 'mcaptcha';
}
});
@@ -65,6 +70,7 @@ const src = computed(() => {
case 'hcaptcha': return 'https://js.hcaptcha.com/1/api.js?render=explicit&recaptchacompat=off';
case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
+ case 'mcaptcha': return null;
}
});
@@ -72,9 +78,9 @@ const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
-if (loaded) {
+if (loaded || props.provider === 'mcaptcha') {
available.value = true;
-} else {
+} else if (src.value !== null) {
(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
async: true,
id: scriptId.value,
@@ -87,7 +93,7 @@ function reset() {
if (captcha.value.reset) captcha.value.reset();
}
-function requestRender() {
+async function requestRender() {
if (captcha.value.render && captchaEl.value instanceof Element) {
captcha.value.render(captchaEl.value, {
sitekey: props.sitekey,
@@ -96,6 +102,15 @@ function requestRender() {
'expired-callback': callback,
'error-callback': callback,
});
+ } else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
+ const { default: Widget } = await import('@mcaptcha/vanilla-glue');
+ // @ts-expect-error avoid typecheck error
+ new Widget({
+ siteKey: {
+ instanceUrl: new URL(props.instanceUrl),
+ key: props.sitekey,
+ },
+ });
} else {
window.setTimeout(requestRender, 1);
}
@@ -105,14 +120,27 @@ function callback(response?: string) {
emit('update:modelValue', typeof response === 'string' ? response : null);
}
+function onReceivedMessage(message: MessageEvent) {
+ if (message.data.token) {
+ if (props.instanceUrl && new URL(message.origin).host === new URL(props.instanceUrl).host) {
+ callback(message.data.token);
+ }
+ }
+}
+
onMounted(() => {
if (available.value) {
+ window.addEventListener('message', onReceivedMessage);
requestRender();
} else {
watch(available, requestRender);
}
});
+onUnmounted(() => {
+ window.removeEventListener('message', onReceivedMessage);
+});
+
onBeforeUnmount(() => {
reset();
});
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index 41b02a7e3f..6b1b380e41 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
@@ -44,12 +44,12 @@ async function onClick() {
try {
if (isFollowing.value) {
- await os.api('channels/unfollow', {
+ await misskeyApi('channels/unfollow', {
channelId: props.channel.id,
});
isFollowing.value = false;
} else {
- await os.api('channels/follow', {
+ await misskeyApi('channels/follow', {
channelId: props.channel.id,
});
isFollowing.value = true;
diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue
index 83d4401d2e..2850ecca16 100644
--- a/packages/frontend/src/components/MkChannelList.vue
+++ b/packages/frontend/src/components/MkChannelList.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index bf6504d6bf..4ff64dc4ba 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index adb3c134ae..dd745c2140 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -21,13 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only
*/
import { onMounted, ref, shallowRef, watch, PropType } from 'vue';
import { Chart } from 'chart.js';
-import gradient from 'chartjs-plugin-gradient';
-import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { alpha } from '@/scripts/color.js';
import date from '@/filters/date.js';
+import bytes from '@/filters/bytes.js';
import { initChart } from '@/scripts/init-chart.js';
import { chartLegend } from '@/scripts/chart-legend.js';
import MkChartLegend from '@/components/MkChartLegend.vue';
@@ -95,7 +95,7 @@ const getColor = (i) => {
};
const now = new Date();
-let chartInstance: Chart = null;
+let chartInstance: Chart | null = null;
let chartData: {
series: {
name: string;
@@ -108,9 +108,10 @@ let chartData: {
y: number;
}[];
}[];
-} = null;
+ bytes?: boolean;
+} | null = null;
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const fetching = ref(true);
const getDate = (ago: number) => {
@@ -132,6 +133,7 @@ const format = (arr) => {
const { handler: externalTooltipHandler } = useChartTooltip();
const render = () => {
+ if (chartData == null || chartEl.value == null) return;
if (chartInstance) {
chartInstance.destroy();
}
@@ -188,7 +190,6 @@ const render = () => {
stacked: props.stacked,
offset: false,
time: {
- stepSize: 1,
unit: props.span === 'day' ? 'month' : 'day',
displayFormats: {
day: 'M/d',
@@ -198,6 +199,7 @@ const render = () => {
grid: {
},
ticks: {
+ stepSize: 1,
display: props.detailed,
maxRotation: 0,
autoSkipPadding: 16,
@@ -237,6 +239,9 @@ const render = () => {
duration: 0,
},
external: externalTooltipHandler,
+ callbacks: {
+ label: (item) => chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString(),
+ },
},
zoom: props.detailed ? {
pan: {
@@ -265,10 +270,9 @@ const render = () => {
},
},
} : undefined,
- gradient,
},
},
- plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl.value)] : [])],
+ plugins: [chartVLine(vLineColor), ...(props.detailed && legendEl.value ? [chartLegend(legendEl.value)] : [])],
});
};
@@ -277,7 +281,7 @@ const exportData = () => {
};
const fetchFederationChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/federation', { limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/federation', { limit: props.limit, span: props.span });
return {
series: [{
name: 'Received',
@@ -327,7 +331,7 @@ const fetchFederationChart = async (): Promise<typeof chartData> => {
};
const fetchApRequestChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/ap-request', { limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/ap-request', { limit: props.limit, span: props.span });
return {
series: [{
name: 'In',
@@ -349,7 +353,7 @@ const fetchApRequestChart = async (): Promise<typeof chartData> => {
};
const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/notes', { limit: props.limit, span: props.span });
return {
series: [{
name: 'All',
@@ -396,7 +400,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
};
const fetchNotesTotalChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/notes', { limit: props.limit, span: props.span });
return {
series: [{
name: 'Combined',
@@ -415,7 +419,7 @@ const fetchNotesTotalChart = async (): Promise<typeof chartData> => {
};
const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/users', { limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/users', { limit: props.limit, span: props.span });
return {
series: [{
name: 'Combined',
@@ -443,7 +447,7 @@ const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
};
const fetchActiveUsersChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/active-users', { limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/active-users', { limit: props.limit, span: props.span });
return {
series: [{
name: 'Read & Write',
@@ -495,7 +499,7 @@ const fetchActiveUsersChart = async (): Promise<typeof chartData> => {
};
const fetchDriveChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/drive', { limit: props.limit, span: props.span });
return {
bytes: true,
series: [{
@@ -531,7 +535,7 @@ const fetchDriveChart = async (): Promise<typeof chartData> => {
};
const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/drive', { limit: props.limit, span: props.span });
return {
series: [{
name: 'All',
@@ -566,7 +570,7 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
};
const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
return {
series: [{
name: 'In',
@@ -588,7 +592,7 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
};
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Users',
@@ -603,7 +607,7 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
};
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Notes',
@@ -618,7 +622,7 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
};
const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Following',
@@ -641,7 +645,7 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
};
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
return {
bytes: true,
series: [{
@@ -649,7 +653,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
type: 'area',
color: '#008FFB',
data: format(total
- ? raw.drive.totalUsage
+ ? sum(raw.drive.incUsage)
: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
),
}],
@@ -657,7 +661,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
};
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
return {
series: [{
name: 'Drive files',
@@ -672,11 +676,11 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
};
const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/user/notes', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
return {
- series: [...(props.args.withoutAll ? [] : [{
+ series: [...(props.args?.withoutAll ? [] : [{
name: 'All',
- type: 'line',
+ type: 'line' as const,
data: format(sum(raw.inc, negate(raw.dec))),
color: '#888888',
}]), {
@@ -704,7 +708,7 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
};
const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/user/pv', { userId: props.args.user.id, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/user/pv', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
return {
series: [{
name: 'Unique PV (user)',
@@ -731,7 +735,7 @@ const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
};
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
return {
series: [{
name: 'Local',
@@ -746,7 +750,7 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
};
const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
return {
series: [{
name: 'Local',
@@ -761,8 +765,9 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
};
const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
- const raw = await os.apiGet('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
+ const raw = await misskeyApiGet('charts/user/drive', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
return {
+ bytes: true,
series: [{
name: 'Inc',
type: 'area',
@@ -806,6 +811,8 @@ const fetchAndRender = async () => {
case 'per-user-following': return fetchPerUserFollowingChart();
case 'per-user-followers': return fetchPerUserFollowersChart();
case 'per-user-drive': return fetchPerUserDriveChart();
+
+ default: return null;
}
};
fetching.value = true;
diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue
index 1a1b4323d9..6eb2009784 100644
--- a/packages/frontend/src/components/MkChartLegend.vue
+++ b/packages/frontend/src/components/MkChartLegend.vue
@@ -1,12 +1,12 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<button v-for="item in items" class="_button item" :class="{ disabled: item.hidden }" @click="onClick(item)">
- <span class="box" :style="{ background: chart.config.type === 'line' ? item.strokeStyle?.toString() : item.fillStyle?.toString() }"></span>
+ <span class="box" :style="{ background: type === 'line' ? item.strokeStyle?.toString() : item.fillStyle?.toString() }"></span>
{{ item.text }}
</button>
</div>
@@ -16,25 +16,23 @@ SPDX-License-Identifier: AGPL-3.0-only
import { shallowRef } from 'vue';
import { Chart, LegendItem } from 'chart.js';
-const props = defineProps({
-});
-
const chart = shallowRef<Chart>();
+const type = shallowRef<string>();
const items = shallowRef<LegendItem[]>([]);
function update(_chart: Chart, _items: LegendItem[]) {
chart.value = _chart,
items.value = _items;
+ if ('type' in _chart.config) type.value = _chart.config.type;
}
function onClick(item: LegendItem) {
if (chart.value == null) return;
- const { type } = chart.value.config;
- if (type === 'pie' || type === 'doughnut') {
+ if (type.value === 'pie' || type.value === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
- chart.value.toggleDataVisibility(item.index);
+ if (item.index != null) chart.value.toggleDataVisibility(item.index);
} else {
- chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
+ if (item.datasetIndex != null) chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
}
chart.value.update();
}
diff --git a/packages/frontend/src/components/MkChartTooltip.vue b/packages/frontend/src/components/MkChartTooltip.vue
index c11f516e37..51081ede23 100644
--- a/packages/frontend/src/components/MkChartTooltip.vue
+++ b/packages/frontend/src/components/MkChartTooltip.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index f255961e25..23046bf345 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue
index 2f6790fa49..c51ad4356d 100644
--- a/packages/frontend/src/components/MkClipPreview.vue
+++ b/packages/frontend/src/components/MkClipPreview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index 8fca3bb15f..f993e983e4 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -1,18 +1,19 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<!-- eslint-disable vue/no-v-html -->
<template>
-<div :class="['codeBlockRoot', { 'codeEditor': codeEditor }]" v-html="html"></div>
+<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }, (darkMode ? $style.dark : $style.light)]" v-html="html"></div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
-import { BUNDLED_LANGUAGES } from 'shiki';
-import type { Lang as ShikiLang } from 'shiki';
-import { getHighlighter } from '@/scripts/code-highlighter.js';
+import { bundledLanguagesInfo } from 'shiki';
+import type { BuiltinLanguage } from 'shiki';
+import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
+import { defaultStore } from '@/store.js';
const props = defineProps<{
code: string;
@@ -21,25 +22,38 @@ const props = defineProps<{
}>();
const highlighter = await getHighlighter();
+const darkMode = defaultStore.reactiveState.darkMode;
+const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
+
+const [lightThemeName, darkThemeName] = await Promise.all([
+ getTheme('light', true),
+ getTheme('dark', true),
+]);
-const codeLang = ref<ShikiLang | 'aiscript'>('js');
const html = computed(() => highlighter.codeToHtml(props.code, {
lang: codeLang.value,
- theme: 'dark-plus',
+ themes: {
+ fallback: 'dark-plus',
+ light: lightThemeName,
+ dark: darkThemeName,
+ },
+ defaultColor: false,
+ cssVariablePrefix: '--shiki-',
}));
async function fetchLanguage(to: string): Promise<void> {
- const language = to as ShikiLang;
+ const language = to as BuiltinLanguage;
// Check for the loaded languages, and load the language if it's not loaded yet.
if (!highlighter.getLoadedLanguages().includes(language)) {
// Check if the language is supported by Shiki
- const bundles = BUNDLED_LANGUAGES.filter((bundle) => {
+ const bundles = bundledLanguagesInfo.filter((bundle) => {
// Languages are specified by their id, they can also have aliases (i. e. "js" and "javascript")
return bundle.id === language || bundle.aliases?.includes(language);
});
if (bundles.length > 0) {
- await highlighter.loadLanguage(language);
+ console.log(`Loading language: ${language}`);
+ await highlighter.loadLanguage(bundles[0].import);
codeLang.value = language;
} else {
codeLang.value = 'js';
@@ -57,12 +71,22 @@ watch(() => props.lang, (to) => {
}, { immediate: true });
</script>
-<style scoped lang="scss">
-.codeBlockRoot :deep(.shiki) {
+<style module lang="scss">
+.codeBlockRoot :global(.shiki) {
padding: 1em;
margin: .5em 0;
overflow: auto;
border-radius: 8px;
+ border: 1px solid var(--divider);
+ font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
+
+ color: var(--shiki-fallback);
+ background-color: var(--shiki-fallback-bg);
+
+ & span {
+ color: var(--shiki-fallback);
+ background-color: var(--shiki-fallback-bg);
+ }
& pre,
& code {
@@ -70,14 +94,35 @@ watch(() => props.lang, (to) => {
}
}
+.light.codeBlockRoot :global(.shiki) {
+ color: var(--shiki-light);
+ background-color: var(--shiki-light-bg);
+
+ & span {
+ color: var(--shiki-light);
+ background-color: var(--shiki-light-bg);
+ }
+}
+
+.dark.codeBlockRoot :global(.shiki) {
+ color: var(--shiki-dark);
+ background-color: var(--shiki-dark-bg);
+
+ & span {
+ color: var(--shiki-dark);
+ background-color: var(--shiki-dark-bg);
+ }
+}
+
.codeBlockRoot.codeEditor {
min-width: 100%;
height: 100%;
- & :deep(.shiki) {
+ & :global(.shiki) {
padding: 12px;
margin: 0;
border-radius: 6px;
+ border: none;
min-height: 130px;
pointer-events: none;
min-width: calc(100% - 24px);
@@ -89,6 +134,11 @@ watch(() => props.lang, (to) => {
text-rendering: inherit;
text-transform: inherit;
white-space: pre;
+
+ & span {
+ display: inline-block;
+ min-height: 1em;
+ }
}
}
</style>
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index 2c016e4d7c..ede068b20d 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -1,58 +1,72 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<Suspense>
- <template #fallback>
- <MkLoading v-if="!inline ?? true"/>
- </template>
- <code v-if="inline" :class="$style.codeInlineRoot">{{ code }}</code>
- <XCode v-else-if="show && lang" :code="code" :lang="lang"/>
- <pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
- <button v-else :class="$style.codePlaceholderRoot" @click="show = true">
- <div :class="$style.codePlaceholderContainer">
- <div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div>
- <div>{{ i18n.ts.clickToShow }}</div>
- </div>
+<div :class="$style.codeBlockRoot">
+ <button :class="$style.codeBlockCopyButton" class="_button" @click="copy">
+ <i class="ti ti-copy"></i>
</button>
-</Suspense>
+ <Suspense>
+ <template #fallback>
+ <MkLoading />
+ </template>
+ <XCode v-if="show && lang" :code="code" :lang="lang"/>
+ <pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
+ <button v-else :class="$style.codePlaceholderRoot" @click="show = true">
+ <div :class="$style.codePlaceholderContainer">
+ <div><i class="ti ti-code"></i> {{ i18n.ts.code }}</div>
+ <div>{{ i18n.ts.clickToShow }}</div>
+ </div>
+ </button>
+ </Suspense>
+</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue';
+import * as os from '@/os.js';
import MkLoading from '@/components/global/MkLoading.vue';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import copyToClipboard from '@/scripts/copy-to-clipboard.js';
-defineProps<{
+const props = defineProps<{
code: string;
lang?: string;
- inline?: boolean;
}>();
const show = ref(!defaultStore.state.dataSaver.code);
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
+
+function copy() {
+ copyToClipboard(props.code);
+ os.success();
+}
</script>
<style module lang="scss">
-.codeInlineRoot {
- display: inline-block;
- font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
- overflow-wrap: anywhere;
- color: #D4D4D4;
- background: #1E1E1E;
- padding: .1em;
- border-radius: .3em;
+.codeBlockRoot {
+ position: relative;
+}
+
+.codeBlockCopyButton {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ opacity: 0.5;
+
+ &:hover {
+ opacity: 0.8;
+ }
}
.codeBlockFallbackRoot {
display: block;
overflow-wrap: anywhere;
- color: #D4D4D4;
- background: #1E1E1E;
+ background: var(--bg);
padding: 1em;
margin: .5em 0;
overflow: auto;
@@ -77,8 +91,8 @@ const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'))
border-radius: 8px;
padding: 24px;
margin-top: 4px;
- color: #D4D4D4;
- background: #1E1E1E;
+ color: var(--fg);
+ background: var(--bg);
}
.codePlaceholderContainer {
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index 6341c454ae..afd9132a12 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.codeEditorScroller">
<textarea
ref="inputEl"
- v-model="vModel"
+ v-model="v"
:class="[$style.textarea]"
:disabled="disabled"
:required="required"
@@ -58,7 +58,6 @@ const emit = defineEmits<{
}>();
const { modelValue } = toRefs(props);
-const vModel = ref<string>(modelValue.value ?? '');
const v = ref<string>(modelValue.value ?? '');
const focused = ref(false);
const changed = ref(false);
@@ -79,15 +78,14 @@ const onKeydown = (ev: KeyboardEvent) => {
if (ev.code === 'Enter') {
const pos = inputEl.value?.selectionStart ?? 0;
- const posEnd = inputEl.value?.selectionEnd ?? vModel.value.length;
+ const posEnd = inputEl.value?.selectionEnd ?? v.value.length;
if (pos === posEnd) {
- const lines = vModel.value.slice(0, pos).split('\n');
+ const lines = v.value.slice(0, pos).split('\n');
const currentLine = lines[lines.length - 1];
const currentLineSpaces = currentLine.match(/^\s+/);
const posDelta = currentLineSpaces ? currentLineSpaces[0].length : 0;
ev.preventDefault();
- vModel.value = vModel.value.slice(0, pos) + '\n' + (currentLineSpaces ? currentLineSpaces[0] : '') + vModel.value.slice(pos);
- v.value = vModel.value;
+ v.value = v.value.slice(0, pos) + '\n' + (currentLineSpaces ? currentLineSpaces[0] : '') + v.value.slice(pos);
nextTick(() => {
inputEl.value?.setSelectionRange(pos + 1 + posDelta, pos + 1 + posDelta);
});
@@ -97,9 +95,8 @@ const onKeydown = (ev: KeyboardEvent) => {
if (ev.key === 'Tab') {
const pos = inputEl.value?.selectionStart ?? 0;
- const posEnd = inputEl.value?.selectionEnd ?? vModel.value.length;
- vModel.value = vModel.value.slice(0, pos) + '\t' + vModel.value.slice(posEnd);
- v.value = vModel.value;
+ const posEnd = inputEl.value?.selectionEnd ?? v.value.length;
+ v.value = v.value.slice(0, pos) + '\t' + v.value.slice(posEnd);
nextTick(() => {
inputEl.value?.setSelectionRange(pos + 1, pos + 1);
});
@@ -199,10 +196,11 @@ watch(v, newValue => {
resize: none;
text-align: left;
color: transparent;
- caret-color: rgb(225, 228, 232);
+ caret-color: var(--fg);
background-color: transparent;
border: 0;
border-radius: 6px;
+ box-sizing: border-box;
outline: 0;
min-width: calc(100% - 24px);
height: 100%;
@@ -213,6 +211,6 @@ watch(v, newValue => {
}
.textarea::selection {
- color: #fff;
+ color: var(--bg);
}
</style>
diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue
new file mode 100644
index 0000000000..6add80d1bc
--- /dev/null
+++ b/packages/frontend/src/components/MkCodeInline.vue
@@ -0,0 +1,25 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<code :class="$style.root">{{ code }}</code>
+</template>
+
+<script lang="ts" setup>
+const props = defineProps<{
+ code: string;
+}>();
+</script>
+
+<style module lang="scss">
+.root {
+ display: inline-block;
+ font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
+ overflow-wrap: anywhere;
+ background: var(--bg);
+ padding: .1em;
+ border-radius: .3em;
+}
+</style>
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index a7a3eff5af..f5c580789b 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -41,8 +41,8 @@ const { modelValue } = toRefs(props);
const v = ref(modelValue.value);
const inputEl = shallowRef<HTMLElement>();
-const onInput = (ev: KeyboardEvent) => {
- emit('update:modelValue', v.value);
+const onInput = () => {
+ emit('update:modelValue', v.value ?? '');
};
</script>
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 659f3a909e..a399acd47f 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index e29cf472f7..5ca3c77fb2 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -44,8 +44,8 @@ onMounted(() => {
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
- const width = rootEl.value.offsetWidth;
- const height = rootEl.value.offsetHeight;
+ const width = rootEl.value!.offsetWidth;
+ const height = rootEl.value!.offsetHeight;
if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
@@ -63,8 +63,10 @@ onMounted(() => {
left = 0;
}
- rootEl.value.style.top = `${top}px`;
- rootEl.value.style.left = `${left}px`;
+ if (rootEl.value) {
+ rootEl.value.style.top = `${top}px`;
+ rootEl.value.style.left = `${left}px`;
+ }
document.body.addEventListener('mousedown', onMousedown);
});
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 0a1ddd3171..54f6f39c9d 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -63,18 +63,25 @@ const loading = ref(true);
const ok = async () => {
const promise = new Promise<Misskey.entities.DriveFile>(async (res) => {
- const croppedCanvas = await cropper?.getCropperSelection()?.$toCanvas();
+ const croppedImage = await cropper?.getCropperImage();
+ const croppedSection = await cropper?.getCropperSelection();
+
+ // 拡大率を計算し、(ほぼ)元の大きさに戻す
+ const zoomedRate = croppedImage.getBoundingClientRect().width / croppedImage.clientWidth;
+ const widthToRender = croppedSection.getBoundingClientRect().width / zoomedRate;
+
+ const croppedCanvas = await croppedSection?.$toCanvas({ width: widthToRender });
croppedCanvas?.toBlob(blob => {
if (!blob) return;
const formData = new FormData();
formData.append('file', blob);
formData.append('name', `cropped_${props.file.name}`);
formData.append('isSensitive', props.file.isSensitive ? 'true' : 'false');
- formData.append('comment', props.file.comment ?? 'null');
+ if (props.file.comment) { formData.append('comment', props.file.comment);}
formData.append('i', $i!.token);
- if (props.uploadFolder || props.uploadFolder === null) {
- formData.append('folderId', props.uploadFolder ?? 'null');
- } else if (defaultStore.state.uploadFolder) {
+ if (props.uploadFolder) {
+ formData.append('folderId', props.uploadFolder);
+ } else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
formData.append('folderId', defaultStore.state.uploadFolder);
}
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
new file mode 100644
index 0000000000..84b5375a41
--- /dev/null
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -0,0 +1,104 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+ <MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
+ <template #header>:{{ emoji.name }}:</template>
+ <template #default>
+ <MkSpacer>
+ <div style="display: flex; flex-direction: column; gap: 1em;">
+ <div :class="$style.emojiImgWrapper">
+ <MkCustomEmoji :name="emoji.name" :normal="true" :useOriginalSize="true" style="height: 100%;"></MkCustomEmoji>
+ </div>
+ <MkKeyValue :copy="`:${emoji.name}:`">
+ <template #key>{{ i18n.ts.name }}</template>
+ <template #value>{{ emoji.name }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.tags }}</template>
+ <template #value>
+ <div v-if="emoji.aliases.length === 0">{{ i18n.ts.none }}</div>
+ <div v-else :class="$style.aliases">
+ <span v-for="alias in emoji.aliases" :key="alias" :class="$style.alias">
+ {{ alias }}
+ </span>
+ </div>
+ </template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.category }}</template>
+ <template #value>{{ emoji.category ?? i18n.ts.none }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.sensitive }}</template>
+ <template #value>{{ emoji.isSensitive ? i18n.ts.yes : i18n.ts.no }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.localOnly }}</template>
+ <template #value>{{ emoji.localOnly ? i18n.ts.yes : i18n.ts.no }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.license }}</template>
+ <template #value><Mfm :text="emoji.license ?? i18n.ts.none" /></template>
+ </MkKeyValue>
+ <MkKeyValue :copy="emoji.url">
+ <template #key>{{ i18n.ts.emojiUrl }}</template>
+ <template #value>
+ <MkLink :url="emoji.url" target="_blank">{{ emoji.url }}</MkLink>
+ </template>
+ </MkKeyValue>
+ </div>
+ </MkSpacer>
+ </template>
+ </MkModalWindow>
+</template>
+
+<script lang="ts" setup>
+import * as Misskey from 'misskey-js';
+import { defineProps, shallowRef } from 'vue';
+import { i18n } from '@/i18n.js';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkLink from './MkLink.vue';
+const props = defineProps<{
+ emoji: Misskey.entities.EmojiDetailed,
+}>();
+const emit = defineEmits<{
+ (ev: 'ok', cropped: Misskey.entities.DriveFile): void;
+ (ev: 'cancel'): void;
+ (ev: 'closed'): void;
+}>();
+const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
+const cancel = () => {
+ emit('cancel');
+ dialogEl.value!.close();
+};
+</script>
+
+<style lang="scss" module>
+.emojiImgWrapper {
+ max-width: 100%;
+ height: 40cqh;
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--X5) 8px, var(--X5) 14px);
+ border-radius: var(--radius);
+ margin: auto;
+ overflow-y: hidden;
+}
+
+.aliases {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+}
+
+.alias {
+ display: inline-block;
+ word-break: break-all;
+ padding: 3px 10px;
+ background-color: var(--X5);
+ border: solid 1px var(--divider);
+ border-radius: var(--radius);
+}
+</style>
diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue
index 4a6d2dfba2..a2cb3185f4 100644
--- a/packages/frontend/src/components/MkCwButton.vue
+++ b/packages/frontend/src/components/MkCwButton.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed } from 'vue';
import * as Misskey from 'misskey-js';
+import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import { concat } from '@/scripts/array.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@@ -17,22 +18,9 @@ import MkButton from '@/components/MkButton.vue';
const props = defineProps<{
modelValue: boolean;
text: string | null;
- renote: Misskey.entities.Note | null;
- files: Misskey.entities.DriveFile[];
- poll?: {
- expiresAt: string | null;
- multiple: boolean;
- choices: {
- isVoted: boolean;
- text: string;
- votes: number;
- }[];
- } | {
- choices: string[];
- multiple: boolean;
- expiresAt: string | null;
- expiredAfter: string | null;
- };
+ renote?: Misskey.entities.Note | null;
+ files?: Misskey.entities.DriveFile[];
+ poll?: Misskey.entities.Note['poll'] | PollEditorModelValue | null;
}>();
const emit = defineEmits<{
@@ -41,9 +29,9 @@ const emit = defineEmits<{
const label = computed(() => {
return concat([
- props.text ? [i18n.t('_cw.chars', { count: props.text.length })] : [],
+ props.text ? [i18n.tsx._cw.chars({ count: props.text.length })] : [],
props.renote ? [i18n.ts.quote] : [],
- props.files.length !== 0 ? [i18n.t('_cw.files', { count: props.files.length })] : [],
+ props.files && props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [],
props.poll != null ? [i18n.ts.poll] : [],
] as string[][]).join(' / ');
});
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 0a71b689fe..85e131cf9b 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -46,7 +46,7 @@ export default defineComponent({
function getDateText(time: string) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
- return i18n.t('monthAndDay', {
+ return i18n.tsx.monthAndDay({
month: month.toString(),
day: date.toString(),
});
@@ -118,34 +118,36 @@ export default defineComponent({
return children;
};
- function onBeforeLeave(el: HTMLElement) {
+ function onBeforeLeave(element: Element) {
+ const el = element as HTMLElement;
el.style.top = `${el.offsetTop}px`;
el.style.left = `${el.offsetLeft}px`;
}
- function onLeaveCanceled(el: HTMLElement) {
+ function onLeaveCancelled(element: Element) {
+ const el = element as HTMLElement;
el.style.top = '';
el.style.left = '';
}
- return () => h(
- defaultStore.state.animation ? TransitionGroup : 'div',
- {
- class: {
- [$style['date-separated-list']]: true,
- [$style['date-separated-list-nogap']]: props.noGap,
- [$style['reversed']]: props.reversed,
- [$style['direction-down']]: props.direction === 'down',
- [$style['direction-up']]: props.direction === 'up',
- },
- ...(defaultStore.state.animation ? {
- name: 'list',
- tag: 'div',
- onBeforeLeave,
- onLeaveCanceled,
- } : {}),
- },
- { default: renderChildren });
+ // eslint-disable-next-line vue/no-setup-props-destructure
+ const classes = {
+ [$style['date-separated-list']]: true,
+ [$style['date-separated-list-nogap']]: props.noGap,
+ [$style['reversed']]: props.reversed,
+ [$style['direction-down']]: props.direction === 'down',
+ [$style['direction-up']]: props.direction === 'up',
+ };
+
+ return () => defaultStore.state.animation ? h(TransitionGroup, {
+ class: classes,
+ name: 'list',
+ tag: 'div',
+ onBeforeLeave,
+ onLeaveCancelled,
+ }, { default: renderChildren }) : h('div', {
+ class: classes,
+ }, { default: renderChildren });
},
});
</script>
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 3c1f83d335..4b7584faaa 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
<template #caption>
- <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.t('_dialog.charactersExceeded', { current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
- <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.t('_dialog.charactersBelow', { current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
+ <span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/>
+ <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
</template>
</MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus>
@@ -125,7 +125,7 @@ const selectedValue = ref(props.select?.default ?? null);
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
if (props.input) {
if (props.input.minLength) {
- if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) {
+ if (inputValue.value == null || (inputValue.value as string).length < props.input.minLength) {
return 'charactersBelow';
}
}
diff --git a/packages/frontend/src/components/MkDigitalClock.stories.impl.ts b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts
index 5d16c09bc5..e3391bcf7e 100644
--- a/packages/frontend/src/components/MkDigitalClock.stories.impl.ts
+++ b/packages/frontend/src/components/MkDigitalClock.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue
index dff6e7d4dd..2e2321e6ac 100644
--- a/packages/frontend/src/components/MkDigitalClock.vue
+++ b/packages/frontend/src/components/MkDigitalClock.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
index 3a1bab5f98..434fc81582 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index b46b25eba2..4106b0a436 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -45,9 +45,9 @@ import bytes from '@/filters/bytes.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { useRouter } from '@/router.js';
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
import { deviceKind } from '@/scripts/device-kind.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index b0c14d1f0b..8da0d78f35 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -35,6 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { claimAchievement } from '@/scripts/achievements.js';
@@ -144,7 +145,7 @@ function onDrop(ev: DragEvent) {
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
emit('removeFile', file.id);
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
folderId: props.folder.id,
});
@@ -160,7 +161,7 @@ function onDrop(ev: DragEvent) {
if (folder.id === props.folder.id) return;
emit('removeFolder', folder.id);
- os.api('drive/folders/update', {
+ misskeyApi('drive/folders/update', {
folderId: folder.id,
parentId: props.folder.id,
}).then(() => {
@@ -204,7 +205,7 @@ function onDragend() {
}
function go() {
- emit('move', props.folder.id);
+ emit('move', props.folder);
}
function rename() {
@@ -214,7 +215,7 @@ function rename() {
default: props.folder.name,
}).then(({ canceled, result: name }) => {
if (canceled) return;
- os.api('drive/folders/update', {
+ misskeyApi('drive/folders/update', {
folderId: props.folder.id,
name: name,
});
@@ -222,7 +223,7 @@ function rename() {
}
function deleteFolder() {
- os.api('drive/folders/delete', {
+ misskeyApi('drive/folders/delete', {
folderId: props.folder.id,
}).then(() => {
if (defaultStore.state.uploadFolder === props.folder.id) {
diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue
index 59458ad568..8df3c86ebf 100644
--- a/packages/frontend/src/components/MkDrive.navFolder.vue
+++ b/packages/frontend/src/components/MkDrive.navFolder.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -112,7 +112,7 @@ function onDrop(ev: DragEvent) {
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
emit('removeFile', file.id);
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
folderId: props.folder ? props.folder.id : null,
});
@@ -126,7 +126,7 @@ function onDrop(ev: DragEvent) {
// 移動先が自分自身ならreject
if (props.folder && folder.id === props.folder.id) return;
emit('removeFolder', folder.id);
- os.api('drive/folders/update', {
+ misskeyApi('drive/folders/update', {
folderId: folder.id,
parentId: props.folder ? props.folder.id : null,
});
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 8dff73d994..a9717b4fb7 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -82,8 +82,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-show="moreFiles" ref="loadMoreFiles" @click="fetchMoreFiles">{{ i18n.ts.loadMore }}</MkButton>
</div>
<div v-if="files.length == 0 && folders.length == 0 && !fetching" :class="$style.empty">
- <div v-if="draghover">{{ i18n.t('empty-draghover') }}</div>
- <div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.t('empty-drive-description') }}</div>
+ <div v-if="draghover">{{ i18n.ts['empty-draghover'] }}</div>
+ <div v-if="!draghover && folder == null"><strong>{{ i18n.ts.emptyDrive }}</strong><br/>{{ i18n.ts['empty-drive-description'] }}</div>
<div v-if="!draghover && folder != null">{{ i18n.ts.emptyFolder }}</div>
</div>
</div>
@@ -98,10 +98,12 @@ SPDX-License-Identifier: AGPL-3.0-only
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue';
+import type { MenuItem } from '@/types/menu.js';
import XNavFolder from '@/components/MkDrive.navFolder.vue';
import XFolder from '@/components/MkDrive.folder.vue';
import XFile from '@/components/MkDrive.file.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -254,7 +256,7 @@ function onDrop(ev: DragEvent): any {
const file = JSON.parse(driveFile);
if (files.value.some(f => f.id === file.id)) return;
removeFile(file.id);
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
folderId: folder.value ? folder.value.id : null,
});
@@ -270,7 +272,7 @@ function onDrop(ev: DragEvent): any {
if (folder.value && droppedFolder.id === folder.value.id) return false;
if (folders.value.some(f => f.id === droppedFolder.id)) return false;
removeFolder(droppedFolder.id);
- os.api('drive/folders/update', {
+ misskeyApi('drive/folders/update', {
folderId: droppedFolder.id,
parentId: folder.value ? folder.value.id : null,
}).then(() => {
@@ -307,7 +309,7 @@ function urlUpload() {
placeholder: i18n.ts.uploadFromUrlDescription,
}).then(({ canceled, result: url }) => {
if (canceled || !url) return;
- os.api('drive/files/upload-from-url', {
+ misskeyApi('drive/files/upload-from-url', {
url: url,
folderId: folder.value ? folder.value.id : undefined,
});
@@ -325,7 +327,7 @@ function createFolder() {
placeholder: i18n.ts.folderName,
}).then(({ canceled, result: name }) => {
if (canceled) return;
- os.api('drive/folders/create', {
+ misskeyApi('drive/folders/create', {
name: name,
parentId: folder.value ? folder.value.id : undefined,
}).then(createdFolder => {
@@ -341,7 +343,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
default: folderToRename.name,
}).then(({ canceled, result: name }) => {
if (canceled) return;
- os.api('drive/folders/update', {
+ misskeyApi('drive/folders/update', {
folderId: folderToRename.id,
name: name,
}).then(updatedFolder => {
@@ -352,7 +354,7 @@ function renameFolder(folderToRename: Misskey.entities.DriveFolder) {
}
function deleteFolder(folderToDelete: Misskey.entities.DriveFolder) {
- os.api('drive/folders/delete', {
+ misskeyApi('drive/folders/delete', {
folderId: folderToDelete.id,
}).then(() => {
// 削除時に親フォルダに移動
@@ -426,7 +428,7 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
}
}
-function move(target?: Misskey.entities.DriveFolder) {
+function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
if (!target) {
goRoot();
return;
@@ -436,7 +438,7 @@ function move(target?: Misskey.entities.DriveFolder) {
fetching.value = true;
- os.api('drive/folders/show', {
+ misskeyApi('drive/folders/show', {
folderId: target,
}).then(folderToMove => {
folder.value = folderToMove;
@@ -535,7 +537,7 @@ async function fetch() {
const foldersMax = 30;
const filesMax = 30;
- const foldersPromise = os.api('drive/folders', {
+ const foldersPromise = misskeyApi('drive/folders', {
folderId: folder.value ? folder.value.id : null,
limit: foldersMax + 1,
}).then(fetchedFolders => {
@@ -546,7 +548,7 @@ async function fetch() {
return fetchedFolders;
});
- const filesPromise = os.api('drive/files', {
+ const filesPromise = misskeyApi('drive/files', {
folderId: folder.value ? folder.value.id : null,
type: props.type,
limit: filesMax + 1,
@@ -571,7 +573,7 @@ function fetchMoreFolders() {
const max = 30;
- os.api('drive/folders', {
+ misskeyApi('drive/folders', {
folderId: folder.value ? folder.value.id : null,
type: props.type,
untilId: folders.value.at(-1)?.id,
@@ -594,7 +596,7 @@ function fetchMoreFiles() {
const max = 30;
// ファイル一覧取得
- os.api('drive/files', {
+ misskeyApi('drive/files', {
folderId: folder.value ? folder.value.id : null,
type: props.type,
untilId: files.value.at(-1)?.id,
@@ -612,7 +614,7 @@ function fetchMoreFiles() {
}
function getMenu() {
- return [{
+ const menu: MenuItem[] = [{
type: 'switch',
text: i18n.ts.keepOriginalUploading,
ref: keepOriginal,
@@ -633,7 +635,7 @@ function getMenu() {
}, folder.value ? {
text: i18n.ts.renameFolder,
icon: 'ti ti-forms',
- action: () => { renameFolder(folder.value); },
+ action: () => { if (folder.value) renameFolder(folder.value); },
} : undefined, folder.value ? {
text: i18n.ts.deleteFolder,
icon: 'ti ti-trash',
@@ -643,6 +645,8 @@ function getMenu() {
icon: 'ti ti-folder-plus',
action: () => { createFolder(); },
}];
+
+ return menu;
}
function showMenu(ev: MouseEvent) {
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue
index 5b07fed2d1..706c9a55c7 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkDriveSelectDialog.vue b/packages/frontend/src/components/MkDriveSelectDialog.vue
index e65f4dd403..77b5532f79 100644
--- a/packages/frontend/src/components/MkDriveSelectDialog.vue
+++ b/packages/frontend/src/components/MkDriveSelectDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkDriveWindow.vue b/packages/frontend/src/components/MkDriveWindow.vue
index 72aa79b153..c0142ec76e 100644
--- a/packages/frontend/src/components/MkDriveWindow.vue
+++ b/packages/frontend/src/components/MkDriveWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index 14f3f5770f..30ad2bcbbf 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
<header class="_acrylic" @click="shown = !shown">
- <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
+ <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
</header>
<div v-if="shown" style="padding-left: 9px;">
<MkEmojiPickerSection
@@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, Ref } from 'vue';
import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
-import { i18n } from '../i18n.js';
+import { i18n } from '@/i18n.js';
import { customEmojis } from '@/custom-emojis.js';
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
@@ -87,7 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
elm.title = getEmojiName(emoji) ?? emoji;
}
-function nestedChosen(emoji: any, ev?: MouseEvent) {
+function nestedChosen(emoji: any, ev: MouseEvent) {
emit('chosen', emoji, ev);
}
</script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index f36d46506f..366273118b 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</section>
<div v-if="tab === 'index'" class="group index">
- <section v-if="showPinned && pinned.length > 0">
+ <section v-if="showPinned && (pinned && pinned.length > 0)">
<div class="body">
<button
v-for="emoji in pinned"
@@ -118,6 +118,7 @@ import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js';
+import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
const props = withDefaults(defineProps<{
showPinned?: boolean;
@@ -126,6 +127,7 @@ const props = withDefaults(defineProps<{
asDrawer?: boolean;
asWindow?: boolean;
asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
+ targetNote?: Misskey.entities.Note;
}>(), {
showPinned: true,
});
@@ -221,6 +223,19 @@ watch(q, () => {
}
}
} else {
+ if (customEmojisMap.has(newQ)) {
+ matches.add(customEmojisMap.get(newQ)!);
+ }
+ if (matches.size >= max) return matches;
+
+ for (const emoji of emojis) {
+ if (emoji.aliases.some(alias => alias === newQ)) {
+ matches.add(emoji);
+ if (matches.size >= max) break;
+ }
+ }
+ if (matches.size >= max) return matches;
+
for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) {
matches.add(emoji);
@@ -327,7 +342,7 @@ watch(q, () => {
});
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
- return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
+ return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
}
function focus() {
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 6660dcf1ed..59f4b51522 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -24,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:showPinned="showPinned"
:pinnedEmojis="pinnedEmojis"
:asReactionPicker="asReactionPicker"
+ :targetNote="targetNote"
:asDrawer="type === 'drawer'"
:max-height="maxHeight"
@chosen="chosen"
@@ -32,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import * as Misskey from 'misskey-js';
import { shallowRef } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@@ -43,6 +45,7 @@ const props = withDefaults(defineProps<{
showPinned?: boolean;
pinnedEmojis?: string[],
asReactionPicker?: boolean;
+ targetNote?: Misskey.entities.Note;
choseAndClose?: boolean;
}>(), {
manualShowing: null,
diff --git a/packages/frontend/src/components/MkEmojiPickerWindow.vue b/packages/frontend/src/components/MkEmojiPickerWindow.vue
index 1a2c55e785..6952943345 100644
--- a/packages/frontend/src/components/MkEmojiPickerWindow.vue
+++ b/packages/frontend/src/components/MkEmojiPickerWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,12 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
:front="true"
@closed="emit('closed')"
>
- <MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" asWindow :class="$style.picker" @chosen="chosen"/>
+ <MkEmojiPicker :showPinned="showPinned" :asReactionPicker="asReactionPicker" :targetNote="targetNote" asWindow :class="$style.picker" @chosen="chosen"/>
</MkWindow>
</template>
<script lang="ts" setup>
import { } from 'vue';
+import * as Misskey from 'misskey-js';
import MkWindow from '@/components/MkWindow.vue';
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
@@ -26,6 +27,7 @@ withDefaults(defineProps<{
src?: HTMLElement;
showPinned?: boolean;
asReactionPicker?: boolean;
+ targetNote?: Misskey.entities.Note
}>(), {
showPinned: true,
});
diff --git a/packages/frontend/src/components/MkFeaturedPhotos.vue b/packages/frontend/src/components/MkFeaturedPhotos.vue
index 6d1bad7433..8d875790bc 100644
--- a/packages/frontend/src/components/MkFeaturedPhotos.vue
+++ b/packages/frontend/src/components/MkFeaturedPhotos.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -10,11 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
const meta = ref<Misskey.entities.MetaResponse>();
-os.api('meta', { detail: true }).then(gotMeta => {
+misskeyApi('meta', { detail: true }).then(gotMeta => {
meta.value = gotMeta;
});
</script>
diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
index 922089a78b..76bb965101 100644
--- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue
+++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:withOkButton="true"
:okButtonDisabled="false"
@ok="ok()"
- @close="dialog.close()"
+ @close="dialog?.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.describeFile }}</template>
@@ -48,6 +48,6 @@ const caption = ref(props.default);
async function ok() {
emit('done', caption.value);
- dialog.value.close();
+ dialog.value?.close();
}
</script>
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index 3edd30bc37..30822ef655 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
<MkA
- v-for="file in items"
+ v-for="file in (items as Misskey.entities.DriveFile[])"
:key="file.id"
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}\nby ${file.user ? '@' + Misskey.acct.toString(file.user) : 'system'}`"
:to="`/admin/file/${file.id}`"
diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue
index ab435585d9..c5dd877971 100644
--- a/packages/frontend/src/components/MkFlashPreview.vue
+++ b/packages/frontend/src/components/MkFlashPreview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -9,7 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<header>
<h1 :title="flash.title">{{ flash.title }}</h1>
</header>
- <p v-if="flash.summary" :title="flash.summary">{{ flash.summary.length > 85 ? flash.summary.slice(0, 85) + '…' : flash.summary }}</p>
+ <p v-if="flash.summary" :title="flash.summary">
+ <Mfm class="summaryMfm" :text="flash.summary" :plain="true" :nowrap="true"/>
+ </p>
<footer>
<img class="icon" :src="flash.user.avatarUrl"/>
<p>{{ userName(flash.user) }}</p>
@@ -54,6 +56,12 @@ const props = defineProps<{
margin: 0;
color: var(--urlPreviewText);
font-size: 0.8em;
+ overflow: clip;
+
+ > .summaryMfm {
+ display: block;
+ width: 100%;
+ }
}
> footer {
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index 1ffc95d944..f10d58b38a 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -1,10 +1,10 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div ref="el" :class="$style.root">
+<div ref="rootEl" :class="$style.root">
<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
<div :class="$style.title"><div><slot name="header"></slot></div></div>
<div :class="$style.divider"></div>
@@ -14,7 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</header>
<Transition
- :name="defaultStore.state.animation ? 'folder-toggle' : ''"
+ :enterActiveClass="defaultStore.state.animation ? $style.folderToggleEnterActive : ''"
+ :leaveActiveClass="defaultStore.state.animation ? $style.folderToggleLeaveActive : ''"
+ :enterFromClass="defaultStore.state.animation ? $style.folderToggleEnterFrom : ''"
+ :leaveToClass="defaultStore.state.animation ? $style.folderToggleLeaveTo : ''"
@enter="enter"
@afterEnter="afterEnter"
@leave="leave"
@@ -42,8 +45,8 @@ const props = withDefaults(defineProps<{
expanded: true,
});
-const el = shallowRef<HTMLDivElement>();
-const bg = ref<string | null>(null);
+const rootEl = shallowRef<HTMLDivElement>();
+const bg = ref<string>();
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
watch(showBody, () => {
@@ -52,40 +55,44 @@ watch(showBody, () => {
}
});
-function enter(el: Element) {
+function enter(element: Element) {
+ const el = element as HTMLElement;
const elementHeight = el.getBoundingClientRect().height;
- el.style.height = 0;
+ el.style.height = '0';
el.offsetHeight; // reflow
el.style.height = elementHeight + 'px';
}
-function afterEnter(el: Element) {
- el.style.height = null;
+function afterEnter(element: Element) {
+ const el = element as HTMLElement;
+ el.style.height = 'unset';
}
-function leave(el: Element) {
+function leave(element: Element) {
+ const el = element as HTMLElement;
const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px';
el.offsetHeight; // reflow
- el.style.height = 0;
+ el.style.height = '0';
}
-function afterLeave(el: Element) {
- el.style.height = null;
+function afterLeave(element: Element) {
+ const el = element as HTMLElement;
+ el.style.height = 'unset';
}
onMounted(() => {
- function getParentBg(el: HTMLElement | null): string {
+ function getParentBg(el?: HTMLElement | null): string {
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
- const bg = el.style.background || el.style.backgroundColor;
- if (bg) {
- return bg;
+ const background = el.style.background || el.style.backgroundColor;
+ if (background) {
+ return background;
} else {
return getParentBg(el.parentElement);
}
}
- const rawBg = getParentBg(el.value);
+ const rawBg = getParentBg(rootEl.value);
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
_bg.setAlpha(0.85);
bg.value = _bg.toRgbString();
@@ -93,14 +100,12 @@ onMounted(() => {
</script>
<style lang="scss" module>
-.folder-toggle-enter-active, .folder-toggle-leave-active {
+.folderToggleEnterActive, .folderToggleLeaveActive {
overflow-y: clip;
transition: opacity 0.5s, height 0.5s !important;
}
-.folder-toggle-enter-from {
- opacity: 0;
-}
-.folder-toggle-leave-to {
+
+.folderToggleEnterFrom, .folderToggleLeaveTo {
opacity: 0;
}
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 6b7dfb20e3..9b8eb19a11 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
- <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened">
+ <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
<Transition
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
@@ -109,7 +109,7 @@ function toggle() {
onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
- const parentBg = getBgColor(rootEl.value.parentElement);
+ const parentBg = getBgColor(rootEl.value!.parentElement!);
const myBg = computedStyle.getPropertyValue('--panel');
bgSame.value = parentBg === myBg;
});
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index eb5c54de6b..28450e11fc 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -38,11 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onBeforeUnmount, onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { $i } from '@/account.js';
-import { defaultStore } from "@/store.js";
+import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed,
@@ -63,7 +64,7 @@ const wait = ref(false);
const connection = useStream().useChannel('main');
if (props.user.isFollowing == null) {
- os.api('users/show', {
+ misskeyApi('users/show', {
userId: props.user.id,
})
.then(onFollowChange);
@@ -83,22 +84,22 @@ async function onClick() {
if (isFollowing.value) {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('unfollowConfirm', { name: props.user.name || props.user.username }),
+ text: i18n.tsx.unfollowConfirm({ name: props.user.name || props.user.username }),
});
if (canceled) return;
- await os.api('following/delete', {
+ await misskeyApi('following/delete', {
userId: props.user.id,
});
} else {
if (hasPendingFollowRequestFromYou.value) {
- await os.api('following/requests/cancel', {
+ await misskeyApi('following/requests/cancel', {
userId: props.user.id,
});
hasPendingFollowRequestFromYou.value = false;
} else {
- await os.api('following/create', {
+ await misskeyApi('following/create', {
userId: props.user.id,
withReplies: defaultStore.state.defaultWithReplies,
});
diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue
index 9b57688a02..35112ad45d 100644
--- a/packages/frontend/src/components/MkForgotPassword.vue
+++ b/packages/frontend/src/components/MkForgotPassword.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog"
:width="370"
:height="400"
- @close="dialog.close()"
+ @close="dialog?.close()"
@closed="emit('closed')"
>
<template #header>{{ i18n.ts.forgotPassword }}</template>
@@ -66,6 +66,6 @@ async function onSubmit() {
email: email.value,
});
emit('done');
- dialog.value.close();
+ dialog.value?.close();
}
</script>
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 6f882cfab7..0d8734799c 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<MkSpacer :marginMin="20" :marginMax="32">
- <div class="_gaps_m">
+ <div v-if="Object.keys(form).filter(item => !form[item].hidden).length > 0" class="_gaps_m">
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
<MkInput v-if="form[item].type === 'number'" v-model="values[item]" type="number" :step="form[item].step || 1">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
@@ -40,11 +40,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
- <option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
+ <option v-for="option in form[item].enum" :key="option.value" :value="option.value">{{ option.label }}</option>
</MkSelect>
<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
- <option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
+ <option v-for="option in form[item].options" :key="option.value" :value="option.value">{{ option.label }}</option>
</MkRadios>
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
@@ -55,6 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton>
</template>
</div>
+ <div v-else class="_fullinfo">
+ <img :src="infoImageUrl" class="_ghost"/>
+ <div>{{ i18n.ts.nothing }}</div>
+ </div>
</MkSpacer>
</MkModalWindow>
</template>
@@ -70,6 +74,7 @@ import MkButton from './MkButton.vue';
import MkRadios from './MkRadios.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
+import { infoImageUrl } from '@/instance.js';
const props = defineProps<{
title: string;
@@ -81,6 +86,7 @@ const emit = defineEmits<{
canceled?: boolean;
result?: any;
}): void;
+ (ev: 'closed'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
@@ -94,13 +100,13 @@ function ok() {
emit('done', {
result: values,
});
- dialog.value.close();
+ dialog.value?.close();
}
function cancel() {
emit('done', {
canceled: true,
});
- dialog.value.close();
+ dialog.value?.close();
}
</script>
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts
index 035b727a35..a433ad680b 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts
+++ b/packages/frontend/src/components/MkGalleryPostPreview.stories.impl.ts
@@ -1,11 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { expect } from '@storybook/jest';
-import { userEvent, waitFor, within } from '@storybook/testing-library';
+import { expect, userEvent, waitFor, within } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import { galleryPost } from '../../.storybook/fakes.js';
import MkGalleryPostPreview from './MkGalleryPostPreview.vue';
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 316632b1a6..47cccd9b7c 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
leaveActiveClass: $style.transition_toggle_leaveActive,
leaveToClass: $style.transition_toggle_leaveTo,
}"
- :src="post.files[0].thumbnailUrl"
- :hash="post.files[0].blurhash"
+ :src="post.files?.[0]?.thumbnailUrl"
+ :hash="post.files?.[0]?.blurhash"
:forceBlurhash="!show"
/>
</Transition>
diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue
index fb142b31b5..2988d77fe3 100644
--- a/packages/frontend/src/components/MkGoogle.vue
+++ b/packages/frontend/src/components/MkGoogle.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index a57e6c9292..0cc0df9911 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -15,7 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
-import * as os from '@/os.js';
+import * as Misskey from 'misskey-js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { alpha } from '@/scripts/color.js';
@@ -23,14 +24,21 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
-const props = defineProps<{
- src: string;
-}>();
+export type HeatmapSource = 'active-users' | 'notes' | 'ap-requests-inbox-received' | 'ap-requests-deliver-succeeded' | 'ap-requests-deliver-failed';
-const rootEl = shallowRef<HTMLDivElement>(null);
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const props = withDefaults(defineProps<{
+ src: HeatmapSource;
+ user?: Misskey.entities.User;
+ label?: string;
+}>(), {
+ user: undefined,
+ label: '',
+});
+
+const rootEl = shallowRef<HTMLDivElement | null>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const now = new Date();
-let chartInstance: Chart = null;
+let chartInstance: Chart | null = null;
const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
@@ -38,6 +46,7 @@ const { handler: externalTooltipHandler } = useChartTooltip({
});
async function renderChart() {
+ if (rootEl.value == null) return;
if (chartInstance) {
chartInstance.destroy();
}
@@ -56,7 +65,7 @@ async function renderChart() {
return new Date(y, m, d - ago);
};
- const format = (arr) => {
+ const format = (arr: number[]) => {
return arr.map((v, i) => {
const dt = getDate(i);
const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
@@ -69,22 +78,27 @@ async function renderChart() {
});
};
- let values;
+ let values: number[] = [];
if (props.src === 'active-users') {
- const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
values = raw.readWrite;
} else if (props.src === 'notes') {
- const raw = await os.api('charts/notes', { limit: chartLimit, span: 'day' });
- values = raw.local.inc;
+ if (props.user) {
+ const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
+ values = raw.inc;
+ } else {
+ const raw = await misskeyApi('charts/notes', { limit: chartLimit, span: 'day' });
+ values = raw.local.inc;
+ }
} else if (props.src === 'ap-requests-inbox-received') {
- const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
values = raw.inboxReceived;
} else if (props.src === 'ap-requests-deliver-succeeded') {
- const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
values = raw.deliverSucceeded;
} else if (props.src === 'ap-requests-deliver-failed') {
- const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
values = raw.deliverFailed;
}
@@ -101,25 +115,25 @@ async function renderChart() {
const marginEachCell = 4;
+ if (chartEl.value == null) return;
+
chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
- label: 'Read & Write',
- data: format(values),
- pointRadius: 0,
+ label: props.label,
+ data: format(values) as any,
borderWidth: 0,
- borderJoinStyle: 'round',
borderRadius: 3,
backgroundColor(c) {
- const value = c.dataset.data[c.dataIndex].v;
+ // @ts-expect-error TS(2339)
+ const value = c.dataset.data[c.dataIndex].v as number;
let a = (value - min) / max;
if (value !== 0) { // 0でない限りは完全に不可視にはしない
a = Math.max(a, 0.05);
}
return alpha(color, a);
},
- fill: true,
width(c) {
const a = c.chart.chartArea ?? {};
return (a.right - a.left) / weeks - marginEachCell;
@@ -128,6 +142,9 @@ async function renderChart() {
const a = c.chart.chartArea ?? {};
return (a.bottom - a.top) / 7 - marginEachCell;
},
+ /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
+ }] satisfies ChartData[],
+ */
}],
},
options: {
@@ -190,12 +207,14 @@ async function renderChart() {
enabled: false,
callbacks: {
title(context) {
- const v = context[0].dataset.data[context[0].dataIndex];
- return v.d;
+ // @ts-expect-error TS(2339)
+ return context[0].dataset.data[context[0].dataIndex].d;
},
label(context) {
const v = context.dataset.data[context.dataIndex];
- return ['Active: ' + v.v];
+
+ // @ts-expect-error TS(2339)
+ return [v.v];
},
},
//mode: 'index',
diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue
new file mode 100644
index 0000000000..196c962a06
--- /dev/null
+++ b/packages/frontend/src/components/MkHorizontalSwipe.vue
@@ -0,0 +1,239 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ ref="rootEl"
+ :class="[$style.transitionRoot, { [$style.enableAnimation]: shouldAnimate }]"
+ @touchstart.passive="touchStart"
+ @touchmove.passive="touchMove"
+ @touchend.passive="touchEnd"
+>
+ <Transition
+ :class="[$style.transitionChildren, { [$style.swiping]: isSwipingForClass }]"
+ :enterActiveClass="$style.swipeAnimation_enterActive"
+ :leaveActiveClass="$style.swipeAnimation_leaveActive"
+ :enterFromClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_enterFrom : $style.swipeAnimationRight_enterFrom"
+ :leaveToClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_leaveTo : $style.swipeAnimationRight_leaveTo"
+ :style="`--swipe: ${pullDistance}px;`"
+ >
+ <!-- 【注意】slot内の最上位要素に動的にkeyを設定すること -->
+ <!-- 各最上位要素にユニークなkeyの指定がないとTransitionがうまく動きません -->
+ <slot></slot>
+ </Transition>
+</div>
+</template>
+<script lang="ts" setup>
+import { ref, shallowRef, computed, nextTick, watch } from 'vue';
+import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
+import { defaultStore } from '@/store.js';
+import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
+
+const rootEl = shallowRef<HTMLDivElement>();
+
+// eslint-disable-next-line no-undef
+const tabModel = defineModel<string>('tab');
+
+const props = defineProps<{
+ tabs: Tab[];
+}>();
+
+const emit = defineEmits<{
+ (ev: 'swiped', newKey: string, direction: 'left' | 'right'): void;
+}>();
+
+const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value);
+
+// ▼ しきい値 ▼ //
+
+// スワイプと判定される最小の距離
+const MIN_SWIPE_DISTANCE = 20;
+
+// スワイプ時の動作を発火する最小の距離
+const SWIPE_DISTANCE_THRESHOLD = 70;
+
+// スワイプを中断するY方向の移動距離
+const SWIPE_ABORT_Y_THRESHOLD = 75;
+
+// スワイプできる最大の距離
+const MAX_SWIPE_DISTANCE = 120;
+
+// ▲ しきい値 ▲ //
+
+let startScreenX: number | null = null;
+let startScreenY: number | null = null;
+
+const currentTabIndex = computed(() => props.tabs.findIndex(tab => tab.key === tabModel.value));
+
+const pullDistance = ref(0);
+const isSwipingForClass = ref(false);
+let swipeAborted = false;
+
+function touchStart(event: TouchEvent) {
+ if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+
+ if (event.touches.length !== 1) return;
+
+ if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
+
+ startScreenX = event.touches[0].screenX;
+ startScreenY = event.touches[0].screenY;
+}
+
+function touchMove(event: TouchEvent) {
+ if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+
+ if (event.touches.length !== 1) return;
+
+ if (startScreenX == null || startScreenY == null) return;
+
+ if (swipeAborted) return;
+
+ if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
+
+ let distanceX = event.touches[0].screenX - startScreenX;
+ let distanceY = event.touches[0].screenY - startScreenY;
+
+ if (Math.abs(distanceY) > SWIPE_ABORT_Y_THRESHOLD) {
+ swipeAborted = true;
+
+ pullDistance.value = 0;
+ isSwiping.value = false;
+ setTimeout(() => {
+ isSwipingForClass.value = false;
+ }, 400);
+
+ return;
+ }
+
+ if (Math.abs(distanceX) < MIN_SWIPE_DISTANCE) return;
+ if (Math.abs(distanceX) > MAX_SWIPE_DISTANCE) return;
+
+ if (currentTabIndex.value === 0 || props.tabs[currentTabIndex.value - 1].onClick) {
+ distanceX = Math.min(distanceX, 0);
+ }
+ if (currentTabIndex.value === props.tabs.length - 1 || props.tabs[currentTabIndex.value + 1].onClick) {
+ distanceX = Math.max(distanceX, 0);
+ }
+ if (distanceX === 0) return;
+
+ isSwiping.value = true;
+ isSwipingForClass.value = true;
+ nextTick(() => {
+ // グリッチを控えるため、1.5px以上の差がないと更新しない
+ if (Math.abs(distanceX - pullDistance.value) < 1.5) return;
+ pullDistance.value = distanceX;
+ });
+}
+
+function touchEnd(event: TouchEvent) {
+ if (swipeAborted) {
+ swipeAborted = false;
+ return;
+ }
+
+ if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+
+ if (event.touches.length !== 0) return;
+
+ if (startScreenX == null) return;
+
+ if (!isSwiping.value) return;
+
+ if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
+
+ const distance = event.changedTouches[0].screenX - startScreenX;
+
+ if (Math.abs(distance) > SWIPE_DISTANCE_THRESHOLD) {
+ if (distance > 0) {
+ if (props.tabs[currentTabIndex.value - 1] && !props.tabs[currentTabIndex.value - 1].onClick) {
+ tabModel.value = props.tabs[currentTabIndex.value - 1].key;
+ emit('swiped', props.tabs[currentTabIndex.value - 1].key, 'right');
+ }
+ } else {
+ if (props.tabs[currentTabIndex.value + 1] && !props.tabs[currentTabIndex.value + 1].onClick) {
+ tabModel.value = props.tabs[currentTabIndex.value + 1].key;
+ emit('swiped', props.tabs[currentTabIndex.value + 1].key, 'left');
+ }
+ }
+ }
+
+ pullDistance.value = 0;
+ isSwiping.value = false;
+ window.setTimeout(() => {
+ isSwipingForClass.value = false;
+ }, 400);
+}
+
+/** 横スワイプに関与する可能性のある要素を調べる */
+function hasSomethingToDoWithXSwipe(el: HTMLElement) {
+ if (['INPUT', 'TEXTAREA'].includes(el.tagName)) return true;
+ if (el.isContentEditable) return true;
+ if (el.scrollWidth > el.clientWidth) return true;
+
+ const style = window.getComputedStyle(el);
+ if (['absolute', 'fixed', 'sticky'].includes(style.position)) return true;
+ if (['scroll', 'auto'].includes(style.overflowX)) return true;
+ if (style.touchAction === 'pan-x') return true;
+
+ if (el.parentElement && el.parentElement !== rootEl.value) {
+ return hasSomethingToDoWithXSwipe(el.parentElement);
+ } else {
+ return false;
+ }
+}
+
+const transitionName = ref<'swipeAnimationLeft' | 'swipeAnimationRight' | undefined>(undefined);
+
+watch(tabModel, (newTab, oldTab) => {
+ const newIndex = props.tabs.findIndex(tab => tab.key === newTab);
+ const oldIndex = props.tabs.findIndex(tab => tab.key === oldTab);
+
+ if (oldIndex >= 0 && newIndex && oldIndex < newIndex) {
+ transitionName.value = 'swipeAnimationLeft';
+ } else {
+ transitionName.value = 'swipeAnimationRight';
+ }
+
+ window.setTimeout(() => {
+ transitionName.value = undefined;
+ }, 400);
+});
+</script>
+
+<style lang="scss" module>
+.transitionRoot {
+ touch-action: pan-y pinch-zoom;
+ display: grid;
+ grid-template-columns: 100%;
+ overflow: clip;
+}
+
+.transitionChildren {
+ grid-area: 1 / 1 / 2 / 2;
+ transform: translateX(var(--swipe));
+}
+
+.enableAnimation .transitionChildren {
+ &.swipeAnimation_enterActive,
+ &.swipeAnimation_leaveActive {
+ transition: transform .3s cubic-bezier(0.65, 0.05, 0.36, 1);
+ }
+
+ &.swipeAnimationRight_leaveTo,
+ &.swipeAnimationLeft_enterFrom {
+ transform: translateX(calc(100% + 24px));
+ }
+
+ &.swipeAnimationRight_enterFrom,
+ &.swipeAnimationLeft_leaveTo {
+ transform: translateX(calc(-100% - 24px));
+ }
+}
+
+.swiping {
+ transition: transform .2s ease-out;
+}
+</style>
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 942861e1f4..4e3fafe845 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -73,7 +73,7 @@ const props = withDefaults(defineProps<{
leaveFromClass?: string;
} | null;
src?: string | null;
- hash?: string;
+ hash?: string | null;
alt?: string | null;
title?: string | null;
height?: number;
diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue
index 19402a44ce..33e65ccc4e 100644
--- a/packages/frontend/src/components/MkInfo.vue
+++ b/packages/frontend/src/components/MkInfo.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index ae797eb7d2..d3cddad15b 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -88,17 +88,18 @@ const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
-const inputEl = shallowRef<HTMLElement>();
+const inputEl = shallowRef<HTMLInputElement>();
const prefixEl = shallowRef<HTMLElement>();
const suffixEl = shallowRef<HTMLElement>();
const height =
props.small ? 33 :
props.large ? 39 :
36;
-let autocomplete: Autocomplete;
+let autocompleteWorker: Autocomplete | null = null;
-const focus = () => inputEl.value.focus();
-const onInput = (ev: KeyboardEvent) => {
+const focus = () => inputEl.value?.focus();
+const onInput = (event: Event) => {
+ const ev = event as KeyboardEvent;
changed.value = true;
emit('change', ev);
};
@@ -115,9 +116,9 @@ const onKeydown = (ev: KeyboardEvent) => {
const updated = () => {
changed.value = false;
if (type.value === 'number') {
- emit('update:modelValue', parseFloat(v.value));
+ emit('update:modelValue', typeof v.value === 'number' ? v.value : parseFloat(v.value ?? '0'));
} else {
- emit('update:modelValue', v.value);
+ emit('update:modelValue', v.value ?? '');
}
};
@@ -127,7 +128,7 @@ watch(modelValue, newValue => {
v.value = newValue;
});
-watch(v, newValue => {
+watch(v, () => {
if (!props.manualSave) {
if (props.debounce) {
debouncedUpdated();
@@ -136,12 +137,14 @@ watch(v, newValue => {
}
}
- invalid.value = inputEl.value.validity.badInput;
+ invalid.value = inputEl.value?.validity.badInput ?? true;
});
// このコンポーネントが作成された時、非表示状態である場合がある
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
useInterval(() => {
+ if (inputEl.value == null) return;
+
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@@ -163,15 +166,15 @@ onMounted(() => {
focus();
}
});
-
- if (props.mfmAutocomplete) {
- autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
+
+ if (props.mfmAutocomplete && inputEl.value) {
+ autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete);
}
});
onUnmounted(() => {
- if (autocomplete) {
- autocomplete.detach();
+ if (autocompleteWorker) {
+ autocompleteWorker.detach();
}
});
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index 8a63e0cced..e26aef0f69 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkMiniChart from '@/components/MkMiniChart.vue';
-import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
@@ -27,7 +27,7 @@ const props = defineProps<{
const chartValues = ref<number[] | null>(null);
-os.apiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => {
+misskeyApiGet('charts/instance', { host: props.instance.host, limit: 16 + 1, span: 'day' }).then(res => {
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
res['requests.received'].splice(0, 1);
chartValues.value = res['requests.received'];
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 7b763ad385..d74c885041 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
</MkSelect>
<div class="_panel" :class="$style.heatmap">
- <MkHeatmap :src="heatmapSrc"/>
+ <MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
</div>
</MkFoldableSection>
@@ -90,8 +90,9 @@ import MkSelect from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-import MkHeatmap from '@/components/MkHeatmap.vue';
+import MkHeatmap, { type HeatmapSource } from '@/components/MkHeatmap.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
@@ -102,7 +103,7 @@ initChart();
const chartLimit = 500;
const chartSpan = ref<'hour' | 'day'>('hour');
const chartSrc = ref('active-users');
-const heatmapSrc = ref('active-users');
+const heatmapSrc = ref<HeatmapSource>('active-users');
const subDoughnutEl = shallowRef<HTMLCanvasElement>();
const pubDoughnutEl = shallowRef<HTMLCanvasElement>();
@@ -137,7 +138,8 @@ function createDoughnut(chartEl, tooltip, data) {
},
},
onClick: (ev) => {
- const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
+ if (ev.native == null) return;
+ const hit = chartInstance.getElementsAtEventForMode(ev.native, 'nearest', { intersect: true }, false)[0];
if (hit && data[hit.index].onClick) {
data[hit.index].onClick();
}
@@ -162,24 +164,47 @@ function createDoughnut(chartEl, tooltip, data) {
}
onMounted(() => {
- os.apiGet('federation/stats', { limit: 30 }).then(fedStats => {
- createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
+ misskeyApiGet('federation/stats', { limit: 30 }).then(fedStats => {
+ type ChartData = {
+ name: string,
+ color: string | null,
+ value: number,
+ onClick?: () => void,
+ }[];
+
+ const subs: ChartData = fedStats.topSubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followersCount,
onClick: () => {
os.pageWindow(`/instance-info/${x.host}`);
},
- })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
+ }));
+
+ subs.push({
+ name: '(other)',
+ color: '#80808080',
+ value: fedStats.otherFollowersCount,
+ });
+
+ createDoughnut(subDoughnutEl.value, externalTooltipHandler1, subs);
- createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
+ const pubs: ChartData = fedStats.topPubInstances.map(x => ({
name: x.host,
color: x.themeColor,
value: x.followingCount,
onClick: () => {
os.pageWindow(`/instance-info/${x.host}`);
},
- })).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
+ }));
+
+ pubs.push({
+ name: '(other)',
+ color: '#80808080',
+ value: fedStats.otherFollowingCount,
+ });
+
+ createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, pubs);
});
});
</script>
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index 3ee2aa7174..82c82199b5 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,9 +18,9 @@ import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
instance?: {
- faviconUrl?: string
- name: string
- themeColor?: string
+ faviconUrl?: string | null
+ name?: string | null
+ themeColor?: string | null
}
}>();
@@ -30,7 +30,7 @@ const instance = props.instance ?? {
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
};
-const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
const themeColor = instance.themeColor ?? '#777777';
diff --git a/packages/frontend/src/components/MkInviteCode.stories.impl.ts b/packages/frontend/src/components/MkInviteCode.stories.impl.ts
index 2ea32dd3b6..456d215288 100644
--- a/packages/frontend/src/components/MkInviteCode.stories.impl.ts
+++ b/packages/frontend/src/components/MkInviteCode.stories.impl.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { userDetailed, inviteCode } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkInviteCode from './MkInviteCode.vue';
@@ -39,8 +39,8 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users/show', (req, res, ctx) => {
- return res(ctx.json(userDetailed(req.params.userId as string)));
+ http.post('/api/users/show', ({ params }) => {
+ return HttpResponse.json(userDetailed(params.userId as string));
}),
],
},
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index 84d797484d..1c6f412dc1 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue
index 4f1e4df040..20b1ef2be2 100644
--- a/packages/frontend/src/components/MkKeyValue.vue
+++ b/packages/frontend/src/components/MkKeyValue.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 120ed7a86c..f9d4334c4c 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -1,10 +1,10 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')">
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
<div class="main">
<template v-for="item in items" :key="item.text">
@@ -63,7 +63,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
}));
function close() {
- modal.value.close();
+ modal.value?.close();
}
</script>
@@ -119,6 +119,7 @@ function close() {
margin-top: 12px;
font-size: 0.8em;
line-height: 1.5em;
+ text-align: center;
}
> .indicatorWithValue {
@@ -138,7 +139,7 @@ function close() {
left: 32px;
color: var(--indicator);
font-size: 8px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
@media (max-width: 500px) {
top: 16px;
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 1bd0c8e5c9..a5abbeceac 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkMarquee.vue b/packages/frontend/src/components/MkMarquee.vue
index 145b60c8e7..4a89d21b92 100644
--- a/packages/frontend/src/components/MkMarquee.vue
+++ b/packages/frontend/src/components/MkMarquee.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -30,6 +30,7 @@ export default {
const contentEl = ref<HTMLElement>();
function calc() {
+ if (contentEl.value == null) return;
const eachLength = contentEl.value.offsetWidth / props.repeat;
const factor = 3000;
const duration = props.duration / ((1 / eachLength) * factor);
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
new file mode 100644
index 0000000000..d42146f941
--- /dev/null
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -0,0 +1,361 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ :class="[
+ $style.audioContainer,
+ (audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
+ ]"
+ @contextmenu.stop
+>
+ <button v-if="hide" :class="$style.hidden" @click="hide = false">
+ <div :class="$style.hiddenTextWrapper">
+ <b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
+ <b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
+ <span style="display: block;">{{ i18n.ts.clickToShow }}</span>
+ </div>
+ </button>
+ <div v-else :class="$style.audioControls">
+ <audio
+ ref="audioEl"
+ preload="metadata"
+ >
+ <source :src="audio.url">
+ </audio>
+ <div :class="[$style.controlsChild, $style.controlsLeft]">
+ <button class="_button" :class="$style.controlButton" @click="togglePlayPause">
+ <i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
+ <i v-else class="ti ti-player-play-filled"></i>
+ </button>
+ </div>
+ <div :class="[$style.controlsChild, $style.controlsRight]">
+ <button class="_button" :class="$style.controlButton" @click="showMenu">
+ <i class="ti ti-settings"></i>
+ </button>
+ </div>
+ <div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
+ <div :class="[$style.controlsChild, $style.controlsVolume]">
+ <button class="_button" :class="$style.controlButton" @click="toggleMute">
+ <i v-if="volume === 0" class="ti ti-volume-3"></i>
+ <i v-else class="ti ti-volume"></i>
+ </button>
+ <MkMediaRange
+ v-model="volume"
+ :class="$style.volumeSeekbar"
+ />
+ </div>
+ <MkMediaRange
+ v-model="rangePercent"
+ :class="$style.seekbarRoot"
+ :buffer="bufferedDataRatio"
+ />
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
+import * as Misskey from 'misskey-js';
+import type { MenuItem } from '@/types/menu.js';
+import { defaultStore } from '@/store.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import bytes from '@/filters/bytes.js';
+import { hms } from '@/filters/hms.js';
+import MkMediaRange from '@/components/MkMediaRange.vue';
+import { iAmModerator } from '@/account.js';
+
+const props = defineProps<{
+ audio: Misskey.entities.DriveFile;
+}>();
+
+const audioEl = shallowRef<HTMLAudioElement>();
+
+// eslint-disable-next-line vue/no-setup-props-destructure
+const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
+
+// Menu
+const menuShowing = ref(false);
+
+function showMenu(ev: MouseEvent) {
+ let menu: MenuItem[] = [];
+
+ menu = [
+ // TODO: 再生キューに追加
+ {
+ text: i18n.ts.hide,
+ icon: 'ti ti-eye-off',
+ action: () => {
+ hide.value = true;
+ },
+ },
+ ];
+
+ if (iAmModerator) {
+ menu.push({
+ type: 'divider',
+ }, {
+ text: props.audio.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
+ icon: props.audio.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
+ danger: true,
+ action: () => toggleSensitive(props.audio),
+ });
+ }
+
+ menuShowing.value = true;
+ os.popupMenu(menu, ev.currentTarget ?? ev.target, {
+ align: 'right',
+ onClosing: () => {
+ menuShowing.value = false;
+ },
+ });
+}
+
+function toggleSensitive(file: Misskey.entities.DriveFile) {
+ os.apiWithDialog('drive/files/update', {
+ fileId: file.id,
+ isSensitive: !file.isSensitive,
+ });
+}
+
+// MediaControl: Common State
+const oncePlayed = ref(false);
+const isReady = ref(false);
+const isPlaying = ref(false);
+const isActuallyPlaying = ref(false);
+const elapsedTimeMs = ref(0);
+const durationMs = ref(0);
+const rangePercent = computed({
+ get: () => {
+ return (elapsedTimeMs.value / durationMs.value) || 0;
+ },
+ set: (to) => {
+ if (!audioEl.value) return;
+ audioEl.value.currentTime = to * durationMs.value / 1000;
+ },
+});
+const volume = ref(.25);
+const bufferedEnd = ref(0);
+const bufferedDataRatio = computed(() => {
+ if (!audioEl.value) return 0;
+ return bufferedEnd.value / audioEl.value.duration;
+});
+
+// MediaControl Events
+function togglePlayPause() {
+ if (!isReady.value || !audioEl.value) return;
+
+ if (isPlaying.value) {
+ audioEl.value.pause();
+ isPlaying.value = false;
+ } else {
+ audioEl.value.play();
+ isPlaying.value = true;
+ oncePlayed.value = true;
+ }
+}
+
+function toggleMute() {
+ if (volume.value === 0) {
+ volume.value = .25;
+ } else {
+ volume.value = 0;
+ }
+}
+
+let onceInit = false;
+let stopAudioElWatch: () => void;
+
+function init() {
+ if (onceInit) return;
+ onceInit = true;
+
+ stopAudioElWatch = watch(audioEl, () => {
+ if (audioEl.value) {
+ isReady.value = true;
+
+ function updateMediaTick() {
+ if (audioEl.value) {
+ try {
+ bufferedEnd.value = audioEl.value.buffered.end(0);
+ } catch (err) {
+ bufferedEnd.value = 0;
+ }
+
+ elapsedTimeMs.value = audioEl.value.currentTime * 1000;
+ }
+ window.requestAnimationFrame(updateMediaTick);
+ }
+
+ updateMediaTick();
+
+ audioEl.value.addEventListener('play', () => {
+ isActuallyPlaying.value = true;
+ });
+
+ audioEl.value.addEventListener('pause', () => {
+ isActuallyPlaying.value = false;
+ isPlaying.value = false;
+ });
+
+ audioEl.value.addEventListener('ended', () => {
+ oncePlayed.value = false;
+ isActuallyPlaying.value = false;
+ isPlaying.value = false;
+ });
+
+ durationMs.value = audioEl.value.duration * 1000;
+ audioEl.value.addEventListener('durationchange', () => {
+ if (audioEl.value) {
+ durationMs.value = audioEl.value.duration * 1000;
+ }
+ });
+
+ audioEl.value.volume = volume.value;
+ }
+ }, {
+ immediate: true,
+ });
+}
+
+watch(volume, (to) => {
+ if (audioEl.value) audioEl.value.volume = to;
+});
+
+onMounted(() => {
+ init();
+});
+
+onActivated(() => {
+ init();
+});
+
+onDeactivated(() => {
+ isReady.value = false;
+ isPlaying.value = false;
+ isActuallyPlaying.value = false;
+ elapsedTimeMs.value = 0;
+ durationMs.value = 0;
+ bufferedEnd.value = 0;
+ hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore');
+ stopAudioElWatch();
+ onceInit = false;
+});
+</script>
+
+<style lang="scss" module>
+.audioContainer {
+ container-type: inline-size;
+ position: relative;
+ border: .5px solid var(--divider);
+ border-radius: var(--radius);
+ overflow: clip;
+}
+
+.sensitive {
+ position: relative;
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ border-radius: inherit;
+ box-shadow: inset 0 0 0 4px var(--warn);
+ }
+}
+
+.hidden {
+ width: 100%;
+ background: #000;
+ border: none;
+ outline: none;
+ font: inherit;
+ color: inherit;
+ cursor: pointer;
+ padding: 12px 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.hiddenTextWrapper {
+ text-align: center;
+ font-size: 0.8em;
+ color: #fff;
+}
+
+.audioControls {
+ display: grid;
+ grid-template-areas:
+ "left time . volume right"
+ "seekbar seekbar seekbar seekbar seekbar";
+ grid-template-columns: auto auto 1fr auto auto;
+ align-items: center;
+ gap: 4px 8px;
+ padding: 10px;
+}
+
+.controlsChild {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+
+ .controlButton {
+ padding: 6px;
+ border-radius: calc(var(--radius) / 2);
+ font-size: 1.05rem;
+
+ &:hover {
+ color: var(--accent);
+ background-color: var(--accentedBg);
+ }
+ }
+}
+
+.controlsLeft {
+ grid-area: left;
+}
+
+.controlsRight {
+ grid-area: right;
+}
+
+.controlsTime {
+ grid-area: time;
+ font-size: .9rem;
+}
+
+.controlsVolume {
+ grid-area: volume;
+
+ .volumeSeekbar {
+ display: none;
+ }
+}
+
+.seekbarRoot {
+ grid-area: seekbar;
+}
+
+@container (min-width: 500px) {
+ .audioControls {
+ grid-template-areas: "left seekbar time volume right";
+ grid-template-columns: auto 1fr auto auto auto;
+ }
+
+ .controlsVolume {
+ .volumeSeekbar {
+ max-width: 90px;
+ display: block;
+ flex-grow: 1;
+ }
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 3f8fef6632..a219848b7f 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -1,24 +1,16 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
- <div v-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
+ <MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
+ <div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="hide = false">
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
<b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
- <div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :class="$style.audio">
- <audio
- ref="audioEl"
- :src="media.url"
- :title="media.name"
- controls
- preload="metadata"
- />
- </div>
<a
v-else :class="$style.download"
:href="media.url"
@@ -35,6 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { shallowRef, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
+import MkMediaAudio from '@/components/MkMediaAudio.vue';
const props = withDefaults(defineProps<{
media: Misskey.entities.DriveFile;
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index d236b222aa..4ba2c76133 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 09c5ad9222..b1321a8ef9 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -52,7 +52,7 @@ const count = computed(() => props.mediaList.filter(media => previewable(media))
let lightbox: PhotoSwipeLightbox | null;
const popstateHandler = (): void => {
- if (lightbox.pswp && lightbox.pswp.isOpen === true) {
+ if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
lightbox.pswp.close();
}
};
@@ -67,7 +67,10 @@ async function calcAspectRatio() {
return;
}
- const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
+ const ratioMax = (ratio: number) => {
+ if (img.properties.width == null || img.properties.height == null) return '';
+ return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
+ };
switch (defaultStore.state.mediaListWithOneImageAppearance) {
case '16_9':
@@ -137,7 +140,7 @@ onMounted(() => {
// element is children
const { element } = itemData;
- const id = element.dataset.id;
+ const id = element?.dataset.id;
const file = props.mediaList.find(media => media.id === id);
if (!file) return;
@@ -147,14 +150,14 @@ onMounted(() => {
if (file.properties.orientation != null && file.properties.orientation >= 5) {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
- itemData.msrc = file.thumbnailUrl;
+ itemData.msrc = file.thumbnailUrl ?? undefined;
itemData.alt = file.comment ?? file.name;
itemData.comment = file.comment ?? file.name;
itemData.thumbCropped = true;
});
lightbox.on('uiRegister', () => {
- lightbox.pswp.ui.registerElement({
+ lightbox?.pswp?.ui?.registerElement({
name: 'altText',
className: 'pwsp__alt-text-container',
appendTo: 'wrapper',
@@ -163,8 +166,8 @@ onMounted(() => {
textBox.className = 'pwsp__alt-text _acrylic';
el.appendChild(textBox);
- pwsp.on('change', (a) => {
- textBox.textContent = pwsp.currSlide.data.comment;
+ pwsp.on('change', () => {
+ textBox.textContent = pwsp.currSlide?.data.comment;
});
},
});
diff --git a/packages/frontend/src/components/MkMediaRange.vue b/packages/frontend/src/components/MkMediaRange.vue
new file mode 100644
index 0000000000..86ed8ba2cf
--- /dev/null
+++ b/packages/frontend/src/components/MkMediaRange.vue
@@ -0,0 +1,152 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<!-- Media系専用のinput range -->
+<template>
+<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'">
+ <div :class="$style.controlsSeekbar">
+ <progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress>
+ <input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { computed, ModelRef } from 'vue';
+
+withDefaults(defineProps<{
+ buffer?: number;
+ sliderBgWhite?: boolean;
+}>(), {
+ buffer: undefined,
+ sliderBgWhite: false,
+});
+
+const emit = defineEmits<{
+ (ev: 'dragEnded', value: number): void;
+}>();
+
+// eslint-disable-next-line no-undef
+const model = defineModel({ required: true }) as ModelRef<string | number>;
+const modelValue = computed({
+ get: () => typeof model.value === 'number' ? model.value : parseFloat(model.value),
+ set: v => { model.value = v; },
+});
+</script>
+
+<style lang="scss" module>
+.controlsSeekbar {
+ position: relative;
+}
+
+.seek {
+ position: relative;
+ -webkit-appearance: none;
+ appearance: none;
+ background: transparent;
+ border: 0;
+ border-radius: 26px;
+ color: var(--accent);
+ display: block;
+ height: 19px;
+ margin: 0;
+ min-width: 0;
+ padding: 0;
+ transition: box-shadow .3s ease;
+ width: 100%;
+
+ &::-webkit-slider-runnable-track {
+ background-color: var(--sliderBg);
+ background-image: linear-gradient(to right,currentColor var(--value,0),transparent var(--value,0));
+ border: 0;
+ border-radius: 99rem;
+ height: 5px;
+ transition: box-shadow .3s ease;
+ user-select: none;
+ }
+
+ &::-moz-range-track {
+ background: transparent;
+ border: 0;
+ border-radius: 99rem;
+ height: 5px;
+ transition: box-shadow .3s ease;
+ user-select: none;
+ background-color: var(--sliderBg);
+ }
+
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ background: #fff;
+ border: 0;
+ border-radius: 100%;
+ box-shadow: 0 1px 1px rgba(35, 40, 47, .15),0 0 0 1px rgba(35, 40, 47, .2);
+ height: 13px;
+ margin-top: -4px;
+ position: relative;
+ transition: all .2s ease;
+ width: 13px;
+
+ &:active {
+ box-shadow: 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .15), 0 0 0 3px rgba(255, 255, 255, .5);
+ }
+ }
+
+ &::-moz-range-thumb {
+ background: #fff;
+ border: 0;
+ border-radius: 100%;
+ box-shadow: 0 1px 1px rgba(35, 40, 47, .15),0 0 0 1px rgba(35, 40, 47, .2);
+ height: 13px;
+ position: relative;
+ transition: all .2s ease;
+ width: 13px;
+
+ &:active {
+ box-shadow: 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .15), 0 0 0 3px rgba(255, 255, 255, .5);
+ }
+ }
+
+ &::-moz-range-progress {
+ background: currentColor;
+ border-radius: 99rem;
+ height: 5px;
+ }
+}
+
+.buffer {
+ appearance: none;
+ background: transparent;
+ color: var(--sliderBg);
+ border: 0;
+ border-radius: 99rem;
+ height: 5px;
+ left: 0;
+ margin-top: -2.5px;
+ padding: 0;
+ position: absolute;
+ top: 50%;
+ width: 100%;
+
+ &::-webkit-progress-bar {
+ background: transparent;
+ }
+
+ &::-webkit-progress-value {
+ background: currentColor;
+ border-radius: 100px;
+ min-width: 5px;
+ transition: width .2s ease;
+ }
+
+ &::-moz-progress-bar {
+ background: currentColor;
+ border-radius: 100px;
+ min-width: 5px;
+ transition: width .2s ease;
+ }
+}
+</style>
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index f9dba0b15a..eab4fdfd6b 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -1,71 +1,348 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div v-if="hide" :class="[$style.hidden, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]" @click="hide = false">
- <!-- 【注意】dataSaverMode が有効になっている際には、hide が false になるまでサムネイルや動画を読み込まないようにすること -->
- <div :class="$style.sensitive">
- <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
- <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
- <span>{{ i18n.ts.clickToShow }}</span>
- </div>
-</div>
-<div v-else :class="[$style.visible, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]">
- <video
- ref="videoEl"
- :class="$style.video"
- :poster="video.thumbnailUrl"
- :title="video.comment"
- :alt="video.comment"
- preload="none"
- controls
- @contextmenu.stop
- >
- <source
- :src="video.url"
+<div
+ ref="playerEl"
+ :class="[
+ $style.videoContainer,
+ controlsShowing && $style.active,
+ (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
+ ]"
+ @mouseover="onMouseOver"
+ @mouseleave="onMouseLeave"
+ @contextmenu.stop
+>
+ <button v-if="hide" :class="$style.hidden" @click="hide = false">
+ <div :class="$style.hiddenTextWrapper">
+ <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
+ <b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
+ <span style="display: block;">{{ i18n.ts.clickToShow }}</span>
+ </div>
+ </button>
+ <div v-else :class="$style.videoRoot" @click.self="togglePlayPause">
+ <video
+ ref="videoEl"
+ :class="$style.video"
+ :poster="video.thumbnailUrl ?? undefined"
+ :title="video.comment ?? undefined"
+ :alt="video.comment"
+ preload="metadata"
+ playsinline
>
- </video>
- <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
+ <source :src="video.url">
+ </video>
+ <button v-if="isReady && !isPlaying" class="_button" :class="$style.videoOverlayPlayButton" @click="togglePlayPause"><i class="ti ti-player-play-filled"></i></button>
+ <div v-else-if="!isActuallyPlaying" :class="$style.videoLoading">
+ <MkLoading/>
+ </div>
+ <i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
+ <div :class="$style.indicators">
+ <div v-if="video.comment" :class="$style.indicator">ALT</div>
+ <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+ </div>
+ <div :class="$style.videoControls" @click.self="togglePlayPause">
+ <div :class="[$style.controlsChild, $style.controlsLeft]">
+ <button class="_button" :class="$style.controlButton" @click="togglePlayPause">
+ <i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
+ <i v-else class="ti ti-player-play-filled"></i>
+ </button>
+ </div>
+ <div :class="[$style.controlsChild, $style.controlsRight]">
+ <button class="_button" :class="$style.controlButton" @click="showMenu">
+ <i class="ti ti-settings"></i>
+ </button>
+ <button class="_button" :class="$style.controlButton" @click="toggleFullscreen">
+ <i v-if="isFullscreen" class="ti ti-arrows-minimize"></i>
+ <i v-else class="ti ti-arrows-maximize"></i>
+ </button>
+ </div>
+ <div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
+ <div :class="[$style.controlsChild, $style.controlsVolume]">
+ <button class="_button" :class="$style.controlButton" @click="toggleMute">
+ <i v-if="volume === 0" class="ti ti-volume-3"></i>
+ <i v-else class="ti ti-volume"></i>
+ </button>
+ <MkMediaRange
+ v-model="volume"
+ :sliderBgWhite="true"
+ :class="$style.volumeSeekbar"
+ />
+ </div>
+ <MkMediaRange
+ v-model="rangePercent"
+ :sliderBgWhite="true"
+ :class="$style.seekbarRoot"
+ :buffer="bufferedDataRatio"
+ />
+ </div>
+ </div>
</div>
</template>
<script lang="ts" setup>
-import { ref, shallowRef, watch } from 'vue';
+import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
+import type { MenuItem } from '@/types/menu.js';
import bytes from '@/filters/bytes.js';
+import { hms } from '@/filters/hms.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { isFullscreenNotSupported } from '@/scripts/device-kind.js';
import hasAudio from '@/scripts/media-has-audio.js';
+import MkMediaRange from '@/components/MkMediaRange.vue';
+import { iAmModerator } from '@/account.js';
const props = defineProps<{
video: Misskey.entities.DriveFile;
}>();
+// eslint-disable-next-line vue/no-setup-props-destructure
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
+// Menu
+const menuShowing = ref(false);
+
+function showMenu(ev: MouseEvent) {
+ let menu: MenuItem[] = [];
+
+ menu = [
+ // TODO: 再生キューに追加
+ {
+ text: i18n.ts.hide,
+ icon: 'ti ti-eye-off',
+ action: () => {
+ hide.value = true;
+ },
+ },
+ ];
+
+ if (iAmModerator) {
+ menu.push({
+ type: 'divider',
+ }, {
+ text: props.video.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive,
+ icon: props.video.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation',
+ danger: true,
+ action: () => toggleSensitive(props.video),
+ });
+ }
+
+ menuShowing.value = true;
+ os.popupMenu(menu, ev.currentTarget ?? ev.target, {
+ align: 'right',
+ onClosing: () => {
+ menuShowing.value = false;
+ },
+ });
+}
+
+function toggleSensitive(file: Misskey.entities.DriveFile) {
+ os.apiWithDialog('drive/files/update', {
+ fileId: file.id,
+ isSensitive: !file.isSensitive,
+ });
+}
+
+// MediaControl: Video State
const videoEl = shallowRef<HTMLVideoElement>();
+const playerEl = shallowRef<HTMLDivElement>();
+const isHoverring = ref(false);
+const controlsShowing = computed(() => {
+ if (!oncePlayed.value) return true;
+ if (isHoverring.value) return true;
+ if (menuShowing.value) return true;
+ return false;
+});
+const isFullscreen = ref(false);
+let controlStateTimer: string | number;
+
+// MediaControl: Common State
+const oncePlayed = ref(false);
+const isReady = ref(false);
+const isPlaying = ref(false);
+const isActuallyPlaying = ref(false);
+const elapsedTimeMs = ref(0);
+const durationMs = ref(0);
+const rangePercent = computed({
+ get: () => {
+ return (elapsedTimeMs.value / durationMs.value) || 0;
+ },
+ set: (to) => {
+ if (!videoEl.value) return;
+ videoEl.value.currentTime = to * durationMs.value / 1000;
+ },
+});
+const volume = ref(.25);
+const bufferedEnd = ref(0);
+const bufferedDataRatio = computed(() => {
+ if (!videoEl.value) return 0;
+ return bufferedEnd.value / videoEl.value.duration;
+});
+
+// MediaControl Events
+function onMouseOver() {
+ if (controlStateTimer) {
+ clearTimeout(controlStateTimer);
+ }
+ isHoverring.value = true;
+}
+
+function onMouseLeave() {
+ controlStateTimer = window.setTimeout(() => {
+ isHoverring.value = false;
+ }, 100);
+}
+
+function togglePlayPause() {
+ if (!isReady.value || !videoEl.value) return;
+
+ if (isPlaying.value) {
+ videoEl.value.pause();
+ isPlaying.value = false;
+ } else {
+ videoEl.value.play();
+ isPlaying.value = true;
+ oncePlayed.value = true;
+ }
+}
+
+function toggleFullscreen() {
+ if (isFullscreenNotSupported && videoEl.value) {
+ if (isFullscreen.value) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ //@ts-ignore
+ videoEl.value.webkitExitFullscreen();
+ isFullscreen.value = false;
+ } else {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ //@ts-ignore
+ videoEl.value.webkitEnterFullscreen();
+ isFullscreen.value = true;
+ }
+ } else if (playerEl.value) {
+ if (isFullscreen.value) {
+ document.exitFullscreen();
+ isFullscreen.value = false;
+ } else {
+ playerEl.value.requestFullscreen({ navigationUI: 'hide' });
+ isFullscreen.value = true;
+ }
+ }
+}
+
+function toggleMute() {
+ if (volume.value === 0) {
+ volume.value = .25;
+ } else {
+ volume.value = 0;
+ }
+}
+
+let onceInit = false;
+let stopVideoElWatch: () => void;
-watch(videoEl, () => {
- if (videoEl.value) {
- videoEl.value.volume = 0.3;
- hasAudio(videoEl.value).then(had => {
- if (!had) {
- videoEl.value.loop = videoEl.value.muted = true;
- videoEl.value.play();
+function init() {
+ if (onceInit) return;
+ onceInit = true;
+
+ stopVideoElWatch = watch(videoEl, () => {
+ if (videoEl.value) {
+ isReady.value = true;
+
+ function updateMediaTick() {
+ if (videoEl.value) {
+ try {
+ bufferedEnd.value = videoEl.value.buffered.end(0);
+ } catch (err) {
+ bufferedEnd.value = 0;
+ }
+
+ elapsedTimeMs.value = videoEl.value.currentTime * 1000;
+ }
+ window.requestAnimationFrame(updateMediaTick);
}
- });
+
+ updateMediaTick();
+
+ videoEl.value.addEventListener('play', () => {
+ isActuallyPlaying.value = true;
+ });
+
+ videoEl.value.addEventListener('pause', () => {
+ isActuallyPlaying.value = false;
+ isPlaying.value = false;
+ });
+
+ videoEl.value.addEventListener('ended', () => {
+ oncePlayed.value = false;
+ isActuallyPlaying.value = false;
+ isPlaying.value = false;
+ });
+
+ durationMs.value = videoEl.value.duration * 1000;
+ videoEl.value.addEventListener('durationchange', () => {
+ if (videoEl.value) {
+ durationMs.value = videoEl.value.duration * 1000;
+ }
+ });
+
+ videoEl.value.volume = volume.value;
+ hasAudio(videoEl.value).then(had => {
+ if (!had && videoEl.value) {
+ videoEl.value.loop = videoEl.value.muted = true;
+ videoEl.value.play();
+ }
+ });
+ }
+ }, {
+ immediate: true,
+ });
+}
+
+watch(volume, (to) => {
+ if (videoEl.value) videoEl.value.volume = to;
+});
+
+watch(hide, (to) => {
+ if (to && isFullscreen.value) {
+ document.exitFullscreen();
+ isFullscreen.value = false;
}
});
+
+onMounted(() => {
+ init();
+});
+
+onActivated(() => {
+ init();
+});
+
+onDeactivated(() => {
+ isReady.value = false;
+ isPlaying.value = false;
+ isActuallyPlaying.value = false;
+ elapsedTimeMs.value = 0;
+ durationMs.value = 0;
+ bufferedEnd.value = 0;
+ hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore');
+ stopVideoElWatch();
+ onceInit = false;
+});
</script>
<style lang="scss" module>
-.visible {
+.videoContainer {
+ container-type: inline-size;
position: relative;
+ overflow: clip;
}
-.sensitiveContainer {
+.sensitive {
position: relative;
&::after {
@@ -81,44 +358,199 @@ watch(videoEl, () => {
}
}
+.indicators {
+ display: inline-flex;
+ position: absolute;
+ top: 10px;
+ left: 10px;
+ pointer-events: none;
+ opacity: .5;
+ gap: 6px;
+}
+
+.indicator {
+ /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+ background-color: black;
+ border-radius: 6px;
+ color: var(--accentLighten);
+ display: inline-block;
+ font-weight: bold;
+ font-size: 0.8em;
+ padding: 2px 5px;
+}
+
.hide {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--fg);
color: var(--accentLighten);
- font-size: 14px;
+ font-size: 12px;
opacity: .5;
- padding: 3px 6px;
+ padding: 5px 8px;
text-align: center;
cursor: pointer;
top: 12px;
right: 12px;
}
-.video {
+.hidden {
+ width: 100%;
+ height: 100%;
+ background: #000;
+ border: none;
+ outline: none;
+ font: inherit;
+ color: inherit;
+ cursor: pointer;
+ padding: 120px 0;
display: flex;
- justify-content: center;
align-items: center;
- font-size: 3.5em;
- overflow: hidden;
- background-position: center;
- background-size: cover;
+ justify-content: center;
+}
+
+.hiddenTextWrapper {
+ text-align: center;
+ font-size: 0.8em;
+ color: #fff;
+}
+
+.videoRoot {
+ background: #000;
+ position: relative;
width: 100%;
height: 100%;
+ object-fit: contain;
}
-.hidden {
+.video {
+ display: block;
+ height: 100%;
+ width: 100%;
+ pointer-events: none;
+}
+
+.videoOverlayPlayButton {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+
+ opacity: 0;
+ transition: opacity .4s ease-in-out;
+
+ background: var(--accent);
+ color: #fff;
+ padding: 1rem;
+ border-radius: 99rem;
+
+ font-size: 1.1rem;
+}
+
+.videoLoading {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
display: flex;
+ align-items: center;
justify-content: center;
+}
+
+.videoControls {
+ display: grid;
+ grid-template-areas:
+ "left time . volume right"
+ "seekbar seekbar seekbar seekbar seekbar";
+ grid-template-columns: auto auto 1fr auto auto;
align-items: center;
- background: #111;
+ gap: 4px 8px;
+
+ padding: 35px 10px 10px 10px;
+ background: linear-gradient(rgba(0, 0, 0, 0),rgba(0, 0, 0, .75));
+
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ transform: translateY(100%);
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity .4s ease-in-out, transform .4s ease-in-out;
+}
+
+.active {
+ .videoControls {
+ transform: translateY(0);
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .videoOverlayPlayButton {
+ opacity: 1;
+ }
+}
+
+.controlsChild {
+ display: flex;
+ align-items: center;
+ gap: 4px;
color: #fff;
+
+ .controlButton {
+ padding: 6px;
+ border-radius: calc(var(--radius) / 2);
+ transition: background-color .2s ease-in-out;
+ font-size: 1.05rem;
+
+ &:hover {
+ background-color: var(--accent);
+ }
+ }
}
-.sensitive {
- display: table-cell;
- text-align: center;
- font-size: 12px;
+.controlsLeft {
+ grid-area: left;
+}
+
+.controlsRight {
+ grid-area: right;
+}
+
+.controlsTime {
+ grid-area: time;
+ font-size: .9rem;
+}
+
+.controlsVolume {
+ grid-area: volume;
+
+ .volumeSeekbar {
+ display: none;
+ }
+}
+
+.seekbarRoot {
+ grid-area: seekbar;
+ /* ▼シークバー操作をやりやすくするためにクリックイベントが伝播されないエリアを拡張する */
+ margin: -10px;
+ padding: 10px;
+}
+
+@container (min-width: 500px) {
+ .videoControls {
+ grid-template-areas: "left seekbar time volume right";
+ grid-template-columns: auto 1fr auto auto auto;
+ }
+
+ .controlsVolume {
+ .volumeSeekbar {
+ max-width: 90px;
+ display: block;
+ flex-grow: 1;
+ }
+ }
}
</style>
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index 80426157e6..e6e8711f67 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue
index 962dcd91eb..dfb6d34618 100644
--- a/packages/frontend/src/components/MkMenu.child.vue
+++ b/packages/frontend/src/components/MkMenu.child.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -33,6 +33,7 @@ const align = 'left';
const SCROLLBAR_THICKNESS = 16;
function setPosition() {
+ if (el.value == null) return;
const rootRect = props.rootElement.getBoundingClientRect();
const parentRect = props.targetElement.getBoundingClientRect();
const myRect = el.value.getBoundingClientRect();
@@ -66,7 +67,7 @@ const ro = new ResizeObserver((entries, observer) => {
});
onMounted(() => {
- ro.observe(el.value);
+ if (el.value) ro.observe(el.value);
setPosition();
nextTick(() => {
setPosition();
@@ -79,7 +80,7 @@ onUnmounted(() => {
defineExpose({
checkHit: (ev: MouseEvent) => {
- return (ev.target === el.value || el.value.contains(ev.target));
+ return (ev.target === el.value || el.value?.contains(ev.target as Node));
},
});
</script>
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 3026d4f015..faed6416d0 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@contextmenu.self="e => e.preventDefault()"
>
- <template v-for="(item, i) in items2">
+ <template v-for="(item, i) in (items2 ?? [])">
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
<span style="opacity: 0.7;">{{ item.text }}</span>
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
</div>
</button>
- <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+ <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<div :class="$style.item_content">
@@ -63,18 +63,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</button>
</template>
- <span v-if="items2.length === 0" :class="[$style.none, $style.item]">
+ <span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
<span>{{ i18n.ts.none }}</span>
</span>
</div>
<div v-if="childMenu">
- <XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned" @close="close(false)"/>
+ <XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
</div>
</div>
</template>
<script lang="ts">
-import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
+import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus.js';
import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js';
@@ -104,7 +104,7 @@ const emit = defineEmits<{
const itemsEl = shallowRef<HTMLDivElement>();
-const items2 = ref<InnerMenuItem[]>([]);
+const items2 = ref<InnerMenuItem[]>();
const child = shallowRef<InstanceType<typeof XChild>>();
@@ -119,15 +119,15 @@ const childShowingItem = ref<MenuItem | null>();
let preferClick = isTouchUsing || props.asDrawer;
watch(() => props.items, () => {
- const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
+ const items = [...props.items].filter(item => item !== undefined) as (NonNullable<MenuItem> | MenuPending)[];
for (let i = 0; i < items.length; i++) {
const item = items[i];
- if (item && 'then' in item) { // if item is Promise
+ if ('then' in item) { // if item is Promise
items[i] = { type: 'pending' };
item.then(actualItem => {
- items2.value[i] = actualItem;
+ if (items2.value?.[i]) items2.value[i] = actualItem;
});
}
}
@@ -151,7 +151,7 @@ function childActioned() {
}
const onGlobalMousedown = (event: MouseEvent) => {
- if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target))) return;
+ if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
if (child.value && child.value.checkHit(event)) return;
closeChild();
};
@@ -169,7 +169,7 @@ function onItemMouseLeave(item) {
}
async function showChildren(item: MenuParent, ev: MouseEvent) {
- const children = await (async () => {
+ const children: MenuItem[] = await (async () => {
if (childrenCache.has(item)) {
return childrenCache.get(item)!;
} else {
@@ -189,7 +189,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
});
emit('hide');
} else {
- childTarget.value = ev.currentTarget ?? ev.target;
+ childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
// これでもリアクティビティは保たれる
childMenu.value = children;
childShowingItem.value = item;
@@ -218,6 +218,10 @@ function switchItem(item: MenuSwitch & { ref: any }) {
item.ref = !item.ref;
}
+function getValue<T>(item?: ComputedRef<T> | T) {
+ return isRef(item) ? item.value : item;
+}
+
onMounted(() => {
if (props.viaKeyboard) {
nextTick(() => {
@@ -450,7 +454,7 @@ onBeforeUnmount(() => {
align-items: center;
color: var(--indicator);
font-size: 12px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
}
.divider {
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index f0a2c232bd..f2f2bf47a8 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
stroke-width="2"
/>
<circle
- :cx="headX"
- :cy="headY"
+ :cx="headX ?? undefined"
+ :cy="headY ?? undefined"
r="3"
:fill="color"
/>
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 5cd31cdf7c..40e67fb4e0 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index 7e185e8453..d3657afa94 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -51,7 +51,7 @@ const bodyWidth = ref(0);
const bodyHeight = ref(0);
const close = () => {
- modal.value.close();
+ modal.value?.close();
};
const onBgClick = () => {
@@ -67,11 +67,13 @@ const onKeydown = (evt) => {
};
const ro = new ResizeObserver((entries, observer) => {
+ if (rootEl.value == null || headerEl.value == null) return;
bodyWidth.value = rootEl.value.offsetWidth;
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
});
onMounted(() => {
+ if (rootEl.value == null || headerEl.value == null) return;
bodyWidth.value = rootEl.value.offsetWidth;
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
ro.observe(rootEl.value);
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 7d4207f0fb..03a283cab3 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -1,13 +1,13 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div
- v-if="!hardMuted && !muted"
+ v-if="!hardMuted && muted === false"
v-show="!isDeleted"
- ref="el"
+ ref="rootEl"
v-hotkey="keymap"
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
:tabindex="!isDeleted ? '-1' : undefined"
@@ -72,16 +72,16 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
- <div v-else>
- <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
+ <div v-else-if="translation">
+ <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
</div>
- <div v-if="appearNote.files.length > 0">
+ <div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
- <MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
+ <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
@@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<i class="ti ti-paperclip"></i>
</button>
- <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()">
+ <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@@ -134,7 +134,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</article>
</div>
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
- <I18n :src="i18n.ts.userSaysSomething" tag="small">
+ <I18n v-if="muted === 'sensitiveMute'" :src="i18n.ts.userSaysSomethingSensitive" tag="small">
+ <template #name>
+ <MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
+ <MkUserName :user="appearNote.user"/>
+ </MkA>
+ </template>
+ </I18n>
+ <I18n v-else :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
<MkUserName :user="appearNote.user"/>
@@ -170,6 +177,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
import * as sound from '@/scripts/sound.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
@@ -202,6 +210,7 @@ const emit = defineEmits<{
(ev: 'removeReaction', emoji: string): void;
}>();
+const inTimeline = inject<boolean>('inTimeline', false);
const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
@@ -213,7 +222,7 @@ if (noteViewInterruptors.length > 0) {
let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
- result = await interruptor.handler(result);
+ result = await interruptor.handler(result!) as Misskey.entities.Note | null;
if (result === null) {
isDeleted.value = true;
return;
@@ -222,7 +231,7 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note.value = result;
+ note.value = result as Misskey.entities.Note;
});
}
@@ -230,11 +239,11 @@ const isRenote = (
note.value.renote != null &&
note.value.text == null &&
note.value.cw == null &&
- note.value.fileIds.length === 0 &&
+ note.value.fileIds && note.value.fileIds.length === 0 &&
note.value.poll == null
);
-const el = shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>();
@@ -244,40 +253,53 @@ const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entiti
const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false);
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
-const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value) : null);
+const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
const collapsed = ref(appearNote.value.cw == null && isLong);
const isDeleted = ref(false);
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
-const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords));
+const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
-const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
+const renoteCollapsed = ref(
+ defaultStore.state.collapseRenotes && isRenote && (
+ ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
+ (appearNote.value.myReaction != null)
+ )
+);
-function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
+/* Overload FunctionにLintが対応していないのでコメントアウト
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
+*/
+function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
if (mutedWords == null) return false;
- if (checkWordMute(note, $i, mutedWords)) return true;
- if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
- if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
+ if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
+ if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
+ if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
+
+ if (checkOnly) return false;
+
+ if (inTimeline && !defaultStore.state.tl.filter.withSensitive && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute';
return false;
}
const keymap = {
'r': () => reply(true),
'e|a|plus': () => react(true),
- 'q': () => renoteButton.value.renote(true),
+ 'q': () => renote(true),
'up|k|shift+tab': focusBefore,
'down|j|tab': focusAfter,
'esc': blur,
- 'm|o': () => menu(true),
+ 'm|o': () => showMenu(true),
's': () => showContent.value !== showContent.value,
};
provide('react', (reaction: string) => {
- os.api('notes/reactions/create', {
+ misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id,
reaction: reaction,
});
@@ -289,7 +311,7 @@ if (props.mock) {
}, { deep: true });
} else {
useNoteCapture({
- rootEl: el,
+ rootEl: rootEl,
note: appearNote,
pureNote: note,
isDeletedRef: isDeleted,
@@ -298,7 +320,7 @@ if (props.mock) {
if (!props.mock) {
useTooltip(renoteButton, async (showing) => {
- const renotes = await os.api('notes/renotes', {
+ const renotes = await misskeyApi('notes/renotes', {
noteId: appearNote.value.id,
limit: 11,
});
@@ -335,7 +357,7 @@ function reply(viaKeyboard = false): void {
reply: appearNote.value,
channel: appearNote.value.channel,
animation: !viaKeyboard,
- }, () => {
+ }).then(() => {
focus();
});
}
@@ -344,17 +366,17 @@ function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') {
- sound.play('reaction');
+ sound.playMisskeySfx('reaction');
if (props.mock) {
return;
}
- os.api('notes/reactions/create', {
+ misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id,
reaction: '❤️',
});
- const el = reactButton.value as HTMLElement | null | undefined;
+ const el = reactButton.value;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
@@ -363,15 +385,15 @@ function react(viaKeyboard = false): void {
}
} else {
blur();
- reactionPicker.show(reactButton.value, reaction => {
- sound.play('reaction');
+ reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
+ sound.playMisskeySfx('reaction');
if (props.mock) {
emit('reaction', reaction);
return;
}
- os.api('notes/reactions/create', {
+ misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id,
reaction: reaction,
});
@@ -384,8 +406,8 @@ function react(viaKeyboard = false): void {
}
}
-function undoReact(note): void {
- const oldReaction = note.myReaction;
+function undoReact(targetNote: Misskey.entities.Note): void {
+ const oldReaction = targetNote.myReaction;
if (!oldReaction) return;
if (props.mock) {
@@ -393,8 +415,8 @@ function undoReact(note): void {
return;
}
- os.api('notes/reactions/delete', {
- noteId: note.id,
+ misskeyApi('notes/reactions/delete', {
+ noteId: targetNote.id,
});
}
@@ -403,32 +425,34 @@ function onContextmenu(ev: MouseEvent): void {
return;
}
- const isLink = (el: HTMLElement) => {
+ const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true;
// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
if (el.tagName === 'AUDIO') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
+ return false;
};
- if (isLink(ev.target)) return;
- if (window.getSelection().toString() !== '') return;
+
+ if (ev.target && isLink(ev.target as HTMLElement)) return;
+ if (window.getSelection()?.toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
-function menu(viaKeyboard = false): void {
+function showMenu(viaKeyboard = false): void {
if (props.mock) {
return;
}
- const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
@@ -453,7 +477,7 @@ function showRenoteMenu(viaKeyboard = false): void {
icon: 'ti ti-trash',
danger: true,
action: () => {
- os.api('notes/delete', {
+ misskeyApi('notes/delete', {
noteId: note.value.id,
});
isDeleted.value = true;
@@ -475,7 +499,7 @@ function showRenoteMenu(viaKeyboard = false): void {
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' },
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
- $i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
+ ($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
], renoteTime.value, {
viaKeyboard: viaKeyboard,
});
@@ -483,23 +507,23 @@ function showRenoteMenu(viaKeyboard = false): void {
}
function focus() {
- el.value.focus();
+ rootEl.value?.focus();
}
function blur() {
- el.value.blur();
+ rootEl.value?.blur();
}
function focusBefore() {
- focusPrev(el.value);
+ focusPrev(rootEl.value ?? null);
}
function focusAfter() {
- focusNext(el.value);
+ focusNext(rootEl.value ?? null);
}
function readPromo() {
- os.api('promo/read', {
+ misskeyApi('promo/read', {
noteId: appearNote.value.id,
});
isDeleted.value = true;
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 33a6786d03..e3ef14120f 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div
v-if="!muted"
v-show="!isDeleted"
- ref="el"
+ ref="rootEl"
v-hotkey="keymap"
:class="$style.root"
>
@@ -86,15 +86,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
- <div v-else>
- <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
+ <div v-else-if="translation">
+ <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div>
</div>
- <div v-if="appearNote.files.length > 0">
+ <div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/>
</div>
- <MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>
+ <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
</div>
@@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<i class="ti ti-paperclip"></i>
</button>
- <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
+ <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@@ -210,6 +210,7 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
import { userPage } from '@/filters/user.js';
import { notePage } from '@/filters/note.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import * as sound from '@/scripts/sound.js';
import { defaultStore, noteViewInterruptors } from '@/store.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
@@ -224,7 +225,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination from '@/components/MkPagination.vue';
+import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
@@ -242,7 +243,7 @@ if (noteViewInterruptors.length > 0) {
let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) {
try {
- result = await interruptor.handler(result);
+ result = await interruptor.handler(result!) as Misskey.entities.Note | null;
if (result === null) {
isDeleted.value = true;
return;
@@ -251,18 +252,18 @@ if (noteViewInterruptors.length > 0) {
console.error(err);
}
}
- note.value = result;
+ note.value = result as Misskey.entities.Note;
});
}
const isRenote = (
note.value.renote != null &&
note.value.text == null &&
- note.value.fileIds.length === 0 &&
+ note.value.fileIds && note.value.fileIds.length === 0 &&
note.value.poll == null
);
-const el = shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>();
@@ -276,23 +277,23 @@ const muted = ref($i ? checkWordMute(appearNote.value, $i, $i.mutedWords) : fals
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false);
const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
-const urls = parsed ? extractUrlFromMfm(parsed) : null;
+const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id);
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
const keymap = {
'r': () => reply(true),
'e|a|plus': () => react(true),
- 'q': () => renoteButton.value.renote(true),
+ 'q': () => renote(true),
'esc': blur,
- 'm|o': () => menu(true),
+ 'm|o': () => showMenu(true),
's': () => showContent.value !== showContent.value,
};
provide('react', (reaction: string) => {
- os.api('notes/reactions/create', {
+ misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id,
reaction: reaction,
});
@@ -301,7 +302,7 @@ provide('react', (reaction: string) => {
const tab = ref('replies');
const reactionTabType = ref<string | null>(null);
-const renotesPagination = computed(() => ({
+const renotesPagination = computed<Paging>(() => ({
endpoint: 'notes/renotes',
limit: 10,
params: {
@@ -309,7 +310,7 @@ const renotesPagination = computed(() => ({
},
}));
-const reactionsPagination = computed(() => ({
+const reactionsPagination = computed<Paging>(() => ({
endpoint: 'notes/reactions',
limit: 10,
params: {
@@ -319,14 +320,14 @@ const reactionsPagination = computed(() => ({
}));
useNoteCapture({
- rootEl: el,
+ rootEl: rootEl,
note: appearNote,
pureNote: note,
isDeletedRef: isDeleted,
});
useTooltip(renoteButton, async (showing) => {
- const renotes = await os.api('notes/renotes', {
+ const renotes = await misskeyApi('notes/renotes', {
noteId: appearNote.value.id,
limit: 11,
});
@@ -360,7 +361,7 @@ function reply(viaKeyboard = false): void {
reply: appearNote.value,
channel: appearNote.value.channel,
animation: !viaKeyboard,
- }, () => {
+ }).then(() => {
focus();
});
}
@@ -369,9 +370,9 @@ function react(viaKeyboard = false): void {
pleaseLogin();
showMovedDialog();
if (appearNote.value.reactionAcceptance === 'likeOnly') {
- sound.play('reaction');
+ sound.playMisskeySfx('reaction');
- os.api('notes/reactions/create', {
+ misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id,
reaction: '❤️',
});
@@ -384,10 +385,10 @@ function react(viaKeyboard = false): void {
}
} else {
blur();
- reactionPicker.show(reactButton.value, reaction => {
- sound.play('reaction');
+ reactionPicker.show(reactButton.value ?? null, note.value, reaction => {
+ sound.playMisskeySfx('reaction');
- os.api('notes/reactions/create', {
+ misskeyApi('notes/reactions/create', {
noteId: appearNote.value.id,
reaction: reaction,
});
@@ -403,32 +404,34 @@ function react(viaKeyboard = false): void {
function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
- os.api('notes/reactions/delete', {
+ misskeyApi('notes/reactions/delete', {
noteId: note.id,
});
}
function onContextmenu(ev: MouseEvent): void {
- const isLink = (el: HTMLElement) => {
+ const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
+ return false;
};
- if (isLink(ev.target)) return;
- if (window.getSelection().toString() !== '') return;
+
+ if (ev.target && isLink(ev.target as HTMLElement)) return;
+ if (window.getSelection()?.toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault();
react();
} else {
- const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup);
}
}
-function menu(viaKeyboard = false): void {
- const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
+function showMenu(viaKeyboard = false): void {
+ const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
os.popupMenu(menu, menuButton.value, {
viaKeyboard,
}).then(focus).finally(cleanup);
@@ -446,7 +449,7 @@ function showRenoteMenu(viaKeyboard = false): void {
icon: 'ti ti-trash',
danger: true,
action: () => {
- os.api('notes/delete', {
+ misskeyApi('notes/delete', {
noteId: note.value.id,
});
isDeleted.value = true;
@@ -457,18 +460,18 @@ function showRenoteMenu(viaKeyboard = false): void {
}
function focus() {
- el.value.focus();
+ rootEl.value?.focus();
}
function blur() {
- el.value.blur();
+ rootEl.value?.blur();
}
const repliesLoaded = ref(false);
function loadReplies() {
repliesLoaded.value = true;
- os.api('notes/children', {
+ misskeyApi('notes/children', {
noteId: appearNote.value.id,
limit: 30,
}).then(res => {
@@ -480,7 +483,8 @@ const conversationLoaded = ref(false);
function loadConversation() {
conversationLoaded.value = true;
- os.api('notes/conversation', {
+ if (appearNote.value.replyId == null) return;
+ misskeyApi('notes/conversation', {
noteId: appearNote.value.replyId,
}).then(res => {
conversation.value = res.reverse();
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index b2236b99c2..be5829d92f 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
- <img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
+ <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
</div>
<div :class="$style.info">
<div v-if="mock">
diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue
index d664d88231..cc2f770cda 100644
--- a/packages/frontend/src/components/MkNotePreview.vue
+++ b/packages/frontend/src/components/MkNotePreview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div>
<p v-if="useCw" :class="$style.cw">
- <Mfm v-if="cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
+ <Mfm v-if="cw != null && cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
<MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/>
</p>
<div v-show="!useCw || showContent">
@@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
+import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import MkCwButton from '@/components/MkCwButton.vue';
const showContent = ref(false);
@@ -33,12 +34,7 @@ const showContent = ref(false);
const props = defineProps<{
text: string;
files: Misskey.entities.DriveFile[];
- poll?: {
- choices: string[];
- multiple: boolean;
- expiresAt: string | null;
- expiredAfter: string | null;
- };
+ poll?: PollEditorModelValue;
useCw: boolean;
cw: string | null;
user: Misskey.entities.User;
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue
index e7cb3f96f1..c3f3c42b42 100644
--- a/packages/frontend/src/components/MkNoteSimple.vue
+++ b/packages/frontend/src/components/MkNoteSimple.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 40362a955a..829b37e7a7 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -46,7 +46,7 @@ import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
import { notePage } from '@/filters/note.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { userPage } from '@/filters/user.js';
@@ -68,7 +68,7 @@ const showContent = ref(false);
const replies = ref<Misskey.entities.Note[]>([]);
if (props.detail) {
- os.api('notes/children', {
+ misskeyApi('notes/children', {
noteId: props.note.id,
limit: 5,
}).then(res => {
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 7af31074db..0856c146ba 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index da7eb36d90..322b9400be 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -1,15 +1,13 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div :class="$style.head">
- <MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
- <MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
- <MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
- <MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
+ <MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
+ <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
@@ -26,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
+ [$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
}]"
>
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
@@ -37,12 +36,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
- <img v-else-if="notification.type === 'roleAssigned'" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
- <!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
+ <template v-else-if="notification.type === 'roleAssigned'">
+ <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
+ <i v-else class="ti ti-badges"></i>
+ </template>
<MkReactionIcon
v-else-if="notification.type === 'reaction'"
:withTooltip="true"
- :reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
+ :reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
:noStyle="true"
style="width: 100%; height: 100%;"
/>
@@ -55,10 +56,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
- <MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
- <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.t('_notification.reactedBySomeUsers', { n: notification.reactions.length }) }}</span>
- <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.t('_notification.renotedBySomeUsers', { n: notification.users.length }) }}</span>
- <span v-else>{{ notification.header }}</span>
+ <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
+ <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
+ <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
+ <span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header>
<div>
@@ -97,7 +98,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
<template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
- <div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
</template>
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<template v-else-if="notification.type === 'receiveFollowRequest'">
@@ -113,12 +113,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</span>
<div v-if="notification.type === 'reaction:grouped'">
- <div v-for="reaction of notification.reactions" :class="$style.reactionsItem">
+ <div v-for="reaction of notification.reactions" :key="reaction.user.id + reaction.reaction" :class="$style.reactionsItem">
<MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/>
<div :class="$style.reactionsItemReaction">
<MkReactionIcon
:withTooltip="true"
- :reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction"
+ :reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
:noStyle="true"
style="width: 100%; height: 100%;"
/>
@@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-else-if="notification.type === 'renote:grouped'">
- <div v-for="user of notification.users" :class="$style.reactionsItem">
+ <div v-for="user of notification.users" :key="user.id" :class="$style.reactionsItem">
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
</div>
</div>
@@ -139,16 +139,17 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
-import MkFollowButton from '@/components/MkFollowButton.vue';
import MkButton from '@/components/MkButton.vue';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
import { notePage } from '@/filters/note.js';
import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
-import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { signinRequired } from '@/account.js';
import { infoImageUrl } from '@/instance.js';
+const $i = signinRequired();
+
const props = withDefaults(defineProps<{
notification: Misskey.entities.Notification;
withTime?: boolean;
@@ -161,13 +162,15 @@ const props = withDefaults(defineProps<{
const followRequestDone = ref(false);
const acceptFollowRequest = () => {
+ if (props.notification.user == null) return;
followRequestDone.value = true;
- os.api('following/requests/accept', { userId: props.notification.user.id });
+ misskeyApi('following/requests/accept', { userId: props.notification.user.id });
};
const rejectFollowRequest = () => {
+ if (props.notification.user == null) return;
followRequestDone.value = true;
- os.api('following/requests/reject', { userId: props.notification.user.id });
+ misskeyApi('following/requests/reject', { userId: props.notification.user.id });
};
</script>
@@ -283,6 +286,12 @@ const rejectFollowRequest = () => {
pointer-events: none;
}
+.t_roleAssigned {
+ padding: 3px;
+ background: #88a6b7;
+ pointer-events: none;
+}
+
.tail {
flex: 1;
min-width: 0;
diff --git a/packages/frontend/src/components/MkNotificationSelectWindow.vue b/packages/frontend/src/components/MkNotificationSelectWindow.vue
index 6725776f43..71b38d99ed 100644
--- a/packages/frontend/src/components/MkNotificationSelectWindow.vue
+++ b/packages/frontend/src/components/MkNotificationSelectWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton inline @click="disableAll">{{ i18n.ts.disableAll }}</MkButton>
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
</div>
- <MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.t(`_notification._types.${ntype}`) }}</MkSwitch>
+ <MkSwitch v-for="ntype in notificationTypes" :key="ntype" v-model="typesMap[ntype].value">{{ i18n.ts._notification._types[ntype] }}</MkSwitch>
</div>
</MkSpacer>
</MkModalWindow>
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index bb8a5d2e72..a9f019dd9c 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
- <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
+ <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList>
</template>
@@ -63,7 +63,7 @@ function onNotification(notification) {
}
if (!isMuted) {
- pagingComponent.value.prepend(notification);
+ pagingComponent.value?.prepend(notification);
}
}
diff --git a/packages/frontend/src/components/MkNumber.vue b/packages/frontend/src/components/MkNumber.vue
index aa04ab253b..a278205b61 100644
--- a/packages/frontend/src/components/MkNumber.vue
+++ b/packages/frontend/src/components/MkNumber.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -9,7 +9,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { reactive, watch } from 'vue';
-import gsap from 'gsap';
import number from '@/filters/number.js';
const props = defineProps<{
@@ -20,8 +19,24 @@ const tweened = reactive({
number: 0,
});
-watch(() => props.value, (n) => {
- gsap.to(tweened, { duration: 1, number: Number(n) || 0 });
+watch(() => props.value, (to, from) => {
+ // requestAnimationFrameを利用して、500msでfromからtoまでを1次関数的に変化させる
+ let start: number | null = null;
+
+ function step(timestamp: number) {
+ if (start === null) {
+ start = timestamp;
+ }
+ const elapsed = timestamp - start;
+ tweened.number = (from ?? 0) + (to - (from ?? 0)) * elapsed / 500;
+ if (elapsed < 500) {
+ window.requestAnimationFrame(step);
+ } else {
+ tweened.number = to;
+ }
+ }
+
+ window.requestAnimationFrame(step);
}, {
immediate: true,
});
diff --git a/packages/frontend/src/components/MkNumberDiff.vue b/packages/frontend/src/components/MkNumberDiff.vue
index a98b6c4713..1825cc5405 100644
--- a/packages/frontend/src/components/MkNumberDiff.vue
+++ b/packages/frontend/src/components/MkNumberDiff.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index aa05c43c0b..870599aa94 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkObjectView.vue b/packages/frontend/src/components/MkObjectView.vue
index 30ec896ce4..bb9122c976 100644
--- a/packages/frontend/src/components/MkObjectView.vue
+++ b/packages/frontend/src/components/MkObjectView.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index 1b0ec72e41..100a025653 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -27,7 +27,7 @@ const omitted = ref(false);
const ignoreOmit = ref(false);
const calcOmit = () => {
- if (omitted.value || ignoreOmit.value) return;
+ if (omitted.value || ignoreOmit.value || content.value == null) return;
omitted.value = content.value.offsetHeight > props.maxHeight;
};
@@ -37,7 +37,7 @@ const omitObserver = new ResizeObserver((entries, observer) => {
onMounted(() => {
calcOmit();
- omitObserver.observe(content.value);
+ omitObserver.observe(content.value as HTMLElement);
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 6c8a0e56a6..f6dc00698c 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</header>
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
<footer>
- <img class="icon" :src="page.user.avatarUrl"/>
+ <img v-if="page.user.avatarUrl" class="icon" :src="page.user.avatarUrl"/>
<p>{{ userName(page.user) }}</p>
</footer>
</article>
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 2647ace7db..aa4509b14b 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,33 +16,33 @@ SPDX-License-Identifier: AGPL-3.0-only
@closed="$emit('closed')"
>
<template #header>
- <template v-if="pageMetadata?.value">
- <i v-if="pageMetadata.value.icon" :class="pageMetadata.value.icon" style="margin-right: 0.5em;"></i>
- <span>{{ pageMetadata.value.title }}</span>
+ <template v-if="pageMetadata">
+ <i v-if="pageMetadata.icon" :class="pageMetadata.icon" style="margin-right: 0.5em;"></i>
+ <span>{{ pageMetadata.title }}</span>
</template>
</template>
<div ref="contents" :class="$style.root" style="container-type: inline-size;">
- <RouterView :key="reloadCount" :router="router"/>
+ <RouterView :key="reloadCount" :router="windowRouter"/>
</div>
</MkWindow>
</template>
<script lang="ts" setup>
-import { ComputedRef, onMounted, onUnmounted, provide, shallowRef, ref, computed } from 'vue';
+import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
-import { mainRouter, routes, page } from '@/router.js';
-import { $i } from '@/account.js';
-import { Router, useScrollPositionManager } from '@/nirax.js';
+import { useScrollPositionManager } from '@/nirax.js';
import { i18n } from '@/i18n.js';
-import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getScrollContainer } from '@/scripts/scroll.js';
+import { useRouterFactory } from '@/router/supplier.js';
+import { mainRouter } from '@/router/main.js';
const props = defineProps<{
initialPath: string;
@@ -52,17 +52,18 @@ defineEmits<{
(ev: 'closed'): void;
}>();
-const router = new Router(routes, props.initialPath, !!$i, page(() => import('@/pages/not-found.vue')));
+const routerFactory = useRouterFactory();
+const windowRouter = routerFactory(props.initialPath);
-const contents = shallowRef<HTMLElement>();
-const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
+const contents = shallowRef<HTMLElement | null>(null);
+const pageMetadata = ref<null | PageMetadata>(null);
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
const history = ref<{ path: string; key: any; }[]>([{
- path: router.getCurrentPath(),
- key: router.getCurrentKey(),
+ path: windowRouter.getCurrentPath(),
+ key: windowRouter.getCurrentKey(),
}]);
const buttonsLeft = computed(() => {
- const buttons = [];
+ const buttons: Record<string, unknown>[] = [];
if (history.value.length > 1) {
buttons.push({
@@ -88,14 +89,23 @@ const buttonsRight = computed(() => {
});
const reloadCount = ref(0);
-router.addListener('push', ctx => {
+windowRouter.addListener('push', ctx => {
history.value.push({ path: ctx.path, key: ctx.key });
});
-provide('router', router);
-provideMetadataReceiver((info) => {
+windowRouter.addListener('replace', ctx => {
+ history.value.pop();
+ history.value.push({ path: ctx.path, key: ctx.key });
+});
+
+windowRouter.init();
+
+provide('router', windowRouter);
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
pageMetadata.value = info;
});
+provideReactiveMetadata(pageMetadata);
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
provide('forceSpacerMin', true);
@@ -112,20 +122,20 @@ const contextmenu = computed(() => ([{
icon: 'ti ti-external-link',
text: i18n.ts.openInNewTab,
action: () => {
- window.open(url + router.getCurrentPath(), '_blank', 'noopener');
- windowEl.value.close();
+ window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
+ windowEl.value?.close();
},
}, {
icon: 'ti ti-link',
text: i18n.ts.copyLink,
action: () => {
- copyToClipboard(url + router.getCurrentPath());
+ copyToClipboard(url + windowRouter.getCurrentPath());
},
}]));
function back() {
history.value.pop();
- router.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
+ windowRouter.replace(history.value.at(-1)!.path, history.value.at(-1)!.key);
}
function reload() {
@@ -133,20 +143,20 @@ function reload() {
}
function close() {
- windowEl.value.close();
+ windowEl.value?.close();
}
function expand() {
- mainRouter.push(router.getCurrentPath(), 'forcePage');
- windowEl.value.close();
+ mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
+ windowEl.value?.close();
}
function popout() {
- _popout(router.getCurrentPath(), windowEl.value.$el);
- windowEl.value.close();
+ _popout(windowRouter.getCurrentPath(), windowEl.value?.$el);
+ windowEl.value?.close();
}
-useScrollPositionManager(() => getScrollContainer(contents.value), router);
+useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
onMounted(() => {
openingWindowsCount.value++;
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index bdd96238d3..62a85389ad 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -46,6 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
import { useDocumentVisibility } from '@/scripts/use-document-visibility.js';
import { defaultStore } from '@/store.js';
@@ -203,7 +204,7 @@ async function init(): Promise<void> {
queue.value = new Map();
fetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
- await os.api(props.pagination.endpoint, {
+ await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params,
limit: props.pagination.limit ?? 10,
allowPartial: true,
@@ -239,7 +240,7 @@ const fetchMore = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
- await os.api(props.pagination.endpoint, {
+ await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? {
@@ -303,7 +304,7 @@ const fetchMoreAhead = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
- await os.api(props.pagination.endpoint, {
+ await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params,
limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? {
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index c77e912199..c49526d8e2 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -41,7 +41,9 @@ import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
const emit = defineEmits<{
(ev: 'done', v: { password: string; token: string | null; }): void;
diff --git a/packages/frontend/src/components/MkPlusOneEffect.vue b/packages/frontend/src/components/MkPlusOneEffect.vue
index a741a3f7a8..6c22edb943 100644
--- a/packages/frontend/src/components/MkPlusOneEffect.vue
+++ b/packages/frontend/src/components/MkPlusOneEffect.vue
@@ -1,11 +1,11 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
- <span class="text" :class="{ up }">+1</span>
+ <span class="text" :class="{ up }">+{{ value }}</span>
</div>
</template>
@@ -16,7 +16,9 @@ import * as os from '@/os.js';
const props = withDefaults(defineProps<{
x: number;
y: number;
+ value?: number | string;
}>(), {
+ value: 1,
});
const emit = defineEmits<{
@@ -40,6 +42,7 @@ onMounted(() => {
<style lang="scss" module>
.root {
+ user-select: none;
pointer-events: none;
position: fixed;
width: 128px;
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 682f8e3060..a98690f1c3 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -1,22 +1,22 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="{ [$style.done]: closed || isVoted }">
<ul :class="$style.choices">
- <li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
+ <li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span :class="$style.fg">
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
<Mfm :text="choice.text" :plain="true"/>
- <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.t('_poll.votesCount', { n: choice.votes }) }})</span>
+ <span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
</span>
</li>
</ul>
<p v-if="!readOnly" :class="$style.info">
- <span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
+ <span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span>
<span> · </span>
<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
@@ -32,35 +32,38 @@ import * as Misskey from 'misskey-js';
import { sum } from '@/scripts/array.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js';
const props = defineProps<{
- note: Misskey.entities.Note;
+ noteId: string;
+ poll: NonNullable<Misskey.entities.Note['poll']>;
readOnly?: boolean;
}>();
const remaining = ref(-1);
-const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
+const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0);
-const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
-const timer = computed(() => i18n.t(
- remaining.value >= 86400 ? '_poll.remainingDays' :
- remaining.value >= 3600 ? '_poll.remainingHours' :
- remaining.value >= 60 ? '_poll.remainingMinutes' : '_poll.remainingSeconds', {
- s: Math.floor(remaining.value % 60),
- m: Math.floor(remaining.value / 60) % 60,
- h: Math.floor(remaining.value / 3600) % 24,
- d: Math.floor(remaining.value / 86400),
- }));
+const isVoted = computed(() => !props.poll.multiple && props.poll.choices.some(c => c.isVoted));
+const timer = computed(() => i18n.tsx._poll[
+ remaining.value >= 86400 ? 'remainingDays' :
+ remaining.value >= 3600 ? 'remainingHours' :
+ remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
+]({
+ s: Math.floor(remaining.value % 60),
+ m: Math.floor(remaining.value / 60) % 60,
+ h: Math.floor(remaining.value / 3600) % 24,
+ d: Math.floor(remaining.value / 86400),
+}));
const showResult = ref(props.readOnly || isVoted.value);
// 期限付きアンケート
-if (props.note.poll.expiresAt) {
+if (props.poll.expiresAt) {
const tick = () => {
- remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000);
+ remaining.value = Math.floor(Math.max(new Date(props.poll.expiresAt!).getTime() - Date.now(), 0) / 1000);
if (remaining.value === 0) {
showResult.value = true;
}
@@ -79,15 +82,15 @@ const vote = async (id) => {
const { canceled } = await os.confirm({
type: 'question',
- text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }),
+ text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
});
if (canceled) return;
- await os.api('notes/polls/vote', {
- noteId: props.note.id,
+ await misskeyApi('notes/polls/vote', {
+ noteId: props.noteId,
choice: id,
});
- if (!showResult.value) showResult.value = !props.note.poll.multiple;
+ if (!showResult.value) showResult.value = !props.poll.multiple;
};
</script>
diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue
index 43e576d1ab..db74354bbb 100644
--- a/packages/frontend/src/components/MkPollEditor.vue
+++ b/packages/frontend/src/components/MkPollEditor.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</p>
<ul>
<li v-for="(choice, i) in choices" :key="i">
- <MkInput class="input" small :modelValue="choice" :placeholder="i18n.t('_poll.choiceN', { n: i + 1 })" @update:modelValue="onInput(i, $event)">
+ <MkInput class="input" small :modelValue="choice" :placeholder="i18n.tsx._poll.choiceN({ n: i + 1 })" @update:modelValue="onInput(i, $event)">
</MkInput>
<button class="_button" @click="remove(i)">
<i class="ti ti-x"></i>
@@ -62,21 +62,18 @@ import { formatDateTimeString } from '@/scripts/format-time-string.js';
import { addTime } from '@/scripts/time.js';
import { i18n } from '@/i18n.js';
+export type PollEditorModelValue = {
+ expiresAt: number | null;
+ expiredAfter: number | null;
+ choices: string[];
+ multiple: boolean;
+};
+
const props = defineProps<{
- modelValue: {
- expiresAt: string;
- expiredAfter: number;
- choices: string[];
- multiple: boolean;
- };
+ modelValue: PollEditorModelValue;
}>();
const emit = defineEmits<{
- (ev: 'update:modelValue', v: {
- expiresAt: string;
- expiredAfter: number;
- choices: string[];
- multiple: boolean;
- }): void;
+ (ev: 'update:modelValue', v: PollEditorModelValue): void;
}>();
const choices = ref(props.modelValue.choices);
@@ -89,7 +86,9 @@ const unit = ref('second');
if (props.modelValue.expiresAt) {
expiration.value = 'at';
- atDate.value = atTime.value = props.modelValue.expiresAt;
+ const expiresAt = new Date(props.modelValue.expiresAt);
+ atDate.value = formatDateTimeString(expiresAt, 'yyyy-MM-dd');
+ atTime.value = formatDateTimeString(expiresAt, 'HH:mm');
} else if (typeof props.modelValue.expiredAfter === 'number') {
expiration.value = 'after';
after.value = props.modelValue.expiredAfter / 1000;
@@ -113,20 +112,21 @@ function remove(i) {
choices.value = choices.value.filter((_, _i) => _i !== i);
}
-function get() {
+function get(): PollEditorModelValue {
const calcAt = () => {
return new Date(`${atDate.value} ${atTime.value}`).getTime();
};
const calcAfter = () => {
- let base = parseInt(after.value);
+ let base = parseInt(after.value.toString());
switch (unit.value) {
+ // @ts-expect-error fallthrough
case 'day': base *= 24;
- // fallthrough
+ // @ts-expect-error fallthrough
case 'hour': base *= 60;
- // fallthrough
+ // @ts-expect-error fallthrough
case 'minute': base *= 60;
- // fallthrough
+ // eslint-disable-next-line no-fallthrough
case 'second': return base *= 1000;
default: return null;
}
@@ -135,10 +135,8 @@ function get() {
return {
choices: choices.value,
multiple: multiple.value,
- ...(
- expiration.value === 'at' ? { expiresAt: calcAt() } :
- expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
- ),
+ expiresAt: expiration.value === 'at' ? calcAt() : null,
+ expiredAfter: expiration.value === 'after' ? calcAfter() : null,
};
}
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index 95ef2d9b75..be0b07612a 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 3aacf4c2da..819f0f692c 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
- <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
+ <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
</div>
@@ -108,19 +108,20 @@ import { toASCII } from 'punycode/';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
-import MkPollEditor from '@/components/MkPollEditor.vue';
+import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
import { host, url } from '@/config.js';
import { erase, unique } from '@/scripts/array.js';
import { extractMentions } from '@/scripts/extract-mentions.js';
import { formatTimeString } from '@/scripts/format-time-string.js';
import { Autocomplete } from '@/scripts/autocomplete.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { selectFiles } from '@/scripts/select-file.js';
import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-import { $i, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js';
+import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { uploadFile } from '@/scripts/upload.js';
import { deepClone } from '@/scripts/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
@@ -129,6 +130,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
+const $i = signinRequired();
+
const modal = inject('modal');
const props = withDefaults(defineProps<{
@@ -136,13 +139,13 @@ const props = withDefaults(defineProps<{
renote?: Misskey.entities.Note;
channel?: Misskey.entities.Channel; // TODO
mention?: Misskey.entities.User;
- specified?: Misskey.entities.User;
+ specified?: Misskey.entities.UserDetailed;
initialText?: string;
initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
- initialVisibleUsers?: Misskey.entities.User[];
+ initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note;
instant?: boolean;
fixed?: boolean;
@@ -175,12 +178,7 @@ const posting = ref(false);
const posted = ref(false);
const text = ref(props.initialText ?? '');
const files = ref(props.initialFiles ?? []);
-const poll = ref<{
- choices: string[];
- multiple: boolean;
- expiresAt: string | null;
- expiredAfter: string | null;
-} | null>(null);
+const poll = ref<PollEditorModelValue | null>(null);
const useCw = ref<boolean>(!!props.initialCw);
const showPreview = ref(defaultStore.state.showPreview);
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
@@ -307,7 +305,7 @@ if (props.reply && props.reply.text != null) {
}
}
-if ($i?.isSilenced && visibility.value === 'public') {
+if ($i.isSilenced && visibility.value === 'public') {
visibility.value = 'home';
}
@@ -328,15 +326,15 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
if (visibility.value === 'specified') {
if (props.reply.visibleUserIds) {
- os.api('users/show', {
- userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
+ misskeyApi('users/show', {
+ userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId),
}).then(users => {
users.forEach(pushVisibleUser);
});
}
if (props.reply.userId !== $i.id) {
- os.api('users/show', { userId: props.reply.userId }).then(user => {
+ misskeyApi('users/show', { userId: props.reply.userId }).then(user => {
pushVisibleUser(user);
});
}
@@ -383,7 +381,7 @@ function addMissingMention() {
for (const x of extractMentions(ast)) {
if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
- os.api('users/show', { username: x.username, host: x.host }).then(user => {
+ misskeyApi('users/show', { username: x.username, host: x.host }).then(user => {
visibleUsers.value.push(user);
});
}
@@ -460,7 +458,7 @@ function setVisibility() {
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
currentVisibility: visibility.value,
- isSilenced: $i?.isSilenced,
+ isSilenced: $i.isSilenced,
localOnly: localOnly.value,
src: visibilityButton.value,
}, {
@@ -531,7 +529,7 @@ async function toggleReactionAcceptance() {
reactionAcceptance.value = select.result;
}
-function pushVisibleUser(user) {
+function pushVisibleUser(user: Misskey.entities.UserDetailed) {
if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
visibleUsers.value.push(user);
}
@@ -573,10 +571,12 @@ function onCompositionEnd(ev: CompositionEvent) {
async function onPaste(ev: ClipboardEvent) {
if (props.mock) return;
+ if (!ev.clipboardData) return;
- for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) {
+ for (const { item, i } of Array.from(ev.clipboardData.items, (data, x) => ({ item: data, i: x }))) {
if (item.kind === 'file') {
const file = item.getAsFile();
+ if (!file) continue;
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
@@ -598,7 +598,7 @@ async function onPaste(ev: ClipboardEvent) {
return;
}
- quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+ quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? null;
});
}
}
@@ -629,26 +629,26 @@ function onDragover(ev) {
}
}
-function onDragenter(ev) {
+function onDragenter() {
draghover.value = true;
}
-function onDragleave(ev) {
+function onDragleave() {
draghover.value = false;
}
-function onDrop(ev): void {
+function onDrop(ev: DragEvent): void {
draghover.value = false;
// ファイルだったら
- if (ev.dataTransfer.files.length > 0) {
+ if (ev.dataTransfer && ev.dataTransfer.files.length > 0) {
ev.preventDefault();
for (const x of Array.from(ev.dataTransfer.files)) upload(x);
return;
}
//#region ドライブのファイル
- const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+ const driveFile = ev.dataTransfer?.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile);
files.value.push(file);
@@ -696,11 +696,14 @@ async function post(ev?: MouseEvent) {
}
if (ev) {
- const el = ev.currentTarget ?? ev.target;
- const rect = el.getBoundingClientRect();
- const x = rect.left + (el.offsetWidth / 2);
- const y = rect.top + (el.offsetHeight / 2);
- os.popup(MkRippleEffect, { x, y }, {}, 'end');
+ const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
+
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ const x = rect.left + (el.offsetWidth / 2);
+ const y = rect.top + (el.offsetHeight / 2);
+ os.popup(MkRippleEffect, { x, y }, {}, 'end');
+ }
}
if (props.mock) return;
@@ -752,29 +755,39 @@ async function post(ev?: MouseEvent) {
if (withHashtags.value && hashtags.value && hashtags.value.trim() !== '') {
const hashtags_ = hashtags.value.trim().split(' ').map(x => x.startsWith('#') ? x : '#' + x).join(' ');
- postData.text = postData.text ? `${postData.text} ${hashtags_}` : hashtags_;
+ if (!postData.text) {
+ postData.text = hashtags_;
+ } else {
+ const postTextLines = postData.text.split('\n');
+ if (postTextLines[postTextLines.length - 1].trim() === '') {
+ postTextLines[postTextLines.length - 1] += hashtags_;
+ } else {
+ postTextLines[postTextLines.length - 1] += ' ' + hashtags_;
+ }
+ postData.text = postTextLines.join('\n');
+ }
}
// plugin
if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) {
try {
- postData = await interruptor.handler(deepClone(postData));
+ postData = await interruptor.handler(deepClone(postData)) as typeof postData;
} catch (err) {
console.error(err);
}
}
}
- let token = undefined;
+ let token: string | undefined = undefined;
if (postAccount.value) {
const storedAccounts = await getAccounts();
- token = storedAccounts.find(x => x.id === postAccount.value.id)?.token;
+ token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token;
}
posting.value = true;
- os.api('notes/create', postData, token).then(() => {
+ misskeyApi('notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
posted.value = true;
} else {
@@ -784,7 +797,7 @@ async function post(ev?: MouseEvent) {
deleteDraft();
emit('posted');
if (postData.text && postData.text !== '') {
- const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
+ const hashtags_ = mfm.parse(postData.text).map(x => x.type === 'hashtag' && x.props.hashtag).filter(x => x) as string[];
const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
}
@@ -847,16 +860,17 @@ function cancel() {
}
function insertMention() {
- os.selectUser().then(user => {
+ os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => {
insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
});
}
async function insertEmoji(ev: MouseEvent) {
textAreaReadOnly.value = true;
-
+ const target = ev.currentTarget ?? ev.target;
+ if (target == null) return;
emojiPicker.show(
- ev.currentTarget ?? ev.target,
+ target as HTMLElement,
emoji => {
insertTextAtCursor(textareaEl.value, emoji);
},
@@ -868,6 +882,7 @@ async function insertEmoji(ev: MouseEvent) {
}
async function insertMfmFunction(ev: MouseEvent) {
+ if (textareaEl.value == null) return;
mfmFunctionPicker(
ev.currentTarget ?? ev.target,
textareaEl.value,
@@ -875,14 +890,15 @@ async function insertMfmFunction(ev: MouseEvent) {
);
}
-function showActions(ev) {
+function showActions(ev: MouseEvent) {
os.popupMenu(postFormActions.map(action => ({
text: action.title,
action: () => {
action.handler({
text: text.value,
cw: cw.value,
- }, (key, value) => {
+ }, (key, value: any) => {
+ if (typeof key !== 'string') return;
if (key === 'text') { text.value = value; }
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
});
@@ -919,9 +935,9 @@ onMounted(() => {
}
// TODO: detach when unmount
- new Autocomplete(textareaEl.value, text);
- new Autocomplete(cwInputEl.value, cw);
- new Autocomplete(hashtagsInputEl.value, hashtags);
+ if (textareaEl.value) new Autocomplete(textareaEl.value, text);
+ if (cwInputEl.value) new Autocomplete(cwInputEl.value, cw);
+ if (hashtagsInputEl.value) new Autocomplete(hashtagsInputEl.value, hashtags);
nextTick(() => {
// 書きかけの投稿を復元
@@ -944,19 +960,19 @@ onMounted(() => {
if (props.initialNote) {
const init = props.initialNote;
text.value = init.text ? init.text : '';
- files.value = init.files;
- cw.value = init.cw;
+ files.value = init.files ?? [];
+ cw.value = init.cw ?? null;
useCw.value = init.cw != null;
if (init.poll) {
poll.value = {
choices: init.poll.choices.map(x => x.text),
multiple: init.poll.multiple,
- expiresAt: init.poll.expiresAt,
- expiredAfter: init.poll.expiredAfter,
+ expiresAt: init.poll.expiresAt ? (new Date(init.poll.expiresAt)).getTime() : null,
+ expiredAfter: null,
};
}
visibility.value = init.visibility;
- localOnly.value = init.localOnly;
+ localOnly.value = init.localOnly ?? false;
quoteId.value = init.renote ? init.renote.id : null;
}
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index 28a09c571f..3f775bc6e2 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -24,6 +24,7 @@ import { defineAsyncComponent, inject } from 'vue';
import * as Misskey from 'misskey-js';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -55,13 +56,30 @@ function detachMedia(id: string) {
}
}
+async function detachAndDeleteMedia(file: Misskey.entities.DriveFile) {
+ if (mock) return;
+
+ detachMedia(file.id);
+
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
+ });
+
+ if (canceled) return;
+
+ os.apiWithDialog('drive/files/delete', {
+ fileId: file.id,
+ });
+}
+
function toggleSensitive(file) {
if (mock) {
emit('changeSensitive', file, !file.isSensitive);
return;
}
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
isSensitive: !file.isSensitive,
}).then(() => {
@@ -75,10 +93,10 @@ async function rename(file) {
const { canceled, result } = await os.inputText({
title: i18n.ts.enterFileName,
default: file.name,
- allowEmpty: false,
+ minLength: 1,
});
if (canceled) return;
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
name: result,
}).then(() => {
@@ -96,7 +114,7 @@ async function describe(file) {
}, {
done: caption => {
let comment = caption.length === 0 ? null : caption;
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
comment: comment,
}).then(() => {
@@ -137,6 +155,13 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
text: i18n.ts.attachCancel,
icon: 'ti ti-circle-x',
action: () => { detachMedia(file.id); },
+ }, {
+ type: 'divider',
+ }, {
+ text: i18n.ts.deleteFile,
+ icon: 'ti ti-trash',
+ danger: true,
+ action: () => { detachAndDeleteMedia(file); },
}], ev.currentTarget ?? ev.target).then(() => menuShowing = false);
menuShowing = true;
}
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index 7734e5a6d1..6331dfed29 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -1,11 +1,11 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModal ref="modal" :preferType="'dialog'" @click="modal.close()" @closed="onModalClosed()">
- <MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
+<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
+ <MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
</MkModal>
</template>
@@ -20,13 +20,13 @@ const props = defineProps<{
renote?: Misskey.entities.Note;
channel?: any; // TODO
mention?: Misskey.entities.User;
- specified?: Misskey.entities.User;
+ specified?: Misskey.entities.UserDetailed;
initialText?: string;
initialCw?: string;
- initialVisibility?: typeof Misskey.noteVisibilities;
+ initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
- initialVisibleUsers?: Misskey.entities.User[];
+ initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note;
instant?: boolean;
fixed?: boolean;
@@ -41,7 +41,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
const form = shallowRef<InstanceType<typeof MkPostForm>>();
function onPosted() {
- modal.value.close({
+ modal.value?.close({
useSendAnimation: true,
});
}
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 54ef117d77..e0d0b561be 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js';
+import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
const SCROLL_STOP = 10;
const MAX_PULL_DISTANCE = Infinity;
@@ -129,7 +130,7 @@ function moveEnd() {
function moving(event: TouchEvent | PointerEvent) {
if (!isPullStart.value || isRefreshing.value || disabled) return;
- if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)) {
+ if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) {
pullDistance.value = 0;
isPullEnd.value = false;
moveEnd();
@@ -148,6 +149,10 @@ function moving(event: TouchEvent | PointerEvent) {
if (event.cancelable) event.preventDefault();
}
+ if (pullDistance.value > SCROLL_STOP) {
+ event.stopPropagation();
+ }
+
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
}
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index ebbd5e6cdc..5e42df4795 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -45,7 +45,8 @@ import { ref } from 'vue';
import { $i, getAccounts } from '@/account.js';
import MkButton from '@/components/MkButton.vue';
import { instance } from '@/instance.js';
-import { api, apiWithDialog, promiseDialog } from '@/os.js';
+import { apiWithDialog, promiseDialog } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
defineProps<{
@@ -82,7 +83,7 @@ function subscribe() {
pushSubscription.value = subscription;
// Register
- pushRegistrationInServer.value = await api('sw/register', {
+ pushRegistrationInServer.value = await misskeyApi('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh')),
@@ -125,7 +126,7 @@ async function unsubscribe() {
}
function encode(buffer: ArrayBuffer | null) {
- return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
+ return btoa(String.fromCharCode.apply(null, buffer ? new Uint8Array(buffer) as any : []));
}
/**
@@ -159,7 +160,7 @@ if (navigator.serviceWorker == null) {
supported.value = true;
if (pushSubscription.value) {
- const res = await api('sw/show-registration', {
+ const res = await misskeyApi('sw/show-registration', {
endpoint: pushSubscription.value.endpoint,
});
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index 2d68557aad..6676e3bf5b 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index d9178f3362..549438f61b 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,6 +18,9 @@ export default defineComponent({
watch(value, () => {
context.emit('update:modelValue', value.value);
});
+ watch(() => props.modelValue, v => {
+ value.value = v;
+ });
if (!context.slots.default) return null;
let options = context.slots.default();
const label = context.slots.label && context.slots.label();
@@ -35,7 +38,7 @@ export default defineComponent({
h('div', {
class: 'body',
}, options.map(option => h(MkRadio, {
- key: option.key,
+ key: option.key as string,
value: option.props?.value,
modelValue: value.value,
'onUpdate:modelValue': _v => value.value = _v,
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 04390c6f0c..15f8128e98 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -43,6 +43,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
(ev: 'update:modelValue', value: number): void;
+ (ev: 'dragEnded', value: number): void;
}>();
const containerEl = shallowRef<HTMLElement>();
@@ -85,7 +86,7 @@ onMounted(() => {
ro = new ResizeObserver((entries, observer) => {
calcThumbPosition();
});
- ro.observe(containerEl.value);
+ if (containerEl.value) ro.observe(containerEl.value);
});
onUnmounted(() => {
@@ -121,7 +122,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
const onDrag = (ev: MouseEvent | TouchEvent) => {
ev.preventDefault();
const containerRect = containerEl.value!.getBoundingClientRect();
- const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
+ const pointerX = 'touches' in ev && ev.touches.length > 0 ? ev.touches[0].clientX : 'clientX' in ev ? ev.clientX : 0;
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2));
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth)));
@@ -143,6 +144,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
// 値が変わってたら通知
if (beforeValue !== finalValue.value) {
emit('update:modelValue', finalValue.value);
+ emit('dragEnded', finalValue.value);
}
};
diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue
index 75eb91e7ad..361e246e9f 100644
--- a/packages/frontend/src/components/MkReactionEffect.vue
+++ b/packages/frontend/src/components/MkReactionEffect.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue
index fdc3bfd23c..59ceab27dc 100644
--- a/packages/frontend/src/components/MkReactionIcon.vue
+++ b/packages/frontend/src/components/MkReactionIcon.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue
index 8527b45347..15409a216a 100644
--- a/packages/frontend/src/components/MkReactionTooltip.vue
+++ b/packages/frontend/src/components/MkReactionTooltip.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index 1b0d8f74a3..3158ba436e 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 250b7b96d5..0dcd8b0ea2 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="_button"
:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
@click="toggleReaction()"
+ @contextmenu.prevent.stop="menu"
>
<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
<span :class="$style.count">{{ count }}</span>
@@ -19,9 +20,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, inject, onMounted, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
+import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import XDetails from '@/components/MkReactionsViewer.details.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import * as os from '@/os.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { $i } from '@/account.js';
import MkReactionEffect from '@/components/MkReactionEffect.vue';
@@ -29,6 +32,8 @@ import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import * as sound from '@/scripts/sound.js';
+import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
+import { customEmojis } from '@/custom-emojis.js';
const props = defineProps<{
reaction: string;
@@ -45,13 +50,19 @@ const emit = defineEmits<{
const buttonEl = shallowRef<HTMLElement>();
-const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
+const isCustomEmoji = computed(() => props.reaction.includes(':'));
+const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
+
+const canToggle = computed(() => {
+ return !props.reaction.match(/@\w/) && $i
+ && (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
+ || !isCustomEmoji.value;
+});
+const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
async function toggleReaction() {
if (!canToggle.value) return;
- // TODO: その絵文字を使う権限があるかどうか確認
-
const oldReaction = props.note.myReaction;
if (oldReaction) {
const confirm = await os.confirm({
@@ -61,7 +72,7 @@ async function toggleReaction() {
if (confirm.canceled) return;
if (oldReaction !== props.reaction) {
- sound.play('reaction');
+ sound.playMisskeySfx('reaction');
}
if (mock) {
@@ -69,25 +80,25 @@ async function toggleReaction() {
return;
}
- os.api('notes/reactions/delete', {
+ misskeyApi('notes/reactions/delete', {
noteId: props.note.id,
}).then(() => {
if (oldReaction !== props.reaction) {
- os.api('notes/reactions/create', {
+ misskeyApi('notes/reactions/create', {
noteId: props.note.id,
reaction: props.reaction,
});
}
});
} else {
- sound.play('reaction');
+ sound.playMisskeySfx('reaction');
if (mock) {
emit('reactionToggled', props.reaction, (props.count + 1));
return;
}
- os.api('notes/reactions/create', {
+ misskeyApi('notes/reactions/create', {
noteId: props.note.id,
reaction: props.reaction,
});
@@ -97,9 +108,24 @@ async function toggleReaction() {
}
}
+async function menu(ev) {
+ if (!canGetInfo.value) return;
+
+ os.popupMenu([{
+ text: i18n.ts.info,
+ icon: 'ti ti-info-circle',
+ action: async () => {
+ os.popup(MkCustomEmojiDetailedDialog, {
+ emoji: await misskeyApiGet('emoji', {
+ name: props.reaction.replace(/:/g, '').replace(/@\./, ''),
+ }),
+ });
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+
function anime() {
- if (document.hidden) return;
- if (!defaultStore.state.animation) return;
+ if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return;
const rect = buttonEl.value.getBoundingClientRect();
const x = rect.left + 16;
@@ -117,7 +143,7 @@ onMounted(() => {
if (!mock) {
useTooltip(buttonEl, async (showing) => {
- const reactions = await os.apiGet('notes/reactions', {
+ const reactions = await misskeyApiGet('notes/reactions', {
noteId: props.note.id,
type: props.reaction,
limit: 10,
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index a14f2512f8..1bd37d842b 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue
index 0ce67e872b..f1050d26e6 100644
--- a/packages/frontend/src/components/MkRemoteCaution.vue
+++ b/packages/frontend/src/components/MkRemoteCaution.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index e69aa1be80..64b573c4d3 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, nextTick, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { alpha } from '@/scripts/color.js';
@@ -23,10 +23,9 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
-const rootEl = shallowRef<HTMLDivElement>(null);
-const chartEl = shallowRef<HTMLCanvasElement>(null);
-const now = new Date();
-let chartInstance: Chart = null;
+const rootEl = shallowRef<HTMLDivElement | null>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
+let chartInstance: Chart | null = null;
const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({
@@ -34,6 +33,7 @@ const { handler: externalTooltipHandler } = useChartTooltip({
});
async function renderChart() {
+ if (rootEl.value == null) return;
if (chartInstance) {
chartInstance.destroy();
}
@@ -43,11 +43,16 @@ async function renderChart() {
const maxDays = wide ? 10 : narrow ? 5 : 7;
- let raw = await os.api('retention', { });
+ let raw = await misskeyApi('retention', { });
raw = raw.slice(0, maxDays + 1);
- const data = [];
+ const data: {
+ x: number;
+ y: string;
+ v: number;
+ }[] = [];
+
for (const record of raw) {
data.push({
x: 0,
@@ -83,19 +88,20 @@ async function renderChart() {
const marginEachCell = 12;
+ if (chartEl.value == null) return;
+
chartInstance = new Chart(chartEl.value, {
type: 'matrix',
data: {
datasets: [{
label: 'Active',
- data: data,
- pointRadius: 0,
+ data: data as any,
borderWidth: 0,
- borderJoinStyle: 'round',
borderRadius: 3,
backgroundColor(c) {
- const value = c.dataset.data[c.dataIndex].v;
- const m = max(c.dataset.data[c.dataIndex].y);
+ const v = c.dataset.data[c.dataIndex] as unknown as typeof data[0];
+ const value = v.v;
+ const m = max(v.y);
if (m === 0) {
return alpha(color, 0);
} else {
@@ -103,7 +109,6 @@ async function renderChart() {
return alpha(color, a);
}
},
- fill: true,
width(c) {
const a = c.chart.chartArea ?? {};
return (a.right - a.left) / maxDays - marginEachCell;
@@ -146,7 +151,6 @@ async function renderChart() {
},
y: {
type: 'time',
- min: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() - maxDays),
offset: true,
reverse: true,
position: 'left',
@@ -179,7 +183,7 @@ async function renderChart() {
return getYYYYMMDD(new Date(new Date(v.y).getTime() + (v.x * 86400000)));
},
label(context) {
- const v = context.dataset.data[context.dataIndex];
+ const v = context.dataset.data[context.dataIndex] as unknown as typeof data[0];
const m = max(v.y);
if (m === 0) {
return [`Active: ${v.v} (-%)`];
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index e2682ec06b..c3daa9c9a4 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,15 +16,15 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { alpha } from '@/scripts/color.js';
import { initChart } from '@/scripts/init-chart.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
initChart();
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const { handler: externalTooltipHandler } = useChartTooltip();
-let chartInstance: Chart;
+let chartInstance: Chart | null = null;
const getYYYYMMDD = (date: Date) => {
const y = date.getFullYear().toString().padStart(2, '0');
@@ -40,13 +40,15 @@ const getDate = (ymd: string) => {
};
onMounted(async () => {
- let raw = await os.api('retention', { });
+ let raw = await misskeyApi('retention', { });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
const color = accent.toHex();
+ if (chartEl.value == null) return;
+
chartInstance = new Chart(chartEl.value, {
type: 'line',
data: {
@@ -67,7 +69,7 @@ onMounted(async () => {
x: (i + 1).toString(),
y: (v / record.users) * 100,
d: getYYYYMMDD(new Date(record.createdAt)),
- }))],
+ }))] as any,
})),
},
options: {
@@ -109,11 +111,11 @@ onMounted(async () => {
enabled: false,
callbacks: {
title(context) {
- const v = context[0].dataset.data[context[0].dataIndex];
+ const v = context[0].dataset.data[context[0].dataIndex] as unknown as { x: string, y: number, d: string };
return `${v.x} days later`;
},
label(context) {
- const v = context.dataset.data[context.dataIndex];
+ const v = context.dataset.data[context.dataIndex] as unknown as { x: string, y: number, d: string };
const p = Math.round(v.y) + '%';
return `${v.d} ${p}`;
},
diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue
index 860b083327..ee5bb73ebf 100644
--- a/packages/frontend/src/components/MkRippleEffect.vue
+++ b/packages/frontend/src/components/MkRippleEffect.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -77,7 +77,14 @@ const emit = defineEmits<{
(ev: 'end'): void;
}>();
-const particles = [];
+const particles: {
+ size: number;
+ xA: number;
+ yA: number;
+ xB: number;
+ yB: number;
+ color: string;
+}[] = [];
const origin = 64;
const colors = ['#FF1493', '#00FFFF', '#FFE202'];
const zIndex = os.claimZIndex('high');
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index 4b6b0940ba..c1b922198f 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 33b8a9a86d..358d9b1f4b 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -27,16 +27,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.caption"><slot name="caption"></slot></div>
- <MkButton v-if="manualSave && changed" primary @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton v-if="manualSave && changed" primary :class="$style.save" @click="updated"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</template>
<script lang="ts" setup>
-import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots } from 'vue';
+import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
+import { MenuItem } from '@/types/menu.js';
const props = defineProps<{
modelValue: string | null;
@@ -52,7 +53,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
- (ev: 'change', _ev: KeyboardEvent): void;
+ (ev: 'changeByUser', value: string | null): void;
(ev: 'update:modelValue', value: string | null): void;
}>();
@@ -74,10 +75,9 @@ const height =
props.large ? 39 :
36;
-const focus = () => inputEl.value.focus();
+const focus = () => inputEl.value?.focus();
const onInput = (ev) => {
changed.value = true;
- emit('change', ev);
};
const updated = () => {
@@ -89,17 +89,19 @@ watch(modelValue, newValue => {
v.value = newValue;
});
-watch(v, newValue => {
+watch(v, () => {
if (!props.manualSave) {
updated();
}
- invalid.value = inputEl.value.validity.badInput;
+ invalid.value = inputEl.value?.validity.badInput ?? true;
});
// このコンポーネントが作成された時、非表示状態である場合がある
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
useInterval(() => {
+ if (inputEl.value == null) return;
+
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@@ -123,35 +125,38 @@ onMounted(() => {
});
});
-function show(ev: MouseEvent) {
+function show() {
focused.value = true;
opening.value = true;
- const menu = [];
+ const menu: MenuItem[] = [];
let options = slots.default!();
const pushOption = (option: VNode) => {
menu.push({
- text: option.children,
- active: computed(() => v.value === option.props.value),
+ text: option.children as string,
+ active: computed(() => v.value === option.props?.value),
action: () => {
- v.value = option.props.value;
+ v.value = option.props?.value;
+ changed.value = true;
+ emit('changeByUser', v.value);
},
});
};
- const scanOptions = (options: VNode[]) => {
+ const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) {
+ if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
if (vnode.type === 'optgroup') {
const optgroup = vnode;
menu.push({
type: 'label',
- text: optgroup.props.label,
+ text: optgroup.props?.label,
});
- scanOptions(optgroup.children);
+ if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
const fragment = vnode;
- scanOptions(fragment.children);
+ if (Array.isArray(fragment.children)) scanOptions(fragment.children);
} else if (vnode.props == null) { // v-if で条件が false のときにこうなる
// nop?
} else {
@@ -164,7 +169,7 @@ function show(ev: MouseEvent) {
scanOptions(options);
os.popupMenu(menu, container.value, {
- width: container.value.offsetWidth,
+ width: container.value?.offsetWidth,
onClosing: () => {
opening.value = false;
},
@@ -284,6 +289,10 @@ function show(ev: MouseEvent) {
padding-left: 6px;
}
+.save {
+ margin: 8px 0 0 0;
+}
+
.chevron {
transition: transform 0.1s ease-out;
}
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 2fc2c9ec5e..852af01b5a 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -59,6 +59,7 @@ import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
import { host as configHost } from '@/config.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
@@ -95,7 +96,7 @@ const props = defineProps({
});
function onUsernameChange(): void {
- os.api('users/show', {
+ misskeyApi('users/show', {
username: username.value,
}).then(userResponse => {
user.value = userResponse;
@@ -111,6 +112,7 @@ function onLogin(res: any): Promise<void> | void {
}
async function queryKey(): Promise<void> {
+ if (credentialRequest.value == null) return;
queryingKey.value = true;
await webAuthnRequest(credentialRequest.value)
.catch(() => {
@@ -120,7 +122,7 @@ async function queryKey(): Promise<void> {
credentialRequest.value = null;
queryingKey.value = false;
signing.value = true;
- return os.api('signin', {
+ return misskeyApi('signin', {
username: username.value,
password: password.value,
credential: credential.toJSON(),
@@ -142,7 +144,7 @@ function onSubmit(): void {
signing.value = true;
if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
if (webAuthnSupported() && user.value.securityKeys) {
- os.api('signin', {
+ misskeyApi('signin', {
username: username.value,
password: password.value,
}).then(res => {
@@ -159,7 +161,7 @@ function onSubmit(): void {
signing.value = false;
}
} else {
- os.api('signin', {
+ misskeyApi('signin', {
username: username.value,
password: password.value,
token: user.value?.twoFactorEnabled ? token.value : undefined,
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 6f961cff05..33355bb99e 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 44cfb6f0fa..5f08e416c1 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -63,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkInput>
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+ <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
@@ -79,11 +80,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { toUnicode } from 'punycode/';
+import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
import * as config from '@/config.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
@@ -95,7 +98,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'signup', user: Record<string, any>): void;
+ (ev: 'signup', user: Misskey.entities.SigninResponse): void;
(ev: 'signupEmailPending'): void;
}>();
@@ -116,6 +119,7 @@ const passwordStrength = ref<'' | 'low' | 'medium' | 'high'>('');
const passwordRetypeState = ref<null | 'match' | 'not-match'>(null);
const submitting = ref<boolean>(false);
const hCaptchaResponse = ref<string | null>(null);
+const mCaptchaResponse = ref<string | null>(null);
const reCaptchaResponse = ref<string | null>(null);
const turnstileResponse = ref<string | null>(null);
const usernameAbortController = ref<null | AbortController>(null);
@@ -124,6 +128,7 @@ const emailAbortController = ref<null | AbortController>(null);
const shouldDisableSubmitting = computed((): boolean => {
return submitting.value ||
instance.enableHcaptcha && !hCaptchaResponse.value ||
+ instance.enableMcaptcha && !mCaptchaResponse.value ||
instance.enableRecaptcha && !reCaptchaResponse.value ||
instance.enableTurnstile && !turnstileResponse.value ||
instance.emailRequiredForSignup && emailState.value !== 'ok' ||
@@ -180,7 +185,7 @@ function onChangeUsername(): void {
usernameState.value = 'wait';
usernameAbortController.value = new AbortController();
- os.api('username/available', {
+ misskeyApi('username/available', {
username: username.value,
}, undefined, usernameAbortController.value.signal).then(result => {
usernameState.value = result.available ? 'ok' : 'unavailable';
@@ -203,7 +208,7 @@ function onChangeEmail(): void {
emailState.value = 'wait';
emailAbortController.value = new AbortController();
- os.api('email-address/available', {
+ misskeyApi('email-address/available', {
emailAddress: email.value,
}, undefined, emailAbortController.value.signal).then(result => {
emailState.value = result.available ? 'ok' :
@@ -245,12 +250,13 @@ async function onSubmit(): Promise<void> {
submitting.value = true;
try {
- await os.api('signup', {
+ await misskeyApi('signup', {
username: username.value,
password: password.value,
emailAddress: email.value,
invitationCode: invitationCode.value,
'hcaptcha-response': hCaptchaResponse.value,
+ 'm-captcha-response': mCaptchaResponse.value,
'g-recaptcha-response': reCaptchaResponse.value,
'turnstile-response': turnstileResponse.value,
});
@@ -258,11 +264,11 @@ async function onSubmit(): Promise<void> {
os.alert({
type: 'success',
title: i18n.ts._signup.almostThere,
- text: i18n.t('_signup.emailSent', { email: email.value }),
+ text: i18n.tsx._signup.emailSent({ email: email.value }),
});
emit('signupEmailPending');
} else {
- const res = await os.api('signin', {
+ const res = await misskeyApi('signin', {
username: username.value,
password: password.value,
});
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts b/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
index ab26df6342..fcd1ffde3e 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
+++ b/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
@@ -1,11 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { expect } from '@storybook/jest';
-import { userEvent, waitFor, within } from '@storybook/testing-library';
+import { expect, userEvent, waitFor, within } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import { onBeforeUnmount } from 'vue';
import MkSignupServerRules from './MkSignupDialog.rules.vue';
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 8cf7ce92ad..59a3651cd4 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -34,8 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ tosPrivacyPolicyLabel }}</template>
<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
<div class="_gaps_s">
- <div v-if="availableTos"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
- <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
+ <div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
+ <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
</div>
<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
@@ -96,7 +96,7 @@ const tosPrivacyPolicyLabel = computed(() => {
} else if (availablePrivacyPolicy) {
return i18n.ts.privacyPolicy;
} else {
- return "";
+ return '';
}
});
@@ -105,7 +105,7 @@ async function updateAgreeServerRules(v: boolean) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts.doYouAgree,
- text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.serverRules }),
+ text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.serverRules }),
});
if (confirm.canceled) return;
agreeServerRules.value = true;
@@ -119,7 +119,7 @@ async function updateAgreeTosAndPrivacyPolicy(v: boolean) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts.doYouAgree,
- text: i18n.t('iHaveReadXCarefullyAndAgree', {
+ text: i18n.tsx.iHaveReadXCarefullyAndAgree({
x: tosPrivacyPolicyLabel.value,
}),
});
@@ -135,7 +135,7 @@ async function updateAgreeNote(v: boolean) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts.doYouAgree,
- text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.basicNotesBeforeCreateAccount }),
+ text: i18n.tsx.iHaveReadXCarefullyAndAgree({ x: i18n.ts.basicNotesBeforeCreateAccount }),
});
if (confirm.canceled) return;
agreeNote.value = true;
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index b4fba114a6..97310d32a6 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog"
:width="500"
:height="600"
- @close="dialog.close()"
+ @close="dialog?.close()"
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.signup }}</template>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="!isAcceptedServerRule">
- <XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/>
+ <XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
</template>
<template v-else>
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { shallowRef, ref } from 'vue';
-
+import * as Misskey from 'misskey-js';
import XSignup from '@/components/MkSignupDialog.form.vue';
import XServerRules from '@/components/MkSignupDialog.rules.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done'): void;
+ (ev: 'done', res: Misskey.entities.SigninResponse): void;
(ev: 'closed'): void;
}>();
@@ -55,13 +55,13 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const isAcceptedServerRule = ref(false);
-function onSignup(res) {
+function onSignup(res: Misskey.entities.SigninResponse) {
emit('done', res);
- dialog.value.close();
+ dialog.value?.close();
}
function onSignupEmailPending() {
- dialog.value.close();
+ dialog.value?.close();
}
</script>
diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
new file mode 100644
index 0000000000..80f3a6709c
--- /dev/null
+++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
@@ -0,0 +1,112 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_panel _shadow" :class="$style.root">
+ <div :class="$style.icon">
+ <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-open-source" width="40" height="40" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
+ <path d="M12 3a9 9 0 0 1 3.618 17.243l-2.193 -5.602a3 3 0 1 0 -2.849 0l-2.193 5.603a9 9 0 0 1 3.617 -17.244z"/>
+ </svg>
+ </div>
+ <div :class="$style.main">
+ <div :class="$style.title">
+ <I18n :src="i18n.ts.aboutX" tag="span">
+ <template #x>
+ {{ instance.name ?? host }}
+ </template>
+ </I18n>
+ </div>
+ <div :class="$style.text">
+ <I18n :src="i18n.ts._aboutMisskey.thisIsModifiedVersion" tag="span">
+ <template #name>
+ {{ instance.name ?? host }}
+ </template>
+ </I18n>
+ <I18n :src="i18n.ts.correspondingSourceIsAvailable" tag="span">
+ <template #anchor>
+ <MkA to="/about-misskey" class="_link">{{ i18n.ts.aboutMisskey }}</MkA>
+ </template>
+ </I18n>
+ </div>
+ <div class="_buttons">
+ <MkButton @click="close">{{ i18n.ts.gotIt }}</MkButton>
+ </div>
+ </div>
+ <button class="_button" :class="$style.close" @click="close"><i class="ti ti-x"></i></button>
+</div>
+</template>
+
+<script lang="ts" setup>
+import MkButton from '@/components/MkButton.vue';
+import { host } from '@/config.js';
+import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
+import { miLocalStorage } from '@/local-storage.js';
+import * as os from '@/os.js';
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+
+const zIndex = os.claimZIndex('low');
+
+function close() {
+ miLocalStorage.setItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read', 'true');
+ emit('closed');
+}
+</script>
+
+<style lang="scss" module>
+.root {
+ position: fixed;
+ z-index: v-bind(zIndex);
+ bottom: var(--margin);
+ left: 0;
+ right: 0;
+ margin: auto;
+ box-sizing: border-box;
+ width: calc(100% - (var(--margin) * 2));
+ max-width: 500px;
+ display: flex;
+}
+
+.icon {
+ text-align: center;
+ padding-top: 25px;
+ width: 100px;
+ color: var(--accent);
+}
+@media (max-width: 500px) {
+ .icon {
+ width: 80px;
+ }
+}
+@media (max-width: 450px) {
+ .icon {
+ width: 70px;
+ }
+}
+
+.main {
+ padding: 25px 25px 25px 0;
+ flex: 1;
+}
+
+.close {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ padding: 8px;
+}
+
+.title {
+ font-weight: bold;
+}
+
+.text {
+ margin: 0.7em 0 1em 0;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSparkle.vue b/packages/frontend/src/components/MkSparkle.vue
index 269825e25e..8491ce2f84 100644
--- a/packages/frontend/src/components/MkSparkle.vue
+++ b/packages/frontend/src/components/MkSparkle.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -89,10 +89,11 @@ let ro: ResizeObserver | undefined;
onMounted(() => {
ro = new ResizeObserver((entries, observer) => {
- width.value = el.value?.offsetWidth + 64;
- height.value = el.value?.offsetHeight + 64;
+ if (el.value == null) return;
+ width.value = el.value.offsetWidth + 64;
+ height.value = el.value.offsetHeight + 64;
});
- ro.observe(el.value);
+ if (el.value) ro.observe(el.value);
const add = () => {
if (stop) return;
const x = (Math.random() * (width.value - 64));
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 438140649e..9a07826f1a 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,18 +7,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
<div>
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
- <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
+ <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
- <details v-if="note.files.length > 0">
- <summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
+ <details v-if="note.files && note.files.length > 0">
+ <summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
<MkMediaList :mediaList="note.files"/>
</details>
<details v-if="note.poll">
<summary>{{ i18n.ts.poll }}</summary>
- <MkPoll :note="note"/>
+ <MkPoll :noteId="note.id" :poll="note.poll"/>
</details>
<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index efd5665396..3023f63e5d 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue
index a7e91acc39..c95c933663 100644
--- a/packages/frontend/src/components/MkSwitch.button.vue
+++ b/packages/frontend/src/components/MkSwitch.button.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -24,7 +24,7 @@ import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
checked: boolean | Ref<boolean>;
- disabled?: boolean;
+ disabled?: boolean | Ref<boolean>;
}>(), {
disabled: false,
});
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index 2e2c0e15a2..a19b45448b 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index 9785d89403..f2d0c95013 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,18 +13,18 @@ export default defineComponent({
},
},
setup(props, { emit, slots }) {
- const options = slots.default();
+ const options = slots.default?.() ?? [];
return () => h('div', {
class: 'pxhvhrfw',
}, options.map(option => withDirectives(h('button', {
- class: ['_button', { active: props.modelValue === option.props.value }],
- key: option.key,
- disabled: props.modelValue === option.props.value,
+ class: ['_button', { active: props.modelValue === option.props?.value }],
+ key: option.key as string,
+ disabled: props.modelValue === option.props?.value,
onClick: () => {
- emit('update:modelValue', option.props.value);
+ emit('update:modelValue', option.props?.value);
},
- }, option.children), [
+ }, option.children ?? []), [
[resolveDirective('click-anime')],
])));
},
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index 083c34906f..6b9c181597 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -52,7 +52,7 @@ watch(available, () => {
});
onMounted(() => {
- width.value = rootEl.value.offsetWidth;
+ if (rootEl.value) width.value = rootEl.value.offsetWidth;
if (loaded) {
available.value = true;
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 3ee374300a..59490c552a 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
- :autocomplete="props.autocomplete"
+ :autocomplete="autocomplete"
:spellcheck="spellcheck"
@focus="focused = true"
@blur="focused = false"
@@ -76,9 +76,9 @@ const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = shallowRef<HTMLTextAreaElement>();
const preview = ref(false);
-let autocomplete: Autocomplete;
+let autocompleteWorker: Autocomplete | null = null;
-const focus = () => inputEl.value.focus();
+const focus = () => inputEl.value?.focus();
const onInput = (ev) => {
changed.value = true;
emit('change', ev);
@@ -111,10 +111,10 @@ const updated = () => {
const debouncedUpdated = debounce(1000, updated);
watch(modelValue, newValue => {
- v.value = newValue;
+ v.value = newValue ?? '';
});
-watch(v, newValue => {
+watch(v, () => {
if (!props.manualSave) {
if (props.debounce) {
debouncedUpdated();
@@ -123,7 +123,7 @@ watch(v, newValue => {
}
}
- invalid.value = inputEl.value.validity.badInput;
+ invalid.value = inputEl.value?.validity.badInput ?? true;
});
onMounted(() => {
@@ -133,14 +133,14 @@ onMounted(() => {
}
});
- if (props.mfmAutocomplete) {
- autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
+ if (props.mfmAutocomplete && inputEl.value) {
+ autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete);
}
});
onUnmounted(() => {
- if (autocomplete) {
- autocomplete.detach();
+ if (autocompleteWorker) {
+ autocompleteWorker.detach();
}
});
</script>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 23afb922f3..03dccb18e9 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -11,14 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
:pagination="paginationQuery"
:noGap="!defaultStore.state.showGapBetweenNotesInTimeline"
@queue="emit('queue', $event)"
- @status="prComponent.setDisabled($event)"
+ @status="prComponent?.setDisabled($event)"
/>
</MkPullToRefresh>
</template>
<script lang="ts" setup>
-import { computed, watch, onUnmounted, provide, ref } from 'vue';
-import { Connection } from 'misskey-js/built/streaming.js';
+import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
+import * as Misskey from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js';
@@ -29,7 +29,7 @@ import { defaultStore } from '@/store.js';
import { Paging } from '@/components/MkPagination.vue';
const props = withDefaults(defineProps<{
- src: string;
+ src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
list?: string;
antenna?: string;
channel?: string;
@@ -49,6 +49,7 @@ const emit = defineEmits<{
(ev: 'queue', count: number): void;
}>();
+provide('inTimeline', true);
provide('inChannel', computed(() => props.src === 'channel'));
type TimelineQueryType = {
@@ -62,12 +63,14 @@ type TimelineQueryType = {
roleId?: string
}
-const prComponent = ref<InstanceType<typeof MkPullToRefresh>>();
-const tlComponent = ref<InstanceType<typeof MkNotes>>();
+const prComponent = shallowRef<InstanceType<typeof MkPullToRefresh>>();
+const tlComponent = shallowRef<InstanceType<typeof MkNotes>>();
let tlNotesCount = 0;
-const prepend = note => {
+function prepend(note) {
+ if (tlComponent.value == null) return;
+
tlNotesCount++;
if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) {
@@ -79,18 +82,19 @@ const prepend = note => {
emit('note');
if (props.sound) {
- sound.play($i && (note.userId === $i.id) ? 'noteMy' : 'note');
+ sound.playMisskeySfx($i && (note.userId === $i.id) ? 'noteMy' : 'note');
}
-};
+}
-let connection: Connection;
-let connection2: Connection;
+let connection: Misskey.ChannelConnection | null = null;
+let connection2: Misskey.ChannelConnection | null = null;
let paginationQuery: Paging | null = null;
const stream = useStream();
function connectChannel() {
if (props.src === 'antenna') {
+ if (props.antenna == null) return;
connection = stream.useChannel('antenna', {
antennaId: props.antenna,
});
@@ -129,20 +133,24 @@ function connectChannel() {
connection = stream.useChannel('main');
connection.on('mention', onNote);
} else if (props.src === 'list') {
+ if (props.list == null) return;
connection = stream.useChannel('userList', {
+ withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
listId: props.list,
});
} else if (props.src === 'channel') {
+ if (props.channel == null) return;
connection = stream.useChannel('channel', {
channelId: props.channel,
});
} else if (props.src === 'role') {
+ if (props.role == null) return;
connection = stream.useChannel('roleTimeline', {
roleId: props.role,
});
}
- if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend);
+ if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
}
function disconnectChannel() {
@@ -151,7 +159,7 @@ function disconnectChannel() {
}
function updatePaginationQuery() {
- let endpoint: string | null;
+ let endpoint: keyof Misskey.Endpoints | null;
let query: TimelineQueryType | null;
if (props.src === 'antenna') {
@@ -196,6 +204,7 @@ function updatePaginationQuery() {
} else if (props.src === 'list') {
endpoint = 'notes/user-list-timeline';
query = {
+ withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined,
listId: props.list,
};
@@ -234,8 +243,9 @@ function refreshEndpointAndChannel() {
updatePaginationQuery();
}
+// デッキのリストカラムでwithRenotesを変更した場合に自動的に更新されるようにさせる
// IDが切り替わったら切り替え先のTLを表示させたい
-watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel);
+watch(() => [props.list, props.antenna, props.channel, props.role, props.withRenotes], refreshEndpointAndChannel);
// 初回表示用
refreshEndpointAndChannel();
@@ -246,6 +256,8 @@ onUnmounted(() => {
function reloadTimeline() {
return new Promise<void>((res) => {
+ if (tlComponent.value == null) return;
+
tlNotesCount = 0;
tlComponent.value.pagingComponent?.reload().then(() => {
diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue
index 0446f9196a..e256640649 100644
--- a/packages/frontend/src/components/MkToast.vue
+++ b/packages/frontend/src/components/MkToast.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 8e8e26ed5f..b32066c950 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:withOkButton="true"
:okButtonDisabled="false"
:canClose="false"
- @close="dialog.close()"
+ @close="dialog?.close()"
@closed="$emit('closed')"
@ok="ok()"
>
@@ -33,7 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton inline @click="enableAll">{{ i18n.ts.enableAll }}</MkButton>
</div>
<div class="_gaps_s">
- <MkSwitch v-for="kind in Object.keys(permissions)" :key="kind" v-model="permissions[kind]">{{ i18n.t(`_permissions.${kind}`) }}</MkSwitch>
+ <MkSwitch v-for="kind in Object.keys(permissionSwitches)" :key="kind" v-model="permissionSwitches[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
+ </div>
+ <div v-if="iAmAdmin" :class="$style.adminPermissions">
+ <div :class="$style.adminPermissionsHeader"><b>{{ i18n.ts.adminPermission }}</b></div>
+ <div class="_gaps_s">
+ <MkSwitch v-for="kind in Object.keys(permissionSwitchesForAdmin)" :key="kind" v-model="permissionSwitchesForAdmin[kind]">{{ i18n.ts._permissions[kind] }}</MkSwitch>
+ </div>
</div>
</div>
</MkSpacer>
@@ -49,6 +55,7 @@ import MkButton from './MkButton.vue';
import MkInfo from './MkInfo.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
+import { iAmAdmin } from '@/account.js';
const props = withDefaults(defineProps<{
title?: string | null;
@@ -68,37 +75,76 @@ const emit = defineEmits<{
}>();
const defaultPermissions = Misskey.permissions.filter(p => !p.startsWith('read:admin') && !p.startsWith('write:admin'));
+const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admin') || p.startsWith('write:admin'));
+
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const name = ref(props.initialName);
-const permissions = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
+const permissionSwitches = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
+const permissionSwitchesForAdmin = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
if (props.initialPermissions) {
for (const kind of props.initialPermissions) {
- permissions.value[kind] = true;
+ permissionSwitches.value[kind] = true;
}
} else {
for (const kind of defaultPermissions) {
- permissions.value[kind] = false;
+ permissionSwitches.value[kind] = false;
+ }
+
+ if (iAmAdmin) {
+ for (const kind of adminPermissions) {
+ permissionSwitchesForAdmin.value[kind] = false;
+ }
}
}
function ok(): void {
emit('done', {
name: name.value,
- permissions: Object.keys(permissions.value).filter(p => permissions.value[p]),
+ permissions: [
+ ...Object.keys(permissionSwitches.value).filter(p => permissionSwitches.value[p]),
+ ...(iAmAdmin ? Object.keys(permissionSwitchesForAdmin.value).filter(p => permissionSwitchesForAdmin.value[p]) : []),
+ ],
});
- dialog.value.close();
+ dialog.value?.close();
}
function disableAll(): void {
- for (const p in permissions.value) {
- permissions.value[p] = false;
+ for (const p in permissionSwitches.value) {
+ permissionSwitches.value[p] = false;
+ }
+ if (iAmAdmin) {
+ for (const p in permissionSwitchesForAdmin.value) {
+ permissionSwitchesForAdmin.value[p] = false;
+ }
}
}
function enableAll(): void {
- for (const p in permissions.value) {
- permissions.value[p] = true;
+ for (const p in permissionSwitches.value) {
+ permissionSwitches.value[p] = true;
+ }
+ if (iAmAdmin) {
+ for (const p in permissionSwitchesForAdmin.value) {
+ permissionSwitchesForAdmin.value[p] = true;
+ }
}
}
</script>
+
+<style module lang="scss">
+.adminPermissions {
+ margin: 8px -6px 0;
+ padding: 24px 6px 6px;
+ border: 2px solid var(--error);
+ border-radius: calc(var(--radius) / 2);
+}
+
+.adminPermissionsHeader {
+ margin: -34px 0 6px 12px;
+ padding: 0 4px;
+ width: fit-content;
+ color: var(--error);
+ background: var(--panel);
+}
+</style>
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index d21c6317aa..a3620aab68 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,8 +13,10 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>
- <Mfm v-if="asMfm" :text="text"/>
- <span v-else>{{ text }}</span>
+ <template v-if="text">
+ <Mfm v-if="asMfm" :text="text"/>
+ <span v-else>{{ text }}</span>
+ </template>
</slot>
</div>
</Transition>
@@ -53,6 +55,7 @@ const el = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex('high');
function setPosition() {
+ if (el.value == null) return;
const data = calcPopupPosition(el.value, {
anchorElement: props.targetElement,
direction: props.direction,
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index c7df1a576e..f03a83293b 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="phase === 'howToReact'" class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
- <MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction" @updateReaction="updateReaction"/>
+ <MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
</div>
</template>
@@ -53,7 +53,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
isBot: false,
isCat: true,
emojis: {},
- onlineStatus: null,
+ onlineStatus: 'unknown',
badgeRoles: [],
},
text: 'just setting up my msky',
@@ -86,7 +86,6 @@ function doNotification(emoji: string): void {
const notification: Misskey.entities.Notification = {
id: Math.random().toString(),
createdAt: new Date().toUTCString(),
- isRead: false,
type: 'reaction',
reaction: emoji,
user: $i,
diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
index b395f64853..2b8c586dac 100644
--- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -58,7 +58,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
isBot: false,
isCat: true,
emojis: {},
- onlineStatus: null,
+ onlineStatus: 'unknown',
badgeRoles: [],
},
text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note,
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index 896db5eb3a..b17ec66461 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -40,7 +40,7 @@ const emit = defineEmits<{
const onceSucceeded = ref<boolean>(false);
function doSucceeded(fileId: string, to: boolean) {
- if (fileId === exampleNote.fileIds[0] && to) {
+ if (fileId === exampleNote.fileIds?.[0] && to) {
onceSucceeded.value = true;
emit('succeeded');
}
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index 93181cf2b1..57f26e86a7 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 963e78a1ff..d2711e4ec5 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
- <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
+ <div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index 391733931a..188cc37f41 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -1,15 +1,15 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModal ref="modal" :zPriority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
+<MkModal ref="modal" :zPriority="'middle'" @click="modal?.close()" @closed="$emit('closed')">
<div :class="$style.root">
<div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
<div :class="$style.version">✨{{ version }}🚀</div>
<MkButton full @click="whatIsNew">{{ i18n.ts.whatIsNew }}</MkButton>
- <MkButton :class="$style.gotIt" primary full @click="$refs.modal.close()">{{ i18n.ts.gotIt }}</MkButton>
+ <MkButton :class="$style.gotIt" primary full @click="modal?.close()">{{ i18n.ts.gotIt }}</MkButton>
</div>
</MkModal>
</template>
@@ -25,10 +25,10 @@ import { confetti } from '@/scripts/confetti.js';
const modal = shallowRef<InstanceType<typeof MkModal>>();
-const whatIsNew = () => {
- modal.value.close();
+function whatIsNew() {
+ modal.value?.close();
window.open(`https://misskey-hub.net/docs/releases/#_${version.replace(/\./g, '')}`, '_blank');
-};
+}
onMounted(() => {
confetti({
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index f0f1a13d0b..efc58b7e29 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
scrolling="no"
- :allow="player.allow.join(';')"
+ :allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
:class="$style.playerIframe"
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
:style="{ border: 0 }"
@@ -83,8 +83,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { defineAsyncComponent, onUnmounted, ref } from 'vue';
-import type { summaly } from 'summaly';
+import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
+import type { summaly } from '@misskey-dev/summaly';
import { url as local } from '@/config.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
@@ -131,6 +131,10 @@ const embedId = `embed${Math.random().toString().replace(/\D/, '')}`;
const tweetHeight = ref(150);
const unknownUrl = ref(false);
+onDeactivated(() => {
+ playerEnabled.value = false;
+});
+
const requestUrl = new URL(props.url);
if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue
index 81c383540c..cf75064be7 100644
--- a/packages/frontend/src/components/MkUrlPreviewPopup.vue
+++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index b5489d8e59..3c5f563aa0 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModalWindow
ref="dialog"
:width="400"
- @close="dialog.close()"
+ @close="dialog?.close()"
@closed="$emit('closed')"
>
<template v-if="announcement" #header>:{{ announcement.title }}:</template>
@@ -56,6 +56,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -63,14 +64,14 @@ import MkRadios from '@/components/MkRadios.vue';
const props = defineProps<{
user: Misskey.entities.User,
- announcement?: any,
+ announcement?: Misskey.entities.Announcement,
}>();
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
-const title = ref<string>(props.announcement ? props.announcement.title : '');
-const text = ref<string>(props.announcement ? props.announcement.text : '');
-const icon = ref<string>(props.announcement ? props.announcement.icon : 'info');
-const display = ref<string>(props.announcement ? props.announcement.display : 'dialog');
+const title = ref(props.announcement ? props.announcement.title : '');
+const text = ref(props.announcement ? props.announcement.text : '');
+const icon = ref(props.announcement ? props.announcement.icon : 'info');
+const display = ref(props.announcement ? props.announcement.display : 'dialog');
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
const emit = defineEmits<{
@@ -91,18 +92,18 @@ async function done() {
if (props.announcement) {
await os.apiWithDialog('admin/announcements/update', {
- id: props.announcement.id,
...params,
+ id: props.announcement.id,
});
emit('done', {
updated: {
- id: props.announcement.id,
...params,
+ id: props.announcement.id,
},
});
- dialog.value.close();
+ dialog.value?.close();
} else {
const created = await os.apiWithDialog('admin/announcements/create', params);
@@ -110,25 +111,27 @@ async function done() {
created: created,
});
- dialog.value.close();
+ dialog.value?.close();
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: title.value }),
+ text: i18n.tsx.removeAreYouSure({ x: title.value }),
});
if (canceled) return;
- os.api('admin/announcements/delete', {
- id: props.announcement.id,
- }).then(() => {
- emit('done', {
- deleted: true,
+ if (props.announcement) {
+ await misskeyApi('admin/announcements/delete', {
+ id: props.announcement.id,
});
- dialog.value.close();
+ }
+
+ emit('done', {
+ deleted: true,
});
+ dialog.value?.close();
}
</script>
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index 75288aac02..d3e77b2818 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -1,16 +1,16 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, red: user.isSuspended, gray: false }]">
- <MkAvatar class="avatar" :user="user" indicator/>
- <div class="body">
- <span class="name"><MkUserName class="name" :user="user"/></span>
- <span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
+<div v-adaptive-bg :class="[$style.root]">
+ <MkAvatar :class="$style.avatar" :user="user" indicator/>
+ <div :class="$style.body">
+ <span :class="$style.name"><MkUserName :user="user"/></span>
+ <span :class="$style.sub"><span class="_monospace">@{{ acct(user) }}</span></span>
</div>
- <MkMiniChart v-if="chartValues" class="chart" :src="chartValues"/>
+ <MkMiniChart v-if="chartValues" :class="$style.chart" :src="chartValues"/>
</div>
</template>
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'misskey-js';
import { onMounted, ref } from 'vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
-import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { acct } from '@/filters/user.js';
const props = withDefaults(defineProps<{
@@ -32,7 +32,7 @@ const chartValues = ref<number[] | null>(null);
onMounted(() => {
if (props.withChart) {
- os.apiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => {
+ misskeyApiGet('charts/user/notes', { userId: props.user.id, limit: 16 + 1, span: 'day' }).then(res => {
// 今日のぶんの値はまだ途中の値であり、それも含めると大抵の場合前日よりも下降しているようなグラフになってしまうため今日は弾く
res.inc.splice(0, 1);
chartValues.value = res.inc;
@@ -42,71 +42,53 @@ onMounted(() => {
</script>
<style lang="scss" module>
-.root {
- $bodyTitleHieght: 18px;
- $bodyInfoHieght: 16px;
+$bodyTitleHieght: 18px;
+$bodyInfoHieght: 16px;
+.root {
display: flex;
align-items: center;
padding: 16px;
background: var(--panel);
border-radius: 8px;
+}
- > :global(.avatar) {
- display: block;
- width: ($bodyTitleHieght + $bodyInfoHieght);
- height: ($bodyTitleHieght + $bodyInfoHieght);
- margin-right: 12px;
- }
-
- > :global(.body) {
- flex: 1;
- overflow: hidden;
- font-size: 0.9em;
- color: var(--fg);
- padding-right: 8px;
-
- > :global(.name) {
- display: block;
- width: 100%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- line-height: $bodyTitleHieght;
- }
-
- > :global(.sub) {
- display: block;
- width: 100%;
- font-size: 95%;
- opacity: 0.7;
- line-height: $bodyInfoHieght;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- }
+.avatar {
+ display: block;
+ width: ($bodyTitleHieght + $bodyInfoHieght);
+ height: ($bodyTitleHieght + $bodyInfoHieght);
+ margin-right: 12px;
+}
- > :global(.chart) {
- height: 30px;
- }
+.body {
+ flex: 1;
+ overflow: hidden;
+ font-size: 0.9em;
+ color: var(--fg);
+ padding-right: 8px;
+}
- &:global(.yellow) {
- --c: rgb(255 196 0 / 15%);
- background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
- background-size: 16px 16px;
- }
+.name {
+ display: block;
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: $bodyTitleHieght;
+}
- &:global(.red) {
- --c: rgb(255 0 0 / 15%);
- background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
- background-size: 16px 16px;
- }
+.sub {
+ display: block;
+ width: 100%;
+ font-size: 95%;
+ opacity: 0.7;
+ line-height: $bodyInfoHieght;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
- &:global(.gray) {
- --c: var(--bg);
- background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
- background-size: 16px 16px;
- }
+.chart {
+ height: 30px;
}
</style>
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index 762b9b4316..f247ba8fdd 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index 56a61dce23..17a9254d01 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue
index 31d6fe6d04..c39a900bcf 100644
--- a/packages/frontend/src/components/MkUserOnlineIndicator.vue
+++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index df8252fb14..fb1a8f4fdc 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -60,6 +60,7 @@ import * as Misskey from 'misskey-js';
import MkFollowButton from '@/components/MkFollowButton.vue';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js';
import { i18n } from '@/i18n.js';
@@ -85,6 +86,7 @@ const top = ref(0);
const left = ref(0);
function showMenu(ev: MouseEvent) {
+ if (user.value == null) return;
const { menu, cleanup } = getUserMenu(user.value);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
}
@@ -97,7 +99,7 @@ onMounted(() => {
Misskey.acct.parse(props.q.substring(1)) :
{ userId: props.q };
- os.api('users/show', query).then(res => {
+ misskeyApi('users/show', query).then(res => {
if (!props.showing) return;
user.value = res;
});
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 9d41147bd2..cc3ea78ffe 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,7 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>{{ i18n.ts.selectUser }}</template>
<div>
<div :class="$style.form">
- <FormSplit :minWidth="170">
+ <MkInput v-if="localOnly" v-model="username" :autofocus="true" @update:modelValue="search">
+ <template #label>{{ i18n.ts.username }}</template>
+ <template #prefix>@</template>
+ </MkInput>
+ <FormSplit v-else :minWidth="170">
<MkInput v-model="username" :autofocus="true" @update:modelValue="search">
<template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template>
@@ -62,11 +66,11 @@ import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import FormSplit from '@/components/form/split.vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { hostname } from '@/config.js';
+import { host as currentHost, hostname } from '@/config.js';
const emit = defineEmits<{
(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
@@ -74,58 +78,84 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
-const props = defineProps<{
+const props = withDefaults(defineProps<{
includeSelf?: boolean;
-}>();
+ localOnly?: boolean;
+}>(), {
+ includeSelf: false,
+ localOnly: false,
+});
const username = ref('');
const host = ref('');
-const users = ref<Misskey.entities.UserDetailed[]>([]);
+const users = ref<Misskey.entities.UserLite[]>([]);
const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
-const selected = ref<Misskey.entities.UserDetailed | null>(null);
+const selected = ref<Misskey.entities.UserLite | null>(null);
const dialogEl = ref();
-const search = () => {
+function search() {
if (username.value === '' && host.value === '') {
users.value = [];
return;
}
- os.api('users/search-by-username-and-host', {
+ misskeyApi('users/search-by-username-and-host', {
username: username.value,
- host: host.value,
+ host: props.localOnly ? '.' : host.value,
limit: 10,
detail: false,
}).then(_users => {
- users.value = _users;
+ users.value = _users.filter((u) => {
+ if (props.includeSelf) {
+ return true;
+ } else {
+ return u.id !== $i?.id;
+ }
+ });
});
-};
+}
-const ok = () => {
+async function ok() {
if (selected.value == null) return;
- emit('ok', selected.value);
+
+ const user = await misskeyApi('users/show', {
+ userId: selected.value.id,
+ });
+ emit('ok', user);
+
dialogEl.value.close();
// 最近使ったユーザー更新
let recents = defaultStore.state.recentlyUsedUsers;
- recents = recents.filter(x => x !== selected.value.id);
+ recents = recents.filter(x => x !== selected.value?.id);
recents.unshift(selected.value.id);
defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
-};
+}
-const cancel = () => {
+function cancel() {
emit('cancel');
dialogEl.value.close();
-};
+}
onMounted(() => {
- os.api('users/show', {
+ misskeyApi('users/show', {
userIds: defaultStore.state.recentlyUsedUsers,
- }).then(users => {
- if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) {
- recentUsers.value = [$i, ...users];
- } else {
- recentUsers.value = users;
- }
+ }).then(foundUsers => {
+ let _users = foundUsers;
+ _users = _users.filter((u) => {
+ if (props.localOnly) {
+ return u.host == null;
+ } else {
+ return true;
+ }
+ });
+ _users = _users.filter((u) => {
+ if (props.includeSelf) {
+ return true;
+ } else {
+ return u.id !== $i?.id;
+ }
+ });
+ recentUsers.value = _users;
});
});
</script>
@@ -133,7 +163,7 @@ onMounted(() => {
<style lang="scss" module>
.form {
- padding: 0 var(--root-margin);
+ padding: calc(var(--root-margin) / 2) var(--root-margin);
}
.result,
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts
index 45c7da40ce..638bfb4372 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import { userDetailed } from '../../.storybook/fakes.js';
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
@@ -38,17 +38,17 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users', (req, res, ctx) => {
- return res(ctx.json([
+ http.post('/api/users', () => {
+ return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
- ]));
+ ]);
}),
- rest.post('/api/pinned-users', (req, res, ctx) => {
- return res(ctx.json([
+ http.post('/api/pinned-users', () => {
+ return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
- ]));
+ ]);
}),
],
},
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index 5f3f5b81dd..1524ea0ec9 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :pagination="pinnedUsers">
<template #default="{ items }">
<div :class="$style.users">
- <XUser v-for="item in items" :key="item.id" :user="item"/>
+ <XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
</div>
</template>
</MkPagination>
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :pagination="popularUsers">
<template #default="{ items }">
<div :class="$style.users">
- <XUser v-for="item in items" :key="item.id" :user="item"/>
+ <XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
</div>
</template>
</MkPagination>
@@ -34,18 +34,28 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
+import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';
-import MkPagination from '@/components/MkPagination.vue';
+import MkPagination, { type Paging } from '@/components/MkPagination.vue';
-const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
+const pinnedUsers: Paging = {
+ endpoint: 'pinned-users',
+ noPaging: true,
+ limit: 10,
+};
-const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
- state: 'alive',
- origin: 'local',
- sort: '+follower',
-} };
+const popularUsers: Paging = {
+ endpoint: 'users',
+ limit: 10,
+ noPaging: true,
+ params: {
+ state: 'alive',
+ origin: 'local',
+ sort: '+follower',
+ },
+};
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts
index 0f81c0817d..2a7947c6f8 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
index ecdfbb4969..62e5d1da8a 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -49,7 +49,7 @@ import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
const isLocked = ref(false);
const hideOnlineStatus = ref(false);
@@ -57,7 +57,7 @@ const noCrawle = ref(false);
const preventAiLearning = ref(true);
watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
- os.api('i/update', {
+ misskeyApi('i/update', {
isLocked: !!isLocked.value,
hideOnlineStatus: !!hideOnlineStatus.value,
noCrawle: !!noCrawle.value,
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts
index d2c6f7d479..c6088a5ae3 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 37aa677b44..3194641cdb 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -39,7 +39,9 @@ import FormSlot from '@/components/form/slot.vue';
import MkInfo from '@/components/MkInfo.vue';
import { chooseFileFromPc } from '@/scripts/select-file.js';
import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
const name = ref($i.name ?? '');
const description = ref($i.description ?? '');
@@ -68,7 +70,7 @@ function setAvatar(ev) {
const { canceled } = await os.confirm({
type: 'question',
- text: i18n.t('cropImageAsk'),
+ text: i18n.ts.cropImageAsk,
okText: i18n.ts.cropYes,
cancelText: i18n.ts.cropNo,
});
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts
index 31176c0832..f0206e0cb4 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index 49476c7364..bb9af676e2 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js';
import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
const props = defineProps<{
user: Misskey.entities.UserDetailed;
@@ -39,7 +39,7 @@ const isFollowing = ref(false);
async function follow() {
isFollowing.value = true;
- os.api('following/create', {
+ misskeyApi('following/create', {
userId: props.user.id,
});
}
diff --git a/packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts
index 5182db12b2..3f5ae734bd 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts
+++ b/packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import { userDetailed } from '../../.storybook/fakes.js';
import MkUserSetupDialog from './MkUserSetupDialog.vue';
@@ -38,17 +38,17 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users', (req, res, ctx) => {
- return res(ctx.json([
+ http.post('/api/users', () => {
+ return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
- ]));
+ ]);
}),
- rest.post('/api/pinned-users', (req, res, ctx) => {
- return res(ctx.json([
+ http.post('/api/pinned-users', () => {
+ return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
- ]));
+ ]);
}),
],
},
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 05b55f77a7..1d376382ca 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps" style="text-align: center;">
<i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
- <div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div>
+ <div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
@@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps" style="text-align: center;">
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
- <div>{{ i18n.t('_initialAccountSetting.youCanContinueTutorial', { name: instance.name ?? host }) }}</div>
+ <div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
diff --git a/packages/frontend/src/components/MkUsersTooltip.vue b/packages/frontend/src/components/MkUsersTooltip.vue
index 37548952b6..054a503257 100644
--- a/packages/frontend/src/components/MkUsersTooltip.vue
+++ b/packages/frontend/src/components/MkUsersTooltip.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 1324ed12e1..3439a751a0 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -1,10 +1,10 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')">
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
<div :class="[$style.label, $style.item]">
{{ i18n.ts.visibility }}
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index 746ed3e0de..cab42cd59d 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,11 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, shallowRef, ref, nextTick } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import tinycolor from 'tinycolor2';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
@@ -25,9 +25,9 @@ import { initChart } from '@/scripts/init-chart.js';
initChart();
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const now = new Date();
-let chartInstance: Chart = null;
+let chartInstance: Chart | null = null;
const chartLimit = 30;
const fetching = ref(true);
@@ -53,7 +53,11 @@ async function renderChart() {
}));
};
- const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
+
+ fetching.value = false;
+
+ await nextTick();
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
@@ -65,6 +69,8 @@ async function renderChart() {
const max = Math.max(...raw.read);
+ if (chartEl.value == null) return;
+
chartInstance = new Chart(chartEl.value, {
type: 'bar',
data: {
@@ -97,7 +103,6 @@ async function renderChart() {
type: 'time',
offset: true,
time: {
- stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@@ -108,6 +113,7 @@ async function renderChart() {
display: false,
},
ticks: {
+ stepSize: 1,
display: true,
maxRotation: 0,
autoSkipPadding: 8,
@@ -141,13 +147,10 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
- gradient,
},
},
plugins: [chartVLine(vLineColor)],
});
-
- fetching.value = false;
}
onMounted(async () => {
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 9ed08ee372..be80baa774 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -1,12 +1,12 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-if="meta" :class="$style.root">
<div :class="[$style.main, $style.panel]">
- <img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.mainIcon"/>
+ <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.mainIcon"/>
<button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
<div :class="$style.mainFg">
<h1 :class="$style.mainTitle">
@@ -60,6 +60,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
import { instanceName } from '@/config.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import MkNumber from '@/components/MkNumber.vue';
@@ -68,11 +69,11 @@ import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.
const meta = ref<Misskey.entities.MetaResponse | null>(null);
const stats = ref<Misskey.entities.StatsResponse | null>(null);
-os.api('meta', { detail: true }).then(_meta => {
+misskeyApi('meta', { detail: true }).then(_meta => {
meta.value = _meta;
});
-os.api('stats', {}).then((res) => {
+misskeyApi('stats', {}).then((res) => {
stats.value = res;
});
@@ -105,19 +106,19 @@ function showMenu(ev) {
text: i18n.ts.impressum,
icon: 'ti ti-file-invoice',
action: () => {
- window.open(instance.impressumUrl, '_blank', 'noopener');
+ window.open(instance.impressumUrl!, '_blank', 'noopener');
},
} : undefined, (instance.tosUrl) ? {
text: i18n.ts.termsOfService,
icon: 'ti ti-notebook',
action: () => {
- window.open(instance.tosUrl, '_blank', 'noopener');
+ window.open(instance.tosUrl!, '_blank', 'noopener');
},
} : undefined, (instance.privacyPolicyUrl) ? {
text: i18n.ts.privacyPolicy,
icon: 'ti ti-shield-lock',
action: () => {
- window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
+ window.open(instance.privacyPolicyUrl!, '_blank', 'noopener');
},
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
text: i18n.ts.help,
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index 1326ca2693..60b75b6d30 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -32,7 +32,7 @@ const emit = defineEmits<{
function done() {
emit('done');
- modal.value.close();
+ modal.value?.close();
}
watch(() => props.showing, () => {
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index ee4e29dd8f..7550edd120 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<header :class="$style.editHeader">
<MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
<template #label>{{ i18n.ts.selectWidget }}</template>
- <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.t(`_widgets.${widget}`) }}</option>
+ <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
</MkSelect>
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton>
@@ -97,19 +97,21 @@ const updateWidget = (id, data) => {
};
function onContextmenu(widget: Widget, ev: MouseEvent) {
- const isLink = (el: HTMLElement) => {
+ const element = ev.target as HTMLElement | null;
+ const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
+ return false;
};
- if (isLink(ev.target)) return;
- if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
+ if (element && isLink(element)) return;
+ if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return;
if (window.getSelection()?.toString() !== '') return;
os.contextMenu([{
type: 'label',
- text: i18n.t(`_widgets.${widget.name}`),
+ text: i18n.ts._widgets[widget.name],
}, {
icon: 'ti ti-settings',
text: i18n.ts.settings,
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 7c8ffcccf9..303e49de00 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -63,7 +63,7 @@ import { defaultStore } from '@/store.js';
const minHeight = 50;
const minWidth = 250;
-function dragListen(fn: (ev: MouseEvent) => void) {
+function dragListen(fn: (ev: MouseEvent | TouchEvent) => void) {
window.addEventListener('mousemove', fn);
window.addEventListener('touchmove', fn);
window.addEventListener('mouseleave', dragClear.bind(null, fn));
@@ -138,11 +138,12 @@ function onContextmenu(ev: MouseEvent) {
// 最前面へ移動
function top() {
if (rootEl.value) {
- rootEl.value.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
+ rootEl.value.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low').toString();
}
}
function maximize() {
+ if (rootEl.value == null) return;
maximized.value = true;
unResizedTop = rootEl.value.style.top;
unResizedLeft = rootEl.value.style.left;
@@ -155,6 +156,7 @@ function maximize() {
}
function unMaximize() {
+ if (rootEl.value == null) return;
maximized.value = false;
rootEl.value.style.top = unResizedTop;
rootEl.value.style.left = unResizedLeft;
@@ -163,6 +165,7 @@ function unMaximize() {
}
function minimize() {
+ if (rootEl.value == null) return;
minimized.value = true;
unResizedWidth = rootEl.value.style.width;
unResizedHeight = rootEl.value.style.height;
@@ -171,8 +174,8 @@ function minimize() {
}
function unMinimize() {
+ if (rootEl.value == null) return;
const main = rootEl.value;
- if (main == null) return;
minimized.value = false;
rootEl.value.style.width = unResizedWidth;
@@ -199,9 +202,17 @@ function onDblClick() {
}
}
-function onHeaderMousedown(evt: MouseEvent) {
+function getPositionX(event: MouseEvent | TouchEvent) {
+ return 'touches' in event && event.touches.length > 0 ? event.touches[0].clientX : 'clientX' in event ? event.clientX : 0;
+}
+
+function getPositionY(event: MouseEvent | TouchEvent) {
+ return 'touches' in event && event.touches.length > 0 ? event.touches[0].clientY : 'clientY' in event ? event.clientY : 0;
+}
+
+function onHeaderMousedown(evt: MouseEvent | TouchEvent) {
// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
- if (evt.button === 2) return;
+ if ('button' in evt && evt.button === 2) return;
let beforeMaximized = false;
@@ -226,8 +237,8 @@ function onHeaderMousedown(evt: MouseEvent) {
const position = main.getBoundingClientRect();
- const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
- const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
+ const clickX = getPositionX(evt);
+ const clickY = getPositionY(evt);
const moveBaseX = beforeMaximized ? parseInt(unResizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる
const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
const browserWidth = window.innerWidth;
@@ -251,8 +262,10 @@ function onHeaderMousedown(evt: MouseEvent) {
// 右はみ出し
if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
- rootEl.value.style.left = moveLeft + 'px';
- rootEl.value.style.top = moveTop + 'px';
+ if (rootEl.value) {
+ rootEl.value.style.left = moveLeft + 'px';
+ rootEl.value.style.top = moveTop + 'px';
+ }
}
if (beforeMaximized) {
@@ -261,26 +274,26 @@ function onHeaderMousedown(evt: MouseEvent) {
// 動かした時
dragListen(me => {
- const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
- const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
+ const x = getPositionX(me);
+ const y = getPositionY(me);
move(x, y);
});
}
// 上ハンドル掴み時
-function onTopHandleMousedown(evt) {
+function onTopHandleMousedown(evt: MouseEvent | TouchEvent) {
const main = rootEl.value;
// どういうわけかnullになることがある
if (main == null) return;
- const base = evt.clientY;
+ const base = getPositionY(evt);
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
// 動かした時
dragListen(me => {
- const move = me.clientY - base;
+ const move = getPositionY(me) - base;
if (top + move > 0) {
if (height + -move > minHeight) {
applyTransformHeight(height + -move);
@@ -297,18 +310,18 @@ function onTopHandleMousedown(evt) {
}
// 右ハンドル掴み時
-function onRightHandleMousedown(evt) {
+function onRightHandleMousedown(evt: MouseEvent | TouchEvent) {
const main = rootEl.value;
if (main == null) return;
- const base = evt.clientX;
+ const base = getPositionX(evt);
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const browserWidth = window.innerWidth;
// 動かした時
dragListen(me => {
- const move = me.clientX - base;
+ const move = getPositionX(me) - base;
if (left + width + move < browserWidth) {
if (width + move > minWidth) {
applyTransformWidth(width + move);
@@ -322,18 +335,18 @@ function onRightHandleMousedown(evt) {
}
// 下ハンドル掴み時
-function onBottomHandleMousedown(evt) {
+function onBottomHandleMousedown(evt: MouseEvent | TouchEvent) {
const main = rootEl.value;
if (main == null) return;
- const base = evt.clientY;
+ const base = getPositionY(evt);
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
const browserHeight = window.innerHeight;
// 動かした時
dragListen(me => {
- const move = me.clientY - base;
+ const move = getPositionY(me) - base;
if (top + height + move < browserHeight) {
if (height + move > minHeight) {
applyTransformHeight(height + move);
@@ -347,17 +360,17 @@ function onBottomHandleMousedown(evt) {
}
// 左ハンドル掴み時
-function onLeftHandleMousedown(evt) {
+function onLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
const main = rootEl.value;
if (main == null) return;
- const base = evt.clientX;
+ const base = getPositionX(evt);
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
// 動かした時
dragListen(me => {
- const move = me.clientX - base;
+ const move = getPositionX(me) - base;
if (left + move > 0) {
if (width + -move > minWidth) {
applyTransformWidth(width + -move);
@@ -374,25 +387,25 @@ function onLeftHandleMousedown(evt) {
}
// 左上ハンドル掴み時
-function onTopLeftHandleMousedown(evt) {
+function onTopLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
onTopHandleMousedown(evt);
onLeftHandleMousedown(evt);
}
// 右上ハンドル掴み時
-function onTopRightHandleMousedown(evt) {
+function onTopRightHandleMousedown(evt: MouseEvent | TouchEvent) {
onTopHandleMousedown(evt);
onRightHandleMousedown(evt);
}
// 右下ハンドル掴み時
-function onBottomRightHandleMousedown(evt) {
+function onBottomRightHandleMousedown(evt: MouseEvent | TouchEvent) {
onBottomHandleMousedown(evt);
onRightHandleMousedown(evt);
}
// 左下ハンドル掴み時
-function onBottomLeftHandleMousedown(evt) {
+function onBottomLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
onBottomHandleMousedown(evt);
onLeftHandleMousedown(evt);
}
@@ -400,23 +413,23 @@ function onBottomLeftHandleMousedown(evt) {
// 高さを適用
function applyTransformHeight(height) {
if (height > window.innerHeight) height = window.innerHeight;
- rootEl.value.style.height = height + 'px';
+ if (rootEl.value) rootEl.value.style.height = height + 'px';
}
// 幅を適用
function applyTransformWidth(width) {
if (width > window.innerWidth) width = window.innerWidth;
- rootEl.value.style.width = width + 'px';
+ if (rootEl.value) rootEl.value.style.width = width + 'px';
}
// Y座標を適用
function applyTransformTop(top) {
- rootEl.value.style.top = top + 'px';
+ if (rootEl.value) rootEl.value.style.top = top + 'px';
}
// X座標を適用
function applyTransformLeft(left) {
- rootEl.value.style.left = left + 'px';
+ if (rootEl.value) rootEl.value.style.left = left + 'px';
}
function onBrowserResize() {
@@ -438,8 +451,10 @@ onMounted(() => {
applyTransformWidth(props.initialWidth);
if (props.initialHeight) applyTransformHeight(props.initialHeight);
- applyTransformTop((window.innerHeight / 2) - (rootEl.value.offsetHeight / 2));
- applyTransformLeft((window.innerWidth / 2) - (rootEl.value.offsetWidth / 2));
+ if (rootEl.value) {
+ applyTransformTop((window.innerHeight / 2) - (rootEl.value.offsetHeight / 2));
+ applyTransformLeft((window.innerWidth / 2) - (rootEl.value.offsetWidth / 2));
+ }
// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
top();
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index c6b18aeceb..1fad222fc5 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -39,7 +39,7 @@ if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid
const fetching = ref(true);
const title = ref<string | null>(null);
const player = ref({
- url: null,
+ url: null as string | null,
width: null,
height: null,
});
diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue
index 4d711814b5..e76ed9a849 100644
--- a/packages/frontend/src/components/form/link.vue
+++ b/packages/frontend/src/components/form/link.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue
index 6af63d1ec6..ad37daa265 100644
--- a/packages/frontend/src/components/form/section.vue
+++ b/packages/frontend/src/components/form/section.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue
index dc4d197507..f54db0ca82 100644
--- a/packages/frontend/src/components/form/slot.vue
+++ b/packages/frontend/src/components/form/slot.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/form/split.vue b/packages/frontend/src/components/form/split.vue
index 8cb24b479e..2a015c9520 100644
--- a/packages/frontend/src/components/form/split.vue
+++ b/packages/frontend/src/components/form/split.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index eaf5ae4744..5226c61d68 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue
new file mode 100644
index 0000000000..162aa2bcf8
--- /dev/null
+++ b/packages/frontend/src/components/global/I18n.vue
@@ -0,0 +1,46 @@
+<template>
+<render/>
+</template>
+
+<script setup lang="ts" generic="T extends string | ParameterizedString">
+import { computed, h } from 'vue';
+import type { ParameterizedString } from '../../../../../locales/index.js';
+
+const props = withDefaults(defineProps<{
+ src: T;
+ tag?: string;
+ // eslint-disable-next-line vue/require-default-prop
+ textTag?: string;
+}>(), {
+ tag: 'span',
+});
+
+const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: () => unknown } : NonNullable<unknown>>();
+
+const parsed = computed(() => {
+ let str = props.src as string;
+ const value: (string | { arg: string; })[] = [];
+ for (;;) {
+ const nextBracketOpen = str.indexOf('{');
+ const nextBracketClose = str.indexOf('}');
+
+ if (nextBracketOpen === -1) {
+ value.push(str);
+ break;
+ } else {
+ if (nextBracketOpen > 0) value.push(str.substring(0, nextBracketOpen));
+ value.push({
+ arg: str.substring(nextBracketOpen + 1, nextBracketClose),
+ });
+ }
+
+ str = str.substring(nextBracketClose + 1);
+ }
+
+ return value;
+});
+
+const render = () => {
+ return h(props.tag, parsed.value.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
+};
+</script>
diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts
index 62f4805a11..9d57841f04 100644
--- a/packages/frontend/src/components/global/MkA.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkA.stories.impl.ts
@@ -1,11 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { expect } from '@storybook/jest';
-import { userEvent, within } from '@storybook/testing-library';
+import { expect, userEvent, within } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import MkA from './MkA.vue';
import { tick } from '@/scripts/test-utils.js';
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index d34f47a68a..61d7ac17d9 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -15,7 +15,7 @@ import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
import { i18n } from '@/i18n.js';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/router/supplier.js';
const props = withDefaults(defineProps<{
to: string;
diff --git a/packages/frontend/src/components/global/MkAcct.stories.impl.ts b/packages/frontend/src/components/global/MkAcct.stories.impl.ts
index 49ec61211c..04960ec60c 100644
--- a/packages/frontend/src/components/global/MkAcct.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkAcct.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index 594494f3c8..8cb082585b 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -21,7 +21,7 @@ import { host as hostRaw } from '@/config.js';
import { defaultStore } from '@/store.js';
defineProps<{
- user: Misskey.entities.UserDetailed;
+ user: Misskey.entities.User;
detail?: boolean;
}>();
diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts
index 5ae45ec58f..f6cdc2bf23 100644
--- a/packages/frontend/src/components/global/MkAd.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 3ef5db3fe3..8f5ed760d5 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkAvatar.stories.impl.ts b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts
index 515d7eab18..933754ec4c 100644
--- a/packages/frontend/src/components/global/MkAvatar.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index af5b6e44f5..e8e1bc696b 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<img
v-for="decoration in decorations ?? user.avatarDecorations"
:class="[$style.decoration]"
- :src="decoration.url"
+ :src="getDecorationUrl(decoration)"
:style="{
rotate: getDecorationAngle(decoration),
scale: getDecorationScale(decoration),
@@ -81,15 +81,22 @@ const bound = computed(() => props.link
? { to: userPage(props.user), target: props.target }
: {});
-const url = computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar)
- ? getStaticImageUrl(props.user.avatarUrl)
- : props.user.avatarUrl);
+const url = computed(() => {
+ if (props.user.avatarUrl == null) return null;
+ if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
+ return props.user.avatarUrl;
+});
function onClick(ev: MouseEvent): void {
if (props.link) return;
emit('click', ev);
}
+function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+ if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(decoration.url);
+ return decoration.url;
+}
+
function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
const angle = decoration.angle ?? 0;
return angle === 0 ? undefined : `${angle * 360}deg`;
@@ -109,6 +116,7 @@ function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['ava
const color = ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
+ if (props.user.avatarBlurhash == null) return;
color.value = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
}, {
immediate: true,
diff --git a/packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts b/packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts
index 7df49a2066..e4e90cddd5 100644
--- a/packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkCondensedLine.vue b/packages/frontend/src/components/global/MkCondensedLine.vue
index 2ed615f5ff..7c4957d77f 100644
--- a/packages/frontend/src/components/global/MkCondensedLine.vue
+++ b/packages/frontend/src/components/global/MkCondensedLine.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts b/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts
index f50217b70d..e0da6a4a13 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkCustomEmoji.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index a9643d68ca..dbcb00460c 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -24,9 +24,11 @@ import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'
import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js';
import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
+import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
const props = defineProps<{
name: string;
@@ -55,7 +57,7 @@ const rawUrl = computed(() => {
});
const url = computed(() => {
- if (rawUrl.value == null) return null;
+ if (rawUrl.value == null) return undefined;
const proxied =
(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
@@ -91,9 +93,21 @@ function onClick(ev: MouseEvent) {
icon: 'ti ti-plus',
action: () => {
react(`:${props.name}:`);
- sound.play('reaction');
+ sound.playMisskeySfx('reaction');
},
- }] : [])], ev.currentTarget ?? ev.target);
+ }] : []), {
+ text: i18n.ts.info,
+ icon: 'ti ti-info-circle',
+ action: async () => {
+ os.popup(MkCustomEmojiDetailedDialog, {
+ emoji: await misskeyApiGet('emoji', {
+ name: customEmojiName.value,
+ }),
+ }, {
+ anchor: ev.target,
+ });
+ },
+ }], ev.currentTarget ?? ev.target);
}
}
</script>
diff --git a/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts b/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts
index 32deaae8e2..6a8fcf4fe3 100644
--- a/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkEllipsis.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkEllipsis.vue b/packages/frontend/src/components/global/MkEllipsis.vue
index 5cc07f7040..4ba6be10fe 100644
--- a/packages/frontend/src/components/global/MkEllipsis.vue
+++ b/packages/frontend/src/components/global/MkEllipsis.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkEmoji.stories.impl.ts b/packages/frontend/src/components/global/MkEmoji.stories.impl.ts
index c8beec7e8f..309c015757 100644
--- a/packages/frontend/src/components/global/MkEmoji.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkEmoji.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 76ca8688d1..ba4026f0f6 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -1,19 +1,18 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<img v-if="!useOsNativeEmojis" :class="$style.root" :src="url" :alt="props.emoji" decoding="async" @pointerenter="computeTitle" @click="onClick"/>
-<span v-else-if="useOsNativeEmojis" :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ props.emoji }}</span>
-<span v-else>{{ emoji }}</span>
+<span v-else :alt="props.emoji" @pointerenter="computeTitle" @click="onClick">{{ colorizedNativeEmoji }}</span>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue';
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
import { defaultStore } from '@/store.js';
-import { getEmojiName } from '@/scripts/emojilist.js';
+import { colorizeEmoji, getEmojiName } from '@/scripts/emojilist.js';
import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import * as sound from '@/scripts/sound.js';
@@ -30,9 +29,8 @@ const react = inject<((name: string) => void) | null>('react', null);
const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
-const url = computed(() => {
- return char2path(props.emoji);
-});
+const url = computed(() => char2path(props.emoji));
+const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
// Searching from an array with 2000 items for every emoji felt like too energy-consuming, so I decided to do it lazily on pointerenter
function computeTitle(event: PointerEvent): void {
@@ -57,7 +55,7 @@ function onClick(ev: MouseEvent) {
icon: 'ti ti-plus',
action: () => {
react(props.emoji);
- sound.play('reaction');
+ sound.playMisskeySfx('reaction');
},
}] : [])], ev.currentTarget ?? ev.target);
}
diff --git a/packages/frontend/src/components/global/MkError.stories.impl.ts b/packages/frontend/src/components/global/MkError.stories.impl.ts
index cf0a1dbb5f..daef04cd87 100644
--- a/packages/frontend/src/components/global/MkError.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkError.stories.impl.ts
@@ -1,12 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
-import { expect } from '@storybook/jest';
-import { waitFor } from '@storybook/testing-library';
+import { expect, waitFor } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import MkError from './MkError.vue';
export const Default = {
diff --git a/packages/frontend/src/components/global/MkError.stories.meta.ts b/packages/frontend/src/components/global/MkError.stories.meta.ts
index a3955c5786..1abbc56f50 100644
--- a/packages/frontend/src/components/global/MkError.stories.meta.ts
+++ b/packages/frontend/src/components/global/MkError.stories.meta.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue
index 7181ae61a1..c594cc752b 100644
--- a/packages/frontend/src/components/global/MkError.vue
+++ b/packages/frontend/src/components/global/MkError.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkFooterSpacer.vue b/packages/frontend/src/components/global/MkFooterSpacer.vue
index e78df6b8d9..1a75855fa1 100644
--- a/packages/frontend/src/components/global/MkFooterSpacer.vue
+++ b/packages/frontend/src/components/global/MkFooterSpacer.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkLazy.vue b/packages/frontend/src/components/global/MkLazy.vue
index 6d7ff4ca49..f35932ae77 100644
--- a/packages/frontend/src/components/global/MkLazy.vue
+++ b/packages/frontend/src/components/global/MkLazy.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkLoading.stories.impl.ts b/packages/frontend/src/components/global/MkLoading.stories.impl.ts
index 9cedd68fd8..c781ad0479 100644
--- a/packages/frontend/src/components/global/MkLoading.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkLoading.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue
index 3f34e83f58..49d8ace37b 100644
--- a/packages/frontend/src/components/global/MkLoading.vue
+++ b/packages/frontend/src/components/global/MkLoading.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
index 9cdb490e4b..730351f795 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
@@ -1,12 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import { within } from '@storybook/testing-library';
-import { expect } from '@storybook/jest';
+import { expect, within } from '@storybook/test';
import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js';
export const Default = {
render(args) {
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index 650c79dff7..6ce3b6752f 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -13,12 +13,14 @@ import MkMention from '@/components/MkMention.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkCode from '@/components/MkCode.vue';
+import MkCodeInline from '@/components/MkCodeInline.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue';
import { host } from '@/config.js';
import { defaultStore } from '@/store.js';
import { nyaize as doNyaize } from '@/scripts/nyaize.js';
+import { safeParseFloat } from '@/scripts/safe-parse.js';
const QUOTE_STYLE = `
display: block;
@@ -35,7 +37,7 @@ type MfmProps = {
nowrap?: boolean;
author?: Misskey.entities.UserLite;
isNote?: boolean;
- emojiUrls?: string[];
+ emojiUrls?: Record<string, string>;
rootScale?: number;
nyaize?: boolean | 'respect';
parsedNodes?: mfm.MfmNode[] | null;
@@ -48,7 +50,7 @@ type MfmEvents = {
};
// eslint-disable-next-line import/no-default-export
-export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
+export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
const isNote = props.isNote ?? true;
const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
@@ -57,11 +59,17 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
- const validTime = (t: string | null | undefined) => {
+ const validTime = (t: string | boolean | null | undefined) => {
if (t == null) return null;
+ if (typeof t === 'boolean') return null;
return t.match(/^[0-9.]+s$/) ? t : null;
};
+ const validColor = (c: unknown): string | null => {
+ if (typeof c !== 'string') return null;
+ return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
+ };
+
const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
/**
@@ -112,7 +120,7 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
case 'tada': {
const speed = validTime(token.props.args.speed) ?? '1s';
const delay = validTime(token.props.args.delay) ?? '0s';
- style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
+ style = 'font-size: 150%;' + (useAnim ? `animation: global-tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
break;
}
case 'jelly': {
@@ -217,14 +225,14 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
return h(MkSparkle, {}, genEl(token.children, scale));
}
case 'rotate': {
- const degrees = parseFloat(token.props.args.deg ?? '90');
+ const degrees = safeParseFloat(token.props.args.deg) ?? 90;
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
break;
}
case 'position': {
if (!defaultStore.state.advancedMfm) break;
- const x = parseFloat(token.props.args.x ?? '0');
- const y = parseFloat(token.props.args.y ?? '0');
+ const x = safeParseFloat(token.props.args.x) ?? 0;
+ const y = safeParseFloat(token.props.args.y) ?? 0;
style = `transform: translateX(${x}em) translateY(${y}em);`;
break;
}
@@ -233,24 +241,38 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
style = '';
break;
}
- const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
- const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
+ const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5);
+ const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5);
style = `transform: scale(${x}, ${y});`;
scale = scale * Math.max(x, y);
break;
}
case 'fg': {
- let color = token.props.args.color;
- if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+ let color = validColor(token.props.args.color);
+ color = color ?? 'f00';
style = `color: #${color}; overflow-wrap: anywhere;`;
break;
}
case 'bg': {
- let color = token.props.args.color;
- if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+ let color = validColor(token.props.args.color);
+ color = color ?? 'f00';
style = `background-color: #${color}; overflow-wrap: anywhere;`;
break;
}
+ case 'border': {
+ let color = validColor(token.props.args.color);
+ color = color ? `#${color}` : 'var(--accent)';
+ let b_style = token.props.args.style;
+ if (
+ typeof b_style !== 'string' ||
+ !['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset']
+ .includes(b_style)
+ ) b_style = 'solid';
+ const width = safeParseFloat(token.props.args.width) ?? 1;
+ const radius = safeParseFloat(token.props.args.radius) ?? 0;
+ style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`;
+ break;
+ }
case 'ruby': {
if (token.children.length === 1) {
const child = token.children[0];
@@ -289,7 +311,8 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
return h('span', { onClick(ev: MouseEvent): void {
ev.stopPropagation();
ev.preventDefault();
- context.emit('clickEv', token.props.args.ev ?? '');
+ const clickEv = typeof token.props.args.ev === 'string' ? token.props.args.ev : '';
+ emit('clickEv', clickEv);
} }, genEl(token.children, scale));
}
}
@@ -350,15 +373,14 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
return [h(MkCode, {
key: Math.random(),
code: token.props.code,
- lang: token.props.lang,
+ lang: token.props.lang ?? undefined,
})];
}
case 'inlineCode': {
- return [h(MkCode, {
+ return [h(MkCodeInline, {
key: Math.random(),
code: token.props.code,
- inline: true,
})];
}
@@ -394,8 +416,7 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
return [h(MkCustomEmoji, {
key: Math.random(),
name: token.props.name,
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- url: props.emojiUrls ? props.emojiUrls[token.props.name] : null,
+ url: props.emojiUrls && props.emojiUrls[token.props.name],
normal: props.plain,
host: props.author.host,
useOriginalSize: scale >= 2.5,
diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
index d3fd1bdc08..eb74e874dd 100644
--- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { waitFor } from '@storybook/testing-library';
+import { waitFor } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import MkPageHeader from './MkPageHeader.vue';
export const Empty = {
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts
index 130dde63af..5d2126435e 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 24b92cb83a..e93b09721a 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -38,6 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
export type Tab = {
key: string;
+ title: string;
onClick?: (ev: MouseEvent) => void;
} & (
| {
@@ -120,8 +121,9 @@ function onTabWheel(ev: WheelEvent) {
let entering = false;
-async function enter(el: HTMLElement) {
+async function enter(element: Element) {
entering = true;
+ const el = element as HTMLElement;
const elementWidth = el.getBoundingClientRect().width;
el.style.width = '0';
el.style.paddingLeft = '0';
@@ -135,11 +137,12 @@ async function enter(el: HTMLElement) {
setTimeout(renderTab, 170);
}
-function afterEnter(el: HTMLElement) {
+function afterEnter(element: Element) {
//el.style.width = '';
}
-async function leave(el: HTMLElement) {
+async function leave(element: Element) {
+ const el = element as HTMLElement;
const elementWidth = el.getBoundingClientRect().width;
el.style.width = elementWidth + 'px';
el.style.paddingLeft = '';
@@ -148,7 +151,8 @@ async function leave(el: HTMLElement) {
el.style.paddingLeft = '0';
}
-function afterLeave(el: HTMLElement) {
+function afterLeave(element: Element) {
+ const el = element as HTMLElement;
el.style.width = '';
}
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 8624aebdcf..f16d951679 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -11,18 +11,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="!thin_ && narrow && !hideTitle" :class="$style.buttonsLeft"/>
- <template v-if="metadata">
+ <template v-if="pageMetadata">
<div v-if="!hideTitle" :class="$style.titleContainer" @click="top">
- <div v-if="metadata.avatar" :class="$style.titleAvatarContainer">
- <MkAvatar :class="$style.titleAvatar" :user="metadata.avatar" indicator/>
+ <div v-if="pageMetadata.avatar" :class="$style.titleAvatarContainer">
+ <MkAvatar :class="$style.titleAvatar" :user="pageMetadata.avatar" indicator/>
</div>
- <i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i>
+ <i v-else-if="pageMetadata.icon" :class="[$style.titleIcon, pageMetadata.icon]"></i>
<div :class="$style.title">
- <MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true"/>
- <div v-else-if="metadata.title">{{ metadata.title }}</div>
- <div v-if="metadata.subtitle" :class="$style.subtitle">
- {{ metadata.subtitle }}
+ <MkUserName v-if="pageMetadata.userName" :user="pageMetadata.userName" :nowrap="true"/>
+ <div v-else-if="pageMetadata.title">{{ pageMetadata.title }}</div>
+ <div v-if="pageMetadata.subtitle" :class="$style.subtitle">
+ {{ pageMetadata.subtitle }}
</div>
</div>
</div>
@@ -46,7 +46,7 @@ import tinycolor from 'tinycolor2';
import XTabs, { Tab } from './MkPageHeader.tabs.vue';
import { scrollToTop } from '@/scripts/scroll.js';
import { globalEvents } from '@/events.js';
-import { injectPageMetadata } from '@/scripts/page-metadata.js';
+import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { PageHeaderItem } from '@/types/page-header.js';
@@ -64,7 +64,7 @@ const emit = defineEmits<{
(ev: 'update:tab', key: string);
}>();
-const metadata = injectPageMetadata();
+const pageMetadata = injectReactiveMetadata();
const hideTitle = inject('shouldOmitHeaderTitle', false);
const thin_ = props.thin || inject('shouldHeaderThin', false);
diff --git a/packages/frontend/src/components/global/MkSpacer.vue b/packages/frontend/src/components/global/MkSpacer.vue
index a384e06f77..db01c10eb0 100644
--- a/packages/frontend/src/components/global/MkSpacer.vue
+++ b/packages/frontend/src/components/global/MkSpacer.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts b/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts
index 16c62ce03d..186048991e 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkStickyContainer.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 70cc68b14c..89993e1b8e 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -63,27 +63,32 @@ onMounted(() => {
watch([parentStickyTop, parentStickyBottom], calc);
watch(childStickyTop, () => {
+ if (bodyEl.value == null) return;
bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
}, {
immediate: true,
});
watch(childStickyBottom, () => {
+ if (bodyEl.value == null) return;
bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
}, {
immediate: true,
});
- headerEl.value.style.position = 'sticky';
- headerEl.value.style.top = 'var(--stickyTop, 0)';
- headerEl.value.style.zIndex = '1000';
-
- footerEl.value.style.position = 'sticky';
- footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
- footerEl.value.style.zIndex = '1000';
+ if (headerEl.value != null) {
+ headerEl.value.style.position = 'sticky';
+ headerEl.value.style.top = 'var(--stickyTop, 0)';
+ headerEl.value.style.zIndex = '1000';
+ observer.observe(headerEl.value);
+ }
- observer.observe(headerEl.value);
- observer.observe(footerEl.value);
+ if (footerEl.value != null) {
+ footerEl.value.style.position = 'sticky';
+ footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
+ footerEl.value.style.zIndex = '1000';
+ observer.observe(footerEl.value);
+ }
});
onUnmounted(() => {
diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts
index 0eeefa4859..2b4b1485fd 100644
--- a/packages/frontend/src/components/global/MkTime.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { expect } from '@storybook/jest';
+import { expect } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import MkTime from './MkTime.vue';
import { i18n } from '@/i18n.js';
@@ -123,7 +123,7 @@ export const DetailNow = {
export const RelativeOneHourAgo = {
...Empty,
async play({ canvasElement }) {
- await expect(canvasElement).toHaveTextContent(i18n.t('_ago.hoursAgo', { n: 1 }));
+ await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.hoursAgo({ n: 1 }));
},
args: {
...Empty.args,
@@ -162,7 +162,7 @@ export const DetailOneHourAgo = {
export const RelativeOneDayAgo = {
...Empty,
async play({ canvasElement }) {
- await expect(canvasElement).toHaveTextContent(i18n.t('_ago.daysAgo', { n: 1 }));
+ await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.daysAgo({ n: 1 }));
},
args: {
...Empty.args,
@@ -201,7 +201,7 @@ export const DetailOneDayAgo = {
export const RelativeOneWeekAgo = {
...Empty,
async play({ canvasElement }) {
- await expect(canvasElement).toHaveTextContent(i18n.t('_ago.weeksAgo', { n: 1 }));
+ await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.weeksAgo({ n: 1 }));
},
args: {
...Empty.args,
@@ -240,7 +240,7 @@ export const DetailOneWeekAgo = {
export const RelativeOneMonthAgo = {
...Empty,
async play({ canvasElement }) {
- await expect(canvasElement).toHaveTextContent(i18n.t('_ago.monthsAgo', { n: 1 }));
+ await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.monthsAgo({ n: 1 }));
},
args: {
...Empty.args,
@@ -279,7 +279,7 @@ export const DetailOneMonthAgo = {
export const RelativeOneYearAgo = {
...Empty,
async play({ canvasElement }) {
- await expect(canvasElement).toHaveTextContent(i18n.t('_ago.yearsAgo', { n: 1 }));
+ await expect(canvasElement).toHaveTextContent(i18n.tsx._ago.yearsAgo({ n: 1 }));
},
args: {
...Empty.args,
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index e11db9dc31..67532268d3 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
mode?: 'relative' | 'absolute' | 'detail';
colored?: boolean;
}>(), {
- origin: isChromatic() ? new Date('2023-04-01T00:00:00Z') : null,
+ origin: isChromatic() ? () => new Date('2023-04-01T00:00:00Z') : null,
mode: 'relative',
});
@@ -55,21 +55,21 @@ const relative = computed<string>(() => {
if (invalid) return i18n.ts._ago.invalid;
return (
- ago.value >= 31536000 ? i18n.t('_ago.yearsAgo', { n: Math.round(ago.value / 31536000).toString() }) :
- ago.value >= 2592000 ? i18n.t('_ago.monthsAgo', { n: Math.round(ago.value / 2592000).toString() }) :
- ago.value >= 604800 ? i18n.t('_ago.weeksAgo', { n: Math.round(ago.value / 604800).toString() }) :
- ago.value >= 86400 ? i18n.t('_ago.daysAgo', { n: Math.round(ago.value / 86400).toString() }) :
- ago.value >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago.value / 3600).toString() }) :
- ago.value >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago.value / 60)).toString() }) :
- ago.value >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago.value % 60)).toString() }) :
+ ago.value >= 31536000 ? i18n.tsx._ago.yearsAgo({ n: Math.round(ago.value / 31536000).toString() }) :
+ ago.value >= 2592000 ? i18n.tsx._ago.monthsAgo({ n: Math.round(ago.value / 2592000).toString() }) :
+ ago.value >= 604800 ? i18n.tsx._ago.weeksAgo({ n: Math.round(ago.value / 604800).toString() }) :
+ ago.value >= 86400 ? i18n.tsx._ago.daysAgo({ n: Math.round(ago.value / 86400).toString() }) :
+ ago.value >= 3600 ? i18n.tsx._ago.hoursAgo({ n: Math.round(ago.value / 3600).toString() }) :
+ ago.value >= 60 ? i18n.tsx._ago.minutesAgo({ n: (~~(ago.value / 60)).toString() }) :
+ ago.value >= 10 ? i18n.tsx._ago.secondsAgo({ n: (~~(ago.value % 60)).toString() }) :
ago.value >= -3 ? i18n.ts._ago.justNow :
- ago.value < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago.value / 31536000).toString() }) :
- ago.value < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago.value / 2592000).toString() }) :
- ago.value < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago.value / 604800).toString() }) :
- ago.value < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago.value / 86400).toString() }) :
- ago.value < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago.value / 3600).toString() }) :
- ago.value < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago.value / 60)).toString() }) :
- i18n.t('_timeIn.seconds', { n: (~~(-ago.value % 60)).toString() })
+ ago.value < -31536000 ? i18n.tsx._timeIn.years({ n: Math.round(-ago.value / 31536000).toString() }) :
+ ago.value < -2592000 ? i18n.tsx._timeIn.months({ n: Math.round(-ago.value / 2592000).toString() }) :
+ ago.value < -604800 ? i18n.tsx._timeIn.weeks({ n: Math.round(-ago.value / 604800).toString() }) :
+ ago.value < -86400 ? i18n.tsx._timeIn.days({ n: Math.round(-ago.value / 86400).toString() }) :
+ ago.value < -3600 ? i18n.tsx._timeIn.hours({ n: Math.round(-ago.value / 3600).toString() }) :
+ ago.value < -60 ? i18n.tsx._timeIn.minutes({ n: (~~(-ago.value / 60)).toString() }) :
+ i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
);
});
diff --git a/packages/frontend/src/components/global/MkUrl.stories.impl.ts b/packages/frontend/src/components/global/MkUrl.stories.impl.ts
index b35b6114fd..34a4adfe49 100644
--- a/packages/frontend/src/components/global/MkUrl.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkUrl.stories.impl.ts
@@ -1,13 +1,12 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { expect } from '@storybook/jest';
-import { userEvent, waitFor, within } from '@storybook/testing-library';
+import { expect, userEvent, waitFor, within } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../../.storybook/mocks.js';
import MkUrl from './MkUrl.vue';
export const Default = {
@@ -59,8 +58,8 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
- rest.get('/url', (req, res, ctx) => {
- return res(ctx.json({
+ http.get('/url', () => {
+ return HttpResponse.json({
title: 'Misskey Hub',
icon: 'https://misskey-hub.net/favicon.ico',
description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
@@ -74,7 +73,7 @@ export const Default = {
sitename: 'misskey-hub.net',
sensitive: false,
url: 'https://misskey-hub.net/',
- }));
+ });
}),
],
},
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 9a59b5a68e..0c3eee63ff 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/MkUserName.stories.impl.ts b/packages/frontend/src/components/global/MkUserName.stories.impl.ts
index 8f47a6c1ab..88bf4f4e6c 100644
--- a/packages/frontend/src/components/global/MkUserName.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkUserName.stories.impl.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
-import { expect } from '@storybook/jest';
+import { expect } from '@storybook/test';
import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../../.storybook/fakes.js';
import MkUserName from './MkUserName.vue';
diff --git a/packages/frontend/src/components/global/MkUserName.vue b/packages/frontend/src/components/global/MkUserName.vue
index be283ea922..c5bcf53102 100644
--- a/packages/frontend/src/components/global/MkUserName.vue
+++ b/packages/frontend/src/components/global/MkUserName.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/components/global/RouterView.stories.impl.ts b/packages/frontend/src/components/global/RouterView.stories.impl.ts
index 2fe4c53e78..5dfe12b0c9 100644
--- a/packages/frontend/src/components/global/RouterView.stories.impl.ts
+++ b/packages/frontend/src/components/global/RouterView.stories.impl.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 99ed8adbef..06cb30eff1 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -1,10 +1,13 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<KeepAlive :max="defaultStore.state.numberOfPageCache">
+<KeepAlive
+ :max="defaultStore.state.numberOfPageCache"
+ :exclude="pageCacheController"
+>
<Suspense :timeout="0">
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
@@ -16,12 +19,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { inject, onBeforeUnmount, provide, shallowRef, ref } from 'vue';
-import { Resolved, Router } from '@/nirax.js';
+import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
+import { IRouter, Resolved, RouteDef } from '@/nirax.js';
import { defaultStore } from '@/store.js';
+import { globalEvents } from '@/events.js';
+import MkLoadingPage from '@/pages/_loading_.vue';
const props = defineProps<{
- router?: Router;
+ router?: IRouter;
}>();
const router = props.router ?? inject('router');
@@ -46,20 +51,47 @@ function resolveNested(current: Resolved, d = 0): Resolved | null {
}
const current = resolveNested(router.current)!;
-const currentPageComponent = shallowRef(current.route.component);
+const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
const currentPageProps = ref(current.props);
const key = ref(current.route.path + JSON.stringify(Object.fromEntries(current.props)));
function onChange({ resolved, key: newKey }) {
const current = resolveNested(resolved);
- if (current == null) return;
+ if (current == null || 'redirect' in current.route) return;
currentPageComponent.value = current.route.component;
currentPageProps.value = current.props;
key.value = current.route.path + JSON.stringify(Object.fromEntries(current.props));
+
+ nextTick(() => {
+ // ページ遷移完了後に再びキャッシュを有効化
+ if (clearCacheRequested.value) {
+ clearCacheRequested.value = false;
+ }
+ });
}
router.addListener('change', onChange);
+// #region キャッシュ制御
+
+/**
+ * キャッシュクリアが有効になったら、全キャッシュをクリアする
+ *
+ * keepAlive側にwatcherがあるのですぐ消えるとはおもうけど、念のためページ遷移完了まではキャッシュを無効化しておく。
+ * キャッシュ有効時向けにexcludeを使いたい場合は、pageCacheControllerに並列に突っ込むのではなく、下に追記すること
+ */
+const pageCacheController = computed(() => clearCacheRequested.value ? /.*/ : undefined);
+const clearCacheRequested = ref(false);
+
+globalEvents.on('requestClearPageCache', () => {
+ if (_DEV_) console.log('clear page cache requested');
+ if (!clearCacheRequested.value) {
+ clearCacheRequested.value = true;
+ }
+});
+
+// #endregion
+
onBeforeUnmount(() => {
router.removeListener('change', onChange);
});
diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts
deleted file mode 100644
index 2f4d7edabd..0000000000
--- a/packages/frontend/src/components/global/i18n.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { h } from 'vue';
-
-export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
- let str = props.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.substring(0, nextBracketOpen));
- parsed.push({
- arg: str.substring(nextBracketOpen + 1, nextBracketClose),
- });
- }
-
- str = str.substring(nextBracketClose + 1);
- }
-
- return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
-}
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index a3e13c3a50..44d8d59941 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -16,7 +16,7 @@ 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.js';
+import I18n from './global/I18n.vue';
import RouterView from './global/RouterView.vue';
import MkLoading from './global/MkLoading.vue';
import MkError from './global/MkError.vue';
diff --git a/packages/frontend/src/components/page/block.type.ts b/packages/frontend/src/components/page/block.type.ts
deleted file mode 100644
index cdd39339e6..0000000000
--- a/packages/frontend/src/components/page/block.type.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-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[];
-};
-
-export type ImageBlock = BlockBase & {
- type: 'image';
- fileId: string | null;
-};
-
-export type NoteBlock = BlockBase & {
- type: 'note';
- detailed: boolean;
- note: string | null;
-};
-
-export type Block =
- TextBlock | SectionBlock | ImageBlock | NoteBlock;
diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue
index 7dbbaa03b4..164720ac6b 100644
--- a/packages/frontend/src/components/page/page.block.vue
+++ b/packages/frontend/src/components/page/page.block.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,7 +14,6 @@ import XText from './page.text.vue';
import XSection from './page.section.vue';
import XImage from './page.image.vue';
import XNote from './page.note.vue';
-import { Block } from './block.type.js';
function getComponent(type: string) {
switch (type) {
@@ -27,7 +26,7 @@ function getComponent(type: string) {
}
defineProps<{
- block: Block,
+ block: Misskey.entities.PageBlock,
h: number,
page: Misskey.entities.Page,
}>();
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index 29aebf63e5..ced02943db 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,15 +14,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import { ImageBlock } from './block.type.js';
import MediaImage from '@/components/MkMediaImage.vue';
const props = defineProps<{
- block: ImageBlock,
+ block: Misskey.entities.PageBlock,
page: Misskey.entities.Page,
}>();
-const image = ref<Misskey.entities.DriveFile>(props.page.attachedFiles.find(x => x.id === props.block.fileId));
+const image = ref<Misskey.entities.DriveFile | null>(null);
+
+onMounted(() => {
+ image.value = props.page.attachedFiles.find(x => x.id === props.block.fileId) ?? null;
+});
+
</script>
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index d885ebb1d6..7b56494a6e 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,20 +13,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import { NoteBlock } from './block.type.js';
import MkNote from '@/components/MkNote.vue';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
const props = defineProps<{
- block: NoteBlock,
+ block: Misskey.entities.PageBlock,
page: Misskey.entities.Page,
}>();
const note = ref<Misskey.entities.Note | null>(null);
onMounted(() => {
- os.api('notes/show', { noteId: props.block.note })
+ if (props.block.note == null) return;
+ misskeyApi('notes/show', { noteId: props.block.note })
.then(result => {
note.value = result;
});
diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue
index e4e5a43b59..e3d26d924f 100644
--- a/packages/frontend/src/components/page/page.section.vue
+++ b/packages/frontend/src/components/page/page.section.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,12 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
-import { SectionBlock } from './block.type.js';
const XBlock = defineAsyncComponent(() => import('./page.block.vue'));
defineProps<{
- block: SectionBlock,
+ block: Misskey.entities.PageBlock,
h: number,
page: Misskey.entities.Page,
}>();
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index ee6b2dca5b..81a4c4fa93 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -1,11 +1,11 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
- <Mfm :text="block.text" :isNote="false"/>
+ <Mfm :text="block.text ?? ''" :isNote="false"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
</div>
</template>
@@ -14,13 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only
import { defineAsyncComponent } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
-import { TextBlock } from './block.type.js';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
const props = defineProps<{
- block: TextBlock,
+ block: Misskey.entities.PageBlock,
page: Misskey.entities.Page,
}>();
diff --git a/packages/frontend/src/components/page/page.vue b/packages/frontend/src/components/page/page.vue
index 94ca7bdf04..53c70b01f4 100644
--- a/packages/frontend/src/components/page/page.vue
+++ b/packages/frontend/src/components/page/page.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/config.ts b/packages/frontend/src/config.ts
index 2968ab12e6..277dfc12aa 100644
--- a/packages/frontend/src/config.ts
+++ b/packages/frontend/src/config.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -18,7 +18,7 @@ export const langs = _LANGS_;
const preParseLocale = miLocalStorage.getItem('locale');
export let locale = preParseLocale ? JSON.parse(preParseLocale) : null;
export const version = _VERSION_;
-export const instanceName = siteName === 'Misskey' ? host : siteName;
+export const instanceName = siteName === 'Misskey' || siteName == null ? host : siteName;
export const ui = miLocalStorage.getItem('ui');
export const debug = miLocalStorage.getItem('debug') === 'true';
diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts
index 01c224ae2d..0bac4d0b7c 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend/src/const.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -108,4 +108,28 @@ export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://xn--931a.moe/assets/error
export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://xn--931a.moe/assets/not-found.jpg';
export const DEFAULT_INFO_IMAGE_URL = 'https://xn--931a.moe/assets/info.jpg';
-export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
+export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
+export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
+ tada: ['speed=', 'delay='],
+ jelly: ['speed=', 'delay='],
+ twitch: ['speed=', 'delay='],
+ shake: ['speed=', 'delay='],
+ spin: ['speed=', 'delay=', 'left', 'alternate', 'x', 'y'],
+ jump: ['speed=', 'delay='],
+ bounce: ['speed=', 'delay='],
+ flip: ['h', 'v'],
+ x2: [],
+ x3: [],
+ x4: [],
+ scale: ['x=', 'y='],
+ position: ['x=', 'y='],
+ fg: ['color='],
+ bg: ['color='],
+ border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
+ font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
+ blur: [],
+ rainbow: ['speed=', 'delay='],
+ rotate: ['deg='],
+ ruby: [],
+ unixtime: [],
+};
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 6a48159f13..9da3582e1a 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { shallowRef, computed, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js';
-import { api, apiGet } from '@/os.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { get, set } from '@/scripts/idb-proxy.js';
@@ -52,11 +52,11 @@ export async function fetchCustomEmojis(force = false) {
let res;
if (force) {
- res = await api('emojis', {});
+ res = await misskeyApi('emojis', {});
} else {
const lastFetchedAt = await get('lastEmojisFetchedAt');
if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return;
- res = await apiGet('emojis', {});
+ res = await misskeyApiGet('emojis', {});
}
customEmojis.value = res.emojis;
diff --git a/packages/frontend/src/debug.ts b/packages/frontend/src/debug.ts
index 6df65bb763..8bb8012ae3 100644
--- a/packages/frontend/src/debug.ts
+++ b/packages/frontend/src/debug.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts
index dd9691d9e2..23fd1bddf4 100644
--- a/packages/frontend/src/directives/adaptive-bg.ts
+++ b/packages/frontend/src/directives/adaptive-bg.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts
index 220cf4b9a6..b436075fcd 100644
--- a/packages/frontend/src/directives/adaptive-border.ts
+++ b/packages/frontend/src/directives/adaptive-border.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/anim.ts b/packages/frontend/src/directives/anim.ts
index cf49799ef5..d5b6ae4287 100644
--- a/packages/frontend/src/directives/anim.ts
+++ b/packages/frontend/src/directives/anim.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/appear.ts b/packages/frontend/src/directives/appear.ts
index 3fcff4d978..706d4a9ee4 100644
--- a/packages/frontend/src/directives/appear.ts
+++ b/packages/frontend/src/directives/appear.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts
index 2b3cdb27a5..5bb48bbcdd 100644
--- a/packages/frontend/src/directives/click-anime.ts
+++ b/packages/frontend/src/directives/click-anime.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/follow-append.ts b/packages/frontend/src/directives/follow-append.ts
index ae3e31e291..f200f242ed 100644
--- a/packages/frontend/src/directives/follow-append.ts
+++ b/packages/frontend/src/directives/follow-append.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/get-size.ts b/packages/frontend/src/directives/get-size.ts
index 56ff64035f..2655c76c48 100644
--- a/packages/frontend/src/directives/get-size.ts
+++ b/packages/frontend/src/directives/get-size.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts
index 13e548299f..b082b6edf2 100644
--- a/packages/frontend/src/directives/hotkey.ts
+++ b/packages/frontend/src/directives/hotkey.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts
index fcd7c3091e..bda7738ccd 100644
--- a/packages/frontend/src/directives/index.ts
+++ b/packages/frontend/src/directives/index.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
index 4916fcbd8d..bbcc220e09 100644
--- a/packages/frontend/src/directives/panel.ts
+++ b/packages/frontend/src/directives/panel.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts
index cabd155c87..2d724f771e 100644
--- a/packages/frontend/src/directives/ripple.ts
+++ b/packages/frontend/src/directives/ripple.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts
index 5d6ec2928b..b1c1b19907 100644
--- a/packages/frontend/src/directives/tooltip.ts
+++ b/packages/frontend/src/directives/tooltip.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts
index e0fd10047a..0d6c330da1 100644
--- a/packages/frontend/src/directives/user-preview.ts
+++ b/packages/frontend/src/directives/user-preview.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts
index 90d5f6eede..d476aec04a 100644
--- a/packages/frontend/src/events.ts
+++ b/packages/frontend/src/events.ts
@@ -1,9 +1,13 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { EventEmitter } from 'eventemitter3';
+import * as Misskey from 'misskey-js';
-// TODO: 型付け
-export const globalEvents = new EventEmitter();
+export const globalEvents = new EventEmitter<{
+ themeChanged: () => void;
+ clientNotification: (notification: Misskey.entities.Notification) => void;
+ requestClearPageCache: () => void;
+}>();
diff --git a/packages/frontend/src/filters/bytes.ts b/packages/frontend/src/filters/bytes.ts
index d40b020a9e..49b44167d4 100644
--- a/packages/frontend/src/filters/bytes.ts
+++ b/packages/frontend/src/filters/bytes.ts
@@ -1,14 +1,14 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export default (v, digits = 0) => {
if (v == null) return '?';
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB'];
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];
+ return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/(\.[1-9]*)0+$/, '$1').replace(/\.$/, '') + (sizes[i] ?? `e+${ i * 3 }B`);
};
diff --git a/packages/frontend/src/filters/date.ts b/packages/frontend/src/filters/date.ts
index 23541f1094..2ffe93e868 100644
--- a/packages/frontend/src/filters/date.ts
+++ b/packages/frontend/src/filters/date.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/filters/hms.ts b/packages/frontend/src/filters/hms.ts
new file mode 100644
index 0000000000..7f90c92e99
--- /dev/null
+++ b/packages/frontend/src/filters/hms.ts
@@ -0,0 +1,65 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { i18n } from '@/i18n.js';
+
+export function hms(ms: number, options?: {
+ textFormat?: 'colon' | 'locale';
+ enableSeconds?: boolean;
+ enableMs?: boolean;
+}) {
+ const _options = {
+ textFormat: 'colon',
+ enableSeconds: true,
+ enableMs: false,
+ ...options,
+ };
+
+ const res: {
+ h?: string;
+ m?: string;
+ s?: string;
+ ms?: string;
+ } = {};
+
+ // ミリ秒を秒に変換
+ let seconds = Math.floor(ms / 1000);
+
+ // 小数点以下の値(2位まで)
+ const mili = ms - seconds * 1000;
+
+ // 時間を計算
+ const hours = Math.floor(seconds / 3600);
+ res.h = format(hours);
+ seconds %= 3600;
+
+ // 分を計算
+ const minutes = Math.floor(seconds / 60);
+ res.m = format(minutes);
+ seconds %= 60;
+
+ // 残った秒数を取得
+ seconds = seconds % 60;
+ res.s = format(seconds);
+
+ // ミリ秒を取得
+ res.ms = format(Math.floor(mili / 10));
+
+ // 結果を返す
+ if (_options.textFormat === 'locale') {
+ res.h += i18n.ts._time.hour;
+ res.m += i18n.ts._time.minute;
+ res.s += i18n.ts._time.second;
+ }
+ return [
+ res.h.startsWith('00') ? undefined : res.h,
+ res.m,
+ (_options.enableSeconds ? res.s : undefined),
+ ].filter(v => v !== undefined).join(_options.textFormat === 'colon' ? ':' : ' ') + (_options.enableMs ? _options.textFormat === 'colon' ? `.${res.ms}` : ` ${res.ms}` : '');
+}
+
+function format(n: number) {
+ return n.toString().padStart(2, '0');
+}
diff --git a/packages/frontend/src/filters/kmg.ts b/packages/frontend/src/filters/kmg.ts
new file mode 100644
index 0000000000..4dcb5c5800
--- /dev/null
+++ b/packages/frontend/src/filters/kmg.ts
@@ -0,0 +1,9 @@
+export default (v, fractionDigits = 0) => {
+ if (v == null) return 'N/A';
+ if (v === 0) return '0';
+ const sizes = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q'];
+ const isMinus = v < 0;
+ if (isMinus) v = -v;
+ const i = Math.floor(Math.log(v) / Math.log(1000));
+ return (isMinus ? '-' : '') + (v / Math.pow(1000, i)).toFixed(fractionDigits).replace(/(\.[1-9]*)0+$/, '$1').replace(/\.$/, '') + (sizes[i] ?? `e+${ i * 3 }`);
+};
diff --git a/packages/frontend/src/filters/note.ts b/packages/frontend/src/filters/note.ts
index 626d03a096..ce31021469 100644
--- a/packages/frontend/src/filters/note.ts
+++ b/packages/frontend/src/filters/note.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/filters/number.ts b/packages/frontend/src/filters/number.ts
index d0e4f4991f..2e7cc60ff4 100644
--- a/packages/frontend/src/filters/number.ts
+++ b/packages/frontend/src/filters/number.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/filters/user.ts b/packages/frontend/src/filters/user.ts
index 8d20603725..b713d41789 100644
--- a/packages/frontend/src/filters/user.ts
+++ b/packages/frontend/src/filters/user.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts
index 858db74dac..cc9faddb20 100644
--- a/packages/frontend/src/i18n.ts
+++ b/packages/frontend/src/i18n.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -10,6 +10,7 @@ import { I18n } from '@/scripts/i18n.js';
export const i18n = markRaw(new I18n<Locale>(locale));
-export function updateI18n(newLocale) {
- i18n.ts = newLocale;
+export function updateI18n(newLocale: Locale) {
+ // @ts-expect-error -- private field
+ i18n.locale = newLocale;
}
diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
index 8de01e4802..cd84145f40 100644
--- a/packages/frontend/src/index.html
+++ b/packages/frontend/src/index.html
@@ -1,5 +1,5 @@
<!--
- SPDX-FileCopyrightText: syuilo and other misskey contributors
+ SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,13 +16,14 @@
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
- content="default-src 'self';
+ content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
worker-src 'self';
- script-src 'self' 'unsafe-eval';
+ script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com;
style-src 'self' 'unsafe-inline';
- img-src 'self' data: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
+ img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
- connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;"
+ connect-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000 https://newassets.hcaptcha.com;
+ frame-src *;"
/>
<meta property="og:site_name" content="[DEV BUILD] Misskey" />
<meta name="viewport" content="width=device-width, initial-scale=1">
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index b09264dabb..2056023692 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed, reactive } from 'vue';
import * as Misskey from 'misskey-js';
-import { api } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { miLocalStorage } from '@/local-storage.js';
import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@/const.js';
@@ -26,7 +26,7 @@ export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
export async function fetchInstance() {
- const meta = await api('meta', {
+ const meta = await misskeyApi('meta', {
detail: false,
});
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 1ef115978e..3de81c9bb9 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -12,6 +12,7 @@ type Keys =
'latestDonationInfoShownAt' |
'neverShowDonationInfo' |
'neverShowLocalOnlyInfo' |
+ 'modifiedVersionMustProminentlyOfferInAgplV3Section13Read' |
'lastUsed' |
'lang' |
'drafts' |
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 95fd6bf29c..d7910935fa 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -1,9 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed, reactive } from 'vue';
+import { clearCache } from './scripts/clear-cache.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
@@ -12,7 +13,6 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { ui } from '@/config.js';
import { unisonReload } from '@/scripts/unison-reload.js';
-import { clearCache } from './scripts/clear-cache.js';
export const navbarItemDef = reactive({
notifications: {
@@ -117,6 +117,11 @@ export const navbarItemDef = reactive({
show: computed(() => $i != null),
to: '/my/achievements',
},
+ games: {
+ title: 'Misskey Games',
+ icon: 'ti ti-device-gamepad',
+ to: '/games',
+ },
ui: {
title: i18n.ts.switchUi,
icon: 'ti ti-devices',
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index 9755bdcb18..616fb104e6 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -1,24 +1,33 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// NIRAX --- A lightweight router
-import { EventEmitter } from 'eventemitter3';
import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
+import { EventEmitter } from 'eventemitter3';
import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
-type RouteDef = {
+interface RouteDefBase {
path: string;
- component: Component;
query?: Record<string, string>;
loginRequired?: boolean;
name?: string;
hash?: string;
globalCacheKey?: string;
children?: RouteDef[];
-};
+}
+
+interface RouteDefWithComponent extends RouteDefBase {
+ component: Component,
+}
+
+interface RouteDefWithRedirect extends RouteDefBase {
+ redirect: string | ((props: Map<string, string | boolean>) => string);
+}
+
+export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
type ParsedPath = (string | {
name: string;
@@ -27,7 +36,40 @@ type ParsedPath = (string | {
optional?: boolean;
})[];
-export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; };
+export type RouterEvent = {
+ 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<string, string> | null;
+ key: string;
+ }) => void;
+ same: () => void;
+}
+
+export type Resolved = {
+ route: RouteDef;
+ props: Map<string, string | boolean>;
+ child?: Resolved;
+ redirected?: boolean;
+
+ /** @internal */
+ _parsedRoute: {
+ fullPath: string;
+ queryString: string | null;
+ hash: string | null;
+ };
+};
function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath;
@@ -54,34 +96,99 @@ function parsePath(path: string): ParsedPath {
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<string, string> | null;
- key: string;
- }) => void;
- same: () => void;
-}> {
+export interface IRouter extends EventEmitter<RouterEvent> {
+ current: Resolved;
+ currentRef: ShallowRef<Resolved>;
+ currentRoute: ShallowRef<RouteDef>;
+ navHook: ((path: string, flag?: any) => boolean) | null;
+
+ /**
+ * ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
+ */
+ init(): void;
+
+ resolve(path: string): Resolved | null;
+
+ getCurrentPath(): any;
+
+ getCurrentKey(): string;
+
+ push(path: string, flag?: any): void;
+
+ replace(path: string, key?: string | null): void;
+
+ /** @see EventEmitter */
+ eventNames(): Array<EventEmitter.EventNames<RouterEvent>>;
+
+ /** @see EventEmitter */
+ listeners<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T
+ ): Array<EventEmitter.EventListener<RouterEvent, T>>;
+
+ /** @see EventEmitter */
+ listenerCount(
+ event: EventEmitter.EventNames<RouterEvent>
+ ): number;
+
+ /** @see EventEmitter */
+ emit<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ ...args: EventEmitter.EventArgs<RouterEvent, T>
+ ): boolean;
+
+ /** @see EventEmitter */
+ on<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any
+ ): this;
+
+ /** @see EventEmitter */
+ addListener<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any
+ ): this;
+
+ /** @see EventEmitter */
+ once<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any
+ ): this;
+
+ /** @see EventEmitter */
+ removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn?: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any,
+ once?: boolean | undefined
+ ): this;
+
+ /** @see EventEmitter */
+ off<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn?: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any,
+ once?: boolean | undefined
+ ): this;
+
+ /** @see EventEmitter */
+ removeAllListeners(
+ event?: EventEmitter.EventNames<RouterEvent>
+ ): this;
+}
+
+export class Router extends EventEmitter<RouterEvent> implements IRouter {
private routes: RouteDef[];
public current: Resolved;
- public currentRef: ShallowRef<Resolved> = shallowRef();
- public currentRoute: ShallowRef<RouteDef> = shallowRef();
+ public currentRef: ShallowRef<Resolved>;
+ public currentRoute: ShallowRef<RouteDef>;
private currentPath: string;
private isLoggedIn: boolean;
private notFoundPageComponent: Component;
private currentKey = Date.now().toString();
+ private redirectCount = 0;
public navHook: ((path: string, flag?: any) => boolean) | null = null;
@@ -89,13 +196,24 @@ export class Router extends EventEmitter<{
super();
this.routes = routes;
+ this.current = this.resolve(currentPath)!;
+ this.currentRef = shallowRef(this.current);
+ this.currentRoute = shallowRef(this.current.route);
this.currentPath = currentPath;
this.isLoggedIn = isLoggedIn;
this.notFoundPageComponent = notFoundPageComponent;
- this.navigate(currentPath, null, false);
+ }
+
+ public init() {
+ const res = this.navigate(this.currentPath, null, false);
+ this.emit('replace', {
+ path: res._parsedRoute.fullPath,
+ key: this.currentKey,
+ });
}
public resolve(path: string): Resolved | null {
+ const fullPath = path;
let queryString: string | null = null;
let hash: string | null = null;
if (path[0] === '/') path = path.substring(1);
@@ -108,6 +226,12 @@ export class Router extends EventEmitter<{
path = path.substring(0, path.indexOf('?'));
}
+ const _parsedRoute = {
+ fullPath,
+ queryString,
+ hash,
+ };
+
if (_DEV_) console.log('Routing: ', path, queryString);
function check(routes: RouteDef[], _parts: string[]): Resolved | null {
@@ -158,6 +282,7 @@ export class Router extends EventEmitter<{
route,
props,
child,
+ _parsedRoute,
};
} else {
continue forEachRouteLoop;
@@ -183,6 +308,7 @@ export class Router extends EventEmitter<{
return {
route,
props,
+ _parsedRoute,
};
} else {
if (route.children) {
@@ -192,6 +318,7 @@ export class Router extends EventEmitter<{
route,
props,
child,
+ _parsedRoute,
};
} else {
continue forEachRouteLoop;
@@ -210,7 +337,7 @@ export class Router extends EventEmitter<{
return check(this.routes, _parts);
}
- private navigate(path: string, key: string | null | undefined, emitChange = true) {
+ private navigate(path: string, key: string | null | undefined, emitChange = true, _redirected = false): Resolved {
const beforePath = this.currentPath;
this.currentPath = path;
@@ -220,6 +347,20 @@ export class Router extends EventEmitter<{
throw new Error('no route found for: ' + path);
}
+ if ('redirect' in res.route) {
+ let redirectPath: string;
+ if (typeof res.route.redirect === 'function') {
+ redirectPath = res.route.redirect(res.props);
+ } else {
+ redirectPath = res.route.redirect + (res._parsedRoute.queryString ? '?' + res._parsedRoute.queryString : '') + (res._parsedRoute.hash ? '#' + res._parsedRoute.hash : '');
+ }
+ if (_DEV_) console.log('Redirecting to: ', redirectPath);
+ if (_redirected && this.redirectCount++ > 10) {
+ throw new Error('redirect loop detected');
+ }
+ return this.navigate(redirectPath, null, emitChange, true);
+ }
+
if (res.route.loginRequired && !this.isLoggedIn) {
res.route.component = this.notFoundPageComponent;
res.props.set('showLoginPopup', true);
@@ -241,7 +382,11 @@ export class Router extends EventEmitter<{
});
}
- return res;
+ this.redirectCount = 0;
+ return {
+ ...res,
+ redirected: _redirected,
+ };
}
public getCurrentPath() {
@@ -265,7 +410,7 @@ export class Router extends EventEmitter<{
const res = this.navigate(path, null);
this.emit('push', {
beforePath,
- path,
+ path: res._parsedRoute.fullPath,
route: res.route,
props: res.props,
key: this.currentKey,
@@ -273,15 +418,20 @@ export class Router extends EventEmitter<{
}
public replace(path: string, key?: string | null) {
- this.navigate(path, key);
+ const res = this.navigate(path, key);
+ this.emit('replace', {
+ path: res._parsedRoute.fullPath,
+ key: this.currentKey,
+ });
}
}
-export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: Router) {
+export function useScrollPositionManager(getScrollContainer: () => HTMLElement | null, router: IRouter) {
const scrollPosStore = new Map<string, number>();
onMounted(() => {
const scrollContainer = getScrollContainer();
+ if (scrollContainer == null) return;
scrollContainer.addEventListener('scroll', () => {
scrollPosStore.set(router.getCurrentKey(), scrollContainer.scrollTop);
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index b02f6aa640..a4fde6b701 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -1,16 +1,16 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
-import { pendingApiRequestsCount, api, apiGet } from '@/scripts/api.js';
-export { pendingApiRequestsCount, api, apiGet };
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 type { ComponentProps } from 'vue-component-type-helpers';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
@@ -33,7 +33,7 @@ export const apiWithDialog = ((
data: Record<string, any> = {},
token?: string | null | undefined,
) => {
- const promise = api(endpoint, data, token);
+ const promise = misskeyApi(endpoint, data, token);
promiseDialog(promise, null, async (err) => {
let title = null;
let text = err.message + '\n' + (err as any).id;
@@ -83,7 +83,7 @@ export const apiWithDialog = ((
});
return promise;
-}) as typeof api;
+}) as typeof misskeyApi;
export function promiseDialog<T extends Promise<any>>(
promise: T,
@@ -128,9 +128,10 @@ export function promiseDialog<T extends Promise<any>>(
let popupIdCount = 0;
export const popups = ref([]) as Ref<{
- id: any;
- component: any;
+ id: number;
+ component: Component;
props: Record<string, any>;
+ events: Record<string, any>;
}[]>;
const zIndexes = {
@@ -144,7 +145,18 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
return zIndexes[priority];
}
-export async function popup(component: Component, props: Record<string, any>, events = {}, disposeEvent?: string) {
+// InstanceType<typeof Component>['$emit'] だとインターセクション型が返ってきて
+// 使い物にならないので、代わりに ['$props'] から色々省くことで emit の型を生成する
+// FIXME: 何故か *.ts ファイルからだと型がうまく取れない?ことがあるのをなんとかしたい
+type ComponentEmit<T> = T extends new () => { $props: infer Props }
+ ? EmitsExtractor<Props>
+ : never;
+
+type EmitsExtractor<T> = {
+ [K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
+};
+
+export async function popup<T extends Component>(component: T, props: ComponentProps<T>, events: ComponentEmit<T> = {} as ComponentEmit<T>, disposeEvent?: keyof ComponentEmit<T>) {
markRaw(component);
const id = ++popupIdCount;
@@ -420,10 +432,11 @@ export function form(title, form) {
});
}
-export async function selectUser(opts: { includeSelf?: boolean } = {}) {
+export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
return new Promise((resolve, reject) => {
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
includeSelf: opts.includeSelf,
+ localOnly: opts.localOnly,
}, {
ok: user => {
resolve(user);
@@ -621,7 +634,7 @@ export function checkExistence(fileData: ArrayBuffer): Promise<any> {
const data = new FormData();
data.append('md5', getMD5(fileData));
- os.api('drive/files/find-by-hash', {
+ 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
index 9403d862c2..236d3fa14d 100644
--- a/packages/frontend/src/pages/_empty_.vue
+++ b/packages/frontend/src/pages/_empty_.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index 72a12e3c7b..c04f399c6d 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ i18n.ts.youShouldUpgradeClient }}</div>
<MkButton style="margin: 8px auto;" @click="reload">{{ i18n.ts.reload }}</MkButton>
</template>
- <div><MkA to="/docs/general/troubleshooting" class="_link">{{ i18n.ts.troubleshooting }}</MkA></div>
+ <div><MkLink url="https://misskey-hub.net/docs/for-users/resources/troubleshooting/" target="_blank">{{ i18n.ts.troubleshooting }}</MkLink></div>
<div v-if="error" style="opacity: 0.7;">ERROR: {{ error }}</div>
</div>
</div>
@@ -28,8 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
+import MkLink from '@/components/MkLink.vue';
import { version } from '@/config.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -46,7 +47,7 @@ const loaded = ref(false);
const serverIsDead = ref(false);
const meta = ref<Misskey.entities.MetaResponse | null>(null);
-os.api('meta', {
+misskeyApi('meta', {
detail: false,
}).then(res => {
loaded.value = true;
@@ -66,10 +67,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.error,
icon: 'ti ti-alert-triangle',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/_loading_.vue b/packages/frontend/src/pages/_loading_.vue
index 9f3c9fd355..5175979642 100644
--- a/packages/frontend/src/pages/_loading_.vue
+++ b/packages/frontend/src/pages/_loading_.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index f8eced8d72..fd97ab97b9 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<FormLink to="https://github.com/misskey-dev/misskey" external>
<template #icon><i class="ti ti-code"></i></template>
- {{ i18n.ts._aboutMisskey.source }}
+ {{ i18n.ts._aboutMisskey.source }} ({{ i18n.ts._aboutMisskey.original }})
<template #suffix>GitHub</template>
</FormLink>
<FormLink to="https://crowdin.com/project/misskey" external>
@@ -46,6 +46,25 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormLink>
</div>
</FormSection>
+ <FormSection v-if="instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey'">
+ <div class="_gaps_s">
+ <MkInfo>
+ {{ i18n.tsx._aboutMisskey.thisIsModifiedVersion({ name: instance.name }) }}
+ </MkInfo>
+ <FormLink v-if="instance.repositoryUrl" :to="instance.repositoryUrl" external>
+ <template #icon><i class="ti ti-code"></i></template>
+ {{ i18n.ts._aboutMisskey.source }}
+ </FormLink>
+ <FormLink v-if="instance.providesTarball" :to="`/tarball/misskey-${version}.tar.gz`" external>
+ <template #icon><i class="ti ti-download"></i></template>
+ {{ i18n.ts._aboutMisskey.source }}
+ <template #suffix>Tarball</template>
+ </FormLink>
+ <MkInfo v-if="!instance.repositoryUrl && !instance.providesTarball" warn>
+ {{ i18n.ts.sourceCodeIsNotYetProvided }}
+ </MkInfo>
+ </div>
+ </FormSection>
<FormSection>
<template #label>{{ i18n.ts._aboutMisskey.projectMembers }}</template>
<div :class="$style.contributors">
@@ -80,24 +99,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</FormSection>
<FormSection>
- <template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
- <div :class="$style.contributors" style="margin-bottom: 8px;">
- <a href="https://github.com/mei23" target="_blank" :class="$style.contributor">
- <img src="https://avatars.githubusercontent.com/u/30769358?v=4" :class="$style.contributorAvatar">
- <span :class="$style.contributorUsername">@mei23</span>
- </a>
- <a href="https://github.com/rinsuki" target="_blank" :class="$style.contributor">
- <img src="https://avatars.githubusercontent.com/u/6533808?v=4" :class="$style.contributorAvatar">
- <span :class="$style.contributorUsername">@rinsuki</span>
- </a>
- <a href="https://github.com/robflop" target="_blank" :class="$style.contributor">
- <img src="https://avatars.githubusercontent.com/u/8159402?v=4" :class="$style.contributorAvatar">
- <span :class="$style.contributorUsername">@robflop</span>
- </a>
- </div>
- <MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink>
- </FormSection>
- <FormSection>
<template #label>Special thanks</template>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));grid-gap:24px;align-items:center;">
<div>
@@ -136,9 +137,10 @@ import { version } from '@/config.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
-import MkLink from '@/components/MkLink.vue';
+import MkInfo from '@/components/MkInfo.vue';
import { physics } from '@/scripts/physics.js';
import { i18n } from '@/i18n.js';
+import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -213,7 +215,13 @@ const patronsWithIcon = [{
icon: 'https://assets.misskey-hub.net/patrons/302dce2898dd457ba03c3f7dc037900b.jpg',
}, {
name: 'taichan',
- icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.png',
+ icon: 'https://assets.misskey-hub.net/patrons/f981ab0159fb4e2c998e05f7263e1cd9.jpg',
+}, {
+ name: '猫吉よりお',
+ icon: 'https://assets.misskey-hub.net/patrons/a11518b3b34b4536a4bdd7178ba76a7b.jpg',
+}, {
+ name: '有栖かずみ',
+ icon: 'https://assets.misskey-hub.net/patrons/9240e8e0ba294a8884143e99ac7ed6a0.jpg',
}];
const patrons = [
@@ -375,10 +383,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.aboutMisskey,
icon: null,
-});
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index 60b515be3c..d7d526f3ba 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue
index e01c5f7542..24e96b4f4e 100644
--- a/packages/frontend/src/pages/about.federation.vue
+++ b/packages/frontend/src/pages/about.federation.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index 44095348f6..324d1c11de 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -1,103 +1,131 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
- <div class="_gaps_m">
- <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
- <div style="overflow: clip;">
- <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
- <div :class="$style.bannerName">
- <b>{{ instance.name ?? host }}</b>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <MkSpacer v-if="tab === 'overview'" :contentMax="600" :marginMin="20">
+ <div class="_gaps_m">
+ <div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }">
+ <div style="overflow: clip;">
+ <img :src="instance.iconUrl ?? instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.bannerIcon"/>
+ <div :class="$style.bannerName">
+ <b>{{ instance.name ?? host }}</b>
+ </div>
</div>
</div>
- </div>
- <MkKeyValue>
- <template #key>{{ i18n.ts.description }}</template>
- <template #value><div v-html="instance.description"></div></template>
- </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.description }}</template>
+ <template #value><div v-html="instance.description"></div></template>
+ </MkKeyValue>
- <FormSection>
- <div class="_gaps_m">
- <MkKeyValue :copy="version">
- <template #key>Misskey</template>
- <template #value>{{ version }}</template>
- </MkKeyValue>
- <div v-html="i18n.t('poweredByMisskeyDescription', { name: instance.name ?? host })">
+ <FormSection>
+ <div class="_gaps_m">
+ <MkKeyValue :copy="version">
+ <template #key>Misskey</template>
+ <template #value>{{ version }}</template>
+ </MkKeyValue>
+ <div v-html="i18n.tsx.poweredByMisskeyDescription({ name: instance.name ?? host })">
+ </div>
+ <FormLink to="/about-misskey">
+ <template #icon><i class="ti ti-info-circle"></i></template>
+ {{ i18n.ts.aboutMisskey }}
+ </FormLink>
+ <FormLink v-if="instance.repositoryUrl || instance.providesTarball" :to="instance.repositoryUrl || `/tarball/misskey-${version}.tar.gz`" external>
+ <template #icon><i class="ti ti-code"></i></template>
+ {{ i18n.ts.sourceCode }}
+ </FormLink>
+ <MkInfo v-else warn>
+ {{ i18n.ts.sourceCodeIsNotYetProvided }}
+ </MkInfo>
</div>
- <FormLink to="/about-misskey">{{ i18n.ts.aboutMisskey }}</FormLink>
- </div>
- </FormSection>
+ </FormSection>
- <FormSection>
- <div class="_gaps_m">
- <FormSplit>
- <MkKeyValue>
- <template #key>{{ i18n.ts.administrator }}</template>
- <template #value>{{ instance.maintainerName }}</template>
- </MkKeyValue>
- <MkKeyValue>
- <template #key>{{ i18n.ts.contact }}</template>
- <template #value>{{ instance.maintainerEmail }}</template>
- </MkKeyValue>
- </FormSplit>
- <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
- <div class="_gaps_s">
- <MkFolder v-if="instance.serverRules.length > 0">
- <template #label>{{ i18n.ts.serverRules }}</template>
+ <FormSection>
+ <div class="_gaps_m">
+ <FormSplit>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.administrator }}</template>
+ <template #value>{{ instance.maintainerName }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.contact }}</template>
+ <template #value>{{ instance.maintainerEmail }}</template>
+ </MkKeyValue>
+ </FormSplit>
+ <FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>
+ <template #icon><i class="ti ti-user-shield"></i></template>
+ {{ i18n.ts.impressum }}
+ </FormLink>
+ <div class="_gaps_s">
+ <MkFolder v-if="instance.serverRules.length > 0">
+ <template #label>
+ <i class="ti ti-checkup-list"></i>
+ {{ i18n.ts.serverRules }}
+ </template>
- <ol class="_gaps_s" :class="$style.rules">
- <li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
- </ol>
- </MkFolder>
- <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
- <FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink>
+ <ol class="_gaps_s" :class="$style.rules">
+ <li v-for="(item, index) in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
+ </ol>
+ </MkFolder>
+ <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>
+ <template #icon><i class="ti ti-license"></i></template>
+ {{ i18n.ts.termsOfService }}
+ </FormLink>
+ <FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>
+ <template #icon><i class="ti ti-shield-lock"></i></template>
+ {{ i18n.ts.privacyPolicy }}
+ </FormLink>
+ <FormLink v-if="instance.feedbackUrl" :to="instance.feedbackUrl" external>
+ <template #icon><i class="ti ti-message"></i></template>
+ {{ i18n.ts.feedback }}
+ </FormLink>
+ </div>
</div>
- </div>
- </FormSection>
+ </FormSection>
+
+ <FormSuspense :p="initStats">
+ <FormSection>
+ <template #label>{{ i18n.ts.statistics }}</template>
+ <FormSplit>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.users }}</template>
+ <template #value>{{ number(stats.originalUsersCount) }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.notes }}</template>
+ <template #value>{{ number(stats.originalNotesCount) }}</template>
+ </MkKeyValue>
+ </FormSplit>
+ </FormSection>
+ </FormSuspense>
- <FormSuspense :p="initStats">
<FormSection>
- <template #label>{{ i18n.ts.statistics }}</template>
- <FormSplit>
- <MkKeyValue>
- <template #key>{{ i18n.ts.users }}</template>
- <template #value>{{ number(stats.originalUsersCount) }}</template>
- </MkKeyValue>
- <MkKeyValue>
- <template #key>{{ i18n.ts.notes }}</template>
- <template #value>{{ number(stats.originalNotesCount) }}</template>
- </MkKeyValue>
- </FormSplit>
+ <template #label>Well-known resources</template>
+ <div class="_gaps_s">
+ <FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
+ <FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
+ <FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
+ <FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
+ <FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
+ </div>
</FormSection>
- </FormSuspense>
-
- <FormSection>
- <template #label>Well-known resources</template>
- <div class="_gaps_s">
- <FormLink :to="`/.well-known/host-meta`" external>host-meta</FormLink>
- <FormLink :to="`/.well-known/host-meta.json`" external>host-meta.json</FormLink>
- <FormLink :to="`/.well-known/nodeinfo`" external>nodeinfo</FormLink>
- <FormLink :to="`/robots.txt`" external>robots.txt</FormLink>
- <FormLink :to="`/manifest.json`" external>manifest.json</FormLink>
- </div>
- </FormSection>
- </div>
- </MkSpacer>
- <MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
- <XEmojis/>
- </MkSpacer>
- <MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
- <XFederation/>
- </MkSpacer>
- <MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
- <MkInstanceStats/>
- </MkSpacer>
+ </div>
+ </MkSpacer>
+ <MkSpacer v-else-if="tab === 'emojis'" :contentMax="1000" :marginMin="20">
+ <XEmojis/>
+ </MkSpacer>
+ <MkSpacer v-else-if="tab === 'federation'" :contentMax="1000" :marginMin="20">
+ <XFederation/>
+ </MkSpacer>
+ <MkSpacer v-else-if="tab === 'charts'" :contentMax="1000" :marginMin="20">
+ <MkInstanceStats/>
+ </MkSpacer>
+ </MkHorizontalSwipe>
</MkStickyContainer>
</template>
@@ -113,8 +141,10 @@ import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkInfo from '@/components/MkInfo.vue';
import MkInstanceStats from '@/components/MkInstanceStats.vue';
-import * as os from '@/os.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import number from '@/filters/number.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -136,7 +166,7 @@ watch(tab, () => {
}
});
-const initStats = () => os.api('stats', {
+const initStats = () => misskeyApi('stats', {
}).then((res) => {
stats.value = res;
});
@@ -160,10 +190,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-chart-line',
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.instanceInfo,
icon: 'ti ti-info-circle',
-})));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue
index 188ec5caa8..77ab473ea2 100644
--- a/packages/frontend/src/pages/achievements.vue
+++ b/packages/frontend/src/pages/achievements.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -48,10 +48,10 @@ onDeactivated(() => {
}
});
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.achievements,
icon: 'ti ti-medal',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue
index 56b5e7d926..d8311186ab 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -79,6 +79,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
import bytes from '@/filters/bytes.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { iAmAdmin, iAmModerator } from '@/account.js';
@@ -93,8 +94,8 @@ const props = defineProps<{
}>();
async function fetch() {
- file.value = await os.api('drive/files/show', { fileId: props.fileId });
- info.value = await os.api('admin/drive/show-file', { fileId: props.fileId });
+ file.value = await misskeyApi('drive/files/show', { fileId: props.fileId });
+ info.value = await misskeyApi('admin/drive/show-file', { fileId: props.fileId });
isSensitive.value = file.value.isSensitive;
}
@@ -103,7 +104,7 @@ fetch();
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: file.value.name }),
+ text: i18n.tsx.removeAreYouSure({ x: file.value.name }),
});
if (canceled) return;
@@ -113,7 +114,7 @@ async function del() {
}
async function toggleIsSensitive(v) {
- await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v });
+ await misskeyApi('drive/files/update', { fileId: props.fileId, isSensitive: v });
isSensitive.value = v;
}
@@ -139,10 +140,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-code',
}]);
-definePageMetadata(computed(() => ({
- title: file.value ? i18n.ts.file + ': ' + file.value.name : i18n.ts.file,
+definePageMetadata(() => ({
+ title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
icon: 'ti ti-file',
-})));
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index d69d627ce8..2cef55df6c 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -182,9 +182,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
</div>
<div class="charts">
- <div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
+ <div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
<MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
- <div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div>
+ <div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
<MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ user, withoutAll: true }" :detailed="true"></MkChart>
</div>
</div>
@@ -219,11 +219,12 @@ import FormSuspense from '@/components/form/suspense.vue';
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@/config.js';
import { acct } from '@/filters/user.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
-import { iAmAdmin, $i } from '@/account.js';
+import { iAmAdmin, $i, iAmModerator } from '@/account.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -262,11 +263,11 @@ const announcementsPagination = {
const expandedRoles = ref([]);
function createFetcher() {
- return () => Promise.all([os.api('users/show', {
+ return () => Promise.all([misskeyApi('users/show', {
userId: props.userId,
- }), os.api('admin/show-user', {
+ }), misskeyApi('admin/show-user', {
userId: props.userId,
- }), iAmAdmin ? os.api('admin/get-user-ips', {
+ }), iAmAdmin ? misskeyApi('admin/get-user-ips', {
userId: props.userId,
}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => {
user.value = _user;
@@ -278,7 +279,7 @@ function createFetcher() {
moderationNote.value = info.value.moderationNote;
watch(moderationNote, async () => {
- await os.api('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
+ await misskeyApi('admin/update-user-note', { userId: user.value.id, text: moderationNote.value });
await refreshUser();
});
});
@@ -301,12 +302,12 @@ async function resetPassword() {
if (confirm.canceled) {
return;
} else {
- const { password } = await os.api('admin/reset-password', {
+ const { password } = await misskeyApi('admin/reset-password', {
userId: user.value.id,
});
os.alert({
type: 'success',
- text: i18n.t('newPasswordIs', { password }),
+ text: i18n.tsx.newPasswordIs({ password }),
});
}
}
@@ -319,7 +320,7 @@ async function toggleSuspend(v) {
if (confirm.canceled) {
suspended.value = !v;
} else {
- await os.api(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.value.id });
+ await misskeyApi(v ? 'admin/suspend-user' : 'admin/unsuspend-user', { userId: user.value.id });
await refreshUser();
}
}
@@ -331,7 +332,7 @@ async function unsetUserAvatar() {
});
if (confirm.canceled) return;
const process = async () => {
- await os.api('admin/unset-user-avatar', { userId: user.value.id });
+ await misskeyApi('admin/unset-user-avatar', { userId: user.value.id });
os.success();
};
await process().catch(err => {
@@ -350,7 +351,7 @@ async function unsetUserBanner() {
});
if (confirm.canceled) return;
const process = async () => {
- await os.api('admin/unset-user-banner', { userId: user.value.id });
+ await misskeyApi('admin/unset-user-banner', { userId: user.value.id });
os.success();
};
await process().catch(err => {
@@ -369,7 +370,7 @@ async function deleteAllFiles() {
});
if (confirm.canceled) return;
const process = async () => {
- await os.api('admin/delete-all-files-of-a-user', { userId: user.value.id });
+ await misskeyApi('admin/delete-all-files-of-a-user', { userId: user.value.id });
os.success();
};
await process().catch(err => {
@@ -389,7 +390,7 @@ async function deleteAccount() {
if (confirm.canceled) return;
const typed = await os.inputText({
- text: i18n.t('typeToConfirm', { x: user.value?.username }),
+ text: i18n.tsx.typeToConfirm({ x: user.value?.username }),
});
if (typed.canceled) return;
@@ -406,7 +407,7 @@ async function deleteAccount() {
}
async function assignRole() {
- const roles = await os.api('admin/roles/list');
+ const roles = await misskeyApi('admin/roles/list');
const { canceled, result: roleId } = await os.select({
title: i18n.ts._role.chooseRoleToAssign,
@@ -482,7 +483,7 @@ watch(() => props.userId, () => {
});
watch(user, () => {
- os.api('ap/get', {
+ misskeyApi('ap/get', {
uri: user.value.uri ?? `${url}/users/${user.value.id}`,
}).then(res => {
ap.value = res;
@@ -517,10 +518,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-code',
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: user.value ? acct(user.value) : i18n.ts.userInfo,
icon: 'ti ti-user-exclamation',
-})));
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index 0112c9eb7f..f4a8f44955 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index 9050621075..c5a9609e6e 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -1,16 +1,16 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick">
- <template v-if="metadata">
+ <template v-if="pageMetadata">
<div class="titleContainer" @click="showTabsPopup">
- <i v-if="metadata.icon" class="icon" :class="metadata.icon"></i>
+ <i v-if="pageMetadata.icon" class="icon" :class="pageMetadata.icon"></i>
<div class="title">
- <div class="title">{{ metadata.title }}</div>
+ <div class="title">{{ pageMetadata.title }}</div>
</div>
</div>
<div class="tabs">
@@ -39,7 +39,7 @@ import { popupMenu } from '@/os.js';
import { scrollToTop } from '@/scripts/scroll.js';
import MkButton from '@/components/MkButton.vue';
import { globalEvents } from '@/events.js';
-import { injectPageMetadata } from '@/scripts/page-metadata.js';
+import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
type Tab = {
key?: string | null;
@@ -65,7 +65,7 @@ const emit = defineEmits<{
(ev: 'update:tab', key: string);
}>();
-const metadata = injectPageMetadata();
+const pageMetadata = injectReactiveMetadata();
const el = shallowRef<HTMLElement>(null);
const tabRefs = {};
@@ -118,7 +118,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
}
const calcBg = () => {
- const rawBg = metadata?.bg ?? 'var(--bg)';
+ const rawBg = pageMetadata.value?.bg ?? 'var(--bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index 3613189548..d2f4a4b531 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -87,8 +87,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.abuseReports,
icon: 'ti ti-exclamation-circle',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index 5884ac74b5..bd442ccc69 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -96,6 +96,7 @@ import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -108,7 +109,7 @@ const daysOfWeek: string[] = [i18n.ts._weekday.sunday, i18n.ts._weekday.monday,
const filterType = ref('all');
let publishing: boolean | null = null;
-os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
+misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
if (adsResponse != null) {
ads.value = adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
@@ -159,7 +160,7 @@ function add() {
function remove(ad) {
os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: ad.url }),
+ text: i18n.tsx.removeAreYouSure({ x: ad.url }),
}).then(({ canceled }) => {
if (canceled) return;
ads.value = ads.value.filter(x => x !== ad);
@@ -174,7 +175,7 @@ function remove(ad) {
function save(ad) {
if (ad.id == null) {
- os.api('admin/ad/create', {
+ misskeyApi('admin/ad/create', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime(),
startsAt: new Date(ad.startsAt).getTime(),
@@ -191,7 +192,7 @@ function save(ad) {
});
});
} else {
- os.api('admin/ad/update', {
+ misskeyApi('admin/ad/update', {
...ad,
expiresAt: new Date(ad.expiresAt).getTime(),
startsAt: new Date(ad.startsAt).getTime(),
@@ -210,7 +211,7 @@ function save(ad) {
}
function more() {
- os.api('admin/ad/list', { untilId: ads.value.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
+ misskeyApi('admin/ad/list', { untilId: ads.value.reduce((acc, ad) => ad.id != null ? ad : acc).id, publishing: publishing }).then(adsResponse => {
if (adsResponse == null) return;
ads.value = ads.value.concat(adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
@@ -227,7 +228,7 @@ function more() {
}
function refresh() {
- os.api('admin/ad/list', { publishing: publishing }).then(adsResponse => {
+ misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
if (adsResponse == null) return;
ads.value = adsResponse.map(r => {
const exdate = new Date(r.expiresAt);
@@ -254,10 +255,10 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.ads,
icon: 'ti ti-ad',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index e4bbe15955..e7fb62ec1d 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
{{ i18n.ts._announcement.needConfirmationToRead }}
</MkSwitch>
- <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p>
+ <p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
<div class="buttons _buttons">
<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
@@ -79,6 +79,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFolder from '@/components/MkFolder.vue';
@@ -86,7 +87,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
const announcements = ref<any[]>([]);
-os.api('admin/announcements/list').then(announcementResponse => {
+misskeyApi('admin/announcements/list').then(announcementResponse => {
announcements.value = announcementResponse;
});
@@ -108,11 +109,11 @@ function add() {
function del(announcement) {
os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: announcement.title }),
+ text: i18n.tsx.deleteAreYouSure({ x: announcement.title }),
}).then(({ canceled }) => {
if (canceled) return;
announcements.value = announcements.value.filter(x => x !== announcement);
- os.api('admin/announcements/delete', announcement);
+ misskeyApi('admin/announcements/delete', announcement);
});
}
@@ -134,13 +135,13 @@ async function save(announcement) {
}
function more() {
- os.api('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => {
+ misskeyApi('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => {
announcements.value = announcements.value.concat(announcementResponse);
});
}
function refresh() {
- os.api('admin/announcements/list').then(announcementResponse => {
+ misskeyApi('admin/announcements/list').then(announcementResponse => {
announcements.value = announcementResponse;
});
}
@@ -156,8 +157,8 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.announcements,
icon: 'ti ti-speakerphone',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 8c9d670d11..e5e04fdeb8 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkRadios v-model="provider">
<option :value="null">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
<option value="hcaptcha">hCaptcha</option>
+ <option value="mcaptcha">mCaptcha</option>
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
</MkRadios>
@@ -28,6 +29,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCaptcha provider="hcaptcha" :sitekey="hcaptchaSiteKey || '10000000-ffff-ffff-ffff-000000000001'"/>
</FormSlot>
</template>
+ <template v-else-if="provider === 'mcaptcha'">
+ <MkInput v-model="mcaptchaSiteKey">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.mcaptchaSiteKey }}</template>
+ </MkInput>
+ <MkInput v-model="mcaptchaSecretKey">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>{{ i18n.ts.mcaptchaSecretKey }}</template>
+ </MkInput>
+ <MkInput v-model="mcaptchaInstanceUrl">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label>{{ i18n.ts.mcaptchaInstanceUrl }}</template>
+ </MkInput>
+ <FormSlot v-if="mcaptchaSiteKey && mcaptchaInstanceUrl">
+ <template #label>{{ i18n.ts.preview }}</template>
+ <MkCaptcha provider="mcaptcha" :sitekey="mcaptchaSiteKey" :instanceUrl="mcaptchaInstanceUrl"/>
+ </FormSlot>
+ </template>
<template v-else-if="provider === 'recaptcha'">
<MkInput v-model="recaptchaSiteKey">
<template #prefix><i class="ti ti-key"></i></template>
@@ -72,6 +91,7 @@ import MkButton from '@/components/MkButton.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
@@ -80,21 +100,30 @@ const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'
const provider = ref<CaptchaProvider | null>(null);
const hcaptchaSiteKey = ref<string | null>(null);
const hcaptchaSecretKey = ref<string | null>(null);
+const mcaptchaSiteKey = ref<string | null>(null);
+const mcaptchaSecretKey = ref<string | null>(null);
+const mcaptchaInstanceUrl = ref<string | null>(null);
const recaptchaSiteKey = ref<string | null>(null);
const recaptchaSecretKey = ref<string | null>(null);
const turnstileSiteKey = ref<string | null>(null);
const turnstileSecretKey = ref<string | null>(null);
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
hcaptchaSiteKey.value = meta.hcaptchaSiteKey;
hcaptchaSecretKey.value = meta.hcaptchaSecretKey;
+ mcaptchaSiteKey.value = meta.mcaptchaSiteKey;
+ mcaptchaSecretKey.value = meta.mcaptchaSecretKey;
+ mcaptchaInstanceUrl.value = meta.mcaptchaInstanceUrl;
recaptchaSiteKey.value = meta.recaptchaSiteKey;
recaptchaSecretKey.value = meta.recaptchaSecretKey;
turnstileSiteKey.value = meta.turnstileSiteKey;
turnstileSecretKey.value = meta.turnstileSecretKey;
- provider.value = meta.enableHcaptcha ? 'hcaptcha' : meta.enableRecaptcha ? 'recaptcha' : meta.enableTurnstile ? 'turnstile' : null;
+ provider.value = meta.enableHcaptcha ? 'hcaptcha' :
+ meta.enableRecaptcha ? 'recaptcha' :
+ meta.enableTurnstile ? 'turnstile' :
+ meta.enableMcaptcha ? 'mcaptcha' : null;
}
function save() {
@@ -102,6 +131,10 @@ function save() {
enableHcaptcha: provider.value === 'hcaptcha',
hcaptchaSiteKey: hcaptchaSiteKey.value,
hcaptchaSecretKey: hcaptchaSecretKey.value,
+ enableMcaptcha: provider.value === 'mcaptcha',
+ mcaptchaSiteKey: mcaptchaSiteKey.value,
+ mcaptchaSecretKey: mcaptchaSecretKey.value,
+ mcaptchaInstanceUrl: mcaptchaInstanceUrl.value,
enableRecaptcha: provider.value === 'recaptcha',
recaptchaSiteKey: recaptchaSiteKey.value,
recaptchaSecretKey: recaptchaSecretKey.value,
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index 38cce69735..2b559f92c9 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
<template #caption>
- <div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
+ <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
- <div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '192x192px' }) }}</strong></div>
+ <div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
</template>
</MkInput>
@@ -30,10 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
<template #caption>
- <div>{{ i18n.t('_serverSettings.appIconDescription', { host: instance.name ?? host }) }}</div>
+ <div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
<div>{{ i18n.ts._serverSettings.appIconStyleRecommendation }}</div>
- <div><strong>{{ i18n.t('_serverSettings.appIconResolutionMustBe', { resolution: '512x512px' }) }}</strong></div>
+ <div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
</template>
</MkInput>
@@ -76,6 +76,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
+ <MkInput v-model="repositoryUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label>{{ i18n.ts.repositoryUrl }}</template>
+ </MkInput>
+
+ <MkInput v-model="feedbackUrl" type="url">
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #label>{{ i18n.ts.feedbackUrl }}</template>
+ </MkInput>
+
<MkTextarea v-model="manifestJsonOverride">
<template #label>{{ i18n.ts._serverSettings.manifestJsonOverride }}</template>
</MkTextarea>
@@ -101,6 +111,7 @@ import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { instance, fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -119,10 +130,12 @@ const defaultDarkTheme = ref<string | null>(null);
const serverErrorImageUrl = ref<string | null>(null);
const infoImageUrl = ref<string | null>(null);
const notFoundImageUrl = ref<string | null>(null);
+const repositoryUrl = ref<string | null>(null);
+const feedbackUrl = ref<string | null>(null);
const manifestJsonOverride = ref<string>('{}');
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
iconUrl.value = meta.iconUrl;
app192IconUrl.value = meta.app192IconUrl;
app512IconUrl.value = meta.app512IconUrl;
@@ -134,6 +147,8 @@ async function init() {
serverErrorImageUrl.value = meta.serverErrorImageUrl;
infoImageUrl.value = meta.infoImageUrl;
notFoundImageUrl.value = meta.notFoundImageUrl;
+ repositoryUrl.value = meta.repositoryUrl;
+ feedbackUrl.value = meta.feedbackUrl;
manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
}
@@ -147,9 +162,11 @@ function save() {
themeColor: themeColor.value === '' ? null : themeColor.value,
defaultLightTheme: defaultLightTheme.value === '' ? null : defaultLightTheme.value,
defaultDarkTheme: defaultDarkTheme.value === '' ? null : defaultDarkTheme.value,
- infoImageUrl: infoImageUrl.value,
- notFoundImageUrl: notFoundImageUrl.value,
- serverErrorImageUrl: serverErrorImageUrl.value,
+ infoImageUrl: infoImageUrl.value === '' ? null : infoImageUrl.value,
+ notFoundImageUrl: notFoundImageUrl.value === '' ? null : notFoundImageUrl.value,
+ serverErrorImageUrl: serverErrorImageUrl.value === '' ? null : serverErrorImageUrl.value,
+ repositoryUrl: repositoryUrl.value === '' ? null : repositoryUrl.value,
+ feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
}).then(() => {
fetchInstance();
@@ -158,10 +175,10 @@ function save() {
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.branding,
icon: 'ti ti-paint',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue
index 53f556bb64..e092efd92c 100644
--- a/packages/frontend/src/pages/admin/database.vue
+++ b/packages/frontend/src/pages/admin/database.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -21,20 +21,20 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import bytes from '@/filters/bytes.js';
import number from '@/filters/number.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
+const databasePromiseFactory = () => misskeyApi('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.database,
icon: 'ti ti-database',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 2e6ad3b1d3..839b9bee16 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -73,6 +73,7 @@ import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance, instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -87,7 +88,7 @@ const smtpUser = ref<string>('');
const smtpPass = ref<string>('');
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
enableEmail.value = meta.enableEmail;
email.value = meta.email;
smtpSecure.value = meta.smtpSecure;
@@ -129,10 +130,10 @@ function save() {
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.emailServer,
icon: 'ti ti-mail',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index 22dc115fda..ba3eb05e72 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -42,6 +42,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -50,7 +51,7 @@ const deeplAuthKey = ref<string>('');
const deeplIsPro = ref<boolean>(false);
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
deeplAuthKey.value = meta.deeplAuthKey;
deeplIsPro.value = meta.deeplIsPro;
}
@@ -68,10 +69,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.externalServices,
icon: 'ti ti-link',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index bfe9a8c570..de27e1f67a 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -102,10 +102,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.federation,
icon: 'ti ti-whirl',
-})));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index a366b302c7..3fe021e771 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -42,6 +42,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -79,11 +80,11 @@ function show(file) {
async function find() {
const { canceled, result: q } = await os.inputText({
title: i18n.ts.fileIdOrUrl,
- allowEmpty: false,
+ minLength: 1,
});
if (canceled) return;
- os.api('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
+ misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
show(file);
}).catch(err => {
if (err.code === 'NO_SUCH_FILE') {
@@ -107,8 +108,8 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.files,
icon: 'ti ti-cloud',
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 5e92cbd600..d4a41c66cc 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -28,15 +28,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ComputedRef, Ref, onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
+import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
import MkInfo from '@/components/MkInfo.vue';
import { instance } from '@/instance.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
-import { useRouter } from '@/router.js';
-import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
+import { useRouter } from '@/router/supplier.js';
const isEmpty = (x: string | null) => x == null || x === '';
@@ -51,7 +52,7 @@ const indexInfo = {
provide('shouldOmitHeaderTitle', false);
const INFO = ref(indexInfo);
-const childInfo: Ref<ComputedRef<PageMetadata> | null> = ref(null);
+const childInfo = ref<null | PageMetadata>(null);
const narrow = ref(false);
const view = ref(null);
const el = ref<HTMLDivElement | null>(null);
@@ -62,7 +63,7 @@ let noEmailServer = !instance.enableEmail;
const thereIsUnresolvedAbuseReport = ref(false);
const currentPage = computed(() => router.currentRef.value.child);
-os.api('admin/abuse-user-reports', {
+misskeyApi('admin/abuse-user-reports', {
state: 'unresolved',
limit: 1,
}).then(reports => {
@@ -256,17 +257,19 @@ watch(router.currentRef, (to) => {
}
});
-provideMetadataReceiver((info) => {
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
if (info == null) {
childInfo.value = null;
} else {
childInfo.value = info;
- INFO.value.needWideArea = info.value.needWideArea ?? undefined;
+ INFO.value.needWideArea = info.needWideArea ?? undefined;
}
});
+provideReactiveMetadata(INFO);
function invite() {
- os.api('admin/invite/create').then(x => {
+ misskeyApi('admin/invite/create').then(x => {
os.alert({
type: 'info',
text: x[0].code,
@@ -317,7 +320,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(INFO.value);
+definePageMetadata(() => INFO.value);
defineExpose({
header: {
diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue
index 356eca2af6..5167b2e6b2 100644
--- a/packages/frontend/src/pages/admin/instance-block.vue
+++ b/packages/frontend/src/pages/admin/instance-block.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -29,6 +29,7 @@ import MkButton from '@/components/MkButton.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -38,7 +39,7 @@ const silencedHosts = ref<string>('');
const tab = ref('block');
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
blockedHosts.value = meta.blockedHosts.join('\n');
silencedHosts.value = meta.silencedHosts.join('\n');
}
@@ -65,8 +66,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-eye-off',
}]);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.instanceBlocking,
icon: 'ti ti-ban',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue
index 838ef52b14..95727fb14c 100644
--- a/packages/frontend/src/pages/admin/invites.vue
+++ b/packages/frontend/src/pages/admin/invites.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -59,6 +59,7 @@ import { computed, ref, shallowRef } from 'vue';
import XHeader from './_header_.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
@@ -93,14 +94,14 @@ async function createWithOptions() {
count: createCount.value,
};
- const tickets = await os.api('admin/invite/create', options);
+ const tickets = await misskeyApi('admin/invite/create', options);
os.alert({
type: 'success',
title: i18n.ts.inviteCodeCreated,
- text: tickets?.map(x => x.code).join('\n'),
+ text: tickets.map(x => x.code).join('\n'),
});
- tickets?.forEach(ticket => pagingComponent.value?.prepend(ticket));
+ tickets.forEach(ticket => pagingComponent.value?.prepend(ticket));
}
function deleted(id: string) {
@@ -112,10 +113,10 @@ function deleted(id: string) {
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.invite,
icon: 'ti ti-user-plus',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index f6c0b29403..d6cb1e39a7 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -40,6 +40,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
+ <MkTextarea v-model="prohibitedWords">
+ <template #label>{{ i18n.ts.prohibitedWords }}</template>
+ <template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
+ </MkTextarea>
+
<MkTextarea v-model="hiddenTags">
<template #label>{{ i18n.ts.hiddenTags }}</template>
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
@@ -66,6 +71,7 @@ import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -75,16 +81,18 @@ import FormLink from '@/components/form/link.vue';
const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false);
const sensitiveWords = ref<string>('');
+const prohibitedWords = ref<string>('');
const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const tosUrl = ref<string | null>(null);
const privacyPolicyUrl = ref<string | null>(null);
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup;
sensitiveWords.value = meta.sensitiveWords.join('\n');
+ prohibitedWords.value = meta.prohibitedWords.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
tosUrl.value = meta.tosUrl;
@@ -98,6 +106,7 @@ function save() {
tosUrl: tosUrl.value,
privacyPolicyUrl: privacyPolicyUrl.value,
sensitiveWords: sensitiveWords.value.split('\n'),
+ prohibitedWords: prohibitedWords.value.split('\n'),
hiddenTags: hiddenTags.value.split('\n'),
preservedUsernames: preservedUsernames.value.split('\n'),
}).then(() => {
@@ -107,10 +116,10 @@ function save() {
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.moderation,
icon: 'ti ti-shield',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 524c35a943..21d68331cb 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index 8540156d43..5e251b8a6f 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -60,8 +60,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.moderationLogs,
icon: 'ti ti-list-search',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index 7019971e90..4ff5ab09ca 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -90,6 +90,7 @@ import MkInput from '@/components/MkInput.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -110,7 +111,7 @@ const objectStorageSetPublicRead = ref<boolean>(false);
const objectStorageS3ForcePathStyle = ref<boolean>(true);
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
useObjectStorage.value = meta.useObjectStorage;
objectStorageBaseUrl.value = meta.objectStorageBaseUrl;
objectStorageBucket.value = meta.objectStorageBucket;
@@ -148,10 +149,10 @@ function save() {
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.objectStorage,
icon: 'ti ti-cloud',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/other-settings.vue
index 5bb328ac92..651f0ef936 100644
--- a/packages/frontend/src/pages/admin/other-settings.vue
+++ b/packages/frontend/src/pages/admin/other-settings.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -47,6 +47,7 @@ import { ref, computed } from 'vue';
import XHeader from './_header_.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -58,7 +59,7 @@ const enableChartsForRemoteUser = ref<boolean>(false);
const enableChartsForFederatedInstances = ref<boolean>(false);
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
enableServerMachineStats.value = meta.enableServerMachineStats;
enableIdenticonGeneration.value = meta.enableIdenticonGeneration;
enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser;
@@ -85,8 +86,8 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.other,
icon: 'ti ti-adjustments',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 5e67370c2b..79dd6fd5fd 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
@@ -52,7 +52,7 @@ async function renderChart() {
}));
};
- const raw = await os.api('charts/active-users', { limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index 0de62fadea..d4c83f21b6 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, shallowRef, ref } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
import { defaultStore } from '@/store.js';
@@ -65,7 +65,7 @@ onMounted(async () => {
}));
};
- const raw = await os.api('charts/ap-request', { limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const succColor = '#87e000';
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index cfd1c6a566..022b392d2d 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -49,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onMounted, ref } from 'vue';
import XPie, { type InstanceForPie } from './overview.pie.vue';
import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import number from '@/filters/number.js';
import MkNumberDiff from '@/components/MkNumberDiff.vue';
import { i18n } from '@/i18n.js';
@@ -65,13 +66,13 @@ const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip();
onMounted(async () => {
- const chart = await os.apiGet('charts/federation', { limit: 2, span: 'day' });
+ const chart = await misskeyApiGet('charts/federation', { limit: 2, span: 'day' });
federationPubActive.value = chart.pubActive[0];
federationPubActiveDiff.value = chart.pubActive[0] - chart.pubActive[1];
federationSubActive.value = chart.subActive[0];
federationSubActiveDiff.value = chart.subActive[0] - chart.subActive[1];
- os.apiGet('federation/stats', { limit: 10 }).then(res => {
+ misskeyApiGet('federation/stats', { limit: 10 }).then(res => {
topSubInstancesForPie.value = [
...res.topSubInstances.map(x => ({
name: x.host,
diff --git a/packages/frontend/src/pages/admin/overview.heatmap.vue b/packages/frontend/src/pages/admin/overview.heatmap.vue
index 8e3c809353..7b2b142b16 100644
--- a/packages/frontend/src/pages/admin/overview.heatmap.vue
+++ b/packages/frontend/src/pages/admin/overview.heatmap.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/overview.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue
index de34f0c09b..a09db2a6d5 100644
--- a/packages/frontend/src/pages/admin/overview.instances.vue
+++ b/packages/frontend/src/pages/admin/overview.instances.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
import { defaultStore } from '@/store.js';
@@ -28,7 +28,7 @@ const instances = ref<Misskey.entities.FederationInstance[]>([]);
const fetching = ref(true);
const fetch = async () => {
- const fetchedInstances = await os.api('federation/instances', {
+ const fetchedInstances = await misskeyApi('federation/instances', {
sort: '+latestRequestReceivedAt',
limit: 6,
});
diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue
index 3034bdd57e..f0691534c8 100644
--- a/packages/frontend/src/pages/admin/overview.moderators.vue
+++ b/packages/frontend/src/pages/admin/overview.moderators.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,15 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
const moderators = ref<Misskey.entities.UserDetailed[] | null>(null);
const fetching = ref(true);
onMounted(async () => {
- moderators.value = await os.api('admin/show-users', {
+ moderators.value = await misskeyApi('admin/show-users', {
sort: '+lastActiveDate',
state: 'adminOrModerator',
limit: 30,
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index 95c1f57b29..c7a9f2a702 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue
index 38309e351a..2efc17c888 100644
--- a/packages/frontend/src/pages/admin/overview.queue.chart.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index b6b3bf194a..c7478f252a 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/overview.retention.vue b/packages/frontend/src/pages/admin/overview.retention.vue
index 514db663ab..adcb9d5948 100644
--- a/packages/frontend/src/pages/admin/overview.retention.vue
+++ b/packages/frontend/src/pages/admin/overview.retention.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 78f435e731..0f4707f08d 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import MkNumberDiff from '@/components/MkNumberDiff.vue';
import MkNumber from '@/components/MkNumber.vue';
import { i18n } from '@/i18n.js';
@@ -78,17 +78,17 @@ const fetching = ref(true);
onMounted(async () => {
const [_stats, _onlineUsersCount] = await Promise.all([
- os.api('stats', {}),
- os.apiGet('get-online-users-count').then(res => res.count),
+ misskeyApi('stats', {}),
+ misskeyApiGet('get-online-users-count').then(res => res.count),
]);
stats.value = _stats;
onlineUsersCount.value = _onlineUsersCount;
- os.apiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
+ misskeyApiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
usersComparedToThePrevDay.value = stats.value.originalUsersCount - chart.local.total[1];
});
- os.apiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
+ misskeyApiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
notesComparedToThePrevDay.value = stats.value.originalNotesCount - chart.local.total[1];
});
diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue
index 79579367c1..408be88d47 100644
--- a/packages/frontend/src/pages/admin/overview.users.vue
+++ b/packages/frontend/src/pages/admin/overview.users.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import { defaultStore } from '@/store.js';
@@ -28,7 +28,7 @@ const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null);
const fetching = ref(true);
const fetch = async () => {
- const _newUsers = await os.api('admin/show-users', {
+ const _newUsers = await misskeyApi('admin/show-users', {
limit: 5,
sort: '+createdAt',
origin: 'local',
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index 2e0791e24f..1de4dc0dc8 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -79,6 +79,7 @@ import XModerators from './overview.moderators.vue';
import XHeatmap from './overview.heatmap.vue';
import type { InstanceForPie } from './overview.pie.vue';
import * as os from '@/os.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -117,14 +118,14 @@ onMounted(async () => {
magicGrid.listen();
*/
- os.apiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => {
+ misskeyApiGet('charts/federation', { limit: 2, span: 'day' }).then(chart => {
federationPubActive.value = chart.pubActive[0];
federationPubActiveDiff.value = chart.pubActive[0] - chart.pubActive[1];
federationSubActive.value = chart.subActive[0];
federationSubActiveDiff.value = chart.subActive[0] - chart.subActive[1];
});
- os.apiGet('federation/stats', { limit: 10 }).then(res => {
+ misskeyApiGet('federation/stats', { limit: 10 }).then(res => {
topSubInstancesForPie.value = [
...res.topSubInstances.map(x => ({
name: x.host,
@@ -149,18 +150,18 @@ onMounted(async () => {
];
});
- os.api('admin/server-info').then(serverInfoResponse => {
+ misskeyApi('admin/server-info').then(serverInfoResponse => {
serverInfo.value = serverInfoResponse;
});
- os.api('admin/show-users', {
+ misskeyApi('admin/show-users', {
limit: 5,
sort: '+createdAt',
}).then(res => {
newUsers.value = res;
});
- os.api('federation/instances', {
+ misskeyApi('federation/instances', {
sort: '+latestRequestReceivedAt',
limit: 25,
}).then(res => {
@@ -183,10 +184,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.dashboard,
icon: 'ti ti-dashboard',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue
index 05e48f7ac1..02b506d13d 100644
--- a/packages/frontend/src/pages/admin/proxy-account.vue
+++ b/packages/frontend/src/pages/admin/proxy-account.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -28,6 +28,7 @@ import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -36,15 +37,15 @@ const proxyAccount = ref<Misskey.entities.UserDetailed | null>(null);
const proxyAccountId = ref<string | null>(null);
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
proxyAccountId.value = meta.proxyAccountId;
if (proxyAccountId.value) {
- proxyAccount.value = await os.api('users/show', { userId: proxyAccountId.value });
+ proxyAccount.value = await misskeyApi('users/show', { userId: proxyAccountId.value });
}
}
function chooseProxyAccount() {
- os.selectUser().then(user => {
+ os.selectUser({ localOnly: true }).then(user => {
proxyAccount.value = user;
proxyAccountId.value = user.id;
save();
@@ -63,8 +64,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.proxyAccount,
icon: 'ti ti-ghost',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue
index 566670c843..cc18898172 100644
--- a/packages/frontend/src/pages/admin/queue.chart.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 72276c1eed..8d3fe35320 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { markRaw, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import XChart from './queue.chart.chart.vue';
import number from '@/filters/number.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
@@ -105,7 +105,7 @@ const onStatsLog = (statsLog) => {
onMounted(() => {
if (props.domain === 'inbox' || props.domain === 'deliver') {
- os.api(`admin/queue/${props.domain}-delayed`).then(result => {
+ misskeyApi(`admin/queue/${props.domain}-delayed`).then(result => {
jobs.value = result;
});
}
diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue
index 5a8f960cf6..8d77d927d7 100644
--- a/packages/frontend/src/pages/admin/queue.vue
+++ b/packages/frontend/src/pages/admin/queue.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -68,8 +68,8 @@ const headerTabs = computed(() => [{
title: 'Inbox',
}]);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.jobQueue,
icon: 'ti ti-clock-play',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 0056f2bd9f..04982eea1f 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i>
<i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i>
<i v-else class="ti ti-clock" :class="$style.icon"></i>
- <span>{{ i18n.t(`_relayStatus.${relay.status}`) }}</span>
+ <span>{{ i18n.ts._relayStatus[relay.status] }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
@@ -29,6 +29,7 @@ import * as Misskey from 'misskey-js';
import XHeader from './_header_.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -41,7 +42,7 @@ async function addRelay() {
placeholder: i18n.ts.inboxUrl,
});
if (canceled) return;
- os.api('admin/relays/add', {
+ misskeyApi('admin/relays/add', {
inbox,
}).then((relay: any) => {
refresh();
@@ -54,7 +55,7 @@ async function addRelay() {
}
function remove(inbox: string) {
- os.api('admin/relays/remove', {
+ misskeyApi('admin/relays/remove', {
inbox,
}).then(() => {
refresh();
@@ -67,7 +68,7 @@ function remove(inbox: string) {
}
function refresh() {
- os.api('admin/relays/list').then(relayList => {
+ misskeyApi('admin/relays/list').then(relayList => {
relays.value = relayList;
});
}
@@ -83,10 +84,10 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.relays,
icon: 'ti ti-planet',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index 585b50aad6..60f06d50ba 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -28,11 +28,12 @@ import { v4 as uuid } from 'uuid';
import XHeader from './_header_.vue';
import XEditor from './roles.editor.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/router.js';
import MkButton from '@/components/MkButton.vue';
import { rolesCache } from '@/cache.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -44,7 +45,7 @@ const role = ref<Misskey.entities.Role | null>(null);
const data = ref<any>(null);
if (props.id) {
- role.value = await os.api('admin/roles/show', {
+ role.value = await misskeyApi('admin/roles/show', {
roleId: props.id,
});
@@ -86,11 +87,8 @@ async function save() {
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => role.value ? {
- title: i18n.ts._role.edit + ': ' + role.value.name,
- icon: 'ti ti-badge',
-} : {
- title: i18n.ts._role.new,
+definePageMetadata(() => ({
+ title: role.value ? `${i18n.ts._role.edit}: ${role.value.name}` : i18n.ts._role.new,
icon: 'ti ti-badge',
}));
</script>
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index 5ded8d6931..ad9df35dbf 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 9aa7d8dd3c..ab8005045b 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -67,14 +67,15 @@ import XHeader from './_header_.vue';
import XEditor from './roles.editor.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/router.js';
import MkButton from '@/components/MkButton.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPagination from '@/components/MkPagination.vue';
import { infoImageUrl } from '@/instance.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -92,7 +93,7 @@ const usersPagination = {
const expandedItems = ref([]);
-const role = reactive(await os.api('admin/roles/show', {
+const role = reactive(await misskeyApi('admin/roles/show', {
roleId: props.id,
}));
@@ -103,7 +104,7 @@ function edit() {
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: role.name }),
+ text: i18n.tsx.deleteAreYouSure({ x: role.name }),
});
if (canceled) return;
@@ -115,9 +116,7 @@ async function del() {
}
async function assign() {
- const user = await os.selectUser({
- includeSelf: true,
- });
+ const user = await os.selectUser({ includeSelf: true });
const { canceled: canceled2, result: period } = await os.select({
title: i18n.ts.period,
@@ -171,10 +170,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
- title: i18n.ts.role + ': ' + role.name,
+definePageMetadata(() => ({
+ title: `${i18n.ts.role}: ${role.name}`,
icon: 'ti ti-badge',
-})));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 3962e04218..496cb09664 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -233,17 +233,18 @@ import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import MkRolePreview from '@/components/MkRolePreview.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { instance } from '@/instance.js';
-import { useRouter } from '@/router.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { ROLE_POLICIES } from '@/const.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
const baseRoleQ = ref('');
-const roles = await os.api('admin/roles/list');
+const roles = await misskeyApi('admin/roles/list');
const policies = reactive<Record<typeof ROLE_POLICIES[number], any>>({});
for (const ROLE_POLICY of ROLE_POLICIES) {
@@ -269,10 +270,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.roles,
icon: 'ti ti-badges',
-})));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 7070157ca9..cadcf5a8cc 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-shield"></i></template>
<template #label>{{ i18n.ts.botProtection }}</template>
<template v-if="enableHcaptcha" #suffix>hCaptcha</template>
+ <template v-else-if="enableMcaptcha" #suffix>mCaptcha</template>
<template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template>
<template v-else-if="enableTurnstile" #suffix>Turnstile</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
@@ -70,16 +71,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
- <MkSwitch v-model="enableActiveEmailValidation" @update:modelValue="save">
+ <MkSwitch v-model="enableActiveEmailValidation">
<template #label>Enable</template>
</MkSwitch>
- <MkSwitch v-model="enableVerifymailApi" @update:modelValue="save">
+ <MkSwitch v-model="enableVerifymailApi">
<template #label>Use Verifymail.io API</template>
</MkSwitch>
- <MkInput v-model="verifymailAuthKey" @update:modelValue="save">
+ <MkInput v-model="verifymailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Verifymail.io API Auth Key</template>
</MkInput>
+ <MkSwitch v-model="enableTruemailApi">
+ <template #label>Use TrueMail API</template>
+ </MkSwitch>
+ <MkInput v-model="truemailInstance">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>TrueMail API Instance</template>
+ </MkInput>
+ <MkInput v-model="truemailAuthKey">
+ <template #prefix><i class="ti ti-key"></i></template>
+ <template #label>TrueMail API Auth Key</template>
+ </MkInput>
+ <MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
@@ -137,12 +150,14 @@ import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const summalyProxy = ref<string>('');
const enableHcaptcha = ref<boolean>(false);
+const enableMcaptcha = ref<boolean>(false);
const enableRecaptcha = ref<boolean>(false);
const enableTurnstile = ref<boolean>(false);
const sensitiveMediaDetection = ref<string>('none');
@@ -153,12 +168,16 @@ const enableIpLogging = ref<boolean>(false);
const enableActiveEmailValidation = ref<boolean>(false);
const enableVerifymailApi = ref<boolean>(false);
const verifymailAuthKey = ref<string | null>(null);
+const enableTruemailApi = ref<boolean>(false);
+const truemailInstance = ref<string | null>(null);
+const truemailAuthKey = ref<string | null>(null);
const bannedEmailDomains = ref<string>('');
async function init() {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
summalyProxy.value = meta.summalyProxy;
enableHcaptcha.value = meta.enableHcaptcha;
+ enableMcaptcha.value = meta.enableMcaptcha;
enableRecaptcha.value = meta.enableRecaptcha;
enableTurnstile.value = meta.enableTurnstile;
sensitiveMediaDetection.value = meta.sensitiveMediaDetection;
@@ -174,7 +193,10 @@ async function init() {
enableActiveEmailValidation.value = meta.enableActiveEmailValidation;
enableVerifymailApi.value = meta.enableVerifymailApi;
verifymailAuthKey.value = meta.verifymailAuthKey;
- bannedEmailDomains.value = meta.bannedEmailDomains.join('\n');
+ enableTruemailApi.value = meta.enableTruemailApi;
+ truemailInstance.value = meta.truemailInstance;
+ truemailAuthKey.value = meta.truemailAuthKey;
+ bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || "";
}
function save() {
@@ -194,6 +216,9 @@ function save() {
enableActiveEmailValidation: enableActiveEmailValidation.value,
enableVerifymailApi: enableVerifymailApi.value,
verifymailAuthKey: verifymailAuthKey.value,
+ enableTruemailApi: enableTruemailApi.value,
+ truemailInstance: truemailInstance.value,
+ truemailAuthKey: truemailAuthKey.value,
bannedEmailDomains: bannedEmailDomains.value.split('\n'),
}).then(() => {
fetchInstance();
@@ -204,8 +229,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.security,
icon: 'ti ti-lock',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index 231f4ba56f..87318bccce 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -67,10 +67,10 @@ const remove = (index: number): void => {
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.serverRules,
icon: 'ti ti-checkbox',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 224028edf3..c505d70aa9 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -34,6 +34,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</FormSplit>
+ <MkInput v-model="repositoryUrl" type="url">
+ <template #label>{{ i18n.ts.repositoryUrl }}</template>
+ <template #prefix><i class="ti ti-link"></i></template>
+ <template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
+ </MkInput>
+
+ <MkInfo v-if="!instance.providesTarball && !repositoryUrl" warn>
+ {{ i18n.ts.repositoryUrlOrTarballRequired }}
+ </MkInfo>
+
<MkInput v-model="impressumUrl" type="url">
<template #label>{{ i18n.ts.impressumUrl }}</template>
<template #prefix><i class="ti ti-link"></i></template>
@@ -158,7 +168,8 @@ import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
-import { fetchInstance } from '@/instance.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { fetchInstance, instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
@@ -168,6 +179,7 @@ const shortName = ref<string | null>(null);
const description = ref<string | null>(null);
const maintainerName = ref<string | null>(null);
const maintainerEmail = ref<string | null>(null);
+const repositoryUrl = ref<string | null>(null);
const impressumUrl = ref<string | null>(null);
const pinnedUsers = ref<string>('');
const cacheRemoteFiles = ref<boolean>(false);
@@ -184,12 +196,13 @@ const perUserListTimelineCacheMax = ref<number>(0);
const notesPerOneAd = ref<number>(0);
async function init(): Promise<void> {
- const meta = await os.api('admin/meta');
+ const meta = await misskeyApi('admin/meta');
name.value = meta.name;
shortName.value = meta.shortName;
description.value = meta.description;
maintainerName.value = meta.maintainerName;
maintainerEmail.value = meta.maintainerEmail;
+ repositoryUrl.value = meta.repositoryUrl;
impressumUrl.value = meta.impressumUrl;
pinnedUsers.value = meta.pinnedUsers.join('\n');
cacheRemoteFiles.value = meta.cacheRemoteFiles;
@@ -213,6 +226,7 @@ async function save(): void {
description: description.value,
maintainerName: maintainerName.value,
maintainerEmail: maintainerEmail.value,
+ repositoryUrl: repositoryUrl.value,
impressumUrl: impressumUrl.value,
pinnedUsers: pinnedUsers.value.split('\n'),
cacheRemoteFiles: cacheRemoteFiles.value,
@@ -234,10 +248,10 @@ async function save(): void {
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.general,
icon: 'ti ti-settings',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index ea4c231af2..06317760d2 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -90,7 +90,7 @@ const pagination = {
};
function searchUser() {
- os.selectUser().then(user => {
+ os.selectUser({ includeSelf: true }).then(user => {
show(user);
});
}
@@ -137,10 +137,10 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.users,
icon: 'ti ti-users',
-})));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue
index 9e85e81f19..b31807f9f5 100644
--- a/packages/frontend/src/pages/ads.vue
+++ b/packages/frontend/src/pages/ads.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -20,9 +20,9 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.ads,
icon: 'ti ti-ad',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 8eca403707..bcd6eb7c0f 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,34 +7,36 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
- <div class="_gaps">
- <MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo>
- <MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps">
- <section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
- <div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
- <div :class="$style.header">
- <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
- <span style="margin-right: 0.5em;">
- <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
- <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
- <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
- <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
- </span>
- <span>{{ announcement.title }}</span>
- </div>
- <div :class="$style.content">
- <Mfm :text="announcement.text"/>
- <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
- <div style="opacity: 0.7; font-size: 85%;">
- <MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div :key="tab" class="_gaps">
+ <MkInfo v-if="$i && $i.hasUnreadAnnouncement && tab === 'current'" warn>{{ i18n.ts.youHaveUnreadAnnouncements }}</MkInfo>
+ <MkPagination ref="paginationEl" :key="tab" v-slot="{items}" :pagination="tab === 'current' ? paginationCurrent : paginationPast" class="_gaps">
+ <section v-for="announcement in items" :key="announcement.id" class="_panel" :class="$style.announcement">
+ <div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
+ <div :class="$style.header">
+ <span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
+ <span style="margin-right: 0.5em;">
+ <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
+ <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
+ <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
+ <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+ </span>
+ <span>{{ announcement.title }}</span>
</div>
- </div>
- <div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
- <MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
- </div>
- </section>
- </MkPagination>
- </div>
+ <div :class="$style.content">
+ <Mfm :text="announcement.text"/>
+ <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
+ <div style="opacity: 0.7; font-size: 85%;">
+ <MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
+ </div>
+ </div>
+ <div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
+ <MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
+ </div>
+ </section>
+ </MkPagination>
+ </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -44,7 +46,9 @@ import { ref, computed } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i, updateAccount } from '@/account.js';
@@ -74,7 +78,7 @@ async function read(announcement) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts._announcement.readConfirmTitle,
- text: i18n.t('_announcement.readConfirmText', { title: announcement.title }),
+ text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }),
});
if (confirm.canceled) return;
}
@@ -84,7 +88,7 @@ async function read(announcement) {
a.isRead = true;
return a;
});
- os.api('i/read-announcement', { announcementId: announcement.id });
+ misskeyApi('i/read-announcement', { announcementId: announcement.id });
updateAccount({
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== announcement.id),
});
@@ -102,10 +106,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-point',
}]);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.announcements,
icon: 'ti ti-speakerphone',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index cba54790ce..273250d1d0 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -29,9 +29,10 @@ import * as Misskey from 'misskey-js';
import MkTimeline from '@/components/MkTimeline.vue';
import { scroll } from '@/scripts/scroll.js';
import * as os from '@/os.js';
-import { useRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -73,7 +74,7 @@ function focus() {
}
watch(() => props.antennaId, async () => {
- antenna.value = await os.api('antennas/show', {
+ antenna.value = await misskeyApi('antennas/show', {
antennaId: props.antennaId,
});
}, { immediate: true });
@@ -90,10 +91,10 @@ const headerActions = computed(() => antenna.value ? [{
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => antenna.value ? {
- title: antenna.value.name,
+definePageMetadata(() => ({
+ title: antenna.value ? antenna.value.name : i18n.ts.antennas,
icon: 'ti ti-antenna',
-} : null));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue
index 0cd4a8dae8..30f12a8fb3 100644
--- a/packages/frontend/src/pages/api-console.vue
+++ b/packages/frontend/src/pages/api-console.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -41,7 +41,7 @@ import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const body = ref('{}');
@@ -51,14 +51,14 @@ const sending = ref(false);
const res = ref('');
const withCredential = ref(true);
-os.api('endpoints').then(endpointResponse => {
+misskeyApi('endpoints').then(endpointResponse => {
endpoints.value = endpointResponse;
});
function send() {
sending.value = true;
const requestBody = JSON5.parse(body.value);
- os.api(endpoint.value as keyof Endpoints, requestBody, requestBody.i || (withCredential.value ? undefined : null)).then(resp => {
+ misskeyApi(endpoint.value as keyof Endpoints, requestBody, requestBody.i || (withCredential.value ? undefined : null)).then(resp => {
sending.value = false;
res.value = JSON5.stringify(resp, null, 2);
}, err => {
@@ -68,7 +68,7 @@ function send() {
}
function onEndpointChange() {
- os.api('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => {
+ misskeyApi('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => {
const endpointBody = {};
for (const p of resp.params) {
endpointBody[p.name] =
@@ -87,8 +87,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: 'API console',
icon: 'ti ti-terminal-2',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue
index 8a17e5895d..f4fb2ef4d5 100644
--- a/packages/frontend/src/pages/auth.form.vue
+++ b/packages/frontend/src/pages/auth.form.vue
@@ -1,17 +1,17 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<section>
<div v-if="app.permission.length > 0">
- <p>{{ i18n.t('_auth.permission', { name }) }}</p>
+ <p>{{ i18n.tsx._auth.permission({ name }) }}</p>
<ul>
- <li v-for="p in app.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
+ <li v-for="p in app.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
</ul>
</div>
- <div>{{ i18n.t('_auth.shareAccess', { name: `${name} (${app.id})` }) }}</div>
+ <div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div>
<div :class="$style.buttons">
<MkButton inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
<MkButton inline primary @click="accept">{{ i18n.ts.accept }}</MkButton>
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -44,7 +44,7 @@ const name = computed(() => {
});
function cancel() {
- os.api('auth/deny', {
+ misskeyApi('auth/deny', {
token: props.session.token,
}).then(() => {
emit('denied');
@@ -52,7 +52,7 @@ function cancel() {
}
function accept() {
- os.api('auth/accept', {
+ misskeyApi('auth/accept', {
token: props.session.token,
}).then(() => {
emit('accepted');
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index 1b342647fb..d8f8d0b428 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<h1>{{ i18n.ts._auth.denied }}</h1>
</div>
<div v-if="state == 'accepted' && session">
- <h1>{{ session.app.isAuthorized ? i18n.t('already-authorized') : i18n.ts.allowed }}</h1>
+ <h1>{{ session.app.isAuthorized ? i18n.ts['already-authorized'] : i18n.ts.allowed }}</h1>
<p v-if="session.app.callbackUrl">
{{ i18n.ts._auth.callback }}
<MkEllipsis/>
@@ -46,7 +46,7 @@ import { onMounted, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import XForm from './auth.form.vue';
import MkSignin from '@/components/MkSignin.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i, login } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
@@ -75,13 +75,13 @@ onMounted(async () => {
if (!$i) return;
try {
- session.value = await os.api('auth/session/show', {
+ session.value = await misskeyApi('auth/session/show', {
token: props.token,
});
// 既に連携していた場合
if (session.value.app.isAuthorized) {
- await os.api('auth/accept', {
+ await misskeyApi('auth/accept', {
token: session.value.token,
});
accepted();
@@ -97,10 +97,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts._auth.shareAccessTitle,
icon: 'ti ti-apps',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index 87964ac697..ad9ec3c4ee 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -40,6 +40,7 @@ import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkFolder from '@/components/MkFolder.vue';
@@ -59,11 +60,11 @@ function add() {
function del(avatarDecoration) {
os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: avatarDecoration.name }),
+ text: i18n.tsx.deleteAreYouSure({ x: avatarDecoration.name }),
}).then(({ canceled }) => {
if (canceled) return;
avatarDecorations.value = avatarDecorations.value.filter(x => x !== avatarDecoration);
- os.api('admin/avatar-decorations/delete', avatarDecoration);
+ misskeyApi('admin/avatar-decorations/delete', avatarDecoration);
});
}
@@ -77,7 +78,7 @@ async function save(avatarDecoration) {
}
function load() {
- os.api('admin/avatar-decorations/list').then(_avatarDecorations => {
+ misskeyApi('admin/avatar-decorations/list').then(_avatarDecorations => {
avatarDecorations.value = _avatarDecorations;
});
}
@@ -93,8 +94,8 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.avatarDecorations,
icon: 'ti ti-sparkles',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index fcbd03553d..d3f4a65b89 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -76,12 +76,13 @@ import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import { selectFile } from '@/scripts/select-file.js';
import * as os from '@/os.js';
-import { useRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkTextarea from '@/components/MkTextarea.vue';
+import { useRouter } from '@/router/supplier.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@@ -105,7 +106,7 @@ watch(() => bannerId.value, async () => {
if (bannerId.value == null) {
bannerUrl.value = null;
} else {
- bannerUrl.value = (await os.api('drive/files/show', {
+ bannerUrl.value = (await misskeyApi('drive/files/show', {
fileId: bannerId.value,
})).url;
}
@@ -114,7 +115,7 @@ watch(() => bannerId.value, async () => {
async function fetchChannel() {
if (props.channelId == null) return;
- channel.value = await os.api('channels/show', {
+ channel.value = await misskeyApi('channels/show', {
channelId: props.channelId,
});
@@ -173,13 +174,13 @@ function save() {
async function archive() {
const { canceled } = await os.confirm({
type: 'warning',
- title: i18n.t('channelArchiveConfirmTitle', { name: name.value }),
+ title: i18n.tsx.channelArchiveConfirmTitle({ name: name.value }),
text: i18n.ts.channelArchiveConfirmDescription,
});
if (canceled) return;
- os.api('channels/update', {
+ misskeyApi('channels/update', {
channelId: props.channelId,
isArchived: true,
}).then(() => {
@@ -201,11 +202,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => props.channelId ? {
- title: i18n.ts._channel.edit,
- icon: 'ti ti-device-tv',
-} : {
- title: i18n.ts._channel.create,
+definePageMetadata(() => ({
+ title: props.channelId ? i18n.ts._channel.edit : i18n.ts._channel.create,
icon: 'ti ti-device-tv',
}));
</script>
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index af09189654..611ae6feca 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,53 +7,55 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :class="$style.main">
- <div v-if="channel && tab === 'overview'" class="_gaps">
- <div class="_panel" :class="$style.bannerContainer">
- <XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
- <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
- <MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton>
- <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
- <div :class="$style.bannerStatus">
- <div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
- <div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="channel && tab === 'overview'" key="overview" class="_gaps">
+ <div class="_panel" :class="$style.bannerContainer">
+ <XChannelFollowButton :channel="channel" :full="true" :class="$style.subscribe"/>
+ <MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" asLike class="button" rounded primary :class="$style.favorite" @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
+ <MkButton v-else v-tooltip="i18n.ts.favorite" asLike class="button" rounded :class="$style.favorite" @click="favorite()"><i class="ti ti-star"></i></MkButton>
+ <div :style="{ backgroundImage: channel.bannerUrl ? `url(${channel.bannerUrl})` : undefined }" :class="$style.banner">
+ <div :class="$style.bannerStatus">
+ <div><i class="ti ti-users ti-fw"></i><I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.usersCount }}</b></template></I18n></div>
+ <div><i class="ti ti-pencil ti-fw"></i><I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"><template #n><b>{{ channel.notesCount }}</b></template></I18n></div>
+ </div>
+ <div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
+ <div :class="$style.bannerFade"></div>
+ </div>
+ <div v-if="channel.description" :class="$style.description">
+ <Mfm :text="channel.description" :isNote="false"/>
</div>
- <div v-if="channel.isSensitive" :class="$style.sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
- <div :class="$style.bannerFade"></div>
- </div>
- <div v-if="channel.description" :class="$style.description">
- <Mfm :text="channel.description" :isNote="false"/>
</div>
- </div>
- <MkFoldableSection>
- <template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
- <div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
- <MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
- </div>
- </MkFoldableSection>
- </div>
- <div v-if="channel && tab === 'timeline'" class="_gaps">
- <MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
+ <MkFoldableSection>
+ <template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
+ <div v-if="channel.pinnedNotes && channel.pinnedNotes.length > 0" class="_gaps">
+ <MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
+ </div>
+ </MkFoldableSection>
+ </div>
+ <div v-if="channel && tab === 'timeline'" key="timeline" class="_gaps">
+ <MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
- <!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
- <MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
+ <!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
+ <MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
- <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
- </div>
- <div v-else-if="tab === 'featured'">
- <MkNotes :pagination="featuredPagination"/>
- </div>
- <div v-else-if="tab === 'search'">
- <div class="_gaps">
- <div>
- <MkInput v-model="searchQuery" @enter="search()">
- <template #prefix><i class="ti ti-search"></i></template>
- </MkInput>
- <MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
+ <MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
+ </div>
+ <div v-else-if="tab === 'featured'" key="featured">
+ <MkNotes :pagination="featuredPagination"/>
+ </div>
+ <div v-else-if="tab === 'search'" key="search">
+ <div class="_gaps">
+ <div>
+ <MkInput v-model="searchQuery" @enter="search()">
+ <template #prefix><i class="ti ti-search"></i></template>
+ </MkInput>
+ <MkButton primary rounded style="margin-top: 8px;" @click="search()">{{ i18n.ts.search }}</MkButton>
+ </div>
+ <MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
</div>
- <MkNotes v-if="searchPagination" :key="searchKey" :pagination="searchPagination"/>
</div>
- </div>
+ </MkHorizontalSwipe>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
@@ -74,7 +76,7 @@ import MkPostForm from '@/components/MkPostForm.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
import * as os from '@/os.js';
-import { useRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i, iAmModerator } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -87,10 +89,12 @@ import { defaultStore } from '@/store.js';
import MkNote from '@/components/MkNote.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { PageHeaderItem } from '@/types/page-header.js';
import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { miLocalStorage } from '@/local-storage.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -99,6 +103,7 @@ const props = defineProps<{
}>();
const tab = ref('overview');
+
const channel = ref<Misskey.entities.Channel | null>(null);
const favorited = ref(false);
const searchQuery = ref('');
@@ -113,7 +118,7 @@ const featuredPagination = computed(() => ({
}));
watch(() => props.channelId, async () => {
- channel.value = await os.api('channels/show', {
+ channel.value = await misskeyApi('channels/show', {
channelId: props.channelId,
});
favorited.value = channel.value.isFavorited ?? false;
@@ -253,10 +258,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-search',
}]);
-definePageMetadata(computed(() => channel.value ? {
- title: channel.value.name,
+definePageMetadata(() => ({
+ title: channel.value ? channel.value.name : i18n.ts.channel,
icon: 'ti ti-device-tv',
-} : null));
+}));
</script>
<style lang="scss" module>
@@ -267,6 +272,7 @@ definePageMetadata(computed(() => channel.value ? {
.footer {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
+ background: var(--acrylicBg);
border-top: solid 0.5px var(--divider);
}
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index b7cc5cd36e..bde1650754 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,44 +7,46 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700">
- <div v-if="tab === 'search'">
- <div class="_gaps">
- <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
- <template #prefix><i class="ti ti-search"></i></template>
- </MkInput>
- <MkRadios v-model="searchType" @update:modelValue="search()">
- <option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
- <option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
- </MkRadios>
- <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
- </div>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="tab === 'search'" key="search">
+ <div class="_gaps">
+ <MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
+ <template #prefix><i class="ti ti-search"></i></template>
+ </MkInput>
+ <MkRadios v-model="searchType" @update:modelValue="search()">
+ <option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
+ <option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
+ </MkRadios>
+ <MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
+ </div>
- <MkFoldableSection v-if="channelPagination">
- <template #header>{{ i18n.ts.searchResult }}</template>
- <MkChannelList :key="key" :pagination="channelPagination"/>
- </MkFoldableSection>
- </div>
- <div v-if="tab === 'featured'">
- <MkPagination v-slot="{items}" :pagination="featuredPagination">
- <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
- </MkPagination>
- </div>
- <div v-else-if="tab === 'favorites'">
- <MkPagination v-slot="{items}" :pagination="favoritesPagination">
- <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
- </MkPagination>
- </div>
- <div v-else-if="tab === 'following'">
- <MkPagination v-slot="{items}" :pagination="followingPagination">
- <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
- </MkPagination>
- </div>
- <div v-else-if="tab === 'owned'">
- <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
- <MkPagination v-slot="{items}" :pagination="ownedPagination">
- <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
- </MkPagination>
- </div>
+ <MkFoldableSection v-if="channelPagination">
+ <template #header>{{ i18n.ts.searchResult }}</template>
+ <MkChannelList :key="key" :pagination="channelPagination"/>
+ </MkFoldableSection>
+ </div>
+ <div v-if="tab === 'featured'" key="featured">
+ <MkPagination v-slot="{items}" :pagination="featuredPagination">
+ <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
+ </MkPagination>
+ </div>
+ <div v-else-if="tab === 'favorites'" key="favorites">
+ <MkPagination v-slot="{items}" :pagination="favoritesPagination">
+ <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
+ </MkPagination>
+ </div>
+ <div v-else-if="tab === 'following'" key="following">
+ <MkPagination v-slot="{items}" :pagination="followingPagination">
+ <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
+ </MkPagination>
+ </div>
+ <div v-else-if="tab === 'owned'" key="owned">
+ <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
+ <MkPagination v-slot="{items}" :pagination="ownedPagination">
+ <MkChannelPreview v-for="channel in items" :key="channel.id" class="_margin" :channel="channel"/>
+ </MkPagination>
+ </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -58,9 +60,10 @@ import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkButton from '@/components/MkButton.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { useRouter } from '@/router.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -149,8 +152,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-edit',
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.channel,
icon: 'ti ti-device-tv',
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/clicker.vue b/packages/frontend/src/pages/clicker.vue
index 5b194881d1..9e9b5e8688 100644
--- a/packages/frontend/src/pages/clicker.vue
+++ b/packages/frontend/src/pages/clicker.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,10 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only
import MkClickerGame from '@/components/MkClickerGame.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-definePageMetadata({
+definePageMetadata(() => ({
title: '🍪👈',
icon: 'ti ti-cookie',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index ec9876f70c..c38cc117bc 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -32,6 +32,7 @@ import MkNotes from '@/components/MkNotes.vue';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { url } from '@/config.js';
import MkButton from '@/components/MkButton.vue';
@@ -56,7 +57,7 @@ const pagination = {
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
watch(() => props.clipId, async () => {
- clip.value = await os.api('clips/show', {
+ clip.value = await misskeyApi('clips/show', {
clipId: props.clipId,
});
favorited.value = clip.value.isFavorited;
@@ -144,7 +145,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
handler: async (): Promise<void> => {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: clip.value.name }),
+ text: i18n.tsx.deleteAreYouSure({ x: clip.value.name }),
});
if (canceled) return;
@@ -156,10 +157,10 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
},
}] : null);
-definePageMetadata(computed(() => clip.value ? {
- title: clip.value.name,
+definePageMetadata(() => ({
+ title: clip.value ? clip.value.name : i18n.ts.clip,
icon: 'ti ti-paperclip',
-} : null));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 011857688d..3e2332e408 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -82,6 +82,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
import FormSplit from '@/components/form/split.vue';
import { selectFile } from '@/scripts/select-file.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -177,7 +178,7 @@ const menu = (ev: MouseEvent) => {
icon: 'ti ti-download',
text: i18n.ts.export,
action: async () => {
- os.api('export-custom-emojis', {
+ misskeyApi('export-custom-emojis', {
})
.then(() => {
os.alert({
@@ -196,7 +197,7 @@ const menu = (ev: MouseEvent) => {
text: i18n.ts.import,
action: async () => {
const file = await selectFile(ev.currentTarget ?? ev.target);
- os.api('admin/emoji/import-zip', {
+ misskeyApi('admin/emoji/import-zip', {
fileId: file.id,
})
.then(() => {
@@ -304,10 +305,10 @@ const headerTabs = computed(() => [{
title: i18n.ts.remote,
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.customEmojis,
icon: 'ti ti-icons',
-})));
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 1a2fc197f9..8077edff5f 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -79,7 +79,8 @@ import bytes from '@/filters/bytes.js';
import { infoImageUrl } from '@/instance.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
-import { useRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -94,7 +95,7 @@ const isImage = computed(() => file.value?.type.startsWith('image/'));
async function fetch() {
fetching.value = true;
- file.value = await os.api('drive/files/show', {
+ file.value = await misskeyApi('drive/files/show', {
fileId: props.fileId,
}).catch((err) => {
console.error(err);
@@ -179,7 +180,7 @@ async function deleteFile() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('driveFileDeleteConfirm', { name: file.value.name }),
+ text: i18n.tsx.driveFileDeleteConfirm({ name: file.value.name }),
});
if (canceled) return;
diff --git a/packages/frontend/src/pages/drive.file.notes.vue b/packages/frontend/src/pages/drive.file.notes.vue
index ee1a0ee9b0..ca63d43747 100644
--- a/packages/frontend/src/pages/drive.file.notes.vue
+++ b/packages/frontend/src/pages/drive.file.notes.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/drive.file.vue b/packages/frontend/src/pages/drive.file.vue
index 2c1e5d20a7..5711ec8b3a 100644
--- a/packages/frontend/src/pages/drive.file.vue
+++ b/packages/frontend/src/pages/drive.file.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -9,13 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/>
</template>
- <MkSpacer v-if="tab === 'info'" :contentMax="800">
- <XFileInfo :fileId="fileId"/>
- </MkSpacer>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <MkSpacer v-if="tab === 'info'" key="info" :contentMax="800">
+ <XFileInfo :fileId="fileId"/>
+ </MkSpacer>
- <MkSpacer v-else-if="tab === 'notes'" :contentMax="800">
- <XNotes :fileId="fileId"/>
- </MkSpacer>
+ <MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800">
+ <XNotes :fileId="fileId"/>
+ </MkSpacer>
+ </MkHorizontalSwipe>
</MkStickyContainer>
</template>
@@ -23,6 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref, defineAsyncComponent } from 'vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const props = defineProps<{
fileId: string;
@@ -45,8 +48,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-pencil',
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts._fileViewer.title,
icon: 'ti ti-file',
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue
index f260ab0543..25e140f67f 100644
--- a/packages/frontend/src/pages/drive.vue
+++ b/packages/frontend/src/pages/drive.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,9 +22,9 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: folder.value ? folder.value.name : i18n.ts.drive,
icon: 'ti ti-cloud',
hideHeader: true,
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
new file mode 100644
index 0000000000..d9881cebbf
--- /dev/null
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -0,0 +1,1517 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkSpacer :contentMax="800">
+ <div :class="$style.root">
+ <div v-if="!gameLoaded" :class="$style.loadingScreen">
+ <div>
+ Loading...
+ </div>
+ </div>
+ <!-- ↓に対してTransitionコンポーネントを使うと何故かkeyを指定していてもキャッシュが効かず様々なコンポーネントが都度再評価されてパフォーマンスが低下する -->
+ <div v-show="gameLoaded" class="_gaps_s">
+ <div v-if="readyGo === 'ready'" :class="$style.readyGo_bg">
+ </div>
+ <Transition
+ :enterActiveClass="$style.transition_zoom_enterActive"
+ :leaveActiveClass="$style.transition_zoom_leaveActive"
+ :enterFromClass="$style.transition_zoom_enterFrom"
+ :leaveToClass="$style.transition_zoom_leaveTo"
+ :moveClass="$style.transition_zoom_move"
+ mode="default"
+ >
+ <div v-if="readyGo === 'ready'" :class="$style.readyGo_ready">
+ <img src="/client-assets/drop-and-fusion/ready.png" :class="$style.readyGo_img"/>
+ </div>
+ <div v-else-if="readyGo === 'go'" :class="$style.readyGo_go">
+ <img src="/client-assets/drop-and-fusion/go.png" :class="$style.readyGo_img"/>
+ </div>
+ </Transition>
+
+ <div :class="$style.header">
+ <div :class="[$style.frame, $style.headerTitle]">
+ <div :class="$style.frameInner">
+ <b>BUBBLE GAME</b>
+ <div>- {{ gameMode }} -</div>
+ </div>
+ </div>
+ <div :class="[$style.frame, $style.frameH]">
+ <div :class="$style.frameInner">
+ <MkButton inline small @click="hold">HOLD</MkButton>
+ <img v-if="holdingStock" :src="getTextureImageUrl(holdingStock.mono)" style="width: 32px; margin-left: 8px; vertical-align: bottom;"/>
+ </div>
+ <div :class="[$style.frameInner, $style.stock]" style="text-align: center;">
+ <TransitionGroup
+ :enterActiveClass="$style.transition_stock_enterActive"
+ :leaveActiveClass="$style.transition_stock_leaveActive"
+ :enterFromClass="$style.transition_stock_enterFrom"
+ :leaveToClass="$style.transition_stock_leaveTo"
+ :moveClass="$style.transition_stock_move"
+ >
+ <img v-for="x in stock" :key="x.id" :src="getTextureImageUrl(x.mono)" style="width: 32px; vertical-align: bottom;"/>
+ </TransitionGroup>
+ </div>
+ </div>
+ </div>
+
+ <div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
+ <img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
+ <img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
+ <canvas ref="canvasEl" :class="$style.canvas"/>
+ <Transition
+ :enterActiveClass="$style.transition_combo_enterActive"
+ :leaveActiveClass="$style.transition_combo_leaveActive"
+ :enterFromClass="$style.transition_combo_enterFrom"
+ :leaveToClass="$style.transition_combo_leaveTo"
+ :moveClass="$style.transition_combo_move"
+ >
+ <div v-show="combo > 1" :class="$style.combo" :style="{ fontSize: `${100 + ((comboPrev - 2) * 15)}%` }">{{ comboPrev }} Chain!</div>
+ </Transition>
+ <div v-if="!isGameOver && !replaying && readyGo !== 'ready'" :class="$style.dropperContainer" :style="{ left: dropperX + 'px' }">
+ <!--<img v-if="currentPick" src="/client-assets/drop-and-fusion/dropper.png" :class="$style.dropper" :style="{ left: dropperX + 'px' }"/>-->
+ <Transition
+ :enterActiveClass="$style.transition_picked_enterActive"
+ :leaveActiveClass="$style.transition_picked_leaveActive"
+ :enterFromClass="$style.transition_picked_enterFrom"
+ :leaveToClass="$style.transition_picked_leaveTo"
+ :moveClass="$style.transition_picked_move"
+ mode="out-in"
+ >
+ <img v-if="currentPick" :key="currentPick.id" :src="getTextureImageUrl(currentPick.mono)" :class="$style.currentMono" :style="{ marginBottom: -((currentPick?.mono.sizeY * viewScale) / 2) + 'px', left: -((currentPick?.mono.sizeX * viewScale) / 2) + 'px', width: `${currentPick?.mono.sizeX * viewScale}px` }"/>
+ </Transition>
+ <template v-if="dropReady && currentPick">
+ <img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow"/>
+ <div :class="$style.dropGuide"/>
+ </template>
+ </div>
+ <div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
+ <div class="_gaps_s">
+ <img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
+ <div>SCORE: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
+ <div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
+ <div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b><MkNumber :value="yenTotal ?? score"/>円</b></div>
+ <div v-if="gameMode === 'sweets'"><b>おにぎり<MkNumber :value="score / 130"/>個分</b></div>
+ </div>
+ </div>
+ <div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div>
+ </div>
+
+ <div v-if="replaying" :class="$style.frame">
+ <div :class="$style.frameInner">
+ <div style="background: #0004;">
+ <div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
+ </div>
+ </div>
+ <div :class="$style.frameInner">
+ <div class="_buttonsCenter">
+ <MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END</MkButton>
+ <MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton>
+ <MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="isGameOver" :class="$style.frame">
+ <div :class="$style.frameInner">
+ <div class="_buttonsCenter">
+ <MkButton primary rounded @click="backToTitle">{{ i18n.ts.backToTitle }}</MkButton>
+ <MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</MkButton>
+ <MkButton primary rounded @click="share">{{ i18n.ts.share }}</MkButton>
+ <MkButton rounded @click="exportLog">Copy replay data</MkButton>
+ </div>
+ </div>
+ </div>
+
+ <div style="display: flex;">
+ <div :class="$style.frame" style="flex: 1; margin-right: 10px;">
+ <div :class="$style.frameInner">
+ <div>SCORE: <b><MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</b></div>
+ <div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/>{{ getScoreUnit(gameMode) }}</b><b v-else>-</b></div>
+ <div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b v-if="yenTotal"><MkNumber :value="yenTotal"/>円</b><b v-else>-</b></div>
+ </div>
+ </div>
+ <div :class="[$style.frame]" style="margin-left: auto;">
+ <div :class="$style.frameInner" style="text-align: center;">
+ <div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="showConfig" :class="$style.frame">
+ <div :class="$style.frameInner">
+ <div class="_gaps">
+ <MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.01" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true" @dragEnded="(v) => updateSettings('bgmVolume', v)">
+ <template #label>BGM {{ i18n.ts.volume }}</template>
+ </MkRange>
+ <MkRange v-model="sfxVolume" :min="0" :max="1" :step="0.01" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true" @dragEnded="(v) => updateSettings('sfxVolume', v)">
+ <template #label>{{ i18n.ts.sfx }} {{ i18n.ts.volume }}</template>
+ </MkRange>
+ </div>
+ </div>
+ </div>
+
+ <div :class="$style.frame">
+ <div :class="$style.frameInner">
+ <div>FUSION RECIPE</div>
+ <div>
+ <div v-for="(mono, i) in game.monoDefinitions.sort((a, b) => a.level - b.level)" :key="mono.id" style="display: inline-block;">
+ <img :src="getTextureImageUrl(mono)" style="width: 32px; vertical-align: bottom;"/>
+ <div v-if="i < game.monoDefinitions.length - 1" style="display: inline-block; margin-left: 4px; vertical-align: bottom;"><i class="ti ti-arrow-big-right"></i></div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div :class="$style.frame">
+ <div :class="$style.frameInner">
+ <MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">Surrender</MkButton>
+ <MkButton v-else full @click="restart">Retry</MkButton>
+ </div>
+ </div>
+ </div>
+ </div>
+</MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import * as Matter from 'matter-js';
+import * as Misskey from 'misskey-js';
+import { DropAndFusionGame, Mono } from 'misskey-bubble-game';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkRippleEffect from '@/components/MkRippleEffect.vue';
+import * as os from '@/os.js';
+import MkNumber from '@/components/MkNumber.vue';
+import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
+import MkButton from '@/components/MkButton.vue';
+import { claimAchievement } from '@/scripts/achievements.js';
+import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { useInterval } from '@/scripts/use-interval.js';
+import { apiUrl } from '@/config.js';
+import { $i } from '@/account.js';
+import * as sound from '@/scripts/sound.js';
+import MkRange from '@/components/MkRange.vue';
+import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+
+type FrontendMonoDefinition = {
+ id: string;
+ img: string;
+ imgSizeX: number;
+ imgSizeY: number;
+ spriteScale: number;
+ sfxPitch: number;
+};
+
+const NORAML_MONOS: FrontendMonoDefinition[] = [{
+ id: '9377076d-c980-4d83-bdaf-175bc58275b7',
+ sfxPitch: 0.25,
+ img: '/client-assets/drop-and-fusion/normal_monos/exploding_head.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: 'be9f38d2-b267-4b1a-b420-904e22e80568',
+ sfxPitch: 0.5,
+ img: '/client-assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: 'beb30459-b064-4888-926b-f572e4e72e0c',
+ sfxPitch: 0.75,
+ img: '/client-assets/drop-and-fusion/normal_monos/cold_face.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: 'feab6426-d9d8-49ae-849c-048cdbb6cdf0',
+ sfxPitch: 1,
+ img: '/client-assets/drop-and-fusion/normal_monos/zany_face.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: 'd6d8fed6-6d18-4726-81a1-6cf2c974df8a',
+ sfxPitch: 1.5,
+ img: '/client-assets/drop-and-fusion/normal_monos/pleading_face.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '249c728e-230f-4332-bbbf-281c271c75b2',
+ sfxPitch: 2,
+ img: '/client-assets/drop-and-fusion/normal_monos/face_with_open_mouth.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '23d67613-d484-4a93-b71e-3e81b19d6186',
+ sfxPitch: 2.5,
+ img: '/client-assets/drop-and-fusion/normal_monos/smiling_face_with_sunglasses.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '3cbd0add-ad7d-4685-bad0-29f6dddc0b99',
+ sfxPitch: 3,
+ img: '/client-assets/drop-and-fusion/normal_monos/grinning_squinting_face.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '8f86d4f4-ee02-41bf-ad38-1ce0ae457fb5',
+ sfxPitch: 3.5,
+ img: '/client-assets/drop-and-fusion/normal_monos/smiling_face_with_hearts.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '64ec4add-ce39-42b4-96cb-33908f3f118d',
+ sfxPitch: 4,
+ img: '/client-assets/drop-and-fusion/normal_monos/heart_suit.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}];
+
+const YEN_MONOS: FrontendMonoDefinition[] = [{
+ id: '880f9bd9-802f-4135-a7e1-fd0e0331f726',
+ sfxPitch: 0.25,
+ img: '/client-assets/drop-and-fusion/yen_monos/10000yen.png',
+ imgSizeX: 512,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: 'e807beb6-374a-4314-9cc2-aa5f17d96b6b',
+ sfxPitch: 0.5,
+ img: '/client-assets/drop-and-fusion/yen_monos/5000yen.png',
+ imgSizeX: 512,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: '033445b7-8f90-4fc9-beca-71a9e87cb530',
+ sfxPitch: 0.75,
+ img: '/client-assets/drop-and-fusion/yen_monos/2000yen.png',
+ imgSizeX: 512,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: '410a09ec-5f7f-46f6-b26f-cbca4ccbd091',
+ sfxPitch: 1,
+ img: '/client-assets/drop-and-fusion/yen_monos/1000yen.png',
+ imgSizeX: 512,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: '2aae82bc-3fa4-49ad-a6b5-94d888e809f5',
+ sfxPitch: 1.5,
+ img: '/client-assets/drop-and-fusion/yen_monos/500yen.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: 'a619bd67-d08f-4cc0-8c7e-c8072a4950cd',
+ sfxPitch: 2,
+ img: '/client-assets/drop-and-fusion/yen_monos/100yen.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: 'c1c5d8e4-17d6-4455-befd-12154d731faa',
+ sfxPitch: 2.5,
+ img: '/client-assets/drop-and-fusion/yen_monos/50yen.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: '7082648c-e428-44c4-887a-25c07a8ebdd5',
+ sfxPitch: 3,
+ img: '/client-assets/drop-and-fusion/yen_monos/10yen.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: '0d8d40d5-e6e0-4d26-8a95-b8d842363379',
+ sfxPitch: 3.5,
+ img: '/client-assets/drop-and-fusion/yen_monos/5yen.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}, {
+ id: '9dec1b38-d99d-40de-8288-37367b983d0d',
+ sfxPitch: 4,
+ img: '/client-assets/drop-and-fusion/yen_monos/1yen.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 0.97,
+}];
+
+const SQUARE_MONOS: FrontendMonoDefinition[] = [{
+ id: 'f75fd0ba-d3d4-40a4-9712-b470e45b0525',
+ sfxPitch: 0.25,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_10.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '7b70f4af-1c01-45fd-af72-61b1f01e03d1',
+ sfxPitch: 0.5,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_9.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '41607ef3-b6d6-4829-95b6-3737bf8bb956',
+ sfxPitch: 0.75,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_8.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '8a8310d2-0374-460f-bb50-ca9cd3ee3416',
+ sfxPitch: 1,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_7.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '1092e069-fe1a-450b-be97-b5d477ec398c',
+ sfxPitch: 1.5,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_6.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '2294734d-7bb8-4781-bb7b-ef3820abf3d0',
+ sfxPitch: 2,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_5.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: 'ea8a61af-e350-45f7-ba6a-366fcd65692a',
+ sfxPitch: 2.5,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_4.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: 'd0c74815-fc1c-4fbe-9953-c92e4b20f919',
+ sfxPitch: 3,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_3.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: 'd8fbd70e-611d-402d-87da-1a7fd8cd2c8d',
+ sfxPitch: 3.5,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_2.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}, {
+ id: '35e476ee-44bd-4711-ad42-87be245d3efd',
+ sfxPitch: 4,
+ img: '/client-assets/drop-and-fusion/square_monos/keycap_1.png',
+ imgSizeX: 256,
+ imgSizeY: 256,
+ spriteScale: 1.12,
+}];
+
+const SWEETS_MONOS: FrontendMonoDefinition[] = [{
+ id: '77f724c0-88be-4aeb-8e1a-a00ed18e3844',
+ sfxPitch: 0.25,
+ img: '/client-assets/drop-and-fusion/sweets_monos/shortcake_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: 'f3468ef4-2e1e-4906-8795-f147f39f7e1f',
+ sfxPitch: 0.5,
+ img: '/client-assets/drop-and-fusion/sweets_monos/pancakes_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: 'bcb41129-6f2d-44ee-89d3-86eb2df564ba',
+ sfxPitch: 0.75,
+ img: '/client-assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: 'f058e1ad-1981-409b-b3a7-302de0a43744',
+ sfxPitch: 1,
+ img: '/client-assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: 'd22cfe38-5a3b-4b9c-a1a6-907930a3d732',
+ sfxPitch: 1.5,
+ img: '/client-assets/drop-and-fusion/sweets_monos/doughnut_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: '79867083-a073-427e-ae82-07a70d9f3b4f',
+ sfxPitch: 2,
+ img: '/client-assets/drop-and-fusion/sweets_monos/custard_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: '2e152a12-a567-4100-b4d4-d15d81ba47b1',
+ sfxPitch: 2.5,
+ img: '/client-assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: '12250376-2258-4716-8eec-b3a7239461fc',
+ sfxPitch: 3,
+ img: '/client-assets/drop-and-fusion/sweets_monos/lollipop_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: '4d4f2668-4be7-44a3-aa3a-856df6e25aa6',
+ sfxPitch: 3.5,
+ img: '/client-assets/drop-and-fusion/sweets_monos/candy_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}, {
+ id: 'c9984b40-4045-44c3-b260-d47b7b4625b2',
+ sfxPitch: 4,
+ img: '/client-assets/drop-and-fusion/sweets_monos/cookie_color.svg',
+ imgSizeX: 32,
+ imgSizeY: 32,
+ spriteScale: 1,
+}];
+
+const props = defineProps<{
+ gameMode: 'normal' | 'square' | 'yen' | 'sweets' | 'space';
+ mute: boolean;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'end'): void;
+}>();
+
+const monoDefinitions = computed(() => {
+ return props.gameMode === 'normal' ? NORAML_MONOS :
+ props.gameMode === 'square' ? SQUARE_MONOS :
+ props.gameMode === 'yen' ? YEN_MONOS :
+ props.gameMode === 'sweets' ? SWEETS_MONOS :
+ props.gameMode === 'space' ? NORAML_MONOS :
+ [] as never;
+});
+
+function getScoreUnit(gameMode: string) {
+ return gameMode === 'normal' ? 'pt' :
+ gameMode === 'square' ? 'pt' :
+ gameMode === 'yen' ? '円' :
+ gameMode === 'sweets' ? 'kcal' :
+ '' as never;
+}
+
+function getMonoRenderOptions(mono: Mono) {
+ const def = monoDefinitions.value.find(x => x.id === mono.id)!;
+ return {
+
+ sprite: {
+ texture: def.img,
+ xScale: (mono.sizeX / def.imgSizeX) * def.spriteScale,
+ yScale: (mono.sizeY / def.imgSizeY) * def.spriteScale,
+ },
+
+ };
+}
+
+let viewScale = 1;
+let seed: string = Date.now().toString();
+let containerElRect: DOMRect | null = null;
+let logs: ReturnType<DropAndFusionGame['getLogs']> | null = null;
+let endedAtFrame = 0;
+let bgmNodes: ReturnType<typeof sound.createSourceNode> | null = null;
+let renderer: Matter.Render | null = null;
+let monoTextures: Record<string, Blob> = {};
+let monoTextureUrls: Record<string, string> = {};
+let tickRaf: number | null = null;
+let game = new DropAndFusionGame({
+ seed: seed,
+ gameMode: props.gameMode,
+ getMonoRenderOptions,
+});
+attachGameEvents();
+
+const containerEl = shallowRef<HTMLElement>();
+const canvasEl = shallowRef<HTMLCanvasElement>();
+const dropperX = ref(0);
+const currentPick = shallowRef<{ id: string; mono: Mono } | null>(null);
+const stock = shallowRef<{ id: string; mono: Mono }[]>([]);
+const holdingStock = shallowRef<{ id: string; mono: Mono } | null>(null);
+const score = ref(0);
+const combo = ref(0);
+const comboPrev = ref(0);
+const maxCombo = ref(0);
+const dropReady = ref(true);
+const isGameOver = ref(false);
+const gameLoaded = ref(false);
+const readyGo = ref<'ready' | 'go' | null>('ready');
+const highScore = ref<number | null>(null);
+const yenTotal = ref<number | null>(null);
+const showConfig = ref(false);
+const replaying = ref(false);
+const replayPlaybackRate = ref(1);
+const currentFrame = ref(0);
+const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
+const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
+
+watch(replayPlaybackRate, (newValue) => {
+ game.replayPlaybackRate = newValue;
+});
+
+watch(bgmVolume, (newValue) => {
+ if (bgmNodes) {
+ bgmNodes.gainNode.gain.value = props.mute ? 0 : newValue;
+ }
+});
+
+function createRendererInstance(game: DropAndFusionGame) {
+ return Matter.Render.create({
+ engine: game.engine,
+ canvas: canvasEl.value!,
+ options: {
+ width: game.GAME_WIDTH,
+ height: game.GAME_HEIGHT,
+ background: 'transparent', // transparent to hide
+ wireframeBackground: 'transparent', // transparent to hide
+ wireframes: false,
+ showSleeping: false,
+ pixelRatio: Math.max(2, window.devicePixelRatio),
+ },
+ });
+}
+
+function loadMonoTextures() {
+ async function loadSingleMonoTexture(mono: FrontendMonoDefinition) {
+ if (renderer == null) return;
+
+ // Matter-js内にキャッシュがある場合はスキップ
+ if (renderer.textures[mono.img]) return;
+
+ let src = mono.img;
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (monoTextureUrls[mono.img]) {
+ src = monoTextureUrls[mono.img];
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ } else if (monoTextures[mono.img]) {
+ src = URL.createObjectURL(monoTextures[mono.img]);
+ monoTextureUrls[mono.img] = src;
+ } else {
+ const res = await fetch(mono.img);
+ const blob = await res.blob();
+ monoTextures[mono.img] = blob;
+ src = URL.createObjectURL(blob);
+ monoTextureUrls[mono.img] = src;
+ }
+
+ const image = new Image();
+ image.src = src;
+ renderer.textures[mono.img] = image;
+ }
+
+ return Promise.all(monoDefinitions.value.map(x => loadSingleMonoTexture(x)));
+}
+
+function getTextureImageUrl(mono: Mono) {
+ const def = monoDefinitions.value.find(x => x.id === mono.id)!;
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (monoTextureUrls[def.img]) {
+ return monoTextureUrls[def.img];
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ } else if (monoTextures[def.img]) {
+ // Gameクラス内にキャッシュがある場合はそれを使う
+ const out = URL.createObjectURL(monoTextures[def.img]);
+ monoTextureUrls[def.img] = out;
+ return out;
+ } else {
+ return def.img;
+ }
+}
+
+function tick() {
+ const hasNextTick = game.tick();
+ if (hasNextTick) {
+ tickRaf = window.requestAnimationFrame(tick);
+ } else {
+ tickRaf = null;
+ }
+}
+
+function tickReplay() {
+ let hasNextTick;
+ for (let i = 0; i < replayPlaybackRate.value; i++) {
+ const log = logs!.find(x => x.frame === game.frame);
+ if (log) {
+ switch (log.operation) {
+ case 'drop': {
+ game.drop(log.x);
+ break;
+ }
+ case 'hold': {
+ game.hold();
+ break;
+ }
+ case 'surrender': {
+ game.surrender();
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ hasNextTick = game.tick();
+ currentFrame.value = game.frame;
+ if (!hasNextTick) break;
+ }
+
+ if (hasNextTick) {
+ tickRaf = window.requestAnimationFrame(tickReplay);
+ } else {
+ tickRaf = null;
+ }
+}
+
+async function start() {
+ renderer = createRendererInstance(game);
+ await loadMonoTextures();
+ Matter.Render.lookAt(renderer, {
+ min: { x: 0, y: 0 },
+ max: { x: game.GAME_WIDTH, y: game.GAME_HEIGHT },
+ });
+ Matter.Render.run(renderer);
+ game.start();
+ window.requestAnimationFrame(tick);
+
+ gameLoaded.value = true;
+
+ window.setTimeout(() => {
+ readyGo.value = 'go';
+ window.setTimeout(() => {
+ readyGo.value = null;
+ }, 1000);
+ }, 1500);
+}
+
+function onClick(ev: MouseEvent) {
+ if (!containerElRect) return;
+ if (replaying.value) return;
+ const x = (ev.clientX - containerElRect.left) / viewScale;
+ game.drop(x);
+}
+
+function onTouchend(ev: TouchEvent) {
+ if (!containerElRect) return;
+ if (replaying.value) return;
+ const x = (ev.changedTouches[0].clientX - containerElRect.left) / viewScale;
+ game.drop(x);
+}
+
+function onMousemove(ev: MouseEvent) {
+ if (!containerElRect) return;
+ const x = (ev.clientX - containerElRect.left);
+ moveDropper(containerElRect, x);
+}
+
+function onTouchmove(ev: TouchEvent) {
+ if (!containerElRect) return;
+ const x = (ev.touches[0].clientX - containerElRect.left);
+ moveDropper(containerElRect, x);
+}
+
+function moveDropper(rect: DOMRect, x: number) {
+ dropperX.value = Math.min(rect.width * ((game.GAME_WIDTH - game.PLAYAREA_MARGIN) / game.GAME_WIDTH), Math.max(rect.width * (game.PLAYAREA_MARGIN / game.GAME_WIDTH), x));
+}
+
+function hold() {
+ game.hold();
+}
+
+async function surrender() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.areYouSure,
+ });
+ if (canceled) return;
+ game.surrender();
+}
+
+async function restart() {
+ reset();
+ game = new DropAndFusionGame({
+ seed: seed,
+ gameMode: props.gameMode,
+ getMonoRenderOptions,
+ });
+ attachGameEvents();
+ await start();
+}
+
+function reset() {
+ dispose();
+ seed = Date.now().toString();
+ isGameOver.value = false;
+ replaying.value = false;
+ replayPlaybackRate.value = 1;
+ currentPick.value = null;
+ dropReady.value = true;
+ stock.value = [];
+ holdingStock.value = null;
+ score.value = 0;
+ combo.value = 0;
+ comboPrev.value = 0;
+ maxCombo.value = 0;
+ gameLoaded.value = false;
+ readyGo.value = null;
+}
+
+function dispose() {
+ game.dispose();
+ if (renderer) Matter.Render.stop(renderer);
+ if (tickRaf) {
+ window.cancelAnimationFrame(tickRaf);
+ }
+}
+
+function backToTitle() {
+ emit('end');
+}
+
+function replay() {
+ replaying.value = true;
+ dispose();
+ game = new DropAndFusionGame({
+ seed: seed,
+ gameMode: props.gameMode,
+ getMonoRenderOptions,
+ });
+ attachGameEvents();
+ os.promiseDialog(loadMonoTextures(), async () => {
+ renderer = createRendererInstance(game);
+ Matter.Render.lookAt(renderer, {
+ min: { x: 0, y: 0 },
+ max: { x: game.GAME_WIDTH, y: game.GAME_HEIGHT },
+ });
+ Matter.Render.run(renderer);
+ game.start();
+ window.requestAnimationFrame(tickReplay);
+ });
+}
+
+function endReplay() {
+ replaying.value = false;
+ dispose();
+}
+
+function exportLog() {
+ if (!logs) return;
+ const data = JSON.stringify({
+ v: game.GAME_VERSION,
+ m: props.gameMode,
+ s: seed,
+ d: new Date().toISOString(),
+ l: DropAndFusionGame.serializeLogs(logs),
+ });
+ copyToClipboard(data);
+ os.success();
+}
+
+function updateSettings<
+ K extends keyof typeof defaultStore.state.dropAndFusion,
+ V extends typeof defaultStore.state.dropAndFusion[K],
+>(key: K, value: V) {
+ const changes: { [P in K]?: V } = {};
+ changes[key] = value;
+ defaultStore.set('dropAndFusion', {
+ ...defaultStore.state.dropAndFusion,
+ ...changes,
+ });
+}
+
+function loadImage(url: string) {
+ return new Promise<HTMLImageElement>(res => {
+ const img = new Image();
+ img.src = url;
+ img.addEventListener('load', () => {
+ res(img);
+ });
+ });
+}
+
+function getGameImageDriveFile() {
+ return new Promise<Misskey.entities.DriveFile | null>(res => {
+ const dcanvas = document.createElement('canvas');
+ dcanvas.width = game.GAME_WIDTH;
+ dcanvas.height = game.GAME_HEIGHT;
+ const ctx = dcanvas.getContext('2d');
+ if (!ctx || !canvasEl.value) return res(null);
+ Promise.all([
+ loadImage('/client-assets/drop-and-fusion/frame-light.svg'),
+ loadImage('/client-assets/drop-and-fusion/logo.png'),
+ ]).then((images) => {
+ const [frame, logo] = images;
+ ctx.fillStyle = '#fff';
+ ctx.fillRect(0, 0, game.GAME_WIDTH, game.GAME_HEIGHT);
+
+ ctx.drawImage(frame, 0, 0, game.GAME_WIDTH, game.GAME_HEIGHT);
+ ctx.drawImage(canvasEl.value!, 0, 0, game.GAME_WIDTH, game.GAME_HEIGHT);
+
+ ctx.fillStyle = '#000';
+ ctx.font = '16px bold sans-serif';
+ ctx.textBaseline = 'top';
+ ctx.fillText(`SCORE: ${score.value.toLocaleString()}${getScoreUnit(props.gameMode)}`, 10, 10);
+
+ ctx.globalAlpha = 0.7;
+ ctx.drawImage(logo, game.GAME_WIDTH * 0.55, 6, game.GAME_WIDTH * 0.45, game.GAME_WIDTH * 0.45 * (logo.height / logo.width));
+ ctx.globalAlpha = 1;
+
+ dcanvas.toBlob(blob => {
+ if (!blob) return res(null);
+ if ($i == null) return res(null);
+ const formData = new FormData();
+ formData.append('file', blob);
+ formData.append('name', `bubble-game-${Date.now()}.png`);
+ formData.append('isSensitive', 'false');
+ formData.append('i', $i.token);
+ if (defaultStore.state.uploadFolder) {
+ formData.append('folderId', defaultStore.state.uploadFolder);
+ }
+
+ window.fetch(apiUrl + '/drive/files/create', {
+ method: 'POST',
+ body: formData,
+ })
+ .then(response => response.json())
+ .then(f => {
+ res(f);
+ });
+ }, 'image/png');
+
+ dcanvas.remove();
+ });
+ });
+}
+
+async function share() {
+ const uploading = getGameImageDriveFile();
+ os.promiseDialog(uploading);
+ const file = await uploading;
+ if (!file) return;
+ os.post({
+ initialText: `#BubbleGame (${props.gameMode})
+SCORE: ${score.value.toLocaleString()}${getScoreUnit(props.gameMode)}`,
+ initialFiles: [file],
+ instant: true,
+ });
+}
+
+function attachGameEvents() {
+ game.addListener('changeScore', value => {
+ score.value = value;
+ });
+
+ game.addListener('changeCombo', value => {
+ if (value === 0) {
+ comboPrev.value = combo.value;
+ } else {
+ comboPrev.value = value;
+ }
+ maxCombo.value = Math.max(maxCombo.value, value);
+ combo.value = value;
+ });
+
+ game.addListener('changeStock', value => {
+ currentPick.value = JSON.parse(JSON.stringify(value[0]));
+ stock.value = JSON.parse(JSON.stringify(value.slice(1)));
+ });
+
+ game.addListener('changeHolding', value => {
+ holdingStock.value = value;
+
+ if (!props.mute) {
+ sound.playUrl('/client-assets/drop-and-fusion/hold.mp3', {
+ volume: 0.5 * sfxVolume.value,
+ playbackRate: replayPlaybackRate.value,
+ });
+ }
+ });
+
+ game.addListener('dropped', (x) => {
+ if (!props.mute) {
+ const panV = x - game.PLAYAREA_MARGIN;
+ const panW = game.GAME_WIDTH - game.PLAYAREA_MARGIN - game.PLAYAREA_MARGIN;
+ const pan = ((panV / panW) - 0.5) * 2;
+ if (props.gameMode === 'yen') {
+ sound.playUrl('/client-assets/drop-and-fusion/drop_yen.mp3', {
+ volume: sfxVolume.value,
+ pan,
+ playbackRate: replayPlaybackRate.value,
+ });
+ } else {
+ sound.playUrl('/client-assets/drop-and-fusion/drop.mp3', {
+ volume: sfxVolume.value,
+ pan,
+ playbackRate: replayPlaybackRate.value,
+ });
+ }
+ }
+
+ if (replaying.value) return;
+
+ dropReady.value = false;
+ window.setTimeout(() => {
+ if (!isGameOver.value) {
+ dropReady.value = true;
+ }
+ }, game.frameToMs(game.DROP_COOLTIME));
+ });
+
+ game.addListener('fusioned', (x, y, nextMono, scoreDelta) => {
+ if (!canvasEl.value) return;
+
+ const rect = canvasEl.value.getBoundingClientRect();
+ const domX = rect.left + (x * viewScale);
+ const domY = rect.top + (y * viewScale);
+ const scoreUnit = getScoreUnit(props.gameMode);
+ os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end');
+ os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta + (scoreUnit === 'pt' ? '' : scoreUnit) }, {}, 'end');
+
+ if (nextMono) {
+ const def = monoDefinitions.value.find(x => x.id === nextMono.id)!;
+ if (!props.mute) {
+ const panV = x - game.PLAYAREA_MARGIN;
+ const panW = game.GAME_WIDTH - game.PLAYAREA_MARGIN - game.PLAYAREA_MARGIN;
+ const pan = ((panV / panW) - 0.5) * 2;
+ const pitch = def.sfxPitch;
+ if (props.gameMode === 'yen') {
+ sound.playUrl('/client-assets/drop-and-fusion/fusion_yen.mp3', {
+ volume: 0.25 * sfxVolume.value,
+ pan: pan,
+ playbackRate: (pitch / 4) * replayPlaybackRate.value,
+ });
+ } else {
+ sound.playUrl('/client-assets/drop-and-fusion/fusion.mp3', {
+ volume: sfxVolume.value,
+ pan: pan,
+ playbackRate: pitch * replayPlaybackRate.value,
+ });
+ }
+ }
+ } else {
+ if (!props.mute) {
+ // TODO: 融合後のモノがない場合でも何らかの効果音を再生
+ }
+ }
+ });
+
+ const minCollisionEnergyForSound = 2.5;
+ const maxCollisionEnergyForSound = 9;
+ const soundPitchMax = 4;
+ const soundPitchMin = 0.5;
+
+ game.addListener('collision', (energy, bodyA, bodyB) => {
+ if (!props.mute && (energy > minCollisionEnergyForSound)) {
+ const volume = (Math.min(maxCollisionEnergyForSound, energy - minCollisionEnergyForSound) / maxCollisionEnergyForSound) / 4;
+ const panV =
+ bodyA.label === '_wall_' ? bodyB.position.x - game.PLAYAREA_MARGIN :
+ bodyB.label === '_wall_' ? bodyA.position.x - game.PLAYAREA_MARGIN :
+ ((bodyA.position.x + bodyB.position.x) / 2) - game.PLAYAREA_MARGIN;
+ const panW = game.GAME_WIDTH - game.PLAYAREA_MARGIN - game.PLAYAREA_MARGIN;
+ const pan = ((panV / panW) - 0.5) * 2;
+ const pitch = soundPitchMin + ((soundPitchMax - soundPitchMin) * (1 - (Math.min(10, energy) / 10)));
+
+ if (props.gameMode === 'yen') {
+ sound.playUrl('/client-assets/drop-and-fusion/collision_yen.mp3', {
+ volume: volume * sfxVolume.value,
+ pan: pan,
+ playbackRate: Math.max(1, pitch) * replayPlaybackRate.value,
+ });
+ } else {
+ sound.playUrl('/client-assets/drop-and-fusion/collision.mp3', {
+ volume: volume * sfxVolume.value,
+ pan: pan,
+ playbackRate: pitch * replayPlaybackRate.value,
+ });
+ }
+ }
+ });
+
+ game.addListener('monoAdded', (mono) => {
+ if (replaying.value) return;
+
+ // 実績関連
+ if (mono.level === 10) {
+ claimAchievement('bubbleGameExplodingHead');
+
+ const monos = game.getActiveMonos();
+ if (monos.filter(x => x.level === 10).length >= 2) {
+ claimAchievement('bubbleGameDoubleExplodingHead');
+ }
+ }
+ });
+
+ game.addListener('gameOver', () => {
+ if (!props.mute) {
+ if (props.gameMode === 'yen') {
+ sound.playUrl('/client-assets/drop-and-fusion/gameover_yen.mp3', {
+ volume: 0.5 * sfxVolume.value,
+ });
+ } else {
+ sound.playUrl('/client-assets/drop-and-fusion/gameover.mp3', {
+ volume: sfxVolume.value,
+ });
+ }
+ }
+
+ if (replaying.value) {
+ endReplay();
+ return;
+ }
+
+ logs = game.getLogs();
+ endedAtFrame = game.frame;
+ currentPick.value = null;
+ dropReady.value = false;
+ isGameOver.value = true;
+
+ misskeyApi('bubble-game/register', {
+ seed,
+ score: score.value,
+ gameMode: props.gameMode,
+ gameVersion: game.GAME_VERSION,
+ logs: DropAndFusionGame.serializeLogs(logs),
+ });
+
+ if (props.gameMode === 'yen') {
+ yenTotal.value = (yenTotal.value ?? 0) + score.value;
+ misskeyApi('i/registry/set', {
+ scope: ['dropAndFusionGame'],
+ key: 'yenTotal',
+ value: yenTotal.value,
+ });
+ }
+
+ if (score.value > (highScore.value ?? 0)) {
+ highScore.value = score.value;
+
+ misskeyApi('i/registry/set', {
+ scope: ['dropAndFusionGame'],
+ key: 'highScore:' + props.gameMode,
+ value: highScore.value,
+ });
+ }
+ });
+}
+
+useInterval(() => {
+ if (!canvasEl.value) return;
+ const actualCanvasWidth = canvasEl.value.getBoundingClientRect().width;
+ if (actualCanvasWidth === 0) return;
+ viewScale = actualCanvasWidth / game.GAME_WIDTH;
+ containerElRect = containerEl.value?.getBoundingClientRect() ?? null;
+}, 1000, { immediate: false, afterMounted: true });
+
+onMounted(async () => {
+ try {
+ highScore.value = await misskeyApi('i/registry/get', {
+ scope: ['dropAndFusionGame'],
+ key: 'highScore:' + props.gameMode,
+ });
+ } catch (err) {
+ highScore.value = null;
+ }
+
+ if (props.gameMode === 'yen') {
+ try {
+ yenTotal.value = await misskeyApi('i/registry/get', {
+ scope: ['dropAndFusionGame'],
+ key: 'yenTotal',
+ });
+ } catch (err: any) {
+ if (err.code === 'NO_SUCH_KEY') {
+ // nop
+ } else {
+ os.alert({
+ type: 'error',
+ text: i18n.ts.cannotLoad,
+ });
+ return;
+ }
+ }
+ }
+
+ /*
+ const getVerticesFromSvg = async (path: string) => {
+ const svgDoc = await fetch(path)
+ .then((response) => response.text())
+ .then((svgString) => {
+ const parser = new DOMParser();
+ return parser.parseFromString(svgString, 'image/svg+xml');
+ });
+ const pathDatas = svgDoc.querySelectorAll('path');
+ if (!pathDatas) return;
+ const vertices = Array.from(pathDatas).map((pathData) => {
+ return Matter.Svg.pathToVertices(pathData);
+ });
+ return vertices;
+ };
+
+ getVerticesFromSvg('/client-assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg').then((vertices) => {
+ console.log('doughnut_color', vertices);
+ });
+ */
+
+ await start();
+
+ const bgmBuffer = await sound.loadAudio('/client-assets/drop-and-fusion/bgm_1.mp3');
+ if (!bgmBuffer) return;
+ bgmNodes = sound.createSourceNode(bgmBuffer, {
+ volume: props.mute ? 0 : bgmVolume.value,
+ });
+ if (!bgmNodes) return;
+ bgmNodes.soundSource.loop = true;
+ bgmNodes.soundSource.start();
+});
+
+onUnmounted(() => {
+ dispose();
+ bgmNodes?.soundSource.stop();
+});
+
+onDeactivated(() => {
+ dispose();
+ bgmNodes?.soundSource.stop();
+});
+
+definePageMetadata(() => ({
+ title: i18n.ts.bubbleGame,
+ icon: 'ti ti-apple',
+}));
+</script>
+
+<style lang="scss" module>
+.transition_zoom_move,
+.transition_zoom_enterActive,
+.transition_zoom_leaveActive {
+ transition: opacity 0.5s cubic-bezier(0,.5,.5,1), transform 0.5s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_zoom_enterFrom,
+.transition_zoom_leaveTo {
+ opacity: 0;
+ transform: scale(0.8);
+}
+
+.transition_stock_move,
+.transition_stock_enterActive,
+.transition_stock_leaveActive {
+ transition: opacity 0.4s cubic-bezier(0,.5,.5,1), transform 0.4s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_stock_enterFrom,
+.transition_stock_leaveTo {
+ opacity: 0;
+ transform: scale(0.7);
+}
+.transition_stock_leaveActive {
+ position: absolute;
+}
+
+.transition_picked_move,
+.transition_picked_enterActive {
+ transition: opacity 0.5s cubic-bezier(0,.5,.5,1), transform 0.5s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_picked_leaveActive {
+ transition: all 0s !important;
+}
+.transition_picked_enterFrom,
+.transition_picked_leaveTo {
+ opacity: 0;
+ transform: translateY(-50px);
+}
+.transition_picked_leaveActive {
+ position: absolute;
+}
+
+.transition_combo_move,
+.transition_combo_enterActive {
+ transition: all 0s !important;
+}
+.transition_combo_leaveActive {
+ transition: opacity 0.4s cubic-bezier(0,.5,.5,1), transform 0.4s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_combo_enterFrom,
+.transition_combo_leaveTo {
+ opacity: 0;
+ transform: scale(0.7);
+}
+.transition_combo_leaveActive {
+ position: absolute;
+}
+
+.root {
+ margin: 0 auto;
+ max-width: 600px;
+ user-select: none;
+
+ * {
+ user-select: none;
+ }
+}
+
+.loadingScreen {
+ text-align: center;
+ padding: 32px;
+}
+
+.readyGo_bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 100;
+ backdrop-filter: blur(4px);
+}
+
+.readyGo_ready,
+.readyGo_go {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 101;
+ pointer-events: none;
+}
+
+.readyGo_img {
+ display: block;
+ width: 250px;
+ max-width: 100%;
+}
+
+.frame {
+ padding: 7px;
+ background: #8C4F26;
+ box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
+ border-radius: 10px;
+}
+
+.frameH {
+ display: flex;
+ gap: 6px;
+}
+
+.frameInner {
+ padding: 8px;
+ margin-top: 8px;
+ background: #F1E8DC;
+ box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
+ border-radius: 6px;
+ color: #693410;
+
+ &:first-child {
+ margin-top: 0;
+ }
+}
+
+.frameDivider {
+ height: 0;
+ border: none;
+ border-top: 1px solid #693410;
+ border-bottom: 1px solid #ce8a5c;
+}
+
+.header {
+ position: relative;
+ z-index: 10;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: auto auto;
+ gap: 8px;
+
+ > .headerTitle {
+ text-align: center;
+ }
+
+ @media (min-width: 500px) {
+ grid-template-columns: 1fr auto;
+ grid-template-rows: auto;
+
+ > .headerTitle {
+ text-align: start;
+ }
+ }
+}
+
+.mainFrameImg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ // なんかiOSでちらつく
+ //filter: drop-shadow(0 6px 16px #0007);
+ pointer-events: none;
+ user-select: none;
+}
+
+.canvas {
+ position: relative;
+ display: block;
+ z-index: 1;
+ width: 100% !important;
+ height: auto !important;
+ pointer-events: none;
+ user-select: none;
+}
+
+.gameContainer {
+ position: relative;
+ margin-top: -20px;
+}
+
+.stock {
+ pointer-events: none;
+ user-select: none;
+}
+
+.combo {
+ position: absolute;
+ z-index: 3;
+ top: 50%;
+ width: 100%;
+ text-align: center;
+ font-weight: bold;
+ font-style: oblique;
+ color: #fff;
+ -webkit-text-stroke: 1px rgb(255, 145, 0);
+ text-shadow: 0 0 6px #0005;
+ pointer-events: none;
+ user-select: none;
+}
+
+.dropperContainer {
+ position: absolute;
+ top: 0;
+ height: 100%;
+ z-index: 2;
+ pointer-events: none;
+ user-select: none;
+ will-change: left;
+}
+
+.currentMono {
+ position: absolute;
+ display: block;
+ bottom: 88%;
+ z-index: 2;
+ filter: drop-shadow(0 6px 16px #0007);
+}
+
+.dropper {
+ position: relative;
+ top: 0;
+ width: 70px;
+ margin-top: -10px;
+ margin-left: -30px;
+ z-index: 2;
+ filter: drop-shadow(0 6px 16px #0007);
+}
+
+.currentMonoArrow {
+ position: absolute;
+ width: 20px;
+ bottom: 80%;
+ left: -10px;
+ z-index: 3;
+ animation: currentMonoArrow 2s ease infinite;
+}
+
+.dropGuide {
+ position: absolute;
+ z-index: 3;
+ bottom: 0;
+ width: 3px;
+ margin-left: -2px;
+ height: 85%;
+ background: #f002;
+}
+
+.gameOverLabel {
+ position: absolute;
+ z-index: 10;
+ top: 50%;
+ left: 0;
+ right: 0;
+ margin: auto;
+ width: calc(100% - 50px);
+ max-width: 320px;
+ padding: 16px;
+ box-sizing: border-box;
+ background: #0007;
+ border-radius: 16px;
+ color: #fff;
+ text-align: center;
+ font-weight: bold;
+}
+
+.gameOver {
+ .canvas {
+ filter: grayscale(1);
+ }
+}
+
+.replayIndicator {
+ position: absolute;
+ z-index: 10;
+ left: 10px;
+ bottom: 10px;
+ padding: 6px 8px;
+ color: #f00;
+ font-weight: bold;
+ background: #0008;
+ border-radius: 6px;
+ pointer-events: none;
+}
+
+.replayIndicatorText {
+ animation: replayIndicator-blink 2s infinite;
+}
+
+@keyframes replayIndicator-blink {
+ 0% { opacity: 1; }
+ 50% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+@keyframes currentMonoArrow {
+ 0% { transform: translateY(0); }
+ 25% { transform: translateY(-8px); }
+ 50% { transform: translateY(0); }
+ 75% { transform: translateY(-8px); }
+ 100% { transform: translateY(0); }
+}
+</style>
diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue
new file mode 100644
index 0000000000..1b11457988
--- /dev/null
+++ b/packages/frontend/src/pages/drop-and-fusion.vue
@@ -0,0 +1,192 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<Transition
+ :enterActiveClass="$style.transition_zoom_enterActive"
+ :leaveActiveClass="$style.transition_zoom_leaveActive"
+ :enterFromClass="$style.transition_zoom_enterFrom"
+ :leaveToClass="$style.transition_zoom_leaveTo"
+ :moveClass="$style.transition_zoom_move"
+ mode="out-in"
+>
+ <MkSpacer v-if="!gameStarted" :contentMax="800">
+ <div :class="$style.root">
+ <div class="_gaps">
+ <div :class="$style.frame" style="text-align: center;">
+ <div :class="$style.frameInner">
+ <img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
+ </div>
+ </div>
+ <div :class="$style.frame" style="text-align: center;">
+ <div :class="$style.frameInner">
+ <div class="_gaps" style="padding: 16px;">
+ <MkSelect v-model="gameMode">
+ <option value="normal">NORMAL</option>
+ <option value="square">SQUARE</option>
+ <option value="yen">YEN</option>
+ <option value="sweets">SWEETS</option>
+ <!--<option value="space">SPACE</option>-->
+ </MkSelect>
+ <MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
+ </div>
+ </div>
+ <div :class="$style.frameInner">
+ <div class="_gaps" style="padding: 16px;">
+ <div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
+ <MkSwitch v-model="mute">
+ <template #label>{{ i18n.ts.mute }}</template>
+ </MkSwitch>
+ </div>
+ </div>
+ </div>
+ <div :class="$style.frame">
+ <div :class="$style.frameInner">
+ <div class="_gaps_s" style="padding: 16px;">
+ <div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div>
+ <div v-if="ranking" class="_gaps_s">
+ <div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
+ <MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
+ <MkUserName :user="r.user" :nowrap="true"/>
+ <b style="margin-left: auto;">{{ r.score.toLocaleString() }} {{ getScoreUnit(gameMode) }}</b>
+ </div>
+ </div>
+ <div v-else>{{ i18n.ts.loading }}</div>
+ </div>
+ </div>
+ </div>
+ <div :class="$style.frame">
+ <div :class="$style.frameInner" style="padding: 16px;">
+ <div style="font-weight: bold;">{{ i18n.ts._bubbleGame.howToPlay }}</div>
+ <ol>
+ <li>{{ i18n.ts._bubbleGame._howToPlay.section1 }}</li>
+ <li>{{ i18n.ts._bubbleGame._howToPlay.section2 }}</li>
+ <li>{{ i18n.ts._bubbleGame._howToPlay.section3 }}</li>
+ </ol>
+ </div>
+ </div>
+ <div :class="$style.frame">
+ <div :class="$style.frameInner">
+ <div class="_gaps_s" style="padding: 16px;">
+ <div><b>Credit</b></div>
+ <div>
+ <div>Ai-chan illustration: @poteriri@misskey.io</div>
+ <div>BGM: @ys@misskey.design</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </MkSpacer>
+ <XGame v-else :gameMode="gameMode" :mute="mute" @end="onGameEnd"/>
+</Transition>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
+import XGame from './drop-and-fusion.game.vue';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkButton from '@/components/MkButton.vue';
+import { i18n } from '@/i18n.js';
+import MkSelect from '@/components/MkSelect.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
+
+const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
+const gameStarted = ref(false);
+const mute = ref(false);
+const ranking = ref(null);
+
+watch(gameMode, async () => {
+ ranking.value = await misskeyApiGet('bubble-game/ranking', { gameMode: gameMode.value });
+}, { immediate: true });
+
+function getScoreUnit(gameMode: string) {
+ return gameMode === 'normal' ? 'pt' :
+ gameMode === 'square' ? 'pt' :
+ gameMode === 'yen' ? '円' :
+ gameMode === 'sweets' ? 'kcal' :
+ gameMode === 'space' ? 'pt' :
+ '' as never;
+}
+
+async function start() {
+ gameStarted.value = true;
+}
+
+function onGameEnd() {
+ gameStarted.value = false;
+}
+
+definePageMetadata(() => ({
+ title: i18n.ts.bubbleGame,
+ icon: 'ti ti-device-gamepad',
+}));
+</script>
+
+<style lang="scss" module>
+.transition_zoom_move,
+.transition_zoom_enterActive,
+.transition_zoom_leaveActive {
+ transition: opacity 0.5s cubic-bezier(0,.5,.5,1), transform 0.5s cubic-bezier(0,.5,.5,1) !important;
+}
+.transition_zoom_enterFrom,
+.transition_zoom_leaveTo {
+ opacity: 0;
+ transform: scale(0.8);
+}
+
+.root {
+ margin: 0 auto;
+ max-width: 600px;
+ user-select: none;
+
+ * {
+ user-select: none;
+ }
+}
+
+.frame {
+ padding: 7px;
+ background: #8C4F26;
+ box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
+ border-radius: 10px;
+}
+
+.frameH {
+ display: flex;
+ gap: 6px;
+}
+
+.frameInner {
+ padding: 8px;
+ margin-top: 8px;
+ background: #F1E8DC;
+ box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
+ border-radius: 6px;
+ color: #693410;
+
+ &:first-child {
+ margin-top: 0;
+ }
+}
+
+.frameDivider {
+ height: 0;
+ border: none;
+ border-top: 1px solid #693410;
+ border-bottom: 1px solid #ce8a5c;
+}
+
+.rankingRecord {
+ display: flex;
+ line-height: 24px;
+ padding-top: 4px;
+ white-space: nowrap;
+ overflow: visible;
+ text-overflow: ellipsis;
+}
+</style>
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index 07b44a1051..12e9416f72 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -1,13 +1,15 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModalWindow
- ref="dialog"
- :width="400"
- @close="dialog.close()"
+<MkWindow
+ ref="windowEl"
+ :initialWidth="400"
+ :initialHeight="500"
+ :canResize="false"
+ @close="windowEl.close()"
@closed="$emit('closed')"
>
<template v-if="emoji" #header>:{{ emoji.name }}:</template>
@@ -39,9 +41,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<MkInput v-model="aliases" autocapitalize="off">
<template #label>{{ i18n.ts.tags }}</template>
- <template #caption>{{ i18n.ts.setMultipleBySeparatingWithSpace }}</template>
+ <template #caption>
+ {{ i18n.ts.theKeywordWhenSearchingForCustomEmoji }}<br/>
+ {{ i18n.ts.setMultipleBySeparatingWithSpace }}
+ </template>
</MkInput>
- <MkInput v-model="license">
+ <MkInput v-model="license" :mfmAutocomplete="true">
<template #label>{{ i18n.ts.license }}</template>
</MkInput>
<MkFolder>
@@ -70,18 +75,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded style="margin: 0 auto;" @click="done"><i class="ti ti-check"></i> {{ props.emoji ? i18n.ts.update : i18n.ts.create }}</MkButton>
</div>
</div>
-</MkModalWindow>
+</MkWindow>
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkWindow from '@/components/MkWindow.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { customEmojiCategories } from '@/custom-emojis.js';
import MkSwitch from '@/components/MkSwitch.vue';
@@ -92,7 +98,7 @@ const props = defineProps<{
emoji?: any,
}>();
-const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
+const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
const name = ref<string>(props.emoji ? props.emoji.name : '');
const category = ref<string>(props.emoji ? props.emoji.category : '');
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
@@ -104,7 +110,7 @@ const rolesThatCanBeUsedThisEmojiAsReaction = ref<Misskey.entities.Role[]>([]);
const file = ref<Misskey.entities.DriveFile>();
watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
- rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
+ rolesThatCanBeUsedThisEmojiAsReaction.value = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.value.map((id) => misskeyApi('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
}, { immediate: true });
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
@@ -123,7 +129,7 @@ async function changeImage(ev) {
}
async function addRole() {
- const roles = await os.api('admin/roles/list');
+ const roles = await misskeyApi('admin/roles/list');
const currentRoleIds = rolesThatCanBeUsedThisEmojiAsReaction.value.map(x => x.id);
const { canceled, result: role } = await os.select({
@@ -166,7 +172,7 @@ async function done() {
},
});
- dialog.value.close();
+ windowEl.value.close();
} else {
const created = await os.apiWithDialog('admin/emoji/add', params);
@@ -174,24 +180,24 @@ async function done() {
created: created,
});
- dialog.value.close();
+ windowEl.value.close();
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: name.value }),
+ text: i18n.tsx.removeAreYouSure({ x: name.value }),
});
if (canceled) return;
- os.api('admin/emoji/delete', {
+ misskeyApi('admin/emoji/delete', {
id: props.emoji.id,
}).then(() => {
emit('done', {
deleted: true,
});
- dialog.value.close();
+ windowEl.value.close();
});
}
</script>
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index 9ba9047ca3..5301a08521 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,18 +14,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { } from 'vue';
import * as os from '@/os.js';
+import * as Misskey from 'misskey-js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
+import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
const props = defineProps<{
- emoji: {
- name: string;
- aliases: string[];
- category: string;
- url: string;
- };
+ emoji: Misskey.entities.EmojiSimple;
}>();
function menu(ev) {
@@ -42,12 +39,13 @@ function menu(ev) {
}, {
text: i18n.ts.info,
icon: 'ti ti-info-circle',
- action: () => {
- os.apiGet('emoji', { name: props.emoji.name }).then(res => {
- os.alert({
- type: 'info',
- text: `Name: ${res.name}\nAliases: ${res.aliases.join(' ')}\nCategory: ${res.category}\nisSensitive: ${res.isSensitive}\nlocalOnly: ${res.localOnly}\nLicense: ${res.license}\nURL: ${res.url}`,
- });
+ action: async () => {
+ os.popup(MkCustomEmojiDetailedDialog, {
+ emoji: await misskeyApiGet('emoji', {
+ name: props.emoji.name,
+ })
+ }, {
+ anchor: ev.target,
});
},
}], ev.currentTarget ?? ev.target);
diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue
index 000371528e..b5c8e70166 100644
--- a/packages/frontend/src/pages/explore.featured.vue
+++ b/packages/frontend/src/pages/explore.featured.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue
index d30e107e97..389cd23ad2 100644
--- a/packages/frontend/src/pages/explore.roles.vue
+++ b/packages/frontend/src/pages/explore.roles.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkRolePreview from '@/components/MkRolePreview.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
const roles = ref<Misskey.entities.Role[] | null>(null);
-os.api('roles/list').then(res => {
+misskeyApi('roles/list').then(res => {
roles.value = res.filter(x => x.target === 'manual').sort((a, b) => b.displayOrder - a.displayOrder);
});
</script>
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index 73c2a94fc0..e9608ae94e 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -68,7 +68,7 @@ import * as Misskey from 'misskey-js';
import MkUserList from '@/components/MkUserList.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkTab from '@/components/MkTab.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -123,14 +123,14 @@ const recentlyRegisteredUsersF = { endpoint: 'users', limit: 10, noPaging: true,
sort: '+createdAt',
} };
-os.api('hashtags/list', {
+misskeyApi('hashtags/list', {
sort: '+attachedLocalUsers',
attachedToLocalUserOnly: true,
limit: 30,
}).then(tags => {
tagsLocal.value = tags;
});
-os.api('hashtags/list', {
+misskeyApi('hashtags/list', {
sort: '+attachedRemoteUsers',
attachedToRemoteUserOnly: true,
limit: 30,
diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue
index f068de8880..b1a8183d9b 100644
--- a/packages/frontend/src/pages/explore.vue
+++ b/packages/frontend/src/pages/explore.vue
@@ -1,22 +1,22 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
- <div>
- <div v-if="tab === 'featured'">
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="tab === 'featured'" key="featured">
<XFeatured/>
</div>
- <div v-else-if="tab === 'users'">
+ <div v-else-if="tab === 'users'" key="users">
<XUsers/>
</div>
- <div v-else-if="tab === 'roles'">
+ <div v-else-if="tab === 'roles'" key="roles">
<XRoles/>
</div>
- </div>
+ </MkHorizontalSwipe>
</MkStickyContainer>
</template>
@@ -26,6 +26,7 @@ import XFeatured from './explore.featured.vue';
import XUsers from './explore.users.vue';
import XRoles from './explore.roles.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
@@ -59,8 +60,8 @@ const headerTabs = computed(() => [{
title: i18n.ts.roles,
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.explore,
icon: 'ti ti-hash',
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index 63a0057b74..c3d4cae4aa 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -38,10 +38,10 @@ const pagination = {
limit: 10,
};
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.favorites,
icon: 'ti ti-star',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index 147a381c98..4418172e62 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="title">
<template #label>{{ i18n.ts._play.title }}</template>
</MkInput>
- <MkTextarea v-model="summary">
+ <MkTextarea v-model="summary" :mfmAutocomplete="true" :mfmPreview="true">
<template #label>{{ i18n.ts._play.summary }}</template>
</MkTextarea>
<MkButton primary @click="selectPreset">{{ i18n.ts.selectFromPresets }}<i class="ti ti-chevron-down"></i></MkButton>
@@ -38,13 +38,14 @@ import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkTextarea from '@/components/MkTextarea.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.vue';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/router/supplier.js';
const PRESET_DEFAULT = `/// @ 0.16.0
@@ -369,7 +370,7 @@ const flash = ref<Misskey.entities.Flash | null>(null);
const visibility = ref<Misskey.entities.FlashUpdateRequest['visibility']>('public');
if (props.id) {
- flash.value = await os.api('flash/show', {
+ flash.value = await misskeyApi('flash/show', {
flashId: props.id,
});
}
@@ -437,7 +438,7 @@ function show() {
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: flash.value.title }),
+ text: i18n.tsx.deleteAreYouSure({ x: flash.value.title }),
});
if (canceled) return;
@@ -451,9 +452,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => flash.value ? {
- title: i18n.ts._play.edit + ': ' + flash.value.title,
-} : {
- title: i18n.ts._play.new,
+definePageMetadata(() => ({
+ title: flash.value ? `${i18n.ts._play.edit}: ${flash.value.title}` : i18n.ts._play.new,
}));
</script>
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index e0b9f87d46..f63a799365 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,32 +7,34 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700">
- <div v-if="tab === 'featured'">
- <MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
- <div class="_gaps_s">
- <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
- </div>
- </MkPagination>
- </div>
-
- <div v-else-if="tab === 'my'">
- <div class="_gaps">
- <MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton>
- <MkPagination v-slot="{items}" :pagination="myFlashsPagination">
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="tab === 'featured'" key="featured">
+ <MkPagination v-slot="{items}" :pagination="featuredFlashsPagination">
<div class="_gaps_s">
<MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
</div>
</MkPagination>
</div>
- </div>
- <div v-else-if="tab === 'liked'">
- <MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
- <div class="_gaps_s">
- <MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
+ <div v-else-if="tab === 'my'" key="my">
+ <div class="_gaps">
+ <MkButton gradate rounded style="margin: 0 auto;" @click="create()"><i class="ti ti-plus"></i></MkButton>
+ <MkPagination v-slot="{items}" :pagination="myFlashsPagination">
+ <div class="_gaps_s">
+ <MkFlashPreview v-for="flash in items" :key="flash.id" :flash="flash"/>
+ </div>
+ </MkPagination>
</div>
- </MkPagination>
- </div>
+ </div>
+
+ <div v-else-if="tab === 'liked'" key="liked">
+ <MkPagination v-slot="{items}" :pagination="likedFlashsPagination">
+ <div class="_gaps_s">
+ <MkFlashPreview v-for="like in items" :key="like.flash.id" :flash="like.flash"/>
+ </div>
+ </MkPagination>
+ </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -42,9 +44,10 @@ import { computed, ref } from 'vue';
import MkFlashPreview from '@/components/MkFlashPreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
-import { useRouter } from '@/router.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -87,8 +90,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-heart',
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: 'Play',
icon: 'ti ti-player-play',
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 6df9bbc241..4aa3ce1672 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else :class="$style.ready">
<div class="_panel main">
<div class="title">{{ flash.title }}</div>
- <div class="summary">{{ flash.summary }}</div>
+ <div class="summary"><Mfm :text="flash.summary"/></div>
<MkButton class="start" gradate rounded large @click="start">Play</MkButton>
<div class="info">
<span v-tooltip="i18n.ts.numberOfLikes"><i class="ti ti-heart"></i> {{ flash.likedCount }}</span>
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts._play.viewSource }}</template>
- <MkCode :code="flash.script" lang="is" :inline="false" class="_monospace"/>
+ <MkCode :code="flash.script" lang="is" class="_monospace"/>
</MkFolder>
<div :class="$style.footer">
<Mfm :text="`By @${flash.user.username}`"/>
@@ -62,12 +62,13 @@ import * as Misskey from 'misskey-js';
import { Interpreter, Parser, values } from '@syuilo/aiscript';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@/config.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkAsUi from '@/components/MkAsUi.vue';
import { AsUiComponent, AsUiRoot, registerAsUiLib } from '@/scripts/aiscript/ui.js';
-import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import MkFolder from '@/components/MkFolder.vue';
import MkCode from '@/components/MkCode.vue';
import { defaultStore } from '@/store.js';
@@ -84,7 +85,7 @@ const error = ref<any>(null);
function fetchFlash() {
flash.value = null;
- os.api('flash/show', {
+ misskeyApi('flash/show', {
flashId: props.id,
}).then(_flash => {
flash.value = _flash;
@@ -162,15 +163,7 @@ async function run() {
THIS_ID: values.STR(flash.value.id),
THIS_URL: values.STR(`${url}/play/${flash.value.id}`),
}, {
- in: (q) => {
- return new Promise(ok => {
- os.inputText({
- title: q,
- }).then(({ result: a }) => {
- ok(a ?? '');
- });
- });
- },
+ in: aiScriptReadline,
out: (value) => {
// nop
},
@@ -212,15 +205,17 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => flash.value ? {
- title: flash.value.title,
- avatar: flash.value.user,
- path: `/play/${flash.value.id}`,
- share: {
- title: flash.value.title,
- text: flash.value.summary,
- },
-} : null));
+definePageMetadata(() => ({
+ title: flash.value ? flash.value.title : 'Play',
+ ...flash.value ? {
+ avatar: flash.value.user,
+ path: `/play/${flash.value.id}`,
+ share: {
+ title: flash.value.title,
+ text: flash.value.summary,
+ },
+ } : {},
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 51f31b1ca5..8991af8086 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -41,7 +41,7 @@ import { shallowRef, computed } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import { userPage, acct } from '@/filters/user.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { infoImageUrl } from '@/instance.js';
@@ -54,13 +54,13 @@ const pagination = {
};
function accept(user) {
- os.api('following/requests/accept', { userId: user.id }).then(() => {
+ misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
paginationComponent.value.reload();
});
}
function reject(user) {
- os.api('following/requests/reject', { userId: user.id }).then(() => {
+ misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
paginationComponent.value.reload();
});
}
@@ -69,10 +69,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.followRequests,
icon: 'ti ti-user-plus',
-})));
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue
index a0a4a480b5..247b0ac639 100644
--- a/packages/frontend/src/pages/follow.vue
+++ b/packages/frontend/src/pages/follow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -12,14 +12,15 @@ SPDX-License-Identifier: AGPL-3.0-only
import { } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
-import { mainRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-import { defaultStore } from "@/store.js";
+import { defaultStore } from '@/store.js';
+import { mainRouter } from '@/router/main.js';
async function follow(user): Promise<void> {
const { canceled } = await os.confirm({
type: 'question',
- text: i18n.t('followConfirm', { name: user.name || user.username }),
+ text: i18n.tsx.followConfirm({ name: user.name || user.username }),
});
if (canceled) {
@@ -42,7 +43,7 @@ if (acct == null) {
let promise;
if (acct.startsWith('https://')) {
- promise = os.api('ap/show', {
+ promise = misskeyApi('ap/show', {
uri: acct,
});
promise.then(res => {
@@ -60,7 +61,7 @@ if (acct.startsWith('https://')) {
}
});
} else {
- promise = os.api('users/show', Misskey.acct.parse(acct));
+ promise = misskeyApi('users/show', Misskey.acct.parse(acct));
promise.then(user => {
follow(user);
});
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index d711cb4e88..a68a7e5c41 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -47,9 +47,10 @@ import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import { selectFiles } from '@/scripts/select-file.js';
import * as os from '@/os.js';
-import { useRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -107,7 +108,7 @@ async function del() {
}
watch(() => props.postId, () => {
- init.value = () => props.postId ? os.api('gallery/posts/show', {
+ init.value = () => props.postId ? misskeyApi('gallery/posts/show', {
postId: props.postId,
}).then(post => {
files.value = post.files ?? [];
@@ -121,11 +122,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => props.postId ? {
- title: i18n.ts.edit,
- icon: 'ti ti-pencil',
-} : {
- title: i18n.ts.postToGallery,
+definePageMetadata(() => ({
+ title: props.postId ? i18n.ts.edit : i18n.ts.postToGallery,
icon: 'ti ti-pencil',
}));
</script>
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index 8d9ac07805..e0e187f2ce 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,8 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="1400">
- <div class="_root">
- <div v-if="tab === 'explore'">
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="tab === 'explore'" key="explore">
<MkFoldableSection class="_margin">
<template #header><i class="ti ti-clock"></i>{{ i18n.ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="recentPostsPagination" :disableAutoLoad="true">
@@ -26,14 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPagination>
</MkFoldableSection>
</div>
- <div v-else-if="tab === 'liked'">
+ <div v-else-if="tab === 'liked'" key="liked">
<MkPagination v-slot="{items}" :pagination="likedPostsPagination">
<div :class="$style.items">
<MkGalleryPostPreview v-for="like in items" :key="like.id" :post="like.post" class="post"/>
</div>
</MkPagination>
</div>
- <div v-else-if="tab === 'my'">
+ <div v-else-if="tab === 'my'" key="my">
<MkA to="/gallery/new" class="_link" style="margin: 16px;"><i class="ti ti-plus"></i> {{ i18n.ts.postToGallery }}</MkA>
<MkPagination v-slot="{items}" :pagination="myPostsPagination">
<div :class="$style.items">
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkPagination>
</div>
- </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -51,9 +51,10 @@ import { watch, ref, computed } from 'vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -118,10 +119,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-edit',
}]);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.gallery,
icon: 'ti ti-icons',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 77af81cec1..615675225d 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -66,18 +66,19 @@ import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import { url } from '@/config.js';
-import { useRouter } from '@/router.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -97,7 +98,7 @@ const otherPostsPagination = {
function fetchPost() {
post.value = null;
- os.api('gallery/posts/show', {
+ misskeyApi('gallery/posts/show', {
postId: props.postId,
}).then(_post => {
post.value = _post;
@@ -162,10 +163,12 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => post.value ? {
- title: post.value.title,
- avatar: post.value.user,
-} : null));
+definePageMetadata(() => ({
+ title: post.value ? post.value.title : i18n.ts.gallery,
+ ...post.value ? {
+ avatar: post.value.user,
+ } : {},
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue
new file mode 100644
index 0000000000..afd6df1ad9
--- /dev/null
+++ b/packages/frontend/src/pages/games.vue
@@ -0,0 +1,34 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+ <template #header><MkPageHeader/></template>
+ <MkSpacer :contentMax="800">
+ <div class="_gaps">
+ <div class="_panel">
+ <MkA to="/bubble-game">
+ <img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
+ </MkA>
+ </div>
+ <div class="_panel">
+ <MkA to="/reversi">
+ <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
+ </MkA>
+ </div>
+ </div>
+ </MkSpacer>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { i18n } from '@/i18n.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+
+definePageMetadata(() => ({
+ title: 'Misskey Games',
+ icon: 'ti ti-device-gamepad',
+}));
+</script>
diff --git a/packages/frontend/src/pages/install-extentions.vue b/packages/frontend/src/pages/install-extensions.vue
index 8117699849..4bee437f65 100644
--- a/packages/frontend/src/pages/install-extentions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -105,6 +105,7 @@ import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
import { parseThemeCode, installTheme } from '@/scripts/install-theme.js';
import { unisonReload } from '@/scripts/unison-reload.js';
@@ -159,7 +160,7 @@ async function fetch() {
uiPhase.value = 'error';
return;
}
- const res = await os.api('fetch-external-resources', {
+ const res = await misskeyApi('fetch-external-resources', {
url: url.value,
hash: hash.value,
}).catch((err) => {
@@ -311,10 +312,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts._externalResourceInstaller.title,
icon: 'ti ti-download',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 97dc0a8633..2f1557182a 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,111 +7,113 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer v-if="instance" :contentMax="600" :marginMin="16" :marginMax="32">
- <div v-if="tab === 'overview'" class="_gaps_m">
- <div class="fnfelxur">
- <img :src="faviconUrl" alt="" class="icon"/>
- <span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
- </div>
- <div style="display: flex; flex-direction: column; gap: 1em;">
- <MkKeyValue :copy="host" oneline>
- <template #key>Host</template>
- <template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
- </MkKeyValue>
- <MkKeyValue oneline>
- <template #key>{{ i18n.ts.software }}</template>
- <template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
- </MkKeyValue>
- <MkKeyValue oneline>
- <template #key>{{ i18n.ts.administrator }}</template>
- <template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="tab === 'overview'" key="overview" class="_gaps_m">
+ <div class="fnfelxur">
+ <img :src="faviconUrl" alt="" class="icon"/>
+ <span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
+ </div>
+ <div style="display: flex; flex-direction: column; gap: 1em;">
+ <MkKeyValue :copy="host" oneline>
+ <template #key>Host</template>
+ <template #value><span class="_monospace"><MkLink :url="`https://${host}`">{{ host }}</MkLink></span></template>
+ </MkKeyValue>
+ <MkKeyValue oneline>
+ <template #key>{{ i18n.ts.software }}</template>
+ <template #value><span class="_monospace">{{ instance.softwareName || `(${i18n.ts.unknown})` }} / {{ instance.softwareVersion || `(${i18n.ts.unknown})` }}</span></template>
+ </MkKeyValue>
+ <MkKeyValue oneline>
+ <template #key>{{ i18n.ts.administrator }}</template>
+ <template #value>{{ instance.maintainerName || `(${i18n.ts.unknown})` }} ({{ instance.maintainerEmail || `(${i18n.ts.unknown})` }})</template>
+ </MkKeyValue>
+ </div>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.description }}</template>
+ <template #value>{{ instance.description }}</template>
</MkKeyValue>
- </div>
- <MkKeyValue>
- <template #key>{{ i18n.ts.description }}</template>
- <template #value>{{ instance.description }}</template>
- </MkKeyValue>
- <FormSection v-if="iAmModerator">
- <template #label>Moderation</template>
- <div class="_gaps_s">
- <MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
- <MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
- <MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
- <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
- </div>
- </FormSection>
+ <FormSection v-if="iAmModerator">
+ <template #label>Moderation</template>
+ <div class="_gaps_s">
+ <MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
+ <MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
+ <MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
+ <MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
+ </div>
+ </FormSection>
- <FormSection>
- <MkKeyValue oneline style="margin: 1em 0;">
- <template #key>{{ i18n.ts.registeredAt }}</template>
- <template #value><MkTime mode="detail" :time="instance.firstRetrievedAt"/></template>
- </MkKeyValue>
- <MkKeyValue oneline style="margin: 1em 0;">
- <template #key>{{ i18n.ts.updatedAt }}</template>
- <template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
- </MkKeyValue>
- <MkKeyValue oneline style="margin: 1em 0;">
- <template #key>{{ i18n.ts.latestRequestReceivedAt }}</template>
- <template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
- </MkKeyValue>
- </FormSection>
+ <FormSection>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>{{ i18n.ts.registeredAt }}</template>
+ <template #value><MkTime mode="detail" :time="instance.firstRetrievedAt"/></template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>{{ i18n.ts.updatedAt }}</template>
+ <template #value><MkTime mode="detail" :time="instance.infoUpdatedAt"/></template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>{{ i18n.ts.latestRequestReceivedAt }}</template>
+ <template #value><MkTime v-if="instance.latestRequestReceivedAt" :time="instance.latestRequestReceivedAt"/><span v-else>N/A</span></template>
+ </MkKeyValue>
+ </FormSection>
- <FormSection>
- <MkKeyValue oneline style="margin: 1em 0;">
- <template #key>Following (Pub)</template>
- <template #value>{{ number(instance.followingCount) }}</template>
- </MkKeyValue>
- <MkKeyValue oneline style="margin: 1em 0;">
- <template #key>Followers (Sub)</template>
- <template #value>{{ number(instance.followersCount) }}</template>
- </MkKeyValue>
- </FormSection>
+ <FormSection>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>Following (Pub)</template>
+ <template #value>{{ number(instance.followingCount) }}</template>
+ </MkKeyValue>
+ <MkKeyValue oneline style="margin: 1em 0;">
+ <template #key>Followers (Sub)</template>
+ <template #value>{{ number(instance.followersCount) }}</template>
+ </MkKeyValue>
+ </FormSection>
- <FormSection>
- <template #label>Well-known resources</template>
- <FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
- <FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink>
- <FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink>
- <FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink>
- <FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
- </FormSection>
- </div>
- <div v-else-if="tab === 'chart'" class="_gaps_m">
- <div class="cmhjzshl">
- <div class="selects">
- <MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
- <option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
- <option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
- <option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
- <option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
- <option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
- <option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
- <option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
- <option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
- <option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
- <option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
- <option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
- </MkSelect>
- </div>
- <div class="charts">
- <div class="label">{{ i18n.t('recentNHours', { n: 90 }) }}</div>
- <MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
- <div class="label">{{ i18n.t('recentNDays', { n: 90 }) }}</div>
- <MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
+ <FormSection>
+ <template #label>Well-known resources</template>
+ <FormLink :to="`https://${host}/.well-known/host-meta`" external style="margin-bottom: 8px;">host-meta</FormLink>
+ <FormLink :to="`https://${host}/.well-known/host-meta.json`" external style="margin-bottom: 8px;">host-meta.json</FormLink>
+ <FormLink :to="`https://${host}/.well-known/nodeinfo`" external style="margin-bottom: 8px;">nodeinfo</FormLink>
+ <FormLink :to="`https://${host}/robots.txt`" external style="margin-bottom: 8px;">robots.txt</FormLink>
+ <FormLink :to="`https://${host}/manifest.json`" external style="margin-bottom: 8px;">manifest.json</FormLink>
+ </FormSection>
+ </div>
+ <div v-else-if="tab === 'chart'" key="chart" class="_gaps_m">
+ <div class="cmhjzshl">
+ <div class="selects">
+ <MkSelect v-model="chartSrc" style="margin: 0 10px 0 0; flex: 1;">
+ <option value="instance-requests">{{ i18n.ts._instanceCharts.requests }}</option>
+ <option value="instance-users">{{ i18n.ts._instanceCharts.users }}</option>
+ <option value="instance-users-total">{{ i18n.ts._instanceCharts.usersTotal }}</option>
+ <option value="instance-notes">{{ i18n.ts._instanceCharts.notes }}</option>
+ <option value="instance-notes-total">{{ i18n.ts._instanceCharts.notesTotal }}</option>
+ <option value="instance-ff">{{ i18n.ts._instanceCharts.ff }}</option>
+ <option value="instance-ff-total">{{ i18n.ts._instanceCharts.ffTotal }}</option>
+ <option value="instance-drive-usage">{{ i18n.ts._instanceCharts.cacheSize }}</option>
+ <option value="instance-drive-usage-total">{{ i18n.ts._instanceCharts.cacheSizeTotal }}</option>
+ <option value="instance-drive-files">{{ i18n.ts._instanceCharts.files }}</option>
+ <option value="instance-drive-files-total">{{ i18n.ts._instanceCharts.filesTotal }}</option>
+ </MkSelect>
+ </div>
+ <div class="charts">
+ <div class="label">{{ i18n.tsx.recentNHours({ n: 90 }) }}</div>
+ <MkChart class="chart" :src="chartSrc" span="hour" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
+ <div class="label">{{ i18n.tsx.recentNDays({ n: 90 }) }}</div>
+ <MkChart class="chart" :src="chartSrc" span="day" :limit="90" :args="{ host: host }" :detailed="true"></MkChart>
+ </div>
</div>
</div>
- </div>
- <div v-else-if="tab === 'users'" class="_gaps_m">
- <MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
- <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`">
- <MkUserCardMini :user="user"/>
- </MkA>
- </MkPagination>
- </div>
- <div v-else-if="tab === 'raw'" class="_gaps_m">
- <MkObjectView tall :value="instance">
- </MkObjectView>
- </div>
+ <div v-else-if="tab === 'users'" key="users" class="_gaps_m">
+ <MkPagination v-slot="{items}" :pagination="usersPagination" style="display: grid; grid-template-columns: repeat(auto-fill,minmax(270px,1fr)); grid-gap: 12px;">
+ <MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/admin/user/${user.id}`">
+ <MkUserCardMini :user="user"/>
+ </MkA>
+ </MkPagination>
+ </div>
+ <div v-else-if="tab === 'raw'" key="raw" class="_gaps_m">
+ <MkObjectView tall :value="instance">
+ </MkObjectView>
+ </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -129,12 +131,14 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import number from '@/filters/number.js';
import { iAmModerator, iAmAdmin } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination from '@/components/MkPagination.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
import { dateString } from '@/filters/date.js';
@@ -143,6 +147,7 @@ const props = defineProps<{
}>();
const tab = ref('overview');
+
const chartSrc = ref('instance-requests');
const meta = ref<Misskey.entities.AdminMetaResponse | null>(null);
const instance = ref<Misskey.entities.FederationInstance | null>(null);
@@ -164,9 +169,9 @@ const usersPagination = {
async function fetch(): Promise<void> {
if (iAmAdmin) {
- meta.value = await os.api('admin/meta');
+ meta.value = await misskeyApi('admin/meta');
}
- instance.value = await os.api('federation/show-instance', {
+ instance.value = await misskeyApi('federation/show-instance', {
host: props.host,
});
suspended.value = instance.value?.isSuspended ?? false;
@@ -179,7 +184,7 @@ async function toggleBlock(): Promise<void> {
if (!meta.value) throw new Error('No meta?');
if (!instance.value) throw new Error('No instance?');
const { host } = instance.value;
- await os.api('admin/update-meta', {
+ await misskeyApi('admin/update-meta', {
blockedHosts: isBlocked.value ? meta.value.blockedHosts.concat([host]) : meta.value.blockedHosts.filter(x => x !== host),
});
}
@@ -189,14 +194,14 @@ async function toggleSilenced(): Promise<void> {
if (!instance.value) throw new Error('No instance?');
const { host } = instance.value;
const silencedHosts = meta.value.silencedHosts ?? [];
- await os.api('admin/update-meta', {
+ await misskeyApi('admin/update-meta', {
silencedHosts: isSilenced.value ? silencedHosts.concat([host]) : silencedHosts.filter(x => x !== host),
});
}
async function toggleSuspend(): Promise<void> {
if (!instance.value) throw new Error('No instance?');
- await os.api('admin/federation/update-instance', {
+ await misskeyApi('admin/federation/update-instance', {
host: instance.value.host,
isSuspended: suspended.value,
});
@@ -204,7 +209,7 @@ async function toggleSuspend(): Promise<void> {
function refreshMetadata(): void {
if (!instance.value) throw new Error('No instance?');
- os.api('admin/federation/refresh-remote-instance-metadata', {
+ misskeyApi('admin/federation/refresh-remote-instance-metadata', {
host: instance.value.host,
});
os.alert({
@@ -240,10 +245,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-code',
}]);
-definePageMetadata({
+definePageMetadata(() => ({
title: props.host,
icon: 'ti ti-server',
-});
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 25ce38e0ef..25e56d2b8d 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -19,9 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</MKSpacer>
<MkSpacer v-else :contentMax="800">
<div class="_gaps_m" style="text-align: center;">
- <div v-if="resetCycle && inviteLimit">{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}</div>
+ <div v-if="resetCycle && inviteLimit">{{ i18n.tsx.inviteLimitResetCycle({ time: resetCycle, limit: inviteLimit }) }}</div>
<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton>
- <div v-if="currentInviteLimit !== null">{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}</div>
+ <div v-if="currentInviteLimit !== null">{{ i18n.tsx.createLimitRemaining({ limit: currentInviteLimit }) }}</div>
<MkPagination ref="pagingComponent" :pagination="pagination">
<template #default="{ items }">
@@ -40,6 +40,7 @@ import { computed, ref, shallowRef } from 'vue';
import type * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkButton from '@/components/MkButton.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import MkInviteCode from '@/components/MkInviteCode.vue';
@@ -68,7 +69,7 @@ const resetCycle = computed<null | string>(() => {
});
async function create() {
- const ticket = await os.api('invite/create');
+ const ticket = await misskeyApi('invite/create');
os.alert({
type: 'success',
title: i18n.ts.inviteCodeCreated,
@@ -87,15 +88,15 @@ function deleted(id: string) {
}
async function update() {
- currentInviteLimit.value = (await os.api('invite/limit')).remaining;
+ currentInviteLimit.value = (await misskeyApi('invite/limit')).remaining;
}
update();
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.invite,
icon: 'ti ti-user-plus',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 936d078848..954246ff93 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -37,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { watch, computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
@@ -53,12 +54,12 @@ const error = ref();
const users = ref<Misskey.entities.UserDetailed[]>([]);
function fetchList(): void {
- os.api('users/lists/show', {
+ misskeyApi('users/lists/show', {
listId: props.listId,
forPublic: true,
}).then(_list => {
list.value = _list;
- os.api('users/show', {
+ misskeyApi('users/show', {
userIds: list.value.userIds,
}).then(_users => {
users.value = _users;
@@ -100,10 +101,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => list.value ? {
- title: list.value.name,
+definePageMetadata(() => ({
+ title: list.value ? list.value.name : i18n.ts.lists,
icon: 'ti ti-list',
-} : null));
+}));
</script>
<style lang="scss" module>
.main {
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index ad9bea4548..ffaf739ed0 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else>
<div v-if="_permissions.length > 0">
- <p v-if="name">{{ i18n.t('_auth.permission', { name }) }}</p>
+ <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
<ul>
- <li v-for="p in _permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
+ <li v-for="p in _permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
</ul>
</div>
- <div v-if="name">{{ i18n.t('_auth.shareAccess', { name }) }}</div>
+ <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
<div :class="$style.buttons">
<MkButton inline @click="deny">{{ i18n.ts.cancel }}</MkButton>
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import MkSignin from '@/components/MkSignin.vue';
import MkButton from '@/components/MkButton.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i, login } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -65,7 +65,7 @@ const state = ref<string | null>(null);
async function accept(): Promise<void> {
state.value = 'waiting';
- await os.api('miauth/gen-token', {
+ await misskeyApi('miauth/gen-token', {
session: props.session,
name: props.name,
iconUrl: props.icon,
@@ -93,10 +93,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: 'MiAuth',
icon: 'ti ti-apps',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index c5b1b54222..8b3b3cfbfd 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,8 +14,8 @@ import { ref } from 'vue';
import XAntenna from './editor.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/router.js';
import { antennasCache } from '@/cache.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -38,8 +38,8 @@ function onAntennaCreated() {
router.push('/my/antennas');
}
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.manageAntennas,
icon: 'ti ti-antenna',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index 9b3d56ee36..9471be8575 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -13,11 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import XAntenna from './editor.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-import { useRouter } from '@/router.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { antennasCache } from '@/cache.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -32,12 +32,12 @@ function onAntennaUpdated() {
router.push('/my/antennas');
}
-os.api('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => {
+misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaResponse) => {
antenna.value = antennaResponse;
});
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.manageAntennas,
icon: 'ti ti-antenna',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue
index 9b19206d96..c6dcbadd9b 100644
--- a/packages/frontend/src/pages/my-antennas/editor.vue
+++ b/packages/frontend/src/pages/my-antennas/editor.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -57,6 +57,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -84,7 +85,7 @@ const userLists = ref<Misskey.entities.UserList[] | null>(null);
watch(() => src.value, async () => {
if (src.value === 'list' && userLists.value === null) {
- userLists.value = await os.api('users/lists/list');
+ userLists.value = await misskeyApi('users/lists/list');
}
});
@@ -115,11 +116,11 @@ async function saveAntenna() {
async function deleteAntenna() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: props.antenna.name }),
+ text: i18n.tsx.removeAreYouSure({ x: props.antenna.name }),
});
if (canceled) return;
- await os.api('antennas/delete', {
+ await misskeyApi('antennas/delete', {
antennaId: props.antenna.id,
});
@@ -128,7 +129,7 @@ async function deleteAntenna() {
}
function addUser() {
- os.selectUser().then(user => {
+ os.selectUser({ includeSelf: true }).then(user => {
users.value = users.value.trim();
users.value += '\n@' + Misskey.acct.toString(user as any);
users.value = users.value.trim();
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index f2bf9a7ec5..21c96348f0 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -55,10 +55,10 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.manageAntennas,
icon: 'ti ti-antenna',
-});
+}));
onActivated(() => {
antennasCache.fetch();
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index bc09e916e3..803b28899a 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,20 +7,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700">
- <div v-if="tab === 'my'" class="_gaps">
- <MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="tab === 'my'" key="my" class="_gaps">
+ <MkButton primary rounded class="add" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
- <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
- <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
+ <MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="_gaps">
+ <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">
+ <MkClipPreview :clip="item"/>
+ </MkA>
+ </MkPagination>
+ </div>
+ <div v-else-if="tab === 'favorites'" key="favorites" class="_gaps">
+ <MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
<MkClipPreview :clip="item"/>
</MkA>
- </MkPagination>
- </div>
- <div v-else-if="tab === 'favorites'" class="_gaps">
- <MkA v-for="item in favorites" :key="item.id" :to="`/clips/${item.id}`">
- <MkClipPreview :clip="item"/>
- </MkA>
- </div>
+ </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -32,9 +34,11 @@ import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkClipPreview from '@/components/MkClipPreview.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { clipsCache } from '@/cache.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const pagination = {
endpoint: 'clips/list' as const,
@@ -43,12 +47,13 @@ const pagination = {
};
const tab = ref('my');
+
const favorites = ref<Misskey.entities.Clip[] | null>(null);
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
watch(tab, async () => {
- favorites.value = await os.api('clips/my-favorites');
+ favorites.value = await misskeyApi('clips/my-favorites');
});
async function create() {
@@ -99,14 +104,10 @@ const headerTabs = computed(() => [{
icon: 'ti ti-heart',
}]);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.clip,
icon: 'ti ti-paperclip',
- action: {
- icon: 'ti ti-plus',
- handler: create,
- },
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 0abfb15d98..82fde284c1 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="items.length > 0" class="_gaps">
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
- <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }})</span></div>
+ <div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
<MkAvatars :userIds="list.userIds" :limit="10"/>
</MkA>
</div>
@@ -37,7 +37,9 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { userListsCache } from '@/cache.js';
import { infoImageUrl } from '@/instance.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
const items = computed(() => userListsCache.value.value ?? []);
@@ -69,10 +71,10 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.manageLists,
icon: 'ti ti-list',
-});
+}));
onActivated(() => {
fetch();
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index cf9da02868..7492b099ea 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder defaultOpen>
<template #label>{{ i18n.ts.members }}</template>
- <template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }}</template>
+ <template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
<div class="_gaps_s">
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
@@ -57,7 +57,7 @@ import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
-import { mainRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { userPage } from '@/filters/user.js';
@@ -66,9 +66,12 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInput from '@/components/MkInput.vue';
import { userListsCache } from '@/cache.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
import { defaultStore } from '@/store.js';
import MkPagination from '@/components/MkPagination.vue';
+import { mainRouter } from '@/router/main.js';
+
+const $i = signinRequired();
const {
enableInfiniteScroll,
@@ -91,7 +94,7 @@ const membershipsPagination = {
};
function fetchList() {
- os.api('users/lists/show', {
+ misskeyApi('users/lists/show', {
listId: props.listId,
}).then(_list => {
list.value = _list;
@@ -119,7 +122,7 @@ async function removeUser(item, ev) {
danger: true,
action: async () => {
if (!list.value) return;
- os.api('users/lists/pull', {
+ misskeyApi('users/lists/pull', {
listId: list.value.id,
userId: item.userId,
}).then(() => {
@@ -134,7 +137,7 @@ async function showMembershipMenu(item, ev) {
text: item.withReplies ? i18n.ts.hideRepliesToOthersInTimeline : i18n.ts.showRepliesToOthersInTimeline,
icon: item.withReplies ? 'ti ti-messages-off' : 'ti ti-messages',
action: async () => {
- os.api('users/lists/update-membership', {
+ misskeyApi('users/lists/update-membership', {
listId: list.value.id,
userId: item.userId,
withReplies: !item.withReplies,
@@ -152,7 +155,7 @@ async function deleteList() {
if (!list.value) return;
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: list.value.name }),
+ text: i18n.tsx.removeAreYouSure({ x: list.value.name }),
});
if (canceled) return;
@@ -183,10 +186,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => list.value ? {
- title: list.value.name,
+definePageMetadata(() => ({
+ title: list.value ? list.value.name : i18n.ts.lists,
icon: 'ti ti-list',
-} : null));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 2245147873..93a792c42f 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -31,8 +31,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.notFound,
icon: 'ti ti-alert-triangle',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index ff1e80aaab..4c985b96e6 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -11,11 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="note">
<div v-if="showNext" class="_margin">
- <MkNotes class="" :pagination="nextPagination" :noGap="true" :disableAutoLoad="true"/>
+ <MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
</div>
<div class="_margin">
- <MkButton v-if="!showNext" :class="$style.loadNext" @click="showNext = true"><i class="ti ti-chevron-up"></i></MkButton>
+ <div v-if="!showNext" class="_buttons" :class="$style.loadNext">
+ <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ti ti-chevron-up"></i> <i class="ti ti-device-tv"></i></MkButton>
+ <MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton>
+ </div>
<div class="_margin _gaps_s">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
<MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note"/>
@@ -28,11 +31,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</div>
</div>
- <MkButton v-if="!showPrev" :class="$style.loadPrev" @click="showPrev = true"><i class="ti ti-chevron-down"></i></MkButton>
+ <div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
+ <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ti ti-chevron-down"></i> <i class="ti ti-device-tv"></i></MkButton>
+ <MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ti ti-chevron-down"></i> <i class="ti ti-user"></i></MkButton>
+ </div>
</div>
<div v-if="showPrev" class="_margin">
- <MkNotes class="" :pagination="prevPagination" :noGap="true"/>
+ <MkNotes class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/>
</div>
</div>
<MkError v-else-if="error" @retry="fetchNote()"/>
@@ -46,11 +52,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import type { Paging } from '@/components/MkPagination.vue';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import MkNotes from '@/components/MkNotes.vue';
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
import MkButton from '@/components/MkButton.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
@@ -63,40 +70,59 @@ const props = defineProps<{
const note = ref<null | Misskey.entities.Note>();
const clips = ref<Misskey.entities.Clip[]>();
-const showPrev = ref(false);
-const showNext = ref(false);
+const showPrev = ref<'user' | 'channel' | false>(false);
+const showNext = ref<'user' | 'channel' | false>(false);
const error = ref();
-const prevPagination = {
- endpoint: 'users/notes' as const,
+const prevUserPagination: Paging = {
+ endpoint: 'users/notes',
limit: 10,
params: computed(() => note.value ? ({
userId: note.value.userId,
untilId: note.value.id,
- }) : null),
+ }) : undefined),
};
-const nextPagination = {
+const nextUserPagination: Paging = {
reversed: true,
- endpoint: 'users/notes' as const,
+ endpoint: 'users/notes',
limit: 10,
params: computed(() => note.value ? ({
userId: note.value.userId,
sinceId: note.value.id,
- }) : null),
+ }) : undefined),
+};
+
+const prevChannelPagination: Paging = {
+ endpoint: 'channels/timeline',
+ limit: 10,
+ params: computed(() => note.value ? ({
+ channelId: note.value.channelId,
+ untilId: note.value.id,
+ }) : undefined),
+};
+
+const nextChannelPagination: Paging = {
+ reversed: true,
+ endpoint: 'channels/timeline',
+ limit: 10,
+ params: computed(() => note.value ? ({
+ channelId: note.value.channelId,
+ sinceId: note.value.id,
+ }) : undefined),
};
function fetchNote() {
showPrev.value = false;
showNext.value = false;
note.value = null;
- os.api('notes/show', {
+ misskeyApi('notes/show', {
noteId: props.noteId,
}).then(res => {
note.value = res;
// 古いノートは被クリップ数をカウントしていないので、2023-10-01以前のものは強制的にnotes/clipsを叩く
if (note.value.clippedCount > 0 || new Date(note.value.createdAt).getTime() < new Date('2023-10-01').getTime()) {
- os.api('notes/clips', {
+ misskeyApi('notes/clips', {
noteId: note.value.id,
}).then((_clips) => {
clips.value = _clips;
@@ -115,16 +141,18 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => note.value ? {
+definePageMetadata(() => ({
title: i18n.ts.note,
- subtitle: dateString(note.value.createdAt),
- avatar: note.value.user,
- path: `/notes/${note.value.id}`,
- share: {
- title: i18n.t('noteOf', { user: note.value.user.name }),
- text: note.value.text,
- },
-} : null));
+ ...note.value ? {
+ subtitle: dateString(note.value.createdAt),
+ avatar: note.value.user,
+ path: `/notes/${note.value.id}`,
+ share: {
+ title: i18n.tsx.noteOf({ user: note.value.user.name }),
+ text: note.value.text,
+ },
+ } : {},
+}));
</script>
<style lang="scss" module>
@@ -139,9 +167,7 @@ definePageMetadata(computed(() => note.value ? {
.loadNext,
.loadPrev {
- min-width: 0;
- margin: 0 auto;
- border-radius: 999px;
+ justify-content: center;
}
.loadNext {
@@ -152,6 +178,10 @@ definePageMetadata(computed(() => note.value ? {
margin-top: var(--margin);
}
+.loadButton {
+ min-width: 0;
+}
+
.note {
border-radius: var(--radius);
background: var(--panel);
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index d57bda41b5..7db6fa5395 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,15 +7,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800">
- <div v-if="tab === 'all'">
- <XNotifications class="notifications" :excludeTypes="excludeTypes"/>
- </div>
- <div v-else-if="tab === 'mentions'">
- <MkNotes :pagination="mentionsPagination"/>
- </div>
- <div v-else-if="tab === 'directNotes'">
- <MkNotes :pagination="directNotesPagination"/>
- </div>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="tab === 'all'" key="all">
+ <XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/>
+ </div>
+ <div v-else-if="tab === 'mentions'" key="mention">
+ <MkNotes :pagination="mentionsPagination"/>
+ </div>
+ <div v-else-if="tab === 'directNotes'" key="directNotes">
+ <MkNotes :pagination="directNotesPagination"/>
+ </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -24,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref } from 'vue';
import XNotifications from '@/components/MkNotifications.vue';
import MkNotes from '@/components/MkNotes.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -48,7 +51,7 @@ const directNotesPagination = {
function setFilter(ev) {
const typeItems = notificationTypes.map(t => ({
- text: i18n.t(`_notification._types.${t}`),
+ text: i18n.ts._notification._types[t],
active: includeTypes.value && includeTypes.value.includes(t),
action: () => {
includeTypes.value = [t];
@@ -91,8 +94,15 @@ const headerTabs = computed(() => [{
icon: 'ti ti-mail',
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.notifications,
icon: 'ti ti-bell',
-})));
+}));
</script>
+
+<style module lang="scss">
+.notifications {
+ border-radius: var(--radius);
+ overflow: clip;
+}
+</style>
diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue
index 878fa6be4e..733e34eb2c 100644
--- a/packages/frontend/src/pages/oauth.vue
+++ b/packages/frontend/src/pages/oauth.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -9,13 +9,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800">
<div v-if="$i">
<div v-if="permissions.length > 0">
- <p v-if="name">{{ i18n.t('_auth.permission', { name }) }}</p>
+ <p v-if="name">{{ i18n.tsx._auth.permission({ name }) }}</p>
<p v-else>{{ i18n.ts._auth.permissionAsk }}</p>
<ul>
- <li v-for="p in permissions" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
+ <li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] }}</li>
</ul>
</div>
- <div v-if="name">{{ i18n.t('_auth.shareAccess', { name }) }}</div>
+ <div v-if="name">{{ i18n.tsx._auth.shareAccess({ name }) }}</div>
<div v-else>{{ i18n.ts._auth.shareAccessAsk }}</div>
<form :class="$style.buttons" action="/oauth/decision" accept-charset="utf-8" method="post">
<input name="login_token" type="hidden" :value="$i.token"/>
@@ -51,10 +51,10 @@ function onLogin(res): void {
login(res.i);
}
-definePageMetadata({
+definePageMetadata(() => ({
title: 'OAuth',
icon: 'ti ti-apps',
-});
+}));
</script>
<style lang="scss" module>
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
index 9d6da653b4..1cfe7a6d2d 100644
--- 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
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -26,6 +26,7 @@ import * as Misskey from 'misskey-js';
import XContainer from '../page-editor.container.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -52,7 +53,7 @@ onMounted(async () => {
if (props.modelValue.fileId == null) {
await choose();
} else {
- os.api('drive/files/show', {
+ misskeyApi('drive/files/show', {
fileId: props.modelValue.fileId,
}).then(fileResponse => {
file.value = fileResponse;
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
index ea9d52c2be..194a276f89 100644
--- 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
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -30,7 +30,7 @@ import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkNote from '@/components/MkNote.vue';
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -53,7 +53,7 @@ watch(id, async () => {
...props.modelValue,
note: id.value,
});
- note.value = await os.api('notes/show', { noteId: id.value });
+ note.value = await misskeyApi('notes/show', { noteId: id.value });
}, {
immediate: true,
});
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
index 1220ca29a7..47e9c08c2c 100644
--- 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
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
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
index 643e8ecdad..14c3e6845e 100644
--- 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
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
index 52220d36bb..4967e73000 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.blocks.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue
index 9b0dce820c..f2081c452c 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.container.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index bcfbf5825f..af32fd2274 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -71,11 +71,12 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import { url } from '@/config.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { selectFile } from '@/scripts/select-file.js';
-import { mainRouter } from '@/router.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i } from '@/account.js';
+import { mainRouter } from '@/router/main.js';
const props = defineProps<{
initPageId?: string;
@@ -106,7 +107,7 @@ watch(eyeCatchingImageId, async () => {
if (eyeCatchingImageId.value == null) {
eyeCatchingImage.value = null;
} else {
- eyeCatchingImage.value = await os.api('drive/files/show', {
+ eyeCatchingImage.value = await misskeyApi('drive/files/show', {
fileId: eyeCatchingImageId.value,
});
}
@@ -149,7 +150,7 @@ function save() {
if (pageId.value) {
options.pageId = pageId.value;
- os.api('pages/update', options)
+ misskeyApi('pages/update', options)
.then(page => {
currentName.value = name.value.trim();
os.alert({
@@ -158,7 +159,7 @@ function save() {
});
}).catch(onError);
} else {
- os.api('pages/create', options)
+ misskeyApi('pages/create', options)
.then(created => {
pageId.value = created.id;
currentName.value = name.value.trim();
@@ -174,10 +175,10 @@ function save() {
function del() {
os.confirm({
type: 'warning',
- text: i18n.t('removeAreYouSure', { x: title.value.trim() }),
+ text: i18n.tsx.removeAreYouSure({ x: title.value.trim() }),
}).then(({ canceled }) => {
if (canceled) return;
- os.api('pages/delete', {
+ misskeyApi('pages/delete', {
pageId: pageId.value,
}).then(() => {
os.alert({
@@ -192,7 +193,7 @@ function del() {
function duplicate() {
title.value = title.value + ' - copy';
name.value = name.value + '-copy';
- os.api('pages/create', getSaveOptions()).then(created => {
+ misskeyApi('pages/create', getSaveOptions()).then(created => {
pageId.value = created.id;
currentName.value = name.value.trim();
os.alert({
@@ -236,11 +237,11 @@ function removeEyeCatchingImage() {
async function init() {
if (props.initPageId) {
- page.value = await os.api('pages/show', {
+ page.value = await misskeyApi('pages/show', {
pageId: props.initPageId,
});
} else if (props.initPageName && props.initUser) {
- page.value = await os.api('pages/show', {
+ page.value = await misskeyApi('pages/show', {
name: props.initPageName,
username: props.initUser,
});
@@ -283,17 +284,11 @@ const headerTabs = computed(() => [{
icon: 'ti ti-note',
}]);
-definePageMetadata(computed(() => {
- let title = i18n.ts._pages.newPage;
- if (props.initPageId) {
- title = i18n.ts._pages.editPage;
- } else if (props.initPageName && props.initUser) {
- title = i18n.ts._pages.readPage;
- }
- return {
- title: title,
- icon: 'ti ti-pencil',
- };
+definePageMetadata(() => ({
+ title: props.initPageId ? i18n.ts._pages.editPage
+ : props.initPageName && props.initUser ? i18n.ts._pages.readPage
+ : i18n.ts._pages.newPage,
+ icon: 'ti ti-pencil',
}));
</script>
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 11c8e15e14..bece32fc11 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -81,6 +81,7 @@ import * as Misskey from 'misskey-js';
import XPage from '@/components/page/page.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { url } from '@/config.js';
import MkMediaImage from '@/components/MkMediaImage.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
@@ -113,7 +114,7 @@ const path = computed(() => props.username + '/' + props.pageName);
function fetchPage() {
page.value = null;
- os.api('pages/show', {
+ misskeyApi('pages/show', {
name: props.pageName,
username: props.username,
}).then(async _page => {
@@ -186,15 +187,17 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => page.value ? {
- title: page.value.title || page.value.name,
- avatar: page.value.user,
- path: `/@${page.value.user.username}/pages/${page.value.name}`,
- share: {
- title: page.value.title || page.value.name,
- text: page.value.summary,
- },
-} : null));
+definePageMetadata(() => ({
+ title: page.value ? page.value.title || page.value.name : i18n.ts.pages,
+ ...page.value ? {
+ avatar: page.value.user,
+ path: `/@${page.value.user.username}/pages/${page.value.name}`,
+ share: {
+ title: page.value.title || page.value.name,
+ text: page.value.summary,
+ },
+ } : {},
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue
index bc51b55c7f..4ef9d3b091 100644
--- a/packages/frontend/src/pages/pages.vue
+++ b/packages/frontend/src/pages/pages.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,30 +7,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700">
- <div v-if="tab === 'featured'">
- <MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
- <div class="_gaps">
- <MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
- </div>
- </MkPagination>
- </div>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <div v-if="tab === 'featured'" key="featured">
+ <MkPagination v-slot="{items}" :pagination="featuredPagesPagination">
+ <div class="_gaps">
+ <MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
+ </div>
+ </MkPagination>
+ </div>
- <div v-else-if="tab === 'my'" class="_gaps">
- <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
- <MkPagination v-slot="{items}" :pagination="myPagesPagination">
- <div class="_gaps">
- <MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
- </div>
- </MkPagination>
- </div>
+ <div v-else-if="tab === 'my'" key="my" class="_gaps">
+ <MkButton class="new" @click="create()"><i class="ti ti-plus"></i></MkButton>
+ <MkPagination v-slot="{items}" :pagination="myPagesPagination">
+ <div class="_gaps">
+ <MkPagePreview v-for="page in items" :key="page.id" :page="page"/>
+ </div>
+ </MkPagination>
+ </div>
- <div v-else-if="tab === 'liked'">
- <MkPagination v-slot="{items}" :pagination="likedPagesPagination">
- <div class="_gaps">
- <MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/>
- </div>
- </MkPagination>
- </div>
+ <div v-else-if="tab === 'liked'" key="liked">
+ <MkPagination v-slot="{items}" :pagination="likedPagesPagination">
+ <div class="_gaps">
+ <MkPagePreview v-for="like in items" :key="like.page.id" :page="like.page"/>
+ </div>
+ </MkPagination>
+ </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -40,9 +42,10 @@ import { computed, ref } from 'vue';
import MkPagePreview from '@/components/MkPagePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
-import { useRouter } from '@/router.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -85,8 +88,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-heart',
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.pages,
icon: 'ti ti-note',
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index 822a39c2e8..bac1d2bb70 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -36,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { watch, computed, ref } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import FormLink from '@/components/form/link.vue';
@@ -54,7 +55,7 @@ const scope = computed(() => props.path ? props.path.split('/') : []);
const keys = ref<any>(null);
function fetchKeys() {
- os.api('i/registry/keys-with-type', {
+ misskeyApi('i/registry/keys-with-type', {
scope: scope.value,
domain: props.domain === '@' ? null : props.domain,
}).then(res => {
@@ -95,8 +96,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.registry,
icon: 'ti ti-adjustments',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue
index 243c69eed5..c40d13f664 100644
--- a/packages/frontend/src/pages/registry.value.vue
+++ b/packages/frontend/src/pages/registry.value.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { watch, computed, ref } from 'vue';
import JSON5 from 'json5';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
@@ -68,7 +69,7 @@ const value = ref<any>(null);
const valueForEditor = ref<string | null>(null);
function fetchValue() {
- os.api('i/registry/get-detail', {
+ misskeyApi('i/registry/get-detail', {
scope: scope.value,
key: key.value,
domain: props.domain === '@' ? null : props.domain,
@@ -122,8 +123,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.registry,
icon: 'ti ti-adjustments',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue
index f45f8922ad..c641874b17 100644
--- a/packages/frontend/src/pages/registry.vue
+++ b/packages/frontend/src/pages/registry.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -26,6 +26,7 @@ import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import JSON5 from 'json5';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import FormLink from '@/components/form/link.vue';
@@ -35,7 +36,7 @@ import MkButton from '@/components/MkButton.vue';
const scopesWithDomain = ref<Misskey.entities.IRegistryScopesWithDomainResponse | null>(null);
function fetchScopes() {
- os.api('i/registry/scopes-with-domain').then(res => {
+ misskeyApi('i/registry/scopes-with-domain').then(res => {
scopesWithDomain.value = res;
});
}
@@ -72,8 +73,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.registry,
icon: 'ti ti-adjustments',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index c9d193b787..6b67a9cc87 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,8 +25,8 @@ import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/router.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { mainRouter } from '@/router/main.js';
const props = defineProps<{
token?: string;
@@ -53,8 +53,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.resetPassword,
icon: 'ti ti-lock',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
new file mode 100644
index 0000000000..cf7cec6b5e
--- /dev/null
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -0,0 +1,633 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkSpacer :contentMax="500">
+ <div :class="$style.root" class="_gaps">
+ <div style="display: flex; align-items: center; justify-content: center; gap: 10px;">
+ <span>({{ i18n.ts._reversi.black }})</span>
+ <MkAvatar style="width: 32px; height: 32px;" :user="blackUser" :showIndicator="true"/>
+ <span> vs </span>
+ <MkAvatar style="width: 32px; height: 32px;" :user="whiteUser" :showIndicator="true"/>
+ <span>({{ i18n.ts._reversi.white }})</span>
+ </div>
+
+ <div style="overflow: clip; line-height: 28px;">
+ <div v-if="!iAmPlayer && !game.isEnded && turnUser">
+ <Mfm :key="'turn:' + turnUser.id" :text="i18n.tsx._reversi.turnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
+ <MkEllipsis/>
+ </div>
+ <div v-if="(logPos !== game.logs.length) && turnUser">
+ <Mfm :key="'past-turn-of:' + turnUser.id" :text="i18n.tsx._reversi.pastTurnOf({ name: turnUser.name ?? turnUser.username })" :plain="true" :customEmojis="turnUser.emojis"/>
+ </div>
+ <div v-if="iAmPlayer && !game.isEnded && !isMyTurn">{{ i18n.ts._reversi.opponentTurn }}<MkEllipsis/><span style="margin-left: 1em; opacity: 0.7;">({{ i18n.tsx.remainingN({ n: opTurnTimerRmain }) }})</span></div>
+ <div v-if="iAmPlayer && !game.isEnded && isMyTurn"><span style="display: inline-block; font-weight: bold; animation: global-tada 1s linear infinite both;">{{ i18n.ts._reversi.myTurn }}</span><span style="margin-left: 1em; opacity: 0.7;">({{ i18n.tsx.remainingN({ n: myTurnTimerRmain }) }})</span></div>
+ <div v-if="game.isEnded && logPos == game.logs.length">
+ <template v-if="game.winner">
+ <Mfm :key="'won'" :text="i18n.tsx._reversi.won({ name: game.winner.name ?? game.winner.username })" :plain="true" :customEmojis="game.winner.emojis"/>
+ <span v-if="game.surrenderedUserId != null"> ({{ i18n.ts._reversi.surrendered }})</span>
+ <span v-if="game.timeoutUserId != null"> ({{ i18n.ts._reversi.timeout }})</span>
+ </template>
+ <template v-else>{{ i18n.ts._reversi.drawn }}</template>
+ </div>
+ </div>
+
+ <div :class="$style.board">
+ <div :class="$style.boardInner">
+ <div v-if="showBoardLabels" :class="$style.labelsX">
+ <span v-for="i in game.map[0].length" :key="i" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
+ </div>
+ <div style="display: flex;">
+ <div v-if="showBoardLabels" :class="$style.labelsY">
+ <div v-for="i in game.map.length" :key="i" :class="$style.labelsYLabel">{{ i }}</div>
+ </div>
+ <div :class="$style.boardCells" :style="cellsStyle">
+ <div
+ v-for="(stone, i) in engine.board"
+ :key="i"
+ v-tooltip="`${String.fromCharCode(65 + engine.posToXy(i)[0])}${engine.posToXy(i)[1] + 1}`"
+ :class="[$style.boardCell, {
+ [$style.boardCell_empty]: stone == null,
+ [$style.boardCell_none]: engine.map[i] === 'null',
+ [$style.boardCell_isEnded]: game.isEnded,
+ [$style.boardCell_myTurn]: !game.isEnded && isMyTurn,
+ [$style.boardCell_can]: turnUser ? engine.canPut(turnUser.id === blackUser.id, i) : null,
+ [$style.boardCell_prev]: engine.prevPos === i
+ }]"
+ @click="putStone(i)"
+ >
+ <Transition
+ :enterActiveClass="$style.transition_flip_enterActive"
+ :leaveActiveClass="$style.transition_flip_leaveActive"
+ :enterFromClass="$style.transition_flip_enterFrom"
+ :leaveToClass="$style.transition_flip_leaveTo"
+ mode="default"
+ >
+ <template v-if="useAvatarAsStone">
+ <img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl ?? undefined"/>
+ <img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl ?? undefined"/>
+ </template>
+ <template v-else>
+ <img v-if="stone === true" :class="$style.boardCellStone" src="/client-assets/reversi/stone_b.png"/>
+ <img v-else-if="stone === false" :class="$style.boardCellStone" src="/client-assets/reversi/stone_w.png"/>
+ </template>
+ </Transition>
+ </div>
+ </div>
+ <div v-if="showBoardLabels" :class="$style.labelsY">
+ <div v-for="i in game.map.length" :key="i" :class="$style.labelsYLabel">{{ i }}</div>
+ </div>
+ </div>
+ <div v-if="showBoardLabels" :class="$style.labelsX">
+ <span v-for="i in game.map[0].length" :key="i" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="game.isEnded" class="_panel _gaps_s" style="padding: 16px;">
+ <div>{{ logPos }} / {{ game.logs.length }}</div>
+ <div v-if="!autoplaying" class="_buttonsCenter">
+ <MkButton :disabled="logPos === 0" @click="logPos = 0"><i class="ti ti-chevrons-left"></i></MkButton>
+ <MkButton :disabled="logPos === 0" @click="logPos--"><i class="ti ti-chevron-left"></i></MkButton>
+ <MkButton :disabled="logPos === game.logs.length" @click="logPos++"><i class="ti ti-chevron-right"></i></MkButton>
+ <MkButton :disabled="logPos === game.logs.length" @click="logPos = game.logs.length"><i class="ti ti-chevrons-right"></i></MkButton>
+ </div>
+ <MkButton style="margin: auto;" :disabled="autoplaying" @click="autoplay()"><i class="ti ti-player-play"></i></MkButton>
+ </div>
+
+ <div class="_panel" style="padding: 16px;">
+ <div>
+ <b>{{ i18n.tsx._reversi.turnCount({ count: logPos }) }}</b> {{ i18n.ts._reversi.black }}:{{ engine.blackCount }} {{ i18n.ts._reversi.white }}:{{ engine.whiteCount }} {{ i18n.ts._reversi.total }}:{{ engine.blackCount + engine.whiteCount }}
+ </div>
+ <div>
+ <div style="display: flex; align-items: center;">
+ <span style="margin-right: 8px;">({{ i18n.ts._reversi.black }})</span>
+ <MkAvatar style="width: 32px; height: 32px; margin-right: 8px;" :user="blackUser" :showIndicator="true"/>
+ <MkA :to="userPage(blackUser)"><MkUserName :user="blackUser"/></MkA>
+ </div>
+ <div> vs </div>
+ <div style="display: flex; align-items: center;">
+ <span style="margin-right: 8px;">({{ i18n.ts._reversi.white }})</span>
+ <MkAvatar style="width: 32px; height: 32px; margin-right: 8px;" :user="whiteUser" :showIndicator="true"/>
+ <MkA :to="userPage(whiteUser)"><MkUserName :user="whiteUser"/></MkA>
+ </div>
+ </div>
+ <div>
+ <p v-if="game.isLlotheo">{{ i18n.ts._reversi.isLlotheo }}</p>
+ <p v-if="game.loopedBoard">{{ i18n.ts._reversi.loopedMap }}</p>
+ <p v-if="game.canPutEverywhere">{{ i18n.ts._reversi.canPutEverywhere }}</p>
+ </div>
+ </div>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts.options }}</template>
+ <div class="_gaps_s" style="text-align: left;">
+ <MkSwitch v-model="showBoardLabels">Show labels</MkSwitch>
+ <MkSwitch v-model="useAvatarAsStone">useAvatarAsStone</MkSwitch>
+ </div>
+ </MkFolder>
+
+ <div class="_buttonsCenter">
+ <MkButton v-if="!game.isEnded && iAmPlayer" danger @click="surrender">{{ i18n.ts._reversi.surrender }}</MkButton>
+ <MkButton @click="share">{{ i18n.ts.share }}</MkButton>
+ </div>
+
+ <MkA v-if="game.isEnded" :to="`/reversi`">
+ <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; width: 200px; margin: auto;"/>
+ </MkA>
+ </div>
+</MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, shallowRef, triggerRef, watch } from 'vue';
+import * as Misskey from 'misskey-js';
+import * as Reversi from 'misskey-reversi';
+import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { deepClone } from '@/scripts/clone.js';
+import { useInterval } from '@/scripts/use-interval.js';
+import { signinRequired } from '@/account.js';
+import { i18n } from '@/i18n.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { userPage } from '@/filters/user.js';
+import * as sound from '@/scripts/sound.js';
+import * as os from '@/os.js';
+import { confetti } from '@/scripts/confetti.js';
+
+const $i = signinRequired();
+
+const props = defineProps<{
+ game: Misskey.entities.ReversiGameDetailed;
+ connection?: Misskey.ChannelConnection<Misskey.Channels['reversiGame']> | null;
+}>();
+
+const showBoardLabels = ref<boolean>(false);
+const useAvatarAsStone = ref<boolean>(true);
+const autoplaying = ref<boolean>(false);
+// eslint-disable-next-line vue/no-setup-props-destructure
+const game = ref<Misskey.entities.ReversiGameDetailed & { logs: Reversi.Serializer.SerializedLog[] }>(deepClone(props.game));
+const logPos = ref<number>(game.value.logs.length);
+const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
+ map: game.value.map,
+ isLlotheo: game.value.isLlotheo,
+ canPutEverywhere: game.value.canPutEverywhere,
+ loopedBoard: game.value.loopedBoard,
+ logs: game.value.logs,
+}));
+
+const iAmPlayer = computed(() => {
+ return game.value.user1Id === $i.id || game.value.user2Id === $i.id;
+});
+
+const myColor = computed(() => {
+ if (!iAmPlayer.value) return null;
+ if (game.value.user1Id === $i.id && game.value.black === 1) return true;
+ if (game.value.user2Id === $i.id && game.value.black === 2) return true;
+ return false;
+});
+
+const opColor = computed(() => {
+ if (!iAmPlayer.value) return null;
+ return !myColor.value;
+});
+
+const blackUser = computed(() => {
+ return game.value.black === 1 ? game.value.user1 : game.value.user2;
+});
+
+const whiteUser = computed(() => {
+ return game.value.black === 1 ? game.value.user2 : game.value.user1;
+});
+
+const turnUser = computed(() => {
+ if (engine.value.turn === true) {
+ return game.value.black === 1 ? game.value.user1 : game.value.user2;
+ } else if (engine.value.turn === false) {
+ return game.value.black === 1 ? game.value.user2 : game.value.user1;
+ } else {
+ return null;
+ }
+});
+
+const isMyTurn = computed(() => {
+ if (!iAmPlayer.value) return false;
+ const u = turnUser.value;
+ if (u == null) return false;
+ return u.id === $i.id;
+});
+
+const cellsStyle = computed(() => {
+ return {
+ 'grid-template-rows': `repeat(${game.value.map.length}, 1fr)`,
+ 'grid-template-columns': `repeat(${game.value.map[0].length}, 1fr)`,
+ };
+});
+
+watch(logPos, (v) => {
+ if (!game.value.isEnded) return;
+ engine.value = Reversi.Serializer.restoreGame({
+ map: game.value.map,
+ isLlotheo: game.value.isLlotheo,
+ canPutEverywhere: game.value.canPutEverywhere,
+ loopedBoard: game.value.loopedBoard,
+ logs: game.value.logs.slice(0, v),
+ });
+});
+
+if (game.value.isStarted && !game.value.isEnded) {
+ useInterval(() => {
+ if (game.value.isEnded) return;
+ const crc32 = engine.value.calcCrc32();
+ if (_DEV_) console.log('crc32', crc32);
+ misskeyApi('reversi/verify', {
+ gameId: game.value.id,
+ crc32: crc32.toString(),
+ }).then((res) => {
+ if (res.desynced) {
+ console.log('resynced');
+ restoreGame(res.game!);
+ }
+ });
+ }, 10000, { immediate: false, afterMounted: true });
+}
+
+const appliedOps: string[] = [];
+
+function putStone(pos: number) {
+ if (game.value.isEnded) return;
+ if (!iAmPlayer.value) return;
+ if (!isMyTurn.value) return;
+ if (!engine.value.canPut(myColor.value!, pos)) return;
+
+ engine.value.putStone(pos);
+
+ triggerRef(engine);
+
+ sound.playUrl('/client-assets/reversi/put.mp3', {
+ volume: 1,
+ playbackRate: 1,
+ });
+
+ const id = Math.random().toString(36).slice(2);
+ props.connection!.send('putStone', {
+ pos: pos,
+ id,
+ });
+ appliedOps.push(id);
+
+ myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
+ opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
+
+ checkEnd();
+}
+
+const myTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
+const opTurnTimerRmain = ref<number>(game.value.timeLimitForEachTurn);
+
+const TIMER_INTERVAL_SEC = 3;
+if (!props.game.isEnded) {
+ useInterval(() => {
+ if (myTurnTimerRmain.value > 0) {
+ myTurnTimerRmain.value = Math.max(0, myTurnTimerRmain.value - TIMER_INTERVAL_SEC);
+ }
+ if (opTurnTimerRmain.value > 0) {
+ opTurnTimerRmain.value = Math.max(0, opTurnTimerRmain.value - TIMER_INTERVAL_SEC);
+ }
+
+ if (iAmPlayer.value) {
+ if ((isMyTurn.value && myTurnTimerRmain.value === 0) || (!isMyTurn.value && opTurnTimerRmain.value === 0)) {
+ props.connection!.send('claimTimeIsUp', {});
+ }
+ }
+ }, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
+}
+
+async function onStreamLog(log) {
+ game.value.logs = Reversi.Serializer.serializeLogs([
+ ...Reversi.Serializer.deserializeLogs(game.value.logs),
+ log,
+ ]);
+
+ logPos.value++;
+
+ if (log.id == null || !appliedOps.includes(log.id)) {
+ switch (log.operation) {
+ case 'put': {
+ sound.playUrl('/client-assets/reversi/put.mp3', {
+ volume: 1,
+ playbackRate: 1,
+ });
+
+ if (log.player !== engine.value.turn) { // = desyncが発生している
+ const _game = await misskeyApi('reversi/show-game', {
+ gameId: props.game.id,
+ });
+ restoreGame(_game);
+ return;
+ }
+
+ engine.value.putStone(log.pos);
+ triggerRef(engine);
+
+ myTurnTimerRmain.value = game.value.timeLimitForEachTurn;
+ opTurnTimerRmain.value = game.value.timeLimitForEachTurn;
+
+ checkEnd();
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+function onStreamEnded(x) {
+ game.value = deepClone(x.game);
+
+ if (game.value.winnerId === $i.id) {
+ confetti({
+ duration: 1000 * 3,
+ });
+
+ sound.playUrl('/client-assets/reversi/win.mp3', {
+ volume: 1,
+ playbackRate: 1,
+ });
+ } else {
+ sound.playUrl('/client-assets/reversi/lose.mp3', {
+ volume: 1,
+ playbackRate: 1,
+ });
+ }
+}
+
+function checkEnd() {
+ game.value.isEnded = engine.value.isEnded;
+ if (game.value.isEnded) {
+ if (engine.value.winner === true) {
+ game.value.winnerId = game.value.black === 1 ? game.value.user1Id : game.value.user2Id;
+ game.value.winner = game.value.black === 1 ? game.value.user1 : game.value.user2;
+ } else if (engine.value.winner === false) {
+ game.value.winnerId = game.value.black === 1 ? game.value.user2Id : game.value.user1Id;
+ game.value.winner = game.value.black === 1 ? game.value.user2 : game.value.user1;
+ } else {
+ game.value.winnerId = null;
+ game.value.winner = null;
+ }
+ }
+}
+
+function restoreGame(_game) {
+ game.value = deepClone(_game);
+
+ engine.value = Reversi.Serializer.restoreGame({
+ map: game.value.map,
+ isLlotheo: game.value.isLlotheo,
+ canPutEverywhere: game.value.canPutEverywhere,
+ loopedBoard: game.value.loopedBoard,
+ logs: game.value.logs,
+ });
+
+ logPos.value = game.value.logs.length;
+
+ checkEnd();
+}
+
+async function surrender() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.areYouSure,
+ });
+ if (canceled) return;
+
+ misskeyApi('reversi/surrender', {
+ gameId: game.value.id,
+ });
+}
+
+function autoplay() {
+ autoplaying.value = true;
+ logPos.value = 0;
+ const logs = Reversi.Serializer.deserializeLogs(game.value.logs);
+
+ window.setTimeout(() => {
+ logPos.value = 1;
+
+ let i = 1;
+ let previousLog = logs[0];
+ const tick = () => {
+ const log = logs[i];
+ const time = log.time - previousLog.time;
+ setTimeout(() => {
+ i++;
+ logPos.value++;
+ previousLog = log;
+
+ if (i < logs.length) {
+ tick();
+ } else {
+ autoplaying.value = false;
+ }
+ }, time);
+ };
+
+ tick();
+ }, 1000);
+}
+
+function share() {
+ os.post({
+ initialText: `#MisskeyReversi ${location.href}`,
+ instant: true,
+ });
+}
+
+onMounted(() => {
+ if (props.connection != null) {
+ props.connection.on('log', onStreamLog);
+ props.connection.on('ended', onStreamEnded);
+ }
+});
+
+onActivated(() => {
+ if (props.connection != null) {
+ props.connection.on('log', onStreamLog);
+ props.connection.on('ended', onStreamEnded);
+ }
+});
+
+onDeactivated(() => {
+ if (props.connection != null) {
+ props.connection.off('log', onStreamLog);
+ props.connection.off('ended', onStreamEnded);
+ }
+});
+
+onUnmounted(() => {
+ if (props.connection != null) {
+ props.connection.off('log', onStreamLog);
+ props.connection.off('ended', onStreamEnded);
+ }
+});
+</script>
+
+<style lang="scss" module>
+@use "sass:math";
+
+.transition_flip_enterActive,
+.transition_flip_leaveActive {
+ backface-visibility: hidden;
+ transition: opacity 0.5s ease, transform 0.5s ease;
+}
+.transition_flip_enterFrom {
+ transform: rotateY(-180deg);
+ opacity: 0;
+}
+.transition_flip_leaveTo {
+ transform: rotateY(180deg);
+ opacity: 0;
+}
+
+$label-size: 16px;
+$gap: 4px;
+
+.root {
+ text-align: center;
+}
+
+.board {
+ width: 100%;
+ box-sizing: border-box;
+ margin: 0 auto;
+
+ padding: 7px;
+ background: #8C4F26;
+ box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
+ border-radius: 12px;
+}
+
+.boardInner {
+ padding: 32px;
+
+ background: var(--panel);
+ box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
+ border-radius: 8px;
+}
+
+@container (max-width: 400px) {
+ .boardInner {
+ padding: 16px;
+ }
+}
+
+.labelsX {
+ height: $label-size;
+ padding: 0 $label-size;
+ display: flex;
+}
+
+.labelsXLabel {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 0.8em;
+
+ &:first-child {
+ margin-left: -(math.div($gap, 2));
+ }
+
+ &:last-child {
+ margin-right: -(math.div($gap, 2));
+ }
+}
+
+.labelsY {
+ width: $label-size;
+ display: flex;
+ flex-direction: column;
+}
+
+.labelsYLabel {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+
+ &:first-child {
+ margin-top: -(math.div($gap, 2));
+ }
+
+ &:last-child {
+ margin-bottom: -(math.div($gap, 2));
+ }
+}
+
+.boardCells {
+ flex: 1;
+ display: grid;
+ grid-gap: $gap;
+}
+
+.boardCell {
+ background: transparent;
+ border-radius: 100%;
+ aspect-ratio: 1;
+ transform-style: preserve-3d;
+ perspective: 150px;
+ transition: border 0.25s ease, opacity 0.25s ease;
+
+ &.boardCell_empty {
+ border: solid 2px var(--divider);
+ }
+
+ &.boardCell_empty.boardCell_can {
+ border-color: var(--accent);
+ opacity: 0.5;
+ }
+
+ &.boardCell_empty.boardCell_myTurn {
+ border-color: var(--divider);
+ opacity: 1;
+
+ &.boardCell_can {
+ border-color: var(--accent);
+ cursor: pointer;
+
+ &:hover {
+ background: var(--accent);
+ }
+ }
+ }
+
+ &.boardCell_prev {
+ box-shadow: 0 0 0 4px var(--accent);
+ }
+
+ &.boardCell_isEnded {
+ border-color: var(--divider);
+ }
+
+ &.boardCell_none {
+ border-color: transparent !important;
+ }
+}
+
+.boardCellStone {
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ user-select: none;
+ display: block;
+ width: 100%;
+ height: 100%;
+ border-radius: 100%;
+}
+</style>
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
new file mode 100644
index 0000000000..31c0003130
--- /dev/null
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -0,0 +1,298 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkStickyContainer>
+ <MkSpacer :contentMax="600">
+ <div style="text-align: center;"><b><MkUserName :user="game.user1"/></b> vs <b><MkUserName :user="game.user2"/></b></div>
+
+ <div :class="{ [$style.disallow]: isReady }">
+ <div class="_gaps" :class="{ [$style.disallowInner]: isReady }">
+ <div style="font-size: 1.5em; text-align: center;">{{ i18n.ts._reversi.gameSettings }}</div>
+
+ <template v-if="game.noIrregularRules">
+ <div>{{ i18n.ts._reversi.disallowIrregularRules }}</div>
+ </template>
+ <template v-else>
+ <div class="_panel">
+ <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
+ <div>{{ mapName }}</div>
+ <MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
+ </div>
+
+ <div style="padding: 16px;">
+ <div v-if="game.map == null"><i class="ti ti-dice"></i></div>
+ <div v-else :class="$style.board" :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }">
+ <div v-for="(x, i) in game.map.join('')" :class="[$style.boardCell, { [$style.boardCellNone]: x == ' ' }]" @click="onMapCellClick(i, x)">
+ <i v-if="x === 'b' || x === 'w'" style="pointer-events: none; user-select: none;" :class="x === 'b' ? 'ti ti-circle-filled' : 'ti ti-circle'"></i>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <MkFolder :defaultOpen="true">
+ <template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
+
+ <MkRadios v-model="game.bw">
+ <option value="random">{{ i18n.ts.random }}</option>
+ <option :value="'1'">
+ <I18n :src="i18n.ts._reversi.blackIs" tag="span">
+ <template #name>
+ <b><MkUserName :user="game.user1"/></b>
+ </template>
+ </I18n>
+ </option>
+ <option :value="'2'">
+ <I18n :src="i18n.ts._reversi.blackIs" tag="span">
+ <template #name>
+ <b><MkUserName :user="game.user2"/></b>
+ </template>
+ </I18n>
+ </option>
+ </MkRadios>
+ </MkFolder>
+
+ <MkFolder :defaultOpen="true">
+ <template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
+ <template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
+
+ <MkRadios v-model="game.timeLimitForEachTurn">
+ <option :value="5">5{{ i18n.ts._time.second }}</option>
+ <option :value="10">10{{ i18n.ts._time.second }}</option>
+ <option :value="30">30{{ i18n.ts._time.second }}</option>
+ <option :value="60">60{{ i18n.ts._time.second }}</option>
+ <option :value="90">90{{ i18n.ts._time.second }}</option>
+ <option :value="120">120{{ i18n.ts._time.second }}</option>
+ <option :value="180">180{{ i18n.ts._time.second }}</option>
+ <option :value="3600">3600{{ i18n.ts._time.second }}</option>
+ </MkRadios>
+ </MkFolder>
+
+ <MkFolder :defaultOpen="true">
+ <template #label>{{ i18n.ts._reversi.rules }}</template>
+
+ <div class="_gaps_s">
+ <MkSwitch v-model="game.isLlotheo" @update:modelValue="updateSettings('isLlotheo')">{{ i18n.ts._reversi.isLlotheo }}</MkSwitch>
+ <MkSwitch v-model="game.loopedBoard" @update:modelValue="updateSettings('loopedBoard')">{{ i18n.ts._reversi.loopedMap }}</MkSwitch>
+ <MkSwitch v-model="game.canPutEverywhere" @update:modelValue="updateSettings('canPutEverywhere')">{{ i18n.ts._reversi.canPutEverywhere }}</MkSwitch>
+ </div>
+ </MkFolder>
+ </template>
+ </div>
+ </div>
+ </MkSpacer>
+ <template #footer>
+ <div :class="$style.footer">
+ <MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
+ <div style="text-align: center;" class="_gaps_s">
+ <div v-if="opponentHasSettingsChanged" style="color: var(--warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
+ <div>
+ <template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
+ <template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
+ <template v-if="!isReady && isOpReady">{{ i18n.ts._reversi.waitingForMe }}</template>
+ <template v-if="!isReady && !isOpReady">{{ i18n.ts._reversi.waitingBoth }}<MkEllipsis/></template>
+ </div>
+ <div class="_buttonsCenter">
+ <MkButton rounded danger @click="cancel">{{ i18n.ts.cancel }}</MkButton>
+ <MkButton v-if="!isReady" rounded primary @click="ready">{{ i18n.ts._reversi.ready }}</MkButton>
+ <MkButton v-if="isReady" rounded @click="unready">{{ i18n.ts._reversi.cancelReady }}</MkButton>
+ </div>
+ <div>
+ <MkSwitch v-model="shareWhenStart">{{ i18n.ts._reversi.shareToTlTheGameWhenStart }}</MkSwitch>
+ </div>
+ </div>
+ </MkSpacer>
+ </div>
+ </template>
+</MkStickyContainer>
+</template>
+
+<script lang="ts" setup>
+import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
+import * as Misskey from 'misskey-js';
+import * as Reversi from 'misskey-reversi';
+import { i18n } from '@/i18n.js';
+import { signinRequired } from '@/account.js';
+import { deepClone } from '@/scripts/clone.js';
+import MkButton from '@/components/MkButton.vue';
+import MkRadios from '@/components/MkRadios.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os.js';
+import { MenuItem } from '@/types/menu.js';
+import { useRouter } from '@/router/supplier.js';
+
+const $i = signinRequired();
+
+const router = useRouter();
+
+const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category)));
+
+const props = defineProps<{
+ game: Misskey.entities.ReversiGameDetailed;
+ connection: Misskey.ChannelConnection;
+}>();
+
+const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
+
+const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
+
+const mapName = computed(() => {
+ if (game.value.map == null) return 'Random';
+ const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join(''));
+ return found ? found.name! : '-Custom-';
+});
+const isReady = computed(() => {
+ if (game.value.user1Id === $i.id && game.value.user1Ready) return true;
+ if (game.value.user2Id === $i.id && game.value.user2Ready) return true;
+ return false;
+});
+const isOpReady = computed(() => {
+ if (game.value.user1Id !== $i.id && game.value.user1Ready) return true;
+ if (game.value.user2Id !== $i.id && game.value.user2Ready) return true;
+ return false;
+});
+
+const opponentHasSettingsChanged = ref(false);
+
+watch(() => game.value.bw, () => {
+ updateSettings('bw');
+});
+
+watch(() => game.value.timeLimitForEachTurn, () => {
+ updateSettings('timeLimitForEachTurn');
+});
+
+function chooseMap(ev: MouseEvent) {
+ const menu: MenuItem[] = [];
+
+ for (const c of mapCategories) {
+ const maps = Object.values(Reversi.maps).filter(x => x.category === c);
+ if (maps.length === 0) continue;
+ if (c != null) {
+ menu.push({
+ type: 'label',
+ text: c,
+ });
+ }
+ for (const m of maps) {
+ menu.push({
+ text: m.name!,
+ action: () => {
+ game.value.map = m.data;
+ updateSettings('map');
+ },
+ });
+ }
+ }
+
+ os.popupMenu(menu, ev.currentTarget ?? ev.target);
+}
+
+async function cancel() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.ts.areYouSure,
+ });
+ if (canceled) return;
+
+ props.connection.send('cancel', {});
+
+ router.push('/reversi');
+}
+
+function ready() {
+ props.connection.send('ready', true);
+ opponentHasSettingsChanged.value = false;
+}
+
+function unready() {
+ props.connection.send('ready', false);
+}
+
+function onChangeReadyStates(states) {
+ game.value.user1Ready = states.user1;
+ game.value.user2Ready = states.user2;
+}
+
+function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
+ props.connection.send('updateSettings', {
+ key: key,
+ value: game.value[key],
+ });
+}
+
+function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) {
+ if (userId === $i.id) return;
+ if (game.value[key] === value) return;
+ game.value[key] = value;
+ if (isReady.value) {
+ opponentHasSettingsChanged.value = true;
+ unready();
+ }
+}
+
+function onMapCellClick(pos: number, pixel: string) {
+ const x = pos % game.value.map[0].length;
+ const y = Math.floor(pos / game.value.map[0].length);
+ const newPixel =
+ pixel === ' ' ? '-' :
+ pixel === '-' ? 'b' :
+ pixel === 'b' ? 'w' :
+ ' ';
+ const line = game.value.map[y].split('');
+ line[x] = newPixel;
+ game.value.map[y] = line.join('');
+ updateSettings('map');
+}
+
+props.connection.on('changeReadyStates', onChangeReadyStates);
+props.connection.on('updateSettings', onUpdateSettings);
+
+onUnmounted(() => {
+ props.connection.off('changeReadyStates', onChangeReadyStates);
+ props.connection.off('updateSettings', onUpdateSettings);
+});
+</script>
+
+<style lang="scss" module>
+.disallow {
+ cursor: not-allowed;
+}
+.disallowInner {
+ pointer-events: none;
+ user-select: none;
+ opacity: 0.7;
+}
+
+.board {
+ display: grid;
+ grid-gap: 4px;
+ width: 300px;
+ height: 300px;
+ margin: 0 auto;
+ color: var(--fg);
+}
+
+.boardCell {
+ display: grid;
+ place-items: center;
+ background: transparent;
+ border: solid 2px var(--divider);
+ border-radius: 6px;
+ overflow: clip;
+ cursor: pointer;
+}
+.boardCellNone {
+ border-color: transparent;
+}
+
+.footer {
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+ background: var(--acrylicBg);
+ border-top: solid 0.5px var(--divider);
+}
+</style>
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
new file mode 100644
index 0000000000..eadc51881c
--- /dev/null
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -0,0 +1,120 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div v-if="game == null || (!game.isEnded && connection == null)"><MkLoading/></div>
+<GameSetting v-else-if="!game.isStarted" v-model:shareWhenStart="shareWhenStart" :game="game" :connection="connection!"/>
+<GameBoard v-else :game="game" :connection="connection"/>
+</template>
+
+<script lang="ts" setup>
+import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
+import * as Misskey from 'misskey-js';
+import GameSetting from './game.setting.vue';
+import GameBoard from './game.board.vue';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useStream } from '@/stream.js';
+import { signinRequired } from '@/account.js';
+import { useRouter } from '@/router/supplier.js';
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { useInterval } from '@/scripts/use-interval.js';
+
+const $i = signinRequired();
+
+const router = useRouter();
+
+const props = defineProps<{
+ gameId: string;
+}>();
+
+const game = shallowRef<Misskey.entities.ReversiGameDetailed | null>(null);
+const connection = shallowRef<Misskey.ChannelConnection | null>(null);
+const shareWhenStart = ref(false);
+
+watch(() => props.gameId, () => {
+ fetchGame();
+});
+
+function start(_game: Misskey.entities.ReversiGameDetailed) {
+ if (game.value?.isStarted) return;
+
+ if (shareWhenStart.value) {
+ misskeyApi('notes/create', {
+ text: i18n.ts._reversi.iStartedAGame + '\n' + location.href,
+ visibility: 'home',
+ });
+ }
+
+ game.value = _game;
+}
+
+async function fetchGame() {
+ const _game = await misskeyApi('reversi/show-game', {
+ gameId: props.gameId,
+ });
+
+ game.value = _game;
+ shareWhenStart.value = false;
+
+ if (connection.value) {
+ connection.value.dispose();
+ }
+ if (!game.value.isEnded) {
+ connection.value = useStream().useChannel('reversiGame', {
+ gameId: game.value.id,
+ });
+ connection.value.on('started', x => {
+ start(x.game);
+ });
+ connection.value.on('canceled', x => {
+ connection.value?.dispose();
+
+ if (x.userId !== $i.id) {
+ os.alert({
+ type: 'warning',
+ text: i18n.ts._reversi.gameCanceled,
+ });
+ router.push('/reversi');
+ }
+ });
+ }
+}
+
+// 通信を取りこぼした場合の救済
+useInterval(async () => {
+ if (game.value == null) return;
+ if (game.value.isStarted) return;
+
+ const _game = await misskeyApi('reversi/show-game', {
+ gameId: props.gameId,
+ });
+
+ if (_game.isStarted) {
+ start(_game);
+ } else {
+ game.value = _game;
+ }
+}, 1000 * 10, {
+ immediate: false,
+ afterMounted: true,
+});
+
+onMounted(() => {
+ fetchGame();
+});
+
+onUnmounted(() => {
+ if (connection.value) {
+ connection.value.dispose();
+ }
+});
+
+definePageMetadata(() => ({
+ title: 'Reversi',
+ icon: 'ti ti-device-gamepad',
+}));
+</script>
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
new file mode 100644
index 0000000000..51a03e4418
--- /dev/null
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -0,0 +1,353 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkSpacer v-if="!matchingAny && !matchingUser" :contentMax="600">
+ <div class="_gaps">
+ <div>
+ <img src="/client-assets/reversi/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
+ </div>
+
+ <div class="_panel _gaps" style="padding: 16px;">
+ <div class="_buttonsCenter">
+ <MkButton primary gradate rounded @click="matchAny">{{ i18n.ts._reversi.freeMatch }}</MkButton>
+ <MkButton primary gradate rounded @click="matchUser">{{ i18n.ts.invite }}</MkButton>
+ </div>
+ <div style="font-size: 90%; opacity: 0.7; text-align: center;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
+ </div>
+
+ <MkFolder v-if="invitations.length > 0" :defaultOpen="true">
+ <template #label>{{ i18n.ts.invitations }}</template>
+ <div class="_gaps_s">
+ <button v-for="user in invitations" :key="user.id" v-panel :class="$style.invitation" class="_button" tabindex="-1" @click="accept(user)">
+ <MkAvatar style="width: 32px; height: 32px; margin-right: 8px;" :user="user" :showIndicator="true"/>
+ <span style="margin-right: 8px;"><b><MkUserName :user="user"/></b></span>
+ <span>@{{ user.username }}</span>
+ </button>
+ </div>
+ </MkFolder>
+
+ <MkFolder v-if="$i" :defaultOpen="true">
+ <template #label>{{ i18n.ts._reversi.myGames }}</template>
+ <MkPagination :pagination="myGamesPagination" :disableAutoLoad="true">
+ <template #default="{ items }">
+ <div :class="$style.gamePreviews">
+ <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
+ <div :class="$style.gamePreviewPlayers">
+ <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+ <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
+ <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
+ <span style="margin: 0 1em;">vs</span>
+ <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
+ <span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
+ <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+ </div>
+ <div :class="$style.gamePreviewFooter">
+ <span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
+ <span v-else-if="!g.isEnded" :class="$style.gamePreviewStatusWaiting"><MkEllipsis/></span>
+ <span v-else>{{ i18n.ts._reversi.ended }}</span>
+ <MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/>
+ </div>
+ </MkA>
+ </div>
+ </template>
+ </MkPagination>
+ </MkFolder>
+
+ <MkFolder :defaultOpen="true">
+ <template #label>{{ i18n.ts._reversi.allGames }}</template>
+ <MkPagination :pagination="gamesPagination" :disableAutoLoad="true">
+ <template #default="{ items }">
+ <div :class="$style.gamePreviews">
+ <MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
+ <div :class="$style.gamePreviewPlayers">
+ <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+ <span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
+ <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
+ <span style="margin: 0 1em;">vs</span>
+ <MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
+ <span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
+ <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+ </div>
+ <div :class="$style.gamePreviewFooter">
+ <span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
+ <span v-else-if="!g.isEnded" :class="$style.gamePreviewStatusWaiting"><MkEllipsis/></span>
+ <span v-else>{{ i18n.ts._reversi.ended }}</span>
+ <MkTime style="margin-left: auto; opacity: 0.7;" :time="g.createdAt"/>
+ </div>
+ </MkA>
+ </div>
+ </template>
+ </MkPagination>
+ </MkFolder>
+ </div>
+</MkSpacer>
+<MkSpacer v-else :contentMax="600">
+ <div :class="$style.waitingScreen">
+ <div v-if="matchingUser" :class="$style.waitingScreenTitle">
+ <I18n :src="i18n.ts.waitingFor" tag="span">
+ <template #x>
+ <b><MkUserName :user="matchingUser"/></b>
+ </template>
+ </I18n>
+ <MkEllipsis/>
+ </div>
+ <div v-else :class="$style.waitingScreenTitle">
+ {{ i18n.ts._reversi.lookingForPlayer }}<MkEllipsis/>
+ </div>
+ <div class="cancel">
+ <MkButton inline rounded @click="cancelMatching">{{ i18n.ts.cancel }}</MkButton>
+ </div>
+ </div>
+</MkSpacer>
+</template>
+
+<script lang="ts" setup>
+import { onDeactivated, onMounted, onUnmounted, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useStream } from '@/stream.js';
+import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import { i18n } from '@/i18n.js';
+import { $i } from '@/account.js';
+import MkPagination from '@/components/MkPagination.vue';
+import { useRouter } from '@/router/supplier.js';
+import * as os from '@/os.js';
+import { useInterval } from '@/scripts/use-interval.js';
+import { pleaseLogin } from '@/scripts/please-login.js';
+import * as sound from '@/scripts/sound.js';
+
+const myGamesPagination = {
+ endpoint: 'reversi/games' as const,
+ limit: 10,
+ params: {
+ my: true,
+ },
+};
+
+const gamesPagination = {
+ endpoint: 'reversi/games' as const,
+ limit: 10,
+};
+
+const router = useRouter();
+
+if ($i) {
+ const connection = useStream().useChannel('reversi');
+
+ connection.on('matched', x => {
+ if (matchingUser.value != null || matchingAny.value) {
+ startGame(x.game);
+ }
+ });
+
+ connection.on('invited', invitation => {
+ if (invitations.value.some(x => x.id === invitation.user.id)) return;
+ invitations.value.unshift(invitation.user);
+ });
+
+ onUnmounted(() => {
+ connection.dispose();
+ });
+}
+
+const invitations = ref<Misskey.entities.UserLite[]>([]);
+const matchingUser = ref<Misskey.entities.UserLite | null>(null);
+const matchingAny = ref<boolean>(false);
+const noIrregularRules = ref<boolean>(false);
+
+function startGame(game: Misskey.entities.ReversiGameDetailed) {
+ matchingUser.value = null;
+ matchingAny.value = false;
+
+ sound.playUrl('/client-assets/reversi/matched.mp3', {
+ volume: 1,
+ playbackRate: 1,
+ });
+
+ router.push(`/reversi/g/${game.id}`);
+}
+
+async function matchHeatbeat() {
+ if (matchingUser.value) {
+ const res = await misskeyApi('reversi/match', {
+ userId: matchingUser.value.id,
+ });
+
+ if (res != null) {
+ startGame(res);
+ }
+ } else if (matchingAny.value) {
+ const res = await misskeyApi('reversi/match', {
+ userId: null,
+ noIrregularRules: noIrregularRules.value,
+ });
+
+ if (res != null) {
+ startGame(res);
+ }
+ }
+}
+
+async function matchUser() {
+ pleaseLogin();
+
+ const user = await os.selectUser({ includeSelf: false, localOnly: true });
+ if (user == null) return;
+
+ matchingUser.value = user;
+
+ matchHeatbeat();
+}
+
+function matchAny(ev: MouseEvent) {
+ pleaseLogin();
+
+ os.popupMenu([{
+ text: i18n.ts._reversi.allowIrregularRules,
+ action: () => {
+ noIrregularRules.value = false;
+ matchingAny.value = true;
+ matchHeatbeat();
+ },
+ }, {
+ text: i18n.ts._reversi.disallowIrregularRules,
+ action: () => {
+ noIrregularRules.value = true;
+ matchingAny.value = true;
+ matchHeatbeat();
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+
+function cancelMatching() {
+ if (matchingUser.value) {
+ misskeyApi('reversi/cancel-match', { userId: matchingUser.value.id });
+ matchingUser.value = null;
+ } else if (matchingAny.value) {
+ misskeyApi('reversi/cancel-match', { userId: null });
+ matchingAny.value = false;
+ }
+}
+
+async function accept(user) {
+ const game = await misskeyApi('reversi/match', {
+ userId: user.id,
+ });
+ if (game) {
+ startGame(game);
+ }
+}
+
+useInterval(matchHeatbeat, 1000 * 5, { immediate: false, afterMounted: true });
+
+onMounted(() => {
+ misskeyApi('reversi/invitations').then(_invitations => {
+ invitations.value = _invitations;
+ });
+
+ window.addEventListener('beforeunload', cancelMatching);
+});
+
+onDeactivated(() => {
+ cancelMatching();
+});
+
+onUnmounted(() => {
+ cancelMatching();
+});
+
+definePageMetadata(() => ({
+ title: 'Reversi',
+ icon: 'ti ti-device-gamepad',
+}));
+</script>
+
+<style lang="scss" module>
+@keyframes blink {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.2; }
+}
+
+.invitation {
+ display: flex;
+ box-sizing: border-box;
+ width: 100%;
+ padding: 16px;
+ line-height: 32px;
+ text-align: left;
+}
+
+.gamePreviews {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+ grid-gap: var(--margin);
+}
+
+.gamePreview {
+ font-size: 90%;
+ border-radius: 8px;
+ overflow: clip;
+}
+
+.gamePreviewActive {
+ box-shadow: inset 0 0 8px 0px var(--accent);
+}
+
+.gamePreviewWaiting {
+ box-shadow: inset 0 0 8px 0px var(--warn);
+}
+
+.gamePreviewPlayers {
+ text-align: center;
+ padding: 16px;
+ line-height: 32px;
+}
+
+.gamePreviewPlayersAvatar {
+ width: 32px;
+ height: 32px;
+
+ &:first-child {
+ margin-right: 8px;
+ }
+
+ &:last-child {
+ margin-left: 8px;
+ }
+}
+
+.gamePreviewFooter {
+ display: flex;
+ align-items: baseline;
+ border-top: solid 0.5px var(--divider);
+ padding: 6px 10px;
+ font-size: 0.9em;
+}
+
+.gamePreviewStatusActive {
+ color: var(--accent);
+ font-weight: bold;
+ animation: blink 2s infinite;
+}
+
+.gamePreviewStatusWaiting {
+ color: var(--warn);
+ font-weight: bold;
+ animation: blink 2s infinite;
+}
+
+.waitingScreen {
+ text-align: center;
+}
+
+.waitingScreenTitle {
+ font-size: 1.5em;
+ margin-bottom: 16px;
+ margin-top: 32px;
+}
+</style>
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index 10642ddefe..ce80579cf9 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkUserList from '@/components/MkUserList.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
@@ -59,7 +59,7 @@ const error = ref();
const visible = ref(false);
watch(() => props.role, () => {
- os.api('roles/show', {
+ misskeyApi('roles/show', {
roleId: props.role,
}).then(res => {
role.value = res;
@@ -93,10 +93,10 @@ const headerTabs = computed(() => [{
title: i18n.ts.timeline,
}]);
-definePageMetadata(computed(() => ({
- title: role.value?.name,
+definePageMetadata(() => ({
+ title: role.value ? role.value.name : i18n.ts.role,
icon: 'ti ti-badge',
-})));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 1453bc1658..9aaa8ff9c6 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -44,7 +44,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import MkContainer from '@/components/MkContainer.vue';
import MkButton from '@/components/MkButton.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
-import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
@@ -86,19 +86,7 @@ async function run() {
root.value = _root.value;
}),
}), {
- in: (q) => {
- return new Promise(ok => {
- os.inputText({
- title: q,
- }).then(({ canceled, result: a }) => {
- if (canceled) {
- ok('');
- } else {
- ok(a);
- }
- });
- });
- },
+ in: aiScriptReadline,
out: (value) => {
if (value.type === 'str' && value.value.toLowerCase().replace(',', '').includes('hello world')) {
claimAchievement('outputHelloWorldOnScratchpad');
@@ -164,10 +152,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.scratchpad,
icon: 'ti ti-terminal-2',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 5c0b54e2d9..d68bbaeeca 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkSwitch v-model="isLocalOnly">{{ i18n.ts.localOnly }}</MkSwitch>
- <MkFolder>
+ <MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.specifyUser }}</template>
<template v-if="user" #suffix>@{{ user.username }}</template>
@@ -49,9 +49,10 @@ import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { useRouter } from '@/router.js';
import MkFolder from '@/components/MkFolder.vue';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -63,7 +64,7 @@ const user = ref<any>(null);
const isLocalOnly = ref(false);
function selectUser() {
- os.selectUser().then(_user => {
+ os.selectUser({ includeSelf: true }).then(_user => {
user.value = _user;
});
}
@@ -74,7 +75,7 @@ async function search() {
if (query == null || query === '') return;
if (query.startsWith('https://')) {
- const promise = os.api('ap/show', {
+ const promise = misskeyApi('ap/show', {
uri: query,
});
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index 829c706e0e..b9c2704bc7 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -33,7 +33,8 @@ import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { useRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -48,7 +49,7 @@ async function search() {
if (query == null || query === '') return;
if (query.startsWith('https://')) {
- const promise = os.api('ap/show', {
+ const promise = misskeyApi('ap/show', {
uri: query,
});
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index 9d5e5697ce..a3dcda77be 100644
--- a/packages/frontend/src/pages/search.vue
+++ b/packages/frontend/src/pages/search.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,18 +7,20 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
- <MkSpacer v-if="tab === 'note'" :contentMax="800">
- <div v-if="notesSearchAvailable">
- <XNote/>
- </div>
- <div v-else>
- <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
- </div>
- </MkSpacer>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <MkSpacer v-if="tab === 'note'" key="note" :contentMax="800">
+ <div v-if="notesSearchAvailable">
+ <XNote/>
+ </div>
+ <div v-else>
+ <MkInfo warn>{{ i18n.ts.notesSearchNotAvailable }}</MkInfo>
+ </div>
+ </MkSpacer>
- <MkSpacer v-else-if="tab === 'user'" :contentMax="800">
- <XUser/>
- </MkSpacer>
+ <MkSpacer v-else-if="tab === 'user'" key="user" :contentMax="800">
+ <XUser/>
+ </MkSpacer>
+ </MkHorizontalSwipe>
</MkStickyContainer>
</template>
@@ -29,6 +31,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
import MkInfo from '@/components/MkInfo.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XNote = defineAsyncComponent(() => import('./search.note.vue'));
const XUser = defineAsyncComponent(() => import('./search.user.vue'));
@@ -49,8 +52,8 @@ const headerTabs = computed(() => [{
icon: 'ti ti-users',
}]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.search,
icon: 'ti ti-search',
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 4641b49103..2608560cc4 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -110,7 +110,9 @@ import * as os from '@/os.js';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import { confetti } from '@/scripts/confetti.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
defineProps<{
twoFactorData: {
@@ -151,7 +153,7 @@ function downloadBackupCodes() {
const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' });
const dummya = document.createElement('a');
dummya.href = URL.createObjectURL(txtBlob);
- dummya.download = `${$i?.username}-2fa-backup-codes.txt`;
+ dummya.download = `${$i.username}-2fa-backup-codes.txt`;
dummya.click();
}
}
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 4c165ef4ee..d8c5f848fe 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -80,9 +80,11 @@ import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
import { i18n } from '@/i18n.js';
+const $i = signinRequired();
+
// メモ: 各エンドポイントはmeUpdatedを発行するため、refreshAccountは不要
withDefaults(defineProps<{
@@ -91,7 +93,7 @@ withDefaults(defineProps<{
first: false,
});
-const usePasswordLessLogin = computed(() => $i?.usePasswordLessLogin ?? false);
+const usePasswordLessLogin = computed(() => $i.usePasswordLessLogin ?? false);
async function registerTOTP(): Promise<void> {
const auth = await os.authenticateDialog();
@@ -139,7 +141,7 @@ async function unregisterKey(key) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts._2fa.removeKey,
- text: i18n.t('_2fa.removeKeyConfirm', { name: key.name }),
+ text: i18n.tsx._2fa.removeKeyConfirm({ name: key.name }),
});
if (confirm.canceled) return;
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 4320ad7e9e..1182346de9 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -24,6 +24,7 @@ import type * as Misskey from 'misskey-js';
import FormSuspense from '@/components/form/suspense.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { getAccounts, addAccount as addAccounts, removeAccount as _removeAccount, login, $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -36,7 +37,7 @@ const init = async () => {
getAccounts().then(accounts => {
storedAccounts.value = accounts.filter(x => x.id !== $i!.id);
- return os.api('users/show', {
+ return misskeyApi('users/show', {
userIds: storedAccounts.value.map(x => x.id),
});
}).then(response => {
@@ -105,10 +106,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.accounts,
icon: 'ti ti-users',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue
index eee7884aaa..d9596b4e45 100644
--- a/packages/frontend/src/pages/settings/api.vue
+++ b/packages/frontend/src/pages/settings/api.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,6 +16,7 @@ import { defineAsyncComponent, ref, computed } from 'vue';
import FormLink from '@/components/form/link.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -25,7 +26,7 @@ function generateToken() {
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, {
done: async result => {
const { name, permissions } = result;
- const { token } = await os.api('miauth/gen-token', {
+ const { token } = await misskeyApi('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
@@ -44,8 +45,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: 'API',
icon: 'ti ti-api',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 419bcd6fee..0e0c1f4c0c 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<details>
<summary>{{ i18n.ts.details }}</summary>
<ul>
- <li v-for="p in token.permission" :key="p">{{ i18n.t(`_permissions.${p}`) }}</li>
+ <li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
</ul>
</details>
<div>
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
import FormPagination from '@/components/MkPagination.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
@@ -66,7 +66,7 @@ const pagination = {
};
function revoke(token) {
- os.api('i/revoke-token', { tokenId: token.id }).then(() => {
+ misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
list.value.reload();
});
}
@@ -75,10 +75,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.installedApps,
icon: 'ti ti-plug',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index 9c95b5547e..f37b53aebb 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,7 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { } from 'vue';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
const props = defineProps<{
active?: boolean;
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 329ab4d47a..ce1d4e48d8 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -51,7 +51,9 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { i18n } from '@/i18n.js';
import MkRange from '@/components/MkRange.vue';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
const props = defineProps<{
usingIndex: number | null;
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index 6551fc917e..3cc911c014 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -1,12 +1,12 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div>
<div v-if="!loading" class="_gaps">
- <MkInfo>{{ i18n.t('_profile.avatarDecorationMax', { max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.t('remainingN', { n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
+ <MkInfo>{{ i18n.tsx._profile.avatarDecorationMax({ max: $i.policies.avatarDecorationLimit }) }} ({{ i18n.tsx.remainingN({ n: $i.policies.avatarDecorationLimit - $i.avatarDecorations.length }) }})</MkInfo>
<MkAvatar :class="$style.avatar" :user="$i" forceShowDecoration/>
@@ -50,15 +50,18 @@ import * as Misskey from 'misskey-js';
import XDecoration from './avatar-decoration.decoration.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
import MkInfo from '@/components/MkInfo.vue';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+const $i = signinRequired();
+
const loading = ref(true);
const avatarDecorations = ref<Misskey.entities.GetAvatarDecorationsResponse>([]);
-os.api('get-avatar-decorations').then(_avatarDecorations => {
+misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
avatarDecorations.value = _avatarDecorations;
loading.value = false;
});
@@ -125,10 +128,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.avatarDecorations,
icon: 'ti ti-sparkles',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue
index 2564562089..cf05e75acc 100644
--- a/packages/frontend/src/pages/settings/custom-css.vue
+++ b/packages/frontend/src/pages/settings/custom-css.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -45,8 +45,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.customCss,
icon: 'ti ti-code',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index b681e0d159..e574ec7dc0 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -36,8 +36,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.deck,
icon: 'ti ti-columns',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index 4efcdb31da..b20774c4ec 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -51,6 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, ref, watch } from 'vue';
import tinycolor from 'tinycolor2';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkPagination from '@/components/MkPagination.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import { i18n } from '@/i18n.js';
@@ -94,7 +95,7 @@ watch(sortModeSelect, () => {
function fetchDriveInfo(): void {
fetching.value = true;
- os.api('drive').then(info => {
+ misskeyApi('drive').then(info => {
capacity.value = info.capacity;
usage.value = info.usage;
fetching.value = false;
@@ -116,10 +117,10 @@ function onContextMenu(ev: MouseEvent, file): void {
os.contextMenu(getDriveFileMenu(file), ev);
}
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.drivecleaner,
icon: 'ti ti-trash',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 7857cf7125..cd38f9850f 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -66,12 +66,15 @@ import FormSection from '@/components/form/section.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import bytes from '@/filters/bytes.js';
import { defaultStore } from '@/store.js';
import MkChart from '@/components/MkChart.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
const fetching = ref(true);
const usage = ref<number | null>(null);
@@ -81,6 +84,7 @@ const alwaysMarkNsfw = ref($i.alwaysMarkNsfw);
const autoSensitive = ref($i.autoSensitive);
const meterStyle = computed(() => {
+ if (!capacity.value || !usage.value) return {};
return {
width: `${usage.value / capacity.value * 100}%`,
background: tinycolor({
@@ -93,14 +97,14 @@ const meterStyle = computed(() => {
const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading'));
-os.api('drive').then(info => {
+misskeyApi('drive').then(info => {
capacity.value = info.capacity;
usage.value = info.usage;
fetching.value = false;
});
if (defaultStore.state.uploadFolder) {
- os.api('drive/folders/show', {
+ misskeyApi('drive/folders/show', {
folderId: defaultStore.state.uploadFolder,
}).then(response => {
uploadFolder.value = response;
@@ -112,7 +116,7 @@ function chooseUploadFolder() {
defaultStore.set('uploadFolder', folder ? folder.id : null);
os.success();
if (defaultStore.state.uploadFolder) {
- uploadFolder.value = await os.api('drive/folders/show', {
+ uploadFolder.value = await misskeyApi('drive/folders/show', {
folderId: defaultStore.state.uploadFolder,
});
} else {
@@ -122,7 +126,7 @@ function chooseUploadFolder() {
}
function saveProfile() {
- os.api('i/update', {
+ misskeyApi('i/update', {
alwaysMarkNsfw: !!alwaysMarkNsfw.value,
autoSensitive: !!autoSensitive.value,
}).catch(err => {
@@ -139,10 +143,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.drive,
icon: 'ti ti-cloud',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index 309e025ada..f226647569 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -54,15 +54,18 @@ import MkInfo from '@/components/MkInfo.vue';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { signinRequired } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { instance } from '@/instance.js';
-const emailAddress = ref($i!.email);
+const $i = signinRequired();
+
+const emailAddress = ref($i.email);
const onChangeReceiveAnnouncementEmail = (v) => {
- os.api('i/update', {
+ misskeyApi('i/update', {
receiveAnnouncementEmail: v,
});
};
@@ -78,14 +81,14 @@ async function saveEmailAddress() {
});
}
-const emailNotification_mention = ref($i!.emailNotificationTypes.includes('mention'));
-const emailNotification_reply = ref($i!.emailNotificationTypes.includes('reply'));
-const emailNotification_quote = ref($i!.emailNotificationTypes.includes('quote'));
-const emailNotification_follow = ref($i!.emailNotificationTypes.includes('follow'));
-const emailNotification_receiveFollowRequest = ref($i!.emailNotificationTypes.includes('receiveFollowRequest'));
+const emailNotification_mention = ref($i.emailNotificationTypes.includes('mention'));
+const emailNotification_reply = ref($i.emailNotificationTypes.includes('reply'));
+const emailNotification_quote = ref($i.emailNotificationTypes.includes('quote'));
+const emailNotification_follow = ref($i.emailNotificationTypes.includes('follow'));
+const emailNotification_receiveFollowRequest = ref($i.emailNotificationTypes.includes('receiveFollowRequest'));
const saveNotificationSettings = () => {
- os.api('i/update', {
+ misskeyApi('i/update', {
emailNotificationTypes: [
...[emailNotification_mention.value ? 'mention' : null],
...[emailNotification_reply.value ? 'reply' : null],
@@ -110,8 +113,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.email,
icon: 'ti ti-mail',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index 61f3332122..79969427ec 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -157,7 +157,7 @@ const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
const setDefaultEmoji = () => setDefault(pinnedEmojis);
function previewReaction(ev: MouseEvent) {
- reactionPicker.show(getHTMLElement(ev));
+ reactionPicker.show(getHTMLElement(ev), null);
}
function previewEmoji(ev: MouseEvent) {
@@ -237,10 +237,10 @@ watch(pinnedEmojis, () => {
deep: true,
});
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.emojiPicker,
icon: 'ti ti-mood-happy',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 3e5f5cb8c8..d13b6884bd 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -17,6 +17,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkSelect>
+ <MkRadios v-model="hemisphere">
+ <template #label>{{ i18n.ts.hemisphere }}</template>
+ <option value="N">{{ i18n.ts._hemisphere.N }}</option>
+ <option value="S">{{ i18n.ts._hemisphere.S }}</option>
+ <template #caption>{{ i18n.ts._hemisphere.caption }}</template>
+ </MkRadios>
+
<MkRadios v-model="overridedDeviceKind">
<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
<option :value="null">{{ i18n.ts.auto }}</option>
@@ -77,9 +84,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkRadios v-model="mediaListWithOneImageAppearance">
<template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</template>
<option value="expand">{{ i18n.ts.default }}</option>
- <option value="16_9">{{ i18n.t('limitTo', { x: '16:9' }) }}</option>
- <option value="1_1">{{ i18n.t('limitTo', { x: '1:1' }) }}</option>
- <option value="2_3">{{ i18n.t('limitTo', { x: '2:3' }) }}</option>
+ <option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
+ <option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
+ <option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
</MkRadios>
</div>
</FormSection>
@@ -155,6 +162,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
+ <MkSwitch v-model="enableHorizontalSwipe">{{ i18n.ts.enableHorizontalSwipe }}</MkSwitch>
</div>
<MkSelect v-model="serverDisconnectedBehavior">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@@ -206,9 +214,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkFolder>
<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
- <div v-for="lang in emojiIndexLangs" class="_buttons">
- <MkButton @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ lang }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
- <MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
+ <div class="_buttons">
+ <template v-for="lang in emojiIndexLangs" :key="lang">
+ <MkButton v-if="defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
+ <MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
+ </template>
</div>
</MkFolder>
<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
@@ -234,6 +244,7 @@ import MkInfo from '@/components/MkInfo.vue';
import { langs } from '@/config.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -256,6 +267,7 @@ async function reloadAsk() {
unisonReload();
}
+const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere'));
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
@@ -293,6 +305,7 @@ const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
+const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
@@ -317,6 +330,7 @@ watch(useSystemFont, () => {
});
watch([
+ hemisphere,
lang,
fontSize,
useSystemFont,
@@ -337,15 +351,29 @@ watch([
await reloadAsk();
});
-const emojiIndexLangs = ['en-US'];
+const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
+
+function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) {
+ if (langs.find(x => x[0] === targetLang)) {
+ return langs.find(x => x[0] === targetLang)![1];
+ } else {
+ // 絵文字辞書限定の言語定義
+ switch (targetLang) {
+ case 'ja-JP_hira': return 'ひらがな';
+ default: return targetLang;
+ }
+ }
+}
-function downloadEmojiIndex(lang: string) {
+function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
async function main() {
const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
function download() {
switch (lang) {
case 'en-US': return import('../../unicode-emoji-indexes/en-US.json').then(x => x.default);
+ case 'ja-JP': return import('../../unicode-emoji-indexes/ja-JP.json').then(x => x.default);
+ case 'ja-JP_hira': return import('../../unicode-emoji-indexes/ja-JP_hira.json').then(x => x.default);
default: throw new Error('unrecognized lang: ' + lang);
}
}
@@ -368,7 +396,7 @@ function removeEmojiIndex(lang: string) {
}
async function setPinnedList() {
- const lists = await os.api('users/lists/list');
+ const lists = await misskeyApi('users/lists/list');
const { canceled, result: list } = await os.select({
title: i18n.ts.selectList,
items: lists.map(x => ({
@@ -437,8 +465,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.general,
icon: 'ti ti-adjustments',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 858983a214..9bb3957a84 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,6 +22,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
</FormSection>
<FormSection>
+ <template #label><i class="ti ti-star"></i> {{ i18n.ts._exportOrImport.clips }}</template>
+ <MkFolder>
+ <template #label>{{ i18n.ts.export }}</template>
+ <template #icon><i class="ti ti-download"></i></template>
+ <MkButton primary :class="$style.button" inline @click="exportClips()"><i class="ti ti-download"></i> {{ i18n.ts.export }}</MkButton>
+ </MkFolder>
+ </FormSection>
+ <FormSection>
<template #label><i class="ti ti-users"></i> {{ i18n.ts._exportOrImport.followingList }}</template>
<div class="_gaps_s">
<MkFolder>
@@ -117,11 +125,12 @@ import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { selectFile } from '@/scripts/select-file.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { $i } from '@/account.js';
-import { defaultStore } from "@/store.js";
+import { defaultStore } from '@/store.js';
const excludeMutingUsers = ref(false);
const excludeInactiveUsers = ref(false);
@@ -149,15 +158,19 @@ const onError = (ev) => {
};
const exportNotes = () => {
- os.api('i/export-notes', {}).then(onExportSuccess).catch(onError);
+ misskeyApi('i/export-notes', {}).then(onExportSuccess).catch(onError);
};
const exportFavorites = () => {
- os.api('i/export-favorites', {}).then(onExportSuccess).catch(onError);
+ misskeyApi('i/export-favorites', {}).then(onExportSuccess).catch(onError);
+};
+
+const exportClips = () => {
+ misskeyApi('i/export-clips', {}).then(onExportSuccess).catch(onError);
};
const exportFollowing = () => {
- os.api('i/export-following', {
+ misskeyApi('i/export-following', {
excludeMuting: excludeMutingUsers.value,
excludeInactive: excludeInactiveUsers.value,
})
@@ -165,24 +178,24 @@ const exportFollowing = () => {
};
const exportBlocking = () => {
- os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError);
+ misskeyApi('i/export-blocking', {}).then(onExportSuccess).catch(onError);
};
const exportUserLists = () => {
- os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError);
+ misskeyApi('i/export-user-lists', {}).then(onExportSuccess).catch(onError);
};
const exportMuting = () => {
- os.api('i/export-mute', {}).then(onExportSuccess).catch(onError);
+ misskeyApi('i/export-mute', {}).then(onExportSuccess).catch(onError);
};
const exportAntennas = () => {
- os.api('i/export-antennas', {}).then(onExportSuccess).catch(onError);
+ misskeyApi('i/export-antennas', {}).then(onExportSuccess).catch(onError);
};
const importFollowing = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
- os.api('i/import-following', {
+ misskeyApi('i/import-following', {
fileId: file.id,
withReplies: withReplies.value,
}).then(onImportSuccess).catch(onError);
@@ -190,32 +203,32 @@ const importFollowing = async (ev) => {
const importUserLists = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
- os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
+ misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importMuting = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
- os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
+ misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importBlocking = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
- os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
+ misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const importAntennas = async (ev) => {
const file = await selectFile(ev.currentTarget ?? ev.target);
- os.api('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError);
+ misskeyApi('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError);
};
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.importAndExport,
icon: 'ti ti-package',
-});
+}));
</script>
<style module>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index ee0188873e..5fc1fd1bca 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -28,16 +28,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
-import { ComputedRef, Ref, computed, onActivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
+import { computed, onActivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
import { i18n } from '@/i18n.js';
import MkInfo from '@/components/MkInfo.vue';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
import { signout, $i } from '@/account.js';
import { clearCache } from '@/scripts/clear-cache.js';
import { instance } from '@/instance.js';
-import { useRouter } from '@/router.js';
-import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import * as os from '@/os.js';
+import { useRouter } from '@/router/supplier.js';
const indexInfo = {
title: i18n.ts.settings,
@@ -46,7 +46,7 @@ const indexInfo = {
};
const INFO = ref(indexInfo);
const el = shallowRef<HTMLElement | null>(null);
-const childInfo: Ref<ComputedRef<PageMetadata> | null> = ref(null);
+const childInfo = ref<null | PageMetadata>(null);
const router = useRouter();
@@ -231,20 +231,22 @@ watch(router.currentRef, (to) => {
const emailNotConfigured = computed(() => instance.enableEmail && ($i.email == null || !$i.emailVerified));
-provideMetadataReceiver((info) => {
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
if (info == null) {
childInfo.value = null;
} else {
childInfo.value = info;
- INFO.value.needWideArea = info.value.needWideArea ?? undefined;
+ INFO.value.needWideArea = info.needWideArea ?? undefined;
}
});
+provideReactiveMetadata(INFO);
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(INFO);
+definePageMetadata(() => INFO.value);
// w 890
// h 700
</script>
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index 15bf4691b2..ddc23945dd 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -21,13 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkInput v-for="(_, i) in accountAliases" v-model="accountAliases[i]">
<template #prefix><i class="ti ti-plane-arrival"></i></template>
- <template #label>{{ i18n.t('_accountMigration.moveFromLabel', { n: i + 1 }) }}</template>
+ <template #label>{{ i18n.tsx._accountMigration.moveFromLabel({ n: i + 1 }) }}</template>
</MkInput>
</div>
</div>
</MkFolder>
- <MkFolder :defaultOpen="!!$i?.movedTo">
+ <MkFolder :defaultOpen="!!$i.movedTo">
<template #icon><i class="ti ti-plane-departure"></i></template>
<template #label>{{ i18n.ts._accountMigration.moveTo }}</template>
@@ -66,24 +66,27 @@ import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkUserInfo from '@/components/MkUserInfo.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
import { unisonReload } from '@/scripts/unison-reload.js';
+const $i = signinRequired();
+
const moveToAccount = ref('');
const movedTo = ref<Misskey.entities.UserDetailed>();
const accountAliases = ref(['']);
async function init() {
- if ($i?.movedTo) {
- movedTo.value = await os.api('users/show', { userId: $i.movedTo });
+ if ($i.movedTo) {
+ movedTo.value = await misskeyApi('users/show', { userId: $i.movedTo });
} else {
moveToAccount.value = '';
}
- if ($i?.alsoKnownAs && $i.alsoKnownAs.length > 0) {
- const alsoKnownAs = await os.api('users/show', { userIds: $i.alsoKnownAs });
+ if ($i.alsoKnownAs && $i.alsoKnownAs.length > 0) {
+ const alsoKnownAs = await misskeyApi('users/show', { userIds: $i.alsoKnownAs });
accountAliases.value = (alsoKnownAs && alsoKnownAs.length > 0) ? alsoKnownAs.map(user => `@${Misskey.acct.toString(user)}`) : [''];
} else {
accountAliases.value = [''];
@@ -94,7 +97,7 @@ async function move(): Promise<void> {
const account = moveToAccount.value;
const confirm = await os.confirm({
type: 'warning',
- text: i18n.t('_accountMigration.migrationConfirm', { account }),
+ text: i18n.tsx._accountMigration.migrationConfirm({ account }),
});
if (confirm.canceled) return;
await os.apiWithDialog('i/move', {
@@ -118,10 +121,10 @@ async function save(): Promise<void> {
init();
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.accountMigration,
icon: 'ti ti-plane',
-});
+}));
</script>
<style lang="scss">
diff --git a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
index 4b5080ea8f..d1fde2fc1c 100644
--- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -19,11 +19,13 @@ import { ref, watch } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue';
-import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-const instanceMutes = ref($i!.mutedInstances.join('\n'));
+const $i = signinRequired();
+
+const instanceMutes = ref($i.mutedInstances.join('\n'));
const changed = ref(false);
async function save() {
@@ -32,7 +34,7 @@ async function save() {
.map(el => el.trim())
.filter(el => el);
- await os.api('i/update', {
+ await misskeyApi('i/update', {
mutedInstances: mutes,
});
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 83f7baf428..f4ee7dffbf 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -9,14 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.wordMute }}</template>
- <XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
+ <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.hardWordMute }}</template>
- <XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
+ <XWordMute :muted="$i.hardMutedWords" @save="saveHardMutedWords"/>
</MkFolder>
<MkFolder>
@@ -135,10 +135,13 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { infoImageUrl } from '@/instance.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue';
+const $i = signinRequired();
+
const renoteMutingPagination = {
endpoint: 'renote-mute/list' as const,
limit: 10,
@@ -216,21 +219,21 @@ async function toggleBlockItem(item) {
}
async function saveMutedWords(mutedWords: (string | string[])[]) {
- await os.api('i/update', { mutedWords });
+ await misskeyApi('i/update', { mutedWords });
}
async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
- await os.api('i/update', { hardMutedWords });
+ await misskeyApi('i/update', { hardMutedWords });
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.muteAndBlock,
icon: 'ti ti-ban',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/mute-block.word-mute.vue b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
index 7328967c51..f5837abe98 100644
--- a/packages/frontend/src/pages/settings/mute-block.word-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.word-mute.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -64,7 +64,7 @@ async function save() {
os.alert({
type: 'error',
title: i18n.ts.regexpError,
- text: i18n.t('regexpErrorDescription', { tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
+ text: i18n.tsx.regexpErrorDescription({ tab: 'word mute', line: i + 1 }) + '\n' + err.toString(),
});
// re-throw error so these invalid settings are not saved
throw err;
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index 0e56ebd844..7f8460e316 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -118,10 +118,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.navbar,
icon: 'ti ti-list',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/notifications.notification-config.vue b/packages/frontend/src/pages/settings/notifications.notification-config.vue
index 5c8378e1dc..d6aac63674 100644
--- a/packages/frontend/src/pages/settings/notifications.notification-config.vue
+++ b/packages/frontend/src/pages/settings/notifications.notification-config.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 98b82f7116..febcfa32ed 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.notificationRecieveConfig }}</template>
<div class="_gaps_s">
<MkFolder v-for="type in notificationTypes.filter(x => !nonConfigurableNotificationTypes.includes(x))" :key="type">
- <template #label>{{ i18n.t('_notification._types.' + type) }}</template>
+ <template #label>{{ i18n.ts._notification._types[type] }}</template>
<template #suffix>
{{
$i.notificationRecieveConfig[type]?.type === 'never' ? i18n.ts.none :
@@ -62,18 +62,21 @@ import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { notificationTypes } from '@/const.js';
+const $i = signinRequired();
+
const nonConfigurableNotificationTypes = ['note', 'roleAssigned', 'followRequestAccepted', 'achievementEarned'];
const allowButton = shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
const pushRegistrationInServer = computed(() => allowButton.value?.pushRegistrationInServer);
const sendReadMessage = computed(() => pushRegistrationInServer.value?.sendReadMessage || false);
-const userLists = await os.api('users/lists/list');
+const userLists = await misskeyApi('users/lists/list');
async function readAllUnreadNotes() {
await os.apiWithDialog('i/read-all-unread-notes');
@@ -86,11 +89,11 @@ async function readAllNotifications() {
async function updateReceiveConfig(type, value) {
await os.apiWithDialog('i/update', {
notificationRecieveConfig: {
- ...$i!.notificationRecieveConfig,
+ ...$i.notificationRecieveConfig,
[type]: value,
},
}).then(i => {
- $i!.notificationRecieveConfig = i.notificationRecieveConfig;
+ $i.notificationRecieveConfig = i.notificationRecieveConfig;
});
}
@@ -107,15 +110,15 @@ function onChangeSendReadMessage(v: boolean) {
}
function testNotification(): void {
- os.api('notifications/test-notification');
+ misskeyApi('notifications/test-notification');
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.notifications,
icon: 'ti ti-bell',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 340a9550b4..a1cb2ea1c4 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -93,26 +93,21 @@ import FormInfo from '@/components/MkInfo.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
-import { signout, $i } from '@/account.js';
+import { signout, signinRequired } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import FormSection from '@/components/form/section.vue';
+const $i = signinRequired();
+
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
-function onChangeInjectFeaturedNote(v) {
- os.api('i/update', {
- injectFeaturedNote: v,
- }).then((i) => {
- $i!.injectFeaturedNote = i.injectFeaturedNote;
- });
-}
-
async function deleteAccount() {
{
const { canceled } = await os.confirm({
@@ -154,7 +149,7 @@ async function updateRepliesAll(withReplies: boolean) {
});
if (canceled) return;
- os.api('following/update-all', { withReplies });
+ misskeyApi('following/update-all', { withReplies });
}
watch([
@@ -167,8 +162,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.other,
icon: 'ti ti-dots',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue
index c174ba176f..3ab26e80d9 100644
--- a/packages/frontend/src/pages/settings/plugin.install.vue
+++ b/packages/frontend/src/pages/settings/plugin.install.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -53,8 +53,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts._plugin.install,
icon: 'ti ti-download',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index bf760e623f..0ab75b95a2 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -125,8 +125,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.plugins,
icon: 'ti ti-plug',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index cc6223218f..676159d1b5 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -37,12 +37,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { computed, onMounted, onUnmounted, ref } from 'vue';
+import { onMounted, onUnmounted, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import FormSection from '@/components/form/section.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { useStream } from '@/stream.js';
@@ -144,7 +145,7 @@ const connection = $i && useStream().useChannel('main');
const profiles = ref<Record<string, Profile> | null>(null);
-os.api('i/registry/get-all', { scope })
+misskeyApi('i/registry/get-all', { scope })
.then(res => {
profiles.value = res || {};
});
@@ -436,10 +437,10 @@ onUnmounted(() => {
connection?.off('registryUpdated');
});
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: ts.preferencesBackups,
icon: 'ti ti-device-floppy',
-})));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index 67a2f2cb40..d418be624e 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -77,12 +77,14 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+const $i = signinRequired();
+
const isLocked = ref($i.isLocked);
const autoAcceptFollowed = ref($i.autoAcceptFollowed);
const noCrawle = ref($i.noCrawle);
@@ -90,8 +92,8 @@ const preventAiLearning = ref($i.preventAiLearning);
const isExplorable = ref($i.isExplorable);
const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions);
-const followingVisibility = ref($i?.followingVisibility);
-const followersVisibility = ref($i?.followersVisibility);
+const followingVisibility = ref($i.followingVisibility);
+const followersVisibility = ref($i.followersVisibility);
const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
@@ -99,7 +101,7 @@ const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberN
const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
function save() {
- os.api('i/update', {
+ misskeyApi('i/update', {
isLocked: !!isLocked.value,
autoAcceptFollowed: !!autoAcceptFollowed.value,
noCrawle: !!noCrawle.value,
@@ -116,8 +118,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.privacy,
icon: 'ti ti-lock-open',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index d28c8284cf..60bf9b4d3d 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -120,14 +120,17 @@ import FormSlot from '@/components/form/slot.vue';
import { selectFile } from '@/scripts/select-file.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
import { langmap } from '@/scripts/langmap.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { defaultStore } from '@/store.js';
+import { globalEvents } from '@/events.js';
import MkInfo from '@/components/MkInfo.vue';
import MkTextarea from '@/components/MkTextarea.vue';
+const $i = signinRequired();
+
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
@@ -138,8 +141,8 @@ const profile = reactive({
location: $i.location,
birthday: $i.birthday,
lang: $i.lang,
- isBot: $i.isBot,
- isCat: $i.isCat,
+ isBot: $i.isBot ?? false,
+ isCat: $i.isCat ?? false,
});
watch(() => profile, () => {
@@ -148,7 +151,7 @@ watch(() => profile, () => {
deep: true,
});
-const fields = ref($i?.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
+const fields = ref($i.fields.map(field => ({ id: Math.random().toString(), name: field.name, value: field.value })) ?? []);
const fieldEditMode = ref(false);
function addField() {
@@ -171,6 +174,7 @@ function saveFields() {
os.apiWithDialog('i/update', {
fields: fields.value.filter(field => field.name !== '' && field.value !== '').map(field => ({ name: field.name, value: field.value })),
});
+ globalEvents.emit('requestClearPageCache');
}
function save() {
@@ -189,6 +193,7 @@ function save() {
isBot: !!profile.isBot,
isCat: !!profile.isCat,
});
+ globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled');
if (profile.name === 'syuilo' || profile.name === 'しゅいろ') {
claimAchievement('setNameToSyuilo');
@@ -204,7 +209,7 @@ function changeAvatar(ev) {
const { canceled } = await os.confirm({
type: 'question',
- text: i18n.t('cropImageAsk'),
+ text: i18n.ts.cropImageAsk,
okText: i18n.ts.cropYes,
cancelText: i18n.ts.cropNo,
});
@@ -220,6 +225,7 @@ function changeAvatar(ev) {
});
$i.avatarId = i.avatarId;
$i.avatarUrl = i.avatarUrl;
+ globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled');
});
}
@@ -230,7 +236,7 @@ function changeBanner(ev) {
const { canceled } = await os.confirm({
type: 'question',
- text: i18n.t('cropImageAsk'),
+ text: i18n.ts.cropImageAsk,
okText: i18n.ts.cropYes,
cancelText: i18n.ts.cropNo,
});
@@ -246,6 +252,7 @@ function changeBanner(ev) {
});
$i.bannerId = i.bannerId;
$i.bannerUrl = i.bannerUrl;
+ globalEvents.emit('requestClearPageCache');
});
}
@@ -253,10 +260,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.profile,
icon: 'ti ti-user',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue
index 40671f7132..5346a58a79 100644
--- a/packages/frontend/src/pages/settings/roles.vue
+++ b/packages/frontend/src/pages/settings/roles.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -27,24 +27,20 @@ import { computed } from 'vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
-function save() {
- os.apiWithDialog('i/update', {
-
- });
-}
+const $i = signinRequired();
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.roles,
icon: 'ti ti-badges',
-});
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index 3f85f27e47..de0f63630e 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -47,6 +47,7 @@ import FormSlot from '@/components/form/slot.vue';
import MkButton from '@/components/MkButton.vue';
import MkPagination from '@/components/MkPagination.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -92,7 +93,7 @@ async function regenerateToken() {
const auth = await os.authenticateDialog();
if (auth.canceled) return;
- os.api('i/regenerate-token', {
+ misskeyApi('i/regenerate-token', {
password: auth.result.password,
token: auth.result.token,
});
@@ -102,10 +103,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.security,
icon: 'ti ti-lock',
-});
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 2f4cd1be2c..113abd708b 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -32,7 +32,8 @@ import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
-import { playFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
+import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
import { selectFile } from '@/scripts/select-file.js';
const props = defineProps<{
@@ -53,7 +54,7 @@ const fileName = ref<string>('');
const volume = ref(props.volume);
if (type.value === '_driveFile_' && fileId.value) {
- const apiRes = await os.api('drive/files/show', {
+ const apiRes = await misskeyApi('drive/files/show', {
fileId: fileId.value,
});
fileName.value = apiRes.name;
@@ -118,7 +119,7 @@ function listen() {
return;
}
- playFile(type.value === '_driveFile_' ? {
+ playMisskeySfxFile(type.value === '_driveFile_' ? {
type: '_driveFile_',
fileId: fileId.value as string,
fileUrl: fileUrl.value as string,
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 9fbcce2286..090f0cf14c 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.sounds }}</template>
<div class="_gaps_s">
<MkFolder v-for="type in operationTypes" :key="type">
- <template #label>{{ i18n.t('_sfx.' + type) }}</template>
+ <template #label>{{ i18n.ts._sfx[type] }}</template>
<template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
@@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { Ref, computed, ref } from 'vue';
+import XSound from './sounds.sound.vue';
import type { SoundType, OperationType } from '@/scripts/sound.js';
import type { SoundStore } from '@/store.js';
-import XSound from './sounds.sound.vue';
import MkRange from '@/components/MkRange.vue';
import MkButton from '@/components/MkButton.vue';
import FormSection from '@/components/form/section.vue';
@@ -94,8 +94,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.sounds,
icon: 'ti ti-music',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index de5f1a3db9..92e389a288 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue
index 294da80bb5..1ae3de7994 100644
--- a/packages/frontend/src/pages/settings/statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -21,7 +21,7 @@ import { v4 as uuid } from 'uuid';
import XStatusbar from './statusbar.statusbar.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -31,7 +31,7 @@ const statusbars = defaultStore.reactiveState.statusbars;
const userLists = ref<Misskey.entities.UserList[] | null>(null);
onMounted(() => {
- os.api('users/lists/list').then(res => {
+ misskeyApi('users/lists/list').then(res => {
userLists.value = res;
});
});
@@ -50,8 +50,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.statusbar,
icon: 'ti ti-list',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue
index 45970c88e6..4f05d3784c 100644
--- a/packages/frontend/src/pages/settings/theme.install.vue
+++ b/packages/frontend/src/pages/settings/theme.install.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -33,7 +33,7 @@ async function install(code: string): Promise<void> {
await installTheme(code);
os.alert({
type: 'success',
- text: i18n.t('_theme.installed', { name: theme.name }),
+ text: i18n.tsx._theme.installed({ name: theme.name }),
});
} catch (err) {
switch (err.message.toLowerCase()) {
@@ -59,8 +59,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts._theme.install,
icon: 'ti ti-download',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue
index 7bacf41eec..8a94d7388b 100644
--- a/packages/frontend/src/pages/settings/theme.manage.vue
+++ b/packages/frontend/src/pages/settings/theme.manage.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -76,8 +76,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts._theme.manage,
icon: 'ti ti-tool',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index dedac10270..0a4bd4b826 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -88,6 +88,18 @@ import { uniqueBy } from '@/scripts/array.js';
import { fetchThemes, getThemes } from '@/theme-store.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
+import { unisonReload } from '@/scripts/unison-reload.js';
+import * as os from '@/os.js';
+
+async function reloadAsk() {
+ const { canceled } = await os.confirm({
+ type: 'info',
+ text: i18n.ts.reloadToApplySetting,
+ });
+ if (canceled) return;
+
+ unisonReload();
+}
const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef();
@@ -124,6 +136,7 @@ const lightThemeId = computed({
}
},
});
+
const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
@@ -141,7 +154,7 @@ watch(wallpaper, () => {
} else {
miLocalStorage.setItem('wallpaper', wallpaper.value);
}
- location.reload();
+ reloadAsk();
});
onActivated(() => {
@@ -164,10 +177,10 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.theme,
icon: 'ti ti-palette',
-});
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index c1695dc6a5..e9fb1e471e 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -48,9 +48,10 @@ import FormSection from '@/components/form/section.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/router.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -58,7 +59,7 @@ const props = defineProps<{
webhookId: string;
}>();
-const webhook = await os.api('i/webhooks/show', {
+const webhook = await misskeyApi('i/webhooks/show', {
webhookId: props.webhookId,
});
@@ -98,7 +99,7 @@ async function save(): Promise<void> {
async function del(): Promise<void> {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: webhook.name }),
+ text: i18n.tsx.deleteAreYouSure({ x: webhook.name }),
});
if (canceled) return;
@@ -113,8 +114,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: 'Edit webhook',
icon: 'ti ti-webhook',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index 8a4f03431c..5bf85e48f4 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -82,8 +82,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: 'Create new webhook',
icon: 'ti ti-webhook',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index 334e5e841b..0d11b00c97 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -50,8 +50,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: 'Webhook',
icon: 'ti ti-webhook',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index cb5acf3afa..680934e7ce 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -37,6 +37,7 @@ import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { postMessageToParentWindow } from '@/scripts/post-message.js';
import { i18n } from '@/i18n.js';
@@ -55,7 +56,7 @@ const renote = ref<Misskey.entities.Note | undefined>();
const visibility = ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visibilityQuery : undefined);
const localOnly = ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined);
const files = ref([] as Misskey.entities.DriveFile[]);
-const visibleUsers = ref([] as Misskey.entities.User[]);
+const visibleUsers = ref([] as Misskey.entities.UserDetailed[]);
async function init() {
let noteText = '';
@@ -76,7 +77,7 @@ async function init() {
]
// TypeScriptの指示通りに変換する
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
- .map(q => os.api('users/show', q)
+ .map(q => misskeyApi('users/show', q)
.then(user => {
visibleUsers.value.push(user);
}, () => {
@@ -91,11 +92,11 @@ async function init() {
const replyId = urlParams.get('replyId');
const replyUri = urlParams.get('replyUri');
if (replyId) {
- reply.value = await os.api('notes/show', {
+ reply.value = await misskeyApi('notes/show', {
noteId: replyId,
});
} else if (replyUri) {
- const obj = await os.api('ap/show', {
+ const obj = await misskeyApi('ap/show', {
uri: replyUri,
});
if (obj.type === 'Note') {
@@ -108,11 +109,11 @@ async function init() {
const renoteId = urlParams.get('renoteId');
const renoteUri = urlParams.get('renoteUri');
if (renoteId) {
- renote.value = await os.api('notes/show', {
+ renote.value = await misskeyApi('notes/show', {
noteId: renoteId,
});
} else if (renoteUri) {
- const obj = await os.api('ap/show', {
+ const obj = await misskeyApi('ap/show', {
uri: renoteUri,
});
if (obj.type === 'Note') {
@@ -126,7 +127,7 @@ async function init() {
if (fileIds) {
await Promise.all(
fileIds.split(',')
- .map(fileId => os.api('drive/files/show', { fileId })
+ .map(fileId => misskeyApi('drive/files/show', { fileId })
.then(file => {
files.value.push(file);
}, () => {
@@ -171,8 +172,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.share,
icon: 'ti ti-share',
-});
+}));
</script>
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index 638c7e8773..8c2f7042cd 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-user-check"></i>
</div>
<div class="_gaps_m" style="padding: 32px;">
- <div>{{ i18n.t('clickToFinishEmailVerification', { ok: i18n.ts.gotIt }) }}</div>
+ <div>{{ i18n.tsx.clickToFinishEmailVerification({ ok: i18n.ts.gotIt }) }}</div>
<div>
<MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;">
{{ submitting ? i18n.ts.processing : i18n.ts.gotIt }}<MkEllipsis v-if="submitting"/>
@@ -31,6 +31,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
const submitting = ref(false);
@@ -42,7 +43,7 @@ function submit() {
if (submitting.value) return;
submitting.value = true;
- os.api('signup-pending', {
+ misskeyApi('signup-pending', {
code: props.code,
}).then(res => {
return login(res.i, '/');
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 797ab796d2..9b77392872 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -55,21 +55,22 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: props.tag,
icon: 'ti ti-hash',
-})));
+}));
</script>
<style lang="scss" module>
.footer {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
+ background: var(--acrylicBg);
border-top: solid 0.5px var(--divider);
display: flex;
}
.button {
- margin: 0 auto var(--margin) auto;
+ margin: 0 auto;
}
</style>
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index e14bd6d89b..50c3beeabc 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -190,7 +190,7 @@ function applyThemeCode() {
async function saveAs() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts.name,
- allowEmpty: false,
+ minLength: 1,
});
if (canceled) return;
@@ -208,7 +208,7 @@ async function saveAs() {
changed.value = false;
os.alert({
type: 'success',
- text: i18n.t('_theme.installed', { name: theme.value.name }),
+ text: i18n.tsx._theme.installed({ name: theme.value.name }),
});
}
@@ -228,10 +228,10 @@ const headerActions = computed(() => [{
const headerTabs = computed(() => []);
-definePageMetadata({
+definePageMetadata(() => ({
title: i18n.ts.themeEditor,
icon: 'ti ti-palette',
-});
+}));
</script>
<style lang="scss" scoped>
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 1b24f98bdb..48dfc1fd44 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,27 +7,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
<MkSpacer :contentMax="800">
- <div ref="rootEl" v-hotkey.global="keymap">
- <MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
- {{ i18n.ts._timelineDescription[src] }}
- </MkInfo>
- <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
-
- <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
- <div :class="$style.tl">
- <MkTimeline
- ref="tlComponent"
- :key="src + withRenotes + withReplies + onlyFiles"
- :src="src.split(':')[0]"
- :list="src.split(':')[1]"
- :withRenotes="withRenotes"
- :withReplies="withReplies"
- :onlyFiles="onlyFiles"
- :sound="true"
- @queue="queueUpdated"
- />
+ <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
+ <div :key="src" ref="rootEl" v-hotkey.global="keymap">
+ <MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
+ {{ i18n.ts._timelineDescription[src] }}
+ </MkInfo>
+ <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
+ <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
+ <div :class="$style.tl">
+ <MkTimeline
+ ref="tlComponent"
+ :key="src + withRenotes + withReplies + onlyFiles"
+ :src="src.split(':')[0]"
+ :list="src.split(':')[1]"
+ :withRenotes="withRenotes"
+ :withReplies="withReplies"
+ :onlyFiles="onlyFiles"
+ :sound="true"
+ @queue="queueUpdated"
+ />
+ </div>
</div>
- </div>
+ </MkHorizontalSwipe>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -38,8 +39,10 @@ import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkPostForm from '@/components/MkPostForm.vue';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { scroll } from '@/scripts/scroll.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
@@ -47,6 +50,7 @@ import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { antennasCache, userListsCache } from '@/cache.js';
import { deviceKind } from '@/scripts/device-kind.js';
+import { deepMerge } from '@/scripts/merge.js';
import { MenuItem } from '@/types/menu.js';
import { miLocalStorage } from '@/local-storage.js';
@@ -62,16 +66,63 @@ const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>();
const queue = ref(0);
-const srcWhenNotSignin = ref(isLocalTimelineAvailable ? 'local' : 'global');
-const src = computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value), set: (x) => saveSrc(x) });
-const withRenotes = ref(true);
-const withReplies = ref($i ? defaultStore.state.tlWithReplies : false);
-const onlyFiles = ref(false);
+const srcWhenNotSignin = ref<'local' | 'global'>(isLocalTimelineAvailable ? 'local' : 'global');
+const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`>({
+ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
+ set: (x) => saveSrc(x),
+});
+const withRenotes = computed<boolean>({
+ get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
+ set: (x) => saveTlFilter('withRenotes', x),
+});
-watch(src, () => queue.value = 0);
+// computed内での無限ループを防ぐためのフラグ
+const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies');
+
+const withReplies = computed<boolean>({
+ get: () => {
+ if (!$i) return false;
+ if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') {
+ return false;
+ } else {
+ return defaultStore.reactiveState.tl.value.filter.withReplies;
+ }
+ },
+ set: (x) => saveTlFilter('withReplies', x),
+});
+const onlyFiles = computed<boolean>({
+ get: () => {
+ if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') {
+ return false;
+ } else {
+ return defaultStore.reactiveState.tl.value.filter.onlyFiles;
+ }
+ },
+ set: (x) => saveTlFilter('onlyFiles', x),
+});
+
+watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => {
+ if (withRepliesTo) {
+ localSocialTLFilterSwitchStore.value = 'withReplies';
+ } else if (onlyFilesTo) {
+ localSocialTLFilterSwitchStore.value = 'onlyFiles';
+ } else {
+ localSocialTLFilterSwitchStore.value = false;
+ }
+});
+
+const withSensitive = computed<boolean>({
+ get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
+ set: (x) => saveTlFilter('withSensitive', x),
+});
-watch(withReplies, (x) => {
- if ($i) defaultStore.set('tlWithReplies', x);
+watch(src, () => {
+ queue.value = 0;
+});
+
+watch(withSensitive, () => {
+ // これだけはクライアント側で完結する処理なので手動でリロード
+ tlComponent.value?.reloadTimeline();
});
function queueUpdated(q: number): void {
@@ -122,7 +173,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
}
async function chooseChannel(ev: MouseEvent): Promise<void> {
- const channels = await os.api('channels/my-favorites', {
+ const channels = await misskeyApi('channels/my-favorites', {
limit: 100,
});
const items: MenuItem[] = [
@@ -149,16 +200,24 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
}
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | `list:${string}`): void {
- let userList = null;
+ const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
+
if (newSrc.startsWith('userList:')) {
const id = newSrc.substring('userList:'.length);
- userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id);
+ out.userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id) ?? null;
+ }
+
+ defaultStore.set('tl', out);
+ if (['local', 'global'].includes(newSrc)) {
+ srcWhenNotSignin.value = newSrc as 'local' | 'global';
+ }
+}
+
+function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
+ if (key !== 'withReplies' || $i) {
+ const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
+ defaultStore.set('tl', out);
}
- defaultStore.set('tl', {
- src: newSrc,
- userList,
- });
- srcWhenNotSignin.value = newSrc;
}
async function timetravel(): Promise<void> {
@@ -198,6 +257,10 @@ const headerActions = computed(() => {
disabled: onlyFiles,
} : undefined, {
type: 'switch',
+ text: i18n.ts.withSensitive,
+ ref: withSensitive,
+ }, {
+ type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: src.value === 'local' || src.value === 'social' ? withReplies : false,
@@ -210,8 +273,7 @@ const headerActions = computed(() => {
icon: 'ti ti-refresh',
text: i18n.ts.reload,
handler: (ev: Event) => {
- console.log('called');
- tlComponent.value.reloadTimeline();
+ tlComponent.value?.reloadTimeline();
},
});
}
@@ -275,10 +337,10 @@ const headerTabsWhenNotLogin = computed(() => [
}] : []),
] as Tab[]);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: i18n.ts.timeline,
icon: src.value === 'local' ? 'ti ti-planet' : src.value === 'social' ? 'ti ti-universe' : src.value === 'global' ? 'ti ti-whirl' : 'ti ti-home',
-})));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index ba22d784c7..de6737f37d 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -28,10 +28,10 @@ import { computed, watch, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkTimeline from '@/components/MkTimeline.vue';
import { scroll } from '@/scripts/scroll.js';
-import * as os from '@/os.js';
-import { useRouter } from '@/router.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
+import { useRouter } from '@/router/supplier.js';
const router = useRouter();
@@ -45,7 +45,7 @@ const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>();
watch(() => props.listId, async () => {
- list.value = await os.api('users/lists/show', {
+ list.value = await misskeyApi('users/lists/show', {
listId: props.listId,
});
}, { immediate: true });
@@ -70,10 +70,10 @@ const headerActions = computed(() => list.value ? [{
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => list.value ? {
- title: list.value.name,
+definePageMetadata(() => ({
+ title: list.value ? list.value.name : i18n.ts.lists,
icon: 'ti ti-list',
-} : null));
+}));
</script>
<style lang="scss" module>
diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue
index 5d83efc1a9..a77493fe47 100644
--- a/packages/frontend/src/pages/user-tag.vue
+++ b/packages/frontend/src/pages/user-tag.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -34,9 +34,9 @@ const tagUsers = computed(() => ({
},
}));
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: props.tag,
icon: 'ti ti-user-search',
-})));
+}));
</script>
diff --git a/packages/frontend/src/pages/user/achievements.vue b/packages/frontend/src/pages/user/achievements.vue
index 4e14443074..403e74904c 100644
--- a/packages/frontend/src/pages/user/achievements.vue
+++ b/packages/frontend/src/pages/user/achievements.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index bd1159cb32..aa2c791c76 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue';
import { Chart, ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
@@ -61,7 +61,7 @@ async function renderChart() {
}));
};
- const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
diff --git a/packages/frontend/src/pages/user/activity.heatmap.vue b/packages/frontend/src/pages/user/activity.heatmap.vue
deleted file mode 100644
index ff46db9653..0000000000
--- a/packages/frontend/src/pages/user/activity.heatmap.vue
+++ /dev/null
@@ -1,219 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div ref="rootEl">
- <MkLoading v-if="fetching"/>
- <div v-else :class="$style.root" class="_panel">
- <canvas ref="chartEl"></canvas>
- </div>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
-import { Chart } from 'chart.js';
-import * as Misskey from 'misskey-js';
-import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
-
-initChart();
-
-const props = defineProps<{
- src: string;
- user: Misskey.entities.User;
-}>();
-
-const rootEl = shallowRef<HTMLDivElement>(null);
-const chartEl = shallowRef<HTMLCanvasElement>(null);
-const now = new Date();
-let chartInstance: Chart = null;
-const fetching = ref(true);
-
-const { handler: externalTooltipHandler } = useChartTooltip({
- position: 'middle',
-});
-
-async function renderChart() {
- if (chartInstance) {
- chartInstance.destroy();
- }
-
- const wide = rootEl.value.offsetWidth > 700;
- const narrow = rootEl.value.offsetWidth < 400;
-
- const weeks = wide ? 50 : narrow ? 10 : 25;
- const chartLimit = 7 * weeks;
-
- const getDate = (ago: number) => {
- const y = now.getFullYear();
- const m = now.getMonth();
- const d = now.getDate();
-
- return new Date(y, m, d - ago);
- };
-
- const format = (arr) => {
- return arr.map((v, i) => {
- const dt = getDate(i);
- const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
- return {
- x: iso,
- y: dt.getDay(),
- d: iso,
- v,
- };
- });
- };
-
- let values;
-
- if (props.src === 'notes') {
- const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
- values = raw.inc;
- }
-
- fetching.value = false;
-
- await nextTick();
-
- const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
-
- // 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
- const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
-
- const min = Math.max(0, Math.min(...values) - 1);
-
- const marginEachCell = 4;
-
- chartInstance = new Chart(chartEl.value, {
- type: 'matrix',
- data: {
- datasets: [{
- label: '',
- data: format(values),
- pointRadius: 0,
- borderWidth: 0,
- borderJoinStyle: 'round',
- borderRadius: 3,
- backgroundColor(c) {
- const value = c.dataset.data[c.dataIndex].v;
- let a = (value - min) / max;
- if (value !== 0) { // 0でない限りは完全に不可視にはしない
- a = Math.max(a, 0.05);
- }
- return alpha(color, a);
- },
- fill: true,
- width(c) {
- const a = c.chart.chartArea ?? {};
- return (a.right - a.left) / weeks - marginEachCell;
- },
- height(c) {
- const a = c.chart.chartArea ?? {};
- return (a.bottom - a.top) / 7 - marginEachCell;
- },
- /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
- }] satisfies ChartData[],
- */
- }],
- },
- options: {
- aspectRatio: wide ? 6 : narrow ? 1.8 : 3.2,
- layout: {
- padding: {
- left: 8,
- right: 0,
- top: 0,
- bottom: 0,
- },
- },
- scales: {
- x: {
- type: 'time',
- offset: true,
- position: 'bottom',
- time: {
- unit: 'week',
- round: 'week',
- isoWeekday: 0,
- displayFormats: {
- day: 'M/d',
- month: 'Y/M',
- week: 'M/d',
- },
- },
- grid: {
- display: false,
- },
- ticks: {
- display: true,
- maxRotation: 0,
- autoSkipPadding: 8,
- },
- },
- y: {
- offset: true,
- reverse: true,
- position: 'right',
- grid: {
- display: false,
- },
- ticks: {
- maxRotation: 0,
- autoSkip: true,
- padding: 1,
- font: {
- size: 9,
- },
- callback: (value, index, values) => ['', 'Mon', '', 'Wed', '', 'Fri', ''][value],
- },
- },
- },
- plugins: {
- legend: {
- display: false,
- },
- tooltip: {
- enabled: false,
- callbacks: {
- title(context) {
- const v = context[0].dataset.data[context[0].dataIndex];
- return v.d;
- },
- label(context) {
- const v = context.dataset.data[context.dataIndex];
- return [v.v];
- },
- },
- //mode: 'index',
- animation: {
- duration: 0,
- },
- external: externalTooltipHandler,
- },
- },
- },
- });
-}
-
-watch(() => props.src, () => {
- fetching.value = true;
- renderChart();
-});
-
-onMounted(async () => {
- renderChart();
-});
-</script>
-
-<style lang="scss" module>
-.root {
- padding: 20px;
-}
-</style>
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index dd035641d8..64514716d6 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue';
import { Chart, ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
@@ -61,7 +61,7 @@ async function renderChart() {
}));
};
- const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index 2dd9a1570f..ce24807f93 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,7 +18,7 @@ import { onMounted, shallowRef, ref } from 'vue';
import { Chart, ChartDataset } from 'chart.js';
import * as Misskey from 'misskey-js';
import gradient from 'chartjs-plugin-gradient';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
import { chartVLine } from '@/scripts/chart-vline.js';
@@ -61,7 +61,7 @@ async function renderChart() {
}));
};
- const raw = await os.api('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' });
+ const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' });
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
diff --git a/packages/frontend/src/pages/user/activity.vue b/packages/frontend/src/pages/user/activity.vue
index 6703890893..994bd52705 100644
--- a/packages/frontend/src/pages/user/activity.vue
+++ b/packages/frontend/src/pages/user/activity.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkFoldableSection class="item">
<template #header><i class="ti ti-activity"></i> Heatmap</template>
- <XHeatmap :user="user" :src="'notes'"/>
+ <MkHeatmap :user="user" :src="'notes'"/>
</MkFoldableSection>
<MkFoldableSection class="item">
<template #header><i class="ti ti-pencil"></i> Notes</template>
@@ -28,11 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
-import XHeatmap from './activity.heatmap.vue';
import XPv from './activity.pv.vue';
import XNotes from './activity.notes.vue';
import XFollowing from './activity.following.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
+import MkHeatmap from '@/components/MkHeatmap.vue';
const props = defineProps<{
user: Misskey.entities.User;
diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue
index eaae472516..ac01cff8cd 100644
--- a/packages/frontend/src/pages/user/clips.vue
+++ b/packages/frontend/src/pages/user/clips.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/flashs.vue b/packages/frontend/src/pages/user/flashs.vue
index 5e93a0b04c..b3313476e1 100644
--- a/packages/frontend/src/pages/user/flashs.vue
+++ b/packages/frontend/src/pages/user/flashs.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue
index 19b7290353..e60dccec17 100644
--- a/packages/frontend/src/pages/user/follow-list.vue
+++ b/packages/frontend/src/pages/user/follow-list.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue
index a4d516a1de..70883242e5 100644
--- a/packages/frontend/src/pages/user/followers.vue
+++ b/packages/frontend/src/pages/user/followers.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XFollowList from './follow-list.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
@@ -37,7 +37,7 @@ const error = ref<any>(null);
function fetchUser(): void {
if (props.acct == null) return;
user.value = null;
- os.api('users/show', Misskey.acct.parse(props.acct)).then(u => {
+ misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
user.value = u;
}).catch(err => {
error.value = err;
@@ -52,11 +52,14 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => user.value ? {
+definePageMetadata(() => ({
+ title: i18n.ts.user,
icon: 'ti ti-user',
- title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
- subtitle: i18n.ts.followers,
- userName: user.value,
- avatar: user.value,
-} : null));
+ ...user.value ? {
+ title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
+ subtitle: i18n.ts.followers,
+ userName: user.value,
+ avatar: user.value,
+ } : {},
+}));
</script>
diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue
index 99cb098d65..37b25f694f 100644
--- a/packages/frontend/src/pages/user/following.vue
+++ b/packages/frontend/src/pages/user/following.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XFollowList from './follow-list.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
@@ -37,7 +37,7 @@ const error = ref<any>(null);
function fetchUser(): void {
if (props.acct == null) return;
user.value = null;
- os.api('users/show', Misskey.acct.parse(props.acct)).then(u => {
+ misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
user.value = u;
}).catch(err => {
error.value = err;
@@ -52,11 +52,14 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => user.value ? {
+definePageMetadata(() => ({
+ title: i18n.ts.user,
icon: 'ti ti-user',
- title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
- subtitle: i18n.ts.following,
- userName: user.value,
- avatar: user.value,
-} : null));
+ ...user.value ? {
+ title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
+ subtitle: i18n.ts.following,
+ userName: user.value,
+ avatar: user.value,
+ } : {},
+}));
</script>
diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue
index 0d806100d9..9ba81322ba 100644
--- a/packages/frontend/src/pages/user/gallery.vue
+++ b/packages/frontend/src/pages/user/gallery.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/home.stories.impl.ts b/packages/frontend/src/pages/user/home.stories.impl.ts
index a2ef5d50d1..c623ef9ee4 100644
--- a/packages/frontend/src/pages/user/home.stories.impl.ts
+++ b/packages/frontend/src/pages/user/home.stories.impl.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
-import { rest } from 'msw';
+import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../../.storybook/fakes.js';
import { commonHandlers } from '../../../.storybook/mocks.js';
import home_ from './home.vue';
@@ -39,12 +39,13 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
- rest.post('/api/users/notes', (req, res, ctx) => {
- return res(ctx.json([]));
+ http.post('/api/users/notes', () => {
+ return HttpResponse.json([]);
}),
- rest.get('/api/charts/user/notes', (req, res, ctx) => {
- const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
- return res(ctx.json({
+ http.get('/api/charts/user/notes', ({ request }) => {
+ const url = new URL(request.url);
+ const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
+ return HttpResponse.json({
total: Array.from({ length }, () => 0),
inc: Array.from({ length }, () => 0),
dec: Array.from({ length }, () => 0),
@@ -54,11 +55,12 @@ export const Default = {
renote: Array.from({ length }, () => 0),
withFile: Array.from({ length }, () => 0),
},
- }));
+ });
}),
- rest.get('/api/charts/user/pv', (req, res, ctx) => {
- const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
- return res(ctx.json({
+ http.get('/api/charts/user/pv', ({ request }) => {
+ const url = new URL(request.url);
+ const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
+ return HttpResponse.json({
upv: {
user: Array.from({ length }, () => 0),
visitor: Array.from({ length }, () => 0),
@@ -67,7 +69,7 @@ export const Default = {
user: Array.from({ length }, () => 0),
visitor: Array.from({ length }, () => 0),
},
- }));
+ });
}),
],
},
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 2a9eb5f8e4..4e3e383e33 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</dl>
<dl v-if="user.birthday" class="field">
<dt class="name"><i class="ti ti-cake ti-fw"></i> {{ i18n.ts.birthday }}</dt>
- <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.t('yearsOld', { age }) }})</dd>
+ <dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ i18n.tsx.yearsOld({ age }) }})</dd>
</dl>
<dl class="field">
<dt class="name"><i class="ti ti-calendar ti-fw"></i> {{ i18n.ts.registeredDate }}</dt>
@@ -166,13 +166,13 @@ import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';
import * as os from '@/os.js';
-import { useRouter } from '@/router.js';
import { i18n } from '@/i18n.js';
import { $i, iAmModerator } from '@/account.js';
import { dateString } from '@/filters/date.js';
import { confetti } from '@/scripts/confetti.js';
-import { api } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { useRouter } from '@/router/supplier.js';
function calcAge(birthdate: string): number {
const date = new Date(birthdate);
@@ -215,7 +215,7 @@ const moderationNote = ref(props.user.moderationNote);
const editModerationNote = ref(false);
watch(moderationNote, async () => {
- await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
+ await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
});
const style = computed(() => {
@@ -266,7 +266,7 @@ function adjustMemoTextarea() {
}
async function updateMemo() {
- await api('users/update-memo', {
+ await misskeyApi('users/update-memo', {
memo: memoDraft.value,
userId: props.user.id,
});
diff --git a/packages/frontend/src/pages/user/index.activity.vue b/packages/frontend/src/pages/user/index.activity.vue
index fe17de9061..45bc35067b 100644
--- a/packages/frontend/src/pages/user/index.activity.vue
+++ b/packages/frontend/src/pages/user/index.activity.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index 32561e6b0b..ce4d113cad 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -37,7 +37,7 @@ import { onMounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { notePage } from '@/filters/note.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue';
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { defaultStore } from '@/store.js';
@@ -61,7 +61,7 @@ function thumbnail(image: Misskey.entities.DriveFile): string {
}
onMounted(() => {
- os.api('users/notes', {
+ misskeyApi('users/notes', {
userId: props.user.id,
withFiles: true,
limit: 15,
diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue
index e5a0f49e3d..8dbf90f344 100644
--- a/packages/frontend/src/pages/user/index.timeline.vue
+++ b/packages/frontend/src/pages/user/index.timeline.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index 1e9a860974..a6244e2a93 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -8,19 +8,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
<div>
<div v-if="user">
- <XHome v-if="tab === 'home'" :user="user"/>
- <MkSpacer v-else-if="tab === 'notes'" :contentMax="800" style="padding-top: 0">
- <XTimeline :user="user"/>
- </MkSpacer>
- <XActivity v-else-if="tab === 'activity'" :user="user"/>
- <XAchievements v-else-if="tab === 'achievements'" :user="user"/>
- <XReactions v-else-if="tab === 'reactions'" :user="user"/>
- <XClips v-else-if="tab === 'clips'" :user="user"/>
- <XLists v-else-if="tab === 'lists'" :user="user"/>
- <XPages v-else-if="tab === 'pages'" :user="user"/>
- <XFlashs v-else-if="tab === 'flashs'" :user="user"/>
- <XGallery v-else-if="tab === 'gallery'" :user="user"/>
- <XRaw v-else-if="tab === 'raw'" :user="user"/>
+ <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
+ <XHome v-if="tab === 'home'" key="home" :user="user"/>
+ <MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800" style="padding-top: 0">
+ <XTimeline :user="user"/>
+ </MkSpacer>
+ <XActivity v-else-if="tab === 'activity'" key="activity" :user="user"/>
+ <XAchievements v-else-if="tab === 'achievements'" key="achievements" :user="user"/>
+ <XReactions v-else-if="tab === 'reactions'" key="reactions" :user="user"/>
+ <XClips v-else-if="tab === 'clips'" key="clips" :user="user"/>
+ <XLists v-else-if="tab === 'lists'" key="lists" :user="user"/>
+ <XPages v-else-if="tab === 'pages'" key="pages" :user="user"/>
+ <XFlashs v-else-if="tab === 'flashs'" key="flashs" :user="user"/>
+ <XGallery v-else-if="tab === 'gallery'" key="gallery" :user="user"/>
+ <XRaw v-else-if="tab === 'raw'" key="raw" :user="user"/>
+ </MkHorizontalSwipe>
</div>
<MkError v-else-if="error" @retry="fetchUser()"/>
<MkLoading v-else/>
@@ -32,10 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
import { defineAsyncComponent, computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { acct as getAcct } from '@/filters/user.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
+import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XHome = defineAsyncComponent(() => import('./home.vue'));
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
@@ -57,13 +60,14 @@ const props = withDefaults(defineProps<{
});
const tab = ref(props.page);
+
const user = ref<null | Misskey.entities.UserDetailed>(null);
const error = ref<any>(null);
function fetchUser(): void {
if (props.acct == null) return;
user.value = null;
- os.api('users/show', Misskey.acct.parse(props.acct)).then(u => {
+ misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
user.value = u;
}).catch(err => {
error.value = err;
@@ -92,7 +96,7 @@ const headerTabs = computed(() => user.value ? [{
key: 'achievements',
title: i18n.ts.achievements,
icon: 'ti ti-medal',
-}] : []), ...($i && ($i.id === user.value.id)) || user.value.publicReactions ? [{
+}] : []), ...($i && ($i.id === user.value.id || $i.isAdmin || $i.isModerator)) || user.value.publicReactions ? [{
key: 'reactions',
title: i18n.ts.reaction,
icon: 'ti ti-mood-happy',
@@ -122,15 +126,18 @@ const headerTabs = computed(() => user.value ? [{
icon: 'ti ti-code',
}] : []);
-definePageMetadata(computed(() => user.value ? {
+definePageMetadata(() => ({
+ title: i18n.ts.user,
icon: 'ti ti-user',
- title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
- subtitle: `@${getAcct(user.value)}`,
- userName: user.value,
- avatar: user.value,
- path: `/@${user.value.username}`,
- share: {
- title: user.value.name,
- },
-} : null));
+ ...user.value ? {
+ title: user.value.name ? `${user.value.name} (@${user.value.username})` : `@${user.value.username}`,
+ subtitle: `@${getAcct(user.value)}`,
+ userName: user.value,
+ avatar: user.value,
+ path: `/@${user.value.username}`,
+ share: {
+ title: user.value.name,
+ },
+ } : {},
+}));
</script>
diff --git a/packages/frontend/src/pages/user/lists.vue b/packages/frontend/src/pages/user/lists.vue
index 4b9c5cbf8f..5e9b95eb74 100644
--- a/packages/frontend/src/pages/user/lists.vue
+++ b/packages/frontend/src/pages/user/lists.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/pages.vue b/packages/frontend/src/pages/user/pages.vue
index 94ec80d05e..6375bf7d74 100644
--- a/packages/frontend/src/pages/user/pages.vue
+++ b/packages/frontend/src/pages/user/pages.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/raw.vue b/packages/frontend/src/pages/user/raw.vue
index 0c0bfc29ca..dd57048409 100644
--- a/packages/frontend/src/pages/user/raw.vue
+++ b/packages/frontend/src/pages/user/raw.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue
index 916b6615d5..3671decc18 100644
--- a/packages/frontend/src/pages/user/reactions.vue
+++ b/packages/frontend/src/pages/user/reactions.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 3ad34355f5..89bb010dd6 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -39,7 +39,7 @@ import XTimeline from './welcome.timeline.vue';
import MarqueeText from '@/components/MkMarquee.vue';
import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
import misskeysvg from '/client-assets/misskey.svg';
-import * as os from '@/os.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
@@ -53,11 +53,11 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
return getProxiedImageUrl(instance.iconUrl, 'preview');
}
-os.api('meta', { detail: true }).then(_meta => {
+misskeyApi('meta', { detail: true }).then(_meta => {
meta.value = _meta;
});
-os.apiGet('federation/instances', {
+misskeyApiGet('federation/instances', {
sort: '+pubSub',
limit: 20,
}).then(_instances => {
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 61b86f993d..5c31259499 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -40,6 +40,7 @@ import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import { host, version } from '@/config.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
import MkAnimBg from '@/components/MkAnimBg.vue';
@@ -52,7 +53,7 @@ function submit() {
if (submitting.value) return;
submitting.value = true;
- os.api('admin/accounts/create', {
+ misskeyApi('admin/accounts/create', {
username: username.value,
password: password.value,
}).then(res => {
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 92be80228a..139b2e0a07 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkMediaList :mediaList="note.files"/>
</div>
<div v-if="note.poll">
- <MkPoll :note="note" :readOnly="true"/>
+ <MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
</div>
</div>
<MkReactionsViewer ref="reactionsViewer" :note="note"/>
@@ -32,14 +32,14 @@ import { onUpdated, ref, shallowRef } from 'vue';
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue';
-import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { getScrollContainer } from '@/scripts/scroll.js';
const notes = ref<Misskey.entities.Note[]>([]);
const isScrolling = ref(false);
const scrollEl = shallowRef<HTMLElement>();
-os.apiGet('notes/featured').then(_notes => {
+misskeyApiGet('notes/featured').then(_notes => {
notes.value = _notes;
});
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 7f0af1b83e..9ba6a5885e 100644
--- a/packages/frontend/src/pages/welcome.vue
+++ b/packages/frontend/src/pages/welcome.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,12 +16,12 @@ import * as Misskey from 'misskey-js';
import XSetup from './welcome.setup.vue';
import XEntrance from './welcome.entrance.a.vue';
import { instanceName } from '@/config.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
const meta = ref<Misskey.entities.MetaResponse | null>(null);
-os.api('meta', { detail: true }).then(res => {
+misskeyApi('meta', { detail: true }).then(res => {
meta.value = res;
});
@@ -29,8 +29,8 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
-definePageMetadata(computed(() => ({
+definePageMetadata(() => ({
title: instanceName,
icon: null,
-})));
+}));
</script>
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index b2254a0611..ac325e923f 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -8,11 +8,12 @@
import { onUnmounted, Ref, ref, watch } from 'vue';
import { BroadcastChannel } from 'broadcast-channel';
import { $i } from '@/account.js';
-import { api } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { get, set } from '@/scripts/idb-proxy.js';
import { defaultStore } from '@/store.js';
import { useStream } from '@/stream.js';
import { deepClone } from '@/scripts/clone.js';
+import { deepMerge } from '@/scripts/merge.js';
type StateDef = Record<string, {
where: 'account' | 'device' | 'deviceAccount';
@@ -80,6 +81,21 @@ export class Storage<T extends StateDef> {
this.loaded = this.ready.then(() => this.load());
}
+ private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+ }
+
+ private mergeState<X>(value: X, def: X): X {
+ if (this.isPureObject(value) && this.isPureObject(def)) {
+ const merged = deepMerge(value, def);
+
+ if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
+
+ return merged as X;
+ }
+ return value;
+ }
+
private async init(): Promise<void> {
await this.migrate();
@@ -89,11 +105,11 @@ export class Storage<T extends StateDef> {
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
- this.reactiveState[k].value = this.state[k] = deviceState[k];
+ this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
- this.reactiveState[k].value = this.state[k] = registryCache[k];
+ this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
- this.reactiveState[k].value = this.state[k] = deviceAccountState[k];
+ this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
} else {
this.reactiveState[k].value = this.state[k] = v.default;
if (_DEV_) console.log('Use default value', k, v.default);
@@ -134,7 +150,7 @@ export class Storage<T extends StateDef> {
window.setTimeout(async () => {
await defaultStore.ready;
- api('i/registry/get-all', { scope: ['client', this.key] })
+ misskeyApi('i/registry/get-all', { scope: ['client', this.key] })
.then(kvs => {
const cache: Partial<T> = {};
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
@@ -168,7 +184,7 @@ export class Storage<T extends StateDef> {
this.reactiveState[key].value = this.state[key] = rawValue;
return this.addIdbSetJob(async () => {
- if (_DEV_) console.log(`set ${key} start`);
+ if (_DEV_) console.log(`set ${String(key)} start`);
switch (this.def[key].where) {
case 'device': {
this.pizzaxChannel.postMessage({
@@ -199,7 +215,7 @@ export class Storage<T extends StateDef> {
const cache = await get(this.registryCacheKeyName) || {};
cache[key] = rawValue;
await set(this.registryCacheKeyName, cache);
- await api('i/registry/set', {
+ await misskeyApi('i/registry/set', {
scope: ['client', this.key],
key: key.toString(),
value: rawValue,
@@ -207,7 +223,7 @@ export class Storage<T extends StateDef> {
break;
}
}
- if (_DEV_) console.log(`set ${key} complete`);
+ if (_DEV_) console.log(`set ${String(key)} complete`);
});
}
@@ -223,9 +239,12 @@ export class Storage<T extends StateDef> {
/**
* 特定のキーの、簡易的なgetter/setterを作ります
- * 主にvue場で設定コントロールのmodelとして使う用
+ * 主にvue上で設定コントロールのmodelとして使う用
*/
- public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]) {
+ public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): {
+ get: () => T[K]['default'];
+ set: (value: T[K]['default']) => void;
+ } {
const valueRef = ref(this.state[key]);
const stop = watch(this.reactiveState[key], val => {
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index 5e49af4858..743cadc36a 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
-import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import { inputText } from '@/os.js';
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
@@ -19,19 +19,7 @@ export async function install(plugin: Plugin): Promise<void> {
plugin: plugin,
storageKey: 'plugins:' + plugin.id,
}), {
- in: (q): Promise<string> => {
- return new Promise(ok => {
- inputText({
- title: q,
- }).then(({ canceled, result: a }) => {
- if (canceled) {
- ok('');
- } else {
- ok(a);
- }
- });
- });
- },
+ in: aiScriptReadline,
out: (value): void => {
console.log(value);
},
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
deleted file mode 100644
index baee85866c..0000000000
--- a/packages/frontend/src/router.ts
+++ /dev/null
@@ -1,557 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { AsyncComponentLoader, defineAsyncComponent, inject } from 'vue';
-import { Router } from '@/nirax.js';
-import { $i, iAmModerator } from '@/account.js';
-import MkLoading from '@/pages/_loading_.vue';
-import MkError from '@/pages/_error_.vue';
-
-export const page = (loader: AsyncComponentLoader<any>) => 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')),
-}, {
- name: 'list',
- path: '/list/:listId',
- component: page(() => import('./pages/list.vue')),
-}, {
- path: '/clips/:clipId',
- component: page(() => import('./pages/clip.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: '/avatar-decoration',
- name: 'avatarDecoration',
- component: page(() => import('./pages/settings/avatar-decoration.vue')),
- }, {
- path: '/roles',
- name: 'roles',
- component: page(() => import('./pages/settings/roles.vue')),
- }, {
- path: '/privacy',
- name: 'privacy',
- component: page(() => import('./pages/settings/privacy.vue')),
- }, {
- path: '/emoji-picker',
- name: 'emojiPicker',
- component: page(() => import('./pages/settings/emoji-picker.vue')),
- }, {
- path: '/drive',
- name: 'drive',
- component: page(() => import('./pages/settings/drive.vue')),
- }, {
- path: '/drive/cleaner',
- name: 'drive',
- component: page(() => import('./pages/settings/drive-cleaner.vue')),
- }, {
- path: '/notifications',
- name: 'notifications',
- component: page(() => import('./pages/settings/notifications.vue')),
- }, {
- path: '/email',
- name: 'email',
- component: page(() => import('./pages/settings/email.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: '/mute-block',
- name: 'mute-block',
- component: page(() => import('./pages/settings/mute-block.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: '/migration',
- name: 'migration',
- component: page(() => import('./pages/settings/migration.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: '/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: '/invite',
- name: 'invite',
- component: page(() => import('./pages/invite.vue')),
-}, {
- path: '/ads',
- component: page(() => import('./pages/ads.vue')),
-}, {
- path: '/theme-editor',
- component: page(() => import('./pages/theme-editor.vue')),
- loginRequired: true,
-}, {
- path: '/roles/:role',
- component: page(() => import('./pages/role.vue')),
-}, {
- path: '/user-tags/:tag',
- component: page(() => import('./pages/user-tag.vue')),
-}, {
- path: '/explore',
- component: page(() => import('./pages/explore.vue')),
- hash: 'initialTab',
-}, {
- path: '/search',
- component: page(() => import('./pages/search.vue')),
- query: {
- q: 'query',
- channel: 'channel',
- type: 'type',
- origin: 'origin',
- },
-}, {
- 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: '/scratchpad',
- component: page(() => import('./pages/scratchpad.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: '/oauth/authorize',
- component: page(() => import('./pages/oauth.vue')),
-}, {
- 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: '/play/:id/edit',
- component: page(() => import('./pages/flash/flash-edit.vue')),
- loginRequired: true,
-}, {
- path: '/play/new',
- component: page(() => import('./pages/flash/flash-edit.vue')),
- loginRequired: true,
-}, {
- path: '/play/:id',
- component: page(() => import('./pages/flash/flash.vue')),
-}, {
- path: '/play',
- component: page(() => import('./pages/flash/flash-index.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: '/custom-emojis-manager',
- component: page(() => import('./pages/custom-emojis-manager.vue')),
-}, {
- path: '/avatar-decorations',
- name: 'avatarDecorations',
- component: page(() => import('./pages/avatar-decorations.vue')),
-}, {
- path: '/registry/keys/:domain/:path(*)?',
- component: page(() => import('./pages/registry.keys.vue')),
-}, {
- path: '/registry/value/:domain/:path(*)?',
- component: page(() => import('./pages/registry.value.vue')),
-}, {
- path: '/registry',
- component: page(() => import('./pages/registry.vue')),
-}, {
- path: '/install-extentions',
- component: page(() => import('./pages/install-extentions.vue')),
- loginRequired: true,
-}, {
- path: '/admin/user/:userId',
- component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.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/custom-emojis-manager.vue')),
- }, {
- path: '/avatar-decorations',
- name: 'avatarDecorations',
- component: page(() => import('./pages/avatar-decorations.vue')),
- }, {
- path: '/queue',
- name: 'queue',
- component: page(() => import('./pages/admin/queue.vue')),
- }, {
- path: '/files',
- name: 'files',
- component: page(() => import('./pages/admin/files.vue')),
- }, {
- path: '/federation',
- name: 'federation',
- component: page(() => import('./pages/admin/federation.vue')),
- }, {
- path: '/announcements',
- name: 'announcements',
- component: page(() => import('./pages/admin/announcements.vue')),
- }, {
- path: '/ads',
- name: 'ads',
- component: page(() => import('./pages/admin/ads.vue')),
- }, {
- path: '/roles/:id/edit',
- name: 'roles',
- component: page(() => import('./pages/admin/roles.edit.vue')),
- }, {
- path: '/roles/new',
- name: 'roles',
- component: page(() => import('./pages/admin/roles.edit.vue')),
- }, {
- path: '/roles/:id',
- name: 'roles',
- component: page(() => import('./pages/admin/roles.role.vue')),
- }, {
- path: '/roles',
- name: 'roles',
- component: page(() => import('./pages/admin/roles.vue')),
- }, {
- path: '/database',
- name: 'database',
- component: page(() => import('./pages/admin/database.vue')),
- }, {
- path: '/abuses',
- name: 'abuses',
- component: page(() => import('./pages/admin/abuses.vue')),
- }, {
- path: '/modlog',
- name: 'modlog',
- component: page(() => import('./pages/admin/modlog.vue')),
- }, {
- path: '/settings',
- name: 'settings',
- component: page(() => import('./pages/admin/settings.vue')),
- }, {
- path: '/branding',
- name: 'branding',
- component: page(() => import('./pages/admin/branding.vue')),
- }, {
- path: '/moderation',
- name: 'moderation',
- component: page(() => import('./pages/admin/moderation.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: '/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: '/external-services',
- name: 'external-services',
- component: page(() => import('./pages/admin/external-services.vue')),
- }, {
- path: '/other-settings',
- name: 'other-settings',
- component: page(() => import('./pages/admin/other-settings.vue')),
- }, {
- path: '/server-rules',
- name: 'server-rules',
- component: page(() => import('./pages/admin/server-rules.vue')),
- }, {
- path: '/invites',
- name: 'invites',
- component: page(() => import('./pages/admin/invites.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,
-}, {
- path: '/my/achievements',
- component: page(() => import('./pages/achievements.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/drive/file/:fileId',
- component: page(() => import('./pages/drive.file.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,
-}, {
- path: '/clicker',
- component: page(() => import('./pages/clicker.vue')),
- loginRequired: true,
-}, {
- path: '/timeline',
- component: page(() => import('./pages/timeline.vue')),
-}, {
- 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, !!$i, page(() => import('@/pages/not-found.vue')));
-
-window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
-
-mainRouter.addListener('push', ctx => {
- window.history.pushState({ key: ctx.key }, '', ctx.path);
-});
-
-window.addEventListener('popstate', (event) => {
- mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
-});
-
-export function useRouter(): Router {
- return inject<Router | null>('router', null) ?? mainRouter;
-}
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
new file mode 100644
index 0000000000..eaeeafd499
--- /dev/null
+++ b/packages/frontend/src/router/definition.ts
@@ -0,0 +1,598 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
+import type { RouteDef } from '@/nirax.js';
+import { IRouter, Router } from '@/nirax.js';
+import { $i, iAmModerator } from '@/account.js';
+import MkLoading from '@/pages/_loading_.vue';
+import MkError from '@/pages/_error_.vue';
+import { setMainRouter } from '@/router/main.js';
+
+const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
+ loader: loader,
+ loadingComponent: MkLoading,
+ errorComponent: MkError,
+});
+
+const routes: RouteDef[] = [{
+ 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')),
+}, {
+ name: 'list',
+ path: '/list/:listId',
+ component: page(() => import('@/pages/list.vue')),
+}, {
+ path: '/clips/:clipId',
+ component: page(() => import('@/pages/clip.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: '/avatar-decoration',
+ name: 'avatarDecoration',
+ component: page(() => import('@/pages/settings/avatar-decoration.vue')),
+ }, {
+ path: '/roles',
+ name: 'roles',
+ component: page(() => import('@/pages/settings/roles.vue')),
+ }, {
+ path: '/privacy',
+ name: 'privacy',
+ component: page(() => import('@/pages/settings/privacy.vue')),
+ }, {
+ path: '/emoji-picker',
+ name: 'emojiPicker',
+ component: page(() => import('@/pages/settings/emoji-picker.vue')),
+ }, {
+ path: '/drive',
+ name: 'drive',
+ component: page(() => import('@/pages/settings/drive.vue')),
+ }, {
+ path: '/drive/cleaner',
+ name: 'drive',
+ component: page(() => import('@/pages/settings/drive-cleaner.vue')),
+ }, {
+ path: '/notifications',
+ name: 'notifications',
+ component: page(() => import('@/pages/settings/notifications.vue')),
+ }, {
+ path: '/email',
+ name: 'email',
+ component: page(() => import('@/pages/settings/email.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: '/mute-block',
+ name: 'mute-block',
+ component: page(() => import('@/pages/settings/mute-block.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: '/migration',
+ name: 'migration',
+ component: page(() => import('@/pages/settings/migration.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: '/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: '/invite',
+ name: 'invite',
+ component: page(() => import('@/pages/invite.vue')),
+}, {
+ path: '/ads',
+ component: page(() => import('@/pages/ads.vue')),
+}, {
+ path: '/theme-editor',
+ component: page(() => import('@/pages/theme-editor.vue')),
+ loginRequired: true,
+}, {
+ path: '/roles/:role',
+ component: page(() => import('@/pages/role.vue')),
+}, {
+ path: '/user-tags/:tag',
+ component: page(() => import('@/pages/user-tag.vue')),
+}, {
+ path: '/explore',
+ component: page(() => import('@/pages/explore.vue')),
+ hash: 'initialTab',
+}, {
+ path: '/search',
+ component: page(() => import('@/pages/search.vue')),
+ query: {
+ q: 'query',
+ channel: 'channel',
+ type: 'type',
+ origin: 'origin',
+ },
+}, {
+ 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: '/scratchpad',
+ component: page(() => import('@/pages/scratchpad.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: '/oauth/authorize',
+ component: page(() => import('@/pages/oauth.vue')),
+}, {
+ 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: '/play/:id/edit',
+ component: page(() => import('@/pages/flash/flash-edit.vue')),
+ loginRequired: true,
+}, {
+ path: '/play/new',
+ component: page(() => import('@/pages/flash/flash-edit.vue')),
+ loginRequired: true,
+}, {
+ path: '/play/:id',
+ component: page(() => import('@/pages/flash/flash.vue')),
+}, {
+ path: '/play',
+ component: page(() => import('@/pages/flash/flash-index.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: '/custom-emojis-manager',
+ component: page(() => import('@/pages/custom-emojis-manager.vue')),
+}, {
+ path: '/avatar-decorations',
+ name: 'avatarDecorations',
+ component: page(() => import('@/pages/avatar-decorations.vue')),
+}, {
+ path: '/registry/keys/:domain/:path(*)?',
+ component: page(() => import('@/pages/registry.keys.vue')),
+}, {
+ path: '/registry/value/:domain/:path(*)?',
+ component: page(() => import('@/pages/registry.value.vue')),
+}, {
+ path: '/registry',
+ component: page(() => import('@/pages/registry.vue')),
+}, {
+ path: '/install-extentions',
+ redirect: '/install-extensions',
+ loginRequired: true,
+}, {
+ path: '/install-extensions',
+ component: page(() => import('@/pages/install-extensions.vue')),
+ loginRequired: true,
+}, {
+ path: '/admin/user/:userId',
+ component: iAmModerator ? page(() => import('@/pages/admin-user.vue')) : page(() => import('@/pages/not-found.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/custom-emojis-manager.vue')),
+ }, {
+ path: '/avatar-decorations',
+ name: 'avatarDecorations',
+ component: page(() => import('@/pages/avatar-decorations.vue')),
+ }, {
+ path: '/queue',
+ name: 'queue',
+ component: page(() => import('@/pages/admin/queue.vue')),
+ }, {
+ path: '/files',
+ name: 'files',
+ component: page(() => import('@/pages/admin/files.vue')),
+ }, {
+ path: '/federation',
+ name: 'federation',
+ component: page(() => import('@/pages/admin/federation.vue')),
+ }, {
+ path: '/announcements',
+ name: 'announcements',
+ component: page(() => import('@/pages/admin/announcements.vue')),
+ }, {
+ path: '/ads',
+ name: 'ads',
+ component: page(() => import('@/pages/admin/ads.vue')),
+ }, {
+ path: '/roles/:id/edit',
+ name: 'roles',
+ component: page(() => import('@/pages/admin/roles.edit.vue')),
+ }, {
+ path: '/roles/new',
+ name: 'roles',
+ component: page(() => import('@/pages/admin/roles.edit.vue')),
+ }, {
+ path: '/roles/:id',
+ name: 'roles',
+ component: page(() => import('@/pages/admin/roles.role.vue')),
+ }, {
+ path: '/roles',
+ name: 'roles',
+ component: page(() => import('@/pages/admin/roles.vue')),
+ }, {
+ path: '/database',
+ name: 'database',
+ component: page(() => import('@/pages/admin/database.vue')),
+ }, {
+ path: '/abuses',
+ name: 'abuses',
+ component: page(() => import('@/pages/admin/abuses.vue')),
+ }, {
+ path: '/modlog',
+ name: 'modlog',
+ component: page(() => import('@/pages/admin/modlog.vue')),
+ }, {
+ path: '/settings',
+ name: 'settings',
+ component: page(() => import('@/pages/admin/settings.vue')),
+ }, {
+ path: '/branding',
+ name: 'branding',
+ component: page(() => import('@/pages/admin/branding.vue')),
+ }, {
+ path: '/moderation',
+ name: 'moderation',
+ component: page(() => import('@/pages/admin/moderation.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: '/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: '/external-services',
+ name: 'external-services',
+ component: page(() => import('@/pages/admin/external-services.vue')),
+ }, {
+ path: '/other-settings',
+ name: 'other-settings',
+ component: page(() => import('@/pages/admin/other-settings.vue')),
+ }, {
+ path: '/server-rules',
+ name: 'server-rules',
+ component: page(() => import('@/pages/admin/server-rules.vue')),
+ }, {
+ path: '/invites',
+ name: 'invites',
+ component: page(() => import('@/pages/admin/invites.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,
+}, {
+ path: '/my/achievements',
+ component: page(() => import('@/pages/achievements.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/drive/file/:fileId',
+ component: page(() => import('@/pages/drive.file.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,
+}, {
+ path: '/clicker',
+ component: page(() => import('@/pages/clicker.vue')),
+ loginRequired: true,
+}, {
+ path: '/games',
+ component: page(() => import('@/pages/games.vue')),
+ loginRequired: false,
+}, {
+ path: '/bubble-game',
+ component: page(() => import('@/pages/drop-and-fusion.vue')),
+ loginRequired: true,
+}, {
+ path: '/reversi',
+ component: page(() => import('@/pages/reversi/index.vue')),
+ loginRequired: false,
+}, {
+ path: '/reversi/g/:gameId',
+ component: page(() => import('@/pages/reversi/game.vue')),
+ loginRequired: false,
+}, {
+ path: '/timeline',
+ component: page(() => import('@/pages/timeline.vue')),
+}, {
+ name: 'index',
+ path: '/',
+ component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
+ globalCacheKey: 'index',
+}, {
+ // テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする
+ path: '/redirect-test',
+ redirect: $i ? `@${$i.username}` : '/',
+ loginRequired: true,
+}, {
+ path: '/:(*)',
+ component: page(() => import('@/pages/not-found.vue')),
+}];
+
+function createRouterImpl(path: string): IRouter {
+ return new Router(routes, path, !!$i, page(() => import('@/pages/not-found.vue')));
+}
+
+/**
+ * {@link Router}による画面遷移を可能とするために{@link mainRouter}をセットアップする。
+ * また、{@link Router}のインスタンスを作成するためのファクトリも{@link provide}経由で公開する(`routerFactory`というキーで取得可能)
+ */
+export function setupRouter(app: App) {
+ app.provide('routerFactory', createRouterImpl);
+
+ const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
+
+ window.addEventListener('popstate', (event) => {
+ mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
+ });
+
+ mainRouter.addListener('push', ctx => {
+ window.history.pushState({ key: ctx.key }, '', ctx.path);
+ });
+
+ mainRouter.addListener('replace', ctx => {
+ window.history.replaceState({ key: ctx.key }, '', ctx.path);
+ });
+
+ mainRouter.init();
+
+ setMainRouter(mainRouter);
+}
diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts
new file mode 100644
index 0000000000..7a3fde131e
--- /dev/null
+++ b/packages/frontend/src/router/main.ts
@@ -0,0 +1,167 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ShallowRef } from 'vue';
+import { EventEmitter } from 'eventemitter3';
+import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js';
+
+function getMainRouter(): IRouter {
+ const router = mainRouterHolder;
+ if (!router) {
+ throw new Error('mainRouter is not found.');
+ }
+
+ return router;
+}
+
+/**
+ * メインルータを設定する。一度設定すると、それ以降は変更できない。
+ * {@link setupRouter}から呼び出されることのみを想定している。
+ */
+export function setMainRouter(router: IRouter) {
+ if (mainRouterHolder) {
+ throw new Error('mainRouter is already exists.');
+ }
+
+ mainRouterHolder = router;
+}
+
+/**
+ * {@link mainRouter}用のプロキシ実装。
+ * {@link mainRouter}は起動シーケンスの一部にて初期化されるため、僅かにundefinedになる期間がある。
+ * その僅かな期間のためだけに型をundefined込みにしたくないのでこのクラスを緩衝材として使用する。
+ */
+class MainRouterProxy implements IRouter {
+ private supplier: () => IRouter;
+
+ constructor(supplier: () => IRouter) {
+ this.supplier = supplier;
+ }
+
+ get current(): Resolved {
+ return this.supplier().current;
+ }
+
+ get currentRef(): ShallowRef<Resolved> {
+ return this.supplier().currentRef;
+ }
+
+ get currentRoute(): ShallowRef<RouteDef> {
+ return this.supplier().currentRoute;
+ }
+
+ get navHook(): ((path: string, flag?: any) => boolean) | null {
+ return this.supplier().navHook;
+ }
+
+ set navHook(value) {
+ this.supplier().navHook = value;
+ }
+
+ getCurrentKey(): string {
+ return this.supplier().getCurrentKey();
+ }
+
+ getCurrentPath(): any {
+ return this.supplier().getCurrentPath();
+ }
+
+ push(path: string, flag?: any): void {
+ this.supplier().push(path, flag);
+ }
+
+ replace(path: string, key?: string | null): void {
+ this.supplier().replace(path, key);
+ }
+
+ resolve(path: string): Resolved | null {
+ return this.supplier().resolve(path);
+ }
+
+ init(): void {
+ this.supplier().init();
+ }
+
+ eventNames(): Array<EventEmitter.EventNames<RouterEvent>> {
+ return this.supplier().eventNames();
+ }
+
+ listeners<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ ): Array<EventEmitter.EventListener<RouterEvent, T>> {
+ return this.supplier().listeners(event);
+ }
+
+ listenerCount(
+ event: EventEmitter.EventNames<RouterEvent>,
+ ): number {
+ return this.supplier().listenerCount(event);
+ }
+
+ emit<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ ...args: EventEmitter.EventArgs<RouterEvent, T>
+ ): boolean {
+ return this.supplier().emit(event, ...args);
+ }
+
+ on<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any,
+ ): this {
+ this.supplier().on(event, fn, context);
+ return this;
+ }
+
+ addListener<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any,
+ ): this {
+ this.supplier().addListener(event, fn, context);
+ return this;
+ }
+
+ once<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any,
+ ): this {
+ this.supplier().once(event, fn, context);
+ return this;
+ }
+
+ removeListener<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn?: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any,
+ once?: boolean,
+ ): this {
+ this.supplier().removeListener(event, fn, context, once);
+ return this;
+ }
+
+ off<T extends EventEmitter.EventNames<RouterEvent>>(
+ event: T,
+ fn?: EventEmitter.EventListener<RouterEvent, T>,
+ context?: any,
+ once?: boolean,
+ ): this {
+ this.supplier().off(event, fn, context, once);
+ return this;
+ }
+
+ removeAllListeners(
+ event?: EventEmitter.EventNames<RouterEvent>,
+ ): this {
+ this.supplier().removeAllListeners(event);
+ return this;
+ }
+}
+
+let mainRouterHolder: IRouter | null = null;
+
+export const mainRouter: IRouter = new MainRouterProxy(getMainRouter);
diff --git a/packages/frontend/src/router/supplier.ts b/packages/frontend/src/router/supplier.ts
new file mode 100644
index 0000000000..7da236f4e7
--- /dev/null
+++ b/packages/frontend/src/router/supplier.ts
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { inject } from 'vue';
+import { IRouter, Router } from '@/nirax.js';
+import { mainRouter } from '@/router/main.js';
+
+/**
+ * メインの{@link Router}を取得する。
+ * あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない)
+ */
+export function useRouter(): IRouter {
+ return inject<Router | null>('router', null) ?? mainRouter;
+}
+
+/**
+ * 任意の{@link Router}を取得するためのファクトリを取得する。
+ * あらかじめ{@link setupRouter}を実行しておく必要がある。
+ */
+export function useRouterFactory(): (path: string) => IRouter {
+ const factory = inject<(path: string) => IRouter>('routerFactory');
+ if (!factory) {
+ console.error('routerFactory is not defined.');
+ throw new Error('routerFactory is not defined.');
+ }
+
+ return factory;
+}
diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts
index e7585fcf81..f5d0ab559f 100644
--- a/packages/frontend/src/scripts/achievements.ts
+++ b/packages/frontend/src/scripts/achievements.ts
@@ -1,9 +1,9 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i } from '@/account.js';
export const ACHIEVEMENT_TYPES = [
@@ -83,6 +83,8 @@ export const ACHIEVEMENT_TYPES = [
'brainDiver',
'smashTestNotificationButton',
'tutorialCompleted',
+ 'bubbleGameExplodingHead',
+ 'bubbleGameDoubleExplodingHead',
] as const;
export const ACHIEVEMENT_BADGES = {
@@ -466,6 +468,16 @@ export const ACHIEVEMENT_BADGES = {
bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))',
frame: 'bronze',
},
+ 'bubbleGameExplodingHead': {
+ img: '/fluent-emoji/1f92f.png',
+ bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
+ frame: 'bronze',
+ },
+ 'bubbleGameDoubleExplodingHead': {
+ img: '/fluent-emoji/1f92f.png',
+ bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))',
+ frame: 'silver',
+ },
/* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107>
} as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], {
img: string;
@@ -489,7 +501,7 @@ export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) {
window.setTimeout(() => {
claimingQueue.delete(type);
}, 500);
- os.api('i/claim-achievement', { name: type });
+ misskeyApi('i/claim-achievement', { name: type });
}
if (_DEV_) {
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts
index 038ae23109..98a0c61752 100644
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ b/packages/frontend/src/scripts/aiscript/api.ts
@@ -1,16 +1,27 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { utils, values } from '@syuilo/aiscript';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
import { url, lang } from '@/config.js';
import { nyaize } from '@/scripts/nyaize.js';
+export function aiScriptReadline(q: string): Promise<string> {
+ return new Promise(ok => {
+ os.inputText({
+ title: q,
+ }).then(({ result: a }) => {
+ ok(a ?? '');
+ });
+ });
+}
+
export function createAiScriptEnv(opts) {
return {
USER_ID: $i ? values.STR($i.id) : values.NULL,
@@ -44,7 +55,7 @@ export function createAiScriptEnv(opts) {
if (typeof token.value !== 'string') throw new Error('invalid token');
}
const actualToken: string|null = token?.value ?? opts.token ?? null;
- return os.api(ep.value, utils.valToJs(param), actualToken).then(res => {
+ return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));
diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts
index 08ba1e6d9b..f2493264d3 100644
--- a/packages/frontend/src/scripts/aiscript/ui.ts
+++ b/packages/frontend/src/scripts/aiscript/ui.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -218,7 +218,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
};
}
-function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'type'> {
+function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> {
utils.assertObject(def);
const text = def.value.get('text');
@@ -241,7 +241,7 @@ function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'typ
color: color?.value,
font: font?.value,
onClickEv: (evId: string) => {
- if (onClickEv) call(onClickEv, values.STR(evId));
+ if (onClickEv) call(onClickEv, [values.STR(evId)]);
},
};
}
diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts
index 082703a450..b3d76e149f 100644
--- a/packages/frontend/src/scripts/array.ts
+++ b/packages/frontend/src/scripts/array.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts
index 2a9a42ace5..fe515d81a1 100644
--- a/packages/frontend/src/scripts/autocomplete.ts
+++ b/packages/frontend/src/scripts/autocomplete.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -8,18 +8,18 @@ import getCaretCoordinates from 'textarea-caret';
import { toASCII } from 'punycode/';
import { popup } from '@/os.js';
-export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag';
+export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
export class Autocomplete {
private suggestion: {
x: Ref<number>;
y: Ref<number>;
- q: Ref<string | null>;
+ q: Ref<any>;
close: () => void;
} | null;
private textarea: HTMLInputElement | HTMLTextAreaElement;
private currentType: string;
- private textRef: Ref<string>;
+ private textRef: Ref<string | number | null>;
private opening: boolean;
private onlyType: SuggestionType[];
@@ -38,7 +38,7 @@ export class Autocomplete {
/**
* 対象のテキストエリアを与えてインスタンスを初期化します。
*/
- constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) {
+ constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string | number | null>, onlyType?: SuggestionType[]) {
//#region BIND
this.onInput = this.onInput.bind(this);
this.complete = this.complete.bind(this);
@@ -49,7 +49,7 @@ export class Autocomplete {
this.textarea = textarea;
this.textRef = textRef;
this.opening = false;
- this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag'];
+ this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam'];
this.attach();
}
@@ -80,6 +80,7 @@ export class Autocomplete {
const hashtagIndex = text.lastIndexOf('#');
const emojiIndex = text.lastIndexOf(':');
const mfmTagIndex = text.lastIndexOf('$');
+ const mfmParamIndex = text.lastIndexOf('.');
const max = Math.max(
mentionIndex,
@@ -94,7 +95,8 @@ export class Autocomplete {
const isMention = mentionIndex !== -1;
const isHashtag = hashtagIndex !== -1;
- const isMfmTag = mfmTagIndex !== -1;
+ const isMfmParam = mfmParamIndex !== -1 && text.split(/\$\[[a-zA-Z]+/).pop()?.includes('.');
+ const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
let opened = false;
@@ -134,6 +136,17 @@ export class Autocomplete {
}
}
+ if (isMfmParam && !opened && this.onlyType.includes('mfmParam')) {
+ const mfmParam = text.substring(mfmParamIndex + 1);
+ if (!mfmParam.includes(' ')) {
+ this.open('mfmParam', {
+ tag: text.substring(mfmTagIndex + 2, mfmParamIndex),
+ params: mfmParam.split(','),
+ });
+ opened = true;
+ }
+ }
+
if (!opened) {
this.close();
}
@@ -142,7 +155,7 @@ export class Autocomplete {
/**
* サジェストを提示します。
*/
- private async open(type: string, q: string | null) {
+ private async open(type: string, q: any) {
if (type !== this.currentType) {
this.close();
}
@@ -280,6 +293,22 @@ export class Autocomplete {
const pos = trimmedBefore.length + (value.length + 3);
this.textarea.setSelectionRange(pos, pos);
});
+ } else if (type === 'mfmParam') {
+ const source = this.text;
+
+ const before = source.substring(0, caret);
+ const trimmedBefore = before.substring(0, before.lastIndexOf('.'));
+ const after = source.substring(caret);
+
+ // 挿入
+ this.text = `${trimmedBefore}.${value}${after}`;
+
+ // キャレットを戻す
+ nextTick(() => {
+ this.textarea.focus();
+ const pos = trimmedBefore.length + (value.length + 1);
+ this.textarea.setSelectionRange(pos, pos);
+ });
}
}
}
diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts
index 12347cf4b1..0fbdf34d5d 100644
--- a/packages/frontend/src/scripts/cache.ts
+++ b/packages/frontend/src/scripts/cache.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/chart-legend.ts b/packages/frontend/src/scripts/chart-legend.ts
index e91908e0cb..2d534f60c1 100644
--- a/packages/frontend/src/scripts/chart-legend.ts
+++ b/packages/frontend/src/scripts/chart-legend.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/chart-vline.ts b/packages/frontend/src/scripts/chart-vline.ts
index 336ec6cfbb..24e41245e7 100644
--- a/packages/frontend/src/scripts/chart-vline.ts
+++ b/packages/frontend/src/scripts/chart-vline.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts
new file mode 100644
index 0000000000..c9d2a5bfc6
--- /dev/null
+++ b/packages/frontend/src/scripts/check-reaction-permissions.ts
@@ -0,0 +1,8 @@
+import * as Misskey from 'misskey-js';
+
+export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean {
+ const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
+ return !(emoji.localOnly && note.user.host !== me.host)
+ && !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
+ && (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id)));
+}
diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts
index 5ac19c8d5b..67e896b4b9 100644
--- a/packages/frontend/src/scripts/check-word-mute.ts
+++ b/packages/frontend/src/scripts/check-word-mute.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts
index 5ad076e5ef..f9c4bc1829 100644
--- a/packages/frontend/src/scripts/clicker-game.ts
+++ b/packages/frontend/src/scripts/clicker-game.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref, computed } from 'vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
type SaveData = {
gameVersion: number;
@@ -23,7 +23,7 @@ let prev = '';
export async function load() {
try {
- saveData.value = await os.api('i/registry/get', {
+ saveData.value = await misskeyApi('i/registry/get', {
scope: ['clickerGame'],
key: 'saveData',
});
@@ -63,7 +63,7 @@ export async function save() {
const current = JSON.stringify(saveData.value);
if (current === prev) return;
- await os.api('i/registry/set', {
+ await misskeyApi('i/registry/set', {
scope: ['clickerGame'],
key: 'saveData',
value: saveData.value,
diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts
index 96b53684f3..ea8eea14b5 100644
--- a/packages/frontend/src/scripts/clone.ts
+++ b/packages/frontend/src/scripts/clone.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -8,15 +8,15 @@
// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
-type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
+export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[];
export function deepClone<T extends Cloneable>(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<string, Cloneable>;
+ const obj = {} as Record<string | number | symbol, Cloneable>;
for (const [k, v] of Object.entries(x)) {
- obj[k] = deepClone(v);
+ obj[k] = v === undefined ? undefined : deepClone(v);
}
return obj as T;
} else {
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index 957669122e..2733897bab 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -1,10 +1,51 @@
-import { setWasm, setCDN, Highlighter, getHighlighter as _getHighlighter } from 'shiki';
-
-setWasm('/assets/shiki/dist/onig.wasm');
-setCDN('/assets/shiki/');
+import { bundledThemesInfo } from 'shiki';
+import { getHighlighterCore, loadWasm } from 'shiki/core';
+import darkPlus from 'shiki/themes/dark-plus.mjs';
+import { unique } from './array.js';
+import { deepClone } from './clone.js';
+import { deepMerge } from './merge.js';
+import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki';
+import { ColdDeviceStorage } from '@/store.js';
+import lightTheme from '@/themes/_light.json5';
+import darkTheme from '@/themes/_dark.json5';
let _highlighter: Highlighter | null = null;
+export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
+export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
+export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
+ const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme'));
+
+ if (theme.base) {
+ const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
+ if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
+ }
+
+ if (theme.codeHighlighter) {
+ let _res: ThemeRegistration = {};
+ if (theme.codeHighlighter.base === '_none_') {
+ _res = deepClone(theme.codeHighlighter.overrides);
+ } else {
+ const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
+ _res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
+ }
+ if (_res.name == null) {
+ _res.name = theme.id;
+ }
+ _res.type = mode;
+
+ if (getName) {
+ return _res.name;
+ }
+ return _res;
+ }
+
+ if (getName) {
+ return 'dark-plus';
+ }
+ return darkPlus;
+}
+
export async function getHighlighter(): Promise<Highlighter> {
if (!_highlighter) {
return await initHighlighter();
@@ -13,16 +54,36 @@ export async function getHighlighter(): Promise<Highlighter> {
}
export async function initHighlighter() {
- const highlighter = await _getHighlighter({
- theme: 'dark-plus',
- langs: ['js'],
+ const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
+
+ await loadWasm(import('shiki/onig.wasm?init'));
+
+ // テーマの重複を消す
+ const themes = unique([
+ darkPlus,
+ ...(await Promise.all([getTheme('light'), getTheme('dark')])),
+ ]);
+
+ const highlighter = await getHighlighterCore({
+ themes,
+ langs: [
+ import('shiki/langs/javascript.mjs'),
+ aiScriptGrammar.default as unknown as LanguageRegistration,
+ ],
+ });
+
+ ColdDeviceStorage.watch('lightTheme', async () => {
+ const newTheme = await getTheme('light');
+ if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+ highlighter.loadTheme(newTheme);
+ }
});
- await highlighter.loadLanguage({
- path: 'languages/aiscript.tmLanguage.json',
- id: 'aiscript',
- scopeName: 'source.aiscript',
- aliases: ['is', 'ais'],
+ ColdDeviceStorage.watch('darkTheme', async () => {
+ const newTheme = await getTheme('dark');
+ if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+ highlighter.loadTheme(newTheme);
+ }
});
_highlighter = highlighter;
diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend/src/scripts/collapsed.ts
index 57e6ecf5b5..237bd37c7a 100644
--- a/packages/frontend/src/scripts/collapsed.ts
+++ b/packages/frontend/src/scripts/collapsed.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/collect-page-vars.ts b/packages/frontend/src/scripts/collect-page-vars.ts
index 79356e60eb..5096c0669e 100644
--- a/packages/frontend/src/scripts/collect-page-vars.ts
+++ b/packages/frontend/src/scripts/collect-page-vars.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/color.ts b/packages/frontend/src/scripts/color.ts
index 25ef41d9b7..a11255ffd1 100644
--- a/packages/frontend/src/scripts/color.ts
+++ b/packages/frontend/src/scripts/color.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/confetti.ts b/packages/frontend/src/scripts/confetti.ts
index b394ba3e2a..8e53a6ceeb 100644
--- a/packages/frontend/src/scripts/confetti.ts
+++ b/packages/frontend/src/scripts/confetti.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/contains.ts b/packages/frontend/src/scripts/contains.ts
index b50ce4128c..6137c06e85 100644
--- a/packages/frontend/src/scripts/contains.ts
+++ b/packages/frontend/src/scripts/contains.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts
index 3884d4a20a..216c0464b3 100644
--- a/packages/frontend/src/scripts/copy-to-clipboard.ts
+++ b/packages/frontend/src/scripts/copy-to-clipboard.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/scripts/device-kind.ts
index 3843052a24..7c33f8ccee 100644
--- a/packages/frontend/src/scripts/device-kind.ts
+++ b/packages/frontend/src/scripts/device-kind.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -11,6 +11,13 @@ 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);
+const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
+// navigator.platform may be deprecated but this check is still required
+const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1;
+const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1;
+
+export const isFullscreenNotSupported = isIPhone || isIos;
+
export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind
: isSmartphone ? 'smartphone'
: isTablet ? 'tablet'
diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts
index 46a13462a1..a01540a3e4 100644
--- a/packages/frontend/src/scripts/emoji-base.ts
+++ b/packages/frontend/src/scripts/emoji-base.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts
index f87c3f6fb2..14b5cbf35e 100644
--- a/packages/frontend/src/scripts/emoji-picker.ts
+++ b/packages/frontend/src/scripts/emoji-picker.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts
index 8885bf4b7f..54d45e025f 100644
--- a/packages/frontend/src/scripts/emojilist.ts
+++ b/packages/frontend/src/scripts/emojilist.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -36,7 +36,8 @@ for (let i = 0; i < emojilist.length; i++) {
export const emojiCharByCategory = _charGroupByCategory;
export function getEmojiName(char: string): string | null {
- const idx = _indexByChar.get(char);
+ // Colorize it because emojilist.json assumes that
+ const idx = _indexByChar.get(colorizeEmoji(char));
if (idx == null) {
return null;
} else {
@@ -44,6 +45,10 @@ export function getEmojiName(char: string): string | null {
}
}
+export function colorizeEmoji(char: string) {
+ return char.length === 1 ? `${char}\uFE0F` : char;
+}
+
export interface CustomEmojiFolderTree {
value: string;
category: string;
diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts
index 57b296ab2a..992f6e9a16 100644
--- a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts
+++ b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/extract-mentions.ts b/packages/frontend/src/scripts/extract-mentions.ts
index 74ce45d324..d518562053 100644
--- a/packages/frontend/src/scripts/extract-mentions.ts
+++ b/packages/frontend/src/scripts/extract-mentions.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/extract-url-from-mfm.ts b/packages/frontend/src/scripts/extract-url-from-mfm.ts
index c1ed9338f8..d5654ba850 100644
--- a/packages/frontend/src/scripts/extract-url-from-mfm.ts
+++ b/packages/frontend/src/scripts/extract-url-from-mfm.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts
index 6a31ebd431..ea6ee61c88 100644
--- a/packages/frontend/src/scripts/focus.ts
+++ b/packages/frontend/src/scripts/focus.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts
index 222fd9b0b7..26a027f461 100644
--- a/packages/frontend/src/scripts/form.ts
+++ b/packages/frontend/src/scripts/form.ts
@@ -1,9 +1,13 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-type EnumItem = string | {label: string; value: string;};
+type EnumItem = string | {
+ label: string;
+ value: string;
+};
+
export type FormItem = {
label?: string;
type: 'string';
@@ -38,14 +42,21 @@ export type FormItem = {
}[];
} | {
label?: string;
+ type: 'range';
+ default: number | null;
+ step: number;
+ min: number;
+ max: number;
+} | {
+ label?: string;
type: 'object';
default: Record<string, unknown> | null;
- hidden: true;
+ hidden: boolean;
} | {
label?: string;
type: 'array';
default: unknown[] | null;
- hidden: true;
+ hidden: boolean;
};
export type Form = Record<string, FormItem>;
@@ -55,6 +66,7 @@ type GetItemType<Item extends FormItem> =
Item['type'] extends 'number' ? number :
Item['type'] extends 'boolean' ? boolean :
Item['type'] extends 'radio' ? unknown :
+ Item['type'] extends 'range' ? number :
Item['type'] extends 'enum' ? string :
Item['type'] extends 'array' ? unknown[] :
Item['type'] extends 'object' ? Record<string, unknown>
diff --git a/packages/frontend/src/scripts/format-time-string.ts b/packages/frontend/src/scripts/format-time-string.ts
index 918996dd10..35ad77d982 100644
--- a/packages/frontend/src/scripts/format-time-string.ts
+++ b/packages/frontend/src/scripts/format-time-string.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts
index 54654980f2..60884d08d3 100644
--- a/packages/frontend/src/scripts/gen-search-query.ts
+++ b/packages/frontend/src/scripts/gen-search-query.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -18,7 +18,7 @@ export async function genSearchQuery(v: any, q: string) {
host = at;
}
} else {
- const user = await v.os.api('users/show', Misskey.acct.parse(at)).catch(x => null);
+ const user = await v.api('users/show', Misskey.acct.parse(at)).catch(x => null);
if (user) {
userId = user.id;
} else {
diff --git a/packages/frontend/src/scripts/get-account-from-id.ts b/packages/frontend/src/scripts/get-account-from-id.ts
index 346d283572..40afa10f2d 100644
--- a/packages/frontend/src/scripts/get-account-from-id.ts
+++ b/packages/frontend/src/scripts/get-account-from-id.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts
index f8496f0711..7aca5f83b2 100644
--- a/packages/frontend/src/scripts/get-drive-file-menu.ts
+++ b/packages/frontend/src/scripts/get-drive-file-menu.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -8,6 +8,7 @@ import { defineAsyncComponent } from 'vue';
import { i18n } from '@/i18n.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { MenuItem } from '@/types/menu.js';
import { defaultStore } from '@/store.js';
@@ -18,7 +19,7 @@ function rename(file: Misskey.entities.DriveFile) {
default: file.name,
}).then(({ canceled, result: name }) => {
if (canceled) return;
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
name: name,
});
@@ -31,7 +32,7 @@ function describe(file: Misskey.entities.DriveFile) {
file: file,
}, {
done: caption => {
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
comment: caption.length === 0 ? null : caption,
});
@@ -40,7 +41,7 @@ function describe(file: Misskey.entities.DriveFile) {
}
function toggleSensitive(file: Misskey.entities.DriveFile) {
- os.api('drive/files/update', {
+ misskeyApi('drive/files/update', {
fileId: file.id,
isSensitive: !file.isSensitive,
}).catch(err => {
@@ -65,11 +66,11 @@ function addApp() {
async function deleteFile(file: Misskey.entities.DriveFile) {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('driveFileDeleteConfirm', { name: file.name }),
+ text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }),
});
if (canceled) return;
- os.api('drive/files/delete', {
+ misskeyApi('drive/files/delete', {
fileId: file.id,
});
}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 7130e69279..b273bd36f3 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -1,15 +1,16 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { defineAsyncComponent, Ref } from 'vue';
+import { defineAsyncComponent, Ref, ShallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { claimAchievement } from './achievements.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js';
import { defaultStore, noteActions } from '@/store.js';
@@ -35,18 +36,18 @@ export async function getNoteClipMenu(props: {
const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
const clips = await clipsCache.fetch();
- return [...clips.map(clip => ({
+ const menu: MenuItem[] = [...clips.map(clip => ({
text: clip.name,
action: () => {
claimAchievement('noteClipped1');
os.promiseDialog(
- os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
+ misskeyApi('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 }),
+ text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }),
});
if (!confirm.canceled) {
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
@@ -92,6 +93,8 @@ export async function getNoteClipMenu(props: {
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
},
}];
+
+ return menu;
}
export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem {
@@ -99,10 +102,13 @@ export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): Men
icon: 'ti ti-exclamation-circle',
text,
action: (): void => {
- const u = note.url ?? note.uri ?? `${url}/notes/${note.id}`;
+ const localUrl = `${url}/notes/${note.id}`;
+ let noteInfo = '';
+ if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`;
+ noteInfo += `Local Note: ${localUrl}\n`;
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: note.user,
- initialComment: `Note: ${u}\n-----\n`,
+ initialComment: `${noteInfo}-----\n`,
}, {}, 'closed');
},
};
@@ -121,7 +127,6 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string):
export function getNoteMenu(props: {
note: Misskey.entities.Note;
- menuButton: Ref<HTMLElement>;
translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
translating: Ref<boolean>;
isDeleted: Ref<boolean>;
@@ -145,7 +150,7 @@ export function getNoteMenu(props: {
}).then(({ canceled }) => {
if (canceled) return;
- os.api('notes/delete', {
+ misskeyApi('notes/delete', {
noteId: appearNote.id,
});
@@ -162,7 +167,7 @@ export function getNoteMenu(props: {
}).then(({ canceled }) => {
if (canceled) return;
- os.api('notes/delete', {
+ misskeyApi('notes/delete', {
noteId: appearNote.id,
});
@@ -230,7 +235,7 @@ export function getNoteMenu(props: {
function share(): void {
navigator.share({
- title: i18n.t('noteOf', { user: appearNote.user.name }),
+ title: i18n.tsx.noteOf({ user: appearNote.user.name }),
text: appearNote.text,
url: `${url}/notes/${appearNote.id}`,
});
@@ -243,7 +248,7 @@ export function getNoteMenu(props: {
async function translate(): Promise<void> {
if (props.translation.value != null) return;
props.translating.value = true;
- const res = await os.api('notes/translate', {
+ const res = await misskeyApi('notes/translate', {
noteId: appearNote.id,
targetLang: miLocalStorage.getItem('lang') ?? navigator.language,
});
@@ -253,7 +258,7 @@ export function getNoteMenu(props: {
let menu: MenuItem[];
if ($i) {
- const statePromise = os.api('notes/state', {
+ const statePromise = misskeyApi('notes/state', {
noteId: appearNote.id,
});
@@ -330,7 +335,7 @@ export function getNoteMenu(props: {
icon: 'ti ti-user',
text: i18n.ts.user,
children: async () => {
- const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId });
+ const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId });
const { menu, cleanup } = getUserMenu(user);
cleanups.push(cleanup);
return menu;
@@ -352,6 +357,42 @@ export function getNoteMenu(props: {
]
: []
),
+ ...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [
+ { type: 'divider' },
+ {
+ type: 'parent' as const,
+ icon: 'ti ti-device-tv',
+ text: i18n.ts.channel,
+ children: async () => {
+ const channelChildMenu = [] as MenuItem[];
+
+ const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id });
+
+ if (channel.pinnedNoteIds.includes(appearNote.id)) {
+ channelChildMenu.push({
+ icon: 'ti ti-pinned-off',
+ text: i18n.ts.unpin,
+ action: () => os.apiWithDialog('channels/update', {
+ channelId: appearNote.channel!.id,
+ pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id),
+ }),
+ });
+ } else {
+ channelChildMenu.push({
+ icon: 'ti ti-pin',
+ text: i18n.ts.pin,
+ action: () => os.apiWithDialog('channels/update', {
+ channelId: appearNote.channel!.id,
+ pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id],
+ }),
+ });
+ }
+ return channelChildMenu;
+ },
+ },
+ ]
+ : []
+ ),
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
{ type: 'divider' },
appearNote.userId === $i.id ? {
@@ -389,7 +430,7 @@ export function getNoteMenu(props: {
}
if (noteActions.length > 0) {
- menu = menu.concat([{ type: "divider" }, ...noteActions.map(action => ({
+ menu = menu.concat([{ type: 'divider' }, ...noteActions.map(action => ({
icon: 'ti ti-plug',
text: action.title,
action: () => {
@@ -399,7 +440,7 @@ export function getNoteMenu(props: {
}
if (defaultStore.state.devMode) {
- menu = menu.concat([{ type: "divider" }, {
+ menu = menu.concat([{ type: 'divider' }, {
icon: 'ti ti-id',
text: i18n.ts.copyNoteId,
action: () => {
@@ -434,7 +475,7 @@ function smallerVisibility(a: Visibility | string, b: Visibility | string): Visi
export function getRenoteMenu(props: {
note: Misskey.entities.Note;
- renoteButton: Ref<HTMLElement>;
+ renoteButton: ShallowRef<HTMLElement | undefined>;
mock?: boolean;
}) {
const isRenote = (
@@ -454,7 +495,7 @@ export function getRenoteMenu(props: {
text: i18n.ts.inChannelRenote,
icon: 'ti ti-repeat',
action: () => {
- const el = props.renoteButton.value as HTMLElement | null | undefined;
+ const el = props.renoteButton.value;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
@@ -463,7 +504,7 @@ export function getRenoteMenu(props: {
}
if (!props.mock) {
- os.api('notes/create', {
+ misskeyApi('notes/create', {
renoteId: appearNote.id,
channelId: appearNote.channelId,
}).then(() => {
@@ -490,7 +531,7 @@ export function getRenoteMenu(props: {
text: i18n.ts.renote,
icon: 'ti ti-repeat',
action: () => {
- const el = props.renoteButton.value as HTMLElement | null | undefined;
+ const el = props.renoteButton.value;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
@@ -508,7 +549,7 @@ export function getRenoteMenu(props: {
}
if (!props.mock) {
- os.api('notes/create', {
+ misskeyApi('notes/create', {
localOnly,
visibility,
renoteId: appearNote.id,
@@ -530,7 +571,7 @@ export function getRenoteMenu(props: {
const renoteItems = [
...normalRenoteItems,
- ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] : [],
+ ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [],
...channelRenoteItems,
];
diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts
index 1fd9f04d46..6fd9947ac1 100644
--- a/packages/frontend/src/scripts/get-note-summary.ts
+++ b/packages/frontend/src/scripts/get-note-summary.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -10,7 +10,11 @@ import { i18n } from '@/i18n.js';
* 投稿を表す文字列を取得します。
* @param {*} note (packされた)投稿
*/
-export const getNoteSummary = (note: Misskey.entities.Note): string => {
+export const getNoteSummary = (note?: Misskey.entities.Note | null): string => {
+ if (note == null) {
+ return '';
+ }
+
if (note.deletedAt) {
return `(${i18n.ts.deletedNote})`;
}
@@ -30,7 +34,7 @@ export const getNoteSummary = (note: Misskey.entities.Note): string => {
// ファイルが添付されているとき
if ((note.files || []).length !== 0) {
- summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`;
+ summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`;
}
// 投票が添付されているとき
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 6e5c689d97..c14f75f382 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -10,13 +10,14 @@ import { i18n } from '@/i18n.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { host, url } from '@/config.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore, userActions } from '@/store.js';
import { $i, iAmModerator } from '@/account.js';
-import { mainRouter } from '@/router.js';
-import { Router } from '@/nirax.js';
+import { IRouter } from '@/nirax.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
+import { mainRouter } from '@/router/main.js';
-export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) {
+export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
const meId = $i ? $i.id : null;
const cleanups = [] as (() => void)[];
@@ -131,7 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
}
async function editMemo(): Promise<void> {
- const userDetailed = await os.api('users/show', {
+ const userDetailed = await misskeyApi('users/show', {
userId: user.id,
});
const { canceled, result } = await os.form(i18n.ts.editMemo, {
@@ -169,7 +170,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
action: () => {
copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
},
- }, {
+ }, ...(user.host != null && user.url != null ? [{
+ icon: 'ti ti-external-link',
+ text: i18n.ts.showOnRemote,
+ action: () => {
+ if (user.url == null) return;
+ window.open(user.url, '_blank', 'noopener');
+ },
+ }] : []), {
icon: 'ti ti-share',
text: i18n.ts.copyProfileUrl,
action: () => {
diff --git a/packages/frontend/src/scripts/get-user-name.ts b/packages/frontend/src/scripts/get-user-name.ts
index 3ae80d7fc3..56e91abba0 100644
--- a/packages/frontend/src/scripts/get-user-name.ts
+++ b/packages/frontend/src/scripts/get-user-name.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts
index 48c80c066b..0600bff893 100644
--- a/packages/frontend/src/scripts/hotkey.ts
+++ b/packages/frontend/src/scripts/hotkey.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts
index 8e5f17f38a..c2f44a33cc 100644
--- a/packages/frontend/src/scripts/i18n.ts
+++ b/packages/frontend/src/scripts/i18n.ts
@@ -1,34 +1,294 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import type { ILocale, ParameterizedString } from '../../../../locales/index.js';
-export class I18n<T extends Record<string, any>> {
- public ts: T;
+type FlattenKeys<T extends ILocale, TPrediction> = keyof {
+ [K in keyof T as T[K] extends ILocale
+ ? FlattenKeys<T[K], TPrediction> extends infer C extends string
+ ? `${K & string}.${C}`
+ : never
+ : T[K] extends TPrediction
+ ? K
+ : never]: T[K];
+};
- constructor(locale: T) {
- this.ts = locale;
+type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString>> = TKey extends `${infer K}.${infer C}`
+ // @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。
+ ? ParametersOf<T[K], C>
+ : TKey extends keyof T
+ ? T[TKey] extends ParameterizedString<infer P>
+ ? P
+ : never
+ : never;
+type Tsx<T extends ILocale> = {
+ readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P>
+ ? (arg: { readonly [_ in P]: string | number }) => string
+ // @ts-expect-error -- 証明省略
+ : Tsx<T[K]>;
+};
+
+export class I18n<T extends ILocale> {
+ private tsxCache?: Tsx<T>;
+
+ constructor(public locale: T) {
//#region BIND
this.t = this.t.bind(this);
//#endregion
}
- // string にしているのは、ドット区切りでのパス指定を許可するため
- // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
- public t(key: string, args?: Record<string, string | number>): string {
- try {
- let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string;
+ public get ts(): T {
+ if (_DEV_) {
+ class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
+ get(target: TTarget, p: string | symbol): unknown {
+ const value = target[p as keyof TTarget];
+
+ if (typeof value === 'object') {
+ return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>());
+ }
+
+ if (typeof value === 'string') {
+ const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter);
+
+ if (parameters.length) {
+ console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`);
+ }
+
+ return value;
+ }
+
+ console.error(`Unexpected locale key: ${String(p)}`);
+
+ return p;
+ }
+ }
+
+ return new Proxy(this.locale, new Handler());
+ }
+
+ return this.locale;
+ }
+
+ public get tsx(): Tsx<T> {
+ if (_DEV_) {
+ if (this.tsxCache) {
+ return this.tsxCache;
+ }
+
+ class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> {
+ get(target: TTarget, p: string | symbol): unknown {
+ const value = target[p as keyof TTarget];
+
+ if (typeof value === 'object') {
+ return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>());
+ }
+
+ if (typeof value === 'string') {
+ const quasis: string[] = [];
+ const expressions: string[] = [];
+ let cursor = 0;
+
+ while (~cursor) {
+ const start = value.indexOf('{', cursor);
+
+ if (!~start) {
+ quasis.push(value.slice(cursor));
+ break;
+ }
+
+ quasis.push(value.slice(cursor, start));
+
+ const end = value.indexOf('}', start);
+
+ expressions.push(value.slice(start + 1, end));
+
+ cursor = end + 1;
+ }
+
+ if (!expressions.length) {
+ console.error(`Unexpected locale key: ${String(p)}`);
+
+ return () => value;
+ }
+
+ return (arg) => {
+ let str = quasis[0];
+
+ for (let i = 0; i < expressions.length; i++) {
+ if (!Object.hasOwn(arg, expressions[i])) {
+ console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`);
+ }
+
+ str += arg[expressions[i]] + quasis[i + 1];
+ }
+
+ return str;
+ };
+ }
+
+ console.error(`Unexpected locale key: ${String(p)}`);
+
+ return p;
+ }
+ }
+
+ return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx<T>;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (this.tsxCache) {
+ return this.tsxCache;
+ }
+
+ function build(target: ILocale): Tsx<T> {
+ const result = {} as Tsx<T>;
+
+ for (const k in target) {
+ if (!Object.hasOwn(target, k)) {
+ continue;
+ }
+
+ const value = target[k as keyof typeof target];
+
+ if (typeof value === 'object') {
+ result[k] = build(value as ILocale);
+ } else if (typeof value === 'string') {
+ const quasis: string[] = [];
+ const expressions: string[] = [];
+ let cursor = 0;
+
+ while (~cursor) {
+ const start = value.indexOf('{', cursor);
+
+ if (!~start) {
+ quasis.push(value.slice(cursor));
+ break;
+ }
+
+ quasis.push(value.slice(cursor, start));
+
+ const end = value.indexOf('}', start);
+
+ expressions.push(value.slice(start + 1, end));
+
+ cursor = end + 1;
+ }
+
+ if (!expressions.length) {
+ continue;
+ }
+
+ result[k] = (arg) => {
+ let str = quasis[0];
+
+ for (let i = 0; i < expressions.length; i++) {
+ str += arg[expressions[i]] + quasis[i + 1];
+ }
+
+ return str;
+ };
+ }
+ }
+ return result;
+ }
+
+ return this.tsxCache = build(this.locale);
+ }
+
+ /**
+ * @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
+ */
+ public t<TKey extends FlattenKeys<T, string>>(key: TKey): string;
+ /**
+ * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも
+ */
+ public t<TKey extends FlattenKeys<T, ParameterizedString>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: string | number }): string;
+ public t(key: string, args?: { readonly [_: string]: string | number }) {
+ let str: string | ParameterizedString | ILocale = this.locale;
+
+ for (const k of key.split('.')) {
+ str = str[k];
- if (args) {
- for (const [k, v] of Object.entries(args)) {
- str = str.replace(`{${k}}`, v.toString());
+ if (_DEV_) {
+ if (typeof str === 'undefined') {
+ console.error(`Unexpected locale key: ${key}`);
+ return key;
}
}
- return str;
- } catch (err) {
- console.warn(`missing localization '${key}'`);
- return key;
}
+
+ if (args) {
+ if (_DEV_) {
+ const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
+
+ if (missing.length) {
+ console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`);
+ }
+ }
+
+ for (const [k, v] of Object.entries(args)) {
+ const search = `{${k}}`;
+
+ if (_DEV_) {
+ if (!(str as string).includes(search)) {
+ console.error(`Unexpected locale parameter: ${k} at ${key}`);
+ }
+ }
+
+ str = (str as string).replace(search, v.toString());
+ }
+ }
+
+ return str;
}
}
+
+if (import.meta.vitest) {
+ const { describe, expect, it } = import.meta.vitest;
+
+ describe('i18n', () => {
+ it('t', () => {
+ const i18n = new I18n({
+ foo: 'foo',
+ bar: {
+ baz: 'baz',
+ qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
+ quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
+ },
+ });
+
+ expect(i18n.t('foo')).toBe('foo');
+ expect(i18n.t('bar.baz')).toBe('baz');
+ expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
+ expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
+ });
+ it('ts', () => {
+ const i18n = new I18n({
+ foo: 'foo',
+ bar: {
+ baz: 'baz',
+ qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
+ quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
+ },
+ });
+
+ expect(i18n.ts.foo).toBe('foo');
+ expect(i18n.ts.bar.baz).toBe('baz');
+ });
+ it('tsx', () => {
+ const i18n = new I18n({
+ foo: 'foo',
+ bar: {
+ baz: 'baz',
+ qux: 'qux {0}' as unknown as ParameterizedString<'0'>,
+ quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>,
+ },
+ });
+
+ expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge');
+ expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga');
+ });
+ });
+}
diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts
index a20cfcb1d0..1ca0990ba9 100644
--- a/packages/frontend/src/scripts/idb-proxy.ts
+++ b/packages/frontend/src/scripts/idb-proxy.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/scripts/idle-render.ts
index ac1be50c73..6adfedcb9f 100644
--- a/packages/frontend/src/scripts/idle-render.ts
+++ b/packages/frontend/src/scripts/idle-render.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts
index ebf27667d7..2465a14703 100644
--- a/packages/frontend/src/scripts/init-chart.ts
+++ b/packages/frontend/src/scripts/init-chart.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts
index 007fc0f2f7..1517e4e1e8 100644
--- a/packages/frontend/src/scripts/initialize-sw.ts
+++ b/packages/frontend/src/scripts/initialize-sw.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts
index 1310a0dc73..d0a8675b19 100644
--- a/packages/frontend/src/scripts/install-plugin.ts
+++ b/packages/frontend/src/scripts/install-plugin.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -10,6 +10,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import type { Plugin } from '@/store.js';
import { ColdDeviceStorage } from '@/store.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
export type AiScriptPluginMeta = {
@@ -110,7 +111,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
}, {
done: async result => {
const { name, permissions } = result;
- const { token } = await os.api('miauth/gen-token', {
+ const { token } = await misskeyApi('miauth/gen-token', {
session: null,
name: name,
permission: permissions,
diff --git a/packages/frontend/src/scripts/install-theme.ts b/packages/frontend/src/scripts/install-theme.ts
index 394b642bf4..866f1225bf 100644
--- a/packages/frontend/src/scripts/install-theme.ts
+++ b/packages/frontend/src/scripts/install-theme.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts
index ea16c9c2ae..aaa4f0a86e 100644
--- a/packages/frontend/src/scripts/intl-const.ts
+++ b/packages/frontend/src/scripts/intl-const.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -33,6 +33,10 @@ try {
}
export const dateTimeFormat = _dateTimeFormat;
+export const timeZone = dateTimeFormat.resolvedOptions().timeZone;
+
+export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N';
+
let _numberFormat: Intl.NumberFormat;
try {
_numberFormat = new Intl.NumberFormat(versatileLang);
diff --git a/packages/frontend/src/scripts/is-device-darkmode.ts b/packages/frontend/src/scripts/is-device-darkmode.ts
index badc295726..4f487c7cb9 100644
--- a/packages/frontend/src/scripts/is-device-darkmode.ts
+++ b/packages/frontend/src/scripts/is-device-darkmode.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts
index dc0e90d20a..406404c462 100644
--- a/packages/frontend/src/scripts/isFfVisibleForMe.ts
+++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts
index 57bc4d19ba..bc1f485f5e 100644
--- a/packages/frontend/src/scripts/keycode.ts
+++ b/packages/frontend/src/scripts/keycode.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/langmap.ts b/packages/frontend/src/scripts/langmap.ts
index 3912d58d82..b32de15963 100644
--- a/packages/frontend/src/scripts/langmap.ts
+++ b/packages/frontend/src/scripts/langmap.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/login-id.ts b/packages/frontend/src/scripts/login-id.ts
index fe0e17e66e..b52735caa0 100644
--- a/packages/frontend/src/scripts/login-id.ts
+++ b/packages/frontend/src/scripts/login-id.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/lookup-user.ts b/packages/frontend/src/scripts/lookup-user.ts
index a35fe898e4..efc9132e75 100644
--- a/packages/frontend/src/scripts/lookup-user.ts
+++ b/packages/frontend/src/scripts/lookup-user.ts
@@ -1,11 +1,12 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
export async function lookupUser() {
const { canceled, result } = await os.inputText({
@@ -17,8 +18,8 @@ export async function lookupUser() {
os.pageWindow(`/admin/user/${user.id}`);
};
- const usernamePromise = os.api('users/show', Misskey.acct.parse(result));
- const idPromise = os.api('users/show', { userId: result });
+ const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result));
+ const idPromise = misskeyApi('users/show', { userId: result });
let _notFound = false;
const notFound = () => {
if (_notFound) {
diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts
index 979f40f038..7f020b15cc 100644
--- a/packages/frontend/src/scripts/lookup.ts
+++ b/packages/frontend/src/scripts/lookup.ts
@@ -1,12 +1,13 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/router.js';
import { Router } from '@/nirax.js';
+import { mainRouter } from '@/router/main.js';
export async function lookup(router?: Router) {
const _router = router ?? mainRouter;
@@ -28,7 +29,7 @@ export async function lookup(router?: Router) {
}
if (query.startsWith('https://')) {
- const promise = os.api('ap/show', {
+ const promise = misskeyApi('ap/show', {
uri: query,
});
diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts
index 559e61211d..099a22163a 100644
--- a/packages/frontend/src/scripts/media-proxy.ts
+++ b/packages/frontend/src/scripts/media-proxy.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts
new file mode 100644
index 0000000000..4e39a0fa06
--- /dev/null
+++ b/packages/frontend/src/scripts/merge.ts
@@ -0,0 +1,35 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { deepClone } from './clone.js';
+import type { Cloneable } from './clone.js';
+
+type DeepPartial<T> = {
+ [P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P];
+};
+
+function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+/**
+ * valueにないキーをdefからもらう(再帰的)\
+ * nullはそのまま、undefinedはdefの値
+ **/
+export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: DeepPartial<X>, def: X): X {
+ if (isPureObject(value) && isPureObject(def)) {
+ const result = deepClone(value as Cloneable) as X;
+ for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
+ if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
+ result[k] = v;
+ } else if (isPureObject(v) && isPureObject(result[k])) {
+ const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<string | number | symbol, unknown>>;
+ result[k] = deepMerge<typeof v>(child, v);
+ }
+ }
+ return result;
+ }
+ throw new Error('deepMerge: value and def must be pure objects');
+}
diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts
index 465926fe04..8867a8c50f 100644
--- a/packages/frontend/src/scripts/mfm-function-picker.ts
+++ b/packages/frontend/src/scripts/mfm-function-picker.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/api.ts b/packages/frontend/src/scripts/misskey-api.ts
index 8f3a163938..49fb6f9e59 100644
--- a/packages/frontend/src/scripts/api.ts
+++ b/packages/frontend/src/scripts/misskey-api.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -10,12 +10,17 @@ import { $i } from '@/account.js';
export const pendingApiRequestsCount = ref(0);
// Implements Misskey.api.ApiClient.request
-export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
+export function misskeyApi<
+ ResT = void,
+ E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
+ P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
+ _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
+>(
endpoint: E,
data: P = {} as any,
token?: string | null | undefined,
signal?: AbortSignal,
-): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
+): Promise<_ResT> {
if (endpoint.includes('://')) throw new Error('invalid endpoint');
pendingApiRequestsCount.value++;
@@ -23,7 +28,7 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin
pendingApiRequestsCount.value--;
};
- const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
+ const promise = new Promise<_ResT>((resolve, reject) => {
// Append a credential
if ($i) (data as any).i = $i.token;
if (token !== undefined) (data as any).i = token;
@@ -44,7 +49,7 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
- resolve();
+ resolve(undefined as _ResT); // void -> undefined
} else {
reject(body.error);
}
@@ -57,10 +62,15 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin
}
// Implements Misskey.api.ApiClient.request
-export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>(
+export function misskeyApiGet<
+ ResT = void,
+ E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
+ P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
+ _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
+>(
endpoint: E,
data: P = {} as any,
-): Promise<Misskey.api.SwitchCaseResponseType<E, P>> {
+): Promise<_ResT> {
pendingApiRequestsCount.value++;
const onFinally = () => {
@@ -69,7 +79,7 @@ export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endp
const query = new URLSearchParams(data as any);
- const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => {
+ const promise = new Promise<_ResT>((resolve, reject) => {
// Send request
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
method: 'GET',
@@ -81,7 +91,7 @@ export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endp
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
- resolve();
+ resolve(undefined as _ResT); // void -> undefined
} else {
reject(body.error);
}
diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/scripts/navigator.ts
index b13186a10e..ffc0a457f4 100644
--- a/packages/frontend/src/scripts/navigator.ts
+++ b/packages/frontend/src/scripts/navigator.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts
index 62833b4de3..abc8ada461 100644
--- a/packages/frontend/src/scripts/nyaize.ts
+++ b/packages/frontend/src/scripts/nyaize.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts
index 369e46aae1..0e3b093ecf 100644
--- a/packages/frontend/src/scripts/page-metadata.ts
+++ b/packages/frontend/src/scripts/page-metadata.ts
@@ -1,13 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
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');
+import { MaybeRefOrGetter, Ref, inject, isRef, onActivated, onBeforeUnmount, provide, ref, toValue, watch } from 'vue';
export type PageMetadata = {
title: string;
@@ -18,29 +15,56 @@ export type PageMetadata = {
needWideArea?: boolean;
};
-export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void {
- const _metadata = isRef(metadata) ? metadata : ref(metadata);
+type PageMetadataGetter = () => PageMetadata;
+type PageMetadataReceiver = (getter: PageMetadataGetter) => void;
- provide(pageMetadataProvider, _metadata);
+const RECEIVER_KEY = Symbol('ReceiverKey');
+const setReceiver = (v: PageMetadataReceiver): void => {
+ provide<PageMetadataReceiver>(RECEIVER_KEY, v);
+};
+const getReceiver = (): PageMetadataReceiver | undefined => {
+ return inject<PageMetadataReceiver>(RECEIVER_KEY);
+};
- const set = inject(setPageMetadata) as any;
- if (set) {
- set(_metadata);
+const METADATA_KEY = Symbol('MetadataKey');
+const setMetadata = (v: Ref<PageMetadata | null>): void => {
+ provide<Ref<PageMetadata | null>>(METADATA_KEY, v);
+};
+const getMetadata = (): Ref<PageMetadata | null> | undefined => {
+ return inject<Ref<PageMetadata | null>>(METADATA_KEY);
+};
- onMounted(() => {
- set(_metadata);
- });
+export const definePageMetadata = (maybeRefOrGetterMetadata: MaybeRefOrGetter<PageMetadata>): void => {
+ const metadataRef = ref(toValue(maybeRefOrGetterMetadata));
+ const metadataGetter = () => metadataRef.value;
+ const receiver = getReceiver();
- onActivated(() => {
- set(_metadata);
- });
- }
-}
+ // setup handler
+ receiver?.(metadataGetter);
-export function provideMetadataReceiver(callback: (info: ComputedRef<PageMetadata>) => void): void {
- provide(setPageMetadata, callback);
-}
+ // update handler
+ onBeforeUnmount(watch(
+ () => toValue(maybeRefOrGetterMetadata),
+ (metadata) => {
+ metadataRef.value = metadata;
+ receiver?.(metadataGetter);
+ },
+ { deep: true },
+ ));
+ onActivated(() => {
+ receiver?.(metadataGetter);
+ });
+};
-export function injectPageMetadata(): PageMetadata | undefined {
- return inject(pageMetadataProvider);
-}
+export const provideMetadataReceiver = (receiver: PageMetadataReceiver): void => {
+ setReceiver(receiver);
+};
+
+export const provideReactiveMetadata = (metadataRef: Ref<PageMetadata | null>): void => {
+ setMetadata(metadataRef);
+};
+
+export const injectReactiveMetadata = (): Ref<PageMetadata | null> => {
+ const metadataRef = getMetadata();
+ return isRef(metadataRef) ? metadataRef : ref(null);
+};
diff --git a/packages/frontend/src/scripts/physics.ts b/packages/frontend/src/scripts/physics.ts
index cf9fad70eb..8a4e9319b3 100644
--- a/packages/frontend/src/scripts/physics.ts
+++ b/packages/frontend/src/scripts/physics.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts
index e6c08dfbc0..9e51272791 100644
--- a/packages/frontend/src/scripts/please-login.ts
+++ b/packages/frontend/src/scripts/please-login.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts
index 0c2ff16992..1caa2dfc21 100644
--- a/packages/frontend/src/scripts/popout.ts
+++ b/packages/frontend/src/scripts/popout.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts
index 0a799c5665..8c9e3c02c3 100644
--- a/packages/frontend/src/scripts/popup-position.ts
+++ b/packages/frontend/src/scripts/popup-position.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function calcPopupPosition(el: HTMLElement, props: {
- anchorElement: HTMLElement | null;
+ anchorElement?: HTMLElement | null;
innerMargin: number;
direction: 'top' | 'bottom' | 'left' | 'right';
align: 'top' | 'bottom' | 'left' | 'right' | 'center';
diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts
index 80441caf15..31a9ac1ad9 100644
--- a/packages/frontend/src/scripts/post-message.ts
+++ b/packages/frontend/src/scripts/post-message.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts
index 9b13e794f5..7aec05c0cf 100644
--- a/packages/frontend/src/scripts/reaction-picker.ts
+++ b/packages/frontend/src/scripts/reaction-picker.ts
@@ -1,8 +1,9 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import * as Misskey from 'misskey-js';
import { defineAsyncComponent, Ref, ref } from 'vue';
import { popup } from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -10,6 +11,7 @@ import { defaultStore } from '@/store.js';
class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false);
+ private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
private onChosen?: (reaction: string) => void;
private onClosed?: () => void;
@@ -23,6 +25,7 @@ class ReactionPicker {
src: this.src,
pinnedEmojis: reactionsRef,
asReactionPicker: true,
+ targetNote: this.targetNote,
manualShowing: this.manualShowing,
}, {
done: reaction => {
@@ -38,8 +41,9 @@ class ReactionPicker {
});
}
- public show(src: HTMLElement, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
+ public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
this.src.value = src;
+ this.targetNote.value = targetNote;
this.manualShowing.value = true;
this.onChosen = onChosen;
this.onClosed = onClosed;
diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts
new file mode 100644
index 0000000000..6bfcef6c36
--- /dev/null
+++ b/packages/frontend/src/scripts/safe-parse.ts
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function safeParseFloat(str: unknown): number | null {
+ if (typeof str !== 'string' || str === '') return null;
+ const num = parseFloat(str);
+ if (isNaN(num)) return null;
+ return num;
+}
diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts
index 625d8c34a7..0edf4e9eba 100644
--- a/packages/frontend/src/scripts/safe-uri-decode.ts
+++ b/packages/frontend/src/scripts/safe-uri-decode.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts
index 1f626e4c0d..8edb6fca05 100644
--- a/packages/frontend/src/scripts/scroll.ts
+++ b/packages/frontend/src/scripts/scroll.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts
index 53e2cd5b16..9aa38178b2 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/scripts/select-file.ts
@@ -1,11 +1,12 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -65,7 +66,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
}
});
- os.api('drive/files/upload-from-url', {
+ misskeyApi('drive/files/upload-from-url', {
url: url,
folderId: defaultStore.state.uploadFolder,
marker,
diff --git a/packages/frontend/src/scripts/show-moved-dialog.ts b/packages/frontend/src/scripts/show-moved-dialog.ts
index b4defbfe7d..35b3ef79d8 100644
--- a/packages/frontend/src/scripts/show-moved-dialog.ts
+++ b/packages/frontend/src/scripts/show-moved-dialog.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/show-suspended-dialog.ts b/packages/frontend/src/scripts/show-suspended-dialog.ts
index a2fd5db453..8b89dbb936 100644
--- a/packages/frontend/src/scripts/show-suspended-dialog.ts
+++ b/packages/frontend/src/scripts/show-suspended-dialog.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts
index d9d5bb1037..fed16bc71c 100644
--- a/packages/frontend/src/scripts/shuffle.ts
+++ b/packages/frontend/src/scripts/shuffle.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts
index a09f02cec0..11fcaa0716 100644
--- a/packages/frontend/src/scripts/snowfall-effect.ts
+++ b/packages/frontend/src/scripts/snowfall-effect.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -17,20 +17,20 @@ export class SnowfallEffect {
uniform vec3 u_worldSize;
uniform float u_gravity;
uniform float u_wind;
+ uniform float u_spin_factor;
+ uniform float u_turbulence;
void main() {
v_color = a_color;
- v_rotation = a_rotation.x + u_time * a_rotation.y;
+ v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y;
vec3 pos = a_position.xyz;
- float turbulence = 1.0;
-
pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
- pos.x += sin(u_time * a_speed.z * turbulence) * a_rotation.z;
- pos.z += cos(u_time * a_speed.z * turbulence) * a_rotation.z;
+ pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z;
+ pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z;
gl_Position = u_projection * vec4(pos.xyz, a_position.w);
gl_PointSize = (a_size / gl_Position.w) * 100.0;
@@ -105,6 +105,7 @@ export class SnowfallEffect {
private opacity = 1;
private size = 4;
private snowflake = 'data:image/png;base64,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';
+ private mode = 'snow';
private INITIAL_BUFFERS = () => ({
position: { size: 3, value: [] },
@@ -119,6 +120,8 @@ export class SnowfallEffect {
worldSize: { type: 'vec3', value: [0, 0, 0] },
gravity: { type: 'float', value: this.gravity },
wind: { type: 'float', value: 0 },
+ spin_factor: { type: 'float', value: this.mode === 'sakura' ? 8 : 1 },
+ turbulence: { type: 'float', value: this.mode === 'sakura' ? 2 : 1 },
projection: {
type: 'mat4',
value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
@@ -153,7 +156,16 @@ export class SnowfallEffect {
easing: 0.0005,
};
- constructor() {
+ constructor(options: {
+ sakura?: boolean;
+ }) {
+ if (options.sakura) {
+ this.mode = 'sakura';
+ this.snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDItMDFUMTQ6Mzk6NTYrMDkwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyNC0wMi0wMVQxNDozOTo1NiswOTAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI2NCIKICAgZXhpZjpQaXhlbFlEaW1l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';
+ this.size = 10;
+ this.density = 1 / 280;
+ }
+
const canvas = this.initCanvas();
const gl = canvas.getContext('webgl2', { antialias: true });
if (gl == null) throw new Error('Failed to get WebGL context');
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts
index 2f7545ef0d..9555579e0d 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/scripts/sound.ts
@@ -1,11 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { SoundStore } from '@/store.js';
import { defaultStore } from '@/store.js';
-import * as os from '@/os.js';
let ctx: AudioContext;
const cache = new Map<string, AudioBuffer>();
@@ -89,63 +88,33 @@ export type OperationType = typeof operationTypes[number];
/**
* 音声を読み込む
- * @param soundStore サウンド設定
+ * @param url url
* @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする
*/
-export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) {
- if (_DEV_) console.log('loading audio. opts:', options);
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
- return;
- }
+export async function loadAudio(url: string, options?: { useCache?: boolean; }) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ctx == null) {
ctx = new AudioContext();
}
if (options?.useCache ?? true) {
- if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) {
- if (_DEV_) console.log('use cache');
- return cache.get(soundStore.fileId) as AudioBuffer;
- } else if (cache.has(soundStore.type)) {
- if (_DEV_) console.log('use cache');
- return cache.get(soundStore.type) as AudioBuffer;
+ if (cache.has(url)) {
+ return cache.get(url) as AudioBuffer;
}
}
let response: Response;
- if (soundStore.type === '_driveFile_') {
- try {
- response = await fetch(soundStore.fileUrl);
- } catch (err) {
- try {
- // URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック
- const apiRes = await os.api('drive/files/show', {
- fileId: soundStore.fileId,
- });
- response = await fetch(apiRes.url);
- } catch (fbErr) {
- // それでも無理なら諦める
- return;
- }
- }
- } else {
- try {
- response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`);
- } catch (err) {
- return;
- }
+ try {
+ response = await fetch(url);
+ } catch (err) {
+ return;
}
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
if (options?.useCache ?? true) {
- if (soundStore.type === '_driveFile_') {
- cache.set(soundStore.fileId, audioBuffer);
- } else {
- cache.set(soundStore.type, audioBuffer);
- }
+ cache.set(url, audioBuffer);
}
return audioBuffer;
@@ -155,13 +124,12 @@ export async function loadAudio(soundStore: SoundStore, options?: { useCache?: b
* 既定のスプライトを再生する
* @param type スプライトの種類を指定
*/
-export function play(operationType: OperationType) {
+export function playMisskeySfx(operationType: OperationType) {
const sound = defaultStore.state[`sound_${operationType}`];
- if (_DEV_) console.log('play', operationType, sound);
if (sound.type == null || !canPlay) return;
canPlay = false;
- playFile(sound).finally(() => {
+ playMisskeySfxFile(sound).finally(() => {
// ごく短時間に音が重複しないように
setTimeout(() => {
canPlay = true;
@@ -173,26 +141,59 @@ export function play(operationType: OperationType) {
* サウンド設定形式で指定された音声を再生する
* @param soundStore サウンド設定
*/
-export async function playFile(soundStore: SoundStore) {
- const buffer = await loadAudio(soundStore);
+export async function playMisskeySfxFile(soundStore: SoundStore) {
+ if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
+ return;
+ }
+ const masterVolume = defaultStore.state.sound_masterVolume;
+ if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
+ return;
+ }
+ const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`;
+ const buffer = await loadAudio(url);
if (!buffer) return;
- createSourceNode(buffer, soundStore.volume)?.start();
+ const volume = soundStore.volume * masterVolume;
+ createSourceNode(buffer, { volume }).soundSource.start();
}
-export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null {
- const masterVolume = defaultStore.state.sound_masterVolume;
- if (isMute() || masterVolume === 0 || volume === 0) {
- return null;
+export async function playUrl(url: string, opts: {
+ volume?: number;
+ pan?: number;
+ playbackRate?: number;
+}) {
+ if (opts.volume === 0) {
+ return;
}
+ const buffer = await loadAudio(url);
+ if (!buffer) return;
+ createSourceNode(buffer, opts).soundSource.start();
+}
+
+export function createSourceNode(buffer: AudioBuffer, opts: {
+ volume?: number;
+ pan?: number;
+ playbackRate?: number;
+}): {
+ soundSource: AudioBufferSourceNode;
+ panNode: StereoPannerNode;
+ gainNode: GainNode;
+} {
+ const panNode = ctx.createStereoPanner();
+ panNode.pan.value = opts.pan ?? 0;
const gainNode = ctx.createGain();
- gainNode.gain.value = masterVolume * volume;
+
+ gainNode.gain.value = opts.volume ?? 1;
const soundSource = ctx.createBufferSource();
soundSource.buffer = buffer;
- soundSource.connect(gainNode).connect(ctx.destination);
+ soundSource.playbackRate.value = opts.playbackRate ?? 1;
+ soundSource
+ .connect(panNode)
+ .connect(gainNode)
+ .connect(ctx.destination);
- return soundSource;
+ return { soundSource, panNode, gainNode };
}
/**
diff --git a/packages/frontend/src/scripts/sticky-sidebar.ts b/packages/frontend/src/scripts/sticky-sidebar.ts
index f233c3648e..50f1e6ecc8 100644
--- a/packages/frontend/src/scripts/sticky-sidebar.ts
+++ b/packages/frontend/src/scripts/sticky-sidebar.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/test-utils.ts b/packages/frontend/src/scripts/test-utils.ts
index 1b42811faa..52bb2d94e0 100644
--- a/packages/frontend/src/scripts/test-utils.ts
+++ b/packages/frontend/src/scripts/test-utils.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/theme-editor.ts b/packages/frontend/src/scripts/theme-editor.ts
index 275f4bcdaa..0092af1640 100644
--- a/packages/frontend/src/scripts/theme-editor.ts
+++ b/packages/frontend/src/scripts/theme-editor.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index 21ef85fe7a..5f7e88bd9f 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -1,11 +1,12 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref } from 'vue';
import tinycolor from 'tinycolor2';
import { deepClone } from './clone.js';
+import type { BuiltinTheme } from 'shiki';
import { globalEvents } from '@/events.js';
import lightTheme from '@/themes/_light.json5';
import darkTheme from '@/themes/_dark.json5';
@@ -18,6 +19,13 @@ export type Theme = {
desc?: string;
base?: 'dark' | 'light';
props: Record<string, string>;
+ codeHighlighter?: {
+ base: BuiltinTheme;
+ overrides?: Record<string, any>;
+ } | {
+ base: '_none_';
+ overrides: Record<string, any>;
+ };
};
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
@@ -53,7 +61,7 @@ export const getBuiltinThemesRef = () => {
return builtinThemes;
};
-let timeout = null;
+let timeout: number | null = null;
export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout);
diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/scripts/time.ts
index 4479db1081..275b67ed00 100644
--- a/packages/frontend/src/scripts/time.ts
+++ b/packages/frontend/src/scripts/time.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/timezones.ts b/packages/frontend/src/scripts/timezones.ts
index 55f9be393f..c7582e06da 100644
--- a/packages/frontend/src/scripts/timezones.ts
+++ b/packages/frontend/src/scripts/timezones.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/touch.ts b/packages/frontend/src/scripts/touch.ts
index 05f379e4aa..13c9d648dc 100644
--- a/packages/frontend/src/scripts/touch.ts
+++ b/packages/frontend/src/scripts/touch.ts
@@ -1,8 +1,9 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { ref } from 'vue';
import { deviceKind } from '@/scripts/device-kind.js';
const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
@@ -16,3 +17,6 @@ if (isTouchSupported && !isTouchUsing) {
isTouchUsing = true;
}, { passive: true });
}
+
+/** (MkHorizontalSwipe) 横スワイプ中か? */
+export const isHorizontalSwipeSwiping = ref(false);
diff --git a/packages/frontend/src/scripts/unison-reload.ts b/packages/frontend/src/scripts/unison-reload.ts
index 65fc090888..a24941d02e 100644
--- a/packages/frontend/src/scripts/unison-reload.ts
+++ b/packages/frontend/src/scripts/unison-reload.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts
index b896376ec8..6c46b2bc1b 100644
--- a/packages/frontend/src/scripts/upload.ts
+++ b/packages/frontend/src/scripts/upload.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { reactive, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import { readAndCompressImage } from 'browser-image-resizer';
+import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
import { getCompressionConfig } from './upload/compress-config.js';
import { defaultStore } from '@/store.js';
import { apiUrl } from '@/config.js';
diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts
index 2deb9cbb81..3046b7f518 100644
--- a/packages/frontend/src/scripts/upload/compress-config.ts
+++ b/packages/frontend/src/scripts/upload/compress-config.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import isAnimated from 'is-file-animated';
import { isWebpSupported } from './isWebpSupported.js';
-import type { BrowserImageResizerConfig } from 'browser-image-resizer';
+import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer';
const compressTypeMap = {
'image/jpeg': { quality: 0.90, mimeType: 'image/webp' },
@@ -21,7 +21,7 @@ const compressTypeMapFallback = {
'image/svg+xml': { quality: 1, mimeType: 'image/png' },
} as const;
-export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfig | undefined> {
+export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfigWithConvertedOutput | undefined> {
const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type];
if (!imgConfig || await isAnimated(file)) {
return;
diff --git a/packages/frontend/src/scripts/upload/isWebpSupported.ts b/packages/frontend/src/scripts/upload/isWebpSupported.ts
index 185c3e6b40..2511236ecc 100644
--- a/packages/frontend/src/scripts/upload/isWebpSupported.ts
+++ b/packages/frontend/src/scripts/upload/isWebpSupported.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts
index 625f4ce057..e3072b3b7d 100644
--- a/packages/frontend/src/scripts/url.ts
+++ b/packages/frontend/src/scripts/url.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts
index 3d6489c3b8..7e4bf5c9c6 100644
--- a/packages/frontend/src/scripts/use-chart-tooltip.ts
+++ b/packages/frontend/src/scripts/use-chart-tooltip.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend/src/scripts/use-document-visibility.ts
index a9e2512eb3..a8f4d5e03a 100644
--- a/packages/frontend/src/scripts/use-document-visibility.ts
+++ b/packages/frontend/src/scripts/use-document-visibility.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts
index b8c5431fb6..b50e78c3cc 100644
--- a/packages/frontend/src/scripts/use-interval.ts
+++ b/packages/frontend/src/scripts/use-interval.ts
@@ -1,9 +1,9 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { onMounted, onUnmounted } from 'vue';
+import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
export function useInterval(fn: () => void, interval: number, options: {
immediate: boolean;
@@ -28,6 +28,16 @@ export function useInterval(fn: () => void, interval: number, options: {
intervalId = null;
};
+ onActivated(() => {
+ if (intervalId) return;
+ if (options.immediate) fn();
+ intervalId = window.setInterval(fn, interval);
+ });
+
+ onDeactivated(() => {
+ clear();
+ });
+
onUnmounted(() => {
clear();
});
diff --git a/packages/frontend/src/scripts/use-leave-guard.ts b/packages/frontend/src/scripts/use-leave-guard.ts
index c9750c3923..5f7e56e8a9 100644
--- a/packages/frontend/src/scripts/use-leave-guard.ts
+++ b/packages/frontend/src/scripts/use-leave-guard.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index bda9c04ea4..524ac5d3fe 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -1,15 +1,15 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { onUnmounted, Ref } from 'vue';
+import { onUnmounted, Ref, ShallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { useStream } from '@/stream.js';
import { $i } from '@/account.js';
export function useNoteCapture(props: {
- rootEl: Ref<HTMLElement>;
+ rootEl: ShallowRef<HTMLElement | undefined>;
note: Ref<Misskey.entities.Note>;
pureNote: Ref<Misskey.entities.Note>;
isDeletedRef: Ref<boolean>;
@@ -83,7 +83,7 @@ export function useNoteCapture(props: {
function capture(withHandler = false): void {
if (connection) {
// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
- connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id });
+ connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id });
if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id });
if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
}
diff --git a/packages/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/scripts/use-tooltip.ts
index aaf0a0285a..a26d08cce7 100644
--- a/packages/frontend/src/scripts/use-tooltip.ts
+++ b/packages/frontend/src/scripts/use-tooltip.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/scripts/worker-multi-dispatch.ts b/packages/frontend/src/scripts/worker-multi-dispatch.ts
index 7686b687c5..6b3fcd9383 100644
--- a/packages/frontend/src/scripts/worker-multi-dispatch.ts
+++ b/packages/frontend/src/scripts/worker-multi-dispatch.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 46634af96b..dfc4169a54 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -7,7 +7,9 @@ import { markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { miLocalStorage } from './local-storage.js';
import type { SoundType } from '@/scripts/sound.js';
+import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki';
import { Storage } from '@/pizzax.js';
+import { hemisphere } from '@/scripts/intl-const.js';
interface PostFormAction {
title: string,
@@ -184,6 +186,12 @@ export const defaultStore = markRaw(new Storage('base', {
default: {
src: 'home' as 'home' | 'local' | 'social' | 'global' | `list:${string}`,
userList: null as Misskey.entities.UserList | null,
+ filter: {
+ withReplies: true,
+ withRenotes: true,
+ withSensitive: true,
+ onlyFiles: false,
+ },
},
},
pinnedUserLists: {
@@ -391,10 +399,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
- tlWithReplies: {
- where: 'device',
- default: false,
- },
defaultWithReplies: {
where: 'account',
default: false,
@@ -420,6 +424,21 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
+ dropAndFusion: {
+ where: 'device',
+ default: {
+ bgmVolume: 0.25,
+ sfxVolume: 1,
+ },
+ },
+ hemisphere: {
+ where: 'device',
+ default: hemisphere as 'N' | 'S',
+ },
+ enableHorizontalSwipe: {
+ where: 'device',
+ default: true,
+ },
sound_masterVolume: {
where: 'device',
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index 5f0826b4e3..0c5ee06197 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 274808b13f..cbec377277 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -1,7 +1,7 @@
@charset "utf-8";
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -425,13 +425,13 @@ rt {
transform: scale(0.9);
}
-@keyframes blink {
+@keyframes global-blink {
0% { opacity: 1; transform: scale(1); }
30% { opacity: 1; transform: scale(1); }
90% { opacity: 0; transform: scale(0.5); }
}
-@keyframes tada {
+@keyframes global-tada {
from {
transform: scale3d(1, 1, 1);
}
@@ -461,7 +461,7 @@ rt {
._anime_bounce {
will-change: transform;
- animation: bounce ease 0.7s;
+ animation: global-bounce ease 0.7s;
animation-iteration-count: 1;
transform-origin: 50% 50%;
}
@@ -473,7 +473,7 @@ rt {
transition: transform 0.1s ease;
}
-@keyframes bounce {
+@keyframes global-bounce {
0% {
transform: scaleX(0.90) scaleY(0.90) ;
}
diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts
index f37c01cca1..c41cc17652 100644
--- a/packages/frontend/src/theme-store.ts
+++ b/packages/frontend/src/theme-store.ts
@@ -1,11 +1,11 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Theme, getBuiltinThemes } from '@/scripts/theme.js';
import { miLocalStorage } from '@/local-storage.js';
-import { api } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i } from '@/account.js';
const lsCacheKey = $i ? `themes:${$i.id}` as const : null;
@@ -19,7 +19,7 @@ export async function fetchThemes(): Promise<void> {
if ($i == null) return;
try {
- const themes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
+ const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' });
miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
} catch (err) {
if (err.code === 'NO_SUCH_KEY') return;
@@ -35,13 +35,13 @@ export async function addTheme(theme: Theme): Promise<void> {
}
await fetchThemes();
const themes = getThemes().concat(theme);
- await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
+ await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
}
export async function removeTheme(theme: Theme): Promise<void> {
if ($i == null) return;
const themes = getThemes().filter(t => t.id !== theme.id);
- await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
+ await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
}
diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend/src/themes/_dark.json5
index 3f5822977a..c82a956868 100644
--- a/packages/frontend/src/themes/_dark.json5
+++ b/packages/frontend/src/themes/_dark.json5
@@ -94,4 +94,8 @@
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
},
+
+ codeHighlighter: {
+ base: 'one-dark-pro',
+ },
}
diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend/src/themes/_light.json5
index 6ebfcaafeb..63bc030916 100644
--- a/packages/frontend/src/themes/_light.json5
+++ b/packages/frontend/src/themes/_light.json5
@@ -94,4 +94,8 @@
X16: ':alpha<0.7<@panel',
X17: ':alpha<0.8<@bg',
},
+
+ codeHighlighter: {
+ base: 'catppuccin-latte',
+ },
}
diff --git a/packages/frontend/src/type.ts b/packages/frontend/src/type.ts
new file mode 100644
index 0000000000..9c0fc2a11e
--- /dev/null
+++ b/packages/frontend/src/type.ts
@@ -0,0 +1,3 @@
+export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
+
+export type WithNonNullable<T, K extends keyof T> = T & { [P in K]-?: NonNullable<T[P]> };
diff --git a/packages/frontend/src/types/date-separated-list.ts b/packages/frontend/src/types/date-separated-list.ts
index 678193ca98..af685cff12 100644
--- a/packages/frontend/src/types/date-separated-list.ts
+++ b/packages/frontend/src/types/date-separated-list.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts
index f4516bbe5b..712f3464e5 100644
--- a/packages/frontend/src/types/menu.ts
+++ b/packages/frontend/src/types/menu.ts
@@ -1,10 +1,10 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as Misskey from 'misskey-js';
-import { Ref } from 'vue';
+import { ComputedRef, Ref } from 'vue';
export type MenuAction = (ev: MouseEvent) => void;
@@ -15,7 +15,7 @@ export type MenuLink = { type: 'link', to: string, text: string, icon?: string,
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<boolean>, text: string, disabled?: boolean | Ref<boolean> };
-export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
+export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
export type MenuPending = { type: 'pending' };
diff --git a/packages/frontend/src/types/page-header.ts b/packages/frontend/src/types/page-header.ts
index 295b97a7fd..e9807a2939 100644
--- a/packages/frontend/src/types/page-header.ts
+++ b/packages/frontend/src/types/page-header.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue
index 2b1133d472..362c29e6c2 100644
--- a/packages/frontend/src/ui/_common_/announcements.vue
+++ b/packages/frontend/src/ui/_common_/announcements.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index b970ff1df4..9b510a6292 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 6ece7d86d7..822b552837 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -49,7 +49,8 @@ import { defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { swInject } from './sw-inject.js';
import XNotification from './notification.vue';
-import { popups, pendingApiRequestsCount } from '@/os.js';
+import { popups } from '@/os.js';
+import { pendingApiRequestsCount } from '@/scripts/misskey-api.js';
import { uploads } from '@/scripts/upload.js';
import * as sound from '@/scripts/sound.js';
import { $i } from '@/account.js';
@@ -82,7 +83,7 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
}, 6000);
}
- sound.play('notification');
+ sound.playMisskeySfx('notification');
}
if ($i) {
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 7aee7bbc32..5d0e065f09 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -254,7 +254,7 @@ function more() {
left: 20px;
color: var(--navIndicator);
font-size: 8px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 93d09e95b5..fa1f0eb8c7 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -313,7 +313,7 @@ function more(ev: MouseEvent) {
left: 20px;
color: var(--navIndicator);
font-size: 8px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
@@ -483,7 +483,7 @@ function more(ev: MouseEvent) {
left: 24px;
color: var(--navIndicator);
font-size: 8px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/_common_/notification.vue b/packages/frontend/src/ui/_common_/notification.vue
index dfc1f83960..6dcb6a1fa8 100644
--- a/packages/frontend/src/ui/_common_/notification.vue
+++ b/packages/frontend/src/ui/_common_/notification.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index c92695afed..8dad666623 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MarqueeText from '@/components/MkMarquee.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
@@ -52,7 +52,7 @@ const fetching = ref(true);
const key = ref(0);
const tick = () => {
- os.api('federation/instances', {
+ misskeyApi('federation/instances', {
sort: '+latestRequestReceivedAt',
limit: 30,
}).then(res => {
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index 58e109ad7f..b973a4fd6b 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
index 6057174ba8..67f8b109c4 100644
--- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MarqueeText from '@/components/MkMarquee.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
import { notePage } from '@/filters/note.js';
@@ -54,7 +54,7 @@ const key = ref(0);
const tick = () => {
if (props.userListId == null) return;
- os.api('notes/user-list-timeline', {
+ misskeyApi('notes/user-list-timeline', {
listId: props.userListId,
}).then(res => {
notes.value = res;
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index 81445df1e9..872c69810c 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index be6a4959ec..ad93b7e61c 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts
index 5239b76705..ff851ad99f 100644
--- a/packages/frontend/src/ui/_common_/sw-inject.ts
+++ b/packages/frontend/src/ui/_common_/sw-inject.ts
@@ -1,13 +1,14 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { api, post } from '@/os.js';
+import { post } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i, login } from '@/account.js';
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
-import { mainRouter } from '@/router.js';
import { deepClone } from '@/scripts/clone.js';
+import { mainRouter } from '@/router/main.js';
export function swInject() {
navigator.serviceWorker.addEventListener('message', async ev => {
@@ -30,10 +31,10 @@ export function swInject() {
// プッシュ通知から来たreply,renoteはtruncateBodyが通されているため、
// 完全なノートを取得しなおす
if (props.reply) {
- props.reply = await api('notes/show', { noteId: props.reply.id });
+ props.reply = await misskeyApi('notes/show', { noteId: props.reply.id });
}
if (props.renote) {
- props.renote = await api('notes/show', { noteId: props.renote.id });
+ props.renote = await misskeyApi('notes/show', { noteId: props.renote.id });
}
return post(props);
}
diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue
index 3365571a14..6db7f9cae7 100644
--- a/packages/frontend/src/ui/_common_/upload.vue
+++ b/packages/frontend/src/ui/_common_/upload.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index 2060838f5d..ee5176b558 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -141,7 +141,7 @@ onMounted(() => {
left: 0;
color: var(--navIndicator);
font-size: 8px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
}
&:hover {
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index bc1527813c..19672ef87f 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -220,7 +220,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
left: 0;
color: var(--navIndicator);
font-size: 8px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index e0985fdb11..b833e9f6be 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -52,19 +52,21 @@ import XCommon from './_common_/common.vue';
import { instanceName } from '@/config.js';
import { StickySidebar } from '@/scripts/sticky-sidebar.js';
import * as os from '@/os.js';
-import { mainRouter } from '@/router.js';
-import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
+import { mainRouter } from '@/router/main.js';
const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
+const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
+
const DESKTOP_THRESHOLD = 1100;
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
-const pageMetadata = ref<null | PageMetadata>();
+const pageMetadata = ref<null | PageMetadata>(null);
const widgetsShowing = ref(false);
const fullView = ref(false);
const globalHeaderHeight = ref(0);
@@ -75,12 +77,18 @@ const widgetsLeft = ref<HTMLElement>();
const widgetsRight = ref<HTMLElement>();
provide('router', mainRouter);
-provideMetadataReceiver((info) => {
- pageMetadata.value = info.value;
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
+ pageMetadata.value = info;
if (pageMetadata.value) {
- document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ if (isRoot.value && pageMetadata.value.title === instanceName) {
+ document.title = pageMetadata.value.title;
+ } else {
+ document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ }
}
});
+provideReactiveMetadata(pageMetadata);
provide('shouldHeaderThin', showMenuOnTop.value);
provide('forceSpacerMin', true);
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index d184764b82..92d2e23d9b 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -103,7 +103,6 @@ import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/router.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { defaultStore } from '@/store.js';
@@ -117,6 +116,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue';
import XMentionsColumn from '@/ui/deck/mentions-column.vue';
import XDirectColumn from '@/ui/deck/direct-column.vue';
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
+import { mainRouter } from '@/router/main.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
@@ -189,7 +189,7 @@ const addColumn = async (ev) => {
const { canceled, result: column } = await os.select({
title: i18n.ts._deck.addColumn,
items: columns.map(column => ({
- value: column, text: i18n.t('_deck._columns.' + column),
+ value: column, text: i18n.ts._deck._columns[column],
})),
});
if (canceled) return;
@@ -197,7 +197,7 @@ const addColumn = async (ev) => {
addColumnToStore({
type: column,
id: uuid(),
- name: i18n.t('_deck._columns.' + column),
+ name: i18n.ts._deck._columns[column],
width: 330,
});
};
@@ -241,7 +241,7 @@ function changeProfile(ev: MouseEvent) {
action: async () => {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile,
- allowEmpty: false,
+ minLength: 1,
});
if (canceled) return;
@@ -256,7 +256,7 @@ function changeProfile(ev: MouseEvent) {
async function deleteProfile() {
const { canceled } = await os.confirm({
type: 'warning',
- text: i18n.t('deleteAreYouSure', { x: deckStore.state.profile }),
+ text: i18n.tsx.deleteAreYouSure({ x: deckStore.state.profile }),
});
if (canceled) return;
@@ -488,7 +488,7 @@ body {
left: 0;
color: var(--indicator);
font-size: 16px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index fe4d2a809c..b42a21bf6f 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -19,6 +19,7 @@ import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -35,7 +36,7 @@ onMounted(() => {
});
async function setAntenna() {
- const antennas = await os.api('antennas/list');
+ const antennas = await misskeyApi('antennas/list');
const { canceled, result: antenna } = await os.select({
title: i18n.ts.selectAntenna,
items: antennas.map(x => ({
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index de5d94b4f7..125c85130e 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -26,6 +26,7 @@ import { updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -41,7 +42,7 @@ if (props.column.channelId == null) {
}
async function setChannel() {
- const channels = await os.api('channels/my-favorites', {
+ const channels = await misskeyApi('channels/my-favorites', {
limit: 100,
});
const { canceled, result: channel } = await os.select({
@@ -60,7 +61,7 @@ async function setChannel() {
async function post() {
if (!channel.value || channel.value.id !== props.column.channelId) {
- channel.value = await os.api('channels/show', {
+ channel.value = await misskeyApi('channels/show', {
channelId: props.column.channelId,
});
}
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 9cb0bb2d43..07845bacbb 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index 49fdf4d314..70b55e8172 100644
--- a/packages/frontend/src/ui/deck/deck-store.ts
+++ b/packages/frontend/src/ui/deck/deck-store.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -7,7 +7,7 @@ import { throttle } from 'throttle-debounce';
import { markRaw } from 'vue';
import { notificationTypes } from 'misskey-js';
import { Storage } from '@/pizzax.js';
-import { api } from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { deepClone } from '@/scripts/clone.js';
type ColumnWidget = {
@@ -70,7 +70,7 @@ export const loadDeck = async () => {
let deck;
try {
- deck = await api('i/registry/get', {
+ deck = await misskeyApi('i/registry/get', {
scope: ['client', 'deck', 'profiles'],
key: deckStore.state.profile,
});
@@ -95,7 +95,7 @@ export const loadDeck = async () => {
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
export const saveDeck = throttle(1000, () => {
- api('i/registry/set', {
+ misskeyApi('i/registry/set', {
scope: ['client', 'deck', 'profiles'],
key: deckStore.state.profile,
value: {
@@ -106,13 +106,13 @@ export const saveDeck = throttle(1000, () => {
});
export async function getProfiles(): Promise<string[]> {
- return await api('i/registry/keys', {
+ return await misskeyApi('i/registry/keys', {
scope: ['client', 'deck', 'profiles'],
});
}
export async function deleteProfile(key: string): Promise<void> {
- return await api('i/registry/remove', {
+ return await misskeyApi('i/registry/remove', {
scope: ['client', 'deck', 'profiles'],
key: key,
});
diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue
index fd08623462..e011de0e3b 100644
--- a/packages/frontend/src/ui/deck/direct-column.vue
+++ b/packages/frontend/src/ui/deck/direct-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 7166561c7a..70ea54326f 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -19,6 +19,7 @@ import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -40,7 +41,7 @@ watch(withRenotes, v => {
});
async function setList() {
- const lists = await os.api('users/lists/list');
+ const lists = await misskeyApi('users/lists/list');
const { canceled, result: list } = await os.select({
title: i18n.ts.selectList,
items: lists.map(x => ({
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index c2b8f19079..79c9671917 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -1,14 +1,14 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked">
<template #header>
- <template v-if="pageMetadata?.value">
- <i :class="pageMetadata?.value.icon"></i>
- {{ pageMetadata?.value.title }}
+ <template v-if="pageMetadata">
+ <i :class="pageMetadata.icon"></i>
+ {{ pageMetadata.title }}
</template>
</template>
@@ -19,15 +19,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ComputedRef, provide, shallowRef, ref } from 'vue';
+import { provide, shallowRef, ref } from 'vue';
import XColumn from './column.vue';
import { deckStore, Column } from '@/ui/deck/deck-store.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/router.js';
-import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { useScrollPositionManager } from '@/nirax.js';
import { getScrollContainer } from '@/scripts/scroll.js';
+import { mainRouter } from '@/router/main.js';
defineProps<{
column: Column;
@@ -35,12 +35,14 @@ defineProps<{
}>();
const contents = shallowRef<HTMLElement>();
-const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
+const pageMetadata = ref<null | PageMetadata>(null);
provide('router', mainRouter);
-provideMetadataReceiver((info) => {
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
pageMetadata.value = info;
});
+provideReactiveMetadata(pageMetadata);
/*
function back() {
diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue
index b011ba3ca2..81926dd7cb 100644
--- a/packages/frontend/src/ui/deck/mentions-column.vue
+++ b/packages/frontend/src/ui/deck/mentions-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue
index e6729b4d58..451cc58791 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index d9bcf8d95e..eae2ee13f3 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -19,6 +19,7 @@ import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store.js';
import MkTimeline from '@/components/MkTimeline.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -35,7 +36,7 @@ onMounted(() => {
});
async function setRole() {
- const roles = (await os.api('roles/list')).filter(x => x.isExplorable);
+ const roles = (await misskeyApi('roles/list')).filter(x => x.isExplorable);
const { canceled, result: role } = await os.select({
title: i18n.ts.role,
items: roles.map(x => ({
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index 7ed0f56d02..f9066d9db7 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index ef35d885f3..9995996771 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
index f32f2de3df..db5eb19c20 100644
--- a/packages/frontend/src/ui/minimum.vue
+++ b/packages/frontend/src/ui/minimum.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,21 +14,29 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { provide, ComputedRef, ref } from 'vue';
+import { computed, provide, ref } from 'vue';
import XCommon from './_common_/common.vue';
-import { mainRouter } from '@/router.js';
-import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { instanceName } from '@/config.js';
+import { mainRouter } from '@/router/main.js';
-const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
+const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
+
+const pageMetadata = ref<null | PageMetadata>(null);
provide('router', mainRouter);
-provideMetadataReceiver((info) => {
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
pageMetadata.value = info;
- if (pageMetadata.value.value) {
- document.title = `${pageMetadata.value.value.title} | ${instanceName}`;
+ if (pageMetadata.value) {
+ if (isRoot.value && pageMetadata.value.title === instanceName) {
+ document.title = pageMetadata.value.title;
+ } else {
+ document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ }
}
});
+provideReactiveMetadata(pageMetadata);
document.documentElement.style.overflowY = 'scroll';
</script>
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index f46f55d988..3cb6f598a6 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="isMobile" ref="navFooter" :class="$style.nav">
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
- <button :class="$style.navButton" class="_button" @click="mainRouter.currentRoute.value.name === 'index' ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
+ <button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
<span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
@@ -105,18 +105,20 @@ import { defaultStore } from '@/store.js';
import { navbarItemDef } from '@/navbar.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
-import { mainRouter } from '@/router.js';
-import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { miLocalStorage } from '@/local-storage.js';
import { CURRENT_STICKY_BOTTOM } from '@/const.js';
import { useScrollPositionManager } from '@/nirax.js';
+import { mainRouter } from '@/router/main.js';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
+const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
+
const DESKTOP_THRESHOLD = 1100;
const MOBILE_THRESHOLD = 500;
@@ -127,18 +129,24 @@ window.addEventListener('resize', () => {
isMobile.value = deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD;
});
-const pageMetadata = ref<null | PageMetadata>();
+const pageMetadata = ref<null | PageMetadata>(null);
const widgetsShowing = ref(false);
const navFooter = shallowRef<HTMLElement>();
const contents = shallowRef<InstanceType<typeof MkStickyContainer>>();
provide('router', mainRouter);
-provideMetadataReceiver((info) => {
- pageMetadata.value = info.value;
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
+ pageMetadata.value = info;
if (pageMetadata.value) {
- document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ if (isRoot.value && pageMetadata.value.title === instanceName) {
+ document.title = pageMetadata.value.title;
+ } else {
+ document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ }
}
});
+provideReactiveMetadata(pageMetadata);
const menuIndicated = computed(() => {
for (const def in navbarItemDef) {
@@ -448,7 +456,7 @@ $widgets-hide-threshold: 1090px;
left: 0;
color: var(--indicator);
font-size: 16px;
- animation: blink 1s infinite;
+ animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue
index 376563bd56..fc0a4475d2 100644
--- a/packages/frontend/src/ui/universal.widgets.vue
+++ b/packages/frontend/src/ui/universal.widgets.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 1fb77e42dc..29b305d9bc 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -1,13 +1,13 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="mk-app">
- <a v-if="root" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
+ <a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
- <div v-if="!narrow && !root" class="side">
+ <div v-if="!narrow && !isRoot" class="side">
<div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>
<div class="dashboard">
<MkVisitorDashboard/>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="main">
- <div v-if="!root" class="header">
+ <div v-if="!isRoot" class="header">
<div v-if="narrow === false" class="wide">
<MkA to="/" class="link" activeClass="active"><i class="ti ti-home icon"></i> {{ i18n.ts.home }}</MkA>
<MkA v-if="isTimelineAvailable" to="/timeline" class="link" activeClass="active"><i class="ti ti-message icon"></i> {{ i18n.ts.timeline }}</MkA>
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div class="contents">
- <main v-if="!root" style="container-type: inline-size;">
+ <main v-if="!isRoot" style="container-type: inline-size;">
<RouterView/>
</main>
<main v-else>
@@ -69,31 +69,40 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { ComputedRef, onMounted, provide, ref, computed } from 'vue';
+import { onMounted, provide, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import XCommon from './_common_/common.vue';
import { instanceName } from '@/config.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { instance } from '@/instance.js';
import XSigninDialog from '@/components/MkSigninDialog.vue';
import XSignupDialog from '@/components/MkSignupDialog.vue';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
-import { mainRouter } from '@/router.js';
-import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
+import { mainRouter } from '@/router/main.js';
+
+const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
const DESKTOP_THRESHOLD = 1100;
-const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
+const pageMetadata = ref<null | PageMetadata>(null);
provide('router', mainRouter);
-provideMetadataReceiver((info) => {
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
pageMetadata.value = info;
- if (pageMetadata.value.value) {
- document.title = `${pageMetadata.value.value.title} | ${instanceName}`;
+ if (pageMetadata.value) {
+ if (isRoot.value && pageMetadata.value.title === instanceName) {
+ document.title = pageMetadata.value.title;
+ } else {
+ document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ }
}
});
+provideReactiveMetadata(pageMetadata);
const announcements = {
endpoint: 'announcements',
@@ -119,9 +128,7 @@ const keymap = computed(() => {
};
});
-const root = computed(() => mainRouter.currentRoute.value.name === 'index');
-
-os.api('meta', { detail: true }).then(res => {
+misskeyApi('meta', { detail: true }).then(res => {
meta.value = res;
});
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index b819b6ca0a..bb8cffaf52 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,24 +22,32 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { provide, ComputedRef, ref } from 'vue';
+import { computed, provide, ref } from 'vue';
import XCommon from './_common_/common.vue';
-import { mainRouter } from '@/router.js';
-import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
+import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { instanceName, ui } from '@/config.js';
import { i18n } from '@/i18n.js';
+import { mainRouter } from '@/router/main.js';
-const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
+const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
+
+const pageMetadata = ref<null | PageMetadata>(null);
const showBottom = !(new URLSearchParams(location.search)).has('zen') && ui === 'deck';
provide('router', mainRouter);
-provideMetadataReceiver((info) => {
+provideMetadataReceiver((metadataGetter) => {
+ const info = metadataGetter();
pageMetadata.value = info;
- if (pageMetadata.value.value) {
- document.title = `${pageMetadata.value.value.title} | ${instanceName}`;
+ if (pageMetadata.value) {
+ if (isRoot.value && pageMetadata.value.title === instanceName) {
+ document.title = pageMetadata.value.title;
+ } else {
+ document.title = `${pageMetadata.value.title} | ${instanceName}`;
+ }
}
});
+provideReactiveMetadata(pageMetadata);
function goToMisskey() {
window.location.href = '/';
diff --git a/packages/frontend/src/unicode-emoji-indexes/ja-JP.json b/packages/frontend/src/unicode-emoji-indexes/ja-JP.json
new file mode 100644
index 0000000000..9c491804f2
--- /dev/null
+++ b/packages/frontend/src/unicode-emoji-indexes/ja-JP.json
@@ -0,0 +1,1866 @@
+{
+ "😀":["にやにやした顔","顔","にやにや","幸せ","しあわせ"],
+ "😃":["口を開けた笑顔","顔","口","開ける","笑顔","幸せ","しあわせ"],
+ "😄":["口を開けて目が笑っている笑顔","目","顔","口","開ける","笑顔","幸せ","しあわせ"],
+ "😁":["にやにやした顔","目","顔","にやにや","笑顔"],
+ "😆":["口を開けて笑っている顔","顔","笑い","口","開ける","満足","笑顔"],
+ "😅":["口を開けて冷や汗をかいた笑顔","ぞっとする","顔","口を開ける","笑顔","冷や汗"],
+ "😂":["嬉し泣き","顔","嬉しい","うれしい","笑う","泣く","涙"],
+ "🤣":["大爆笑","顔","床","笑い","大笑い","爆笑","ぐるぐる"],
+ "😇":["天使の笑顔","天使","顔","おとぎ話","ファンタジー","天使の輪","無邪気","笑顔"],
+ "😉":["ウインクした顔","顔","ウインク"],
+ "😊":["目が笑っている笑顔","赤面","目","顔","笑顔"],
+ "🙂":["微笑み","顔","笑顔","幸せ","しあわせ"],
+ "🙃":["逆さの顔","顔","逆さ","さかさ"],
+ "☺️":["笑顔","顔","輪郭","リラックス"],
+ "😋":["食べ物を味わう顔","美味しい","おいしい","顔","味わう","ふーむ","うまい"],
+ "😌":["ほっとした顔","顔","安心","ほっとする"],
+ "😍":["目がハートの笑顔","目","顔","ハート","愛","笑顔"],
+ "🥰":["笑顔とハート","顔","敬愛","べたぼれ","愛"],
+ "😘":["投げキッス","顔","ハート","キス"],
+ "😗":["キスをする顔","顔","キス"],
+ "😙":["笑顔でキス","目","顔","キス","笑顔"],
+ "😚":["目を閉じてキスをする顔","閉じた","目","顔","キス"],
+ "🥲":["涙の出ている笑顔","泣く","幸せ","感謝する","誇りに思う","安心する","笑う"],
+ "🤪":["おどけた顔","目","にやにや","変","興奮","ワイルド"],
+ "😜":["舌を出してウインクしている顔","目","顔","冗談","舌","ウインク"],
+ "😝":["舌を出して目を細めている顔","目","顔","怖い","恐い","こわい","味","舌"],
+ "😛":["舌を出している顔","顔","舌"],
+ "🤑":["強欲な顔","顔","お金","口"],
+ "😎":["サングラスをかけた顔","明るい","かっこいい","目","アイウエア","顔","眼鏡","メガネ","笑顔","太陽","サングラス","天気"],
+ "🤓":["オタク","顔","変な人"],
+ "🥸":["仮装した顔","仮装","メガネ","匿名の人","鼻"],
+ "🧐":["片メガネをかけた顔","退屈","裕福","豊か"],
+ "🤠":["カウボーイハットの顔","カウボーイ","カウガール","顔","帽子"],
+ "🥳":["パーティーフェイス","顔","祝典","帽子","角","パーティー"],
+ "🤡":["ピエロの顔","ピエロ","顔"],
+ "😏":["にやにやした顔","顔","にやにや"],
+ "😶":["口のない顔","顔","口","静かに","沈黙"],
+ "🫥":["点線の顔","落ち込んだ","消える","隠れる","内向的","目に見えない"],
+ "😐":["普通の顔","無表情","顔","平静"],
+ "🫤":["口が斜めになった顔","がっかり","無関心","疑い深い","不安"],
+ "😑":["無表情","顔","ポーカーフェイス","無感情"],
+ "😒":["面白くなさそうな顔","顔","つまらない","不幸"],
+ "🙄":["ぐるぐる目の顔","目","顔","ぐるぐる"],
+ "🤨":["眉が上がっている顔","不信","疑い深い","非難","疑念","やや驚き","懐疑的"],
+ "🤔":["考えている顔","顔","考え中"],
+ "🤫":["シッと言っている顔","シーッ","静か","黙る"],
+ "🤭":["口を手で覆った顔","目","笑顔","覆う","口","手"],
+ "🫢":["目を開いて口を手で覆った顔","驚嘆","畏敬","不信","狼狽","怖い","驚き"],
+ "🫡":["敬礼している顔","ok","敬礼","晴天","部隊","はい"],
+ "🤗":["両手を広げた笑顔","顔","ハグ","抱きしめる"],
+ "🫣":["のぞき見している顔","魅了","のぞき見","凝視","チラ見"],
+ "🤥":["嘘つき顔","顔","嘘","うそ","ピノキオ"],
+ "😳":["赤くなった顔","ぼーっとした","ぼうっとした","顔","赤面"],
+ "😞":["がっかりした顔","がっかり","顔"],
+ "😟":["不安な顔","顔","心配","不安"],
+ "😤":["勝ち誇った顔","顔","勝利","勝つ"],
+ "😠":["怒った顔","怒り","怒った","顔","激怒"],
+ "😡":["ふくれ顔","怒り","怒った","顔","激怒","ふくれっ面","ふくれっつら","憤怒","赤"],
+ "🤬":["口が記号で覆われた顔","呪い","ののしり","罵り"],
+ "😔":["悲しげな顔","がっかり","顔","悲しい"],
+ "😕":["困った顔","困った","こまった","顔"],
+ "🙁":["ご機嫌斜め","顔","しかめっ面","しかめっつら","悲しい","不幸"],
+ "☹":["しかめっつら","顔","しかめっ面","悲しい","不幸"],
+ "😬":["しかめっ面","顔","しかめっつら"],
+ "🥺":["訴えかける顔","顔","物乞い","慈悲","子犬の目"],
+ "😣":["我慢している顔","顔","がんばる","頑張る"],
+ "😖":["うろたえた顔","戸惑い","とまどい","うろたえ","顔"],
+ "😫":["疲れた顔","顔","疲れた","つかれた"],
+ "😩":["うんざりしている顔","顔","疲れた","つかれた","うんざり"],
+ "🥱":["あくびしている顔","飽きた","疲れた","あくび"],
+ "😪":["眠い顔","顔","寝る","睡眠"],
+ "😮‍💨":["ため息の出ている顔","顔","ため息","息切れ","うめき","安心","ささやき","口笛"],
+ "😮":["口を開けた笑顔","顔","口","開ける","同情"],
+ "😱":["絶叫した顔","顔","恐怖","怖い","恐い","こわい","ムンク","怯え","絶叫"],
+ "😨":["ゾッとしている顔","顔","恐怖","恐い","怖い","こわい","怯え"],
+ "😰":["口を開けて冷や汗をかいた顔","青ざめる","ぞっとする","顔","口","開ける","急ぐ","冷や汗"],
+ "😥":["がっかりしたが安心した顔","がっかり","顔","安心","ほっとする","やれやれ"],
+ "😓":["冷や汗をかいている顔","ぞっとする","顔","冷や汗"],
+ "😯":["落ち着いた顔","顔","黙る","呆然","驚き"],
+ "😦":["心配そうな顔の絵文字","顔","しかめっ面","しかめっつら","口","開ける"],
+ "😧":["苦悩に満ちた顔","苦悩","顔"],
+ "🥹":["涙をこらえている顔","怒る","泣く","誇りに思う","逆らう","悲しむ"],
+ "😢":["泣き顔","泣く","顔","悲しい","涙"],
+ "😭":["号泣","泣く","顔","悲しい","涙"],
+ "🤤":["よだれを垂らした顔","よだれ","顔"],
+ "🤩":["スターに夢中","目","顔","にやにや","星","夢想的"],
+ "😵":["目がバツになった顔","めまい","顔","バツ","目"],
+ "😵‍💫":["目がぐるぐるしている顔","めまい","顔","目","うっとり","ぐるぐる","トラブル","おー"],
+ "🥴":["ぼんやしりた顔","顔","目まい","酩酊","ほろ酔い","まっすぐでない目","波状の口"],
+ "😲":["驚いた顔","驚き","びっくり","顔","ショック","驚愕"],
+ "🫨":["震える顔","地震","顔","震え","衝撃","振動"],
+ "🤯":["爆発した頭","顔","ショック","爆発","狂気","びっくり"],
+ "🫠":["ほろりとした顔","消える","溶解する","液体","溶ける"],
+ "🤐":["お口チャック","顔","口","チャック"],
+ "😷":["マスクをした顔","風邪","かぜ","医者","顔","マスク","薬","病気"],
+ "🤕":["怪我","包帯","顔","傷","キズ","けが"],
+ "🤒":["温度計をくわえた顔","顔","病気","風邪","かぜ","体温計"],
+ "🤮":["吐きそうな顔","病気","嘔吐","風邪","かぜ","吐く"],
+ "🤢":["吐きそうな顔","顔","吐き気","嘔吐"],
+ "🤧":["くしゃみをする顔","顔","くしゃみ","ハクション"],
+ "🥵":["ほてった顔","顔","熱っぽい","熱射病","ほてった","赤ら顔","汗をかいた"],
+ "🥶":["青ざめた顔","顔","ぞっとする","凍える","凍傷","つらら"],
+ "😶‍🌫️":["雲で覆われた顔","顔","おっちょこちょい","非現実的","夢","もや","雲で覆われた頭"],
+ "😴":["寝顔","顔","寝る","睡眠","スヤスヤ"],
+ "💤":["睡眠","マンガ","漫画","寝る","スヤスヤ"],
+ "😈":["角つき笑顔","顔","おとぎ話","ファンタジー","角","笑顔"],
+ "👿":["小悪魔","鬼","悪魔","顔","おとぎ話","ファンタジー"],
+ "👹":["鬼","妖怪","顔","昔話","ファンタジー","日本","モンスター"],
+ "👺":["天狗","妖怪","顔","昔話","ファンタジー","日本","モンスター"],
+ "💩":["うんち","マンガ","漫画","フン","顔","モンスター"],
+ "👻":["お化け","妖怪","顔","おとぎ話","ファンタジー","幽霊","モンスター","ハロウィーン"],
+ "💀":["ドクロ","体","死","顔","おとぎ話","モンスター","骸骨","ハロウィーン"],
+ "☠":["ドクロマーク","体","交差した骨","死","顔","モンスター","骸骨","ハロウィーン"],
+ "👽":["宇宙人","怪獣","異星人","顔","おとぎ話","ファンタジー","モンスター","宇宙","UFO"],
+ "🤖":["ロボットの顔","顔","モンスター","ロボット"],
+ "🎃":["ジャック・オ・ランタン","イベント","お祝い","エンタメ","ハロウィン","ジャックオランタン","ランタン","かぼちゃ"],
+ "😺":["口を開けて笑う猫","猫","ネコ","顔","口","開ける","笑顔"],
+ "😸":["ニヤニヤ笑う猫","猫","ネコ","目","顔","ニヤニヤ","笑顔"],
+ "😹":["嬉し泣きしたネコの顔","猫","ネコ","顔","嬉しい","うれしい","涙"],
+ "😻":["ハートの目をした猫の笑顔","猫","ネコ","目","顔","ハート","愛","笑顔"],
+ "😼":["ニヤリと笑う猫の顔","猫","ネコ","顔","皮肉","笑顔","ニヤリ"],
+ "😽":["目を閉じてキスをする猫","猫","ネコ","目","顔","キス"],
+ "🙀":["疲れたネコの顔","猫","ネコ","顔","びっくり","驚く","うんざり"],
+ "😿":["泣いたネコの顔","猫","ネコ","泣く","顔","悲しい","涙"],
+ "😾":["怒ったネコの顔","猫","ネコ","顔","怒る","ふくれっ面","ふくれっつら"],
+ "🫶":["ハートポーズ","愛"],
+ "👐":["開いた手","体","手","広げる"],
+ "🤲":["上に向けた両手のひら","体","祈り","カップのように丸めた手"],
+ "🙌":["両手を上げる","体","お祝い","ジェスチャー","手","バンザイ","万歳","挙げる"],
+ "👏":["拍手","体","手を叩く","手"],
+ "🙏":["握った手","頼む","体","お辞儀","手を合わせる","ジェスチャー","手","お願い","祈る","ありがとう","感謝"],
+ "🤝":["握手","合意","手","手を結ぶ","会議"],
+ "👍":["イイね","体","上","手","指","サムズアップ","+1"],
+ "👎":["ダメ","体","下","手","指","サムズダウン","-1"],
+ "👊":["握りこぶし","体","握る","拳","こぶし","グー","手","パンチ","接近"],
+ "✊":["こぶし","体","握る","拳","グー","手","パンチ"],
+ "🤛":["左向きのこぶし","体","拳","左向き"],
+ "🤜":["右向きのこぶし","体","拳","右向き"],
+ "🤞":["交差させた指","体","交差","指","手","幸運"],
+ "✌":["Vサイン","体","手","V","ブイ","勝つ","勝利","ピース"],
+ "🫰":["人差し指と親指を交差した手","高い","ハート","愛","お金","スナップ"],
+ "🤘":["コルナ","体","指","手","角","最高"],
+ "🤟":["愛してるのジェスチャー","体","愛してる","好き","手"],
+ "👌":["OKサイン","体","手","OK"],
+ "🤌":["つまんでいる指","指","手ぶり","尋問","つまむ","皮肉"],
+ "🤏":["つまんでいる手","体","手","小さい","小型","ちっちゃい"],
+ "👈":["左指差し","手の甲","体","指","手","人差し指","指さす"],
+ "🫳":["手のひらを下にした手","退ける","落とす","シッシ"],
+ "🫴":["手のひらを上にした手","手招き","捕獲","来る","申し出"],
+ "👉":["指差し","手の甲","体","指","手","人差し指","指さす"],
+ "👆":["指差し","手の甲","体","指","手","人差し指","指さす","上"],
+ "👇":["指差し","手の甲","体","下","指","手","人差し指","指さす"],
+ "☝":["指差し","体","指","手","人差し指","指さす","上"],
+ "✋":["挙手","体","手"],
+ "🤚":["手の甲","体","挙げる"],
+ "🖐":["広げた手のひら","体","指","手","広げる"],
+ "🖖":["長寿と繁栄を","体","指","手","スポック","バルカン"],
+ "👋":["バイバイ","体","手","振る","やっほー","ヤッホー","こんにちは"],
+ "🤙":["電話の形の手","体","電話","手"],
+ "🫲":["左手","手","左","ひだり"],
+ "🫱":["右手","手","右","みぎ"],
+ "🫷":["左を押している手","辞退","ハイタッチ","左方向","押し付ける","断る","停止","待つ"],
+ "🫸":["右を押している手","辞退","ハイタッチ","押し付ける","断る","右方向","停止","待つ"],
+ "💪":["曲げた上腕二頭筋","力こぶ","体","マンガ","漫画","運動","筋肉","力","マッスル","マッチョ"],
+ "🦾":["メカニカルアーム","アクセシビリティ","義手","人口装具","体"],
+ "🖕":["中指を立てた手","体","指","手","中指"],
+ "🫵":["見ている人を指している人差し指","指す","あなた","指"],
+ "✍":["書いている手","体","手","書く"],
+ "🤳":["自撮り","カメラ","携帯","腕"],
+ "💅":["マニキュア","体","ケア","化粧品","コスメ","爪","ネイル"],
+ "🦵":["脚","体","キック","手足"],
+ "🦿":["機械の脚","アクセシビリティ","義足","人口装具","体"],
+ "🦶":["足","体","キック","踏みつける"],
+ "👄":["口","体","唇","クチビル"],
+ "🫦":["かんでいる唇","心配","怖い","浮気","神経質","不愉快","不安"],
+ "🦷":["歯","体","歯医者"],
+ "👅":["舌","体"],
+ "👂":["耳","体","鼻"],
+ "🦻":["補聴器を付けている耳","アクセシビリティ","補聴器","聞く","体","耳"],
+ "👃":["鼻","体"],
+ "👁":["目","体"],
+ "👀":["目","体","顔"],
+ "🧠":["脳","体","臓器","知的","賢い"],
+ "🫀":["解剖学的な心臓","解剖学","心臓学","心臓","臓器","脈"],
+ "🫁":["肺","息","呼気","吸入","臓器","呼吸"],
+ "🦴":["骨","体","骨格"],
+ "👤":["上半身のシルエット","上半身","シルエット"],
+ "👥":["上半身のシルエット","上半身","シルエット"],
+ "🗣":["喋る頭のシルエット","顔","頭","シルエット","しゃべる","話す"],
+ "🫂":["ハグしている人たち","さようなら","こんにちは","ハグ","ありがとう"],
+ "👶":["赤ちゃん"],
+ "👧":["女の子","少女","処女","おとめ座","星座","子供"],
+ "🧒":["子供","人","少年","少女"],
+ "👦":["男の子","少年","子供"],
+ "👩":["女性","女","おんな"],
+ "🧑":["成人向け","人","大人","男性","女性","女","男","おとこ","おんな"],
+ "👨":["男性","口ひげ","男","おとこ"],
+ "👩‍🦱":["女性,巻き毛","巻き毛","髪","女性","女","おんな"],
+ "🧑‍🦱":["人,巻き毛","巻き毛","髪"],
+ "👨‍🦱":["男性,巻き毛","巻き毛","髪","男性","男","おとこ"],
+ "👩‍🦰":["女性,赤毛","赤","髪","女性","女","おんな"],
+ "🧑‍🦰":["人,赤毛","赤","髪"],
+ "👨‍🦰":["男性,赤毛","赤","髪","男性","男","おとこ"],
+ "👱‍♀️":["女性,金髪","ブロンド","髪","女","おんな"],
+ "👱":["人,金髪","金髪","ブロンド","髪"],
+ "👱‍♂️":["男性,金髪","ブロンド","髪","男","男性","おとこ"],
+ "👩‍🦳":["女性,白髪","白","髪","女性","女","おんな"],
+ "🧑‍🦳":["人,白髪","白","髪"],
+ "👨‍🦳":["男性,白髪","白","髪","男性","男","おとこ"],
+ "👩‍🦲":["女性,禿","禿","女性","女","おんな"],
+ "🧑‍🦲":["人,禿","禿"],
+ "👨‍🦲":["男性,禿","禿","男性","男","おとこ"],
+ "🧔‍♀️":["ひげのある女性","あごひげ","ひげを生やした","女性","女","おんな"],
+ "🧔":["あごひげのある人","あごひげ","ひげを生やした"],
+ "🧔‍♂️":["ひげのある男性","あごひげ","ひげを生やした","男性","男","おとこ"],
+ "👵":["おばあさん","おばあちゃん","老人","女性","女","おんな"],
+ "🧓":["高齢者","人","男性","女性","女","男","おとこ","おんな"],
+ "👴":["おじいさん","おじいちゃん","老人","男","おとこ","男性"],
+ "👲":["スカルキャップをかぶっている人","中国帽","帽子"],
+ "👳‍♀️":["ターバンを巻いている女性","ターバン","女性","女","おんな"],
+ "👳":["ターバンを巻いている人","ターバン"],
+ "👳‍♂️":["ターバンを巻いている男性","ターバン","男","おとこ","男性"],
+ "🧕":["ヘッドスカーフをかぶった女性","ヘッドスカーフ","ヒジャブ","マンティラ","ティチェル","バンダナ","頭のスカーフ","女性","女","おんな"],
+ "👮‍♀️":["女性警察官","警察官","警官","警察","女性","女","おんな"],
+ "👮":["警察官","警官","警察"],
+ "👮‍♂️":["男性警察官","警察官","警官","警察","男","おとこ","男性"],
+ "👩‍🚒":["女性消防士","火","火事","消防","消防士","女性","女","おんな"],
+ "🧑‍🚒":["消防士","火事"],
+ "👨‍🚒":["男性消防士","火","火事","消防","消防士","男","おとこ","男性"],
+ "👷‍♀️":["女性の建設作業員","工事","建設","作業員","女性","女","おんな"],
+ "👷":["建設作業員","工事","建設","作業員"],
+ "👷‍♂️":["男性の建設作業員","建設","作業員","男性","男","おとこ"],
+ "👩‍🏭":["男性の工場作業員","工場","工業","作業員","女性","女","おんな"],
+ "🧑‍🏭":["工場作業員","工場","工業","溶接"],
+ "👨‍🏭":["男性の工場作業員","工場","工業","作業員","男","おとこ","男性"],
+ "👩‍🔧":["女性整備士","職人","配管工","電気技師","修理人","女性","女","おんな"],
+ "🧑‍🔧":["整備士","職人","配管工","電気技師","修理人"],
+ "👨‍🔧":["男性整備士","職人","配管工","電気技師","修理人","男","おとこ","男性"],
+ "👩‍🌾":["女性の農業従事者","農場労働者","牧場主","庭師","農家","女性","女","おんな"],
+ "🧑‍🌾":["農業従事者","農場労働者","牧場主","庭師","農家"],
+ "👨‍🌾":["男性の農業従事者","農場労働者","牧場主","庭師","農家","男","おとこ","男性"],
+ "👩‍🍳":["女性の料理人","食品","サービス","シェフ","コック","料理人","料理","女性","女","おんな"],
+ "🧑‍🍳":["料理人","食品","サービス","シェフ","コック","料理"],
+ "👨‍🍳":["男性の料理人","食品","サービス","シェフ","コック","料理人","料理","男","おとこ","男性"],
+ "👩‍🎤":["男性シンガー","音楽","ミュージシャン","ロック","ロッカー","ロックスター","芸能人","女性","女","おんな"],
+ "🧑‍🎤":["歌手","音楽","ミュージシャン","ロック","ロッカー","ロックスター","芸能人"],
+ "👨‍🎤":["男性シンガー","音楽","ミュージシャン","ロック","ロッカー","ロックスター","芸能人","男","おとこ","男性"],
+ "👩‍🎨":["女性アーティスト","芸術","アート","芸術家","アーティスト","絵画","画家","女性","女","おんな"],
+ "🧑‍🎨":["アーティスト","芸術","アート","芸術家","絵画","画家"],
+ "👨‍🎨":["男性アーティスト","芸術","アート","芸術家","アーティスト","絵画","画家","男","おとこ","男性"],
+ "👩‍🏫":["女性の教師","教育","先生","教授","教師","講師","女性","女","おんな"],
+ "🧑‍🏫":["教師","教育","先生","教授","講師"],
+ "👨‍🏫":["男性の教師","教育","先生","教授","教師","講師","男","おとこ","男性"],
+ "👩‍🎓":["女子生徒","学生","卒業生","教育","学校","女性","女","おんな"],
+ "🧑‍🎓":["生徒","学生","卒業生","教育","学校"],
+ "👨‍🎓":["男子生徒","学生","卒業生","教育","学校","男","おとこ","男性"],
+ "👩‍💼":["男性会社員","オフィス","会計士","銀行家","管理職","顧問","事務員","アナリスト","女性","女","おんな"],
+ "🧑‍💼":["会社員","オフィス","会計士","銀行家","管理職","顧問","事務員","アナリスト"],
+ "👨‍💼":["男性会社員","オフィス","会計士","銀行家","管理職","顧問","事務員","アナリスト","男","おとこ","男性"],
+ "👩‍💻":["女性技術者","テクノロジー","ソフトウェア","エンジニア","プログラマー","ラップトップ","ノートパソコン","女性","女","おんな"],
+ "🧑‍💻":["技術者","テクノロジー","ソフトウェア","エンジニア","プログラマー","ラップトップ","ノートパソコン"],
+ "👨‍💻":["男性技術者","テクノロジー","ソフトウェア","エンジニア","プログラマー","ラップトップ","ノートパソコン","男","おとこ","男性"],
+ "👩‍🔬":["女性科学者","科学者","化学者","技術者","数学者","物理学者","生物学者","検査技師","女性","女","おんな"],
+ "🧑‍🔬":["科学者","化学者","技術者","数学者","物理学者","生物学者","検査技師"],
+ "👨‍🔬":["男性科学者","科学者","化学者","技術者","数学者","物理学者","生物学者","検査技師","男","おとこ","男性"],
+ "👩‍🚀":["女性宇宙飛行士","宇宙","星","月","惑星","女性","女","おんな"],
+ "🧑‍🚀":["宇宙飛行士","宇宙","星","月","惑星"],
+ "👨‍🚀":["男性宇宙飛行士","宇宙","星","月","惑星","男","おとこ","男性"],
+ "👩‍⚕️":["女性医療関係者","医師","内科医","医学博士","看護師","歯科医","医療専門家","療法士","女性","女","おんな"],
+ "🧑‍⚕️":["医療関係者","医師","内科医","医学博士","看護師","歯科医","医療専門家","療法士"],
+ "👨‍⚕️":["男性医療関係者","医師","内科医","医学博士","看護師","歯科医","医療専門家","療法士","男","おとこ","男性"],
+ "👩‍⚖️":["女性裁判官","裁判官","法廷","裁判所","法律","女性","女","おんな"],
+ "🧑‍⚖️":["裁判官","法廷","裁判所","法律"],
+ "👨‍⚖️":["男性裁判官","裁判官","法廷","裁判所","法律","男","おとこ","男性"],
+ "👩‍✈️":["女性パイロット","パイロット","飛行機","操縦士","航空","女性","女","おんな"],
+ "🧑‍✈️":["パイロット","飛行機","操縦士","航空"],
+ "👨‍✈️":["男性パイロット","パイロット","飛行機","操縦士","航空","男","おとこ","男性"],
+ "💂‍♀️":["女性警備員","警備員","警備","女性","女","おんな"],
+ "💂":["警備員","警備"],
+ "💂‍♂️":["男性警備員","警備員","警備","男","おとこ","男性"],
+ "🥷":["忍者","戦士","隠された","ステルス"],
+ "🕵️‍♀️":["女性の探偵","探偵","刑事","スパイ","女性","女","おんな"],
+ "🕵":["探偵","刑事","スパイ"],
+ "🕵️‍♂️":["男性の探偵","探偵","刑事","スパイ","男","おとこ","男性"],
+ "🤶":["ミセス・クロース","イベント","お祝い","クリスマス","母","サンタ","クロース","女性","女","おんな"],
+ "🧑‍🎄":["ミクスクロース","アクティビティ","お祝い","クリスマス","サンタ","クロース"],
+ "🎅":["サンタクロース","イベント","お祝い","クリスマス","父","サンタ","クロース","男","おとこ","男性"],
+ "👼":["天使の赤ちゃん","天使","赤ちゃん","顔","おとぎ話","ファンタジー"],
+ "👸":["お姫さま","おとぎ話","ファンタジー","女王","女性","女","おんな"],
+ "🫅":["王冠をかぶった人","おとぎ話","ファンタジー","国王","貴族","王","王族"],
+ "🤴":["王子様","おとぎ話","ファンタジー","王","男","おとこ","男性"],
+ "👰":["ベールを付けた女性","花嫁","ベール","結婚式","女性","女","おんな"],
+ "👰‍♀️":["ベールを付けた人","花嫁","ベール","結婚式"],
+ "👰‍♂️":["ベールを付けた男性","花嫁","ベール","ウェディング","男性","男","おとこ"],
+ "🤵‍♀️":["タキシードの女性","タキシード","ウェディング","女性","女","おんな"],
+ "🤵":["タキシードを着る人","花婿","タキシード","ウェディング"],
+ "🤵‍♂️":["タキシードの男性","花婿","タキシード","ウェディング","男性","男","おとこ"],
+ "🩷":["ピンクのハート","かわいい","ハート","好き","愛","ピンク"],
+ "🩵":["ライトブルーのハート","シアン","ハート","ライトブルー","コガモ"],
+ "🩶":["グレーのハート","グレー","ハート","シルバー","スレート"],
+ "🕴️‍♀️":["宙に浮いたスーツの女性","ビジネス","スーツ","女性","女","おんな"],
+ "🕴":["宙に浮いたスーツの人","ビジネス","スーツ"],
+ "🕴️‍♂️":["宙に浮いたスーツの男性","ビジネス","スーツ","男","おとこ","男性"],
+ "🦸‍♀️":["女性のスーパーヒーロー","空想","善","ヒロイン","超大国","女性","女","おんな"],
+ "🦸":["スーパーヒーロー","空想","善","ヒーロー","ヒロイン","超大国"],
+ "🦸‍♂️":["男性のスーパーヒーロー","空想","善","ヒーロー","超大国","男性","男","おとこ"],
+ "🦹‍♀️":["女性の悪党","空想","悪","犯罪","悪事","超大国","悪役","女性","女","おんな"],
+ "🦹":["悪党","空想","悪","犯罪","悪事","超大国","悪役"],
+ "🦹‍♂️":["男性の悪党","空想","悪","犯罪","悪事","超大国","悪役","男性","男","おとこ"],
+ "🧙‍♀️":["女性の魔法使い","空想","魔女","女の魔法使い","女性","女","おんな"],
+ "🧙":["魔法使い","空想","魔術師","男の魔法使い"],
+ "🧙‍♂️":["男性の魔法使い","空想","魔術師","男の魔法使い","男性","男","おとこ"],
+ "🧝‍♀️":["女性の小人","空想","小人","先のとがった耳","女性","女","おんな"],
+ "🧝":["小人","空想","先のとがった耳"],
+ "🧝‍♂️":["男性の小人","空想","小人","先のとがった耳","男性","男","おとこ"],
+ "🧚‍♀️":["女性の妖精","空想","ティターニア","ウィングス","女性","女","おんな"],
+ "🧚":["妖精","空想","ティターニア","ウィングス"],
+ "🧚‍♂️":["男性の妖精","空想","オベロン","小妖精","男性","男","おとこ"],
+ "🧞‍♀️":["女性の精霊","空想","精霊","女性","女","おんな"],
+ "🧞":["精霊","空想"],
+ "🧞‍♂️":["男性の精霊","空想","精霊","男性","男","おとこ"],
+ "🧜‍♀️":["女性の人魚","空想","女性","女","おんな"],
+ "🧜":["人魚","空想"],
+ "🧜‍♂️":["男性の人魚","空想","人魚","男性","男","おとこ"],
+ "🧌":["釣り","おとぎ話","ファンタジ","モンスター"],
+ "🧛‍♀️":["女性の吸血鬼","空想","アンデッド","女性","女","おんな"],
+ "🧛":["吸血鬼","空想","ドラキュラ","アンデッド"],
+ "🧛‍♂️":["男性の吸血鬼","空想","ドラキュラ","アンデッド","男性","男","おとこ"],
+ "🧟‍♀️":["女性のゾンビ","空想","アンデッド","女性","女","おんな"],
+ "🧟":["ゾンビ","空想","アンデッド"],
+ "🧟‍♂️":["男性のゾンビ","空想","アンデッド","男性","男","おとこ"],
+ "🙇‍♀️":["深くお辞儀する女性","謝罪","お辞儀","ジェスチャー","ごめんなさい","女性","女","おんな"],
+ "🙇":["深くお辞儀した人","謝罪","お辞儀","ジェスチャー","ごめんなさい"],
+ "🙇‍♂️":["深くお辞儀する男性","謝罪","お辞儀","ジェスチャー","ごめんなさい","男","おとこ","男性"],
+ "💁‍♀️":["案内する女性","手","助け","情報","ずうずうしい","女性","女","おんな"],
+ "💁":["案内する人","手","助け","情報","ずうずうしい","女性","女","おんな"],
+ "💁‍♂️":["案内する男性","手","助け","情報","ずうずうしい","男","おとこ","男性"],
+ "🙅‍♀️":["NGサインの女性","禁じる","ジェスチャー","手","だめ","ダメ","禁止","女性","女","おんな"],
+ "🙅":["NGサインの人","禁じる","ジェスチャー","手","だめ","ダメ","禁止"],
+ "🙅‍♂️":["NGサインの男性","禁じる","ジェスチャー","手","だめ","ダメ","禁止","男","おとこ","男性"],
+ "🙆‍♀️":["OKサインの女性","ジェスチャー","手","ok","女性","女","おんな"],
+ "🙆":["OKサインの人","ジェスチャー","手","OK"],
+ "🙆‍♂️":["OKサインの男性","ジェスチャー","手","ok","男","おとこ","男性"],
+ "🤷‍♀️":["肩をすくめる女性","疑い","無知","無関心","肩をすくめる","女性","女","おんな"],
+ "🤷":["肩をすくめる人","疑い","無知","無関心","肩をすくめる"],
+ "🤷‍♂️":["肩をすくめる男性","疑い","無知","無関心","肩をすくめる","男","おとこ","男性"],
+ "🙋‍♀️":["片手を上げて喜ぶ女性","ジェスチャー","手","幸せ","しあわせ","挙げる","女性","女","おんな"],
+ "🙋":["片手を上げて喜ぶ人","ジェスチャー","手","幸せ","しあわせ","挙げる"],
+ "🙋‍♂️":["片手を上げて喜ぶ男性","ジェスチャー","手","幸せ","しあわせ","挙げる","男","おとこ","男性"],
+ "🤦‍♀️":["顔を押さえる女性","不信","憤慨","顔","手のひら","女性","女","おんな"],
+ "🤦":["手のひらを顔に当てる人","不信","憤慨","顔","手のひら"],
+ "🤦‍♂️":["顔を押さえる男性","不信","憤慨","顔","手のひら","男","おとこ","男性"],
+ "🧏‍♀️":["耳が不自由な女性","アクセシビリティ","耳が不自由","女性","女","おんな"],
+ "🧏":["耳が不自由な人","アクセシビリティ","耳が不自由"],
+ "🧏‍♂️":["耳が不自由な男性","アクセシビリティ","耳が不自由","男性","男","おとこ"],
+ "🙎‍♀️":["ふくれっ面の女性","ジェスチャー","ふくれっ面","ふくれっつら","女性","女","おんな"],
+ "🙎":["怒った顔の人","ジェスチャー","ふくれっ面","ふくれっつら"],
+ "🙎‍♂️":["ふくれっ面の男性","ジェスチャー","ふくれっ面","ふくれっつら","男","おとこ","男性"],
+ "🙍‍♀️":["顔をしかめた女性","しかめ面","ジェスチャー","悲しい","女性","女","おんな"],
+ "🙍":["不満な顔の人","しかめ面","ジェスチャー","悲しい"],
+ "🙍‍♂️":["顔をしかめた男性","しかめ面","ジェスチャー","悲しい","男性","男","おとこ"],
+ "💇‍♀️":["髪を切られている女性","理髪師","美容師","美容","散髪","ヘアカット","美容院","女性","女","おんな"],
+ "💇":["髪を切られている人","理髪師","美容師","美容","散髪","ヘアカット","美容院"],
+ "💇‍♂️":["髪を切られている男性","理髪師","美容師","美容","散髪","ヘアカット","美容院","男","おとこ","男性"],
+ "💆‍♀️":["フェイスマッサージを受ける女性","マッサージ","サロン","女性","女","おんな"],
+ "💆":["フェイスマッサージを受ける人","マッサージ","サロン"],
+ "💆‍♂️":["フェイスマッサージを受ける男性","マッサージ","サロン","男","おとこ","男性"],
+ "🤰":["妊婦","妊娠","赤ちゃん","女性","女","おんな","腹","ふくれた","ふっくらした"],
+ "🫄":["妊娠した人","腹","ふくれた","ふっくらした","妊娠","赤ちゃん"],
+ "🫃":["妊娠している男性","腹","ふくれた","ふっくらした","妊娠","赤ちゃん","男性","男","おとこ"],
+ "🤱":["母乳","胸","赤ちゃん","赤ん坊","乳児","幼児","母","子供","保育","ミルク","女性","女","おんな"],
+ "👩‍🍼":["赤ちゃんにご飯をあげる女性","赤ちゃん","乳児","子供","授乳","ミルク","ボトル","女性","女","おんな"],
+ "🧑‍🍼":["赤ちゃんにご飯をあげる人","赤ちゃん","乳児","子供","授乳","ミルク","ボトル"],
+ "👨‍🍼":["赤ちゃんにご飯をあげる男性","赤ちゃん","乳児","子供","授乳","ミルク","ボトル","男性","男","おとこ"],
+ "🧎‍♀️":["膝立ちしている女性","膝","膝立ち","女性","女","おんな"],
+ "🧎":["膝立ちしている人","膝","膝立ち"],
+ "🧎‍♂️":["膝立ちしている男性","膝","膝立ち","男性","男","おとこ"],
+ "🧍‍♀️":["立っている女性","立つ","スタンディング","女性","女","おんな"],
+ "🧍":["立っている人","立ち","スタンディング"],
+ "🧍‍♂️":["立っている男性","立つ","スタンディング","男性","男","おとこ"],
+ "🚶‍♀️":["歩く女性","ハイキング","歩行者","歩く","ウォーキング","女性","女","おんな"],
+ "🚶":["歩く人","ハイキング","歩行者","歩く","ウォーキング"],
+ "🚶‍♂️":["歩く男性","ハイキング","歩行者","歩く","ウォーキング","男","おとこ","男性"],
+ "👩‍🦯":["白杖を持った女性","アクセシビリティ","目が不自由","女性","女","おんな"],
+ "🧑‍🦯":["白杖を持った人","アクセシビリティ","目が不自由"],
+ "👨‍🦯":["白杖を持った男性","アクセシビリティ","目が不自由","男性","男","おとこ"],
+ "🏃‍♀️":["走る女性","マラソン","ランナー","ランニング","女性","女","おんな"],
+ "🏃":["走る人","マラソン","ランナー","ランニング"],
+ "🏃‍♂️":["走る男性","マラソン","ランナー","ランニング","男","おとこ","男性"],
+ "👩‍🦼":["電動車いすに座っている女性","アクセシビリティ","車いす","女性","女","おんな"],
+ "🧑‍🦼":["電動車いすに座っている人","アクセシビリティ","車いす"],
+ "👨‍🦼":["電動車いすに座っている男性","アクセシビリティ","車いす","男性","男","おとこ"],
+ "👩‍🦽":["手動車いすに座っている女性","アクセシビリティ","車いす","女性","女","おんな"],
+ "🧑‍🦽":["手動車いすに座っている人","アクセシビリティ","車いす"],
+ "👨‍🦽":["手動車いすに座っている男性","アクセシビリティ","車いす","男性","男","おとこ"],
+ "💃":["女性ダンサー","ダンス","踊る","ダンサー","女性","女","おんな"],
+ "🕺":["男性ダンサー","ダンス","踊る","ダンサー","男","おとこ","男性"],
+ "👯‍♀️":["バニーガール","うさぎ耳","ダンサー","女性","女","おんな"],
+ "👯":["うさぎ耳の人","うさぎ耳","ダンサー"],
+ "👯‍♂️":["うさぎ耳の男性","うさぎ耳","ダンサー","男","おとこ","男性"],
+ "👫":["手をつないだ男女","カップル","手","つなぐ","男","女","男女","おとこ","おんな"],
+ "👭":["手をつないだ女性","カップル","手","つなぐ","女性","女","おんな","プライド","lgbt","レズビアン"],
+ "👬":["手をつないだ男性","カップル","手","つなぐ","男性","男","おとこ","プライド","lgbt","ゲイ"],
+ "🧑‍🤝‍🧑":["手をつないだ人たち","カップル","手","握る"],
+ "👩‍❤️‍👨":["ハートのカップル (女性、男性)","カップル","ハート","愛","恋愛","男","女","男女","おとこ","おんな"],
+ "👩‍❤️‍👩":["ハートのカップル (女性、女性)","カップル","ハート","愛","恋愛","女性","女","おんな","プライド","lgbt","レズビアン"],
+ "💑":["ハートのカップル","カップル","ハート","愛","恋愛","男","女","男女","おとこ","おんな"],
+ "👨‍❤️‍👨":["ハートのカップル (男性、男性)","カップル","ハート","愛","恋愛","男性","男","おとこ","プライド","lgbt","ゲイ"],
+ "👩‍❤️‍💋‍👨":["キス (女性、男性)","カップル","キス","ハート","愛","恋愛","男","女","男女","おとこ","おんな"],
+ "👩‍❤️‍💋‍👩":["キス (女性、女性)","カップル","キス","ハート","愛","恋愛","女性","女","おんな","プライド","lgbt","ゲイ"],
+ "💏":["キス","カップル","キス","ハート","愛","恋愛","男","女","男女","おとこ","おんな"],
+ "👨‍❤️‍💋‍👨":["キス (男性、男性)","カップル","キス","ハート","愛","恋愛","男性","男","おとこ","プライド","lgbt","ゲイ"],
+ "👪":["家族","父親","母親","男","女","男女","おとこ","おんな","男の子","こども"],
+ "👨‍👩‍👧":["家族 (男性、女性、女の子)","父親","母親","男","女","男女","おとこ","おんな","女の子","こども"],
+ "👨‍👩‍👧‍👦":["家族 (男性、女性、女の子、男の子)","父親","母親","男","女","男女","おとこ","おんな","男の子","女の子","こども"],
+ "👨‍👩‍👦‍👦":["家族 (男性、女性、男の子、男の子)","父親","母親","男","女","男女","おとこ","おんな","男の子","こども"],
+ "👨‍👩‍👧‍👧":["家族 (男性、女性、女の子、女の子)","父親","母親","男","女","男女","おとこ","おんな","女の子","こども"],
+ "👩‍👩‍👦":["家族 (女性、女性、男の子)","家族","母親","女性","女","おんな","男の子","子供","プライド","lgbt","レズビアン"],
+ "👩‍👩‍👧":["家族 (女性、女性、女の子)","家族","母親","女性","女","おんな","女の子","子供","プライド","lgbt","レズビアン"],
+ "👩‍👩‍👧‍👦":["家族 (女性、女性、女の子、男の子)","家族","母親","女性","女","おんな","男の子","女の子","子供","プライド","lgbt","レズビアン"],
+ "👩‍👩‍👦‍👦":["家族 (女性、女性、男の子、男の子)","家族","母親","女性","女","おんな","男の子","子供","プライド","lgbt","レズビアン"],
+ "👩‍👩‍👧‍👧":["家族 (女性、女性、女の子、女の子)","家族","母親","女性","女","おんな","女の子","子供","プライド","lgbt","レズビアン"],
+ "👨‍👨‍👦":["家族 (男性、男性、男の子)","家族","父親","男性","男","おとこ","男の子","子供","プライド","lgbt","ゲイ"],
+ "👨‍👨‍👧":["家族 (男性、男性、女の子)","家族","父親","男性","男","おとこ","女の子","子供","プライド","lgbt","ゲイ"],
+ "👨‍👨‍👧‍👦":["家族 (男性、男性、女の子、男の子)","家族","父親","男性","男","おとこ","男の子","女の子","子供","プライド","lgbt","ゲイ"],
+ "👨‍👨‍👦‍👦":["家族 (男性、男性、男の子、男の子)","家族","父親","男性","男","おとこ","男の子","子供","プライド","lgbt","ゲイ"],
+ "👨‍👨‍👧‍👧":["家族 (男性、男性、女の子、女の子)","家族","父親","男性","男","おとこ","女の子","子供","プライド","lgbt","ゲイ"],
+ "👩‍👦":["家族(女性、男の子)","家族","母親","女性","女","おんな","男の子","こども"],
+ "👩‍👧":["家族(女性、女の子)","家族","母親","女性","女","おんな","女の子","こども"],
+ "👩‍👧‍👦":["家族(女性、女の子、男の子)","家族","母親","女性","女","男性","女の子","男の子","こども"],
+ "👩‍👦‍👦":["家族(女性、男の子、男の子)","家族","母親","女性","女","おんな","男の子","こども"],
+ "👩‍👧‍👧":["家族(女性、女の子、女の子)","家族","母親","女性","女","おんな","女の子","こども"],
+ "👨‍👦":["家族(男性、男の子)","父親","男","おとこ","男性","男の子","こども"],
+ "👨‍👧":["家族(男性、女の子)","父親","男","男女","女の子","こども"],
+ "👨‍👧‍👦":["家族(男性、女の子、男の子)","父親","男","おとこ","男性","男の子","女の子","こども"],
+ "👨‍👦‍👦":["家族(男性、男の子、男の子)","父親","男","おとこ","男性","男の子","こども"],
+ "👨‍👧‍👧":["家族(男性、女の子、女の子)","父親","男","男女","女の子","こども"],
+ "👚":["レディースウェア","服","女性","おんな"],
+ "👕":["Tシャツ","服","シャツ"],
+ "🥼":["白衣","服","医者","実験","科学者"],
+ "🦺":["安全ベスト","緊急","安全","ベスト"],
+ "🧥":["コート","服","ジャケット"],
+ "👖":["ジーンズ","服","パンツ","ズボン"],
+ "👔":["ネクタイ","服"],
+ "👗":["ドレス","服"],
+ "👘":["着物","服","和服"],
+ "🥻":["サリー","服","ドレス"],
+ "🩱":["ワンピース","服","水着","スイミングウェア","水泳"],
+ "👙":["ビキニ","服","水泳"],
+ "🩲":["ブリーフ","服","水着","スイミングウェア","水泳","下着"],
+ "🩳":["ショーツ","服","水着","スイミングウェア","水泳","下着"],
+ "💄":["口紅","化粧品","コスメ","化粧","メイク"],
+ "💋":["キスマーク","ハート","キス","唇","クチビル","マーク","恋愛","ロマンス"],
+ "👣":["足あと","体","服","足跡","あしあと"],
+ "🧦":["靴下","服","ソックス","一組"],
+ "🩴":["ゴム製サンダル","ビーチ","サンダル","草履"],
+ "👠":["ハイヒール","服","ヒール","靴","女性","おんな"],
+ "👡":["レディースサンダル","服","サンダル","靴","女性","おんな"],
+ "👢":["レディースブーツ","ブーツ","服","靴","女性","おんな"],
+ "🥿":["レディースフラットシューズ","服","バレエフラット","スリッポン","スリッパ"],
+ "👞":["メンズシューズ","服","男性","おとこ","靴"],
+ "👟":["運動靴","運動","服","シューズ","スニーカー"],
+ "🩰":["バレエシューズ","服","シューズ","バレエ","ダンス"],
+ "🥾":["ハイキングブーツ","服","バックパック","ブーツ","キャンプ","ハイキング"],
+ "🧢":["キャップ","服","野球","ハット","帽子"],
+ "👒":["レディースハット","服","帽子","女性","おんな"],
+ "🎩":["シルクハット","アクティビティ","服","エンターテインメント","娯楽","帽子","トップス"],
+ "🎓":["卒業式の角帽","アクティビティ","帽子","お祝い","服","卒業","ハット"],
+ "👑":["冠","服","王冠","王","女王"],
+ "⛑":["白十字のヘルメット","救助","十字","顔","帽子","ヘルメット"],
+ "🪖":["軍隊のヘルメット","軍","ヘルメット","軍隊","軍人","兵士"],
+ "🎒":["ランドセル","アクティビティ","鞄","バッグ","学生鞄","学校"],
+ "👝":["ポーチ","鞄","バッグ","服"],
+ "👛":["財布","服","コイン"],
+ "👜":["ハンドバッグ","鞄","バッグ","服"],
+ "💼":["ブリーフケース"],
+ "👓":["眼鏡","服","目","メガネ","アイウェア"],
+ "🕶":["サングラス","暗い","目","眼鏡","メガネ"],
+ "🥽":["ゴーグル","服","目の保護","水泳","溶接"],
+ "🧣":["スカーフ","服","首"],
+ "🧤":["手袋","服","手"],
+ "💍":["指輪","ダイヤモンド","恋愛","ロマンス"],
+ "🌂":["閉じた傘","服","雨","傘","天気"],
+ "☂":["傘","服","雨","天気"],
+ "🐶":["イヌの顔","犬","イヌ","顔","ペット"],
+ "🐱":["ネコの顔","猫","ネコ","顔","ペット"],
+ "🐭":["ネズミの顔","顔","ネズミ"],
+ "🐹":["ハムスターの顔","顔","ハムスター","ペット"],
+ "🐰":["ウサギの顔","バニー","顔","ペット","ウサギ"],
+ "🐻":["クマの顔","熊","クマ","顔"],
+ "🧸":["テディベア","玩具","ビロード","ぬいぐるみ","おもちゃ"],
+ "🐼":["パンダの顔","顔","パンダ","熊"],
+ "🐻‍❄️":["シロクマ","顔","北極","熊","白"],
+ "🐨":["コアラ","熊","有袋類","オーストラリア"],
+ "🐯":["トラの顔","顔","虎","トラ"],
+ "🦁":["ライオンの顔","顔","しし座","ライオン","星座"],
+ "🐮":["ウシの顔","牛","ウシ","顔"],
+ "🐷":["ブタの顔","顔","豚","ブタ"],
+ "🐽":["ブタの鼻","顔","鼻","豚","ブタ"],
+ "🐸":["カエルの顔","顔","蛙","カエル"],
+ "🐵":["サルの顔","顔","猿","サル"],
+ "🙈":["見ざる","悪い","顔","禁じる","ジェスチャー","猿","サル","だめ","ダメ","禁止","見る"],
+ "🙉":["聞かざる","悪い","顔","禁じる","ジェスチャー","聞く","サル","ない","なし","禁止"],
+ "🙊":["言わざる","悪い","顔","禁じる","ジェスチャー","猿","サル","ない","なし","禁止","話す"],
+ "🐒":["サル","猿"],
+ "🦍":["ゴリラ"],
+ "🦧":["オランウータン","類人猿"],
+ "🐔":["ニワトリ"],
+ "🐧":["ペンギン"],
+ "🐦":["鳥"],
+ "🐦‍⬛":["黒い鳥","鳥","黒","カラス","ワタリガラス","ミヤマガラス"],
+ "🐤":["ヒヨコ","赤ちゃん","ひよこ"],
+ "🐣":["ひよこ","赤ちゃん","孵化"],
+ "🐥":["正面を向いたヒヨコ","赤ちゃん","ひよこ"],
+ "🐺":["オオカミの顔","顔","オオカミ"],
+ "🦊":["キツネの顔","顔","キツネ"],
+ "🦝":["アライグマ","顔","好奇心が強い","ずる賢い"],
+ "🐗":["イノシシ","豚"],
+ "🐴":["ウマの顔","顔","馬"],
+ "🦓":["シマウマ","顔"],
+ "🦒":["キリン","顔"],
+ "🦌":["シカ"],
+ "🫎":["ヘラジカ","動物","枝角","エルク","哺乳類"],
+ "🦘":["カンガルー","オーストラリア","ジャンプ","有袋類"],
+ "🦥":["怠惰","なまける","遅い"],
+ "🦦":["カワウソ","釣り","ふざける"],
+ "🦫":["ビーバー","ダム"],
+ "🦄":["ユニコーンの顔","顔","ユニコーン"],
+ "🐝":["ミツバチ","ハチ","昆虫"],
+ "🐛":["虫","昆虫"],
+ "🦋":["チョウ","蝶","昆虫","美しい"],
+ "🐌":["カタツムリ"],
+ "🪲":["甲虫","虫","昆虫"],
+ "🐞":["テントウムシ","カブトムシ","昆虫","てんとう虫"],
+ "🐜":["アリ","蟻","昆虫"],
+ "🦗":["クリケット","コオロギ","バッタ目","昆虫"],
+ "🪳":["ゴキブリ","昆虫","害虫"],
+ "🕷":["クモ","昆虫","蜘蛛"],
+ "🕸":["クモの巣","クモ","巣"],
+ "🦂":["サソリ","さそり座","さそり","星座"],
+ "🦟":["蚊","病気","熱","昆虫","マラリア","ウイルス"],
+ "🪰":["ハエ","害虫","昆虫","蛆虫"],
+ "🪱":["蠕虫","環形動物","ミミズ","寄生虫"],
+ "🦠":["微生物","アメーバ","バクテリア","ウイルス"],
+ "🐢":["カメ"],
+ "🐍":["ヘビ","運搬人","へびつかい座","蛇","星座"],
+ "🦎":["トカゲ","爬虫類"],
+ "🐙":["タコ","蛸"],
+ "🦑":["イカ","軟体動物","烏賊"],
+ "🪼":["クラゲ","焼く","無脊椎動物","ゼリー","海","痛い","刺毛"],
+ "🦞":["ロブスター","ビスク","爪","シーフード"],
+ "🦀":["カニ","かに座","蟹","星座"],
+ "🦐":["エビ","貝","小さい"],
+ "🦪":["カキ","真珠","ダイビング"],
+ "🐠":["熱帯魚","魚","熱帯"],
+ "🐟":["魚","うお座","星座"],
+ "🐡":["フグ","魚"],
+ "🐬":["イルカ","ひれ"],
+ "🦈":["サメ","魚"],
+ "🦭":["アザラシ","アシカ"],
+ "🐳":["潮吹きクジラ","顔","潮吹き","クジラ"],
+ "🐋":["クジラ"],
+ "🐊":["ワニ"],
+ "🐆":["ヒョウ"],
+ "🐅":["トラ","虎"],
+ "🐃":["スイギュウ","水牛","水"],
+ "🐂":["雄牛","牡牛","おうし座","星座"],
+ "🐄":["ウシ","牛"],
+ "🦬":["バイソン","バッファロー","群れ","ヴィセント"],
+ "🐪":["ヒトコブラクダ","ラクダ","こぶ"],
+ "🐫":["フタコブラクダ","フタコブ","ラクダ","こぶ"],
+ "🦙":["ラマ","アルパカ","グアナコ","ビクーニャ","ウール"],
+ "🐘":["ゾウ","象"],
+ "🦏":["サイ"],
+ "🦛":["カバ"],
+ "🦣":["マンモス","絶滅","大型","牙","毛に覆われた"],
+ "🐐":["ヤギ","やぎ座","星座"],
+ "🐏":["仔羊","おひつじ座","ヒツジ","星座"],
+ "🐑":["ヒツジ","雌羊"],
+ "🐎":["馬","競馬","レース"],
+ "🫏":["ロバ","動物","ブーロ","哺乳類","ラバ"],
+ "🐖":["ブタ","雌豚"],
+ "🦇":["コウモリ","吸血鬼"],
+ "🐓":["おんどり"],
+ "🦃":["七面鳥(鳥)","七面鳥","鳥"],
+ "🕊":["平和の鳩","鳥","鳩","飛行","平和"],
+ "🦅":["ワシ","鳥"],
+ "🦆":["アヒル","鳥"],
+ "🪿":["ガチョウ","鳥","家禽","警笛の音"],
+ "🦢":["白鳥","鳥","白鳥の雄","醜いアヒルの子"],
+ "🦉":["フクロウ","鳥","賢い"],
+ "🦩":["フラミンゴ","熱帯","鮮やか"],
+ "🦚":["オスのクジャク","鳥","メスのクジャク"],
+ "🦜":["オウム","鳥","海賊"],
+ "🦤":["ドードー","鳥","絶滅"],
+ "🪽":["羽","天使","航空","鳥","飛行","神話"],
+ "🪶":["羽毛","鳥","軽い","羽"],
+ "🐕":["イヌ","犬","ペット"],
+ "🦮":["盲導犬","アクセシビリティ","目が不自由","犬","ガイド"],
+ "🐕‍🦺":["介助犬","アクセシビリティ","支援","犬","サービス"],
+ "🐩":["プードル","イヌ","犬"],
+ "🐈":["ネコ","猫","ペット"],
+ "🐈‍⬛":["黒猫","黒","猫","ペット","ハロウィーン"],
+ "🐇":["ウサギ","バニー","ペット"],
+ "🐀":["ネズミ"],
+ "🐁":["ネズミ"],
+ "🐿":["シマリス"],
+ "🦨":["スカンク","悪臭","臭う"],
+ "🦡":["アナグマ","ラーテル","ねだる"],
+ "🦔":["ハリネズミ","顔"],
+ "🐾":["動物の足あと","足","跡"],
+ "🐉":["ドラゴン","おとぎ話"],
+ "🐲":["ドラゴンの顔","ドラゴン","顔","おとぎ話"],
+ "🦕":["竜脚類","ブラキオサウルス","ブロントサウルス","ディプロドクス","恐竜"],
+ "🦖":["ティラノサウルス","Tレックス","恐竜"],
+ "🌵":["サボテン","植物"],
+ "🎄":["クリスマスツリー","アクティビティ","お祝い","クリスマス","エンターテイメント","ツリー"],
+ "🌲":["常緑樹","常緑","植物","木"],
+ "🌳":["落葉樹","落葉性","植物","落葉","木"],
+ "🌴":["ヤシの木","ヤシ","植物","木"],
+ "🪴":["鉢植え","植物","観葉植物"],
+ "🌱":["苗木","植物","若い"],
+ "🌿":["ハーブ","葉","植物"],
+ "☘":["クローバー","植物"],
+ "🍀":["四つ葉のクローバー","4","クローバー","四","葉","植物"],
+ "🎍":["門松","アクティビティ","竹","お祝い","日本","松","植物"],
+ "🎋":["七夕","アクティビティ","旗","お祝い","エンターテイメント","日本","木"],
+ "🍃":["風になびく葉","吹く","はためく","葉","植物","風"],
+ "🍂":["落ち葉","落下","葉","植物"],
+ "🍁":["カエデの葉","落下","葉","カエデ","植物"],
+ "🌾":["稲穂","稲束","穂","植物","米"],
+ "🪺":["卵のある巣","巣作り","鳥の巣","卵"],
+ "🪹":["空の巣","巣作り","鳥の巣"],
+ "🌺":["ハイビスカス","花","植物"],
+ "🌻":["ヒマワリ","花","植物","太陽","ひまわり"],
+ "🌹":["バラ","花","植物"],
+ "🥀":["しおれた花","花","しおれた"],
+ "🌷":["チューリップ","花","植物"],
+ "🌼":["花","植物"],
+ "🌸":["桜","花","植物"],
+ "🪷":["ハス","仏教","花","ヒンドゥー教","インド","清浄","ベトナム"],
+ "🪻":["ヒアシンス","ブルーボンネット","花","ラベンダー","ルピナス","ノウルーズ","紫","キンギョソウ"],
+ "💐":["花束","花","植物","ロマンス"],
+ "🍄":["キノコ","植物"],
+ "🐚":["巻き貝","貝"],
+ "🪸":["サンゴ","大洋","礁"],
+ "🌎":["アメリカ大陸","アメリカ","地球","世界"],
+ "🌍":["ヨーロッパとアフリカ地域","アフリカ","地球","ヨーロッパ","世界"],
+ "🌏":["アジアとオーストラリア","アジア","オーストラリア","地球","世界"],
+ "🌕":["満月","月","宇宙","天気"],
+ "🌖":["寝待月","十三夜","月","宇宙","欠け","天気"],
+ "🌗":["下弦の月","月","弦","宇宙","天気"],
+ "🌘":["欠けていく三日月","三日月","月","宇宙","欠け","天気"],
+ "🌑":["新月","晦","月","宇宙","天気"],
+ "🌒":["満ちていく三日月","三日月","月","宇宙","上弦","天気"],
+ "🌓":["上弦の月","月","弦","宇宙","天気"],
+ "🌔":["十三夜月","十三夜","月","宇宙","上弦","天気"],
+ "🌙":["三日月","月","宇宙","天気"],
+ "🌚":["顔つき新月","顔","月","宇宙","天気"],
+ "🌝":["顔つき満月","明るい","顔","満ちた","月","宇宙","天気"],
+ "🌛":["顔つき上弦の月","顔","月","弦","宇宙","天気"],
+ "🌜":["顔がある下弦の月","顔","月","弦","宇宙","天気"],
+ "⭐":["中くらいの星","星"],
+ "🌟":["光る星","きらめき","赤い光","輝く","輝き","星"],
+ "💫":["くらくら","漫画","めまい","星"],
+ "✨":["キラキラ","エンターテイメント","輝き","星"],
+ "☄":["彗星","宇宙"],
+ "🪐":["環のある惑星","宇宙","惑星","土星"],
+ "🌞":["顔つき太陽","明るい","顔","宇宙","太陽","天気"],
+ "☀️":["太陽の光","明るい","光線","宇宙","太陽","晴天","天気"],
+ "🌤":["太陽と小さな雲","雲","太陽","天気"],
+ "⛅":["晴れ時々曇り","雲","太陽","天気"],
+ "🌥":["晴れのち曇り","雲","太陽","天気"],
+ "🌦":["晴れのち曇り時々雨","雲","雨","太陽","天気"],
+ "☁️":["雲","天気"],
+ "🌧":["雨雲","雲","雨","天気"],
+ "⛈":["雷雨","雲","雨","雷","天気"],
+ "🌩":["雷雲","雲","雷","天気"],
+ "⚡":["高電圧記号","危険","電気","雷","電圧","ビリビリ"],
+ "🔥":["炎","火","道具"],
+ "💥":["衝突マーク","どかーん","衝突","漫画"],
+ "❄️":["雪の結晶","冷たい","雪","天気"],
+ "🌨":["雪雲","雲","冷","雪","天気"],
+ "☃":["雪だるま","冷","雪","天気"],
+ "⛄":["雪だるま","冷","雪","天気"],
+ "🌬":["風が吹いている","風が吹く","雲","顔","天気","風"],
+ "💨":["ダッシュ","漫画","走る"],
+ "🌪":["竜巻雲","雲","竜巻","天気","旋風"],
+ "🌫":["霧","雲","天気"],
+ "🌈":["虹","雨","レインボー","天気","プライド","lgbt"],
+ "☔":["雨と傘","衣類","しずく","雨","傘","天気"],
+ "💧":["雫","ぞっとする","漫画","したたり","汗","天気"],
+ "💦":["汗マーク","漫画","濡れている","汗"],
+ "🌊":["波","海","水","天気"],
+ "🍏":["青りんご","リンゴ","フルーツ","果物","緑","植物"],
+ "🍎":["赤いリンゴ","リンゴ","フルーツ","果物","植物","赤"],
+ "🍐":["梨","フルーツ","果物","植物"],
+ "🍊":["みかん","フルーツ","果物","オレンジ","植物","赤橙色"],
+ "🍋":["レモン","柑橘類","フルーツ","果物","植物"],
+ "🍌":["バナナ","フルーツ","果物","植物"],
+ "🍉":["スイカ","フルーツ","果物","植物"],
+ "🍇":["ブドウ","フルーツ","果物","植物"],
+ "🍓":["イチゴ","ベリー","フルーツ","果物","植物"],
+ "🍈":["メロン","フルーツ","果物","植物"],
+ "🍒":["さくらんぼ","フルーツ","果物","植物"],
+ "🫐":["ブルーベリー","ベリー","ビルベリー","青","フルーツ"],
+ "🍑":["桃","フルーツ","果物","植物"],
+ "🥭":["マンゴー","熱帯","フルーツ"],
+ "🍍":["パイナップル","フルーツ","果物","植物"],
+ "🥥":["ココナッツ","フルーツ"],
+ "🥝":["キウイフルーツ","フルーツ","果物","キウイ"],
+ "🍅":["トマト","植物","野菜"],
+ "🥑":["アボカド","フルーツ","果物"],
+ "🫒":["オリーブ","フルーツ"],
+ "🍆":["ナス","茄子","植物","野菜"],
+ "🌶":["トウガラシ","辛い","コショウ","植物"],
+ "🫑":["ピーマン","唐辛子","コショウ","植物","野菜"],
+ "🥒":["キュウリ","ピクルス","野菜"],
+ "🥬":["葉っぱの緑","チンゲン菜","キャベツ","ケール","レタス"],
+ "🥦":["ブロッコリー","野菜"],
+ "🫛":["エンドウ豆のさや","豆","枝豆","マメ科","エンドウ豆","さや","野菜"],
+ "🧄":["にんにく","野菜","植物","香味料"],
+ "🧅":["玉ねぎ","野菜","植物","香味料"],
+ "🌽":["トウモロコシ","コーン","植物"],
+ "🥕":["ニンジン","野菜"],
+ "🥗":["グリーンサラダ","緑","サラダ"],
+ "🥔":["ジャガイモ","野菜"],
+ "🍠":["焼き芋","ジャガイモ","焼き","スイーツ"],
+ "🌰":["栗","植物"],
+ "🥜":["ピーナッツ","ナッツ","野菜"],
+ "🫘":["豆","食べ物","腎臓","マメ"],
+ "🍯":["ハニーポット","はちみつ","ポット","スイーツ"],
+ "🍞":["パン","ローフ"],
+ "🥐":["クロワッサン","パン","三日月","ロール","フレンチ"],
+ "🥖":["フランスパン","パン","フレンチ"],
+ "🫓":["フラットブレッド","アレパ","ラヴァシュ","ナン","ピタ"],
+ "🥨":["プレッツェル","ソフトプレッツェル","プレッツェルツイスト","パン"],
+ "🥯":["ベーグル","パン","クリームチーズ","ひと塗り"],
+ "🥞":["パンケーキ","クレープ","ホットケーキ"],
+ "🧇":["ワッフル","ホットケーキ"],
+ "🧀":["チーズ"],
+ "🍗":["ターキー","骨","ニワトリ","脚","家禽"],
+ "🍖":["骨付き肉","骨","肉"],
+ "🥩":["一切れの肉","肉","切り身","ラムチョップ","豚","ステーキ"],
+ "🍤":["エビフライ","フライ","エビ","小エビ","てんぷら"],
+ "🥚":["卵"],
+ "🍳":["料理","卵","フライパン","鍋"],
+ "🥓":["ベーコン","肉"],
+ "🍔":["ハンバーガー","バーガー"],
+ "🍟":["フライドポテト","フライド","ポテト"],
+ "🌭":["ホットドッグ","フランクフルトソーセージ","ホットドッグソーセージ","ソーセージ","ウィンナー","レッドホット"],
+ "🍕":["ピザ","チーズ","1枚"],
+ "🍝":["スパゲッティ","パスタ"],
+ "🥪":["サンドウィッチ","パン","野菜","チーズ","肉","デリ"],
+ "🌮":["タコス","メキシコ"],
+ "🌯":["ブリトー","メキシコ"],
+ "🫔":["タマーレ","タマーリ","メキシカン","包まれた"],
+ "🥙":["フラットブレッドサンド","ファラフェル","フラットブレッド","ジャイロ","ケバブ","詰め物"],
+ "🧆":["ファラフェル","ひよこ豆"],
+ "🍜":["どんぶり","麺","ラーメン","蒸し加熱","スープ"],
+ "🥘":["パエリア","キャセロール","鍋","浅い"],
+ "🍲":["なべ","鍋","シチュー"],
+ "🫕":["フォンデュ","チーズ","チョコレート","フォデュ","溶けた","ポット","スイス"],
+ "🥫":["缶詰","かんづめ","保存用食品"],
+ "🫙":["瓶","香辛料","容器","空","ソース","貯蔵"],
+ "🧂":["塩","香辛料","シェーカー"],
+ "🧈":["バター","乳製品"],
+ "🫚":["ショウガ","ビール","根","スパイス"],
+ "🍥":["なると","固形の食べ物","魚","練り物"],
+ "🍣":["寿司"],
+ "🍱":["弁当箱","弁当","箱"],
+ "🍛":["カレーライス","カレー","ご飯"],
+ "🍙":["おにぎり","日本","米"],
+ "🍚":["ごはん","料理","米"],
+ "🍘":["せんべい","米"],
+ "🥟":["餃子","ギョウザ"],
+ "🍢":["おでん","シーフード","串","スティック"],
+ "🍡":["団子","デザート","日本","串","スティック","スイーツ"],
+ "🍧":["かき氷","デザート","氷","スイーツ"],
+ "🍨":["アイスクリーム","クリーム","デザート","氷","スイーツ"],
+ "🍦":["ソフトクリーム","クリーム","デザート","氷","アイスクリーム","ソフト","スイーツ"],
+ "🍰":["ショートケーキ","ケーキ","デザート","ペイストリー","スライス","スイーツ"],
+ "🎂":["バースデーケーキ","誕生日","ケーキ","お祝い","デザート","ペイストリー","スイーツ"],
+ "🧁":["カップケーキ","ベーカリー","スイーツ","デザート","ペイストリー"],
+ "🥧":["パイ","デザート","スイーツ"],
+ "🍮":["カスタード","デザート","プリン","スイーツ"],
+ "🍭":["ペロペロキャンディー","キャンディ","デザート","ロリポップキャンディ","スイーツ"],
+ "🍬":["アメ","デザート","スイーツ"],
+ "🍫":["チョコレート","バー","デザート","スイーツ"],
+ "🍿":["ポップコーン"],
+ "🍩":["ドーナツ","デザート","スイーツ"],
+ "🍪":["クッキー","デザート","甘い"],
+ "🥠":["おみくじ入りクッキー","フォーチュンクッキー"],
+ "🥮":["月餅","秋","祭"],
+ "☕":["ホットドリンク","飲料","コーヒー","飲み物","温かい","蒸気","お茶"],
+ "🍵":["湯のみ","飲料","カップ","飲み物","お茶","湯飲み"],
+ "🫖":["ティーポット","ドリンク","ポット","ティー","ケトル"],
+ "🥣":["ボウルとスプーン","朝食","シリアル","お粥","オートミール","ポリッジ","食器"],
+ "🍼":["哺乳瓶","赤ちゃん","ボトル","ドリンク","ミルク"],
+ "🥤":["カップとストロー","ジュース","ソーダ","モルト","ソフトドリンク","水","食器"],
+ "🧋":["タピオカティー","バブル","ミルク","パール","ティー","ボバ","タピオカ","モミ"],
+ "🧃":["飲料ボックス","ジュース","飲料","ボックス","ドリンク","ストロー"],
+ "🧉":["マテ","ドリンク","ボンビリヤ","イエルバ"],
+ "🥛":["コップに入った牛乳","ドリンク","グラス","ミルク"],
+ "🫗":["流れ込む液体","飲み物","空","グラス","こぼれる"],
+ "🍺":["ビール","バー","飲む","マグカップ"],
+ "🍻":["乾杯","バー","ビール","カチン","飲み物","マグカップ"],
+ "🍷":["ワイングラス","バー","飲料","飲み物","グラス","ワイン"],
+ "🥂":["グラスで乾杯","祝う","カチン","飲み物","グラス"],
+ "🥃":["タンブラー","グラス","酒","ショット","ウイスキー","ウィスキー","バーボン"],
+ "🍸":["カクテルグラス","バー","カクテル","飲み物","グラス"],
+ "🍹":["トロピカルドリンク","バー","飲み物","トロピカル"],
+ "🍾":["瓶と飛び出す栓","バー","ボトル","シャンパン","シャンペン","シャンパーニュ","コルク","飲み物","飛び出す","パーティー"],
+ "🍶":["とっくりとおちょこ","バー","飲料","ボトル","カップ","飲み物","酒"],
+ "🧊":["角氷","氷","立方体","冷たい","氷山"],
+ "🥄":["スプーン","食器"],
+ "🍴":["フォークとナイフ","調理","フォーク","ナイフ","食器"],
+ "🍽":["フォークとナイフとプレート","調理","フォーク","ナイフ","プレート","食器"],
+ "🥢":["箸","はし"],
+ "🥡":["テイクアウトボックス","テイクアウト","容器","お持ち帰り"],
+ "⚽":["サッカーボール","ボール","サッカー"],
+ "🏀":["バスケットボール","ボール","バスケットリング"],
+ "🏈":["アメリカンフットボール","アメリカン","ボール","フットボール"],
+ "⚾":["野球","ボール"],
+ "🥎":["ソフトボール","ボール","試合","スポーツ"],
+ "🎾":["テニスボール","ボール","ラケット","テニス"],
+ "🏐":["バレーボール","ボール","試合"],
+ "🏉":["ラグビー","ボール","フットボール"],
+ "🎱":["ビリヤード","8","エイトボール","ボール","エイト","ゲーム"],
+ "🥏":["空飛ぶ円盤","ディスク","アルティメット","ゴルフ","試合","スポーツ","フリスビー"],
+ "🪃":["ブーメラン","オーストラリア","逆戻り","跳ね返り"],
+ "🏓":["卓球のラケットとボール","ボール","バット","試合","パドル","卓球"],
+ "🏸":["バドミントンのラケットとシャトル","バドミントン","バーディー","試合","ラケット","シャトル"],
+ "🥅":["ゴールネット","ゴール","ネット"],
+ "🏒":["アイスホッケーのスティックとパック","試合","ホッケー","氷","パック","スティック"],
+ "🏑":["フィールドホッケーのスティックとボール","ボール","フィールド","試合","ホッケー","スティック"],
+ "🏏":["クリケットのバットとボール","ボール","フィールド","クリケット","試合"],
+ "🥍":["ラクロス","ボール","スティック","試合","スポーツ"],
+ "🥌":["カーリングストーン","カーリング","ストーン"],
+ "⛳":["ゴルフのカップ","ピンフラッグ","ゴルフ","ホール"],
+ "🏹":["弓矢","射手","矢","弓","射手座","道具","星座"],
+ "🎣":["釣竿と魚","エンターテイメント","魚","棒"],
+ "🤿":["ダイビングマスク","ダイビング","スキューバ","シュノーケル"],
+ "🥊":["ボクシンググローブ","ボクシング","グローブ"],
+ "🥋":["道着","柔道","空手","武道","テコンドー","ユニフォーム"],
+ "⛸":["アイススケート","氷"],
+ "🎿":["スキーとスキーブーツ","スキー","雪"],
+ "🛷":["そり","ソリ","ルージュ","トボガン"],
+ "⛷":["スキー","雪"],
+ "🏂":["スノーボーダー","スキー","雪","スノーボード"],
+ "🏋️‍♀️":["ウエイトを持ち上げる女性","挙げ","重量","女性","女","おんな"],
+ "🏋":["ウエイトを持ち上げる人","挙げ","重量"],
+ "🏋️‍♂️":["ウエイトを持ち上げる男性","挙げ","重量","男","おとこ","男性"],
+ "🤺":["フェンシングをする人","剣士","剣術","剣"],
+ "🤼‍♀️":["レスリングをする女性","レスリング","レスリング選手","女性","女","おんな"],
+ "🤼":["レスリングをする人たち","レスリング","レスリング選手"],
+ "🤼‍♂️":["レスリングをする男性","レスリング","レスリング選手","男","おとこ","男性"],
+ "🤸‍♀️":["側転をする女性","側方転回","体操","女性","女","おんな"],
+ "🤸":["側転をする人","側方転回","体操"],
+ "🤸‍♂️":["側転をする男性","側方転回","体操","男","おとこ","男性"],
+ "⛹️‍♀️":["ボールをバウンドさせる女性","ボール","女性","女","おんな"],
+ "⛹":["ボールをバウンドさせる人","ボール"],
+ "⛹️‍♂️":["ボールをバウンドさせる男性","ボール","男","おとこ","男性"],
+ "🤾‍♀️":["ハンドボールをする女性","ボール","ハンドボール","女性","女","おんな"],
+ "🤾":["ハンドボールをする人","ボール","ハンドボール"],
+ "🤾‍♂️":["ハンドボールをする男性","ボール","ハンドボール","男","おとこ","男性"],
+ "🧗‍♀️":["クライミングしている女性","クライミング","ロック","女性","女","おんな"],
+ "🧗":["クライミングしている人","クライミング","ロック"],
+ "🧗‍♂️":["クライミングしている男性","クライミング","ロック","男性","男","おとこ"],
+ "🏌️‍♀️":["ゴルフをする女性","ボール","ゴルフ","ゴルファー","ゴルフする","女性","女","おんな"],
+ "🏌":["ゴルフをする人","ボール","ゴルフ","ゴルファー","ゴルフする"],
+ "🏌️‍♂️":["ゴルフをする男性","ボール","ゴルフ","ゴルファー","ゴルフする","男","おとこ","男性"],
+ "🧘‍♀️":["蓮華座の女性","瞑想","ヨガ","静穏","女性","女","おんな"],
+ "🧘":["蓮華座の人","瞑想","ヨガ","静穏"],
+ "🧘‍♂️":["蓮華座の男性","瞑想","ヨガ","静穏","男性","男","おとこ"],
+ "🧖‍♀️":["スチームルームにいる女性","サウナ","スチームルーム","ハマム","スチームバス","女性","女","おんな"],
+ "🧖":["スチームルームにいる人","サウナ","スチームルーム","ハマム","スチームバス"],
+ "🧖‍♂️":["スチームルームにいる男性","サウナ","スチームルーム","ハマム","スチームバス","男性","男","おとこ"],
+ "🏄‍♀️":["サーフィンをする女性","サーファー","サーフィン","波乗り","女性","女","おんな"],
+ "🏄":["サーフィンをする人","サーファー","サーフィン","波乗り"],
+ "🏄‍♂️":["サーフィンをする男性","サーファー","サーフィン","波乗り","男","おとこ","男性"],
+ "🏊‍♀️":["泳ぐ女性","泳ぐ","水泳","女性","女","おんな"],
+ "🏊":["水泳をする人","泳ぐ","水泳"],
+ "🏊‍♂️":["泳ぐ男性","泳ぐ","水泳","男","おとこ","男性"],
+ "🤽‍♀️":["水球をする女性","ポロ","水","水球","女性","女","おんな"],
+ "🤽":["水球をする人","ポロ","水","水球"],
+ "🤽‍♂️":["水球をする男性","ポロ","水","水球","男","おとこ","男性"],
+ "🚣‍♀️":["ボートを漕ぐ女性","ボート","漕ぎ船","乗り物","漕艇","女性","女","おんな"],
+ "🚣":["ボートをこぐ人","ボート","漕ぎ船","乗り物","漕艇"],
+ "🚣‍♂️":["ボートを漕ぐ男性","ボート","漕ぎ船","乗り物","漕艇","男","おとこ","男性"],
+ "🏇":["競馬","馬","騎手","競走馬"],
+ "🚴‍♀️":["自転車に乗る女性","自転車","自転車乗り","自転車に乗る人","サイクリスト","女性","女","おんな"],
+ "🚴":["自転車に乗る人","自転車","自転車乗り","サイクリスト"],
+ "🚴‍♂️":["自転車に乗る男性","自転車","自転車乗り","自転車に乗る人","サイクリスト","男","おとこ","男性"],
+ "🚵‍♀️":["マウンテンバイクに乗る女性","マウンテンバイクライダー","クロスバイク","自転車","自転車乗り","自転車に乗る人","サイクリスト","山","女性","女","おんな"],
+ "🚵":["マウンテンバイクに乗る人","マウンテンバイクライダー","クロスバイク","自転車","自転車乗り","自転車に乗る人","山"],
+ "🚵‍♂️":["マウンテンバイクに乗る男性","マウンテンバイクライダー","クロスバイク","自転車","自転車乗り","自転車に乗る人","サイクリスト","山","男","おとこ","男性"],
+ "🎽":["ランニングシャツと襷","ランニング","襷","シャツ"],
+ "🎖":["勲章","お祝い","メダル","軍事"],
+ "🏅":["スポーツのメダル","メダル"],
+ "🥇":["金メダル","1位","金","メダル","1","第1位"],
+ "🥈":["銀メダル","メダル","2位","銀","2","第2位"],
+ "🥉":["銅メダル","銅","メダル","3位","3","第3位"],
+ "🏆":["トロフィー","賞"],
+ "🏵":["バラ飾り","植物"],
+ "🎗":["リマインダーリボン","お祝い","リマインダー","リボン"],
+ "🎫":["きっぷ","アクティビティ","入場料","エンターテイメント","チケット"],
+ "🎟":["入場券","入場料","エンターテイメント","チケット"],
+ "🎪":["サーカス小屋","アクティビティ","サーカス","エンターテイメント","テント"],
+ "🤹‍♀️":["ジャグリングをする女性","天秤","ジャグリング","女性","女","おんな"],
+ "🤹":["ジャグリングをする人","バランス","ジャグリング"],
+ "🤹‍♂️":["ジャグリングをする男性","天秤","ジャグリング","男性","男","おとこ"],
+ "🎭":["舞台芸術","アクティビティ","芸術","エンターテイメント","仮面","舞台","シアター"],
+ "🎨":["絵の具パレット","アクティビティ","アート","エンターテイメント","美術館","絵画","パレット"],
+ "🎬":["カチンコ","アクティビティ","エンターテイメント","映画"],
+ "🎤":["マイク","アクティビティ","エンターテイメント","カラオケ","マイクロフォン"],
+ "🎧":["ヘッドホン","アクティビティ","イヤホン","エンターテイメント","ヘッドフォン"],
+ "🎼":["楽譜","アクティビティ","エンターテイメント","音楽"],
+ "🎹":["鍵盤","アクティビティ","エンターテイメント","楽器","キーボード","音楽","ピアノ"],
+ "🪗":["アコーディオン","コンサーティーナ","スクイーズボックス"],
+ "🥁":["ドラム","ドラムスティック","音楽"],
+ "🪘":["長いドラム","ビート","コンガ","ドラム","リズム","ジャンベ"],
+ "🪇":["マラカス","祝う","楽器","音楽","騒音","打楽器","ガタガタ","リズム","シェイク"],
+ "🎷":["サックス","アクティビティ","エンターテイメント","楽器","音楽","サクソフォーン"],
+ "🎺":["トランペット","アクティビティ","エンターテイメント","楽器","音楽"],
+ "🪈":["フルート","竹","横笛奏者","フルート奏者","音楽","パイプ","リコーダー","吹く","木管楽器"],
+ "🎸":["ギター","アクティビティ","エンターテイメント","楽器","音楽"],
+ "🪕":["バンジョー","アクティビティ","エンターテイメント","楽器","音楽"],
+ "🎻":["バイオリン","アクティビティ","エンターテイメント","楽器","音楽"],
+ "🎲":["サイコロ","さい","エンターテイメント","ゲーム"],
+ "🧩":["パズルのピース","手がかり","噛み合う","ピース","パズル","ジグソー"],
+ "♟️":["チェスのポーン","チェス","駒","ゲーム","捨て駒"],
+ "🎯":["的中","アクティビティ","ブル","ブルズアイ","ダーツ","エンターテイメント","目","試合","ヒット","標的"],
+ "🎳":["ボウリング","ボール","試合"],
+ "🪀":["ヨーヨー","おもちゃ","上下"],
+ "🪁":["凧","おもちゃ","飛ぶ","舞う"],
+ "🛝":["滑り台","遊園地","遊び"],
+ "🎮":["テレビゲーム","コントローラー","エンターテイメント","ゲーム","ビデオゲーム"],
+ "👾":["エイリアン","宇宙人","怪獣","異星人","顔","おとぎ話","ファンタジー","モンスター","宇宙","UFO"],
+ "🎰":["スロットマシン","アクティビティ","ゲーム","スロット"],
+ "🚗":["自動車","車","乗り物"],
+ "🚙":["キャンピングカー","レクリエーション","RV","乗り物"],
+ "🚕":["タクシー","乗り物"],
+ "🛺":["オートリキシャ","人力車","トゥクトゥク"],
+ "🚌":["バス","乗り物"],
+ "🚎":["トロリーバス","バス","路面電車","市街電車","乗り物"],
+ "🏎":["レーシングカー","車","競争"],
+ "🚓":["パトカー","車","パトロール","警察","乗り物"],
+ "🚑":["救急車","乗り物"],
+ "🚒":["消防車","エンジン","炎","トラック","乗り物"],
+ "🚐":["マイクロバス","バス","乗り物"],
+ "🛻":["ピックアップトラック","ピックアップ","トラック","乗り物"],
+ "🚚":["配達用トラック","配達","トラック","乗り物"],
+ "🚛":["トレーラー","大型トラック","セミ","トラック","乗り物"],
+ "🚜":["トラクター","乗り物"],
+ "🏍":["レースバイク","オートバイ","レース"],
+ "🛵":["スクーター","モーター"],
+ "🚲":["自転車","バイク","乗り物"],
+ "🦼":["電動車いす","アクセシビリティ","車いす"],
+ "🦽":["手動車いす","アクセシビリティ","車いす"],
+ "🛴":["キックボード","キック","スクーター"],
+ "🛹":["スケボー","スケート","ボード"],
+ "🛼":["ローラースケート","ローラー","スケート"],
+ "🛞":["車輪","円","タイヤ","回転"],
+ "🚨":["パトライト","車","光","警察","回転","乗り物","サイレン","警告"],
+ "🚔":["パトカー","車","対向車","警察","乗り物"],
+ "🚍":["バス","対向車","乗り物"],
+ "🚘":["対向車","自動車","車","乗り物"],
+ "🚖":["タクシー","対向車","乗り物"],
+ "🚡":["ロープウェイ","空中","ケーブル","車","ゴンドラ","トラムウェイ","乗り物"],
+ "🚠":["ロープウェイ","ケーブル","ゴンドラ","山","乗り物"],
+ "🚟":["高架鉄道","鉄道","乗り物"],
+ "🚃":["鉄道車両","車","電気","鉄道","列車","路面","トロリーバス","乗り物"],
+ "🚋":["路面電車","車","路面","トロリーバス","乗り物"],
+ "🚝":["モノレール","乗り物"],
+ "🚄":["新幹線","鉄道","高速","列車","乗り物"],
+ "🚅":["新幹線","弾丸","鉄道","高速","列車","乗り物"],
+ "🚈":["ライトレール","鉄道","乗り物"],
+ "🚞":["山岳鉄道","車","山","鉄道","乗り物"],
+ "🚂":["蒸気機関車","エンジン","機関車","鉄道","蒸気","列車","乗り物"],
+ "🚆":["電車","線路","乗り物"],
+ "🚇":["地下鉄","メトロ","乗り物"],
+ "🚊":["路面電車","トロリーバス","乗り物"],
+ "🚉":["駅","線路","電車","乗り物"],
+ "🚁":["ヘリコプター","乗り物"],
+ "🛩":["小型航空機","飛行機","乗り物"],
+ "✈️":["飛行機","乗り物"],
+ "🛫":["飛行機の離陸","飛行機","チェックイン","出発","乗り物"],
+ "🛬":["飛行機の着陸","飛行機","到着","着陸","乗り物"],
+ "🪂":["パラシュート","パラセール","スカイダイブ","ハンググライダー"],
+ "💺":["座席","椅子"],
+ "🛰":["サテライト","衛星","宇宙","乗り物"],
+ "🚀":["ロケット","宇宙","乗り物"],
+ "🛸":["空飛ぶ円盤","UFO","宇宙人","異星人","宇宙","空想"],
+ "🛶":["カヌー","ボート"],
+ "⛵":["ヨット","ボート","リゾート","海","乗り物"],
+ "🛥":["モーターボート","ボート","乗り物"],
+ "🚤":["スピードボート","ボート","乗り物"],
+ "⛴":["フェリー","ボート"],
+ "🛳":["旅客船","旅客","船","乗り物"],
+ "🚢":["船","乗り物"],
+ "🛟":["救命浮き輪","浮き輪","ライフジャケット","ライフセーバー","救助","安全"],
+ "⚓":["いかり","船","ツール"],
+ "⛽":["ガソリンスタンド","燃料","ガソリン","給油機","サービスステーション"],
+ "🚧":["工事中","工事用フェンス","建設工事"],
+ "🚏":["バス停","バス","停止"],
+ "🚦":["縦向きの信号機","信号機","信号","交通"],
+ "🚥":["横向きの信号機","信号機","信号","交通"],
+ "🛑":["一時停止標識","八角形","標識","停止"],
+ "🎡":["観覧車","アクティビティ","遊園地","エンターテイメント","フェリス"],
+ "🎢":["ジェットコースター","アクティビティ","遊園地","コースター","エンターテイメント","ローラー"],
+ "🎠":["メリーゴーランド","アクティビティ","メリーゴーラウンド","エンターテイメント","馬"],
+ "🏗":["建設中","建物","建設"],
+ "🌁":["霧","天気"],
+ "🗼":["東京タワー","東京","タワー"],
+ "🏭":["工場","建物"],
+ "⛲":["噴水"],
+ "🎑":["お月見","アクティビティ","お祝い","授賞式","エンターテイメント","月"],
+ "⛰":["山"],
+ "🏔":["雪山","寒い","山","雪"],
+ "🗻":["富士山","山"],
+ "🌋":["火山","噴火","山","気象"],
+ "🗾":["日本列島","日本","地図"],
+ "🏕":["キャンプ"],
+ "⛺":["テント","キャンプ"],
+ "🏞":["国立公園","公園"],
+ "🛣":["高速道路","ハイウェイ","道路"],
+ "🛤":["線路","鉄道","電車"],
+ "🌅":["日の出","朝","太陽","天候"],
+ "🌄":["山からの日の出","朝","山","太陽","日の出","天候"],
+ "🏜":["砂漠"],
+ "🏖":["ビーチと傘","ビーチ","傘","パラソル"],
+ "🏝":["無人島","砂漠","島"],
+ "🌇":["ビルに沈む夕陽","建物","夕暮れ","太陽","夕日","天気"],
+ "🌆":["夕暮れの街並み","建物","街","夕暮れ","日暮れ","風景","太陽","夕日","天気"],
+ "🏙":["街並み","建物","街"],
+ "🌃":["星空","夜","星","天気"],
+ "🌉":["夜の橋","橋","夜","天気"],
+ "🌌":["天の川","宇宙","天気"],
+ "🌠":["流れ星","アクティビティ","落下","流れる","宇宙","星"],
+ "🎇":["線香花火","アクティビティ","お祝い","エンターテイメント","花火","キラキラ"],
+ "🎆":["花火","アクティビティ","お祝い","エンターテイメント"],
+ "🛖":["小屋","家","扇形庫","パオ"],
+ "🏘":["家","建物"],
+ "🏰":["西洋の城","建物","城","ヨーロッパ"],
+ "🏯":["日本の城","建物","城","日本"],
+ "🏟":["スタジアム"],
+ "🗽":["自由の女神","自由","像"],
+ "🏠":["家","建物","自宅"],
+ "🏡":["庭付きの家","建物","庭","自宅","家"],
+ "🏚":["廃墟","建物","廃屋","家"],
+ "🏢":["オフィスビル","建物"],
+ "🏬":["デパート","建物","店"],
+ "🏣":["日本の郵便局","建物","日本","ポスト"],
+ "🏤":["ヨーロッパの郵便局","建物","ヨーロッパ","ポスト"],
+ "🏥":["病院","建物","医師","薬"],
+ "🏦":["銀行","建物"],
+ "🏨":["ホテル","建物"],
+ "🏪":["コンビニエンスストア","建物","コンビニエンス","ストア"],
+ "🏫":["学校","建物"],
+ "🏩":["ラブホテル","建物","ホテル","ラブ"],
+ "💒":["結婚式","アクティビティ","チャペル","ロマンス"],
+ "🏛":["歴史的な建物","建物","歴史的な"],
+ "⛪":["教会","建物","クリスチャン","十字架","宗教"],
+ "🕌":["モスク","イスラム","ムスリム","宗教"],
+ "🛕":["ヒンドゥー教寺院","ヒンドゥー教","寺院","宗教"],
+ "🕍":["シナゴーグ","ユダヤ人","ユダヤ教","宗教","会堂"],
+ "🕋":["カアバ","イスラム","ムスリム","宗教"],
+ "⛩":["神社","宗教","神道"],
+ "⌚":["腕時計","時計"],
+ "📱":["携帯電話","携帯","コミュニケーション","モバイル","電話"],
+ "📲":["着信中","矢印","通話","携帯","コミュニケーション","モバイル","携帯電話","受信","電話"],
+ "💻":["パソコン","ノートパソコン","コンピューター","パーソナル"],
+ "⌨":["キーボード","コンピューター"],
+ "🖥":["デスクトップパソコン","コンピューター","デスクトップ"],
+ "🖨":["プリンター","コンピューター"],
+ "🖱":["3ボタンマウス","3","ボタン","コンピューター","マウス","三"],
+ "🖲":["トラックボール","コンピューター"],
+ "🕹":["ジョイスティック","エンターテイメント","ゲーム","ビデオゲーム"],
+ "🗜":["圧縮","ツール","欠陥"],
+ "💽":["MD","パソコン","光ディスク","エンターテイメント","ミニディスク","光学"],
+ "💾":["フロッピーディスク","コンピューター","ディスク","フロッピー"],
+ "💿":["CDディスク","ブルーレイ","CD","コンピューター","ディスク","DVD","光学"],
+ "📀":["DVD","ブルーレイ","CD","コンピューター","ディスク","エンターテイメント","光学"],
+ "📼":["ビデオテープ","エンターテイメント","テープ","VHS","ビデオ","ビデオカセット"],
+ "📷":["カメラ","エンターテイメント","ビデオ"],
+ "📸":["フラッシュを焚いたカメラ","カメラ","フラッシュ","ビデオ"],
+ "📹":["ビデオカメラ","カメラ","エンターテイメント","ビデオ"],
+ "🎥":["ビデオカメラ","アクティビティ","カメラ","シネマ","エンターテイメント","映画"],
+ "📽":["映写機","シネマ","娯楽","フィルム","映画","プロジェクター","ビデオ"],
+ "🎞":["フィルムのフレーム","シネマ","エンターテイメント","フィルム","フレーム","映画"],
+ "📞":["受話器","コミュニケーション","電話","受信機"],
+ "☎️":["電話","携帯電話"],
+ "📟":["ポケットベル","コミュニケーション","ポケベル"],
+ "📠":["FAX","コミュニケーション; fAX"],
+ "📺":["テレビ","エンターテイメント","TV","ビデオ"],
+ "📻":["ラジオ","エンターテイメント","ビデオ"],
+ "🎙":["スタジオマイク","マイク","音楽","スタジオ"],
+ "🎚":["調節バー","調節","音楽","バー"],
+ "🎛":["コントロールノブ","コントロール","つまみ","音楽"],
+ "⏱":["ストップウォッチ","時計"],
+ "⏲":["タイマー時計","時計","タイマー"],
+ "⏰":["目覚まし時計","アラーム","時計"],
+ "🕰":["置き時計","時計"],
+ "⏳":["砂時計","砂","タイマー"],
+ "⌛":["砂時計","砂","タイマー"],
+ "🧮":["そろばん","計算","カウント","集計表","数学"],
+ "📡":["衛星アンテナ","アンテナ","コミュニケーション","パラボラアンテナ","衛星"],
+ "🔋":["電池","バッテリー","電子","高エネルギー"],
+ "🪫":["バッテリー残量少","バッテリー","電子","低エネルギー"],
+ "🔌":["コンセント","電気","プラグ"],
+ "💡":["電球","漫画","電気","ひらめき","光"],
+ "🔦":["懐中電灯","電気","光","道具","たいまつ"],
+ "🕯":["ろうそく","光"],
+ "🧯":["消火器","消火","火","消す"],
+ "🗑":["ごみ箱","ゴミ箱","ごみ","ゴミ","缶","ビン"],
+ "🛢":["ドラム缶","ドラム","オイル"],
+ "🛒":["ショッピングカート","カート","ショッピング","トロリー"],
+ "💸":["羽の生えたお札","銀行","紙幣","請求書","ドル","飛ぶ","お金","羽"],
+ "💵":["ドル札","銀行","紙幣","お札","通貨","ドル","お金"],
+ "💴":["円記号の入った小切手","銀行","紙幣","お札","通貨","お金","円"],
+ "💶":["ユーロ札","銀行","紙幣","お札","通貨","ユーロ","お金"],
+ "💷":["ポンド札","銀行","紙幣","お札","通貨","お金","ポンド"],
+ "💰":["ドル袋","バッグ","ドル","お金"],
+ "🪙":["コイン","金","金属","お金","銀","宝"],
+ "💳":["クレジットカード","銀行","カード","クレジット","お金"],
+ "🪪":["身分証明書","資格情報","ID","ライセンス","セキュリティ"],
+ "🧾":["領収書","会計","簿記","証拠","証明"],
+ "💎":["宝石","ダイアモンド","ジュエル","ロマンス"],
+ "⚖":["はかり","天秤","公正","てんびん座","物差し","道具","重量","星座"],
+ "🦯":["白杖","アクセシビリティ","目が不自由"],
+ "🧰":["道具箱","胸","整備士","工具"],
+ "🔧":["レンチ","道具"],
+ "🪛":["ドライバー","ねじ","工具"],
+ "🔨":["ハンマー","道具"],
+ "⚒":["ハンマーとつるはし","ハンマー","つるはし","道具"],
+ "🛠":["ハンマーとレンチ","ハンマー","道具","レンチ"],
+ "⛏":["つるはし","採掘","道具"],
+ "🪓":["斧","たたき切り","手斧","割る","木材","工具"],
+ "🪚":["木工用のこぎり","大工","材木","のこぎり","工具"],
+ "🔩":["ナットとボルト","ボルト","ナット","道具"],
+ "⚙":["歯車","ギア","道具"],
+ "⛓":["鎖"],
+ "🪝":["フック","わな","いかさま","ペテン","誘惑","フィッシング","ツール"],
+ "🪜":["はしご","登る","横木","段","工具"],
+ "🧱":["れんが","粘土","建設","モルタル","壁"],
+ "🪨":["ロック","岩","建造物","重い","固体","石"],
+ "🪵":["木材","建造物","丸太","材木","木"],
+ "🔫":["水鉄砲","水","ピストル","噴射器","銃"],
+ "🧨":["爆竹","ダイナマイト","火薬","花火"],
+ "💣":["爆弾"],
+ "🔪":["包丁","キッチンナイフ","調理","ナイフ"],
+ "🗡":["短剣","ナイフ"],
+ "⚔":["交差した剣","交差","剣"],
+ "🛡":["盾"],
+ "🚬":["喫煙マーク","アクティビティ","喫煙"],
+ "⚰":["棺","死"],
+ "🪦":["墓石","墓地","死","墓","墓場","ハロウィーン"],
+ "⚱":["骨壷","死","葬儀"],
+ "🏺":["アンフォラ","みずがめ座","料理","飲み物","水差し","道具","星座"],
+ "🔮":["水晶玉","玉","水晶","おとぎ話","ファンタジー","占い","道具"],
+ "🪄":["魔法の杖","魔法","棒","魔女","魔法使い"],
+ "📿":["数珠状の祈りの用具","数珠","衣類","ネックレス","祈り","宗教"],
+ "🧿":["ナザールのお守り","数珠玉","お守り","邪視","ナザール","護符"],
+ "🪬":["ハムサ","お守り","ファティマ","手","メアリー","ミリアム","保護"],
+ "💈":["理髪店の看板柱","理髪店","床屋","散髪","看板柱"],
+ "🧲":["磁石","アトラクション","馬蹄"],
+ "⚗":["蒸留器","化学","実験","道具"],
+ "🧪":["試験管","化学者","化学","実験","実験室","科学"],
+ "🧫":["ペトリ皿","バクテリア","生物学者","生物学","文化","実験室"],
+ "🧬":["DNA","生物学者","進化","遺伝子","遺伝子学","生命"],
+ "🔭":["望遠鏡","ツール"],
+ "🔬":["顕微鏡","ツール"],
+ "🕳":["穴"],
+ "🩻":["X線","骨","医師","医療","骨格"],
+ "💊":["薬","医師","ピル","病気"],
+ "💉":["注射器","医師","薬","注射針","注射","病気","道具","ワクチン"],
+ "🩸":["血1滴","医師","薬","血液","生理"],
+ "🩹":["ガーゼ付きばんそうこう","医師","薬","バンドエイド","包帯","ばんそうこう"],
+ "🩺":["聴診器","医師","薬","心臓"],
+ "🌡":["温度計","天気","温度"],
+ "🩼":["松葉杖","杖","障碍","怪我","移動補助","棒"],
+ "🏷":["ラベル","荷札"],
+ "🔖":["ブックマーク","しおり","印"],
+ "🚽":["トイレ"],
+ "🪠":["プランジャー","フォースカップ","配管工","吸引","トイレ"],
+ "🚿":["シャワー","水"],
+ "🛁":["バスタブ","風呂","浴槽"],
+ "🛀":["風呂","浴槽"],
+ "🪮":["ヘアピック","アフロ","くし","髪","ピック"],
+ "🪥":["歯ブラシ","バスルーム","ブラシ","きれい","歯医者","衛生","歯"],
+ "🪒":["カミソリ","鋭い","髭剃り"],
+ "🧴":["ローションボトル","ローション","保湿剤","シャンプー","日焼け止め"],
+ "🧻":["ペーパーロール","ペーパータオル","トイレットペーパー"],
+ "🧼":["せっけん","棒","水浴び","クリーニング","泡","せっけん入れ"],
+ "🫧":["バブル","げっぷ","きれい","せっけん","水中"],
+ "🧽":["スポンジ","吸収","クリーニング","多孔性"],
+ "🧹":["ほうき","クリーニング","掃除","魔女"],
+ "🧺":["バスケット","農業","ランドリー","ピクニック"],
+ "🪣":["バケツ","たる","手桶","大だる"],
+ "🔑":["鍵","錠","パスワード"],
+ "🗝":["古い鍵","かぎ","鍵","錠","古い"],
+ "🪤":["ネズミ捕り器","餌","ネズミ","齧歯動物","輪なわ","わな"],
+ "🛋":["ソファーとランプ","ソファー","ホテル","ランプ"],
+ "🪑":["椅子","座席","座る"],
+ "🛌":["宿泊施設","寝る","ホテル","睡眠","ベッド"],
+ "🛏":["ベッド","ホテル","睡眠"],
+ "🚪":["ドア","扉"],
+ "🪞":["鏡","反射","反射体","反射鏡"],
+ "🪟":["窓","枠","新鮮な空気","ガラス","開口部","透明","視界"],
+ "🧳":["手荷物","パッキング","旅行","スーツケース"],
+ "🛎":["卓上ベル","ベル","ホテル"],
+ "🖼":["額に入った写真","アート","額縁","美術館","絵画","写真"],
+ "🧭":["コンパス","磁石","ナビゲーション","オリエンテーリング"],
+ "🗺":["世界地図","地図","世界"],
+ "⛱":["立てられたパラソル","雨","晴れ","傘","天気"],
+ "🪭":["折り畳み扇子","冷却","遠慮がち","ダンス","ファン","フラッター","熱","熱い","内気","広がる"],
+ "🗿":["モヤイ像","モアイ像","顔","像"],
+ "🛍":["買い物袋","鞄","ホテル","買い物"],
+ "🎈":["風船","アクティビティ","お祝い","エンターテイメント"],
+ "🎏":["こいのぼり","アクティビティ","鯉","お祝い","エンターテイメント","旗","吹流し"],
+ "🎀":["リボン","お祝い"],
+ "🧧":["赤い封筒","ギフト","幸運","紅包","利是","お金"],
+ "🎁":["プレゼント","箱","お祝い","エンターテイメント","贈り物","包装"],
+ "🎊":["くす玉","アクティビティ","お祝い","紙吹雪","エンターテイメント"],
+ "🎉":["クラッカー","アクティビティ","お祝い","エンターテイメント","パーティー","ジャーン"],
+ "🪅":["ピニャータ","お祝い","パーティー","ピナータ"],
+ "🪩":["ミラーボール","ダンス","ディスコ","輝き","パーティー"],
+ "🪆":["入れ子人形","人形","入れ子","ロシア"],
+ "🎎":["ひな祭り","アクティビティ","お祝い","人形","エンターテイメント","祭り","日本"],
+ "🎐":["風鈴","アクティビティ","鐘","お祝い","エンターテイメント","風"],
+ "🏮":["居酒屋の提灯","赤ちょうちん","居酒屋","日本","提灯","灯り","赤"],
+ "🪔":["ディヤランプ","ディヤ","ランプ","オイル"],
+ "✉️":["封筒","Eメール","電子メール"],
+ "📩":["メール受信中","矢印","コミュニケーション","下","Eメール","電子メール","封筒","手紙","メール","送る","送信"],
+ "📨":["メール受信","コミュニケーション","Eメール","電子メール","封筒","受け取る","手紙","メール","受信"],
+ "📧":["Eメール","コミュニケーション","電子メール","手紙","メール"],
+ "💌":["ラブレター","ハート","手紙","愛","メール","ロマンス"],
+ "📮":["ポスト","コミュニケーション","メール","郵便受け"],
+ "📪":["旗が下がっていて閉じている状態の郵便受け","閉じる","コミュニケーション","旗","下がった","メール","ポスト","郵便受け"],
+ "📫":["旗が上がっていて閉じている状態の郵便受け","閉じる","コミュニケーション","旗","メール","郵便受け","ポスト"],
+ "📬":["旗が上がっていて開いている状態の郵便受け","コミュニケーション","旗","メール","ポスト","開ける","郵便受け"],
+ "📭":["旗が下がっていて開いている郵便受け","コミュニケーション","旗","下げ","メール","メールボックス","開ける","郵便受け"],
+ "📦":["荷物","箱","コミュニケーション","パッケージ","小包"],
+ "📯":["郵便ラッパ","コミュニケーション","エンターテイメント","角","ポスト","郵便"],
+ "📥":["受信トレイ","箱","コミュニケーション","手紙","メール","受信","トレイ"],
+ "📤":["送信トレイ","箱","コミュニケーション","手紙","メール","送信","トレイ"],
+ "📜":["巻物","紙"],
+ "📃":["原稿","カール","ドキュメント","ページ"],
+ "📑":["ブックマークタブ","ブックマーク","マーク","マーカー","タブ"],
+ "📊":["棒グラフ","バー","チャート","グラフ"],
+ "📈":["上昇するグラフ","上昇チャート","チャート","グラフ","成長","トレンド","上向き"],
+ "📉":["下降するグラフ","下降チャート","チャート","下","グラフ","トレンド"],
+ "📄":["文書","ページ"],
+ "📅":["カレンダー","日付"],
+ "📆":["日めくりカレンダー","カレンダー"],
+ "🗓":["リングカレンダー","カレンダー","パッド","らせん状"],
+ "📇":["名刺フォルダ","カード","索引","ローラデックス"],
+ "🗃":["カードファイル","箱","カード","ファイル"],
+ "🗳":["投票用紙と投票箱","投票用紙","箱","票","投票"],
+ "🗄":["ファイル収納庫","収納","ファイル"],
+ "📋":["クリップボード"],
+ "🗒":["リングノート","ノート","パッド","らせん状"],
+ "📁":["フォルダ","ファイル"],
+ "📂":["開いたフォルダ","ファイル","フォルダ","開いた"],
+ "🗂":["仕切りカード","カード","仕切り","索引"],
+ "🗞":["丸めた新聞","ニュース","新聞","紙","丸めた"],
+ "📰":["新聞","コミュニケーション","ニュース","紙"],
+ "🪧":["プラカード","デモ","柵","抗議","看板"],
+ "📓":["ノート"],
+ "📕":["閉じた本","本","閉じている"],
+ "📗":["緑色の本","本","緑"],
+ "📘":["青い本","青","本"],
+ "📙":["オレンジ色の本","本","オレンジ"],
+ "📔":["装飾カバーのノート","本","カバー","装飾","ノート"],
+ "📒":["帳簿","元帳","ノート"],
+ "📚":["書籍","本"],
+ "📖":["開いた本","本","開いた"],
+ "🔗":["リンク"],
+ "📎":["クリップ","ペーパークリップ"],
+ "🖇":["繋がったペーパークリップ","コミュニケーション","リンク","ペーパークリップ"],
+ "✂️":["ハサミ","はさみ","道具"],
+ "📐":["三角定規","定規","配置","三角"],
+ "📏":["定規","直定規"],
+ "📌":["画鋲","ピン"],
+ "📍":["画鋲","ピン"],
+ "🧷":["安全ピン","おむつ","パンクロック"],
+ "🪡":["縫い針","刺しゅう","裁縫","縫い目","縫合","仕立て"],
+ "🧵":["スレッド","縫い編み","裁縫","糸巻","糸","手工芸"],
+ "🧶":["糸","ボール","かぎ針編み","ニット","手工芸"],
+ "🪢":["結び目","ロープ","絡んだ","ひも","より糸","ねじれ"],
+ "🔐":["コインロッカー","閉まっている","鍵","施錠","防犯"],
+ "🔒":["鍵","閉じられた","施錠"],
+ "🔓":["解錠","施錠","開ける"],
+ "🔏":["錠前とペン","インク","錠","ペン先","ペン","プライバシー"],
+ "🖊":["左下向きのボールペン","ボールペン","コミュニケーション","ペン"],
+ "🖋":["左下向きの万年筆","コミュニケーション","万年筆","ペン"],
+ "✒️":["ペン先","ペン"],
+ "📝":["メモ","コミュニケーション","鉛筆"],
+ "✏️":["鉛筆"],
+ "🖍":["左下向きのクレヨン","コミュニケーション","クレヨン"],
+ "🖌":["左下向きのブラシ","コミュニケーション","ペイントブラシ","絵"],
+ "🔍":["左向き虫眼鏡","眼鏡","拡大","検索","ツール"],
+ "🔎":["右向き虫眼鏡","眼鏡","拡大","検索","ツール"],
+ "❤️":["赤色のハート","ハート"],
+ "🧡":["オレンジ色のハート","ハート","オレンジ色"],
+ "💛":["黄色のハート","ハート","黄色"],
+ "💚":["緑のハート","ハート","緑"],
+ "💙":["青のハート","ハート","青"],
+ "💜":["紫のハート","ハート","紫"],
+ "🤎":["茶色のハート","ハート","茶色"],
+ "🖤":["黒いハート","ハート","黒","悪","悪者"],
+ "🤍":["白のハート","ハート","白"],
+ "💔":["割れたハート","ハート","壊れる","破局"],
+ "❣":["ハートのビックリマーク","ハート","ビックリマーク","記号"],
+ "💕":["2つのハート","ハート","愛"],
+ "💞":["回転するハート","ハート","回転"],
+ "💓":["鼓動するハート","ハート","鼓動","ドキドキ"],
+ "💗":["光るハート","ハート","ワクワク","光る","鼓動","緊張"],
+ "💖":["きらめくハート","ハート","ワクワク","キラキラ"],
+ "💘":["射抜かれたハート","ハート","矢","キューピッド","ロマンス"],
+ "💝":["リボン付きのハート","ハート","リボン","バレンタイン"],
+ "❤️‍🔥":["燃えているハート","ハート","火","燃える","愛","熱情","神聖なハート"],
+ "❤️‍🩹":["手当しているハート","ハート","健康になる","改善している","手当している","回復している","病み上がり","元気"],
+ "💟":["ハートのデコレーション","ハート"],
+ "☮":["ピースマーク","平和"],
+ "✝":["ラテン十字","クリスチャン","十字架","宗教"],
+ "☪":["星と三日月","イスラム","ムスリム","宗教"],
+ "🕉":["オームマーク","ヒンドゥー教","オーム","宗教"],
+ "☸":["法輪","仏教徒","ダーマ","宗教"],
+ "✡":["ダビデの星","ダビデ","ユダヤ人","ユダヤ教","宗教","星"],
+ "🔯":["六芒星","占い","星"],
+ "🕎":["ハヌッキーヤー","燭台","メノーラー","宗教"],
+ "☯":["陰陽","宗教","道","道家","陽","陰"],
+ "☦":["八端十字架","クリスチャン","十字架","宗教"],
+ "🪯":["カンダ","宗教","シーク教徒"],
+ "🛐":["礼拝所","宗教","礼拝"],
+ "⛎":["へびつかい座","運搬人","蛇","ヘビ","星座"],
+ "♈":["おひつじ座","仔羊","星座"],
+ "♉":["おうし座","牡牛","雄牛","星座"],
+ "♊":["ふたご座","ふたご","星座"],
+ "♋":["ガン","かに座","カニ","蟹","星座"],
+ "♌":["しし座","ライオン","星座"],
+ "♍":["おとめ座","乙女","処女","星座"],
+ "♎":["てんびん座","天秤","公正","はかり","星座"],
+ "♏":["さそり座","さそり","サソリ","星座"],
+ "♐":["いて座","射手","射手座","星座"],
+ "♑":["やぎ座","ヤギ","星座"],
+ "♒":["みずがめ座","運搬人","水","星座"],
+ "♓":["うお座","魚","星座"],
+ "🆔":["四角囲みID","ID","識別"],
+ "⚛":["元素記号","無神論者","原子"],
+ "⚕️":["アスクレピオスの杖","健康","世話","医師","薬","杖","ヘビ"],
+ "☢":["放射能標識","放射能"],
+ "☣":["バイオハザード標識","生物災害"],
+ "📴":["携帯電話電源オフ","携帯","コミュニケーション","モバイル","オフ","携帯電話","電話"],
+ "📳":["マナーモード","携帯","コミュニケーション","モバイル","モード","携帯電話","電話","バイブレーション"],
+ "🈶":["四角囲み有","日本語","あり"],
+ "🈚":["四角囲み無","四角囲み否","日本語","なし"],
+ "🈸":["四角囲み申","四角囲み適","中国語","申請"],
+ "🈺":["四角囲み営","中国語","営業"],
+ "🈷️":["四角囲み月","日本語","月極"],
+ "✴️":["八稜星","星"],
+ "🆚":["四角囲みVS","対","VS"],
+ "🉑":["丸囲み許可","丸囲み可","中国語","可能"],
+ "💮":["白い花","花","たいへんよくできました"],
+ "🉐":["丸囲み得","日本語","得"],
+ "㊙️":["丸囲み秘","中国語","表意文字","秘"],
+ "㊗️":["丸囲み祝","中国語","おめでとう","しゅく"],
+ "🈴":["四角囲みの合","四角囲み合","中国語","合格","適合"],
+ "🈵":["四角囲み満","中国語","満室","満車","満タン"],
+ "🈹":["四角囲み割","四角囲みの割","日本語","割引"],
+ "🈲":["四角囲み禁","日本語","禁止"],
+ "🅰️":["黒四角囲みA","A","血液型"],
+ "🅱️":["黒四角囲みB","B","血液型"],
+ "🆎":["黒四角囲みAB","AB","血液型"],
+ "🆑":["四角囲みCL","CL"],
+ "🅾️":["黒四角囲みO","血液型","O"],
+ "🆘":["四角囲みSOS","ヘルプ","SOS"],
+ "⛔":["立入禁止","立ち入り","禁止","だめ","できない","禁じる","交通"],
+ "📛":["名札","バッジ","名前"],
+ "🚫":["進入禁止","立ち入り","禁止","だめ","できない","禁じる"],
+ "❌":["バツ印","キャンセル","記号","掛け算","乗算","x"],
+ "⭕":["太い大きな丸","丸","O"],
+ "💢":["怒りマーク","怒り","漫画","激怒"],
+ "♨️":["温泉","温かい","湧き出る","蒸気"],
+ "🚷":["歩行者立入禁止","禁止","だめ","ない","歩行者","禁じる"],
+ "🚯":["ポイ捨て禁止","禁止","ごみ","だめ","ない","禁止されている"],
+ "🚳":["自転車禁止","自転車","バイク","禁止","だめ","できない","禁じる","乗り物"],
+ "🚱":["飲用不可","非飲料水","飲料","禁止","だめ","ない","飲用","禁止されている","水"],
+ "🔞":["18歳未満禁止","18","年齢制限","十八","禁止","だめ","ない","禁止した","未成年者"],
+ "📵":["携帯電話禁止","携帯","通信","禁止","モバイル","だめ","できない","携帯電話","禁止されている","電話"],
+ "🚭":["禁煙","禁止","だめ","できない","禁止されている","喫煙"],
+ "❗":["赤いビックリマーク","ビックリ","マーク","記号"],
+ "❕":["白いビックリマーク","ビックリ","マーク","囲み","記号"],
+ "❓":["赤いはてなマーク","マーク","記号","はてな"],
+ "❔":["白いはてなマーク","マーク","囲み","記号","はてな"],
+ "‼️":["!!マーク","バンバン","ビックリ","マーク","記号"],
+ "⁉️":["!?","ビックリ","インテロバング","マーク","記号","はてな"],
+ "💯":["100点","100","フル","百","スコア"],
+ "🔅":["低輝度","明るさ","薄暗い","低"],
+ "🔆":["高輝度","明るい","明るさ"],
+ "🔱":["トライデント","いかり","エンブレム","船","工具"],
+ "⚜":["ユリの紋章"],
+ "〽️":["庵点","印","部分"],
+ "⚠️":["警告"],
+ "🚸":["交差点を渡る子供たち","子供","交差点","歩行者","交通"],
+ "🔰":["初心者マーク","初心者","マーク","緑","日本","若葉","道具","黄"],
+ "♻️":["リサイクルマーク","リサイクル"],
+ "🈯":["四角囲み指","日本語"],
+ "💹":["上昇トレンドのチャートと円記号","上昇中円チャート","銀行","チャート","通貨","グラフ","成長","市場","お金","上昇","トレンド","上向き","円"],
+ "❇️":["キラキラ"],
+ "✳️":["アスタリスク (8本構成)","アスタリスク"],
+ "❎":["四角で囲まれたバツ印","マーク","四角"],
+ "✅":["白い太字のチェックマーク","チェック","マーク"],
+ "💠":["ドット模様のダイヤ","漫画","ダイヤモンド","幾何学","内部"],
+ "🌀":["サイクロン","低気圧","めまい","竜巻","台風","天気"],
+ "➿":["二重のカール状のループ","カール","ダブル","ループ"],
+ "🌐":["子午線・経線のある地球","地球","地球儀","経線","世界"],
+ "♾":["無限","永遠","普遍的"],
+ "Ⓜ️":["丸囲みM","円","M"],
+ "🏧":["ATM","ATM記号","自動","銀行","出納"],
+ "🚾":["トイレ","化粧室","お手洗い","水","WC"],
+ "♿":["車いす","アクセス","車椅子"],
+ "🅿️":["黒四角囲みP","駐車場"],
+ "🈳":["四角囲み空","四角囲みの空","中国語","空室","空き","空車"],
+ "🈂️":["四角囲みサ","日本人","サービス"],
+ "🛂":["入国審査","パスポート"],
+ "🛃":["税関"],
+ "🛄":["手荷物受取所","手荷物","受け取り"],
+ "🛅":["手荷物預かり所","手荷物","ロッカー","携行品"],
+ "🚰":["飲料水","飲み物","水"],
+ "🛗":["エレベーター","アクセシビリティ","引き上げ","昇降機"],
+ "🚹":["男性の記号","男性用","トイレ","男","おとこ","男性"],
+ "♂️":["男性記号","男性","男","おとこ"],
+ "🚺":["女性の記号","女性用","トイレ","女","おんな","女性"],
+ "♀️":["女性記号","女性","女","おんな"],
+ "⚧️":["トランスジェンダーサイン","トランスジェンダー","プライド","lgbt"],
+ "🚼":["赤ちゃんマーク","赤ちゃん","おむつ替え"],
+ "🚻":["トイレ","化粧室","WC"],
+ "🚮":["ゴミ捨て場","ビンのゴミ捨て場","ゴミ","ゴミ箱"],
+ "🎦":["映画","アクティビティ","カメラ","エンターテイメント","フィルム","動画"],
+ "📶":["アンテナ","バー","携帯","コミュニケーション","モバイル","携帯電話","シグナル","電話"],
+ "🛜":["無線","コンピュータ","インターネット","ネットワーク","Wi-Fi","接続"],
+ "🈁":["四角囲みココ","日本人"],
+ "🆖":["四角囲みNG","NG"],
+ "🆗":["四角囲みOK","OK"],
+ "🆙":["四角囲みUP!","マーク","上"],
+ "🆒":["COOL","かっこいい","クール"],
+ "🆕":["四角囲みnew","新"],
+ "🆓":["四角囲みFREE","フリー","無料"],
+ "0️⃣":["0キー","0","キー","ゼロ"],
+ "1️⃣":["1キー","1","キー","一"],
+ "2️⃣":["2キー","2","キー","ニ"],
+ "3️⃣":["3キー","3","キー","三"],
+ "4️⃣":["4キー","4","四","キー"],
+ "5️⃣":["5キー","5","五","キー"],
+ "6️⃣":["6キー","6","キー","六"],
+ "7️⃣":["7キー","7","キー","七"],
+ "8️⃣":["8キー","8","八","キー"],
+ "9️⃣":["9キー","9","キー","九"],
+ "🔟":["10キー","10","キー","十"],
+ "🔢":["番号の入力記号","1234","入力","数字"],
+ "▶️":["右向き三角","再生ボタン","矢印","再生","右","三角形"],
+ "⏸":["2本の垂直バー","一時停止ボタン","バー","2倍","一時停止","垂直"],
+ "⏯":["右向きの三角形と二重垂直棒","再生または一時停止ボタン","矢印","一時停止","再生","右","三角形"],
+ "⏹":["停止","停止ボタン","四角"],
+ "⏺":["録画","録画ボタン","丸"],
+ "⏏️":["取り出しマーク","取り出しボタン"],
+ "⏭":["右向きの二重三角形と垂直棒","「次の曲」ボタン","矢印","次の場面","次の曲","三角形"],
+ "⏮":["左向きの二重三角形と垂直棒","「前の曲」ボタン","矢印","前の場面","前の曲","三角形"],
+ "⏩":["右向きの二重三角形","早送りボタン","矢印","2倍","高速","進む"],
+ "⏪":["左向きの二重三角形","早戻しボタン","矢印","2倍","巻き戻し"],
+ "🔀":["ねじり右向き矢印の絵文字","シャッフル","矢印","交差"],
+ "🔁":["リピート","リピートボタン","矢印","時計回り"],
+ "🔂":["1曲をリピート再生","リピートボタン","矢印","時計回り","一度"],
+ "◀️":["左向きの三角形","反転ボタン","矢印","左","反転","三角形"],
+ "🔼":["上向きの三角形","上ボタン","矢印","ボタン","上"],
+ "🔽":["下向きの三角形","下ボタン","矢印","ボタン","下"],
+ "⏫":["上向きの二重三角形","高速上昇ボタン","矢印","ダブル","上"],
+ "⏬":["下向きの二重三角形","高速ダウンボタン","矢印","ダブル","下"],
+ "➡️":["右向き矢印","右矢印","矢印","主要","方向","東"],
+ "⬅️":["左向き矢印","左矢印","矢印","主要","方向","西"],
+ "⬆️":["上向き矢印","上矢印","矢印","主要","方向","北"],
+ "⬇️":["下向き矢印","下矢印","矢印","主要","方向","下","南"],
+ "↗️":["右上矢印","矢印","方向","斜め","北東"],
+ "↘️":["右下矢印","矢印","方向","斜め","南東"],
+ "↙️":["左下矢印","矢印","方向","斜め","南西"],
+ "↖️":["左上矢印","矢印","方向","斜め","北西"],
+ "↕️":["上下矢印","矢印","方向","斜め","北西"],
+ "↔️":["左右矢印","矢印"],
+ "🔄":["うずまき矢印","反時計回り","矢印","左回り"],
+ "↪️":["右向き段付き矢印","右に曲がった矢印","矢印"],
+ "↩️":["左向き段付き矢印","左に曲がった矢印","矢印"],
+ "🔃":["ループ矢印","時計の針","矢印","時計回り","リロード"],
+ "⤴️":["右上へカーブする矢印","上へカーブする右矢印","矢印"],
+ "⤵️":["右下へカーブする矢印","下にカーブする右矢印","矢印","下"],
+ "#️⃣":["#キー","ハッシュ","キー","ポンド"],
+ "*⃣":["アスタリスクキー","アスタリスク","キー","星"],
+ "ℹ️":["情報源","i","インフォメーション"],
+ "🔤":["アルファベット入力","abc","アルファベット","入力","ラテン","文字"],
+ "🔡":["アルファベット小文字入力","abcd","入力","ラテン","文字","小文字"],
+ "🔠":["アルファベット大文字入力","入力","ラテン","文字","大文字"],
+ "🔣":["記号入力","入力"],
+ "🎵":["音符","アクティビティ","エンターテイメント","音楽"],
+ "🎶":["複数の音符","アクティビティ","エンターテイメント","音楽","音符"],
+ "〰️":["波線","ダッシュ","記号","波"],
+ "➰":["カール状のループ","カール","ループ"],
+ "✔️":["太字のチェックマーク","チェック","マーク"],
+ "➕":["太字の+記号","数学","プラス"],
+ "➖":["太字のマイナス記号","数学","マイナス"],
+ "➗":["太字の÷記号","割り算","数学"],
+ "✖️":["太字の×印","キャンセル","乗算","かける","x"],
+ "🟰":["太い等号","等式","数学","等しい"],
+ "💲":["太字のドル記号","通貨","ドル","お金"],
+ "💱":["外貨両替","銀行","通貨","両替","お金"],
+ "©️":["コピーライトマーク","著作権"],
+ "®️":["登録商標マーク","登録済み","商標"],
+ "™️":["商標マーク","マーク","tm","商標"],
+ "🔚":["ENDと左矢印","矢印","端"],
+ "🔙":["BACKと左矢印","矢印","戻る"],
+ "🔛":["ON!と左右矢印","矢印","マーク","オン"],
+ "🔝":["TOPと上矢印","矢印","トップ","上"],
+ "🔜":["SOONと右矢印","矢印","まもなく"],
+ "☑️":["チェック入りチェックボックス","投票","ボックス","チェック"],
+ "🔘":["ラジオボタン","ボタン","幾何学","ラジオ"],
+ "🔴":["赤丸","円","幾何学","赤"],
+ "🟠":["オレンジ色の円","円","幾何学","オレンジ"],
+ "🟡":["黄色の丸","円","幾何学","茶色"],
+ "🟢":["緑丸","円","幾何学","緑"],
+ "🔵":["青丸","青","円","幾何学"],
+ "🟣":["紫の丸","円","幾何学","紫"],
+ "🟤":["茶色の丸","円","幾何学","茶色"],
+ "⚫":["黒丸","円","幾何学"],
+ "⚪":["白丸","円","幾何学"],
+ "🟥":["赤の正方形","正方形","幾何学","赤"],
+ "🟧":["オレンジ色の正方形","正方形","幾何学","オレンジ"],
+ "🟨":["黄色の正方形","正方形","幾何学","黄色"],
+ "🟩":["緑の正方形","正方形","幾何学","緑"],
+ "🟦":["青の正方形","正方形","幾何学","青"],
+ "🟪":["紫の正方形","正方形","幾何学","紫"],
+ "🟫":["茶色の正方形","正方形","幾何学","茶色"],
+ "⬛":["黒い大きな四角","幾何学","正方形"],
+ "⬜":["白い大きな四角","幾何学","正方形"],
+ "◼️":["黒い中くらいの四角","幾何学","正方形"],
+ "◻️":["白くて中くらいの四角","幾何学","正方形"],
+ "◾":["黒くて中くらいの小さい四角","幾何学","正方形"],
+ "◽":["白い中くらいの小さな四角","幾何学","正方形"],
+ "▪️":["黒い小さな四角","幾何学","正方形"],
+ "▫️":["白い小さな四角","幾何学","正方形"],
+ "🔸":["小さいオレンジのダイヤモンド","ダイヤモンド","幾何学","オレンジ"],
+ "🔹":["小さくて青いダイヤモンド","青","ダイヤモンド","幾何学"],
+ "🔶":["大きいオレンジのダイヤ","ダイヤモンド","幾何学","オレンジ"],
+ "🔷":["大きくて青いダイヤモンド","青","ダイヤモンド","幾何学"],
+ "🔺":["上向きの赤い三角形","上","幾何学","赤"],
+ "🔻":["下向きの三角形","ダウン","幾何学","赤"],
+ "🔲":["黒い四角ボタン","ボタン","幾何学","正方形"],
+ "🔳":["白い四角ボタン","ボタン","幾何学","囲み","四角"],
+ "🔈":["スピーカー","音量"],
+ "🔉":["音量小","電源が入ったスピーカー","低い","スピーカー","音量","波"],
+ "🔊":["音量大","大音量のスピーカー","3","エンターテイメント","高い","音の大きい","スピーカー","ボリューム"],
+ "🔇":["無音のスピーカー","スピーカー","オフ","ミュート","静音","無音","音量"],
+ "📣":["メガホン","応援","コミュニケーション","拡声器"],
+ "📢":["拡声器","コミュニケーション","大声","スピーカー","パブリックアドレス","メガホン"],
+ "🔔":["ベル"],
+ "🔕":["ミュート","スラッシュベル","鐘","禁じられた","だめ","ない","禁止","静か"],
+ "🃏":["トランプのジョーカー","カード","エンターテイメント","ゲーム","ジョーカー","プレイ"],
+ "🀄":["麻雀牌の中","ゲーム","麻雀","赤"],
+ "♠️":["トランプのスペード","カード","ゲーム","スペード","スーツ"],
+ "♣️":["トランプのクラブ","カード","クラブ","ゲーム","スーツ"],
+ "♥️":["トランプのハート","カード","ゲーム","ハート","スーツ"],
+ "♦️":["トランプのダイヤ","カード","ダイヤ","ダイヤモンド","ゲーム","スーツ"],
+ "🎴":["花札","アクティビティ","カード","エンターテイメント","花","ゲーム","日本","プレイ"],
+ "👁‍🗨":["吹き出しの目","吹き出し","目","スピーチ","証人"],
+ "🗨":["左向きの吹き出し","セリフ","スピーチ"],
+ "💭":["考え吹き出し","吹き出し","泡","漫画","考え"],
+ "🗯":["右向きの怒りの吹き出し","怒り","吹き出し","泡","激怒"],
+ "💬":["吹き出し","泡","漫画","セリフ","スピーチ"],
+ "🕐":["1時","0分","1","時計","時","一"],
+ "🕑":["2時","0分","2","時計","時","二"],
+ "🕒":["3時","0分","3","時計","時","三"],
+ "🕓":["4時","0分","4","時計","四","時"],
+ "🕔":["5時","0分","5","時計","五","時"],
+ "🕕":["6時","0分","6","時計","時","六"],
+ "🕖":["7時","0分","7","時計","時","七"],
+ "🕗":["8時","0分","8","時計","八","時"],
+ "🕘":["9時","0分","9","時計","九","時"],
+ "🕙":["10時","0分","10","時計","時","十"],
+ "🕚":["11時","0分","11","時計","十一","時"],
+ "🕛":["12時","0分","12","時計","十二","時"],
+ "🕜":["1時半","1時","半","時刻","一","30"],
+ "🕝":["2時半","2時","半","時刻","30","二"],
+ "🕞":["3時半","3時","半","時刻","30","三"],
+ "🕟":["4時半","30","4時","時刻","四","半"],
+ "🕠":["5時半","30","5時","時刻","五","半"],
+ "🕡":["6時半","30","6時","時刻","六","半"],
+ "🕢":["7時半","30","7時","時刻","七","半"],
+ "🕣":["8時半","30","8時","時刻","八","半"],
+ "🕤":["9時半","30","9時","時刻","九","半"],
+ "🕥":["10時半","10時","半","時刻","十","30"],
+ "🕦":["11時半","11時","半","時刻","十一","30"],
+ "🕧":["12時半","12時","半","時刻","30","十二"],
+ "🏳":["なびく白旗","旗","なびく"],
+ "🏴":["なびく黒旗","旗","なびく"],
+ "🏁":["チェッカーフラッグ","市松模様","旗","レース"],
+ "🚩":["三角旗","旗","ポスト"],
+ "🎌":["交差旗","アクティビティ","お祝い","交差","交差した","旗","日本"],
+ "🏴‍☠️":["海賊旗","旗","海賊"],
+ "🏳️‍🌈":["レインボーフラッグ","フラッグ","レインボー","プライド","lgbt"],
+ "🏳️‍⚧️":["トラスジェンダーフラッグ","フラッグ","トランスジェンダー","プライド","lgbt"],
+ "🇦🇨":["アセンション島の旗","アセンション","国旗","島"],
+ "🇦🇩":["アンドラ国旗","アンドラ","国旗"],
+ "🇦🇪":["アラブ首長国連邦国旗","首長国","国旗","アラブ首長国連邦","連邦"],
+ "🇦🇫":["アフガニスタン国旗","アフガニスタン","国旗"],
+ "🇦🇬":["アンティグア・バーブーダ国旗","アンティグア","バーブーダ","国旗"],
+ "🇦🇮":["アンギラ島の旗","アンギラ島","国旗"],
+ "🇦🇱":["アルバニア国旗","アルバニア","国旗"],
+ "🇦🇲":["アルメニア国旗","アルメニア","国旗"],
+ "🇦🇴":["アンゴラ国旗","アンゴラ","国旗"],
+ "🇦🇶":["南極大陸の旗","南極大陸","国旗"],
+ "🇦🇷":["アルゼンチン国旗","アルゼンチン","国旗"],
+ "🇦🇸":["アメリカ領サモアの旗","アメリカ領","国旗","サモア"],
+ "🇦🇹":["オーストリア国旗","オーストリア","国旗"],
+ "🇦🇺":["オーストラリア国旗","オーストラリア","国旗","ハード","マクドナルド"],
+ "🇦🇼":["アルバ国旗","アルバ","国旗"],
+ "🇦🇽":["オーランド諸島の旗","オーランド諸島","国旗"],
+ "🇦🇿":["アゼルバイジャン国旗","アゼルバイジャン","国旗"],
+ "🇧🇦":["ボスニア・ヘルツェゴビナ国旗","ボスニア","国旗","ヘルツェゴビナ"],
+ "🇧🇧":["バルバドス国旗","バルバドス","国旗"],
+ "🇧🇩":["バングラデシュ国旗","バングラデシュ","国旗"],
+ "🇧🇪":["ベルギー国旗","ベルギー","国旗"],
+ "🇧🇫":["ブルキナファソ国旗","ブルキナファソ","国旗"],
+ "🇧🇬":["ブルガリア国旗","ブルガリア","国旗"],
+ "🇧🇭":["バーレーン国旗","バーレーン","国旗"],
+ "🇧🇮":["ブルンジ国旗","ブルンジ","国旗"],
+ "🇧🇯":["ベナン国旗","ベナン","国旗"],
+ "🇧🇱":["サン・バルテルミー島の旗","バルテルミー","国旗","サン"],
+ "🇧🇲":["バミューダ諸島の旗","バミューダ諸島","国旗"],
+ "🇧🇳":["ブルネイ国旗","ブルネイ","ダルサラーム","国旗"],
+ "🇧🇴":["ボリビア国旗","ボリビア","国旗"],
+ "🇧🇶":["カリブ海のオランダ領島の旗","ボネール島","カリブ海","ユースタティウス","国旗","オランダ","サバ","シント"],
+ "🇧🇷":["ブラジル国旗","ブラジル","国旗"],
+ "🇧🇸":["バハマ国旗","バハマ","国旗"],
+ "🇧🇹":["ブータン国旗","ブータン","国旗"],
+ "🇧🇼":["ボツワナ国旗","ボツワナ","国旗"],
+ "🇧🇾":["ベラルーシ国旗","ベラルーシ","国旗"],
+ "🇧🇿":["ベリーズ国旗","ベリーズ","国旗"],
+ "🇨🇦":["カナダ国旗","カナダ","国旗"],
+ "🇨🇨":["ココス諸島の旗","ココス","国旗","諸島","キーリング"],
+ "🇨🇩":["コンゴ国旗 - キンシャサ","コンゴ","コンゴ - キンシャサ","コンゴ民主共和国","国旗","キンシャサ","共和国"],
+ "🇨🇫":["中央アフリカ国旗","中央アフリカ共和国","国旗","共和国"],
+ "🇨🇬":["コンゴの旗 - ブラザビル","ブラザビル","コンゴ","コンゴ共和国","コンゴ - ブラザビル","国旗","共和国"],
+ "🇨🇭":["スイス国旗","国旗","スイス"],
+ "🇨🇮":["コートジボワール国旗","コートジボワール","国旗"],
+ "🇨🇰":["クック諸島国旗","クック","国旗","諸島"],
+ "🇨🇱":["チリ国旗","チリ","国旗"],
+ "🇨🇲":["カメルーン国旗","カメルーン","国旗"],
+ "🇨🇳":["中国国旗","中国","国旗"],
+ "🇨🇴":["コロンビア国旗","コロンビア","国旗"],
+ "🇨🇷":["コスタリカ国旗","コスタリカ","国旗"],
+ "🇨🇺":["キューバ国旗","キューバ","国旗"],
+ "🇨🇻":["カーボベルデ国旗","カーボ","ケープ","国旗","ベルデ"],
+ "🇨🇼":["キュラソー島の旗","アンティル諸島","キュラソー","国旗"],
+ "🇨🇽":["クリスマス島の旗","クリスマス","国旗","島"],
+ "🇨🇾":["キプロス国旗","キプロス","国旗"],
+ "🇨🇿":["チェコ国旗","チェコ共和国","国旗"],
+ "🇩🇪":["ドイツ国旗","国旗","ドイツ"],
+ "🇩🇯":["ジブチ国旗","ジブチ","国旗"],
+ "🇩🇰":["デンマーク国旗","デンマーク","国旗"],
+ "🇩🇲":["ドミニカ国旗","ドミニカ","国旗"],
+ "🇩🇴":["ドミニカ共和国国旗","ドミニカ共和国","国旗"],
+ "🇩🇿":["アルジェリア国旗","アルジェリア","国旗"],
+ "🇪🇨":["エクアドル国旗","エクアドル","国旗"],
+ "🏴󠁧󠁢󠁥󠁮󠁧󠁿":["イングランドの旗","イングランド","旗"],
+ "🇪🇪":["エストニア国旗","エストニア","国旗"],
+ "🇪🇬":["エジプト国旗","エジプト","国旗"],
+ "🇪🇭":["西サハラの旗","国旗","サハラ","西","西サハラ"],
+ "🇪🇷":["エリトリア国旗","エリトリア","国旗"],
+ "🇪🇸":["スペイン国旗","国旗","スペイン","セウタ","メリリャ"],
+ "🇪🇹":["エチオピア国旗","エチオピア","国旗"],
+ "🇪🇺":["欧州旗","欧州連合","旗"],
+ "🇫🇮":["フィンランド国旗","フィンランド","国旗"],
+ "🇫🇯":["フィジー国旗","フィジー","国旗"],
+ "🇫🇰":["フォークランド諸島の旗","フォークランド","フォークランド諸島","国旗","諸島","マルビナス"],
+ "🇫🇲":["ミクロネシア国旗","国旗","ミクロネシア"],
+ "🇫🇴":["フェロー諸島の旗","フェロー","旗","諸島"],
+ "🇫🇷":["フランス国旗","国旗","フランス","クリッパートン島","セント・マーチン","サン・マルタン"],
+ "🇬🇦":["ガボン国旗","国旗","ガボン"],
+ "🇬🇧":["イギリス国旗","イギリス","イギリス領","コーンウォール","イングランド","国旗","グレートブリテン","アイルランド","北アイルランド","スコットランド","UK","ユニオンジャック","連合","連合王国","ウェールズ"],
+ "🇬🇩":["グレナダ国旗","国旗","グレナダ"],
+ "🇬🇪":["ジョージア国旗","国旗","ジョージア"],
+ "🇬🇫":["フランス領ギアナの旗","国旗","フランス領","ギアナ"],
+ "🇬🇬":["ガーンジー国旗","国旗","ガーンジー"],
+ "🇬🇭":["ガーナ国旗","国旗","ガーナ"],
+ "🇬🇮":["ジブラルタル国旗","国旗","ジブラルタル"],
+ "🇬🇱":["グリーンランド国旗","国旗","グリーンランド"],
+ "🇬🇲":["ガンビア国旗","国旗","ガンビア"],
+ "🇬🇳":["ギニア国旗","国旗","ギニア"],
+ "🇬🇵":["グアドループ国旗","国旗","グアドループ"],
+ "🇬🇶":["赤道ギニア国旗","赤道ギニア","国旗","ギニア"],
+ "🇬🇷":["ギリシャ国旗","国旗","ギリシャ"],
+ "🇬🇸":["サウスジョージア・サウスサンドウィッチ諸島国旗","国旗","ジョージア","諸島","サウス","サウスジョージア","サウスサンドウィッチ"],
+ "🇬🇹":["グアテマラ国旗","国旗","グアテマラ"],
+ "🇬🇺":["グアム旗","国旗","グアム"],
+ "🇬🇼":["ギニアビサウ国旗","ビサウ","国旗","ギニア"],
+ "🇬🇾":["ガイアナ国旗","国旗","ガイアナ"],
+ "🇭🇰":["香港の旗","中国","国旗","香港"],
+ "🇭🇳":["ホンジュラス国旗","国旗","ホンジュラス"],
+ "🇭🇷":["クロアチア国旗","クロアチア","国旗"],
+ "🇭🇹":["ハイチ国旗","国旗","ハイチ"],
+ "🇭🇺":["ハンガリー国旗","国旗","ハンガリー"],
+ "🇮🇨":["カナリア諸島の旗","カナリア","国旗","諸島"],
+ "🇮🇩":["インドネシア国旗","国旗","インドネシア"],
+ "🇮🇪":["アイルランド国旗","国旗","アイルランド"],
+ "🇮🇱":["イスラエル国旗","国旗","イスラエル"],
+ "🇮🇲":["マン島の旗","国旗","マン島"],
+ "🇮🇳":["インド国旗","国旗","インド"],
+ "🇮🇴":["イギリス領インド洋地域の旗","イギリス領","チャゴス","旗","インド洋","島","ディエゴガルシア"],
+ "🇮🇶":["イラク国旗","国旗","イラク"],
+ "🇮🇷":["イラン国旗","国旗","イラン"],
+ "🇮🇸":["アイスランド国旗","国旗","アイスランド"],
+ "🇮🇹":["イタリア国旗","国旗","イタリア"],
+ "🇯🇪":["ジャージー代官管轄区の旗","国旗","ジャージー代官管轄区"],
+ "🇯🇲":["ジャマイカ国旗","国旗","ジャマイカ"],
+ "🇯🇴":["ヨルダン国旗","国旗","ヨルダン"],
+ "🇯🇵":["日本国旗","国旗","日本"],
+ "🇰🇪":["ケニア国旗","国旗","ケニア"],
+ "🇰🇬":["キルギス国旗","国旗","キルギス"],
+ "🇰🇭":["カンボジア国旗","カンボジア","国旗"],
+ "🇰🇮":["キリバス国旗","国旗","キリバス"],
+ "🇰🇲":["コモロ国旗","コモロ","国旗"],
+ "🇰🇳":["セントクリストファー・ネイビス国旗","国旗","キッツ","ネイビス","セント"],
+ "🇰🇵":["北朝鮮国旗","国旗","朝鮮","北","北朝鮮"],
+ "🇰🇷":["韓国国旗","国旗","韓国","南","大韓民国"],
+ "🇰🇼":["クウェート国旗","国旗","クウェート"],
+ "🇰🇾":["ケイマン諸島の旗","ケイマン","国旗","諸島"],
+ "🇰🇿":["カザフスタン国旗","国旗","カザフスタン"],
+ "🇱🇦":["ラオス国旗","国旗","ラオス"],
+ "🇱🇧":["レバノン国旗","国旗","レバノン"],
+ "🇱🇨":["セントルシア国旗","国旗","セントルシア"],
+ "🇱🇮":["リヒテンシュタイン国旗","国旗","リヒテンシュタイン"],
+ "🇱🇰":["スリランカ国旗","国旗","スリランカ"],
+ "🇱🇷":["リベリア国旗","国旗","リベリア"],
+ "🇱🇸":["レソト国旗","国旗","レソト"],
+ "🇱🇹":["リトアニア国旗","国旗","リトアニア"],
+ "🇱🇺":["ルクセンブルク国旗","国旗","ルクセンブルク"],
+ "🇱🇻":["ラトビア国旗","国旗","ラトビア"],
+ "🇱🇾":["リビア国旗","国旗","リビア"],
+ "🇲🇦":["モロッコ国旗","国旗","モロッコ"],
+ "🇲🇨":["モナコ国旗","国旗","モナコ"],
+ "🇲🇩":["モルドバ国旗","国旗","モルドバ"],
+ "🇲🇪":["モンテネグロ国旗","国旗","モンテネグロ"],
+ "🇲🇬":["マダガスカル国旗","国旗","マダガスカル"],
+ "🇲🇭":["マーシャル諸島国旗","国旗","諸島","マーシャル"],
+ "🇲🇰":["マケドニア国旗","国旗","マケドニア"],
+ "🇲🇱":["マリ国旗","国旗","マリ"],
+ "🇲🇲":["ミャンマー国旗","ビルマ","国旗","ミャンマー"],
+ "🇲🇳":["モンゴル国旗","国旗","モンゴル"],
+ "🇲🇴":["マカオの旗","中国","国旗","マカオ"],
+ "🇲🇵":["北マリアナ諸島の旗","国旗","諸島","マリアナ","北","北マリアナ"],
+ "🇲🇶":["マルティニークの旗","旗","マルティニーク"],
+ "🇲🇷":["モーリタニア国旗","国旗","モーリタニア"],
+ "🇲🇸":["モントセラトの旗","旗","モントセラト"],
+ "🇲🇹":["マルタ国旗","国旗","マルタ"],
+ "🇲🇺":["モーリシャス国旗","国旗","モーリシャス"],
+ "🇲🇻":["モルディブ国旗","国旗","モルディブ"],
+ "🇲🇼":["マラウイ国旗","国旗","マラウイ"],
+ "🇲🇽":["メキシコ国旗","国旗","メキシコ"],
+ "🇲🇾":["マレーシア国旗","国旗","マレーシア"],
+ "🇲🇿":["モザンビーク国旗","国旗","モザンビーク"],
+ "🇳🇦":["ナミビア国旗","国旗","ナミビア"],
+ "🇳🇨":["ニューカレドニアの旗","国旗","ニュー","ニューカレドニア"],
+ "🇳🇪":["ニジェール国旗","国旗","ニジェール"],
+ "🇳🇫":["ノーフォーク島の旗","旗","島","ノーフォーク"],
+ "🇳🇬":["ナイジェリア国旗","国旗","ナイジェリア"],
+ "🇳🇮":["ニカラグア国旗","国旗","ニカラグア"],
+ "🇳🇱":["オランダ国旗","国旗","オランダ"],
+ "🇳🇴":["ノルウェー国旗","旗","ノルウェー","ブーべ","スヴァールバル","ヤンマイエン"],
+ "🇳🇵":["ネパール国旗","国旗","ネパール"],
+ "🇳🇷":["ナウル国旗","国旗","ナウル"],
+ "🇳🇺":["ニウエ国旗","国旗","ニウエ"],
+ "🇳🇿":["ニュージーランド国旗","国旗","ニュー","ニュージーランド"],
+ "🇴🇲":["オマーン国旗","国旗","オマーン"],
+ "🇵🇦":["パナマ国旗","国旗","パナマ"],
+ "🇵🇪":["ペルー国旗","国旗","ペルー"],
+ "🇵🇫":["フランス領ポリネシアの旗","国旗","フランス領","ポリネシア"],
+ "🇵🇬":["パプアニューギニア国旗","国旗","ギニア","ニュー","パプアニューギニア"],
+ "🇵🇭":["フィリピン国旗","国旗","フィリピン"],
+ "🇵🇰":["パキスタン国旗","国旗","パキスタン"],
+ "🇵🇱":["ポーランド国旗","国旗","ポーランド"],
+ "🇵🇲":["サンピエール島・ミクロン島の旗","旗","ミクロン","ピエール","サン"],
+ "🇵🇳":["ピトケアン諸島の旗","旗","諸島","ピトケアン"],
+ "🇵🇷":["プエルトリコの旗","国旗","プエルトリコ"],
+ "🇵🇸":["パレスチナ自治政府の旗","国旗","パレスチナ"],
+ "🇵🇹":["ポルトガル国旗","国旗","ポルトガル"],
+ "🇵🇼":["パラオ国旗","国旗","パラオ"],
+ "🇵🇾":["パラグアイ国旗","国旗","パラグアイ"],
+ "🇶🇦":["カタール国旗","国旗","カタール"],
+ "🇷🇪":["レユニオンの旗","旗","レユニオン"],
+ "🇷🇴":["ルーマニア国旗","国旗","ルーマニア"],
+ "🇷🇸":["セルビア国旗","国旗","セルビア"],
+ "🇷🇺":["ロシア国旗","国旗","ロシア"],
+ "🇷🇼":["ルワンダ国旗","国旗","ルワンダ"],
+ "🇸🇦":["サウジアラビア国旗","国旗","サウジアラビア"],
+ "🏴󠁧󠁢󠁳󠁣󠁴󠁿":["スコットランドの旗","スコットランド","旗"],
+ "🇸🇧":["ソロモン諸島国旗","旗","諸島","ソロモン"],
+ "🇸🇨":["セーシェル国旗","国旗","セーシェル"],
+ "🇸🇩":["スーダン国旗","国旗","スーダン"],
+ "🇸🇪":["スウェーデン国旗","国旗","スウェーデン"],
+ "🇸🇬":["シンガポール国旗","国旗","シンガポール"],
+ "🇸🇭":["セントヘレナ島の旗","旗","ヘレナ","セント"],
+ "🇸🇮":["スロベニア国旗","国旗","スロベニア"],
+ "🇸🇰":["スロバキア国旗","国旗","スロバキア"],
+ "🇸🇱":["シエラレオネ国旗","国旗","シエラレオネ"],
+ "🇸🇲":["サンマリノ国旗","国旗","サンマリノ"],
+ "🇸🇳":["セネガル国旗","国旗","セネガル"],
+ "🇸🇴":["ソマリア国旗","国旗","ソマリア"],
+ "🇸🇷":["スリナム国旗","国旗","スリナム"],
+ "🇸🇸":["南スーダン国旗","国旗","南","南スーダン","スーダン"],
+ "🇸🇹":["サントメ・プリンシペ国旗","国旗","プリンシペ","プリンシピ","サントメ","サォントメー"],
+ "🇸🇻":["エルサルバドル国旗","エルサルバドル","国旗"],
+ "🇸🇽":["セント・マーチン島の旗","旗","マーチン","セント"],
+ "🇸🇾":["シリア国旗","国旗","シリア"],
+ "🇸🇿":["スワジランド国旗","国旗","スワジランド"],
+ "🇹🇦":["トリスタンダクーニャの旗","旗","トリスタン・ダ・クーニャ"],
+ "🇹🇨":["タークス・カイコス諸島の旗","カイコス","旗","諸島","タークス"],
+ "🇹🇩":["チャド国旗","チャド","国旗"],
+ "🇹🇫":["フランス領南方・南極地域の旗","南極","国旗","フランス領"],
+ "🇹🇬":["トーゴ国旗","国旗","トーゴ"],
+ "🇹🇭":["タイ国旗","国旗","タイ"],
+ "🇹🇯":["タジキスタン国旗","国旗","タジキスタン"],
+ "🇹🇰":["トケラウ旗","国旗","トケラウ"],
+ "🇹🇱":["東ティモール国旗","東","東ティモール","国旗","ティモール・レステ"],
+ "🇹🇲":["トルクメニスタン国旗","国旗","トルクメニスタン"],
+ "🇹🇳":["チュニジア国旗","国旗","チュニジア"],
+ "🇹🇴":["トンガ国旗","国旗","トンガ"],
+ "🇹🇷":["トルコ国旗","国旗","トルコ"],
+ "🇹🇹":["トリニダード・トバゴ国旗","国旗","トバゴ","トリニダード"],
+ "🇹🇻":["ツバル国旗","国旗","ツバル"],
+ "🇹🇼":["台湾の旗","中国","国旗","台湾"],
+ "🇹🇿":["タンザニア国旗","国旗","タンザニア"],
+ "🇺🇦":["ウクライナ国旗","国旗","ウクライナ"],
+ "🇺🇬":["ウガンダ国旗","国旗","ウガンダ"],
+ "🇺🇳":["国連の旗","旗","国連","連合","国際"],
+ "🇺🇸":["アメリカ国旗","アメリカ","旗","合衆","合衆国","アメリカ合衆国","合衆国領有小離島"],
+ "🇺🇾":["ウルグアイ国旗","国旗","ウルグアイ"],
+ "🇺🇿":["ウズベキスタン国旗","国旗","ウズベキスタン"],
+ "🇻🇦":["バチカン市国旗","国旗","バチカン"],
+ "🇻🇨":["セントビンセント・グレナディーン国旗","国旗","グレナディーン諸島","セント","ビンセント"],
+ "🇻🇪":["ベネズエラ国旗","国旗","ベネズエラ"],
+ "🇻🇬":["イギリス領ヴァージン諸島の旗","イギリス領","国旗","島","ヴァージン"],
+ "🇻🇮":["アメリカ領ヴァージン諸島の旗","アメリカ","国旗","島","アメリカ合衆国","合衆国","ヴァージン"],
+ "🇻🇳":["ベトナム国旗","国旗","ベトナム","ヴェトナム"],
+ "🇻🇺":["バヌアツ国旗","国旗","バヌアツ"],
+ "🏴󠁧󠁢󠁷󠁬󠁳󠁿":["ウェールズの旗","ウェールズ","旗"],
+ "🇼🇫":["ウォリス・フツナの旗","国旗","フツナ","ウォリス"],
+ "🇼🇸":["サモア国旗","国旗","サモア"],
+ "🇽🇰":["コソボ国旗","国旗","コソボ"],
+ "🇾🇪":["イエメン国旗","国旗","イエメン"],
+ "🇾🇹":["マヨットの旗","国旗","マヨット"],
+ "🇿🇦":["南アフリカ国旗","国旗","南","南アフリカ"],
+ "🇿🇲":["ザンビア国旗","国旗","ザンビア"],
+ "🇿🇼":["ジンバブエ国旗","国旗","ジンバブエ"],
+ "": ["渋谷109", "SHIBUYA109", "109"]
+}
diff --git a/packages/frontend/src/unicode-emoji-indexes/ja-JP_hira.json b/packages/frontend/src/unicode-emoji-indexes/ja-JP_hira.json
new file mode 100644
index 0000000000..2ad282d501
--- /dev/null
+++ b/packages/frontend/src/unicode-emoji-indexes/ja-JP_hira.json
@@ -0,0 +1,1866 @@
+{
+ "😀": ["にやにやしたかお","かお","にやにや","しあわせ"],
+ "😃": ["くちをあけたえがお","かお","くち","あける","えがお","しあわせ"],
+ "😄": ["くちをあけてめがわらっているえがお","め","かお","くち","あける","えがお","しあわせ"],
+ "😁": ["にやにやしたかお","め","かお","にやにや","えがお"],
+ "😆": ["くちをあけてわらっているかお","かお","わらい","くち","あける","まんぞく","えがお"],
+ "😅": ["くちをあけてひやあせをかいたえがお","ぞっとする","かお","くちをあける","えがお","ひやあせ"],
+ "😂": ["うれしなき","かお","うれしい","わらう","なく","なみだ"],
+ "🤣": ["だいばくしょう","かお","ゆか","わらい","おおわらい","ばくしょう","ぐるぐる"],
+ "😇": ["てんしのえがお","てんし","かお","おとぎばなし","ふぁんたじー","てんしのわ","むじゃき","えがお"],
+ "😉": ["ういんくしたかお","かお","ういんく"],
+ "😊": ["めがわらっているえがお","せきめん","め","かお","えがお"],
+ "🙂": ["ほほえみ","かお","えがお","しあわせ"],
+ "🙃": ["さかさのかお","かお","さかさ"],
+ "☺️": ["えがお","かお","りんかく","りらっくす"],
+ "😋": ["たべものをあじわうかお","おいしい","かお","あじわう","ふーむ","うまい"],
+ "😌": ["ほっとしたかお","かお","あんしん","ほっとする"],
+ "😍": ["めがはーとのえがお","め","かお","はーと","あい","えがお"],
+ "🥰": ["えがおとはーと","かお","けいあい","べたぼれ","あい"],
+ "😘": ["なげきっす","かお","はーと","きす"],
+ "😗": ["きすをするかお","かお","きす"],
+ "😙": ["えがおできす","め","かお","きす","えがお"],
+ "😚": ["めをとじてきすをするかお","とじた","め","かお","きす"],
+ "🥲": ["なみだのでているえがお","なく","しあわせ","かんしゃする","ほこりにおもう","あんしんする","わらう"],
+ "🤪": ["おどけたかお","め","にやにや","へん","こうふん","わいるど"],
+ "😜": ["したをだしてういんくしているかお","め","かお","じょうだん","した","ういんく"],
+ "😝": ["したをだしてめをほそめているかお","め","かお","こわい","あじ","した"],
+ "😛": ["したをだしているかお","かお","した"],
+ "🤑": ["ごうよくなかお","かお","おかね","くち"],
+ "😎": ["さんぐらすをかけたかお","あかるい","かっこいい","め","あいうぇあ","かお","めがね","えがお","たいよう","さんぐらす","てんき"],
+ "🤓": ["おたく","かお","へんなひと"],
+ "🥸": ["かそうしたかお","かそう","めがね","とくめいのひと","はな"],
+ "🧐": ["かためがねをかけたかお","たいくつ","ゆうふく","ゆたか"],
+ "🤠": ["かうぼーいはっとのかお","かうぼーい","かうがーる","かお","ぼうし"],
+ "🥳": ["ぱーてぃーふぇいす","かお","しゅくてん","ぼうし","つの","ぱーてぃー"],
+ "🤡": ["ぴえろのかお","ぴえろ","かお"],
+ "😏": ["にやにやしたかお","かお","にやにや"],
+ "😶": ["くちのないかお","かお","くち","しずかに","ちんもく"],
+ "🫥": ["てんせんのかお","おちこんだ","きえる","かくれる","ないこうてき","めにみえない"],
+ "😐": ["ふつうのかお","むひょうじょう","かお","へいせい"],
+ "🫤": ["くちがななめになったかお","がっかり","むかんしん","うたがいぶかい","ふあん"],
+ "😑": ["むひょうじょう","かお","ぽーかーふぇいす","むかんじょう"],
+ "😒": ["おもしろくなさそうなかお","かお","つまらない","ふこう"],
+ "🙄": ["ぐるぐるめのかお","め","かお","ぐるぐる"],
+ "🤨": ["まゆがあがっているかお","ふしん","うたがいぶかい","ひなん","ぎねん","ややおどろき","かいぎてき"],
+ "🤔": ["かんがえているかお","かお","かんがえちゅう"],
+ "🤫": ["しっといっているかお","しーっ","しずか","だまる"],
+ "🤭": ["くちをてでおおったかお","め","えがお","おおう","くち","て"],
+ "🫢": ["めをひらいてくちをてでおおったかお","きょうたん","いけい","ふしん","ろうばい","こわい","おどろき"],
+ "🫡": ["けいれいしているかお","ok","けいれい","せいてん","ぶたい","はい"],
+ "🤗": ["りょうてをひろげたえがお","かお","はぐ","だきしめる"],
+ "🫣": ["のぞきみしているかお","みりょう","のぞきみ","ぎょうし","ちらみ"],
+ "🤥": ["うそつきがお","かお","うそ","ぴのきお"],
+ "😳": ["あかくなったかお","ぼーっとした","ぼうっとした","かお","せきめん"],
+ "😞": ["がっかりしたかお","がっかり","かお"],
+ "😟": ["ふあんなかお","かお","しんぱい","ふあん"],
+ "😤": ["かちほこったかお","かお","しょうり","かつ"],
+ "😠": ["おこったかお","いかり","おこった","かお","げきど"],
+ "😡": ["ふくれがお","いかり","おこった","かお","げきど","ふくれっつら","ふんど","あか"],
+ "🤬": ["くちがきごうでおおわれたかお","のろい","ののしり"],
+ "😔": ["かなしげなかお","がっかり","かお","かなしい"],
+ "😕": ["こまったかお","こまった","かお"],
+ "🙁": ["ごきげんななめ","かお","しかめっつら","かなしい","ふこう"],
+ "☹": ["しかめっつら","かお","かなしい","ふこう"],
+ "😬": ["しかめっつら","かお"],
+ "🥺": ["うったえかけるかお","かお","ものごい","じひ","こいぬのめ"],
+ "😣": ["がまんしているかお","かお","がんばる"],
+ "😖": ["うろたえたかお","とまどい","うろたえ","かお"],
+ "😫": ["つかれたかお","かお","つかれた"],
+ "😩": ["うんざりしているかお","かお","つかれた","うんざり"],
+ "🥱": ["あくびしているかお","あきた","つかれた","あくび"],
+ "😪": ["ねむいかお","かお","ねる","すいみん"],
+ "😮‍💨": ["ためいきのでているかお","かお","ためいき","いきぎれ","うめき","あんしん","ささやき","くちぶえ"],
+ "😮": ["くちをあけたえがお","かお","くち","あける","どうじょう"],
+ "😱": ["ぜっきょうしたかお","かお","きょうふ","こわい","むんく","おびえ","ぜっきょう"],
+ "😨": ["ぞっとしているかお","かお","きょうふ","こわい","おびえ"],
+ "😰": ["くちをあけてひやあせをかいたかお","あおざめる","ぞっとする","かお","くち","あける","いそぐ","ひやあせ"],
+ "😥": ["がっかりしたがあんしんしたかお","がっかり","かお","あんしん","ほっとする","やれやれ"],
+ "😓": ["ひやあせをかいているかお","ぞっとする","かお","ひやあせ"],
+ "😯": ["おちついたかお","かお","だまる","ぼうぜん","おどろき"],
+ "😦": ["しんぱいそうなかおのえもじ","かお","しかめっつら","くち","あける"],
+ "😧": ["くのうにみちたかお","くのう","かお"],
+ "🥹": ["なみだをこらえているかお","おこる","なく","ほこりにおもう","さからう","かなしむ"],
+ "😢": ["なきがお","なく","かお","かなしい","なみだ"],
+ "😭": ["ごうきゅう","なく","かお","かなしい","なみだ"],
+ "🤤": ["よだれをたらしたかお","よだれ","かお"],
+ "🤩": ["すたーにむちゅう","め","かお","にやにや","ほし","むそうてき"],
+ "😵": ["めがばつになったかお","めまい","かお","ばつ","め"],
+ "😵‍💫": ["めがぐるぐるしているかお","めまい","かお","め","うっとり","ぐるぐる","とらぶる","おー"],
+ "🥴": ["ぼんやしりたかお","かお","めまい","めいてい","ほろよい","まっすぐでないめ","はじょうのくち"],
+ "😲": ["おどろいたかお","おどろき","びっくり","かお","しょっく","きょうがく"],
+ "🫨": ["ふるえるかお","じしん","かお","ふるえ","しょうげき","しんどう"],
+ "🤯": ["ばくはつしたあたま","かお","しょっく","ばくはつ","きょうき","びっくり"],
+ "🫠": ["ほろりとしたかお","きえる","ようかいする","えきたい","とける"],
+ "🤐": ["おくちちゃっく","かお","くち","ちゃっく"],
+ "😷": ["ますくをしたかお","かぜ","いしゃ","かお","ますく","くすり","びょうき"],
+ "🤕": ["けが","ほうたい","かお","きず"],
+ "🤒": ["おんどけいをくわえたかお","かお","びょうき","かぜ","たいおんけい"],
+ "🤮": ["はきそうなかお","びょうき","おうと","かぜ","はく"],
+ "🤢": ["はきそうなかお","かお","はきけ","おうと"],
+ "🤧": ["くしゃみをするかお","かお","くしゃみ","はくしょん"],
+ "🥵": ["ほてったかお","かお","ねつっぽい","ねっしゃびょう","ほてった","あからがお","あせをかいた"],
+ "🥶": ["あおざめたかお","かお","ぞっとする","こごえる","とうしょう","つらら"],
+ "😶‍🌫️": ["くもでおおわれたかお","かお","おっちょこちょい","ひげんじつてき","ゆめ","もや","くもでおおわれたあたま"],
+ "😴": ["ねがお","かお","ねる","すいみん","すやすや"],
+ "💤": ["すいみん","まんが","ねる","すやすや"],
+ "😈": ["つのつきえがお","かお","おとぎばなし","ふぁんたじー","つの","えがお"],
+ "👿": ["しょうあくま","おに","あくま","かお","おとぎばなし","ふぁんたじー"],
+ "👹": ["おに","ようかい","かお","むかしばなし","ふぁんたじー","にっぽん","もんすたー"],
+ "👺": ["てんぐ","ようかい","かお","むかしばなし","ふぁんたじー","にっぽん","もんすたー"],
+ "💩": ["うんち","まんが","ふん","かお","もんすたー"],
+ "👻": ["おばけ","ようかい","かお","おとぎばなし","ふぁんたじー","ゆうれい","もんすたー","はろうぃーん"],
+ "💀": ["どくろ","からだ","し","かお","おとぎばなし","もんすたー","がいこつ","はろうぃーん"],
+ "☠": ["どくろまーく","からだ","こうさしたほね","し","かお","もんすたー","がいこつ","はろうぃーん"],
+ "👽": ["うちゅうじん","かいじゅう","いせいじん","かお","おとぎばなし","ふぁんたじー","もんすたー","うちゅう","UFO"],
+ "🤖": ["ろぼっとのかお","かお","もんすたー","ろぼっと"],
+ "🎃": ["じゃっく・お・らんたん","いべんと","おいわい","えんため","はろうぃん","じゃっくおらんたん","らんたん","かぼちゃ"],
+ "😺": ["くちをあけてわらうねこ","ねこ","かお","くち","あける","えがお"],
+ "😸": ["にやにやわらうねこ","ねこ","め","かお","にやにや","えがお"],
+ "😹": ["うれしなきしたねこのかお","ねこ","かお","うれしい","なみだ"],
+ "😻": ["はーとのめをしたねこのえがお","ねこ","め","かお","はーと","あい","えがお"],
+ "😼": ["にやりとわらうねこのかお","ねこ","かお","ひにく","えがお","にやり"],
+ "😽": ["めをとじてきすをするねこ","ねこ","め","かお","きす"],
+ "🙀": ["つかれたねこのかお","ねこ","かお","びっくり","おどろく","うんざり"],
+ "😿": ["ないたねこのかお","ねこ","なく","かお","かなしい","なみだ"],
+ "😾": ["おこったねこのかお","ねこ","かお","おこる","ふくれっつら"],
+ "🫶": ["はーとぽーず","あい"],
+ "👐": ["ひらいたて","からだ","て","ひろげる"],
+ "🤲": ["うえにむけたりょうてのひら","からだ","いのり","かっぷのようにまるめたて"],
+ "🙌": ["りょうてをあげる","からだ","おいわい","じぇすちゃー","て","ばんざい","あげる"],
+ "👏": ["はくしゅ","からだ","てをたたく","て"],
+ "🙏": ["にぎったて","たのむ","からだ","おじぎ","てをあわせる","じぇすちゃー","て","おねがい","いのる","ありがとう","かんしゃ"],
+ "🤝": ["あくしゅ","ごうい","て","しゅをむすぶ","かいぎ"],
+ "👍": ["いいね","からだ","うえ","て","ゆび","さむずあっぷ","+1"],
+ "👎": ["だめ","からだ","した","て","ゆび","さむずだうん","-1"],
+ "👊": ["にぎりこぶし","からだ","にぎる","こぶし","ぐー","て","ぱんち","せっきん"],
+ "✊": ["こぶし","からだ","にぎる","ぐー","て","ぱんち"],
+ "🤛": ["ひだりむきのこぶし","からだ","こぶし","ひだりむき"],
+ "🤜": ["みぎむきのこぶし","からだ","こぶし","みぎむき"],
+ "🤞": ["こうささせたゆび","からだ","こうさ","ゆび","て","こううん"],
+ "✌": ["Vさいん","からだ","て","V","ぶい","かつ","しょうり","ぴーす"],
+ "🫰": ["ひとさしゆびとおやゆびをこうさしたて","たかい","はーと","あい","おかね","すなっぷ"],
+ "🤘": ["こるな","からだ","ゆび","て","つの","さいこう"],
+ "🤟": ["あいしてるのじぇすちゃー","からだ","あいしてる","すき","て"],
+ "👌": ["OKさいん","からだ","て","OK"],
+ "🤌": ["つまんでいるゆび","ゆび","てぶり","じんもん","つまむ","ひにく"],
+ "🤏": ["つまんでいるて","からだ","て","ちいさい","こがた","ちっちゃい"],
+ "👈": ["ひだりゆびさし","てのこう","からだ","ゆび","て","ひとさしゆび","ゆびさす"],
+ "🫳": ["てのひらをしたにしたて","しりぞける","おとす","しっし"],
+ "🫴": ["てのひらをうえにしたて","てまねき","ほかく","くる","もうしで"],
+ "👉": ["ゆびさし","てのこう","からだ","ゆび","て","ひとさしゆび","ゆびさす"],
+ "👆": ["ゆびさし","てのこう","からだ","ゆび","て","ひとさしゆび","ゆびさす","うえ"],
+ "👇": ["ゆびさし","てのこう","からだ","した","ゆび","て","ひとさしゆび","ゆびさす"],
+ "☝": ["ゆびさし","からだ","ゆび","て","ひとさしゆび","ゆびさす","うえ"],
+ "✋": ["きょしゅ","からだ","て"],
+ "🤚": ["てのこう","からだ","あげる"],
+ "🖐": ["ひろげたてのひら","からだ","ゆび","て","ひろげる"],
+ "🖖": ["ちょうじゅとはんえいを","からだ","ゆび","て","すぽっく","ばるかん"],
+ "👋": ["ばいばい","からだ","て","ふる","やっほー","こんにちは"],
+ "🤙": ["でんわのかたちのて","からだ","でんわ","て"],
+ "🫲": ["ひだりて","て","ひだり"],
+ "🫱": ["みぎて","て","みぎ"],
+ "🫷": ["ひだりをおしているて","じたい","はいたっち","ひだりほうこう","おしつける","ことわる","ていし","まつ"],
+ "🫸": ["みぎをおしているて","じたい","はいたっち","おしつける","ことわる","みぎほうこう","ていし","まつ"],
+ "💪": ["まげたじょうわんにとうきん","ちからこぶ","からだ","まんが","うんどう","きんにく","ちから","まっする","まっちょ"],
+ "🦾": ["めかにかるあーむ","あくせしびりてぃ","ぎしゅ","じんこうそうぐ","からだ"],
+ "🖕": ["なかゆびをたてたて","からだ","ゆび","て","なかゆび"],
+ "🫵": ["みているひとをさしているひとさしゆび","さす","あなた","ゆび"],
+ "✍": ["かいているて","からだ","て","かく"],
+ "🤳": ["じどり","かめら","けいたい","うで"],
+ "💅": ["まにきゅあ","からだ","けあ","けしょうひん","こすめ","つめ","ねいる"],
+ "🦵": ["あし","からだ","きっく","てあし"],
+ "🦿": ["きかいのあし","あくせしびりてぃ","ぎそく","じんこうそうぐ","からだ"],
+ "🦶": ["あし","からだ","きっく","ふみつける"],
+ "👄": ["くち","からだ","くちびる"],
+ "🫦": ["かんでいるくちびる","しんぱい","こわい","うわき","しんけいしつ","ふゆかい","ふあん"],
+ "🦷": ["は","からだ","はいしゃ"],
+ "👅": ["した","からだ"],
+ "👂": ["みみ","からだ","はな"],
+ "🦻": ["ほちょうきをつけているみみ","あくせしびりてぃ","ほちょうき","きく","からだ","みみ"],
+ "👃": ["はな","からだ"],
+ "👁": ["め","からだ"],
+ "👀": ["め","からだ","かお"],
+ "🧠": ["のう","からだ","ぞうき","ちてき","かしこい"],
+ "🫀": ["かいぼうがくてきなしんぞう","かいぼうがく","しんぞうがく","しんぞう","ぞうき","みゃく"],
+ "🫁": ["はい","いき","こき","きゅうにゅう","ぞうき","こきゅう"],
+ "🦴": ["ほね","からだ","こっかく"],
+ "👤": ["じょうはんしんのしるえっと","じょうはんしん","しるえっと"],
+ "👥": ["じょうはんしんのしるえっと","じょうはんしん","しるえっと"],
+ "🗣": ["しゃべるあたまのしるえっと","かお","あたま","しるえっと","しゃべる","はなす"],
+ "🫂": ["はぐしているひとたち","さようなら","こんにちは","はぐ","ありがとう"],
+ "👶": ["あかちゃん"],
+ "👧": ["おんなのこ","しょうじょ","しょじょ","おとめざ","せいざ","こども"],
+ "🧒": ["こども","ひと","しょうねん","しょうじょ"],
+ "👦": ["おとこのこ","しょうねん","こども"],
+ "👩": ["じょせい","おんな"],
+ "🧑": ["せいじんむけ","ひと","おとな","だんせい","じょせい","おんな","おとこ"],
+ "👨": ["だんせい","くちひげ","おとこ"],
+ "👩‍🦱": ["じょせい","まきげ","かみ","おんな"],
+ "🧑‍🦱": ["ひと","まきげ","かみ"],
+ "👨‍🦱": ["だんせい","まきげ","かみ","おとこ"],
+ "👩‍🦰": ["じょせい","あかげ","あか","かみ","おんな"],
+ "🧑‍🦰": ["ひと","あかげ","あか","かみ"],
+ "👨‍🦰": ["だんせい","あかげ","あか","かみ","おとこ"],
+ "👱‍♀️": ["じょせい","きんぱつ","ぶろんど","かみ","おんな"],
+ "👱": ["ひと","きんぱつ","ぶろんど","かみ"],
+ "👱‍♂️": ["だんせい","きんぱつ","ぶろんど","かみ","おとこ"],
+ "👩‍🦳": ["じょせい","はくはつ","しろ","かみ","おんな"],
+ "🧑‍🦳": ["ひと","はくはつ","しろ","かみ"],
+ "👨‍🦳": ["だんせい","はくはつ","しろ","かみ","おとこ"],
+ "👩‍🦲": ["じょせい","はげ","おんな"],
+ "🧑‍🦲": ["ひと","はげ"],
+ "👨‍🦲": ["だんせい","はげ","おとこ"],
+ "🧔‍♀️": ["ひげのあるじょせい","あごひげ","ひげをはやした","じょせい","おんな"],
+ "🧔": ["あごひげのあるひと","あごひげ","ひげをはやした"],
+ "🧔‍♂️": ["ひげのあるだんせい","あごひげ","ひげをはやした","だんせい","おとこ"],
+ "👵": ["おばあさん","おばあちゃん","ろうじん","じょせい","おんな"],
+ "🧓": ["こうれいしゃ","ひと","だんせい","じょせい","おんな","おとこ"],
+ "👴": ["おじいさん","おじいちゃん","ろうじん","おとこ","だんせい"],
+ "👲": ["すかるきゃっぷをかぶっているひと","ちゅうごくぼう","ぼうし"],
+ "👳‍♀️": ["たーばんをまいているじょせい","たーばん","じょせい","おんな"],
+ "👳": ["たーばんをまいているひと","たーばん"],
+ "👳‍♂️": ["たーばんをまいているだんせい","たーばん","おとこ","だんせい"],
+ "🧕": ["へっどすかーふをかぶったじょせい","へっどすかーふ","ひじゃぶ","まんてぃら","てぃちぇる","ばんだな","あたまのすかーふ","じょせい","おんな"],
+ "👮‍♀️": ["じょせいけいさつかん","けいさつかん","けいかん","けいさつ","じょせい","おんな"],
+ "👮": ["けいさつかん","けいかん","けいさつ"],
+ "👮‍♂️": ["だんせいけいさつかん","けいさつかん","けいかん","けいさつ","おとこ","だんせい"],
+ "👩‍🚒": ["じょせいしょうぼうし","ひ","かじ","しょうぼう","しょうぼうし","じょせい","おんな"],
+ "🧑‍🚒": ["しょうぼうし","かじ"],
+ "👨‍🚒": ["だんせいしょうぼうし","ひ","かじ","しょうぼう","しょうぼうし","おとこ","だんせい"],
+ "👷‍♀️": ["じょせいのけんせつさぎょういん","こうじ","けんせつ","さぎょういん","じょせい","おんな"],
+ "👷": ["けんせつさぎょういん","こうじ","けんせつ","さぎょういん"],
+ "👷‍♂️": ["だんせいのけんせつさぎょういん","けんせつ","さぎょういん","だんせい","おとこ"],
+ "👩‍🏭": ["だんせいのこうじょうさぎょういん","こうじょう","こうぎょう","さぎょういん","じょせい","おんな"],
+ "🧑‍🏭": ["こうじょうさぎょういん","こうじょう","こうぎょう","ようせつ"],
+ "👨‍🏭": ["だんせいのこうじょうさぎょういん","こうじょう","こうぎょう","さぎょういん","おとこ","だんせい"],
+ "👩‍🔧": ["じょせいせいびし","しょくにん","はいかんこう","でんきぎし","しゅうりにん","じょせい","おんな"],
+ "🧑‍🔧": ["せいびし","しょくにん","はいかんこう","でんきぎし","しゅうりじん"],
+ "👨‍🔧": ["だんせいせいびし","しょくにん","はいかんこう","でんきぎし","しゅうりじん","おとこ","だんせい"],
+ "👩‍🌾": ["じょせいののうぎょうじゅうじしゃ","のうじょうろうどうしゃ","ぼくじょうぬし","にわし","のうか","じょせい","おんな"],
+ "🧑‍🌾": ["のうぎょうじゅうじしゃ","のうじょうろうどうしゃ","ぼくじょうぬし","にわし","のうか"],
+ "👨‍🌾": ["だんせいののうぎょうじゅうじしゃ","のうじょうろうどうしゃ","ぼくじょうぬし","にわし","のうか","おとこ","だんせい"],
+ "👩‍🍳": ["じょせいのりょうりにん","しょくひん","さーびす","しぇふ","こっく","りょうりにん","りょうり","じょせい","おんな"],
+ "🧑‍🍳": ["りょうりにん","しょくひん","さーびす","しぇふ","こっく","りょうり"],
+ "👨‍🍳": ["だんせいのりょうりじん","しょくひん","さーびす","しぇふ","こっく","りょうりにん","りょうり","おとこ","だんせい"],
+ "👩‍🎤": ["だんせいしんがー","おんがく","みゅーじしゃん","ろっく","ろっかー","ろっくすたー","げいのうじん","じょせい","おんな"],
+ "🧑‍🎤": ["かしゅ","おんがく","みゅーじしゃん","ろっく","ろっかー","ろっくすたー","げいのうじん"],
+ "👨‍🎤": ["だんせいしんがー","おんがく","みゅーじしゃん","ろっく","ろっかー","ろっくすたー","げいのうじん","おとこ","だんせい"],
+ "👩‍🎨": ["じょせいあーてぃすと","げいじゅつ","あーと","げいじゅつか","あーてぃすと","かいが","がか","じょせい","おんな"],
+ "🧑‍🎨": ["あーてぃすと","げいじゅつ","あーと","げいじゅつか","かいが","がか"],
+ "👨‍🎨": ["だんせいあーてぃすと","げいじゅつ","あーと","げいじゅつか","あーてぃすと","かいが","がか","おとこ","だんせい"],
+ "👩‍🏫": ["じょせいのきょうし","きょういく","せんせい","きょうじゅ","きょうし","こうし","じょせい","おんな"],
+ "🧑‍🏫": ["きょうし","きょういく","せんせい","きょうじゅ","こうし"],
+ "👨‍🏫": ["だんせいのきょうし","きょういく","せんせい","きょうじゅ","きょうし","こうし","おとこ","だんせい"],
+ "👩‍🎓": ["じょしせいと","がくせい","そつぎょうせい","きょういく","がっこう","じょせい","おんな"],
+ "🧑‍🎓": ["せいと","がくせい","そつぎょうせい","きょういく","がっこう"],
+ "👨‍🎓": ["だんしせいと","がくせい","そつぎょうせい","きょういく","がっこう","おとこ","だんせい"],
+ "👩‍💼": ["だんせいかいしゃいん","おふぃす","かいけいし","ぎんこうか","かんりしょく","こもん","じむいん","あなりすと","じょせい","おんな"],
+ "🧑‍💼": ["かいしゃいん","おふぃす","かいけいし","ぎんこうか","かんりしょく","こもん","じむいん","あなりすと"],
+ "👨‍💼": ["だんせいかいしゃいん","おふぃす","かいけいし","ぎんこうか","かんりしょく","こもん","じむいん","あなりすと","おとこ","だんせい"],
+ "👩‍💻": ["じょせいぎじゅつしゃ","てくのろじー","そふとうぇあ","えんじにあ","ぷろぐらまー","らっぷとっぷ","のーとぱそこん","じょせい","おんな"],
+ "🧑‍💻": ["ぎじゅつしゃ","てくのろじー","そふとうぇあ","えんじにあ","ぷろぐらまー","らっぷとっぷ","のーとぱそこん"],
+ "👨‍💻": ["だんせいぎじゅつしゃ","てくのろじー","そふとうぇあ","えんじにあ","ぷろぐらまー","らっぷとっぷ","のーとぱそこん","おとこ","だんせい"],
+ "👩‍🔬": ["じょせいかがくしゃ","かがくしゃ","ぎじゅつしゃ","すうがくしゃ","ぶつりがくしゃ","せいぶつがくしゃ","けんさぎし","じょせい","おんな"],
+ "🧑‍🔬": ["かがくしゃ","ぎじゅつしゃ","すうがくしゃ","ぶつりがくしゃ","せいぶつがくしゃ","けんさぎし"],
+ "👨‍🔬": ["だんせいかがくしゃ","かがくしゃ","ぎじゅつしゃ","すうがくしゃ","ぶつりがくしゃ","せいぶつがくしゃ","けんさぎし","おとこ","だんせい"],
+ "👩‍🚀": ["じょせいうちゅうひこうし","うちゅう","ほし","つき","わくせい","じょせい","おんな"],
+ "🧑‍🚀": ["うちゅうひこうし","うちゅう","ほし","つき","わくせい"],
+ "👨‍🚀": ["だんせいうちゅうひこうし","うちゅう","ほし","つき","わくせい","おとこ","だんせい"],
+ "👩‍⚕️": ["じょせいいりょうかんけいしゃ","いし","ないかい","いがくはかせ","かんごし","しかい","いりょうせんもんか","りょうほうし","じょせい","おんな"],
+ "🧑‍⚕️": ["いりょうかんけいしゃ","いし","ないかい","いがくはかせ","かんごし","しかい","いりょうせんもんか","りょうほうし"],
+ "👨‍⚕️": ["だんせいいりょうかんけいしゃ","いし","ないかい","いがくはかせ","かんごし","しかい","いりょうせんもんか","りょうほうし","おとこ","だんせい"],
+ "👩‍⚖️": ["じょせいさいばんかん","さいばんかん","ほうてい","さいばんしょ","ほうりつ","じょせい","おんな"],
+ "🧑‍⚖️": ["さいばんかん","ほうてい","さいばんしょ","ほうりつ"],
+ "👨‍⚖️": ["だんせいさいばんかん","さいばんかん","ほうてい","さいばんしょ","ほうりつ","おとこ","だんせい"],
+ "👩‍✈️": ["じょせいぱいろっと","ぱいろっと","ひこうき","そうじゅうし","こうくう","じょせい","おんな"],
+ "🧑‍✈️": ["ぱいろっと","ひこうき","そうじゅうし","こうくう"],
+ "👨‍✈️": ["だんせいぱいろっと","ぱいろっと","ひこうき","そうじゅうし","こうくう","おとこ","だんせい"],
+ "💂‍♀️": ["じょせいけいびいん","けいびいん","けいび","じょせい","おんな"],
+ "💂": ["けいびいん","けいび"],
+ "💂‍♂️": ["だんせいけいびいん","けいびいん","けいび","おとこ","だんせい"],
+ "🥷": ["にんじゃ","せんし","かくされた","すてるす"],
+ "🕵️‍♀️": ["じょせいのたんてい","たんてい","けいじ","すぱい","じょせい","おんな"],
+ "🕵": ["たんてい","けいじ","すぱい"],
+ "🕵️‍♂️": ["だんせいのたんてい","たんてい","けいじ","すぱい","おとこ","だんせい"],
+ "🤶": ["みせす・くろーす","いべんと","おいわい","くりすます","はは","さんた","くろーす","じょせい","おんな"],
+ "🧑‍🎄": ["みくすくろーす","あくてぃびてぃ","おいわい","くりすます","さんた","くろーす"],
+ "🎅": ["さんたくろーす","いべんと","おいわい","くりすます","ちち","さんた","くろーす","おとこ","だんせい"],
+ "👼": ["てんしのあかちゃん","てんし","あかちゃん","かお","おとぎばなし","ふぁんたじー"],
+ "👸": ["おひめさま","おとぎばなし","ふぁんたじー","じょおう","じょせい","おんな"],
+ "🫅": ["おうかんをかぶったひと","おとぎばなし","ふぁんたじー","こくおう","きぞく","おう","おうぞく"],
+ "🤴": ["おうじさま","おとぎばなし","ふぁんたじー","おう","おとこ","だんせい"],
+ "👰": ["べーるをつけたじょせい","はなよめ","べーる","けっこんしき","じょせい","おんな"],
+ "👰‍♀️": ["べーるをつけたひと","はなよめ","べーる","けっこんしき"],
+ "👰‍♂️": ["べーるをつけただんせい","はなよめ","べーる","うぇでぃんぐ","だんせい","おとこ"],
+ "🤵‍♀️": ["たきしーどのじょせい","たきしーど","うぇでぃんぐ","じょせい","おんな"],
+ "🤵": ["たきしーどをきるひと","はなむこ","たきしーど","うぇでぃんぐ"],
+ "🤵‍♂️": ["たきしーどのだんせい","はなむこ","たきしーど","うぇでぃんぐ","だんせい","おとこ"],
+ "🩷": ["ぴんくのはーと","かわいい","はーと","すき","あい","ぴんく"],
+ "🩵": ["らいとぶるーのはーと","しあん","はーと","らいとぶるー","こがも"],
+ "🩶": ["ぐれーのはーと","ぐれー","はーと","しるばー","すれーと"],
+ "🕴️‍♀️": ["ちゅうにういたすーつのじょせい","びじねす","すーつ","じょせい","おんな"],
+ "🕴": ["ちゅうにういたすーつのひと","びじねす","すーつ"],
+ "🕴️‍♂️": ["ちゅうにういたすーつのだんせい","びじねす","すーつ","おとこ","だんせい"],
+ "🦸‍♀️": ["じょせいのすーぱーひーろー","くうそう","ぜん","ひろいん","ちょうたいこく","じょせい","おんな"],
+ "🦸": ["すーぱーひーろー","くうそう","ぜん","ひーろー","ひろいん","ちょうたいこく"],
+ "🦸‍♂️": ["だんせいのすーぱーひーろー","くうそう","ぜん","ひーろー","ちょうたいこく","だんせい","おとこ"],
+ "🦹‍♀️": ["じょせいのあくとう","くうそう","あく","はんざい","あくじ","ちょうたいこく","あくやく","じょせい","おんな"],
+ "🦹": ["あくとう","くうそう","あく","はんざい","あくじ","ちょうたいこく","あくやく"],
+ "🦹‍♂️": ["だんせいのあくとう","くうそう","あく","はんざい","あくじ","ちょうたいこく","あくやく","だんせい","おとこ"],
+ "🧙‍♀️": ["じょせいのまほうつかい","くうそう","まじょ","おんなのまほうつかい","じょせい","おんな"],
+ "🧙": ["まほうつかい","くうそう","まじゅつし","おとこのまほうつかい"],
+ "🧙‍♂️": ["だんせいのまほうつかい","くうそう","まじゅつし","おとこのまほうつかい","だんせい","おとこ"],
+ "🧝‍♀️": ["じょせいのこども","くうそう","こども","さきのとがったみみ","じょせい","おんな"],
+ "🧝": ["こども","くうそう","さきのとがったみみ"],
+ "🧝‍♂️": ["だんせいのこども","くうそう","こども","さきのとがったみみ","だんせい","おとこ"],
+ "🧚‍♀️": ["じょせいのようせい","くうそう","てぃたーにあ","うぃんぐす","じょせい","おんな"],
+ "🧚": ["ようせい","くうそう","てぃたーにあ","うぃんぐす"],
+ "🧚‍♂️": ["だんせいのようせい","くうそう","おべろん","しょうようせい","だんせい","おとこ"],
+ "🧞‍♀️": ["じょせいのせいれい","くうそう","せいれい","じょせい","おんな"],
+ "🧞": ["せいれい","くうそう"],
+ "🧞‍♂️": ["だんせいのせいれい","くうそう","せいれい","だんせい","おとこ"],
+ "🧜‍♀️": ["じょせいのにんぎょ","くうそう","じょせい","おんな"],
+ "🧜": ["にんぎょ","くうそう"],
+ "🧜‍♂️": ["だんせいのにんぎょ","くうそう","にんぎょ","だんせい","おとこ"],
+ "🧌": ["つり","おとぎばなし","ふぁんたじ","もんすたー"],
+ "🧛‍♀️": ["じょせいのきゅうけつき","くうそう","あんでっど","じょせい","おんな"],
+ "🧛": ["きゅうけつき","くうそう","どらきゅら","あんでっど"],
+ "🧛‍♂️": ["だんせいのきゅうけつき","くうそう","どらきゅら","あんでっど","だんせい","おとこ"],
+ "🧟‍♀️": ["じょせいのぞんび","くうそう","あんでっど","じょせい","おんな"],
+ "🧟": ["ぞんび","くうそう","あんでっど"],
+ "🧟‍♂️": ["だんせいのぞんび","くうそう","あんでっど","だんせい","おとこ"],
+ "🙇‍♀️": ["ふかくおじぎするじょせい","しゃざい","おじぎ","じぇすちゃー","ごめんなさい","じょせい","おんな"],
+ "🙇": ["ふかくおじぎしたひと","しゃざい","おじぎ","じぇすちゃー","ごめんなさい"],
+ "🙇‍♂️": ["ふかくおじぎするだんせい","しゃざい","おじぎ","じぇすちゃー","ごめんなさい","おとこ","だんせい"],
+ "💁‍♀️": ["あんないするじょせい","て","たすけ","じょうほう","ずうずうしい","じょせい","おんな"],
+ "💁": ["あんないするひと","て","たすけ","じょうほう","ずうずうしい","じょせい","おんな"],
+ "💁‍♂️": ["あんないするだんせい","て","たすけ","じょうほう","ずうずうしい","おとこ","だんせい"],
+ "🙅‍♀️": ["NGさいんのじょせい","きんじる","じぇすちゃー","て","だめ","きんし","じょせい","おんな"],
+ "🙅": ["NGさいんのひと","きんじる","じぇすちゃー","て","だめ","きんし"],
+ "🙅‍♂️": ["NGさいんのだんせい","きんじる","じぇすちゃー","て","だめ","きんし","おとこ","だんせい"],
+ "🙆‍♀️": ["OKさいんのじょせい","じぇすちゃー","て","ok","じょせい","おんな"],
+ "🙆": ["OKさいんのひと","じぇすちゃー","て","OK"],
+ "🙆‍♂️": ["OKさいんのだんせい","じぇすちゃー","て","ok","おとこ","だんせい"],
+ "🤷‍♀️": ["かたをすくめるじょせい","うたがい","むち","むかんしん","かたをすくめる","じょせい","おんな"],
+ "🤷": ["かたをすくめるひと","うたがい","むち","むかんしん","かたをすくめる"],
+ "🤷‍♂️": ["かたをすくめるだんせい","うたがい","むち","むかんしん","かたをすくめる","おとこ","だんせい"],
+ "🙋‍♀️": ["かたてをあげてよろこぶじょせい","じぇすちゃー","て","しあわせ","あげる","じょせい","おんな"],
+ "🙋": ["かたてをあげてよろこぶひと","じぇすちゃー","て","しあわせ","あげる"],
+ "🙋‍♂️": ["かたてをあげてよろこぶだんせい","じぇすちゃー","て","しあわせ","あげる","おとこ","だんせい"],
+ "🤦‍♀️": ["かおをおさえるじょせい","ふしん","ふんがい","かお","てのひら","じょせい","おんな"],
+ "🤦": ["てのひらをかおにあてるひと","ふしん","ふんがい","かお","てのひら"],
+ "🤦‍♂️": ["がおをおさえるだんせい","ふしん","ふんがい","かお","てのひら","おとこ","だんせい"],
+ "🧏‍♀️": ["みみがふじゆうなじょせい","あくせしびりてぃ","みみがふじゆう","じょせい","おんな"],
+ "🧏": ["みみがふじゆうなひと","あくせしびりてぃ","みみがふじゆう"],
+ "🧏‍♂️": ["みみがふじゆうなだんせい","あくせしびりてぃ","みみがふじゆう","だんせい","おとこ"],
+ "🙎‍♀️": ["ふくれっつらのじょせい","じぇすちゃー","ふくれっつら","じょせい","おんな"],
+ "🙎": ["おこったかおのひと","じぇすちゃー","ふくれっつら"],
+ "🙎‍♂️": ["ふくれっつらのだんせい","じぇすちゃー","ふくれっつら","おとこ","だんせい"],
+ "🙍‍♀️": ["がおをしかめたじょせい","しかめめん","じぇすちゃー","かなしい","じょせい","おんな"],
+ "🙍": ["ふまんなかおのひと","しかめめん","じぇすちゃー","かなしい"],
+ "🙍‍♂️": ["がおをしかめただんせい","しかめめん","じぇすちゃー","かなしい","だんせい","おとこ"],
+ "💇‍♀️": ["かみをきられているじょせい","りはつし","びようし","びよう","さんぱつ","へあかっと","びよういん","じょせい","おんな"],
+ "💇": ["かみをきられているひと","りはつし","びようし","びよう","さんぱつ","へあかっと","びよういん"],
+ "💇‍♂️": ["かみをきられているだんせい","りはつし","びようし","びよう","さんぱつ","へあかっと","びよういん","おとこ","だんせい"],
+ "💆‍♀️": ["ふぇいすまっさーじをうけるじょせい","まっさーじ","さろん","じょせい","おんな"],
+ "💆": ["ふぇいすまっさーじをうけるひと","まっさーじ","さろん"],
+ "💆‍♂️": ["ふぇいすまっさーじをうけるだんせい","まっさーじ","さろん","おとこ","だんせい"],
+ "🤰": ["にんぷ","にんしん","あかちゃん","じょせい","おんな","はら","ふくれた","ふっくらした"],
+ "🫄": ["にんしんしたひと","はら","ふくれた","ふっくらした","にんしん","あかちゃん"],
+ "🫃": ["にんしんしているだんせい","はら","ふくれた","ふっくらした","にんしん","あかちゃん","だんせい","おとこ"],
+ "🤱": ["ぼにゅう","むね","あかちゃん","あかんぼう","にゅうじ","ようじ","はは","こども","ほいく","みるく","じょせい","おんな"],
+ "👩‍🍼": ["あかちゃんにごはんをあげるじょせい","あかちゃん","にゅうじ","こども","じゅにゅう","みるく","ぼとる","じょせい","おんな"],
+ "🧑‍🍼": ["あかちゃんにごはんをあげるひと","あかちゃん","にゅうじ","こども","じゅにゅう","みるく","ぼとる"],
+ "👨‍🍼": ["あかちゃんにごはんをあげるだんせい","あかちゃん","にゅうじ","こども","じゅにゅう","みるく","ぼとる","だんせい","おとこ"],
+ "🧎‍♀️": ["ひざたちしているじょせい","ひざ","ひざたち","じょせい","おんな"],
+ "🧎": ["ひざたちしているひと","ひざ","ひざたち"],
+ "🧎‍♂️": ["ひざたちしているだんせい","ひざ","ひざたち","だんせい","おとこ"],
+ "🧍‍♀️": ["たっているじょせい","たつ","すたんでぃんぐ","じょせい","おんな"],
+ "🧍": ["たっているひと","たつ","すたんでぃんぐ"],
+ "🧍‍♂️": ["たっているだんせい","たつ","すたんでぃんぐ","だんせい","おとこ"],
+ "🚶‍♀️": ["あるくじょせい","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ","じょせい","おんな"],
+ "🚶": ["あるくひと","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ"],
+ "🚶‍♂️": ["あるくだんせい","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ","おとこ","だんせい"],
+ "👩‍🦯": ["しろつえをもったじょせい","あくせしびりてぃ","めがふじゆう","じょせい","おんな"],
+ "🧑‍🦯": ["しろつえをもったひと","あくせしびりてぃ","めがふじゆう"],
+ "👨‍🦯": ["しろつえをもっただんせい","あくせしびりてぃ","めがふじゆう","だんせい","おとこ"],
+ "🏃‍♀️": ["はしるじょせい","まらそん","らんなー","らんにんぐ","じょせい","おんな"],
+ "🏃": ["はしるひと","まらそん","らんなー","らんにんぐ"],
+ "🏃‍♂️": ["はしるだんせい","まらそん","らんなー","らんにんぐ","おとこ","だんせい"],
+ "👩‍🦼": ["でんどうくるまいすにすわっているじょせい","あくせしびりてぃ","くるまいす","じょせい","おんな"],
+ "🧑‍🦼": ["でんどうくるまいすにすわっているひと","あくせしびりてぃ","くるまいす"],
+ "👨‍🦼": ["でんどうくるまいすにすわっているだんせい","あくせしびりてぃ","くるまいす","だんせい","おとこ"],
+ "👩‍🦽": ["しゅどうくるまいすにすわっているじょせい","あくせしびりてぃ","くるまいす","じょせい","おんな"],
+ "🧑‍🦽": ["しゅどうくるまいすにすわっているひと","あくせしびりてぃ","くるまいす"],
+ "👨‍🦽": ["しゅどうくるまいすにすわっているだんせい","あくせしびりてぃ","くるまいす","だんせい","おとこ"],
+ "💃": ["じょせいだんさー","だんす","おどる","だんさー","じょせい","おんな"],
+ "🕺": ["だんせいだんさー","だんす","おどる","だんさー","おとこ","だんせい"],
+ "👯‍♀️": ["ばにーがーる","うさぎみみ","だんさー","じょせい","おんな"],
+ "👯": ["うさぎみみのひと","うさぎみみ","だんさー"],
+ "👯‍♂️": ["うさぎみみのだんせい","うさぎみみ","だんさー","おとこ","だんせい"],
+ "👫": ["しゅをつないだだんじょ","かっぷる","て","つなぐ","おとこ","おんな","だんじょ"],
+ "👭": ["しゅをつないだじょせい","かっぷる","て","つなぐ","じょせい","おんな","ぷらいど","lgbt","れずびあん"],
+ "👬": ["しゅをつないだだんせい","かっぷる","て","つなぐ","だんせい","おとこ","ぷらいど","lgbt","げい"],
+ "🧑‍🤝‍🧑": ["しゅをつないだひとたち","かっぷる","て","にぎる"],
+ "👩‍❤️‍👨": ["はーとのかっぷる (じょせい、だんせい)","かっぷる","はーと","あい","れんあい","おとこ","おんな","だんじょ"],
+ "👩‍❤️‍👩": ["はーとのかっぷる (じょせい、じょせい)","かっぷる","はーと","あい","れんあい","じょせい","おんな","ぷらいど","lgbt","れずびあん"],
+ "💑": ["はーとのかっぷる","かっぷる","はーと","あい","れんあい","おとこ","おんな","だんじょ"],
+ "👨‍❤️‍👨": ["はーとのかっぷる (だんせい、だんせい)","かっぷる","はーと","あい","れんあい","だんせい","おとこ","ぷらいど","lgbt","げい"],
+ "👩‍❤️‍💋‍👨": ["きす (じょせい、だんせい)","かっぷる","きす","はーと","あい","れんあい","おとこ","おんな","だんじょ"],
+ "👩‍❤️‍💋‍👩": ["きす (じょせい、じょせい)","かっぷる","きす","はーと","あい","れんあい","じょせい","おんな","ぷらいど","lgbt","げい"],
+ "💏": ["きす","かっぷる","はーと","あい","れんあい","おとこ","おんな","だんじょ"],
+ "👨‍❤️‍💋‍👨": ["きす (だんせい、だんせい)","かっぷる","きす","はーと","あい","れんあい","だんせい","おとこ","ぷらいど","lgbt","げい"],
+ "👪": ["かぞく","ちちおや","ははおや","おとこ","おんな","だんじょ","おとこのこ","こども"],
+ "👨‍👩‍👧": ["かぞく (だんせい、じょせい、おんなのこ)","ちちおや","ははおや","おとこ","おんな","だんじょ","おんなのこ","こども"],
+ "👨‍👩‍👧‍👦": ["かぞく (だんせい、じょせい、おんなのこ、おとこのこ)","ちちおや","ははおや","おとこ","おんな","だんじょ","おとこのこ","おんなのこ","こども"],
+ "👨‍👩‍👦‍👦": ["かぞく (だんせい、じょせい、おとこのこ、おとこのこ)","ちちおや","ははおや","おとこ","おんな","だんじょ","おとこのこ","こども"],
+ "👨‍👩‍👧‍👧": ["かぞく (だんせい、じょせい、おんなのこ、おんなのこ)","ちちおや","ははおや","おとこ","おんな","だんじょ","おんなのこ","こども"],
+ "👩‍👩‍👦": ["かぞく (じょせい、じょせい、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","こども","ぷらいど","lgbt","れずびあん"],
+ "👩‍👩‍👧": ["かぞく (じょせい、じょせい、おんなのこ)","かぞく","ははおや","じょせい","おんな","おんなのこ","こども","ぷらいど","lgbt","れずびあん"],
+ "👩‍👩‍👧‍👦": ["かぞく (じょせい、じょせい、おんなのこ、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","おんなのこ","こども","ぷらいど","lgbt","れずびあん"],
+ "👩‍👩‍👦‍👦": ["かぞく (じょせい、じょせい、おとこのこ、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","こども","ぷらいど","lgbt","れずびあん"],
+ "👩‍👩‍👧‍👧": ["かぞく (じょせい、じょせい、おんなのこ、おんなのこ)","かぞく","ははおや","じょせい","おんな","おんなのこ","こども","ぷらいど","lgbt","れずびあん"],
+ "👨‍👨‍👦": ["かぞく (だんせい、だんせい、おとこのこ)","かぞく","ちちおや","だんせい","おとこ","おとこのこ","こども","ぷらいど","lgbt","げい"],
+ "👨‍👨‍👧": ["かぞく (だんせい、だんせい、おんなのこ)","かぞく","ちちおや","だんせい","おとこ","おんなのこ","こども","ぷらいど","lgbt","げい"],
+ "👨‍👨‍👧‍👦": ["かぞく (だんせい、だんせい、おんなのこ、おとこのこ)","かぞく","ちちおや","だんせい","おとこ","おとこのこ","おんなのこ","こども","ぷらいど","lgbt","げい"],
+ "👨‍👨‍👦‍👦": ["かぞく (だんせい、だんせい、おとこのこ、おとこのこ)","かぞく","ちちおや","だんせい","おとこ","おとこのこ","こども","ぷらいど","lgbt","げい"],
+ "👨‍👨‍👧‍👧": ["かぞく (だんせい、だんせい、おんなのこ、おんなのこ)","かぞく","ちちおや","だんせい","おとこ","おんなのこ","こども","ぷらいど","lgbt","げい"],
+ "👩‍👦": ["かぞく(じょせい、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","こども"],
+ "👩‍👧": ["かぞく(じょせい、おんなのこ)","かぞく","ははおや","じょせい","おんな","おんなのこ","こども"],
+ "👩‍👧‍👦": ["かぞく(じょせい、おんなのこ、おとこのこ)","かぞく","ははおや","じょせい","おんな","だんせい","おんなのこ","おとこのこ","こども"],
+ "👩‍👦‍👦": ["かぞく(じょせい、おとこのこ、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","こども"],
+ "👩‍👧‍👧": ["かぞく(じょせい、おんなのこ、おんなのこ)","かぞく","ははおや","じょせい","おんな","おんなのこ","こども"],
+ "👨‍👦": ["かぞく(だんせい、おとこのこ)","ちちおや","おとこ","だんせい","おとこのこ","こども"],
+ "👨‍👧": ["かぞく(だんせい、おんなのこ)","ちちおや","おとこ","だんじょ","おんなのこ","こども"],
+ "👨‍👧‍👦": ["かぞく(だんせい、おんなのこ、おとこのこ)","ちちおや","おとこ","だんせい","おとこのこ","おんなのこ","こども"],
+ "👨‍👦‍👦": ["かぞく(だんせい、おとこのこ、おとこのこ)","ちちおや","おとこ","だんせい","おとこのこ","こども"],
+ "👨‍👧‍👧": ["かぞく(だんせい、おんなのこ、おんなのこ)","ちちおや","おとこ","だんじょ","おんなのこ","こども"],
+ "👚": ["れでぃーすうぇあ","ふく","じょせい","おんな"],
+ "👕": ["てぃーしゃつ","ふく","しゃつ"],
+ "🥼": ["はくい","ふく","いしゃ","じっけん","かがくしゃ"],
+ "🦺": ["あんぜんべすと","きんきゅう","あんぜん","べすと"],
+ "🧥": ["こーと","ふく","じゃけっと"],
+ "👖": ["じーんず","ふく","ぱんつ","ずぼん"],
+ "👔": ["ねくたい","ふく"],
+ "👗": ["どれす","ふく"],
+ "👘": ["きもの","ふく","わふく"],
+ "🥻": ["さりー","ふく","どれす"],
+ "🩱": ["わんぴーす","ふく","みずぎ","すいみんぐうぇあ","すいえい"],
+ "👙": ["びきに","ふく","すいえい"],
+ "🩲": ["ぶりーふ","ふく","みずぎ","すいみんぐうぇあ","すいえい","したぎ"],
+ "🩳": ["しょーつ","ふく","みずぎ","すいみんぐうぇあ","すいえい","したぎ"],
+ "💄": ["くちべに","けしょうひん","こすめ","けしょう","めいく"],
+ "💋": ["きすまーく","はーと","きす","くちびる","まーく","れんあい","ろまんす"],
+ "👣": ["あしあと","からだ","ふく"],
+ "🧦": ["くつした","ふく","そっくす","いちくみ"],
+ "🩴": ["ごむせいさんだる","びーち","さんだる","ぞうり"],
+ "👠": ["はいひーる","ふく","ひーる","くつ","じょせい","おんな"],
+ "👡": ["れでぃーすさんだる","ふく","さんだる","くつ","じょせい","おんな"],
+ "👢": ["れでぃーすぶーつ","ぶーつ","ふく","くつ","じょせい","おんな"],
+ "🥿": ["れでぃーすふらっとしゅーず","ふく","ばれえふらっと","すりっぽん","すりっぱ"],
+ "👞": ["めんずしゅーず","ふく","だんせい","おとこ","くつ"],
+ "👟": ["うんどうくつ","うんどう","ふく","しゅーず","すにーかー"],
+ "🩰": ["ばれえしゅーず","ふく","しゅーず","ばれえ","だんす"],
+ "🥾": ["はいきんぐぶーつ","ふく","ばっくぱっく","ぶーつ","きゃんぷ","はいきんぐ"],
+ "🧢": ["きゃっぷ","ふく","やきゅう","はっと","ぼうし"],
+ "👒": ["れでぃーすはっと","ふく","ぼうし","じょせい","おんな"],
+ "🎩": ["しるくはっと","あくてぃびてぃ","ふく","えんたーていんめんと","ごらく","ぼうし","とっぷす"],
+ "🎓": ["そつぎょうしきのかくぼう","あくてぃびてぃ","ぼうし","おいわい","ふく","そつぎょう","はっと"],
+ "👑": ["かんむり","ふく","おうかん","おう","じょおう"],
+ "⛑": ["しろじゅうじのへるめっと","きゅうじょ","じゅうじ","かお","ぼうし","へるめっと"],
+ "🪖": ["ぐんたいのへるめっと","ぐん","へるめっと","ぐんたい","ぐんじん","へいし"],
+ "🎒": ["らんどせる","あくてぃびてぃ","かばん","ばっぐ","がくせいかばん","がっこう"],
+ "👝": ["ぽーち","かばん","ばっぐ","ふく"],
+ "👛": ["さいふ","ふく","こいん"],
+ "👜": ["はんどばっぐ","かばん","ばっぐ","ふく"],
+ "💼": ["ぶりーふけーす"],
+ "👓": ["めがね","ふく","め","あいうぇあ"],
+ "🕶": ["さんぐらす","くらい","め","めがね"],
+ "🥽": ["ごーぐる","ふく","めのほご","すいえい","ようせつ"],
+ "🧣": ["すかーふ","ふく","くび"],
+ "🧤": ["てぶくろ","ふく","て"],
+ "💍": ["ゆびわ","だいやもんど","れんあい","ろまんす"],
+ "🌂": ["とじたかさ","ふく","あめ","かさ","てんき"],
+ "☂": ["かさ","ふく","あめ","てんき"],
+ "🐶": ["いぬのかお","けん","いぬ","かお","ぺっと"],
+ "🐱": ["ねこのかお","ねこ","かお","ぺっと"],
+ "🐭": ["ねずみのかお","かお","ねずみ"],
+ "🐹": ["はむすたーのかお","かお","はむすたー","ぺっと"],
+ "🐰": ["うさぎのかお","ばにー","かお","ぺっと","うさぎ"],
+ "🐻": ["くまのかお","くま","かお"],
+ "🧸": ["てでぃべあ","おもちゃ","びろーど","ぬいぐるみ"],
+ "🐼": ["ぱんだのかお","かお","ぱんだ","くま"],
+ "🐻‍❄️": ["しろくま","かお","ほっきょく","くま","しろ"],
+ "🐨": ["こあら","くま","ゆうぶくろるい","おーすとらりあ"],
+ "🐯": ["とらのかお","かお","とら"],
+ "🦁": ["らいおんのかお","かお","ししざ","らいおん","せいざ"],
+ "🐮": ["うしのかお","うし","かお"],
+ "🐷": ["ぶたのかお","かお","ぶた"],
+ "🐽": ["ぶたのはな","かお","はな","ぶた"],
+ "🐸": ["かえるのかお","かお","かえる"],
+ "🐵": ["さるのかお","かお","さる"],
+ "🙈": ["みざる","わるい","かお","きんじる","じぇすちゃー","さる","だめ","きんし","みる"],
+ "🙉": ["きかざる","わるい","かお","きんじる","じぇすちゃー","きく","さる","ない","なし","きんし"],
+ "🙊": ["いわざる","わるい","かお","きんじる","じぇすちゃー","さる","ない","なし","きんし","はなす"],
+ "🐒": ["さる"],
+ "🦍": ["ごりら"],
+ "🦧": ["おらんうーたん","るいじんえん"],
+ "🐔": ["にわとり"],
+ "🐧": ["ぺんぎん"],
+ "🐦": ["とり"],
+ "🐦‍⬛": ["くろいとり","とり","くろ","からす","わたりがらす","みやまがらす"],
+ "🐤": ["ひよこ","あかちゃん"],
+ "🐣": ["ひよこ","あかちゃん","ふか"],
+ "🐥": ["しょうめんをむいたひよこ","あかちゃん","ひよこ"],
+ "🐺": ["おおかみのかお","かお","おおかみ"],
+ "🦊": ["きつねのかお","かお","きつね"],
+ "🦝": ["あらいぐま","かお","こうきしんがつよい","ずるかしこい"],
+ "🐗": ["いのしし","ぶた"],
+ "🐴": ["うまのかお","かお","うま"],
+ "🦓": ["しまうま","かお"],
+ "🦒": ["きりん","かお"],
+ "🦌": ["しか"],
+ "🫎": ["へらじか","どうぶつ","えだつの","えるく","ほにゅうるい"],
+ "🦘": ["かんがるー","おーすとらりあ","じゃんぷ","ゆうぶくろるい"],
+ "🦥": ["たいだ","なまける","おそい"],
+ "🦦": ["かわうそ","づり","ふざける"],
+ "🦫": ["びーばー","だむ"],
+ "🦄": ["ゆにこーんのかお","かお","ゆにこーん"],
+ "🐝": ["みつばち","はち","こんちゅう"],
+ "🐛": ["むし","こんちゅう"],
+ "🦋": ["ちょう","こんちゅう","うつくしい"],
+ "🐌": ["かたつむり"],
+ "🪲": ["かぶとむし","むし","こんちゅう"],
+ "🐞": ["てんとうむし","かぶとむし","こんちゅう","てんとうちゅう"],
+ "🐜": ["あり","こんちゅう"],
+ "🦗": ["くりけっと","こおろぎ","ばっため","こんちゅう"],
+ "🪳": ["ごきぶり","こんちゅう","がいちゅう"],
+ "🕷": ["くも","こんちゅう"],
+ "🕸": ["くものす","くも","す"],
+ "🦂": ["さそり","さそりざ","せいざ"],
+ "🦟": ["か","びょうき","ねつ","こんちゅう","まらりあ","ういるす"],
+ "🪰": ["はえ","がいちゅう","こんちゅう","うじむし"],
+ "🪱": ["ぜんちゅう","たまきがたどうぶつ","みみず","きせいちゅう"],
+ "🦠": ["びせいぶつ","あめーば","ばくてりあ","ういるす"],
+ "🐢": ["かめ"],
+ "🐍": ["へび","うんぱんにん","へびつかいざ","せいざ"],
+ "🦎": ["とかげ","はちゅうるい"],
+ "🐙": ["たこ"],
+ "🦑": ["いか","なんたいどうぶつ"],
+ "🪼": ["くらげ","くすり","むせきついどうぶつ","ぜりー","うみ","いたい","しもう"],
+ "🦞": ["ろぶすたー","びすく","つめ","しーふーど"],
+ "🦀": ["かに","かにざ","せいざ"],
+ "🦐": ["えび","かい","ちいさい"],
+ "🦪": ["かき","しんじゅ","だいびんぐ"],
+ "🐠": ["ねったいぎょ","さかな","ねったい"],
+ "🐟": ["さかな","うおざ","せいざ"],
+ "🐡": ["ふぐ","さかな"],
+ "🐬": ["いるか","ひれ"],
+ "🦈": ["さめ","さかな"],
+ "🦭": ["あざらし","あしか"],
+ "🐳": ["しおふきくじら","かお","しおふき","くじら"],
+ "🐋": ["くじら"],
+ "🐊": ["わに"],
+ "🐆": ["ひょう"],
+ "🐅": ["とら"],
+ "🐃": ["すいぎゅう","みず"],
+ "🐂": ["ゆううし","おすうし","おうしざ","せいざ"],
+ "🐄": ["うし"],
+ "🦬": ["ばいそん","ばっふぁろー","むれ","ヴぃせんと"],
+ "🐪": ["ひとこぶらくだ","らくだ","こぶ"],
+ "🐫": ["ふたこぶらくだ","ふたこぶ","らくだ","こぶ"],
+ "🦙": ["らま","あるぱか","ぐあなこ","びくーにゃ","うーる"],
+ "🐘": ["ぞう"],
+ "🦏": ["さい"],
+ "🦛": ["かば"],
+ "🦣": ["まんもす","ぜつめつ","おおがた","きば","けにおおわれた"],
+ "🐐": ["やぎ","やぎざ","せいざ"],
+ "🐏": ["こひつじ","おひつじざ","ひつじ","せいざ"],
+ "🐑": ["ひつじ","めすひつじ"],
+ "🐎": ["うま","けいば","れーす"],
+ "🫏": ["ろば","どうぶつ","ぶーろ","ほにゅうるい","らば"],
+ "🐖": ["ぶた","めすぶた"],
+ "🦇": ["こうもり","きゅうけつき"],
+ "🐓": ["おんどり"],
+ "🦃": ["しちめんちょう(とり)","しちめんちょう","とり"],
+ "🕊": ["へいわのはと","とり","はと","ひこう","へいわ"],
+ "🦅": ["わし","とり"],
+ "🦆": ["あひる","とり"],
+ "🪿": ["がちょう","とり","かきん","けいてきのおと"],
+ "🦢": ["はくちょう","とり","はくちょうのお","みにくいあひるのこ"],
+ "🦉": ["ふくろう","とり","かしこい"],
+ "🦩": ["ふらみんご","ねったい","あざやか"],
+ "🦚": ["おすのくじゃく","とり","めすのくじゃく"],
+ "🦜": ["おうむ","とり","かいぞく"],
+ "🦤": ["どーどー","とり","ぜつめつ"],
+ "🪽": ["はね","てんし","こうくう","とり","ひこう","しんわ"],
+ "🪶": ["うもう","とり","かるい","はね"],
+ "🐕": ["いぬ","けん","ぺっと"],
+ "🦮": ["もうどうけん","あくせしびりてぃ","めがふじゆう","けん","がいど"],
+ "🐕‍🦺": ["かいじょいぬ","あくせしびりてぃ","しえん","けん","さーびす"],
+ "🐩": ["ぷーどる","いぬ","けん"],
+ "🐈": ["ねこ","ぺっと"],
+ "🐈‍⬛": ["くろねこ","くろ","ねこ","ぺっと","はろうぃーん"],
+ "🐇": ["うさぎ","ばにー","ぺっと"],
+ "🐀": ["ねずみ"],
+ "🐁": ["ねずみ"],
+ "🐿": ["しまりす"],
+ "🦨": ["すかんく","あくしゅう","におう"],
+ "🦡": ["あなぐま","らーてる","ねだる"],
+ "🦔": ["はりねずみ","かお"],
+ "🐾": ["どうぶつのあしあと","あし","あと"],
+ "🐉": ["どらごん","おとぎばなし"],
+ "🐲": ["どらごんのかお","どらごん","かお","おとぎばなし"],
+ "🦕": ["りゅうあしるい","ぶらきおさうるす","ぶろんとさうるす","でぃぷろどくす","きょうりゅう"],
+ "🦖": ["てぃらのさうるす","Tれっくす","きょうりゅう"],
+ "🌵": ["さぼてん","しょくぶつ"],
+ "🎄": ["くりすますつりー","あくてぃびてぃ","おいわい","くりすます","えんたーていめんと","つりー"],
+ "🌲": ["じょうりょくじゅ","じょうりょく","しょくぶつ","はた"],
+ "🌳": ["らくようじゅ","らくようせい","しょくぶつ","らくよう","はた"],
+ "🌴": ["やしのき","やし","しょくぶつ","はた"],
+ "🪴": ["はちうえ","しょくぶつ","かんようしょくぶつ"],
+ "🌱": ["なえぎ","しょくぶつ","わかい"],
+ "🌿": ["はーぶ","は","しょくぶつ"],
+ "☘": ["くろーばー","しょくぶつ"],
+ "🍀": ["よっつはのくろーばー","4","くろーばー","よん","は","しょくぶつ"],
+ "🎍": ["かどまつ","あくてぃびてぃ","たけ","おいわい","にっぽん","まつ","しょくぶつ"],
+ "🎋": ["ななゆう","あくてぃびてぃ","はた","おいわい","えんたーていめんと","にっぽん"],
+ "🍃": ["かぜになびくは","ふく","はためく","は","しょくぶつ","ふう"],
+ "🍂": ["おちば","らっか","は","しょくぶつ"],
+ "🍁": ["かえでのは","らっか","は","かえで","しょくぶつ"],
+ "🌾": ["いなほ","いねたば","ほ","しょくぶつ","こめ"],
+ "🪺": ["たまごのあるす","すづくり","とりのす","たまご"],
+ "🪹": ["そらのす","すづくり","とりのす"],
+ "🌺": ["はいびすかす","はな","しょくぶつ"],
+ "🌻": ["ひまわり","はな","しょくぶつ","たいよう"],
+ "🌹": ["ばら","はな","しょくぶつ"],
+ "🥀": ["しおれたはな","はな","しおれた"],
+ "🌷": ["ちゅーりっぷ","はな","しょくぶつ"],
+ "🌼": ["はな","しょくぶつ"],
+ "🌸": ["さくら","はな","しょくぶつ"],
+ "🪷": ["はす","ぶっきょう","はな","ひんどぅーきょう","いんど","せいじょう","べとなむ"],
+ "🪻": ["ひあしんす","ぶるーぼんねっと","はな","らべんだー","るぴなす","のうるーず","むらさき","きんぎょそう"],
+ "💐": ["はなたば","はな","しょくぶつ","ろまんす"],
+ "🍄": ["きのこ","しょくぶつ"],
+ "🐚": ["まきがい","かい"],
+ "🪸": ["さんご","たいよう","しょう"],
+ "🌎": ["あめりかたいりく","あめりか","ちきゅう","せかい"],
+ "🌍": ["よーろっぱとあふりかちいき","あふりか","ちきゅう","よーろっぱ","せかい"],
+ "🌏": ["あじあとおーすとらりあ","あじあ","おーすとらりあ","ちきゅう","せかい"],
+ "🌕": ["まんげつ","つき","うちゅう","てんき"],
+ "🌖": ["ねまちのつき","じゅうさんや","つき","うちゅう","かけ","てんき"],
+ "🌗": ["かげんのつき","つき","げん","うちゅう","てんき"],
+ "🌘": ["かけていくみかづき","さんじつげつ","つき","うちゅう","かけ","てんき"],
+ "🌑": ["しんげつ","かい","つき","うちゅう","てんき"],
+ "🌒": ["みちていくみかづき","さんじつげつ","つき","うちゅう","じょうげん","てんき"],
+ "🌓": ["じょうげんのつき","つき","げん","うちゅう","てんき"],
+ "🌔": ["じゅうさんやつき","じゅうさんや","つき","うちゅう","じょうげん","てんき"],
+ "🌙": ["さんじつげつ","つき","うちゅう","てんき"],
+ "🌚": ["かおつきしんげつ","かお","つき","うちゅう","てんき"],
+ "🌝": ["かおつきまんげつ","あかるい","かお","みちた","つき","うちゅう","てんき"],
+ "🌛": ["かおつきじょうげんのつき","かお","つき","げん","うちゅう","てんき"],
+ "🌜": ["がおがあるかげんのつき","かお","つき","げん","うちゅう","てんき"],
+ "⭐": ["ちゅうくらいのほし","ほし"],
+ "🌟": ["ひかるほし","きらめき","あかいひかり","かがやく","かがやき","ほし"],
+ "💫": ["くらくら","まんが","めまい","ほし"],
+ "✨": ["きらきら","えんたーていめんと","かがやき","ほし"],
+ "☄": ["すいせい","うちゅう"],
+ "🪐": ["たまきのあるわくせい","うちゅう","わくせい","どせい"],
+ "🌞": ["かおつきたいよう","あかるい","かお","うちゅう","たいよう","てんき"],
+ "☀️": ["たいようのひかり","あかるい","こうせん","うちゅう","たいよう","せいてん","てんき"],
+ "🌤": ["たいようとちいさなくも","くも","たいよう","てんき"],
+ "⛅": ["はれときどきくもり","くも","たいよう","てんき"],
+ "🌥": ["はれのちくもり","くも","たいよう","てんき"],
+ "🌦": ["はれのちくもりときどきあめ","くも","あめ","たいよう","てんき"],
+ "☁️": ["くも","てんき"],
+ "🌧": ["あまぐも","くも","あめ","てんき"],
+ "⛈": ["らいう","くも","あめ","かみなり","てんき"],
+ "🌩": ["らいうん","くも","かみなり","てんき"],
+ "⚡": ["だかでんあつきごう","きけん","でんき","かみなり","でんあつ","びりびり"],
+ "🔥": ["えん","ひ","どうぐ"],
+ "💥": ["しょうとつまーく","どかーん","しょうとつ","まんが"],
+ "❄️": ["せつのけっしょう","つめたい","ゆき","てんき"],
+ "🌨": ["ゆきぐも","くも","れい","ゆき","てんき"],
+ "☃": ["ゆきだるま","れい","ゆき","てんき"],
+ "⛄": ["ゆきだるま","れい","ゆき","てんき"],
+ "🌬": ["かぜがふいている","かぜがふく","くも","かお","てんき","ふう"],
+ "💨": ["だっしゅ","まんが","はしる"],
+ "🌪": ["たつまきぐも","くも","たつまき","てんき","せんぷう"],
+ "🌫": ["きり","くも","てんき"],
+ "🌈": ["にじ","あめ","れいんぼー","てんき","ぷらいど","lgbt"],
+ "☔": ["うとかさ","いるい","しずく","あめ","かさ","てんき"],
+ "💧": ["しずく","ぞっとする","まんが","したたり","あせ","てんき"],
+ "💦": ["あせまーく","まんが","ぬれている","あせ"],
+ "🌊": ["なみ","うみ","みず","てんき"],
+ "🍏": ["あおりんご","りんご","ふるーつ","くだもの","みどり","しょくぶつ"],
+ "🍎": ["あかいりんご","りんご","ふるーつ","くだもの","しょくぶつ","あか"],
+ "🍐": ["なし","ふるーつ","くだもの","しょくぶつ"],
+ "🍊": ["みかん","ふるーつ","くだもの","おれんじ","しょくぶつ","あかだいだいいろ"],
+ "🍋": ["れもん","かんきつるい","ふるーつ","くだもの","しょくぶつ"],
+ "🍌": ["ばなな","ふるーつ","くだもの","しょくぶつ"],
+ "🍉": ["すいか","ふるーつ","くだもの","しょくぶつ"],
+ "🍇": ["ぶどう","ふるーつ","くだもの","しょくぶつ"],
+ "🍓": ["いちご","べりー","ふるーつ","くだもの","しょくぶつ"],
+ "🍈": ["めろん","ふるーつ","くだもの","しょくぶつ"],
+ "🍒": ["さくらんぼ","ふるーつ","くだもの","しょくぶつ"],
+ "🫐": ["ぶるーべりー","べりー","びるべりー","あお","ふるーつ"],
+ "🍑": ["もも","ふるーつ","くだもの","しょくぶつ"],
+ "🥭": ["まんごー","ねったい","ふるーつ"],
+ "🍍": ["ぱいなっぷる","ふるーつ","くだもの","しょくぶつ"],
+ "🥥": ["ここなっつ","ふるーつ"],
+ "🥝": ["きういふるーつ","ふるーつ","くだもの","きうい"],
+ "🍅": ["とまと","しょくぶつ","やさい"],
+ "🥑": ["あぼかど","ふるーつ","くだもの"],
+ "🫒": ["おりーぶ","ふるーつ"],
+ "🍆": ["なす","なすび","しょくぶつ","やさい"],
+ "🌶": ["とうがらし","からい","こしょう","しょくぶつ"],
+ "🫑": ["ぴーまん","とうがらし","こしょう","しょくぶつ","やさい"],
+ "🥒": ["きゅうり","ぴくるす","やさい"],
+ "🥬": ["はっぱのみどり","ちんげんさい","きゃべつ","けーる","れたす"],
+ "🥦": ["ぶろっこりー","やさい"],
+ "🫛": ["えんどうまめのさや","まめ","えだまめ","まめか","えんどうまめ","さや","やさい"],
+ "🧄": ["にんにく","やさい","しょくぶつ","こうみりょう"],
+ "🧅": ["たまねぎ","やさい","しょくぶつ","こうみりょう"],
+ "🌽": ["とうもろこし","こーん","しょくぶつ"],
+ "🥕": ["にんじん","やさい"],
+ "🥗": ["ぐりーんさらだ","みどり","さらだ"],
+ "🥔": ["じゃがいも","やさい"],
+ "🍠": ["やきいも","じゃがいも","やき","すいーつ"],
+ "🌰": ["くり","しょくぶつ"],
+ "🥜": ["ぴーなっつ","なっつ","やさい"],
+ "🫘": ["まめ","たべもの","じんぞう"],
+ "🍯": ["はにーぽっと","はちみつ","ぽっと","すいーつ"],
+ "🍞": ["ぱん","ろーふ"],
+ "🥐": ["くろわっさん","ぱん","さんじつげつ","ろーる","ふれんち"],
+ "🥖": ["ふらんすぱん","ぱん","ふれんち"],
+ "🫓": ["ふらっとぶれっど","あれぱ","らヴぁしゅ","なん","ぴた"],
+ "🥨": ["ぷれっつぇる","そふとぷれっつぇる","ぷれっつぇるついすと","ぱん"],
+ "🥯": ["べーぐる","ぱん","くりーむちーず","ひとぬり"],
+ "🥞": ["ぱんけーき","くれーぷ","ほっとけーき"],
+ "🧇": ["わっふる","ほっとけーき"],
+ "🧀": ["ちーず"],
+ "🍗": ["たーきー","ほね","にわとり","あし","かきん"],
+ "🍖": ["ほねつきにく","ほね","にく"],
+ "🥩": ["いちきれのにく","にく","きりみ","らむちょっぷ","ぶた","すてーき"],
+ "🍤": ["えびふらい","ふらい","えび","こえび","てんぷら"],
+ "🥚": ["たまご"],
+ "🍳": ["りょうり","たまご","ふらいぱん","なべ"],
+ "🥓": ["べーこん","にく"],
+ "🍔": ["はんばーがー","ばーがー"],
+ "🍟": ["ふらいどぽてと","ふらいど","ぽてと"],
+ "🌭": ["ほっとどっぐ","ふらんくふるとそーせーじ","ほっとどっぐそーせーじ","そーせーじ","うぃんなー","れっどほっと"],
+ "🍕": ["ぴざ","ちーず","1まい"],
+ "🍝": ["すぱげってぃ","ぱすた"],
+ "🥪": ["さんどうぃっち","ぱん","やさい","ちーず","にく","でり"],
+ "🌮": ["たこす","めきしこ"],
+ "🌯": ["ぶりとー","めきしこ"],
+ "🫔": ["たまーれ","たまーり","めきしかん","つつまれた"],
+ "🥙": ["ふらっとぶれっどさんど","ふぁらふぇる","ふらっとぶれっど","じゃいろ","けばぶ","つめもの"],
+ "🧆": ["ふぁらふぇる","ひよこまめ"],
+ "🍜": ["どんぶり","めん","らーめん","むしかねつ","すーぷ"],
+ "🥘": ["ぱえりあ","きゃせろーる","なべ","あさい"],
+ "🍲": ["なべ","しちゅー"],
+ "🫕": ["ふぉんでゅ","ちーず","ちょこれーと","ふぉでゅ","とけた","ぽっと","すいす"],
+ "🥫": ["かんづめ","ほぞんようしょくひん"],
+ "🫙": ["びん","こうしんりょう","ようき","そら","そーす","ちょぞう"],
+ "🧂": ["しお","こうしんりょう","しぇーかー"],
+ "🧈": ["ばたー","にゅうせいひん"],
+ "🫚": ["しょうが","びーる","ね","すぱいす"],
+ "🍥": ["なると","こけいのたべもの","さかな","ねりもの"],
+ "🍣": ["すし"],
+ "🍱": ["べんとうばこ","べんとう","はこ"],
+ "🍛": ["かれーらいす","かれー","ごはん"],
+ "🍙": ["おにぎり","にっぽん","こめ"],
+ "🍚": ["ごはん","りょうり","こめ"],
+ "🍘": ["せんべい","こめ"],
+ "🥟": ["ぎょうざ"],
+ "🍢": ["おでん","しーふーど","くし","すてぃっく"],
+ "🍡": ["だんご","でざーと","にっぽん","くし","すてぃっく","すいーつ"],
+ "🍧": ["かきごおり","でざーと","こおり","すいーつ"],
+ "🍨": ["あいすくりーむ","くりーむ","でざーと","こおり","すいーつ"],
+ "🍦": ["そふとくりーむ","くりーむ","でざーと","こおり","あいすくりーむ","そふと","すいーつ"],
+ "🍰": ["しょーとけーき","けーき","でざーと","ぺいすとりー","すらいす","すいーつ"],
+ "🎂": ["ばーすでーけーき","たんじょうび","けーき","おいわい","でざーと","ぺいすとりー","すいーつ"],
+ "🧁": ["かっぷけーき","べーかりー","すいーつ","でざーと","ぺいすとりー"],
+ "🥧": ["ぱい","でざーと","すいーつ"],
+ "🍮": ["かすたーど","でざーと","ぷりん","すいーつ"],
+ "🍭": ["ぺろぺろきゃんでぃー","きゃんでぃ","でざーと","ろりぽっぷきゃんでぃ","すいーつ"],
+ "🍬": ["あめ","でざーと","すいーつ"],
+ "🍫": ["ちょこれーと","ばー","でざーと","すいーつ"],
+ "🍿": ["ぽっぷこーん"],
+ "🍩": ["どーなつ","でざーと","すいーつ"],
+ "🍪": ["くっきー","でざーと","あまい"],
+ "🥠": ["おみくじいりくっきー","ふぉーちゅんくっきー"],
+ "🥮": ["げっぺい","あき","まつり"],
+ "☕": ["ほっとどりんく","いんりょう","こーひー","のみもの","あたたかい","じょうき","おちゃ"],
+ "🍵": ["ゆのみ","いんりょう","かっぷ","のみもの","おちゃ"],
+ "🫖": ["てぃーぽっと","どりんく","ぽっと","てぃー","けとる"],
+ "🥣": ["ぼうるとすぷーん","ちょうしょく","しりある","おかゆ","おーとみーる","ぽりっじ","しょっき"],
+ "🍼": ["ほにゅうびん","あかちゃん","ぼとる","どりんく","みるく"],
+ "🥤": ["かっぷとすとろー","じゅーす","そーだ","もると","そふとどりんく","みず","しょっき"],
+ "🧋": ["たぴおかてぃー","ばぶる","みるく","ぱーる","てぃー","ぼば","たぴおか","もみ"],
+ "🧃": ["いんりょうぼっくす","じゅーす","いんりょう","ぼっくす","どりんく","すとろー"],
+ "🧉": ["まて","どりんく","ぼんびりや","いえるば"],
+ "🥛": ["こっぷにはいったぎゅうにゅう","どりんく","ぐらす","みるく"],
+ "🫗": ["ながれこむえきたい","のみもの","そら","ぐらす","こぼれる"],
+ "🍺": ["びーる","ばー","のむ","まぐかっぷ"],
+ "🍻": ["かんぱい","ばー","びーる","かちん","のみもの","まぐかっぷ"],
+ "🍷": ["わいんぐらす","ばー","いんりょう","のみもの","ぐらす","わいん"],
+ "🥂": ["ぐらすでかんぱい","いわう","かちん","のみもの","ぐらす"],
+ "🥃": ["たんぶらー","ぐらす","て","しょっと","ういすきー","うぃすきー","ばーぼん"],
+ "🍸": ["かくてるぐらす","ばー","かくてる","のみもの","ぐらす"],
+ "🍹": ["とろぴかるどりんく","ばー","のみもの","とろぴかる"],
+ "🍾": ["びんととびだすせん","ばー","ぼとる","しゃんぱん","しゃんぺん","しゃんぱーにゅ","こるく","のみもの","とびだす","ぱーてぃー"],
+ "🍶": ["とっくりとおちょこ","ばー","いんりょう","ぼとる","かっぷ","のみもの","て"],
+ "🧊": ["かくこおり","こおり","りっぽうたい","つめたい","ひょうざん"],
+ "🥄": ["すぷーん","しょっき"],
+ "🍴": ["ふぉーくとないふ","ちょうり","ふぉーく","ないふ","しょっき"],
+ "🍽": ["ふぉーくとないふとぷれーと","ちょうり","ふぉーく","ないふ","ぷれーと","しょっき"],
+ "🥢": ["はし"],
+ "🥡": ["ていくあうとぼっくす","ていくあうと","ようき","おもちかえり"],
+ "⚽": ["さっかーぼーる","ぼーる","さっかー"],
+ "🏀": ["ばすけっとぼーる","ぼーる","ばすけっとりんぐ"],
+ "🏈": ["あめりかんふっとぼーる","あめりかん","ぼーる","ふっとぼーる"],
+ "⚾": ["やきゅう","ぼーる"],
+ "🥎": ["そふとぼーる","ぼーる","しあい","すぽーつ"],
+ "🎾": ["てにすぼーる","ぼーる","らけっと","てにす"],
+ "🏐": ["ばれーぼーる","ぼーる","しあい"],
+ "🏉": ["らぐびー","ぼーる","ふっとぼーる"],
+ "🎱": ["びりやーど","8","えいとぼーる","ぼーる","えいと","げーむ"],
+ "🥏": ["そらとぶえんばん","でぃすく","あるてぃめっと","ごるふ","しあい","すぽーつ","ふりすびー"],
+ "🪃": ["ぶーめらん","おーすとらりあ","ぎゃくもどり","はねかえり"],
+ "🏓": ["たっきゅうのらけっととぼーる","ぼーる","ばっと","しあい","ぱどる","たっきゅう"],
+ "🏸": ["ばどみんとんのらけっととしゃとる","ばどみんとん","ばーでぃー","しあい","らけっと","しゃとる"],
+ "🥅": ["ごーるねっと","ごーる","ねっと"],
+ "🏒": ["あいすほっけーのすてぃっくとぱっく","しあい","ほっけー","こおり","ぱっく","すてぃっく"],
+ "🏑": ["ふぃーるどほっけーのすてぃっくとぼーる","ぼーる","ふぃーるど","しあい","ほっけー","すてぃっく"],
+ "🏏": ["くりけっとのばっととぼーる","ぼーる","ふぃーるど","くりけっと","しあい"],
+ "🥍": ["らくろす","ぼーる","すてぃっく","しあい","すぽーつ"],
+ "🥌": ["かーりんぐすとーん","かーりんぐ","すとーん"],
+ "⛳": ["ごるふのかっぷ","ぴんふらっぐ","ごるふ","ほーる"],
+ "🏹": ["ゆみや","しゃしゅ","や","ゆみ","しゃしゅざ","どうぐ","せいざ"],
+ "🎣": ["つりざおとさかな","えんたーていめんと","さかな","ぼう"],
+ "🤿": ["だいびんぐますく","だいびんぐ","すきゅーば","しゅのーける"],
+ "🥊": ["ぼくしんぐぐろーぶ","ぼくしんぐ","ぐろーぶ"],
+ "🥋": ["どうぎ","じゅうどう","からて","ぶどう","てこんどー","ゆにふぉーむ"],
+ "⛸": ["あいすすけーと","こおり"],
+ "🎿": ["すきーとすきーぶーつ","すきー","ゆき"],
+ "🛷": ["そり","るーじゅ","とぼがん"],
+ "⛷": ["すきー","ゆき"],
+ "🏂": ["すのーぼーだー","すきー","ゆき","すのーぼーど"],
+ "🏋️‍♀️": ["うえいとをもちあげるじょせい","あげ","じゅうりょう","じょせい","おんな"],
+ "🏋": ["うえいとをもちあげるひと","あげ","じゅうりょう"],
+ "🏋️‍♂️": ["うえいとをもちあげるだんせい","あげ","じゅうりょう","おとこ","だんせい"],
+ "🤺": ["ふぇんしんぐをするひと","けんし","けんじゅつ","けん"],
+ "🤼‍♀️": ["れすりんぐをするじょせい","れすりんぐ","れすりんぐせんしゅ","じょせい","おんな"],
+ "🤼": ["れすりんぐをするひとたち","れすりんぐ","れすりんぐせんしゅ"],
+ "🤼‍♂️": ["れすりんぐをするだんせい","れすりんぐ","れすりんぐせんしゅ","おとこ","だんせい"],
+ "🤸‍♀️": ["そくてんをするじょせい","そくほうてんかい","たいそう","じょせい","おんな"],
+ "🤸": ["そくてんをするひと","そくほうてんかい","たいそう"],
+ "🤸‍♂️": ["そくてんをするだんせい","そくほうてんかい","たいそう","おとこ","だんせい"],
+ "⛹️‍♀️": ["ぼーるをばうんどさせるじょせい","ぼーる","じょせい","おんな"],
+ "⛹": ["ぼーるをばうんどさせるひと","ぼーる"],
+ "⛹️‍♂️": ["ぼーるをばうんどさせるだんせい","ぼーる","おとこ","だんせい"],
+ "🤾‍♀️": ["はんどぼーるをするじょせい","ぼーる","はんどぼーる","じょせい","おんな"],
+ "🤾": ["はんどぼーるをするひと","ぼーる","はんどぼーる"],
+ "🤾‍♂️": ["はんどぼーるをするだんせい","ぼーる","はんどぼーる","おとこ","だんせい"],
+ "🧗‍♀️": ["くらいみんぐしているじょせい","くらいみんぐ","ろっく","じょせい","おんな"],
+ "🧗": ["くらいみんぐしているひと","くらいみんぐ","ろっく"],
+ "🧗‍♂️": ["くらいみんぐしているだんせい","くらいみんぐ","ろっく","だんせい","おとこ"],
+ "🏌️‍♀️": ["ごるふをするじょせい","ぼーる","ごるふ","ごるふぁー","ごるふする","じょせい","おんな"],
+ "🏌": ["ごるふをするひと","ぼーる","ごるふ","ごるふぁー","ごるふする"],
+ "🏌️‍♂️": ["ごるふをするだんせい","ぼーる","ごるふ","ごるふぁー","ごるふする","おとこ","だんせい"],
+ "🧘‍♀️": ["れんげざのじょせい","めいそう","よが","せいおん","じょせい","おんな"],
+ "🧘": ["れんげざのひと","めいそう","よが","せいおん"],
+ "🧘‍♂️": ["れんげざのだんせい","めいそう","よが","せいおん","だんせい","おとこ"],
+ "🧖‍♀️": ["すちーむるーむにいるじょせい","さうな","すちーむるーむ","はまむ","すちーむばす","じょせい","おんな"],
+ "🧖": ["すちーむるーむにいるひと","さうな","すちーむるーむ","はまむ","すちーむばす"],
+ "🧖‍♂️": ["すちーむるーむにいるだんせい","さうな","すちーむるーむ","はまむ","すちーむばす","だんせい","おとこ"],
+ "🏄‍♀️": ["さーふぃんをするじょせい","さーふぁー","さーふぃん","なみのり","じょせい","おんな"],
+ "🏄": ["さーふぃんをするひと","さーふぁー","さーふぃん","なみのり"],
+ "🏄‍♂️": ["さーふぃんをするだんせい","さーふぁー","さーふぃん","なみのり","おとこ","だんせい"],
+ "🏊‍♀️": ["およぐじょせい","およぐ","すいえい","じょせい","おんな"],
+ "🏊": ["すいえいをするひと","およぐ","すいえい"],
+ "🏊‍♂️": ["およぐだんせい","およぐ","すいえい","おとこ","だんせい"],
+ "🤽‍♀️": ["すいきゅうをするじょせい","ぽろ","みず","すいきゅう","じょせい","おんな"],
+ "🤽": ["すいきゅうをするひと","ぽろ","みず","すいきゅう"],
+ "🤽‍♂️": ["すいきゅうをするだんせい","ぽろ","みず","すいきゅう","おとこ","だんせい"],
+ "🚣‍♀️": ["ぼーとをこぐじょせい","ぼーと","こぎぶね","のりもの","そうてい","じょせい","おんな"],
+ "🚣": ["ぼーとをこぐひと","ぼーと","こぎぶね","のりもの","そうてい"],
+ "🚣‍♂️": ["ぼーとをこぐだんせい","ぼーと","こぎぶね","のりもの","そうてい","おとこ","だんせい"],
+ "🏇": ["けいば","うま","きしゅ","きょうそうば"],
+ "🚴‍♀️": ["じてんしゃにのるじょせい","じてんしゃ","じてんしゃのり","じてんしゃにのるひと","さいくりすと","じょせい","おんな"],
+ "🚴": ["じてんしゃにのるひと","じてんしゃ","じてんしゃのり","さいくりすと"],
+ "🚴‍♂️": ["じてんしゃにのるだんせい","じてんしゃ","じてんしゃのり","じてんしゃにのるひと","さいくりすと","おとこ","だんせい"],
+ "🚵‍♀️": ["まうんてんばいくにのるじょせい","まうんてんばいくらいだー","くろすばいく","じてんしゃ","じてんしゃのり","じてんしゃにのるひと","さいくりすと","やま","じょせい","おんな"],
+ "🚵": ["まうんてんばいくにのるひと","まうんてんばいくらいだー","くろすばいく","じてんしゃ","じてんしゃのり","じてんしゃにのるひと","やま"],
+ "🚵‍♂️": ["まうんてんばいくにのるだんせい","まうんてんばいくらいだー","くろすばいく","じてんしゃ","じてんしゃのり","じてんしゃにのるひと","さいくりすと","やま","おとこ","だんせい"],
+ "🎽": ["らんにんぐしゃつとたすき","らんにんぐ","たすき","しゃつ"],
+ "🎖": ["くんしょう","おいわい","めだる","ぐんじ"],
+ "🏅": ["すぽーつのめだる","めだる"],
+ "🥇": ["きんめだる","1い","きん","めだる","1","だい1い"],
+ "🥈": ["ぎんめだる","めだる","2い","ぎん","2","だい2い"],
+ "🥉": ["どうめだる","どう","めだる","3い","3","だい3い"],
+ "🏆": ["とろふぃー","しょう"],
+ "🏵": ["ばらかざり","しょくぶつ"],
+ "🎗": ["りまいんだーりぼん","おいわい","りまいんだー","りぼん"],
+ "🎫": ["きっぷ","あくてぃびてぃ","にゅうじょうりょう","えんたーていめんと","ちけっと"],
+ "🎟": ["にゅうじょうけん","にゅうじょうりょう","えんたーていめんと","ちけっと"],
+ "🎪": ["さーかすごや","あくてぃびてぃ","さーかす","えんたーていめんと","てんと"],
+ "🤹‍♀️": ["じゃぐりんぐをするじょせい","てんびん","じゃぐりんぐ","じょせい","おんな"],
+ "🤹": ["じゃぐりんぐをするひと","ばらんす","じゃぐりんぐ"],
+ "🤹‍♂️": ["じゃぐりんぐをするだんせい","てんびん","じゃぐりんぐ","だんせい","おとこ"],
+ "🎭": ["ぶたいげいじゅつ","あくてぃびてぃ","げいじゅつ","えんたーていめんと","かめん","ぶたい","しあたー"],
+ "🎨": ["えのぐぱれっと","あくてぃびてぃ","あーと","えんたーていめんと","びじゅつかん","かいが","ぱれっと"],
+ "🎬": ["かちんこ","あくてぃびてぃ","えんたーていめんと","えいが"],
+ "🎤": ["まいく","あくてぃびてぃ","えんたーていめんと","からおけ","まいくろふぉん"],
+ "🎧": ["へっどほん","あくてぃびてぃ","いやほん","えんたーていめんと","へっどふぉん"],
+ "🎼": ["がくふ","あくてぃびてぃ","えんたーていめんと","おんがく"],
+ "🎹": ["けんばん","あくてぃびてぃ","えんたーていめんと","がっき","きーぼーど","おんがく","ぴあの"],
+ "🪗": ["あこーでぃおん","こんさーてぃーな","すくいーずぼっくす"],
+ "🥁": ["どらむ","どらむすてぃっく","おんがく"],
+ "🪘": ["ながいどらむ","びーと","こんが","どらむ","りずむ","じゃんべ"],
+ "🪇": ["まらかす","いわう","がっき","おんがく","そうおん","だがっき","がたがた","りずむ","しぇいく"],
+ "🎷": ["さっくす","あくてぃびてぃ","えんたーていめんと","がっき","おんがく","さくそふぉーん"],
+ "🎺": ["とらんぺっと","あくてぃびてぃ","えんたーていめんと","がっき","おんがく"],
+ "🪈": ["ふるーと","たけ","よこぶえそうしゃ","ふるーとそうしゃ","おんがく","ぱいぷ","りこーだー","ふく","もっかんがっき"],
+ "🎸": ["ぎたー","あくてぃびてぃ","えんたーていめんと","がっき","おんがく"],
+ "🪕": ["ばんじょー","あくてぃびてぃ","えんたーていめんと","がっき","おんがく"],
+ "🎻": ["ばいおりん","あくてぃびてぃ","えんたーていめんと","がっき","おんがく"],
+ "🎲": ["さいころ","さい","えんたーていめんと","げーむ"],
+ "🧩": ["ぱずるのぴーす","てがかり","かみあう","ぴーす","ぱずる","じぐそー"],
+ "♟️": ["ちぇすのぽーん","ちぇす","こま","げーむ","すてこま"],
+ "🎯": ["てきちゅう","あくてぃびてぃ","ぶる","ぶるずあい","だーつ","えんたーていめんと","め","しあい","ひっと","ひょうてき"],
+ "🎳": ["ぼうりんぐ","ぼーる","しあい"],
+ "🪀": ["よーよー","おもちゃ","じょうげ"],
+ "🪁": ["たこ","おもちゃ","とぶ","まう"],
+ "🛝": ["すべりだい","ゆうえんち","あそび"],
+ "🎮": ["てれびげーむ","こんとろーらー","えんたーていめんと","げーむ","びでおげーむ"],
+ "👾": ["えいりあん","うちゅうじん","かいじゅう","いせいじん","かお","おとぎばなし","ふぁんたじー","もんすたー","うちゅう","UFO"],
+ "🎰": ["すろっとましん","あくてぃびてぃ","げーむ","すろっと"],
+ "🚗": ["じどうしゃ","くるま","のりもの"],
+ "🚙": ["きゃんぴんぐかー","れくりえーしょん","RV","のりもの"],
+ "🚕": ["たくしー","のりもの"],
+ "🛺": ["おーとりきしゃ","じんりきしゃ","とぅくとぅく"],
+ "🚌": ["ばす","のりもの"],
+ "🚎": ["とろりーばす","ばす","ろめんでんしゃ","しがいでんしゃ","のりもの"],
+ "🏎": ["れーしんぐかー","くるま","きょうそう"],
+ "🚓": ["ぱとかー","くるま","ぱとろーる","けいさつ","のりもの"],
+ "🚑": ["きゅうきゅうしゃ","のりもの"],
+ "🚒": ["しょうぼうしゃ","えんじん","えん","とらっく","のりもの"],
+ "🚐": ["まいくろばす","ばす","のりもの"],
+ "🛻": ["ぴっくあっぷとらっく","ぴっくあっぷ","とらっく","のりもの"],
+ "🚚": ["はいたつようとらっく","はいたつ","とらっく","のりもの"],
+ "🚛": ["とれーらー","おおがたとらっく","せみ","とらっく","のりもの"],
+ "🚜": ["とらくたー","のりもの"],
+ "🏍": ["れーすばいく","おーとばい","れーす"],
+ "🛵": ["すくーたー","もーたー"],
+ "🚲": ["じてんしゃ","ばいく","のりもの"],
+ "🦼": ["でんどうくるまいす","あくせしびりてぃ","くるまいす"],
+ "🦽": ["しゅどうくるまいす","あくせしびりてぃ","くるまいす"],
+ "🛴": ["きっくぼーど","きっく","すくーたー"],
+ "🛹": ["すけぼー","すけーと","ぼーど"],
+ "🛼": ["ろーらーすけーと","ろーらー","すけーと"],
+ "🛞": ["しゃりん","えん","たいや","かいてん"],
+ "🚨": ["ぱとらいと","くるま","ひかり","けいさつ","かいてん","のりもの","さいれん","けいこく"],
+ "🚔": ["ぱとかー","くるま","たいこうしゃ","けいさつ","のりもの"],
+ "🚍": ["ばす","たいこうしゃ","のりもの"],
+ "🚘": ["たいこうしゃ","じどうしゃ","くるま","のりもの"],
+ "🚖": ["たくしー","たいこうしゃ","のりもの"],
+ "🚡": ["ろーぷうぇい","くうちゅう","けーぶる","くるま","ごんどら","とらむうぇい","のりもの"],
+ "🚠": ["ろーぷうぇい","けーぶる","ごんどら","やま","のりもの"],
+ "🚟": ["こうかてつどう","てつどう","のりもの"],
+ "🚃": ["てつどうしゃりょう","くるま","でんき","てつどう","れっしゃ","ろめん","とろりーばす","のりもの"],
+ "🚋": ["ろめんでんしゃ","くるま","ろめん","とろりーばす","のりもの"],
+ "🚝": ["ものれーる","のりもの"],
+ "🚄": ["しんかんせん","てつどう","こうそく","れっしゃ","のりもの"],
+ "🚅": ["しんかんせん","だんがん","てつどう","こうそく","れっしゃ","のりもの"],
+ "🚈": ["らいとれーる","てつどう","のりもの"],
+ "🚞": ["さんがくてつどう","くるま","やま","てつどう","のりもの"],
+ "🚂": ["じょうききかんしゃ","えんじん","きかんしゃ","てつどう","じょうき","れっしゃ","のりもの"],
+ "🚆": ["でんしゃ","せんろ","のりもの"],
+ "🚇": ["ちかてつ","めとろ","のりもの"],
+ "🚊": ["ろめんでんしゃ","とろりーばす","のりもの"],
+ "🚉": ["えき","せんろ","でんしゃ","のりもの"],
+ "🚁": ["へりこぷたー","のりもの"],
+ "🛩": ["こがたこうくうき","ひこうき","のりもの"],
+ "✈️": ["ひこうき","のりもの"],
+ "🛫": ["ひこうきのりりく","ひこうき","ちぇっくいん","しゅっぱつ","のりもの"],
+ "🛬": ["ひこうきのちゃくりく","ひこうき","とうちゃく","ちゃくりく","のりもの"],
+ "🪂": ["ぱらしゅーと","ぱらせーる","すかいだいぶ","はんぐぐらいだー"],
+ "💺": ["ざせき","いす"],
+ "🛰": ["さてらいと","えいせい","うちゅう","のりもの"],
+ "🚀": ["ろけっと","うちゅう","のりもの"],
+ "🛸": ["そらとぶえんばん","UFO","うちゅうじん","いほしじん","うちゅう","くうそう"],
+ "🛶": ["かぬー","ぼーと"],
+ "⛵": ["よっと","ぼーと","りぞーと","うみ","のりもの"],
+ "🛥": ["もーたーぼーと","ぼーと","のりもの"],
+ "🚤": ["すぴーどぼーと","ぼーと","のりもの"],
+ "⛴": ["ふぇりー","ぼーと"],
+ "🛳": ["りょかくせん","りょかく","ふね","のりもの"],
+ "🚢": ["ふね","のりもの"],
+ "🛟": ["きゅうめいうきわ","うきわ","らいふじゃけっと","らいふせーばー","きゅうじょ","あんぜん"],
+ "⚓": ["いかり","ふね","つーる"],
+ "⛽": ["がそりんすたんど","ねんりょう","がそりん","きゅうゆき","さーびすすてーしょん"],
+ "🚧": ["こうじちゅう","こうじようふぇんす","けんせつこうじ"],
+ "🚏": ["ばすてい","ばす","ていし"],
+ "🚦": ["たてむきのしんごうき","しんごうき","しんごう","こうつう"],
+ "🚥": ["よこむきのしんごうき","しんごうき","しんごう","こうつう"],
+ "🛑": ["いちじていしひょうしき","はっかっけい","ひょうしき","ていし"],
+ "🎡": ["かんらんしゃ","あくてぃびてぃ","ゆうえんち","えんたーていめんと","ふぇりす"],
+ "🎢": ["じぇっとこーすたー","あくてぃびてぃ","ゆうえんち","こーすたー","えんたーていめんと","ろーらー"],
+ "🎠": ["めりーごーらんど","あくてぃびてぃ","めりーごーらうんど","えんたーていめんと","うま"],
+ "🏗": ["けんせつちゅう","たてもの","けんせつ"],
+ "🌁": ["きり","てんき"],
+ "🗼": ["とうきょうたわー","とうきょう","たわー"],
+ "🏭": ["こうじょう","たてもの"],
+ "⛲": ["ふんすい"],
+ "🎑": ["おつきみ","あくてぃびてぃ","おいわい","じゅしょうしき","えんたーていめんと","つき"],
+ "⛰": ["やま"],
+ "🏔": ["ゆきやま","さむい","やま","ゆき"],
+ "🗻": ["ふじさん","やま"],
+ "🌋": ["かざん","ふんか","やま","きしょう"],
+ "🗾": ["にっぽんれっとう","にっぽん","ちず"],
+ "🏕": ["きゃんぷ"],
+ "⛺": ["てんと","きゃんぷ"],
+ "🏞": ["こくりつこうえん","こうえん"],
+ "🛣": ["こうそくどうろ","はいうぇい","どうろ"],
+ "🛤": ["せんろ","てつどう","でんしゃ"],
+ "🌅": ["ひので","あさ","たいよう","てんこう"],
+ "🌄": ["やまからのひので","あさ","やま","たいよう","ひので","てんこう"],
+ "🏜": ["さばく"],
+ "🏖": ["びーちとかさ","びーち","かさ","ぱらそる"],
+ "🏝": ["むじんとう","さばく","しま"],
+ "🌇": ["びるにしずむゆうひ","たてもの","ゆうぐれ","たいよう","ゆうひ","てんき"],
+ "🌆": ["ゆうぐれのまちなみ","たてもの","まち","ゆうぐれ","ひぐれ","ふうけい","たいよう","ゆうひ","てんき"],
+ "🏙": ["まちなみ","たてもの","まち"],
+ "🌃": ["ほしぞら","よる","ほし","てんき"],
+ "🌉": ["よるのはし","はし","よる","てんき"],
+ "🌌": ["あまのがわ","うちゅう","てんき"],
+ "🌠": ["ながれぼし","あくてぃびてぃ","らっか","ながれる","うちゅう","ほし"],
+ "🎇": ["せんこうはなび","あくてぃびてぃ","おいわい","えんたーていめんと","はなび","きらきら"],
+ "🎆": ["はなび","あくてぃびてぃ","おいわい","えんたーていめんと"],
+ "🛖": ["こや","いえ","せんけいこ","ぱお"],
+ "🏘": ["いえ","たてもの"],
+ "🏰": ["せいようのしろ","たてもの","しろ","よーろっぱ"],
+ "🏯": ["にっぽんのしろ","たてもの","しろ","にっぽん"],
+ "🏟": ["すたじあむ"],
+ "🗽": ["じゆうのめがみ","じゆう","ぞう"],
+ "🏠": ["いえ","たてもの","じたく"],
+ "🏡": ["にわつきのいえ","たてもの","にわ","じたく","いえ"],
+ "🏚": ["はいきょ","たてもの","はいおく","いえ"],
+ "🏢": ["おふぃすびる","たてもの"],
+ "🏬": ["でぱーと","たてもの","てん"],
+ "🏣": ["にっぽんのゆうびんきょく","たてもの","にっぽん","ぽすと"],
+ "🏤": ["よーろっぱのゆうびんきょく","たてもの","よーろっぱ","ぽすと"],
+ "🏥": ["びょういん","たてもの","いし","くすり"],
+ "🏦": ["ぎんこう","たてもの"],
+ "🏨": ["ほてる","たてもの"],
+ "🏪": ["こんびにえんすすとあ","たてもの","こんびにえんす","すとあ"],
+ "🏫": ["がっこう","たてもの"],
+ "🏩": ["らぶほてる","たてもの","ほてる","らぶ"],
+ "💒": ["けっこんしき","あくてぃびてぃ","ちゃぺる","ろまんす"],
+ "🏛": ["れきしてきなたてもの","たてもの","れきしてきな"],
+ "⛪": ["きょうかい","たてもの","くりすちゃん","じゅうじか","しゅうきょう"],
+ "🕌": ["もすく","いすらむ","むすりむ","しゅうきょう"],
+ "🛕": ["ひんどぅーきょうじいん","ひんどぅーきょう","じいん","しゅうきょう"],
+ "🕍": ["しなごーぐ","ゆだやじん","ゆだやきょう","しゅうきょう","かいどう"],
+ "🕋": ["かあば","いすらむ","むすりむ","しゅうきょう"],
+ "⛩": ["じんじゃ","しゅうきょう","しんとう"],
+ "⌚": ["うでどけい","とけい"],
+ "📱": ["けいたいでんわ","けいたい","こみゅにけーしょん","もばいる","でんわ"],
+ "📲": ["ちゃくしんちゅう","やじるし","つうわ","けいたい","こみゅにけーしょん","もばいる","けいたいでんわ","じゅしん","でんわ"],
+ "💻": ["ぱそこん","のーとぱそこん","こんぴゅーたー","ぱーそなる"],
+ "⌨": ["きーぼーど","こんぴゅーたー"],
+ "🖥": ["ですくとっぷぱそこん","こんぴゅーたー","ですくとっぷ"],
+ "🖨": ["ぷりんたー","こんぴゅーたー"],
+ "🖱": ["3ぼたんまうす","3","ぼたん","こんぴゅーたー","まうす","さん"],
+ "🖲": ["とらっくぼーる","こんぴゅーたー"],
+ "🕹": ["じょいすてぃっく","えんたーていめんと","げーむ","びでおげーむ"],
+ "🗜": ["あっしゅく","つーる","けっかん"],
+ "💽": ["MD","ぱそこん","ひかりでぃすく","えんたーていめんと","みにでぃすく","こうがく"],
+ "💾": ["ふろっぴーでぃすく","こんぴゅーたー","でぃすく","ふろっぴー"],
+ "💿": ["CDでぃすく","ぶるーれい","CD","こんぴゅーたー","でぃすく","DVD","こうがく"],
+ "📀": ["DVD","ぶるーれい","CD","こんぴゅーたー","でぃすく","えんたーていめんと","こうがく"],
+ "📼": ["びでおてーぷ","えんたーていめんと","てーぷ","VHS","びでお","びでおかせっと"],
+ "📷": ["かめら","えんたーていめんと","びでお"],
+ "📸": ["ふらっしゅをたいたかめら","かめら","ふらっしゅ","びでお"],
+ "📹": ["びでおかめら","かめら","えんたーていめんと","びでお"],
+ "🎥": ["びでおかめら","あくてぃびてぃ","かめら","しねま","えんたーていめんと","えいが"],
+ "📽": ["えいしゃき","しねま","ごらく","ふぃるむ","えいが","ぷろじぇくたー","びでお"],
+ "🎞": ["ふぃるむのふれーむ","しねま","えんたーていめんと","ふぃるむ","ふれーむ","えいが"],
+ "📞": ["じゅわき","こみゅにけーしょん","でんわ","じゅしんき"],
+ "☎️": ["でんわ","けいたいでんわ"],
+ "📟": ["ぽけっとべる","こみゅにけーしょん","ぽけべる"],
+ "📠": ["FAX","こみゅにけーしょん; fAX"],
+ "📺": ["てれび","えんたーていめんと","TV","びでお"],
+ "📻": ["らじお","えんたーていめんと","びでお"],
+ "🎙": ["すたじおまいく","まいく","おんがく","すたじお"],
+ "🎚": ["ちょうせつばー","ちょうせつ","おんがく","ばー"],
+ "🎛": ["こんとろーるのぶ","こんとろーる","つまみ","おんがく"],
+ "⏱": ["すとっぷうぉっち","とけい"],
+ "⏲": ["たいまーとけい","とけい","たいまー"],
+ "⏰": ["めざましとけい","あらーむ","とけい"],
+ "🕰": ["おきどけい","とけい"],
+ "⏳": ["すなどけい","すな","たいまー"],
+ "⌛": ["すなどけい","すな","たいまー"],
+ "🧮": ["そろばん","けいさん","かうんと","しゅうけいひょう","すうがく"],
+ "📡": ["えいせいあんてな","あんてな","こみゅにけーしょん","ぱらぼらあんてな","えいせい"],
+ "🔋": ["でんち","ばってりー","でんし","だかえねるぎー"],
+ "🪫": ["ばってりーざんりょうしょう","ばってりー","でんし","ていえねるぎー"],
+ "🔌": ["こんせんと","でんき","ぷらぐ"],
+ "💡": ["でんきゅう","まんが","でんき","ひらめき","ひかり"],
+ "🔦": ["かいちゅうでんとう","でんき","ひかり","どうぐ","たいまつ"],
+ "🕯": ["ろうそく","ひかり"],
+ "🧯": ["しょうかき","しょうか","ひ","けす"],
+ "🗑": ["ごみばこ","ごみ","かん","びん"],
+ "🛢": ["どらむかん","どらむ","おいる"],
+ "🛒": ["しょっぴんぐかーと","かーと","しょっぴんぐ","とろりー"],
+ "💸": ["はねのはえたおさつ","ぎんこう","しへい","せいきゅうしょ","どる","とぶ","おかね","はね"],
+ "💵": ["どるさつ","ぎんこう","しへい","おさつ","つうか","どる","おかね"],
+ "💴": ["えんきごうのはいったこぎって","ぎんこう","しへい","おさつ","つうか","おかね","えん"],
+ "💶": ["ゆーろさつ","ぎんこう","しへい","おさつ","つうか","ゆーろ","おかね"],
+ "💷": ["ぽんどさつ","ぎんこう","しへい","おさつ","つうか","おかね","ぽんど"],
+ "💰": ["どるぶくろ","ばっぐ","どる","おかね"],
+ "🪙": ["こいん","きん","きんぞく","おかね","ぎん","たから"],
+ "💳": ["くれじっとかーど","ぎんこう","かーど","くれじっと","おかね"],
+ "🪪": ["みぶんしょうめいしょ","しかくじょうほう","ID","らいせんす","せきゅりてぃ"],
+ "🧾": ["りょうしゅうしょ","かいけい","ぼき","しょうこ","しょうめい"],
+ "💎": ["ほうせき","だいあもんど","じゅえる","ろまんす"],
+ "⚖": ["はかり","てんびん","こうせい","てんびんざ","ものさし","どうぐ","じゅうりょう","せいざ"],
+ "🦯": ["しろつえ","あくせしびりてぃ","めがふじゆう"],
+ "🧰": ["どうぐばこ","むね","せいびし","こうぐ"],
+ "🔧": ["れんち","どうぐ"],
+ "🪛": ["どらいばー","ねじ","こうぐ"],
+ "🔨": ["はんまー","どうぐ"],
+ "⚒": ["はんまーとつるはし","はんまー","つるはし","どうぐ"],
+ "🛠": ["はんまーとれんち","はんまー","どうぐ","れんち"],
+ "⛏": ["つるはし","さいくつ","どうぐ"],
+ "🪓": ["おの","たたきぎり","ておの","われる","もくざい","こうぐ"],
+ "🪚": ["もっこうようのこぎり","だいく","ざいもく","のこぎり","こうぐ"],
+ "🔩": ["なっととぼると","ぼると","なっと","どうぐ"],
+ "⚙": ["はぐるま","ぎあ","どうぐ"],
+ "⛓": ["くさり"],
+ "🪝": ["ふっく","わな","いかさま","ぺてん","ゆうわく","ふぃっしんぐ","つーる"],
+ "🪜": ["はしご","のぼる","よこぎ","だん","こうぐ"],
+ "🧱": ["れんが","ねんど","けんせつ","もるたる","かべ"],
+ "🪨": ["ろっく","いわ","けんぞうぶつ","おもい","こたい","いし"],
+ "🪵": ["もくざい","けんぞうぶつ","まるた","ざいもく","はた"],
+ "🔫": ["みずでっぽう","みず","ぴすとる","ふんしゃき","じゅう"],
+ "🧨": ["ばくちく","だいなまいと","かやく","はなび"],
+ "💣": ["ばくだん"],
+ "🔪": ["ほうちょう","きっちんないふ","ちょうり","ないふ"],
+ "🗡": ["たんけん","ないふ"],
+ "⚔": ["こうさしたけん","こうさ","けん"],
+ "🛡": ["たて"],
+ "🚬": ["きつえんまーく","あくてぃびてぃ","きつえん"],
+ "⚰": ["かん","し"],
+ "🪦": ["はかいし","ぼち","し","ぼ","はかば","はろうぃーん"],
+ "⚱": ["こつつぼ","し","そうぎ"],
+ "🏺": ["あんふぉら","みずがめざ","りょうり","のみもの","みずさし","どうぐ","せいざ"],
+ "🔮": ["すいしょうだま","たま","すいしょう","おとぎばなし","ふぁんたじー","うらない","どうぐ"],
+ "🪄": ["まほうのつえ","まほう","ぼう","まじょ","まほうつかい"],
+ "📿": ["じゅずじょうのいのりのようぐ","じゅず","いるい","ねっくれす","いのり","しゅうきょう"],
+ "🧿": ["なざーるのおまもり","じゅずだま","おまもり","よこしまし","なざーる","ごふ"],
+ "🪬": ["はむさ","おまもり","ふぁてぃま","て","めありー","みりあむ","ほご"],
+ "💈": ["りはつてんのかんばんばしら","りはつてん","とこや","さんぱつ","かんばんばしら"],
+ "🧲": ["じしゃく","あとらくしょん","ばてい"],
+ "⚗": ["じょうりゅうき","かがく","じっけん","どうぐ"],
+ "🧪": ["しけんかん","かがくしゃ","かがく","じっけん","じっけんしつ"],
+ "🧫": ["ぺとりさら","ばくてりあ","せいぶつがくしゃ","せいぶつがく","ぶんか","じっけんしつ"],
+ "🧬": ["DNA","せいぶつがくしゃ","しんか","いでんし","いでんしがく","せいめい"],
+ "🔭": ["ぼうえんきょう","つーる"],
+ "🔬": ["けんびきょう","つーる"],
+ "🕳": ["あな"],
+ "🩻": ["Xせん","ほね","いし","いりょう","こっかく"],
+ "💊": ["くすり","いし","ぴる","びょうき"],
+ "💉": ["ちゅうしゃき","いし","くすり","ちゅうしゃはり","ちゅうしゃ","びょうき","どうぐ","わくちん"],
+ "🩸": ["ち1てき","いし","くすり","けつえき","せいり"],
+ "🩹": ["がーぜつきばんそうこう","いし","くすり","ばんどえいど","ほうたい","ばんそうこう"],
+ "🩺": ["ちょうしんき","いし","くすり","しんぞう"],
+ "🌡": ["おんどけい","てんき","おんど"],
+ "🩼": ["まつばづえ","つえ","しょうがい","けが","いどうほじょ","ぼう"],
+ "🏷": ["らべる","にふだ"],
+ "🔖": ["ぶっくまーく","しおり","しるし"],
+ "🚽": ["といれ"],
+ "🪠": ["ぷらんじゃー","ふぉーすかっぷ","はいかんこう","きゅういん","といれ"],
+ "🚿": ["しゃわー","みず"],
+ "🛁": ["ばすたぶ","ふろ","よくそう"],
+ "🛀": ["ふろ","よくそう"],
+ "🪮": ["へあぴっく","あふろ","くし","かみ","ぴっく"],
+ "🪥": ["はぶらし","ばするーむ","ぶらし","きれい","はいしゃ","えいせい","は"],
+ "🪒": ["かみそり","するどい","ひげすり"],
+ "🧴": ["ろーしょんぼとる","ろーしょん","ほしめざい","しゃんぷー","ひやけとめ"],
+ "🧻": ["ぺーぱーろーる","ぺーぱーたおる","といれっとぺーぱー"],
+ "🧼": ["せっけん","ぼう","みずあび","くりーにんぐ","あわ","せっけんいれ"],
+ "🫧": ["ばぶる","げっぷ","きれい","せっけん","すいちゅう"],
+ "🧽": ["すぽんじ","きゅうしゅう","くりーにんぐ","たこうせい"],
+ "🧹": ["ほうき","くりーにんぐ","そうじ","まじょ"],
+ "🧺": ["ばすけっと","のうぎょう","らんどりー","ぴくにっく"],
+ "🪣": ["ばけつ","たる","ておけ","おおだる"],
+ "🔑": ["かぎ","じょう","ぱすわーど"],
+ "🗝": ["ふるいかぎ","かぎ","じょう","ふるい"],
+ "🪤": ["ねずみとりき","えさ","ねずみ","かじはどうぶつ","わなわ","わな"],
+ "🛋": ["そふぁーとらんぷ","そふぁー","ほてる","らんぷ"],
+ "🪑": ["いす","ざせき","すわる"],
+ "🛌": ["しゅくはくしせつ","ねる","ほてる","すいみん","べっど"],
+ "🛏": ["べっど","ほてる","すいみん"],
+ "🚪": ["どあ","とびら"],
+ "🪞": ["かがみ","はんしゃ","はんしゃたい","はんしゃきょう"],
+ "🪟": ["まど","わく","しんせんなくうき","がらす","かいこうぶ","とうめい","しかい"],
+ "🧳": ["てにもつ","ぱっきんぐ","りょこう","すーつけーす"],
+ "🛎": ["たくじょうべる","べる","ほてる"],
+ "🖼": ["がくにはいったしゃしん","あーと","がくぶち","びじゅつかん","かいが","しゃしん"],
+ "🧭": ["こんぱす","じしゃく","なびげーしょん","おりえんてーりんぐ"],
+ "🗺": ["せかいちず","ちず","せかい"],
+ "⛱": ["たてられたぱらそる","あめ","はれ","かさ","てんき"],
+ "🪭": ["おりたたみせんす","れいきゃく","えんりょがち","だんす","ふぁん","ふらったー","ねつ","あつい","うちき","ひろがる"],
+ "🗿": ["もやいぞう","もあいぞう","かお","ぞう"],
+ "🛍": ["かいものぶくろ","かばん","ほてる","かいもの"],
+ "🎈": ["ふうせん","あくてぃびてぃ","おいわい","えんたーていめんと"],
+ "🎏": ["こいのぼり","あくてぃびてぃ","こい","おいわい","えんたーていめんと","はた","ふきながし"],
+ "🎀": ["りぼん","おいわい"],
+ "🧧": ["あかいふうとう","ぎふと","こううん","ほんばお","らいしー","おかね"],
+ "🎁": ["ぷれぜんと","はこ","おいわい","えんたーていめんと","おくりもの","ほうそう"],
+ "🎊": ["くすだま","あくてぃびてぃ","おいわい","かみふぶき","えんたーていめんと"],
+ "🎉": ["くらっかー","あくてぃびてぃ","おいわい","えんたーていめんと","ぱーてぃー","じゃーん"],
+ "🪅": ["ぴにゃーた","おいわい","ぱーてぃー","ぴなーた"],
+ "🪩": ["みらーぼーる","だんす","でぃすこ","かがやき","ぱーてぃー"],
+ "🪆": ["いれこにんぎょう","にんぎょう","いれこ","ろしあ"],
+ "🎎": ["ひなまつり","あくてぃびてぃ","おいわい","にんぎょう","えんたーていめんと","まつり","にっぽん"],
+ "🎐": ["ふうりん","あくてぃびてぃ","かね","おいわい","えんたーていめんと","ふう"],
+ "🏮": ["いざかやのちょうちん","あかちょうちん","いざかや","にっぽん","ちょうちん","あかり","あか"],
+ "🪔": ["でぃやらんぷ","でぃや","らんぷ","おいる"],
+ "✉️": ["ふうとう","Eめーる","でんしめーる"],
+ "📩": ["めーるじゅしんちゅう","やじるし","こみゅにけーしょん","した","Eめーる","でんしめーる","ふうとう","てがみ","めーる","おくる","そうしん"],
+ "📨": ["めーるじゅしん","こみゅにけーしょん","Eめーる","でんしめーる","ふうとう","うけとる","てがみ","めーる","じゅしん"],
+ "📧": ["Eめーる","こみゅにけーしょん","でんしめーる","てがみ","めーる"],
+ "💌": ["らぶれたー","はーと","てがみ","あい","めーる","ろまんす"],
+ "📮": ["ぽすと","こみゅにけーしょん","めーる","ゆうびんうけ"],
+ "📪": ["はたがさがっていてとじているじょうたいのゆうびんうけ","とじる","こみゅにけーしょん","はた","さがった","めーる","ぽすと","ゆうびんうけ"],
+ "📫": ["はたがあがっていてとじているじょうたいのゆうびんうけ","とじる","こみゅにけーしょん","はた","めーる","ゆうびんうけ","ぽすと"],
+ "📬": ["はたがあがっていてひらいているじょうたいのゆうびんうけ","こみゅにけーしょん","はた","めーる","ぽすと","あける","ゆうびんうけ"],
+ "📭": ["はたがさがっていてひらいているゆうびんうけ","こみゅにけーしょん","はた","さげ","めーる","めーるぼっくす","あける","ゆうびんうけ"],
+ "📦": ["にもつ","はこ","こみゅにけーしょん","ぱっけーじ","こづつみ"],
+ "📯": ["ゆうびんらっぱ","こみゅにけーしょん","えんたーていめんと","かく","ぽすと","ゆうびん"],
+ "📥": ["じゅしんとれい","はこ","こみゅにけーしょん","てがみ","めーる","じゅしん","とれい"],
+ "📤": ["そうしんとれい","はこ","こみゅにけーしょん","てがみ","めーる","そうしん","とれい"],
+ "📜": ["まきもの","かみ"],
+ "📃": ["げんこう","かーる","どきゅめんと","ぺーじ"],
+ "📑": ["ぶっくまーくたぶ","ぶっくまーく","まーく","まーかー","たぶ"],
+ "📊": ["ぼうぐらふ","ばー","ちゃーと","ぐらふ"],
+ "📈": ["じょうしょうするぐらふ","じょうしょうちゃーと","ちゃーと","ぐらふ","せいちょう","とれんど","うわむき"],
+ "📉": ["かこうするぐらふ","かこうちゃーと","ちゃーと","うえ","ぐらふ","とれんど"],
+ "📄": ["ぶんしょ","ぺーじ"],
+ "📅": ["かれんだー","ひづけ"],
+ "📆": ["ひめくりかれんだー","かれんだー"],
+ "🗓": ["りんぐかれんだー","かれんだー","ぱっど","らせんじょう"],
+ "📇": ["めいしふぉるだ","かーど","さくいん","ろーらでっくす"],
+ "🗃": ["かーどふぁいる","はこ","かーど","ふぁいる"],
+ "🗳": ["とうひょうようしととうひょうばこ","とうひょうようし","はこ","ひょう","とうひょう"],
+ "🗄": ["ふぁいるしゅうのうこ","しゅうのう","ふぁいる"],
+ "📋": ["くりっぷぼーど"],
+ "🗒": ["りんぐのーと","のーと","ぱっど","らせんじょう"],
+ "📁": ["ふぉるだ","ふぁいる"],
+ "📂": ["ひらいたふぉるだ","ふぁいる","ふぉるだ","ひらいた"],
+ "🗂": ["しきりかーど","かーど","しきり","さくいん"],
+ "🗞": ["まるめたしんぶん","にゅーす","しんぶん","かみ","まるめた"],
+ "📰": ["しんぶん","こみゅにけーしょん","にゅーす","かみ"],
+ "🪧": ["ぷらかーど","でも","しがらみ","こうぎ","かんばん"],
+ "📓": ["のーと"],
+ "📕": ["とじたほん","ほん","とじている"],
+ "📗": ["みどりいろのほん","ほん","みどり"],
+ "📘": ["あおいほん","あお","ほん"],
+ "📙": ["おれんじいろのほん","ほん","おれんじ"],
+ "📔": ["そうしょくかばーののーと","ほん","かばー","そうしょく","のーと"],
+ "📒": ["ちょうぼ","もとちょう","のーと"],
+ "📚": ["しょせき","ほん"],
+ "📖": ["ひらいたほん","ほん","ひらいた"],
+ "🔗": ["りんく"],
+ "📎": ["くりっぷ","ぺーぱーくりっぷ"],
+ "🖇": ["つながったぺーぱーくりっぷ","こみゅにけーしょん","りんく","ぺーぱーくりっぷ"],
+ "✂️": ["はさみ","どうぐ"],
+ "📐": ["さんかくじょうぎ","じょうぎ","はいち","さんかく"],
+ "📏": ["じょうぎ","ちょくじょうぎ"],
+ "📌": ["がびょう","ぴん"],
+ "📍": ["がびょう","ぴん"],
+ "🧷": ["あんぜんぴん","おむつ","ぱんくろっく"],
+ "🪡": ["ぬいはり","ししゅう","さいほう","ぬいめ","ほうごう","したて"],
+ "🧵": ["すれっど","ぬいあみ","さいほう","いとまき","いと","しゅこうげい"],
+ "🧶": ["いと","ぼーる","かぎばりあみ","にっと","しゅこうげい"],
+ "🪢": ["むすびめ","ろーぷ","からんだ","ひも","よりいと","ねじれ"],
+ "🔐": ["こいんろっかー","しまっている","かぎ","せじょう","ぼうはん"],
+ "🔒": ["かぎ","とじられた","せじょう"],
+ "🔓": ["かいじょう","せじょう","あける"],
+ "🔏": ["じょうまえとぺん","いんく","じょう","ぺんさき","ぺん","ぷらいばしー"],
+ "🖊": ["ひだりしたむきのぼーるぺん","ぼーるぺん","こみゅにけーしょん","ぺん"],
+ "🖋": ["ひだりしたむきのまんねんひつ","こみゅにけーしょん","まんねんひつ","ぺん"],
+ "✒️": ["ぺんさき","ぺん"],
+ "📝": ["めも","こみゅにけーしょん","えんぴつ"],
+ "✏️": ["えんぴつ"],
+ "🖍": ["ひだりしたむきのくれよん","こみゅにけーしょん","くれよん"],
+ "🖌": ["ひだりしたむきのぶらし","こみゅにけーしょん","ぺいんとぶらし","え"],
+ "🔍": ["ひだりむきむしめがね","めがね","かくだい","けんさく","つーる"],
+ "🔎": ["みぎむきむしめがね","めがね","かくだい","けんさく","つーる"],
+ "❤️": ["あかいろのはーと","はーと"],
+ "🧡": ["おれんじいろのはーと","はーと","おれんじいろ"],
+ "💛": ["きいろのはーと","はーと","きいろ"],
+ "💚": ["みどりのはーと","はーと","みどり"],
+ "💙": ["あおのはーと","はーと","あお"],
+ "💜": ["むらさきのはーと","はーと","むらさき"],
+ "🤎": ["ちゃいろのはーと","はーと","ちゃいろ"],
+ "🖤": ["くろいはーと","はーと","くろ","あく","わるもの"],
+ "🤍": ["しろのはーと","はーと","しろ"],
+ "💔": ["われたはーと","はーと","こわれる","はきょく"],
+ "❣": ["はーとのびっくりまーく","はーと","びっくりまーく","きごう"],
+ "💕": ["2つのはーと","はーと","あい"],
+ "💞": ["かいてんするはーと","はーと","かいてん"],
+ "💓": ["こどうするはーと","はーと","こどう","どきどき"],
+ "💗": ["ひかるはーと","はーと","わくわく","ひかる","こどう","きんちょう"],
+ "💖": ["きらめくはーと","はーと","わくわく","きらきら"],
+ "💘": ["いぬかれたはーと","はーと","や","きゅーぴっど","ろまんす"],
+ "💝": ["りぼんつきのはーと","はーと","りぼん","ばれんたいん"],
+ "❤️‍🔥": ["もえているはーと","はーと","ひ","もえる","あい","ねつじょう","しんせいなはーと"],
+ "❤️‍🩹": ["てあてしているはーと","はーと","けんこうになる","かいぜんしている","てあてしている","かいふくしている","やみあがり","げんき"],
+ "💟": ["はーとのでこれーしょん","はーと"],
+ "☮": ["ぴーすまーく","へいわ"],
+ "✝": ["らてんじゅうじ","くりすちゃん","じゅうじか","しゅうきょう"],
+ "☪": ["ほしとみかづき","いすらむ","むすりむ","しゅうきょう"],
+ "🕉": ["おーむまーく","ひんどぅーきょう","おーむ","しゅうきょう"],
+ "☸": ["ほうりん","ぶっきょうと","だーま","しゅうきょう"],
+ "✡": ["だびでのほし","だびで","ゆだやじん","ゆだやきょう","しゅうきょう","ほし"],
+ "🔯": ["ろくぼうせい","うらない","ほし"],
+ "🕎": ["はぬっきーやー","しょくだい","めのーらー","しゅうきょう"],
+ "☯": ["いんよう","しゅうきょう","どう","どうか","ひ","かげ"],
+ "☦": ["はったんじゅうじか","くりすちゃん","じゅうじか","しゅうきょう"],
+ "🪯": ["かんだ","しゅうきょう","しーくきょうと"],
+ "🛐": ["れいはいしょ","しゅうきょう","れいはい"],
+ "⛎": ["へびつかいざ","うんぱんにん","へび","せいざ"],
+ "♈": ["おひつじざ","こひつじ","せいざ"],
+ "♉": ["おうしざ","おすうし","ゆううし","せいざ"],
+ "♊": ["ふたござ","ふたご","せいざ"],
+ "♋": ["がん","かにざ","かに","せいざ"],
+ "♌": ["ししざ","らいおん","せいざ"],
+ "♍": ["おとめざ","おとめ","しょじょ","せいざ"],
+ "♎": ["てんびんざ","てんびん","こうせい","はかり","せいざ"],
+ "♏": ["さそりざ","さそり","せいざ"],
+ "♐": ["いてざ","しゃしゅ","しゃしゅざ","せいざ"],
+ "♑": ["やぎざ","やぎ","せいざ"],
+ "♒": ["みずがめざ","うんぱんじん","みず","せいざ"],
+ "♓": ["うおざ","さかな","せいざ"],
+ "🆔": ["しかくかこみID","ID","しきべつ"],
+ "⚛": ["げんそきごう","むしんろんしゃ","げんし"],
+ "⚕️": ["あすくれぴおすのつえ","けんこう","せわ","いし","くすり","つえ","へび"],
+ "☢": ["ほうしゃのうひょうしき","ほうしゃのう"],
+ "☣": ["ばいおはざーどひょうしき","せいぶつさいがい"],
+ "📴": ["けいたいでんわでんげんおふ","けいたい","こみゅにけーしょん","もばいる","おふ","けいたいでんわ","でんわ"],
+ "📳": ["まなーもーど","けいたい","こみゅにけーしょん","もばいる","もーど","けいたいでんわ","でんわ","ばいぶれーしょん"],
+ "🈶": ["しかくかこみゆう","にほんご","あり"],
+ "🈚": ["しかくかこみむ","しかくかこみいな","にほんご","なし"],
+ "🈸": ["しかくかこみしん","しかくかこみてき","ちゅうごくご","しんせい"],
+ "🈺": ["しかくかこみえい","ちゅうごくご","えいぎょう"],
+ "🈷️": ["しかくかこみつき","にほんご","つきぎめ"],
+ "✴️": ["はちりょうぼし","ほし"],
+ "🆚": ["しかくかこみVS","たい","VS"],
+ "🉑": ["まるかこみきょか","まるかこみか","ちゅうごくご","かのう"],
+ "💮": ["しろいはな","はな","たいへんよくできました"],
+ "🉐": ["まるかこみとく","にほんご","とく"],
+ "㊙️": ["まるかこみひ","ちゅうごくご","ひょういもじ","ひ"],
+ "㊗️": ["まるかこみしゅく","ちゅうごくご","おめでとう","しゅく"],
+ "🈴": ["しかくかこみのごう","しかくかこみごう","ちゅうごくご","ごうかく","てきごう"],
+ "🈵": ["しかくかこみまん","ちゅうごくご","まんしつ","まんしゃ","まんたん"],
+ "🈹": ["しかくかこみわり","しかくかこみのわり","にほんご","わりびき"],
+ "🈲": ["しかくかこみきん","にほんご","きんし"],
+ "🅰️": ["くろしかくかこみA","A","けつえきがた"],
+ "🅱️": ["くろしかくかこみB","B","けつえきがた"],
+ "🆎": ["くろしかくかこみAB","AB","けつえきがた"],
+ "🆑": ["しかくかこみCL","CL"],
+ "🅾️": ["くろしかくかこみO","けつえきがた","O"],
+ "🆘": ["しかくかこみSOS","へるぷ","SOS"],
+ "⛔": ["たちいりきんし","たちいり","きんし","だめ","できない","きんじる","こうつう"],
+ "📛": ["なふだ","ばっじ","なまえ"],
+ "🚫": ["しんにゅうきんし","たちいり","きんし","だめ","できない","きんじる"],
+ "❌": ["ばつしるし","きゃんせる","きごう","かけざん","じょうざん","x"],
+ "⭕": ["ふといおおきなまる","まる","O"],
+ "💢": ["いかりまーく","いかり","まんが","げきど"],
+ "♨️": ["おんせん","あたたかい","わきでる","じょうき"],
+ "🚷": ["ほこうしゃたちいりきんし","きんし","だめ","ない","ほこうしゃ","きんじる"],
+ "🚯": ["ぽいすてきんし","きんし","ごみ","だめ","ない","きんしされている"],
+ "🚳": ["じてんしゃきんし","じてんしゃ","ばいく","きんし","だめ","できない","きんじる","のりもの"],
+ "🚱": ["いんようふか","ひいんりょうすい","いんりょう","きんし","だめ","ない","いんよう","きんしされている","みず"],
+ "🔞": ["18さいみまんきんし","18","ねんれいせいげん","じゅうはち","きんし","だめ","ない","きんしした","みせいねんしゃ"],
+ "📵": ["けいたいでんわきんし","けいたい","つうしん","きんし","もばいる","だめ","できない","けいたいでんわ","きんしされている","でんわ"],
+ "🚭": ["きんえん","きんし","だめ","できない","きんしされている","きつえん"],
+ "❗": ["あかいびっくりまーく","びっくり","まーく","きごう"],
+ "❕": ["しろいびっくりまーく","びっくり","まーく","かこみ","きごう"],
+ "❓": ["あかいはてなまーく","まーく","きごう","はてな"],
+ "❔": ["しろいはてなまーく","まーく","かこみ","きごう","はてな"],
+ "‼️": ["!!まーく","ばんばん","びっくり","まーく","きごう"],
+ "⁉️": ["!?","びっくり","いんてろばんぐ","まーく","きごう","はてな"],
+ "💯": ["100てん","100","ふる","ひゃく","すこあ"],
+ "🔅": ["ていきど","あかるさ","うすぐらい","てい"],
+ "🔆": ["こうきど","あかるい","あかるさ"],
+ "🔱": ["とらいでんと","いかり","えんぶれむ","ふね","こうぐ"],
+ "⚜": ["ゆりのもんしょう"],
+ "〽️": ["いおりてん","しるし","ぶぶん"],
+ "⚠️": ["けいこく"],
+ "🚸": ["こうさてんをわたるこどもたち","こども","こうさてん","ほこうしゃ","こうつう"],
+ "🔰": ["しょしんしゃまーく","しょしんしゃ","まーく","みどり","にっぽん","わかば","どうぐ","き"],
+ "♻️": ["りさいくるまーく","りさいくる"],
+ "🈯": ["しかくかこみゆび","にほんご"],
+ "💹": ["じょうしょうとれんどのちゃーととえんきごう","じょうしょうちゅうえんちゃーと","ぎんこう","ちゃーと","つうか","ぐらふ","せいちょう","しじょう","おかね","じょうしょう","とれんど","うわむき","えん"],
+ "❇️": ["きらきら"],
+ "✳️": ["あすたりすく (8ほんこうせい)","あすたりすく"],
+ "❎": ["しかくでかこまれたばつしるし","まーく","しかく"],
+ "✅": ["しろいふとじのちぇっくまーく","ちぇっく","まーく"],
+ "💠": ["どっともようのだいや","まんが","だいやもんど","きかがく","ないぶ"],
+ "🌀": ["さいくろん","ていきあつ","めまい","たつまき","たいふう","てんき"],
+ "➿": ["にじゅうのかーるじょうのるーぷ","かーる","だぶる","るーぷ"],
+ "🌐": ["しごせん・けいせんのあるちきゅう","ちきゅう","ちきゅうぎ","けいせん","せかい"],
+ "♾": ["むげん","えいえん","ふへんてき"],
+ "Ⓜ️": ["まるかこみM","えん","M"],
+ "🏧": ["ATM","ATMきごう","じどう","ぎんこう","すいとう"],
+ "🚾": ["といれ","けしょうしつ","おてあらい","みず","WC"],
+ "♿": ["くるまいす","あくせす"],
+ "🅿️": ["くろしかくかこみP","ちゅうしゃじょう"],
+ "🈳": ["しかくかこみそら","しかくかこみのそら","ちゅうごくご","そらしつ","あき","くうしゃ"],
+ "🈂️": ["しかくかこみさ","にっぽんじん","さーびす"],
+ "🛂": ["にゅうこくしんさ","ぱすぽーと"],
+ "🛃": ["ぜいかん"],
+ "🛄": ["てにもつうけとりしょ","てにもつ","うけとり"],
+ "🛅": ["てにもつあずかりしょ","てにもつ","ろっかー","けいこうひん"],
+ "🚰": ["いんりょうすい","のみもの","みず"],
+ "🛗": ["えれべーたー","あくせしびりてぃ","ひきあげ","しょうこうき"],
+ "🚹": ["だんせいのきごう","だんせいよう","といれ","おとこ","だんせい"],
+ "♂️": ["だんせいきごう","だんせい","おとこ"],
+ "🚺": ["じょせいのきごう","じょせいよう","といれ","おんな","じょせい"],
+ "♀️": ["じょせいきごう","じょせい","おんな"],
+ "⚧️": ["とらんすじぇんだーさいん","とらんすじぇんだー","ぷらいど","lgbt"],
+ "🚼": ["あかちゃんまーく","あかちゃん","おむつかえ"],
+ "🚻": ["といれ","けしょうしつ","WC"],
+ "🚮": ["ごみすてじょう","びんのごみすてじょう","ごみ","ごみばこ"],
+ "🎦": ["えいが","あくてぃびてぃ","かめら","えんたーていめんと","ふぃるむ","どうが"],
+ "📶": ["あんてな","ばー","けいたい","こみゅにけーしょん","もばいる","けいたいでんわ","しぐなる","でんわ"],
+ "🛜": ["むせん","こんぴゅーた","いんたーねっと","ねっとわーく","Wi-Fi","せつぞく"],
+ "🈁": ["しかくかこみここ","にっぽんじん"],
+ "🆖": ["しかくかこみNG","NG"],
+ "🆗": ["しかくかこみOK","OK"],
+ "🆙": ["しかくかこみUP!","まーく","うえ"],
+ "🆒": ["COOL","かっこいい","くーる"],
+ "🆕": ["しかくかこみnew","しん"],
+ "🆓": ["しかくかこみFREE","ふりー","むりょう"],
+ "0️⃣": ["0きー","0","きー","ぜろ"],
+ "1️⃣": ["1きー","いち","きー"],
+ "2️⃣": ["2きー","2","きー","に"],
+ "3️⃣": ["3きー","3","きー","さん"],
+ "4️⃣": ["4きー","4","よん","きー"],
+ "5️⃣": ["5きー","5","ご","きー"],
+ "6️⃣": ["6きー","6","きー","ろく"],
+ "7️⃣": ["7きー","7","きー","なな"],
+ "8️⃣": ["8きー","8","はち","きー"],
+ "9️⃣": ["9きー","9","きー","きゅう"],
+ "🔟": ["10きー","10","きー","じゅう"],
+ "🔢": ["ばんごうのにゅうりょくきごう","1234","にゅうりょく","すうじ"],
+ "▶️": ["みぎむきさんかく","さいせいぼたん","やじるし","さいせい","みぎ","さんかっけい"],
+ "⏸": ["2ほんのすいちょくばー","いちじていしぼたん","ばー","2ばい","いちじていし","すいちょく"],
+ "⏯": ["みぎむきのさんかっけいとにじゅうすいちょくぼう","さいせいまたはいちじていしぼたん","やじるし","いちじていし","さいせい","みぎ","さんかっけい"],
+ "⏹": ["ていし","ていしぼたん","しかく"],
+ "⏺": ["ろくが","ろくがぼたん","まる"],
+ "⏏️": ["とりだしまーく","とりだしぼたん"],
+ "⏭": ["みぎむきのにじゅうさんかっけいとすいちょくぼう","「つぎのきょく」ぼたん","やじるし","つぎのばめん","つぎのきょく","さんかっけい"],
+ "⏮": ["ひだりむきのにじゅうさんかっけいとすいちょくぼう","「まえのきょく」ぼたん","やじるし","まえのばめん","まえのきょく","さんかっけい"],
+ "⏩": ["みぎむきのにじゅうさんかっけい","はやおくりぼたん","やじるし","2ばい","こうそく","すすむ"],
+ "⏪": ["ひだりむきのにじゅうさんかっけい","はやもどしぼたん","やじるし","2ばい","まきもどし"],
+ "🔀": ["ねじりみぎむきやじるしのえもじ","しゃっふる","やじるし","こうさ"],
+ "🔁": ["りぴーと","りぴーとぼたん","やじるし","とけいまわり"],
+ "🔂": ["1きょくをりぴーとさいせい","りぴーとぼたん","やじるし","とけいまわり","いちど"],
+ "◀️": ["ひだりむきのさんかっけい","はんてんぼたん","やじるし","ひだり","はんてん","さんかっけい"],
+ "🔼": ["うわむきのさんかっけい","うえぼたん","やじるし","ぼたん","うえ"],
+ "🔽": ["したむきのさんかっけい","したぼたん","やじるし","ぼたん","した"],
+ "⏫": ["うわむきのにじゅうさんかっけい","こうそくじょうしょうぼたん","やじるし","だぶる","うえ"],
+ "⏬": ["したむきのにじゅうさんかっけい","こうそくだうんぼたん","やじるし","だぶる","した"],
+ "➡️": ["みぎむきやじるし","みぎやじるし","やじるし","しゅよう","ほうこう","ひがし"],
+ "⬅️": ["ひだりむきやじるし","ひだりやじるし","やじるし","しゅよう","ほうこう","にし"],
+ "⬆️": ["うわむきやじるし","うえやじるし","やじるし","しゅよう","ほうこう","きた"],
+ "⬇️": ["したむきやじるし","したやじるし","やじるし","しゅよう","ほうこう","した","みなみ"],
+ "↗️": ["みぎうえやじるし","やじるし","ほうこう","ななめ","ほくとう"],
+ "↘️": ["みぎしたやじるし","やじるし","ほうこう","ななめ","なんとう"],
+ "↙️": ["ひだりしたやじるし","やじるし","ほうこう","ななめ","なんせい"],
+ "↖️": ["ひだりうえやじるし","やじるし","ほうこう","ななめ","ほくせい"],
+ "↕️": ["じょうげやじるし","やじるし","ほうこう","ななめ","ほくせい"],
+ "↔️": ["さゆうやじるし","やじるし"],
+ "🔄": ["うずまきやじるし","はんとけいまわり","やじるし","ひだりまわり"],
+ "↪️": ["みぎむきだんつきやじるし","みぎにまがったやじるし","やじるし"],
+ "↩️": ["ひだりむきだんつきやじるし","ひだりにまがったやじるし","やじるし"],
+ "🔃": ["るーぷやじるし","とけいのはり","やじるし","とけいまわり","りろーど"],
+ "⤴️": ["みぎうえへかーぶするやじるし","うえへかーぶするみぎやじるし","やじるし"],
+ "⤵️": ["みぎしたへかーぶするやじるし","したにかーぶするみぎやじるし","やじるし","した"],
+ "#️⃣": ["#きー","はっしゅ","きー","ぽんど"],
+ "*⃣": ["あすたりすくきー","あすたりすく","きー","ほし"],
+ "ℹ️": ["じょうほうげん","i","いんふぉめーしょん"],
+ "🔤": ["あるふぁべっとにゅうりょく","abc","あるふぁべっと","にゅうりょく","らてん","もじ"],
+ "🔡": ["あるふぁべっとこもじにゅうりょく","abcd","にゅうりょく","らてん","もじ","こもじ"],
+ "🔠": ["あるふぁべっとおおもじにゅうりょく","にゅうりょく","らてん","もじ","おおもじ"],
+ "🔣": ["きごうにゅうりょく","にゅうりょく"],
+ "🎵": ["おんぷ","あくてぃびてぃ","えんたーていめんと","おんがく"],
+ "🎶": ["ふくすうのおんぷ","あくてぃびてぃ","えんたーていめんと","おんがく","おんぷ"],
+ "〰️": ["はせん","だっしゅ","きごう","なみ"],
+ "➰": ["かーるじょうのるーぷ","かーる","るーぷ"],
+ "✔️": ["ふとじのちぇっくまーく","ちぇっく","まーく"],
+ "➕": ["ふとじの+きごう","すうがく","ぷらす"],
+ "➖": ["ふとじのまいなすきごう","すうがく","まいなす"],
+ "➗": ["ふとじのわるきごう","わりざん","すうがく"],
+ "✖️": ["ふとじのかけるしるし","きゃんせる","じょうざん","かける","x"],
+ "🟰": ["ふといとうごう","とうしき","すうがく","ひとしい"],
+ "💲": ["ふとじのどるきごう","つうか","どる","おかね"],
+ "💱": ["がいかりょうがえ","ぎんこう","つうか","りょうがえ","おかね"],
+ "©️": ["こぴーらいとまーく","ちょさくけん"],
+ "®️": ["とうろくしょうひょうまーく","とうろくずみ","しょうひょう"],
+ "™️": ["しょうひょうまーく","まーく","tm","しょうひょう"],
+ "🔚": ["ENDとひだりやじるし","やじるし","はじ"],
+ "🔙": ["BACKとひだりやじるし","やじるし","もどる"],
+ "🔛": ["ON!とさゆうやじるし","やじるし","まーく","おん"],
+ "🔝": ["TOPとうえやじるし","やじるし","とっぷ","うえ"],
+ "🔜": ["SOONとみぎやじるし","やじるし","まもなく"],
+ "☑️": ["ちぇっくいりちぇっくぼっくす","とうひょう","ぼっくす","ちぇっく"],
+ "🔘": ["らじおぼたん","ぼたん","きかがく","らじお"],
+ "🔴": ["あかまる","えん","きかがく","あか"],
+ "🟠": ["おれんじいろのえん","えん","きかがく","おれんじ"],
+ "🟡": ["きいろのまる","えん","きかがく","ちゃいろ"],
+ "🟢": ["みどりまる","えん","きかがく","みどり"],
+ "🔵": ["あおまる","あお","えん","きかがく"],
+ "🟣": ["むらさきのまる","えん","きかがく","むらさき"],
+ "🟤": ["ちゃいろのまる","えん","きかがく","ちゃいろ"],
+ "⚫": ["くろまる","えん","きかがく"],
+ "⚪": ["しろまる","えん","きかがく"],
+ "🟥": ["あかのせいほうけい","せいほうけい","きかがく","あか"],
+ "🟧": ["おれんじしょくのせいほうけい","せいほうけい","きかがく","おれんじ"],
+ "🟨": ["きいろのせいほうけい","せいほうけい","きかがく","きいろ"],
+ "🟩": ["みどりのせいほうけい","せいほうけい","きかがく","みどり"],
+ "🟦": ["あおのせいほうけい","せいほうけい","きかがく","あお"],
+ "🟪": ["むらさきのせいほうけい","せいほうけい","きかがく","むらさき"],
+ "🟫": ["ちゃいろのせいほうけい","せいほうけい","きかがく","ちゃいろ"],
+ "⬛": ["くろいおおきなしかく","きかがく","せいほうけい"],
+ "⬜": ["しろいおおきなしかく","きかがく","せいほうけい"],
+ "◼️": ["くろいちゅうくらいのしかく","きかがく","せいほうけい"],
+ "◻️": ["しろくてちゅうくらいのしかく","きかがく","せいほうけい"],
+ "◾": ["くろくてちゅうくらいのちいさいしかく","きかがく","せいほうけい"],
+ "◽": ["しろいちゅうくらいのちいさなしかく","きかがく","せいほうけい"],
+ "▪️": ["くろいちいさなしかく","きかがく","せいほうけい"],
+ "▫️": ["しろいちいさなしかく","きかがく","せいほうけい"],
+ "🔸": ["ちいさいおれんじのだいやもんど","だいやもんど","きかがく","おれんじ"],
+ "🔹": ["ちいさくてあおいだいやもんど","あお","だいやもんど","きかがく"],
+ "🔶": ["おおきいおれんじのだいや","だいやもんど","きかがく","おれんじ"],
+ "🔷": ["おおきくてあおいだいやもんど","あお","だいやもんど","きかがく"],
+ "🔺": ["うわむきのあかいさんかっけい","うえ","きかがく","あか"],
+ "🔻": ["したむきのさんかっけい","だうん","きかがく","あか"],
+ "🔲": ["くろいしかくぼたん","ぼたん","きかがく","せいほうけい"],
+ "🔳": ["しろいしかくぼたん","ぼたん","きかがく","かこみ","しかく"],
+ "🔈": ["すぴーかー","おんりょう"],
+ "🔉": ["おんりょうしょう","でんげんがはいったすぴーかー","ひくい","すぴーかー","おんりょう","なみ"],
+ "🔊": ["おんりょうだい","だいおんりょうのすぴーかー","3","えんたーていめんと","たかい","おとのおおきい","すぴーかー","ぼりゅーむ"],
+ "🔇": ["むおんのすぴーかー","すぴーかー","おふ","みゅーと","せいおん","むおん","おんりょう"],
+ "📣": ["めがほん","おうえん","こみゅにけーしょん","かくせいき"],
+ "📢": ["かくせいき","こみゅにけーしょん","おおごえ","すぴーかー","ぱぶりっくあどれす","めがほん"],
+ "🔔": ["べる"],
+ "🔕": ["みゅーと","すらっしゅべる","かね","きんじられた","だめ","ない","きんし","しずか"],
+ "🃏": ["とらんぷのじょーかー","かーど","えんたーていめんと","げーむ","じょーかー","ぷれい"],
+ "🀄": ["まーじゃんぱいのちゅう","げーむ","まーじゃん","あか"],
+ "♠️": ["とらんぷのすぺーど","かーど","げーむ","すぺーど","すーつ"],
+ "♣️": ["とらんぷのくらぶ","かーど","くらぶ","げーむ","すーつ"],
+ "♥️": ["とらんぷのはーと","かーど","げーむ","はーと","すーつ"],
+ "♦️": ["とらんぷのだいや","かーど","だいや","だいやもんど","げーむ","すーつ"],
+ "🎴": ["はなふだ","あくてぃびてぃ","かーど","えんたーていめんと","はな","げーむ","にっぽん","ぷれい"],
+ "👁‍🗨": ["ふきだしのめ","ふきだし","め","すぴーち","しょうにん"],
+ "🗨": ["ひだりむきのふきだし","せりふ","すぴーち"],
+ "💭": ["かんがえふきだし","ふきだし","あわ","まんが","かんがえ"],
+ "🗯": ["みぎむきのいかりのふきだし","いかり","ふきだし","あわ","げきど"],
+ "💬": ["ふきだし","あわ","まんが","せりふ","すぴーち"],
+ "🕐": ["1じ","0ふん","1","とけい","とき","いち"],
+ "🕑": ["2じ","0ふん","2","とけい","とき","に"],
+ "🕒": ["3じ","0ふん","3","とけい","とき","さん"],
+ "🕓": ["4じ","0ふん","4","とけい","よん","とき"],
+ "🕔": ["5じ","0ふん","5","とけい","ご","とき"],
+ "🕕": ["6じ","0ふん","6","とけい","とき","ろく"],
+ "🕖": ["7じ","0ふん","7","とけい","とき","なな"],
+ "🕗": ["8じ","0ふん","8","とけい","はち","とき"],
+ "🕘": ["9じ","0ふん","9","とけい","きゅう","とき"],
+ "🕙": ["10じ","0ふん","10","とけい","とき","じゅう"],
+ "🕚": ["11じ","0ふん","11","とけい","じゅういち","とき"],
+ "🕛": ["12じ","0ふん","12","とけい","じゅうに","とき"],
+ "🕜": ["1じはん","1じ","はん","じこく","いち","30"],
+ "🕝": ["2じはん","2じ","はん","じこく","30","に"],
+ "🕞": ["3じはん","3じ","はん","じこく","30","さん"],
+ "🕟": ["4じはん","30","4じ","じこく","よん","はん"],
+ "🕠": ["5じはん","30","5じ","じこく","ご","はん"],
+ "🕡": ["6じはん","30","6じ","じこく","ろく","はん"],
+ "🕢": ["7じはん","30","7じ","じこく","なな","はん"],
+ "🕣": ["8じはん","30","8じ","じこく","はち","はん"],
+ "🕤": ["9じはん","30","9じ","じこく","きゅう","はん"],
+ "🕥": ["10じはん","10じ","はん","じこく","じゅう","30"],
+ "🕦": ["11じはん","11じ","はん","じこく","じゅういち","30"],
+ "🕧": ["12じはん","12じ","はん","じこく","30","じゅうに"],
+ "🏳": ["なびくしろはた","はた","なびく"],
+ "🏴": ["なびくくろはた","はた","なびく"],
+ "🏁": ["ちぇっかーふらっぐ","いちまつもよう","はた","れーす"],
+ "🚩": ["さんかくはた","はた","ぽすと"],
+ "🎌": ["こうさき","あくてぃびてぃ","おいわい","こうさ","こうさした","はた","にっぽん"],
+ "🏴‍☠️": ["かいぞくはた","はた","かいぞく"],
+ "🏳️‍🌈": ["れいんぼーふらっぐ","ふらっぐ","れいんぼー","ぷらいど","lgbt"],
+ "🏳️‍⚧️": ["とらすじぇんだーふらっぐ","ふらっぐ","とらんすじぇんだー","ぷらいど","lgbt"],
+ "🇦🇨": ["あせんしょんとうのはた","あせんしょん","こっき","しま"],
+ "🇦🇩": ["あんどらこっき","あんどら","こっき"],
+ "🇦🇪": ["あらぶしゅちょうこくれんぽうこっき","しゅちょうこく","こっき","あらぶしゅちょうこくれんぽう","れんぽう"],
+ "🇦🇫": ["あふがにすたんこっき","あふがにすたん","こっき"],
+ "🇦🇬": ["あんてぃぐあばーぶーだこっき","あんてぃぐあ","ばーぶーだ","こっき"],
+ "🇦🇮": ["あんぎらとうのはた","あんぎらとう","こっき"],
+ "🇦🇱": ["あるばにあこっき","あるばにあ","こっき"],
+ "🇦🇲": ["あるめにあこっき","あるめにあ","こっき"],
+ "🇦🇴": ["あんごらこっき","あんごら","こっき"],
+ "🇦🇶": ["なんきょくたいりくのはた","なんきょくたいりく","こっき"],
+ "🇦🇷": ["あるぜんちんこっき","あるぜんちん","こっき"],
+ "🇦🇸": ["あめりかりょうさもあのはた","あめりかりょう","こっき","さもあ"],
+ "🇦🇹": ["おーすとりあこっき","おーすとりあ","こっき"],
+ "🇦🇺": ["おーすとらりあこっき","おーすとらりあ","こっき","はーど","まくどなるど"],
+ "🇦🇼": ["あるばこっき","あるば","こっき"],
+ "🇦🇽": ["おーらんどしょとうのはた","おーらんどしょとう","こっき"],
+ "🇦🇿": ["あぜるばいじゃんこっき","あぜるばいじゃん","こっき"],
+ "🇧🇦": ["ぼすにあへるつぇごびなこっき","ぼすにあ","こっき","へるつぇごびな"],
+ "🇧🇧": ["ばるばどすこっき","ばるばどす","こっき"],
+ "🇧🇩": ["ばんぐらでしゅこっき","ばんぐらでしゅ","こっき"],
+ "🇧🇪": ["べるぎーこっき","べるぎー","こっき"],
+ "🇧🇫": ["ぶるきなふぁそこっき","ぶるきなふぁそ","こっき"],
+ "🇧🇬": ["ぶるがりあこっき","ぶるがりあ","こっき"],
+ "🇧🇭": ["ばーれーんこっき","ばーれーん","こっき"],
+ "🇧🇮": ["ぶるんじこっき","ぶるんじ","こっき"],
+ "🇧🇯": ["べなんこっき","べなん","こっき"],
+ "🇧🇱": ["さん・ばるてるみーとうのはた","ばるてるみー","こっき","さん"],
+ "🇧🇲": ["ばみゅーだしょとうのはた","ばみゅーだしょとう","こっき"],
+ "🇧🇳": ["ぶるねいこっき","ぶるねい","だるさらーむ","こっき"],
+ "🇧🇴": ["ぼりびあこっき","ぼりびあ","こっき"],
+ "🇧🇶": ["かりぶかいのおらんだりょうとうのはた","ぼねーるとう","かりぶかい","ゆーすたてぃうす","こっき","おらんだ","さば","しんと"],
+ "🇧🇷": ["ぶらじるこっき","ぶらじる","こっき"],
+ "🇧🇸": ["ばはまこっき","ばはま","こっき"],
+ "🇧🇹": ["ぶーたんこっき","ぶーたん","こっき"],
+ "🇧🇼": ["ぼつわなこっき","ぼつわな","こっき"],
+ "🇧🇾": ["べらるーしこっき","べらるーし","こっき"],
+ "🇧🇿": ["べりーずこっき","べりーず","こっき"],
+ "🇨🇦": ["かなだこっき","かなだ","こっき"],
+ "🇨🇨": ["ここすしょとうのはた","ここす","こっき","しょとう","きーりんぐ"],
+ "🇨🇩": ["こんごこっき - きんしゃさ","こんご","こんご - きんしゃさ","こんごみんしゅきょうわこく","こっき","きんしゃさ","きょうわこく"],
+ "🇨🇫": ["ちゅうおうあふりかこっき","ちゅうおうあふりかきょうわこく","こっき","きょうわこく"],
+ "🇨🇬": ["こんごのはた - ぶらざびる","ぶらざびる","こんご","こんごきょうわこく","こんご - ぶらざびる","こっき","きょうわこく"],
+ "🇨🇭": ["すいすこっき","こっき","すいす"],
+ "🇨🇮": ["こーとじぼわーるこっき","こーとじぼわーる","こっき"],
+ "🇨🇰": ["くっくしょとうこっき","くっく","こっき","しょとう"],
+ "🇨🇱": ["ちりこっき","ちり","こっき"],
+ "🇨🇲": ["かめるーんこっき","かめるーん","こっき"],
+ "🇨🇳": ["ちゅうごくこっき","ちゅうごく","こっき"],
+ "🇨🇴": ["ころんびあこっき","ころんびあ","こっき"],
+ "🇨🇷": ["こすたりかこっき","こすたりか","こっき"],
+ "🇨🇺": ["きゅーばこっき","きゅーば","こっき"],
+ "🇨🇻": ["かーぼべるでこっき","かーぼ","けーぷ","こっき","べるで"],
+ "🇨🇼": ["きゅらそーとうのはた","あんてぃるしょとう","きゅらそー","こっき"],
+ "🇨🇽": ["くりすますとうのはた","くりすます","こっき","しま"],
+ "🇨🇾": ["きぷろすこっき","きぷろす","こっき"],
+ "🇨🇿": ["ちぇここっき","ちぇこきょうわこく","こっき"],
+ "🇩🇪": ["どいつこっき","こっき","どいつ"],
+ "🇩🇯": ["じぶちこっき","じぶち","こっき"],
+ "🇩🇰": ["でんまーくこっき","でんまーく","こっき"],
+ "🇩🇲": ["どみにかこっき","どみにか","こっき"],
+ "🇩🇴": ["どみにかきょうわこくこっき","どみにかきょうわこく","こっき"],
+ "🇩🇿": ["あるじぇりあこっき","あるじぇりあ","こっき"],
+ "🇪🇨": ["えくあどるこっき","えくあどる","こっき"],
+ "🏴󠁧󠁢󠁥󠁮󠁧󠁿": ["いんぐらんどのはた","いんぐらんど","こっき"],
+ "🇪🇪": ["えすとにあこっき","えすとにあ","こっき"],
+ "🇪🇬": ["えじぷとこっき","えじぷと","こっき"],
+ "🇪🇭": ["にしさはらのはた","こっき","さはら","にし","にしさはら"],
+ "🇪🇷": ["えりとりあこっき","えりとりあ","こっき"],
+ "🇪🇸": ["すぺいんこっき","こっき","すぺいん","せうた","めりりゃ"],
+ "🇪🇹": ["えちおぴあこっき","えちおぴあ","こっき"],
+ "🇪🇺": ["おうしゅうはた","おうしゅうれんごう","こっき"],
+ "🇫🇮": ["ふぃんらんどこっき","ふぃんらんど","こっき"],
+ "🇫🇯": ["ふぃじーこっき","ふぃじー","こっき"],
+ "🇫🇰": ["ふぉーくらんどしょとうのはた","ふぉーくらんど","ふぉーくらんどしょとう","こっき","しょとう","まるびなす"],
+ "🇫🇲": ["みくろねしあこっき","こっき","みくろねしあ"],
+ "🇫🇴": ["ふぇろーしょとうのはた","ふぇろー","はた","しょとう"],
+ "🇫🇷": ["ふらんすこっき","こっき","ふらんす","くりっぱーとんとう","せんと・まーちん","さん・まるたん"],
+ "🇬🇦": ["がぼんこっき","こっき","がぼん"],
+ "🇬🇧": ["いぎりすこっき","いぎりす","いぎりすりょう","こーんうぉーる","いんぐらんど","こっき","ぐれーとぶりてん","あいるらんど","きたあいるらんど","すこっとらんど","UK","ゆにおんじゃっく","れんごう","れんごうおうこく","うぇーるず"],
+ "🇬🇩": ["ぐれなだこっき","こっき","ぐれなだ"],
+ "🇬🇪": ["じょーじあこっき","こっき","じょーじあ"],
+ "🇬🇫": ["ふらんすりょうぎあなのはた","こっき","ふらんすりょう","ぎあな"],
+ "🇬🇬": ["がーんじーこっき","こっき","がーんじー"],
+ "🇬🇭": ["がーなこっき","こっき","がーな"],
+ "🇬🇮": ["じぶらるたるこっき","こっき","じぶらるたる"],
+ "🇬🇱": ["ぐりーんらんどこっき","こっき","ぐりーんらんど"],
+ "🇬🇲": ["がんびあこっき","こっき","がんびあ"],
+ "🇬🇳": ["ぎにあこっき","こっき","ぎにあ"],
+ "🇬🇵": ["ぐあどるーぷこっき","こっき","ぐあどるーぷ"],
+ "🇬🇶": ["せきどうぎにあこっき","せきどうぎにあ","こっき","ぎにあ"],
+ "🇬🇷": ["ぎりしゃこっき","こっき","ぎりしゃ"],
+ "🇬🇸": ["さうすじょーじあ・さうすさんどうぃっちしょとうこっき","こっき","じょーじあ","しょとう","さうす","さうすじょーじあ","さうすさんどうぃっち"],
+ "🇬🇹": ["ぐあてまらこっき","こっき","ぐあてまら"],
+ "🇬🇺": ["ぐあむはた","こっき","ぐあむ"],
+ "🇬🇼": ["ぎにあびさうこっき","びさう","こっき","ぎにあ"],
+ "🇬🇾": ["がいあなこっき","こっき","がいあな"],
+ "🇭🇰": ["ほんこんのはた","ちゅうごく","こっき","ほんこん"],
+ "🇭🇳": ["ほんじゅらすこっき","こっき","ほんじゅらす"],
+ "🇭🇷": ["くろあちあこっき","くろあちあ","こっき"],
+ "🇭🇹": ["はいちこっき","こっき","はいち"],
+ "🇭🇺": ["はんがりーこっき","こっき","はんがりー"],
+ "🇮🇨": ["かなりあしょとうのはた","かなりあ","こっき","しょとう"],
+ "🇮🇩": ["いんどねしあこっき","こっき","いんどねしあ"],
+ "🇮🇪": ["あいるらんどこっき","こっき","あいるらんど"],
+ "🇮🇱": ["いすらえるこっき","こっき","いすらえる"],
+ "🇮🇲": ["まんとうのはた","こっき","まんとう"],
+ "🇮🇳": ["いんどこっき","こっき","いんど"],
+ "🇮🇴": ["いぎりすりょういんどようちいきのはた","いぎりすりょう","ちゃごす","はた","いんどよう","しま","でぃえごがるしあ"],
+ "🇮🇶": ["いらくこっき","こっき","いらく"],
+ "🇮🇷": ["いらんこっき","こっき","いらん"],
+ "🇮🇸": ["あいすらんどこっき","こっき","あいすらんど"],
+ "🇮🇹": ["いたりあこっき","こっき","いたりあ"],
+ "🇯🇪": ["じゃーじーだいかんかんかつくのはた","こっき","じゃーじーだいかんかんかつく"],
+ "🇯🇲": ["じゃまいかこっき","こっき","じゃまいか"],
+ "🇯🇴": ["よるだんこっき","こっき","よるだん"],
+ "🇯🇵": ["にっぽんこっき","こっき","にっぽん"],
+ "🇰🇪": ["けにあこっき","こっき","けにあ"],
+ "🇰🇬": ["きるぎすこっき","こっき","きるぎす"],
+ "🇰🇭": ["かんぼじあこっき","かんぼじあ","こっき"],
+ "🇰🇮": ["きりばすこっき","こっき","きりばす"],
+ "🇰🇲": ["こもろこっき","こもろ","こっき"],
+ "🇰🇳": ["せんとくりすとふぁーねいびすこっき","こっき","きっつ","ねいびす","せんと"],
+ "🇰🇵": ["きたちょうせんこっき","こっき","ちょうせん","きた","きたちょうせん"],
+ "🇰🇷": ["かんこくこっき","こっき","かんこく","みなみ","だいかんみんこく"],
+ "🇰🇼": ["くうぇーとこっき","こっき","くうぇーと"],
+ "🇰🇾": ["けいまんしょとうのはた","けいまん","こっき","しょとう"],
+ "🇰🇿": ["かざふすたんこっき","こっき","かざふすたん"],
+ "🇱🇦": ["らおすこっき","こっき","らおす"],
+ "🇱🇧": ["ればのんこっき","こっき","ればのん"],
+ "🇱🇨": ["せんとるしあこっき","こっき","せんとるしあ"],
+ "🇱🇮": ["りひてんしゅたいんこっき","こっき","りひてんしゅたいん"],
+ "🇱🇰": ["すりらんかこっき","こっき","すりらんか"],
+ "🇱🇷": ["りべりあこっき","こっき","りべりあ"],
+ "🇱🇸": ["れそとこっき","こっき","れそと"],
+ "🇱🇹": ["りとあにあこっき","こっき","りとあにあ"],
+ "🇱🇺": ["るくせんぶるくこっき","こっき","るくせんぶるく"],
+ "🇱🇻": ["らとびあこっき","こっき","らとびあ"],
+ "🇱🇾": ["りびあこっき","こっき","りびあ"],
+ "🇲🇦": ["もろっここっき","こっき","もろっこ"],
+ "🇲🇨": ["もなここっき","こっき","もなこ"],
+ "🇲🇩": ["もるどばこっき","こっき","もるどば"],
+ "🇲🇪": ["もんてねぐろこっき","こっき","もんてねぐろ"],
+ "🇲🇬": ["まだがすかるこっき","こっき","まだがすかる"],
+ "🇲🇭": ["まーしゃるしょとうこっき","こっき","しょとう","まーしゃる"],
+ "🇲🇰": ["まけどにあこっき","こっき","まけどにあ"],
+ "🇲🇱": ["まりこっき","こっき","まり"],
+ "🇲🇲": ["みゃんまーこっき","びるま","こっき","みゃんまー"],
+ "🇲🇳": ["もんごるこっき","こっき","もんごる"],
+ "🇲🇴": ["まかおのはた","ちゅうごく","こっき","まかお"],
+ "🇲🇵": ["きたまりあなしょとうのはた","こっき","しょとう","まりあな","きた","きたまりあな"],
+ "🇲🇶": ["まるてぃにーくのはた","はた","まるてぃにーく"],
+ "🇲🇷": ["もーりたにあこっき","こっき","もーりたにあ"],
+ "🇲🇸": ["もんとせらとのはた","はた","もんとせらと"],
+ "🇲🇹": ["まるたこっき","こっき","まるた"],
+ "🇲🇺": ["もーりしゃすこっき","こっき","もーりしゃす"],
+ "🇲🇻": ["もるでぃぶこっき","こっき","もるでぃぶ"],
+ "🇲🇼": ["まらういこっき","こっき","まらうい"],
+ "🇲🇽": ["めきしここっき","こっき","めきしこ"],
+ "🇲🇾": ["まれーしあこっき","こっき","まれーしあ"],
+ "🇲🇿": ["もざんびーくこっき","こっき","もざんびーく"],
+ "🇳🇦": ["なみびあこっき","こっき","なみびあ"],
+ "🇳🇨": ["にゅーかれどにあのはた","こっき","にゅー","にゅーかれどにあ"],
+ "🇳🇪": ["にじぇーるこっき","こっき","にじぇーる"],
+ "🇳🇫": ["のーふぉーくとうのはた","はた","しま","のーふぉーく"],
+ "🇳🇬": ["ないじぇりあこっき","こっき","ないじぇりあ"],
+ "🇳🇮": ["にからぐあこっき","こっき","にからぐあ"],
+ "🇳🇱": ["おらんだこっき","こっき","おらんだ"],
+ "🇳🇴": ["のるうぇーこっき","はた","のるうぇー","ぶーべ","すヴぁーるばる","やんまいえん"],
+ "🇳🇵": ["ねぱーるこっき","こっき","ねぱーる"],
+ "🇳🇷": ["なうるこっき","こっき","なうる"],
+ "🇳🇺": ["にうえこっき","こっき","にうえ"],
+ "🇳🇿": ["にゅーじーらんどこっき","こっき","にゅー","にゅーじーらんど"],
+ "🇴🇲": ["おまーんこっき","こっき","おまーん"],
+ "🇵🇦": ["ぱなまこっき","こっき","ぱなま"],
+ "🇵🇪": ["ぺるーこっき","こっき","ぺるー"],
+ "🇵🇫": ["ふらんすりょうぽりねしあのはた","こっき","ふらんすりょう","ぽりねしあ"],
+ "🇵🇬": ["ぱぷあにゅーぎにあこっき","こっき","ぎにあ","にゅー","ぱぷあにゅーぎにあ"],
+ "🇵🇭": ["ふぃりぴんこっき","こっき","ふぃりぴん"],
+ "🇵🇰": ["ぱきすたんこっき","こっき","ぱきすたん"],
+ "🇵🇱": ["ぽーらんどこっき","こっき","ぽーらんど"],
+ "🇵🇲": ["さんぴえーるとう・みくろんとうのはた","はた","みくろん","ぴえーる","さん"],
+ "🇵🇳": ["ぴとけあんしょとうのはた","はた","しょとう","ぴとけあん"],
+ "🇵🇷": ["ぷえるとりこのはた","こっき","ぷえるとりこ"],
+ "🇵🇸": ["ぱれすちなじちせいふのはた","こっき","ぱれすちな"],
+ "🇵🇹": ["ぽるとがるこっき","こっき","ぽるとがる"],
+ "🇵🇼": ["ぱらおこっき","こっき","ぱらお"],
+ "🇵🇾": ["ぱらぐあいこっき","こっき","ぱらぐあい"],
+ "🇶🇦": ["かたーるこっき","こっき","かたーる"],
+ "🇷🇪": ["れゆにおんのはた","はた","れゆにおん"],
+ "🇷🇴": ["るーまにあこっき","こっき","るーまにあ"],
+ "🇷🇸": ["せるびあこっき","こっき","せるびあ"],
+ "🇷🇺": ["ろしあこっき","こっき","ろしあ"],
+ "🇷🇼": ["るわんだこっき","こっき","るわんだ"],
+ "🇸🇦": ["さうじあらびあこっき","こっき","さうじあらびあ"],
+ "🏴󠁧󠁢󠁳󠁣󠁴󠁿": ["すこっとらんどのはた","すこっとらんど","はた"],
+ "🇸🇧": ["そろもんしょとうこっき","はた","しょとう","そろもん"],
+ "🇸🇨": ["せーしぇるこっき","こっき","せーしぇる"],
+ "🇸🇩": ["すーだんこっき","こっき","すーだん"],
+ "🇸🇪": ["すうぇーでんこっき","こっき","すうぇーでん"],
+ "🇸🇬": ["しんがぽーるこっき","こっき","しんがぽーる"],
+ "🇸🇭": ["せんとへれなとうのはた","はた","へれな","せんと"],
+ "🇸🇮": ["すろべにあこっき","こっき","すろべにあ"],
+ "🇸🇰": ["すろばきあこっき","こっき","すろばきあ"],
+ "🇸🇱": ["しえられおねこっき","こっき","しえられおね"],
+ "🇸🇲": ["さんまりのこっき","こっき","さんまりの"],
+ "🇸🇳": ["せねがるこっき","こっき","せねがる"],
+ "🇸🇴": ["そまりあこっき","こっき","そまりあ"],
+ "🇸🇷": ["すりなむこっき","こっき","すりなむ"],
+ "🇸🇸": ["みなみすーだんこっき","こっき","みなみ","みなみすーだん","すーだん"],
+ "🇸🇹": ["さんとめぷりんしぺこっき","こっき","ぷりんしぺ","ぷりんしぴ","さんとめ","さぉんとめー"],
+ "🇸🇻": ["えるさるばどるこっき","えるさるばどる","こっき"],
+ "🇸🇽": ["せんと・まーちんとうのはた","はた","まーちん","せんと"],
+ "🇸🇾": ["しりあこっき","こっき","しりあ"],
+ "🇸🇿": ["すわじらんどこっき","こっき","すわじらんど"],
+ "🇹🇦": ["とりすたんだくーにゃのはた","はた","とりすたん・だ・くーにゃ"],
+ "🇹🇨": ["たーくす・かいこすしょとうのはた","かいこす","はた","しょとう","たーくす"],
+ "🇹🇩": ["ちゃどこっき","ちゃど","こっき"],
+ "🇹🇫": ["ふらんすりょうなんぽう・なんきょくちいきのはた","なんきょく","こっき","ふらんすりょう"],
+ "🇹🇬": ["とーごこっき","こっき","とーご"],
+ "🇹🇭": ["たいこっき","こっき","たい"],
+ "🇹🇯": ["たじきすたんこっき","こっき","たじきすたん"],
+ "🇹🇰": ["とけらうはた","こっき","とけらう"],
+ "🇹🇱": ["ひがしてぃもーるこっき","ひがし","ひがしてぃもーる","こっき","てぃもーる・れすて"],
+ "🇹🇲": ["とるくめにすたんこっき","こっき","とるくめにすたん"],
+ "🇹🇳": ["ちゅにじあこっき","こっき","ちゅにじあ"],
+ "🇹🇴": ["とんがこっき","こっき","とんが"],
+ "🇹🇷": ["とるここっき","こっき","とるこ"],
+ "🇹🇹": ["とりにだーどとばごこっき","こっき","とばご","とりにだーど"],
+ "🇹🇻": ["つばるこっき","こっき","つばる"],
+ "🇹🇼": ["たいわんのはた","ちゅうごく","こっき","たいわん"],
+ "🇹🇿": ["たんざにあこっき","こっき","たんざにあ"],
+ "🇺🇦": ["うくらいなこっき","こっき","うくらいな"],
+ "🇺🇬": ["うがんだこっき","こっき","うがんだ"],
+ "🇺🇳": ["こくれんのはた","はた","こくれん","れんごう","こくさい"],
+ "🇺🇸": ["あめりかこっき","あめりか","はた","ごうしゅう","がっしゅうこく","あめりかがっしゅうこく","がっしゅうこくりょうゆうしょうりとう"],
+ "🇺🇾": ["うるぐあいこっき","こっき","うるぐあい"],
+ "🇺🇿": ["うずべきすたんこっき","こっき","うずべきすたん"],
+ "🇻🇦": ["ばちかんしこっき","こっき","ばちかん"],
+ "🇻🇨": ["せんとびんせんと・ぐれなでぃーんこっき","こっき","ぐれなでぃーんしょとう","せんと","びんせんと"],
+ "🇻🇪": ["べねずえらこっき","こっき","べねずえら"],
+ "🇻🇬": ["いぎりすりょうヴぁぁーじんしょとうのはた","いぎりすりょう","こっき","しま","ヴぁーじん"],
+ "🇻🇮": ["あめりかりょうヴぁーじんしょとうのはた","あめりか","こっき","しま","あめりかがっしゅうこく","がっしゅうこく","ヴぁーじん"],
+ "🇻🇳": ["べとなむこっき","こっき","べとなむ","ヴぇとなむ"],
+ "🇻🇺": ["ばぬあつこっき","こっき","ばぬあつ"],
+ "🏴󠁧󠁢󠁷󠁬󠁳󠁿": ["うぇーるずのはた","うぇーるず","はた"],
+ "🇼🇫": ["うぉりす・ふつなのはた","こっき","ふつな","うぉりす"],
+ "🇼🇸": ["さもあこっき","こっき","さもあ"],
+ "🇽🇰": ["こそぼこっき","こっき","こそぼ"],
+ "🇾🇪": ["いえめんこっき","こっき","いえめん"],
+ "🇾🇹": ["まよっとのはた","こっき","まよっと"],
+ "🇿🇦": ["みなみあふりかこっき","こっき","みなみ","みなみあふりか"],
+ "🇿🇲": ["ざんびあこっき","こっき","ざんびあ"],
+ "🇿🇼": ["じんばぶえこっき","こっき","じんばぶえ"],
+ "": ["しぶや109", "SHIBUYA109", "109"]
+}
diff --git a/packages/frontend/src/widgets/WidgetActivity.calendar.vue b/packages/frontend/src/widgets/WidgetActivity.calendar.vue
index bb5a2676dd..58d231d9d4 100644
--- a/packages/frontend/src/widgets/WidgetActivity.calendar.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.calendar.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetActivity.chart.vue b/packages/frontend/src/widgets/WidgetActivity.chart.vue
index 0e87ec3ec3..41c6126c72 100644
--- a/packages/frontend/src/widgets/WidgetActivity.chart.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.chart.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue
index 7759986928..0aaf18ddd1 100644
--- a/packages/frontend/src/widgets/WidgetActivity.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,7 +25,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid
import XCalendar from './WidgetActivity.calendar.vue';
import XChart from './WidgetActivity.chart.vue';
import { GetFormResultType } from '@/scripts/form.js';
-import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
@@ -76,7 +76,7 @@ const toggleView = () => {
save();
};
-os.apiGet('charts/user/notes', {
+misskeyApiGet('charts/user/notes', {
userId: $i.id,
span: 'day',
limit: 7 * 21,
diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue
index fef026244c..00001005de 100644
--- a/packages/frontend/src/widgets/WidgetAichan.vue
+++ b/packages/frontend/src/widgets/WidgetAichan.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index 5968b54626..a74483e85e 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,7 +25,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
import MkContainer from '@/components/MkContainer.vue';
-import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
@@ -69,19 +69,7 @@ const run = async () => {
storageKey: 'widget',
token: $i?.token,
}), {
- in: (q) => {
- return new Promise(ok => {
- os.inputText({
- title: q,
- }).then(({ canceled, result: a }) => {
- if (canceled) {
- ok('');
- } else {
- ok(a);
- }
- });
- });
- },
+ in: aiScriptReadline,
out: (value) => {
logs.value.push({
id: Math.random().toString(),
diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
index 10248a840a..fa79e4aeb7 100644
--- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue
+++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -18,7 +18,7 @@ import { Interpreter, Parser } from '@syuilo/aiscript';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
-import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import { $i } from '@/account.js';
import MkAsUi from '@/components/MkAsUi.vue';
import MkContainer from '@/components/MkContainer.vue';
@@ -64,19 +64,7 @@ async function run() {
root.value = _root.value;
}),
}, {
- in: (q) => {
- return new Promise(ok => {
- os.inputText({
- title: q,
- }).then(({ canceled, result: a }) => {
- if (canceled) {
- ok('');
- } else {
- ok(a);
- }
- });
- });
- },
+ in: aiScriptReadline,
out: (value) => {
// nop
},
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index 7c4455516d..5b448e2c3b 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
import { infoImageUrl } from '@/instance.js';
@@ -70,7 +70,7 @@ const fetch = () => {
now.setHours(0, 0, 0, 0);
if (now > lfAtD) {
- os.api('users/following', {
+ misskeyApi('users/following', {
limit: 18,
birthday: now.toISOString(),
userId: $i.id,
diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue
index 11082c1e3f..6080e120ec 100644
--- a/packages/frontend/src/widgets/WidgetButton.vue
+++ b/packages/frontend/src/widgets/WidgetButton.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -16,7 +16,7 @@ import { Interpreter, Parser } from '@syuilo/aiscript';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
-import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import { $i } from '@/account.js';
import MkButton from '@/components/MkButton.vue';
@@ -56,19 +56,7 @@ const run = async () => {
storageKey: 'widget',
token: $i?.token,
}), {
- in: (q) => {
- return new Promise(ok => {
- os.inputText({
- title: q,
- }).then(({ canceled, result: a }) => {
- if (canceled) {
- ok('');
- } else {
- ok(a);
- }
- });
- });
- },
+ in: aiScriptReadline,
out: (value) => {
// nop
},
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index c78e291a2e..c688e8a0b1 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -7,11 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.root, { _panel: !widgetProps.transparent }]" data-cy-mkw-calendar>
<div :class="[$style.calendar, { [$style.isHoliday]: isHoliday }]">
<p :class="$style.monthAndYear">
- <span :class="$style.year">{{ i18n.t('yearX', { year }) }}</span>
- <span :class="$style.month">{{ i18n.t('monthX', { month }) }}</span>
+ <span :class="$style.year">{{ i18n.tsx.yearX({ year }) }}</span>
+ <span :class="$style.month">{{ i18n.tsx.monthX({ month }) }}</span>
</p>
- <p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
- <p v-else :class="$style.day">{{ i18n.t('dayX', { day }) }}</p>
+ <p v-if="month === 1 && day === 1" class="day">🎉{{ i18n.tsx.dayX({ day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
+ <p v-else :class="$style.day">{{ i18n.tsx.dayX({ day }) }}</p>
<p :class="$style.weekDay">{{ weekDay }}</p>
</div>
<div :class="$style.info">
diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue
index 988ec90369..5c978fdf72 100644
--- a/packages/frontend/src/widgets/WidgetClicker.vue
+++ b/packages/frontend/src/widgets/WidgetClicker.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue
index 22f053db59..b3128ef27e 100644
--- a/packages/frontend/src/widgets/WidgetClock.vue
+++ b/packages/frontend/src/widgets/WidgetClock.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue
index a4b90c49d3..fa9a98d571 100644
--- a/packages/frontend/src/widgets/WidgetDigitalClock.vue
+++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index cc3ad8ff7d..ed907de9b8 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -31,7 +31,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
-import * as os from '@/os.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
@@ -62,11 +62,11 @@ const charts = ref<Misskey.entities.ChartsInstanceResponse[]>([]);
const fetching = ref(true);
const fetch = async () => {
- const fetchedInstances = await os.api('federation/instances', {
+ const fetchedInstances = await misskeyApi('federation/instances', {
sort: '+latestRequestReceivedAt',
limit: 5,
});
- const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.apiGet('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
+ const fetchedCharts = await Promise.all(fetchedInstances.map(i => misskeyApiGet('charts/instance', { host: i.host, limit: 16, span: 'hour' })));
instances.value = fetchedInstances;
charts.value = fetchedCharts;
fetching.value = false;
diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
index 38323ed040..76ccdb3971 100644
--- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -25,6 +25,7 @@ import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import MkTagCloud from '@/components/MkTagCloud.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
@@ -56,7 +57,7 @@ function onInstanceClick(i) {
}
useInterval(() => {
- os.api('federation/instances', {
+ misskeyApi('federation/instances', {
sort: '+latestRequestReceivedAt',
limit: 25,
}).then(res => {
diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
index 6904037532..25d824c8ae 100644
--- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index 10bc257e12..edf6622a13 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -10,19 +10,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="values">
<div>
<div>Process</div>
- <div :class="{ inc: current.inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: current.inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }">{{ number(current.inbox.activeSincePrevTick) }}</div>
+ <div :class="{ inc: current.inbox.activeSincePrevTick > prev.inbox.activeSincePrevTick, dec: current.inbox.activeSincePrevTick < prev.inbox.activeSincePrevTick }" :title="`${current.inbox.activeSincePrevTick}`">{{ kmg(current.inbox.activeSincePrevTick, 2) }}</div>
</div>
<div>
<div>Active</div>
- <div :class="{ inc: current.inbox.active > prev.inbox.active, dec: current.inbox.active < prev.inbox.active }">{{ number(current.inbox.active) }}</div>
+ <div :class="{ inc: current.inbox.active > prev.inbox.active, dec: current.inbox.active < prev.inbox.active }" :title="`${current.inbox.active}`">{{ kmg(current.inbox.active, 2) }}</div>
</div>
<div>
<div>Delayed</div>
- <div :class="{ inc: current.inbox.delayed > prev.inbox.delayed, dec: current.inbox.delayed < prev.inbox.delayed }">{{ number(current.inbox.delayed) }}</div>
+ <div :class="{ inc: current.inbox.delayed > prev.inbox.delayed, dec: current.inbox.delayed < prev.inbox.delayed }" :title="`${current.inbox.delayed}`">{{ kmg(current.inbox.delayed, 2) }}</div>
</div>
<div>
<div>Waiting</div>
- <div :class="{ inc: current.inbox.waiting > prev.inbox.waiting, dec: current.inbox.waiting < prev.inbox.waiting }">{{ number(current.inbox.waiting) }}</div>
+ <div :class="{ inc: current.inbox.waiting > prev.inbox.waiting, dec: current.inbox.waiting < prev.inbox.waiting }" :title="`${current.inbox.waiting}`">{{ kmg(current.inbox.waiting, 2) }}</div>
</div>
</div>
</div>
@@ -31,19 +31,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="values">
<div>
<div>Process</div>
- <div :class="{ inc: current.deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: current.deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }">{{ number(current.deliver.activeSincePrevTick) }}</div>
+ <div :class="{ inc: current.deliver.activeSincePrevTick > prev.deliver.activeSincePrevTick, dec: current.deliver.activeSincePrevTick < prev.deliver.activeSincePrevTick }" :title="`${current.deliver.activeSincePrevTick}`">{{ kmg(current.deliver.activeSincePrevTick, 2) }}</div>
</div>
<div>
<div>Active</div>
- <div :class="{ inc: current.deliver.active > prev.deliver.active, dec: current.deliver.active < prev.deliver.active }">{{ number(current.deliver.active) }}</div>
+ <div :class="{ inc: current.deliver.active > prev.deliver.active, dec: current.deliver.active < prev.deliver.active }" :title="`${current.deliver.active}`">{{ kmg(current.deliver.active, 2) }}</div>
</div>
<div>
<div>Delayed</div>
- <div :class="{ inc: current.deliver.delayed > prev.deliver.delayed, dec: current.deliver.delayed < prev.deliver.delayed }">{{ number(current.deliver.delayed) }}</div>
+ <div :class="{ inc: current.deliver.delayed > prev.deliver.delayed, dec: current.deliver.delayed < prev.deliver.delayed }" :title="`${current.deliver.delayed}`">{{ kmg(current.deliver.delayed, 2) }}</div>
</div>
<div>
<div>Waiting</div>
- <div :class="{ inc: current.deliver.waiting > prev.deliver.waiting, dec: current.deliver.waiting < prev.deliver.waiting }">{{ number(current.deliver.waiting) }}</div>
+ <div :class="{ inc: current.deliver.waiting > prev.deliver.waiting, dec: current.deliver.waiting < prev.deliver.waiting }" :title="`${current.deliver.waiting}`">{{ kmg(current.deliver.waiting, 2) }}</div>
</div>
</div>
</div>
@@ -55,7 +55,7 @@ import { onUnmounted, reactive, ref } from 'vue';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import { useStream } from '@/stream.js';
-import number from '@/filters/number.js';
+import kmg from '@/filters/kmg.js';
import * as sound from '@/scripts/sound.js';
import { deepClone } from '@/scripts/clone.js';
import { defaultStore } from '@/store.js';
@@ -104,10 +104,7 @@ const jammedAudioBuffer = ref<AudioBuffer | null>(null);
const jammedSoundNodePlaying = ref<boolean>(false);
if (defaultStore.state.sound_masterVolume) {
- sound.loadAudio({
- type: 'syuilo/queue-jammed',
- volume: 1,
- }).then(buf => {
+ sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => {
if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
jammedAudioBuffer.value = buf;
});
@@ -126,7 +123,7 @@ const onStats = (stats) => {
current[domain].delayed = stats[domain].delayed;
if (current[domain].waiting > 0 && widgetProps.sound && jammedAudioBuffer.value && !jammedSoundNodePlaying.value) {
- const soundNode = sound.createSourceNode(jammedAudioBuffer.value, 1);
+ const soundNode = sound.createSourceNode(jammedAudioBuffer.value, {}).soundSource;
if (soundNode) {
jammedSoundNodePlaying.value = true;
soundNode.onended = () => jammedSoundNodePlaying.value = false;
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index 167014270a..7ee83157c6 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue
index 506fc6b4d4..4b3265dab7 100644
--- a/packages/frontend/src/widgets/WidgetNotifications.vue
+++ b/packages/frontend/src/widgets/WidgetNotifications.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
index 0a6fec7f2e..5c89a06c62 100644
--- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue
+++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
-import * as os from '@/os.js';
+import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
import number from '@/filters/number.js';
@@ -45,7 +45,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
const onlineUsersCount = ref(0);
const tick = () => {
- os.apiGet('get-online-users-count').then(res => {
+ misskeyApiGet('get-online-users-count').then(res => {
onlineUsersCount.value = res.count;
});
};
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 257753ef10..34be8c5e57 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -28,7 +28,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid
import { GetFormResultType } from '@/scripts/form.js';
import { useStream } from '@/stream.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -74,7 +74,7 @@ const thumbnail = (image: any): string => {
: image.thumbnailUrl;
};
-os.api('drive/stream', {
+misskeyApi('drive/stream', {
type: 'image/*',
limit: 9,
}).then(res => {
diff --git a/packages/frontend/src/widgets/WidgetPostForm.vue b/packages/frontend/src/widgets/WidgetPostForm.vue
index 9979ae256e..7f344505d8 100644
--- a/packages/frontend/src/widgets/WidgetPostForm.vue
+++ b/packages/frontend/src/widgets/WidgetPostForm.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue
index 3ff57bab86..a5578d4de6 100644
--- a/packages/frontend/src/widgets/WidgetProfile.vue
+++ b/packages/frontend/src/widgets/WidgetProfile.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 78678920c7..5d5c1188aa 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 34b4b8f884..af220f95e2 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 7e39a05881..7a3671a240 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<p v-if="widgetProps.folderId == null">
{{ i18n.ts.folder }}
</p>
- <p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.t('no-image') }}</p>
+ <p v-if="widgetProps.folderId != null && images.length === 0 && !fetching">{{ i18n.ts['no-image'] }}</p>
<div ref="slideA" class="slide a"></div>
<div ref="slideB" class="slide b"></div>
</div>
@@ -22,6 +22,7 @@ import * as Misskey from 'misskey-js';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
@@ -77,7 +78,7 @@ const change = () => {
const fetch = () => {
fetching.value = true;
- os.api('drive/files', {
+ misskeyApi('drive/files', {
folderId: widgetProps.folderId,
type: 'image/*',
limit: 100,
diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue
index 4a7b06f1d9..150e838582 100644
--- a/packages/frontend/src/widgets/WidgetTimeline.vue
+++ b/packages/frontend/src/widgets/WidgetTimeline.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #header>
<button class="_button" @click="choose">
- <span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.t('_timelines.' + widgetProps.src) }}</span>
+ <span>{{ widgetProps.src === 'list' ? widgetProps.list.name : widgetProps.src === 'antenna' ? widgetProps.antenna.name : i18n.ts._timelines[widgetProps.src] }}</span>
<i :class="menuOpened ? 'ti ti-chevron-up' : 'ti ti-chevron-down'" style="margin-left: 8px;"></i>
</button>
</template>
@@ -38,6 +38,7 @@ import { ref } from 'vue';
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import { GetFormResultType } from '@/scripts/form.js';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import MkContainer from '@/components/MkContainer.vue';
import MkTimeline from '@/components/MkTimeline.vue';
import { i18n } from '@/i18n.js';
@@ -95,8 +96,8 @@ const setSrc = (src) => {
const choose = async (ev) => {
menuOpened.value = true;
const [antennas, lists] = await Promise.all([
- os.api('antennas/list'),
- os.api('users/lists/list'),
+ misskeyApi('antennas/list'),
+ misskeyApi('users/lists/list'),
]);
const antennaItems = antennas.map(antenna => ({
text: antenna.name,
diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue
index ede7cb6f3b..4299181a27 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-for="stat in stats" :key="stat.tag">
<div class="tag">
<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
- <p>{{ i18n.t('nUsersMentioned', { n: stat.usersCount }) }}</p>
+ <p>{{ i18n.tsx.nUsersMentioned({ n: stat.usersCount }) }}</p>
</div>
<MkMiniChart class="chart" :src="stat.chart"/>
</div>
@@ -30,7 +30,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import MkMiniChart from '@/components/MkMiniChart.vue';
-import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -59,7 +59,7 @@ const stats = ref<Misskey.entities.HashtagsTrendResponse>([]);
const fetching = ref(true);
const fetch = () => {
- os.apiGet('hashtags/trend').then(res => {
+ misskeyApiGet('hashtags/trend').then(res => {
stats.value = res;
fetching.value = false;
});
diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue
index 35f29b5e21..2ac7d1c781 100644
--- a/packages/frontend/src/widgets/WidgetUnixClock.vue
+++ b/packages/frontend/src/widgets/WidgetUnixClock.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue
index e17b2cba93..d9f4dc49ea 100644
--- a/packages/frontend/src/widgets/WidgetUserList.vue
+++ b/packages/frontend/src/widgets/WidgetUserList.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -30,6 +30,7 @@ import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, Wid
import { GetFormResultType } from '@/scripts/form.js';
import MkContainer from '@/components/MkContainer.vue';
import * as os from '@/os.js';
+import { misskeyApi } from '@/scripts/misskey-api.js';
import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
@@ -64,7 +65,7 @@ const users = ref<Misskey.entities.UserDetailed[]>([]);
const fetching = ref(true);
async function chooseList() {
- const lists = await os.api('users/lists/list');
+ const lists = await misskeyApi('users/lists/list');
const { canceled, result: list } = await os.select({
title: i18n.ts.selectList,
items: lists.map(x => ({
@@ -85,11 +86,11 @@ const fetch = () => {
return;
}
- os.api('users/lists/show', {
+ misskeyApi('users/lists/show', {
listId: widgetProps.listId,
}).then(_list => {
list.value = _list;
- os.api('users/show', {
+ misskeyApi('users/show', {
userIds: list.value.userIds,
}).then(_users => {
users.value = _users;
diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts
index 36925e1bd8..e269fcf9eb 100644
--- a/packages/frontend/src/widgets/index.ts
+++ b/packages/frontend/src/widgets/index.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/widgets/server-metric/cpu-mem.vue b/packages/frontend/src/widgets/server-metric/cpu-mem.vue
index f13b6a370d..27d3234207 100644
--- a/packages/frontend/src/widgets/server-metric/cpu-mem.vue
+++ b/packages/frontend/src/widgets/server-metric/cpu-mem.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -80,13 +80,13 @@ import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
const props = defineProps<{
- connection: any,
+ connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
const viewBoxX = ref<number>(50);
const viewBoxY = ref<number>(30);
-const stats = ref<any[]>([]);
+const stats = ref<Misskey.entities.ServerStats[]>([]);
const cpuGradientId = uuid();
const cpuMaskId = uuid();
const memGradientId = uuid();
@@ -107,6 +107,7 @@ onMounted(() => {
props.connection.on('statsLog', onStatsLog);
props.connection.send('requestLog', {
id: Math.random().toString().substring(2, 10),
+ length: 50,
});
});
@@ -115,7 +116,7 @@ onBeforeUnmount(() => {
props.connection.off('statsLog', onStatsLog);
});
-function onStats(connStats) {
+function onStats(connStats: Misskey.entities.ServerStats) {
stats.value.push(connStats);
if (stats.value.length > 50) stats.value.shift();
@@ -136,8 +137,8 @@ function onStats(connStats) {
memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
}
-function onStatsLog(statsLog) {
- for (const revStats of [...statsLog].reverse()) {
+function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
+ for (const revStats of statsLog.reverse()) {
onStats(revStats);
}
}
diff --git a/packages/frontend/src/widgets/server-metric/cpu.vue b/packages/frontend/src/widgets/server-metric/cpu.vue
index c7fd0e9023..ba98a926ff 100644
--- a/packages/frontend/src/widgets/server-metric/cpu.vue
+++ b/packages/frontend/src/widgets/server-metric/cpu.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -20,13 +20,13 @@ import * as Misskey from 'misskey-js';
import XPie from './pie.vue';
const props = defineProps<{
- connection: any,
+ connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
const usage = ref<number>(0);
-function onStats(stats) {
+function onStats(stats: Misskey.entities.ServerStats) {
usage.value = stats.cpu;
}
diff --git a/packages/frontend/src/widgets/server-metric/disk.vue b/packages/frontend/src/widgets/server-metric/disk.vue
index 9299af450f..0602b8cd27 100644
--- a/packages/frontend/src/widgets/server-metric/disk.vue
+++ b/packages/frontend/src/widgets/server-metric/disk.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue
index b4a4182653..86d84b4f33 100644
--- a/packages/frontend/src/widgets/server-metric/index.vue
+++ b/packages/frontend/src/widgets/server-metric/index.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted, ref } from 'vue';
import * as Misskey from 'misskey-js';
-import { useWidgetPropsManager, Widget, WidgetComponentExpose } from '../widget.js';
+import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js';
import XCpuMemory from './cpu-mem.vue';
import XNet from './net.vue';
import XCpu from './cpu.vue';
@@ -30,7 +30,7 @@ import XMemory from './mem.vue';
import XDisk from './disk.vue';
import MkContainer from '@/components/MkContainer.vue';
import { GetFormResultType } from '@/scripts/form.js';
-import * as os from '@/os.js';
+import { misskeyApiGet } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
@@ -54,11 +54,8 @@ const widgetPropsDef = {
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
-// 現時点ではvueの制限によりimportしたtypeをジェネリックに渡せない
-//const props = defineProps<WidgetComponentProps<WidgetProps>>();
-//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
-const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
-const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
+const props = defineProps<WidgetComponentProps<WidgetProps>>();
+const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const { widgetProps, configure, save } = useWidgetPropsManager(name,
widgetPropsDef,
@@ -68,7 +65,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
const meta = ref<Misskey.entities.ServerInfoResponse | null>(null);
-os.apiGet('server-info', {}).then(res => {
+misskeyApiGet('server-info', {}).then(res => {
meta.value = res;
});
diff --git a/packages/frontend/src/widgets/server-metric/mem.vue b/packages/frontend/src/widgets/server-metric/mem.vue
index f51b2af390..ff4c6a44b4 100644
--- a/packages/frontend/src/widgets/server-metric/mem.vue
+++ b/packages/frontend/src/widgets/server-metric/mem.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -22,7 +22,7 @@ import XPie from './pie.vue';
import bytes from '@/filters/bytes.js';
const props = defineProps<{
- connection: any,
+ connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
@@ -31,7 +31,7 @@ const total = ref<number>(0);
const used = ref<number>(0);
const free = ref<number>(0);
-function onStats(stats) {
+function onStats(stats: Misskey.entities.ServerStats) {
usage.value = stats.mem.active / props.meta.mem.total;
total.value = props.meta.mem.total;
used.value = stats.mem.active;
diff --git a/packages/frontend/src/widgets/server-metric/net.vue b/packages/frontend/src/widgets/server-metric/net.vue
index 7af88a94eb..d46aaa5f69 100644
--- a/packages/frontend/src/widgets/server-metric/net.vue
+++ b/packages/frontend/src/widgets/server-metric/net.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
@@ -54,13 +54,13 @@ import * as Misskey from 'misskey-js';
import bytes from '@/filters/bytes.js';
const props = defineProps<{
- connection: any,
+ connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
meta: Misskey.entities.ServerInfoResponse
}>();
const viewBoxX = ref<number>(50);
const viewBoxY = ref<number>(30);
-const stats = ref<any[]>([]);
+const stats = ref<Misskey.entities.ServerStats[]>([]);
const inPolylinePoints = ref<string>('');
const outPolylinePoints = ref<string>('');
const inPolygonPoints = ref<string>('');
@@ -77,6 +77,7 @@ onMounted(() => {
props.connection.on('statsLog', onStatsLog);
props.connection.send('requestLog', {
id: Math.random().toString().substring(2, 10),
+ length: 50,
});
});
@@ -85,7 +86,7 @@ onBeforeUnmount(() => {
props.connection.off('statsLog', onStatsLog);
});
-function onStats(connStats) {
+function onStats(connStats: Misskey.entities.ServerStats) {
stats.value.push(connStats);
if (stats.value.length > 50) stats.value.shift();
@@ -109,8 +110,8 @@ function onStats(connStats) {
outRecent.value = connStats.net.tx;
}
-function onStatsLog(statsLog) {
- for (const revStats of [...statsLog].reverse()) {
+function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
+ for (const revStats of statsLog.reverse()) {
onStats(revStats);
}
}
diff --git a/packages/frontend/src/widgets/server-metric/pie.vue b/packages/frontend/src/widgets/server-metric/pie.vue
index fd18a6a4f2..400cbe9fa2 100644
--- a/packages/frontend/src/widgets/server-metric/pie.vue
+++ b/packages/frontend/src/widgets/server-metric/pie.vue
@@ -1,5 +1,5 @@
<!--
-SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts
index 9c7632fc9b..bfe8067adf 100644
--- a/packages/frontend/src/widgets/widget.ts
+++ b/packages/frontend/src/widgets/widget.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/workers/draw-blurhash.ts b/packages/frontend/src/workers/draw-blurhash.ts
index b919092223..22de6cd3a8 100644
--- a/packages/frontend/src/workers/draw-blurhash.ts
+++ b/packages/frontend/src/workers/draw-blurhash.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/src/workers/test-webgl2.ts b/packages/frontend/src/workers/test-webgl2.ts
index 8f57e5039b..b203ebe666 100644
--- a/packages/frontend/src/workers/test-webgl2.ts
+++ b/packages/frontend/src/workers/test-webgl2.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts
new file mode 100644
index 0000000000..9a2989b373
--- /dev/null
+++ b/packages/frontend/test/emoji.test.ts
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { describe, test, assert, afterEach } from 'vitest';
+import { render, cleanup, type RenderResult } from '@testing-library/vue';
+import { defaultStoreState } from './init.js';
+import { getEmojiName } from '@/scripts/emojilist.js';
+import { components } from '@/components/index.js';
+import { directives } from '@/directives/index.js';
+import MkEmoji from '@/components/global/MkEmoji.vue';
+
+describe('Emoji', () => {
+ const renderEmoji = (emoji: string): RenderResult => {
+ return render(MkEmoji, {
+ props: { emoji },
+ global: { directives, components },
+ });
+ };
+
+ afterEach(() => {
+ cleanup();
+ defaultStoreState.emojiStyle = '';
+ });
+
+ describe('MkEmoji', () => {
+ test('Should render selector-less heart with color in native mode', async () => {
+ defaultStoreState.emojiStyle = 'native';
+ const mkEmoji = await renderEmoji('\u2764'); // monochrome heart
+ assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart
+ assert.ok(!mkEmoji.queryByText('\u2764'));
+ });
+ });
+
+ describe('Emoji list', () => {
+ test('Should get the name of the heart', () => {
+ assert.strictEqual(getEmojiName('\u2764'), 'heart');
+ });
+ });
+});
diff --git a/packages/frontend/test/home.test.ts b/packages/frontend/test/home.test.ts
index 094ea071b9..b3a4e8ff3a 100644
--- a/packages/frontend/test/home.test.ts
+++ b/packages/frontend/test/home.test.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts
index 6d93ff8cb0..0cde571dcb 100644
--- a/packages/frontend/test/init.ts
+++ b/packages/frontend/test/init.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -17,21 +17,23 @@ updateI18n(locales['en-US']);
// XXX: misskey-js panics if WebSocket is not defined
vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
+export const defaultStoreState: Record<string, unknown> = {
+
+ // なんかtestがうまいこと動かないのでここに書く
+ dataSaver: {
+ media: false,
+ avatar: false,
+ urlPreview: false,
+ code: false,
+ },
+
+};
+
// XXX: defaultStore somehow becomes undefined in vitest?
vi.mock('@/store.js', () => {
return {
defaultStore: {
- state: {
-
- // なんかtestがうまいこと動かないのでここに書く
- dataSaver: {
- media: false,
- avatar: false,
- urlPreview: false,
- code: false,
- },
-
- },
+ state: defaultStoreState,
},
};
});
diff --git a/packages/frontend/test/note.test.ts b/packages/frontend/test/note.test.ts
index 8ccc05ff3e..7ce5f23e22 100644
--- a/packages/frontend/test/note.test.ts
+++ b/packages/frontend/test/note.test.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/test/scroll.test.ts b/packages/frontend/test/scroll.test.ts
index 2334268d43..e49ec270d5 100644
--- a/packages/frontend/test/scroll.test.ts
+++ b/packages/frontend/test/scroll.test.ts
@@ -1,5 +1,5 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts
index f760de9274..4b79d33348 100644
--- a/packages/frontend/test/url-preview.test.ts
+++ b/packages/frontend/test/url-preview.test.ts
@@ -1,12 +1,12 @@
/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { describe, test, assert, afterEach } from 'vitest';
import { render, cleanup, type RenderResult } from '@testing-library/vue';
import './init';
-import type { summaly } from 'summaly';
+import type { summaly } from '@misskey-dev/summaly';
import { components } from '@/components/index.js';
import { directives } from '@/directives/index.js';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
@@ -116,6 +116,34 @@ describe('MkUrlPreview', () => {
assert.strictEqual(iframe?.allow, 'fullscreen;web-share');
});
+ test('A Summaly proxy response without allow falls back to the default', async () => {
+ const iframe = await renderAndOpenPreview({
+ url: 'https://example.local',
+ player: {
+ url: 'https://example.local/player',
+ width: null,
+ height: null,
+ allow: undefined as any,
+ },
+ });
+ assert.exists(iframe, 'iframe should exist');
+ assert.strictEqual(iframe?.allow, 'autoplay;encrypted-media;fullscreen');
+ });
+
+ test('Filtering the allow list from the Summaly proxy', async () => {
+ const iframe = await renderAndOpenPreview({
+ url: 'https://example.local',
+ player: {
+ url: 'https://example.local/player',
+ width: null,
+ height: null,
+ allow: ['autoplay', 'camera', 'fullscreen'],
+ },
+ });
+ assert.exists(iframe, 'iframe should exist');
+ assert.strictEqual(iframe?.allow, 'autoplay;fullscreen');
+ });
+
test('Having a player width should keep the fixed aspect ratio', async () => {
const iframe = await renderAndOpenPreview({
url: 'https://example.local',
diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json
index 5d451c878c..819629a9cf 100644
--- a/packages/frontend/tsconfig.json
+++ b/packages/frontend/tsconfig.json
@@ -33,6 +33,7 @@
],
"types": [
"vite/client",
+ "vitest/importMeta",
],
"lib": [
"esnext",
diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts
index 5a6f511c66..6d9488797c 100644
--- a/packages/frontend/vite.config.local-dev.ts
+++ b/packages/frontend/vite.config.local-dev.ts
@@ -1,5 +1,7 @@
import dns from 'dns';
+import { readFile } from 'node:fs/promises';
import { defineConfig } from 'vite';
+import * as yaml from 'js-yaml';
import locales from '../../locales/index.js';
import { getConfig } from './vite.config.js';
@@ -7,6 +9,11 @@ dns.setDefaultResultOrder('ipv4first');
const defaultConfig = getConfig();
+const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
+
+const httpUrl = `http://localhost:${port}/`;
+const websocketUrl = `ws://localhost:${port}/`;
+
const devConfig = {
// 基本の設定は vite.config.js から引き継ぐ
...defaultConfig,
@@ -19,28 +26,28 @@ const devConfig = {
proxy: {
'/api': {
changeOrigin: true,
- target: 'http://localhost:3000/',
+ target: httpUrl,
},
- '/assets': 'http://localhost:3000/',
- '/static-assets': 'http://localhost:3000/',
- '/client-assets': 'http://localhost:3000/',
- '/files': 'http://localhost:3000/',
- '/twemoji': 'http://localhost:3000/',
- '/fluent-emoji': 'http://localhost:3000/',
- '/sw.js': 'http://localhost:3000/',
+ '/assets': httpUrl,
+ '/static-assets': httpUrl,
+ '/client-assets': httpUrl,
+ '/files': httpUrl,
+ '/twemoji': httpUrl,
+ '/fluent-emoji': httpUrl,
+ '/sw.js': httpUrl,
'/streaming': {
- target: 'ws://localhost:3000/',
+ target: websocketUrl,
ws: true,
},
- '/favicon.ico': 'http://localhost:3000/',
+ '/favicon.ico': httpUrl,
'/identicon': {
- target: 'http://localhost:3000/',
+ target: httpUrl,
rewrite(path) {
return path.replace('@localhost:5173', '');
},
},
- '/url': 'http://localhost:3000',
- '/proxy': 'http://localhost:3000',
+ '/url': httpUrl,
+ '/proxy': httpUrl,
},
},
build: {
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 98fe0043c1..35d112f6ec 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -101,11 +101,6 @@ export function getConfig(): UserConfig {
__VUE_PROD_DEVTOOLS__: false,
},
- // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies
- optimizeDeps: {
- include: ['misskey-js'],
- },
-
build: {
target: [
'chrome116',
@@ -135,7 +130,7 @@ export function getConfig(): UserConfig {
// https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies
commonjsOptions: {
- include: [/misskey-js/, /node_modules/],
+ include: [/misskey-js/, /misskey-reversi/, /misskey-bubble-game/, /node_modules/],
},
},
@@ -155,6 +150,7 @@ export function getConfig(): UserConfig {
},
},
},
+ includeSource: ['src/**/*.ts'],
},
};
}