From be7e3b9a0cb81b78a744993fef2fa2fd2833fa9c Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:28:01 +0900 Subject: refactor(frontend): scripts -> utility --- packages/frontend/src/scripts/achievements.ts | 513 ------------- packages/frontend/src/scripts/admin-lookup.ts | 88 --- packages/frontend/src/scripts/aiscript/api.ts | 125 ---- packages/frontend/src/scripts/aiscript/common.ts | 16 - packages/frontend/src/scripts/aiscript/ui.ts | 655 ---------------- packages/frontend/src/scripts/array.ts | 118 --- packages/frontend/src/scripts/autocomplete.ts | 323 -------- .../src/scripts/autogen/settings-search-index.ts | 822 --------------------- packages/frontend/src/scripts/cache.ts | 53 -- packages/frontend/src/scripts/chart-legend.ts | 17 - packages/frontend/src/scripts/chart-vline.ts | 28 - packages/frontend/src/scripts/check-permissions.ts | 19 - .../src/scripts/check-reaction-permissions.ts | 17 - packages/frontend/src/scripts/check-word-mute.ts | 43 -- packages/frontend/src/scripts/clear-cache.ts | 24 - packages/frontend/src/scripts/clicker-game.ts | 73 -- packages/frontend/src/scripts/clone.ts | 25 - packages/frontend/src/scripts/code-highlighter.ts | 99 --- packages/frontend/src/scripts/collect-page-vars.ts | 73 -- packages/frontend/src/scripts/color.ts | 12 - packages/frontend/src/scripts/confetti.ts | 30 - packages/frontend/src/scripts/contains.ts | 14 - packages/frontend/src/scripts/copy-to-clipboard.ts | 11 - packages/frontend/src/scripts/device-kind.ts | 24 - packages/frontend/src/scripts/emoji-picker.ts | 61 -- packages/frontend/src/scripts/extract-mentions.ts | 16 - .../frontend/src/scripts/extract-url-from-mfm.ts | 24 - packages/frontend/src/scripts/file-drop.ts | 121 --- packages/frontend/src/scripts/focus-trap.ts | 134 ---- packages/frontend/src/scripts/focus.ts | 80 -- packages/frontend/src/scripts/form.ts | 104 --- .../frontend/src/scripts/format-time-string.ts | 55 -- packages/frontend/src/scripts/fullscreen.ts | 46 -- .../frontend/src/scripts/get-account-from-id.ts | 12 - packages/frontend/src/scripts/get-appear-note.ts | 10 - packages/frontend/src/scripts/get-bg-color.ts | 18 - .../frontend/src/scripts/get-dom-node-or-null.ts | 19 - .../frontend/src/scripts/get-drive-file-menu.ts | 162 ---- packages/frontend/src/scripts/get-embed-code.ts | 87 --- packages/frontend/src/scripts/get-note-menu.ts | 685 ----------------- packages/frontend/src/scripts/get-note-summary.ts | 64 -- packages/frontend/src/scripts/get-user-menu.ts | 441 ----------- packages/frontend/src/scripts/get-user-name.ts | 8 - packages/frontend/src/scripts/hotkey.ts | 172 ----- packages/frontend/src/scripts/idb-proxy.ts | 53 -- packages/frontend/src/scripts/idle-render.ts | 60 -- packages/frontend/src/scripts/init-chart.ts | 58 -- packages/frontend/src/scripts/initialize-sw.ts | 18 - packages/frontend/src/scripts/intl-const.ts | 50 -- packages/frontend/src/scripts/intl-string.ts | 97 --- .../frontend/src/scripts/is-device-darkmode.ts | 8 - packages/frontend/src/scripts/isFfVisibleForMe.ts | 24 - packages/frontend/src/scripts/key-event.ts | 153 ---- packages/frontend/src/scripts/langmap.ts | 671 ----------------- packages/frontend/src/scripts/login-id.ts | 16 - packages/frontend/src/scripts/lookup.ts | 84 --- packages/frontend/src/scripts/media-has-audio.ts | 14 - packages/frontend/src/scripts/media-proxy.ts | 34 - packages/frontend/src/scripts/merge.ts | 35 - .../frontend/src/scripts/mfm-function-picker.ts | 57 -- packages/frontend/src/scripts/misskey-api.ts | 116 --- packages/frontend/src/scripts/navigator.ts | 8 - packages/frontend/src/scripts/page-metadata.ts | 71 -- packages/frontend/src/scripts/physics.ts | 157 ---- .../frontend/src/scripts/player-url-transform.ts | 26 - packages/frontend/src/scripts/please-login.ts | 76 -- packages/frontend/src/scripts/popout.ts | 28 - packages/frontend/src/scripts/popup-position.ts | 161 ---- packages/frontend/src/scripts/post-message.ts | 25 - packages/frontend/src/scripts/reaction-picker.ts | 54 -- packages/frontend/src/scripts/reload-ask.ts | 40 - packages/frontend/src/scripts/search-emoji.ts | 106 --- packages/frontend/src/scripts/select-file.ts | 130 ---- packages/frontend/src/scripts/show-moved-dialog.ts | 21 - .../frontend/src/scripts/show-suspended-dialog.ts | 15 - packages/frontend/src/scripts/shuffle.ts | 25 - packages/frontend/src/scripts/snowfall-effect.ts | 490 ------------ packages/frontend/src/scripts/sound.ts | 258 ------- packages/frontend/src/scripts/sticky-sidebar.ts | 55 -- packages/frontend/src/scripts/stream-mock.ts | 81 -- packages/frontend/src/scripts/test-utils.ts | 9 - packages/frontend/src/scripts/theme-editor.ts | 87 --- packages/frontend/src/scripts/theme.ts | 189 ----- packages/frontend/src/scripts/time.ts | 45 -- packages/frontend/src/scripts/timezones.ts | 54 -- packages/frontend/src/scripts/touch.ts | 22 - packages/frontend/src/scripts/unison-reload.ts | 20 - packages/frontend/src/scripts/upload.ts | 162 ---- .../frontend/src/scripts/upload/compress-config.ts | 36 - .../frontend/src/scripts/upload/isWebpSupported.ts | 15 - packages/frontend/src/scripts/use-chart-tooltip.ts | 63 -- packages/frontend/src/scripts/use-form.ts | 56 -- packages/frontend/src/scripts/use-leave-guard.ts | 50 -- packages/frontend/src/scripts/use-note-capture.ts | 124 ---- packages/frontend/src/scripts/use-tooltip.ts | 106 --- 95 files changed, 9954 deletions(-) delete mode 100644 packages/frontend/src/scripts/achievements.ts delete mode 100644 packages/frontend/src/scripts/admin-lookup.ts delete mode 100644 packages/frontend/src/scripts/aiscript/api.ts delete mode 100644 packages/frontend/src/scripts/aiscript/common.ts delete mode 100644 packages/frontend/src/scripts/aiscript/ui.ts delete mode 100644 packages/frontend/src/scripts/array.ts delete mode 100644 packages/frontend/src/scripts/autocomplete.ts delete mode 100644 packages/frontend/src/scripts/autogen/settings-search-index.ts delete mode 100644 packages/frontend/src/scripts/cache.ts delete mode 100644 packages/frontend/src/scripts/chart-legend.ts delete mode 100644 packages/frontend/src/scripts/chart-vline.ts delete mode 100644 packages/frontend/src/scripts/check-permissions.ts delete mode 100644 packages/frontend/src/scripts/check-reaction-permissions.ts delete mode 100644 packages/frontend/src/scripts/check-word-mute.ts delete mode 100644 packages/frontend/src/scripts/clear-cache.ts delete mode 100644 packages/frontend/src/scripts/clicker-game.ts delete mode 100644 packages/frontend/src/scripts/clone.ts delete mode 100644 packages/frontend/src/scripts/code-highlighter.ts delete mode 100644 packages/frontend/src/scripts/collect-page-vars.ts delete mode 100644 packages/frontend/src/scripts/color.ts delete mode 100644 packages/frontend/src/scripts/confetti.ts delete mode 100644 packages/frontend/src/scripts/contains.ts delete mode 100644 packages/frontend/src/scripts/copy-to-clipboard.ts delete mode 100644 packages/frontend/src/scripts/device-kind.ts delete mode 100644 packages/frontend/src/scripts/emoji-picker.ts delete mode 100644 packages/frontend/src/scripts/extract-mentions.ts delete mode 100644 packages/frontend/src/scripts/extract-url-from-mfm.ts delete mode 100644 packages/frontend/src/scripts/file-drop.ts delete mode 100644 packages/frontend/src/scripts/focus-trap.ts delete mode 100644 packages/frontend/src/scripts/focus.ts delete mode 100644 packages/frontend/src/scripts/form.ts delete mode 100644 packages/frontend/src/scripts/format-time-string.ts delete mode 100644 packages/frontend/src/scripts/fullscreen.ts delete mode 100644 packages/frontend/src/scripts/get-account-from-id.ts delete mode 100644 packages/frontend/src/scripts/get-appear-note.ts delete mode 100644 packages/frontend/src/scripts/get-bg-color.ts delete mode 100644 packages/frontend/src/scripts/get-dom-node-or-null.ts delete mode 100644 packages/frontend/src/scripts/get-drive-file-menu.ts delete mode 100644 packages/frontend/src/scripts/get-embed-code.ts delete mode 100644 packages/frontend/src/scripts/get-note-menu.ts delete mode 100644 packages/frontend/src/scripts/get-note-summary.ts delete mode 100644 packages/frontend/src/scripts/get-user-menu.ts delete mode 100644 packages/frontend/src/scripts/get-user-name.ts delete mode 100644 packages/frontend/src/scripts/hotkey.ts delete mode 100644 packages/frontend/src/scripts/idb-proxy.ts delete mode 100644 packages/frontend/src/scripts/idle-render.ts delete mode 100644 packages/frontend/src/scripts/init-chart.ts delete mode 100644 packages/frontend/src/scripts/initialize-sw.ts delete mode 100644 packages/frontend/src/scripts/intl-const.ts delete mode 100644 packages/frontend/src/scripts/intl-string.ts delete mode 100644 packages/frontend/src/scripts/is-device-darkmode.ts delete mode 100644 packages/frontend/src/scripts/isFfVisibleForMe.ts delete mode 100644 packages/frontend/src/scripts/key-event.ts delete mode 100644 packages/frontend/src/scripts/langmap.ts delete mode 100644 packages/frontend/src/scripts/login-id.ts delete mode 100644 packages/frontend/src/scripts/lookup.ts delete mode 100644 packages/frontend/src/scripts/media-has-audio.ts delete mode 100644 packages/frontend/src/scripts/media-proxy.ts delete mode 100644 packages/frontend/src/scripts/merge.ts delete mode 100644 packages/frontend/src/scripts/mfm-function-picker.ts delete mode 100644 packages/frontend/src/scripts/misskey-api.ts delete mode 100644 packages/frontend/src/scripts/navigator.ts delete mode 100644 packages/frontend/src/scripts/page-metadata.ts delete mode 100644 packages/frontend/src/scripts/physics.ts delete mode 100644 packages/frontend/src/scripts/player-url-transform.ts delete mode 100644 packages/frontend/src/scripts/please-login.ts delete mode 100644 packages/frontend/src/scripts/popout.ts delete mode 100644 packages/frontend/src/scripts/popup-position.ts delete mode 100644 packages/frontend/src/scripts/post-message.ts delete mode 100644 packages/frontend/src/scripts/reaction-picker.ts delete mode 100644 packages/frontend/src/scripts/reload-ask.ts delete mode 100644 packages/frontend/src/scripts/search-emoji.ts delete mode 100644 packages/frontend/src/scripts/select-file.ts delete mode 100644 packages/frontend/src/scripts/show-moved-dialog.ts delete mode 100644 packages/frontend/src/scripts/show-suspended-dialog.ts delete mode 100644 packages/frontend/src/scripts/shuffle.ts delete mode 100644 packages/frontend/src/scripts/snowfall-effect.ts delete mode 100644 packages/frontend/src/scripts/sound.ts delete mode 100644 packages/frontend/src/scripts/sticky-sidebar.ts delete mode 100644 packages/frontend/src/scripts/stream-mock.ts delete mode 100644 packages/frontend/src/scripts/test-utils.ts delete mode 100644 packages/frontend/src/scripts/theme-editor.ts delete mode 100644 packages/frontend/src/scripts/theme.ts delete mode 100644 packages/frontend/src/scripts/time.ts delete mode 100644 packages/frontend/src/scripts/timezones.ts delete mode 100644 packages/frontend/src/scripts/touch.ts delete mode 100644 packages/frontend/src/scripts/unison-reload.ts delete mode 100644 packages/frontend/src/scripts/upload.ts delete mode 100644 packages/frontend/src/scripts/upload/compress-config.ts delete mode 100644 packages/frontend/src/scripts/upload/isWebpSupported.ts delete mode 100644 packages/frontend/src/scripts/use-chart-tooltip.ts delete mode 100644 packages/frontend/src/scripts/use-form.ts delete mode 100644 packages/frontend/src/scripts/use-leave-guard.ts delete mode 100644 packages/frontend/src/scripts/use-note-capture.ts delete mode 100644 packages/frontend/src/scripts/use-tooltip.ts (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts deleted file mode 100644 index f5d0ab559f..0000000000 --- a/packages/frontend/src/scripts/achievements.ts +++ /dev/null @@ -1,513 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { $i } from '@/account.js'; - -export const ACHIEVEMENT_TYPES = [ - 'notes1', - 'notes10', - 'notes100', - 'notes500', - 'notes1000', - 'notes5000', - 'notes10000', - 'notes20000', - 'notes30000', - 'notes40000', - 'notes50000', - 'notes60000', - 'notes70000', - 'notes80000', - 'notes90000', - 'notes100000', - 'login3', - 'login7', - 'login15', - 'login30', - 'login60', - 'login100', - 'login200', - 'login300', - 'login400', - 'login500', - 'login600', - 'login700', - 'login800', - 'login900', - 'login1000', - 'passedSinceAccountCreated1', - 'passedSinceAccountCreated2', - 'passedSinceAccountCreated3', - 'loggedInOnBirthday', - 'loggedInOnNewYearsDay', - 'noteClipped1', - 'noteFavorited1', - 'myNoteFavorited1', - 'profileFilled', - 'markedAsCat', - 'following1', - 'following10', - 'following50', - 'following100', - 'following300', - 'followers1', - 'followers10', - 'followers50', - 'followers100', - 'followers300', - 'followers500', - 'followers1000', - 'collectAchievements30', - 'viewAchievements3min', - 'iLoveMisskey', - 'foundTreasure', - 'client30min', - 'client60min', - 'noteDeletedWithin1min', - 'postedAtLateNight', - 'postedAt0min0sec', - 'selfQuote', - 'htl20npm', - 'viewInstanceChart', - 'outputHelloWorldOnScratchpad', - 'open3windows', - 'driveFolderCircularReference', - 'reactWithoutRead', - 'clickedClickHere', - 'justPlainLucky', - 'setNameToSyuilo', - 'cookieClicked', - 'brainDiver', - 'smashTestNotificationButton', - 'tutorialCompleted', - 'bubbleGameExplodingHead', - 'bubbleGameDoubleExplodingHead', -] as const; - -export const ACHIEVEMENT_BADGES = { - 'notes1': { - img: '/fluent-emoji/1f4dd.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'notes10': { - img: '/fluent-emoji/1f4d1.png', - bg: null, - frame: 'bronze', - }, - 'notes100': { - img: '/fluent-emoji/1f4d2.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'notes500': { - img: '/fluent-emoji/1f4da.png', - bg: null, - frame: 'bronze', - }, - 'notes1000': { - img: '/fluent-emoji/1f5c3.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'notes5000': { - img: '/fluent-emoji/1f304.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'notes10000': { - img: '/fluent-emoji/1f3d9.png', - bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', - frame: 'silver', - }, - 'notes20000': { - img: '/fluent-emoji/1f307.png', - bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', - frame: 'silver', - }, - 'notes30000': { - img: '/fluent-emoji/1f306.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'silver', - }, - 'notes40000': { - img: '/fluent-emoji/1f303.png', - bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', - frame: 'silver', - }, - 'notes50000': { - img: '/fluent-emoji/1fa90.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'gold', - }, - 'notes60000': { - img: '/fluent-emoji/2604.png', - bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', - frame: 'gold', - }, - 'notes70000': { - img: '/fluent-emoji/1f30c.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'gold', - }, - 'notes80000': { - img: '/fluent-emoji/1f30c.png', - bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', - frame: 'gold', - }, - 'notes90000': { - img: '/fluent-emoji/1f30c.png', - bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))', - frame: 'gold', - }, - 'notes100000': { - img: '/fluent-emoji/267e.png', - bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))', - frame: 'platinum', - }, - 'login3': { - img: '/fluent-emoji/1f331.png', - bg: null, - frame: 'bronze', - }, - 'login7': { - img: '/fluent-emoji/1f331.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'login15': { - img: '/fluent-emoji/1f331.png', - bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', - frame: 'bronze', - }, - 'login30': { - img: '/fluent-emoji/1fab4.png', - bg: null, - frame: 'bronze', - }, - 'login60': { - img: '/fluent-emoji/1fab4.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'login100': { - img: '/fluent-emoji/1fab4.png', - bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', - frame: 'silver', - }, - 'login200': { - img: '/fluent-emoji/1f333.png', - bg: null, - frame: 'silver', - }, - 'login300': { - img: '/fluent-emoji/1f333.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'silver', - }, - 'login400': { - img: '/fluent-emoji/1f333.png', - bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', - frame: 'silver', - }, - 'login500': { - img: '/fluent-emoji/1f304.png', - bg: null, - frame: 'silver', - }, - 'login600': { - img: '/fluent-emoji/1f304.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'gold', - }, - 'login700': { - img: '/fluent-emoji/1f304.png', - bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', - frame: 'gold', - }, - 'login800': { - img: '/fluent-emoji/1f307.png', - bg: null, - frame: 'gold', - }, - 'login900': { - img: '/fluent-emoji/1f307.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'gold', - }, - 'login1000': { - img: '/fluent-emoji/1f307.png', - bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', - frame: 'platinum', - }, - 'noteClipped1': { - img: '/fluent-emoji/1f587.png', - bg: null, - frame: 'bronze', - }, - 'noteFavorited1': { - img: '/fluent-emoji/1f31f.png', - bg: null, - frame: 'bronze', - }, - 'myNoteFavorited1': { - img: '/fluent-emoji/1f320.png', - bg: null, - frame: 'silver', - }, - 'profileFilled': { - img: '/fluent-emoji/1f44c.png', - bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', - frame: 'bronze', - }, - 'markedAsCat': { - img: '/fluent-emoji/1f408.png', - bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', - frame: 'bronze', - }, - 'following1': { - img: '/fluent-emoji/2618.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'following10': { - img: '/fluent-emoji/1f6b8.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'following50': { - img: '/fluent-emoji/1f91d.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'following100': { - img: '/fluent-emoji/1f4af.png', - bg: 'linear-gradient(0deg, rgb(255 53 184), rgb(255 206 69))', - frame: 'silver', - }, - 'following300': { - img: '/fluent-emoji/1f970.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'silver', - }, - 'followers1': { - img: '/fluent-emoji/2618.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'followers10': { - img: '/fluent-emoji/1f44b.png', - bg: 'linear-gradient(0deg, rgb(59 187 116), rgb(199 211 102))', - frame: 'bronze', - }, - 'followers50': { - img: '/fluent-emoji/1f411.png', - bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', - frame: 'bronze', - }, - 'followers100': { - img: '/fluent-emoji/1f60e.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'silver', - }, - 'followers300': { - img: '/fluent-emoji/1f3c6.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'silver', - }, - 'followers500': { - img: '/fluent-emoji/1f4e1.png', - bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', - frame: 'gold', - }, - 'followers1000': { - img: '/fluent-emoji/1f451.png', - bg: 'linear-gradient(0deg, rgb(255 232 119), rgb(255 140 41))', - frame: 'platinum', - }, - 'collectAchievements30': { - img: '/fluent-emoji/1f3c5.png', - bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', - frame: 'silver', - }, - 'viewAchievements3min': { - img: '/fluent-emoji/1f3c5.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'bronze', - }, - 'iLoveMisskey': { - img: '/fluent-emoji/2764.png', - bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', - frame: 'silver', - }, - 'foundTreasure': { - img: '/fluent-emoji/1f3c6.png', - bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', - frame: 'gold', - }, - 'client30min': { - img: '/fluent-emoji/1f552.png', - bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', - frame: 'bronze', - }, - 'client60min': { - img: '/fluent-emoji/1f552.png', - bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', - frame: 'silver', - }, - 'noteDeletedWithin1min': { - img: '/fluent-emoji/1f5d1.png', - bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', - frame: 'bronze', - }, - 'postedAtLateNight': { - img: '/fluent-emoji/1f319.png', - bg: 'linear-gradient(0deg, rgb(197 69 192), rgb(2 112 155))', - frame: 'bronze', - }, - 'postedAt0min0sec': { - img: '/fluent-emoji/1f55b.png', - bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))', - frame: 'bronze', - }, - 'selfQuote': { - img: '/fluent-emoji/1f4dd.png', - bg: null, - frame: 'bronze', - }, - 'htl20npm': { - img: '/fluent-emoji/1f30a.png', - bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', - frame: 'bronze', - }, - 'viewInstanceChart': { - img: '/fluent-emoji/1f4ca.png', - bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))', - frame: 'bronze', - }, - 'outputHelloWorldOnScratchpad': { - img: '/fluent-emoji/1f530.png', - bg: 'linear-gradient(0deg, rgb(58 231 198), rgb(37 194 255))', - frame: 'bronze', - }, - 'open3windows': { - img: '/fluent-emoji/1f5a5.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'bronze', - }, - 'driveFolderCircularReference': { - img: '/fluent-emoji/1f4c2.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'bronze', - }, - 'reactWithoutRead': { - img: '/fluent-emoji/2753.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'bronze', - }, - 'clickedClickHere': { - img: '/fluent-emoji/2757.png', - bg: 'linear-gradient(0deg, rgb(144 224 255), rgb(255 168 252))', - frame: 'bronze', - }, - 'justPlainLucky': { - img: '/fluent-emoji/1f340.png', - bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', - frame: 'silver', - }, - 'setNameToSyuilo': { - img: '/fluent-emoji/1f36e.png', - bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', - frame: 'bronze', - }, - 'passedSinceAccountCreated1': { - img: '/fluent-emoji/0031-20e3.png', - bg: null, - frame: 'bronze', - }, - 'passedSinceAccountCreated2': { - img: '/fluent-emoji/0032-20e3.png', - bg: null, - frame: 'silver', - }, - 'passedSinceAccountCreated3': { - img: '/fluent-emoji/0033-20e3.png', - bg: null, - frame: 'gold', - }, - 'loggedInOnBirthday': { - img: '/fluent-emoji/1f382.png', - bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', - frame: 'silver', - }, - 'loggedInOnNewYearsDay': { - img: '/fluent-emoji/1f38d.png', - bg: 'linear-gradient(0deg, rgb(255 144 144), rgb(255 232 168))', - frame: 'silver', - }, - 'cookieClicked': { - img: '/fluent-emoji/1f36a.png', - bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', - frame: 'bronze', - }, - 'brainDiver': { - img: '/fluent-emoji/1f9e0.png', - bg: 'linear-gradient(0deg, rgb(144, 224, 255), rgb(255, 168, 252))', - frame: 'bronze', - }, - 'smashTestNotificationButton': { - img: '/fluent-emoji/1f514.png', - bg: 'linear-gradient(0deg, rgb(187 183 59), rgb(255 143 77))', - frame: 'bronze', - }, - 'tutorialCompleted': { - img: '/fluent-emoji/1f393.png', - 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 -} as const satisfies Record; - */ -} as const; - -export const claimedAchievements: typeof ACHIEVEMENT_TYPES[number][] = ($i && $i.achievements) ? $i.achievements.map(x => x.name) : []; - -const claimingQueue = new Set(); - -export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) { - if ($i == null) return; - if ($i.movedTo) return; - if (claimedAchievements.includes(type)) return; - claimingQueue.add(type); - claimedAchievements.push(type); - await new Promise(resolve => setTimeout(resolve, (claimingQueue.size - 1) * 500)); - window.setTimeout(() => { - claimingQueue.delete(type); - }, 500); - misskeyApi('i/claim-achievement', { name: type }); -} - -if (_DEV_) { - (window as any).unlockAllAchievements = () => { - for (const t of ACHIEVEMENT_TYPES) { - claimAchievement(t); - } - }; -} diff --git a/packages/frontend/src/scripts/admin-lookup.ts b/packages/frontend/src/scripts/admin-lookup.ts deleted file mode 100644 index 1b57b853c9..0000000000 --- a/packages/frontend/src/scripts/admin-lookup.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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({ - title: i18n.ts.usernameOrUserId, - }); - if (canceled) return; - - const show = (user) => { - os.pageWindow(`/admin/user/${user.id}`); - }; - - const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result)); - const idPromise = misskeyApi('users/show', { userId: result }); - let _notFound = false; - const notFound = () => { - if (_notFound) { - os.alert({ - type: 'error', - text: i18n.ts.noSuchUser, - }); - } else { - _notFound = true; - } - }; - usernamePromise.then(show).catch(err => { - if (err.code === 'NO_SUCH_USER') { - notFound(); - } - }); - idPromise.then(show).catch(err => { - notFound(); - }); -} - -export async function lookupUserByEmail() { - const { canceled, result } = await os.inputText({ - title: i18n.ts.emailAddress, - type: 'email', - }); - if (canceled) return; - - try { - const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result }); - - os.pageWindow(`/admin/user/${user.id}`); - } catch (err) { - if (err.code === 'USER_NOT_FOUND') { - os.alert({ - type: 'error', - text: i18n.ts.noSuchUser, - }); - } else { - throw err; - } - } -} - -export async function lookupFile() { - const { canceled, result: q } = await os.inputText({ - title: i18n.ts.fileIdOrUrl, - minLength: 1, - }); - if (canceled) return; - - const show = (file) => { - os.pageWindow(`/admin/file/${file.id}`); - }; - - 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') { - os.alert({ - type: 'error', - text: i18n.ts.notFound, - }); - } - }); -} diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts deleted file mode 100644 index 2c0c8c816e..0000000000 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { errors, utils, values } from '@syuilo/aiscript'; -import * as Misskey from 'misskey-js'; -import { url, lang } from '@@/js/config.js'; -import { assertStringAndIsIn } from './common.js'; -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'; - -const DIALOG_TYPES = [ - 'error', - 'info', - 'success', - 'warning', - 'waiting', - 'question', -] as const; - -export function aiScriptReadline(q: string): Promise { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ result: a }) => { - ok(a ?? ''); - }); - }); -} - -export function createAiScriptEnv(opts: { storageKey: string, token?: string }) { - return { - USER_ID: $i ? values.STR($i.id) : values.NULL, - USER_NAME: $i?.name ? values.STR($i.name) : values.NULL, - USER_USERNAME: $i ? values.STR($i.username) : values.NULL, - CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value), - LOCALE: values.STR(lang), - SERVER_URL: values.STR(url), - 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { - utils.assertString(title); - utils.assertString(text); - if (type != null) { - assertStringAndIsIn(type, DIALOG_TYPES); - } - await os.alert({ - type: type ? type.value : 'info', - title: title.value, - text: text.value, - }); - return values.NULL; - }), - 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { - utils.assertString(title); - utils.assertString(text); - if (type != null) { - assertStringAndIsIn(type, DIALOG_TYPES); - } - const confirm = await os.confirm({ - type: type ? type.value : 'question', - title: title.value, - text: text.value, - }); - return confirm.canceled ? values.FALSE : values.TRUE; - }), - 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { - utils.assertString(ep); - if (ep.value.includes('://')) { - throw new errors.AiScriptRuntimeError('invalid endpoint'); - } - if (token) { - utils.assertString(token); - // バグがあればundefinedもあり得るため念のため - if (typeof token.value !== 'string') throw new Error('invalid token'); - } - const actualToken: string | null = token?.value ?? opts.token ?? null; - if (param == null) { - throw new errors.AiScriptRuntimeError('expected param'); - } - utils.assertObject(param); - return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => { - return utils.jsToVal(res); - }, err => { - return values.ERROR('request_failed', utils.jsToVal(err)); - }); - }), - /* セキュリティ上の問題があるため無効化 - 'Mk:apiExternal': values.FN_NATIVE(async ([host, ep, param, token]) => { - utils.assertString(host); - utils.assertString(ep); - if (token) utils.assertString(token); - return os.apiExternal(host.value, ep.value, utils.valToJs(param), token?.value).then(res => { - return utils.jsToVal(res); - }, err => { - return values.ERROR('request_failed', utils.jsToVal(err)); - }); - }), - */ - 'Mk:save': values.FN_NATIVE(([key, value]) => { - utils.assertString(key); - utils.expectAny(value); - miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value))); - return values.NULL; - }), - 'Mk:load': values.FN_NATIVE(([key]) => { - utils.assertString(key); - return utils.jsToVal(miLocalStorage.getItemAsJson(`aiscript:${opts.storageKey}:${key.value}`) ?? null); - }), - 'Mk:remove': values.FN_NATIVE(([key]) => { - utils.assertString(key); - miLocalStorage.removeItem(`aiscript:${opts.storageKey}:${key.value}`); - return values.NULL; - }), - 'Mk:url': values.FN_NATIVE(() => { - return values.STR(window.location.href); - }), - 'Mk:nyaize': values.FN_NATIVE(([text]) => { - utils.assertString(text); - return values.STR(Misskey.nyaize(text.value)); - }), - }; -} diff --git a/packages/frontend/src/scripts/aiscript/common.ts b/packages/frontend/src/scripts/aiscript/common.ts deleted file mode 100644 index ba5dfb8368..0000000000 --- a/packages/frontend/src/scripts/aiscript/common.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { errors, utils } from '@syuilo/aiscript'; -import type { values } from '@syuilo/aiscript'; - -export function assertStringAndIsIn(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } { - utils.assertString(value); - const str = value.value; - if (!expects.includes(str)) { - const expected = expects.map((expect) => `"${expect}"`).join(', '); - throw new errors.AiScriptRuntimeError(`"${value.value}" is not in ${expected}`); - } -} diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts deleted file mode 100644 index 46e193f7c1..0000000000 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ /dev/null @@ -1,655 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { utils, values } from '@syuilo/aiscript'; -import { v4 as uuid } from 'uuid'; -import { ref } from 'vue'; -import type { Ref } from 'vue'; -import * as Misskey from 'misskey-js'; -import { assertStringAndIsIn } from './common.js'; - -const ALIGNS = ['left', 'center', 'right'] as const; -const FONTS = ['serif', 'sans-serif', 'monospace'] as const; -const BORDER_STYLES = ['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] as const; - -type Align = (typeof ALIGNS)[number]; -type Font = (typeof FONTS)[number]; -type BorderStyle = (typeof BORDER_STYLES)[number]; - -export type AsUiComponentBase = { - id: string; - hidden?: boolean; -}; - -export type AsUiRoot = AsUiComponentBase & { - type: 'root'; - children: AsUiComponent['id'][]; -}; - -export type AsUiContainer = AsUiComponentBase & { - type: 'container'; - children?: AsUiComponent['id'][]; - align?: Align; - bgColor?: string; - fgColor?: string; - font?: Font; - borderWidth?: number; - borderColor?: string; - borderStyle?: BorderStyle; - borderRadius?: number; - padding?: number; - rounded?: boolean; - hidden?: boolean; -}; - -export type AsUiText = AsUiComponentBase & { - type: 'text'; - text?: string; - size?: number; - bold?: boolean; - color?: string; - font?: Font; -}; - -export type AsUiMfm = AsUiComponentBase & { - type: 'mfm'; - text?: string; - size?: number; - bold?: boolean; - color?: string; - font?: Font; - onClickEv?: (evId: string) => Promise; -}; - -export type AsUiButton = AsUiComponentBase & { - type: 'button'; - text?: string; - onClick?: () => Promise; - primary?: boolean; - rounded?: boolean; - disabled?: boolean; -}; - -export type AsUiButtons = AsUiComponentBase & { - type: 'buttons'; - buttons?: AsUiButton[]; -}; - -export type AsUiSwitch = AsUiComponentBase & { - type: 'switch'; - onChange?: (v: boolean) => Promise; - default?: boolean; - label?: string; - caption?: string; -}; - -export type AsUiTextarea = AsUiComponentBase & { - type: 'textarea'; - onInput?: (v: string) => Promise; - default?: string; - label?: string; - caption?: string; -}; - -export type AsUiTextInput = AsUiComponentBase & { - type: 'textInput'; - onInput?: (v: string) => Promise; - default?: string; - label?: string; - caption?: string; -}; - -export type AsUiNumberInput = AsUiComponentBase & { - type: 'numberInput'; - onInput?: (v: number) => Promise; - default?: number; - label?: string; - caption?: string; -}; - -export type AsUiSelect = AsUiComponentBase & { - type: 'select'; - items?: { - text: string; - value: string; - }[]; - onChange?: (v: string) => Promise; - default?: string; - label?: string; - caption?: string; -}; - -export type AsUiFolder = AsUiComponentBase & { - type: 'folder'; - children?: AsUiComponent['id'][]; - title?: string; - opened?: boolean; -}; - -type PostFormPropsForAsUi = { - text: string; - cw?: string; - visibility?: (typeof Misskey.noteVisibilities)[number]; - localOnly?: boolean; -}; - -export type AsUiPostFormButton = AsUiComponentBase & { - type: 'postFormButton'; - text?: string; - primary?: boolean; - rounded?: boolean; - form?: PostFormPropsForAsUi; -}; - -export type AsUiPostForm = AsUiComponentBase & { - type: 'postForm'; - form?: PostFormPropsForAsUi; -}; - -export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm; - -type Options = T extends AsUiButtons - ? Omit & { 'buttons'?: Options[] } - : Omit; - -export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise) { - // TODO -} - -function getRootOptions(def: values.Value | undefined): Options { - utils.assertObject(def); - - const children = def.value.get('children'); - utils.assertArray(children); - - return { - children: children.value.map(v => { - utils.assertObject(v); - const id = v.value.get('id'); - utils.assertString(id); - return id.value; - }), - }; -} - -function getContainerOptions(def: values.Value | undefined): Options { - utils.assertObject(def); - - const children = def.value.get('children'); - if (children) utils.assertArray(children); - const align = def.value.get('align'); - if (align) assertStringAndIsIn(align, ALIGNS); - const bgColor = def.value.get('bgColor'); - if (bgColor) utils.assertString(bgColor); - const fgColor = def.value.get('fgColor'); - if (fgColor) utils.assertString(fgColor); - const font = def.value.get('font'); - if (font) assertStringAndIsIn(font, FONTS); - const borderWidth = def.value.get('borderWidth'); - if (borderWidth) utils.assertNumber(borderWidth); - const borderColor = def.value.get('borderColor'); - if (borderColor) utils.assertString(borderColor); - const borderStyle = def.value.get('borderStyle'); - if (borderStyle) assertStringAndIsIn(borderStyle, BORDER_STYLES); - const borderRadius = def.value.get('borderRadius'); - if (borderRadius) utils.assertNumber(borderRadius); - const padding = def.value.get('padding'); - if (padding) utils.assertNumber(padding); - const rounded = def.value.get('rounded'); - if (rounded) utils.assertBoolean(rounded); - const hidden = def.value.get('hidden'); - if (hidden) utils.assertBoolean(hidden); - - return { - children: children ? children.value.map(v => { - utils.assertObject(v); - const id = v.value.get('id'); - utils.assertString(id); - return id.value; - }) : [], - align: align?.value, - fgColor: fgColor?.value, - bgColor: bgColor?.value, - font: font?.value, - borderWidth: borderWidth?.value, - borderColor: borderColor?.value, - borderStyle: borderStyle?.value, - borderRadius: borderRadius?.value, - padding: padding?.value, - rounded: rounded?.value, - hidden: hidden?.value, - }; -} - -function getTextOptions(def: values.Value | undefined): Options { - utils.assertObject(def); - - const text = def.value.get('text'); - if (text) utils.assertString(text); - const size = def.value.get('size'); - if (size) utils.assertNumber(size); - const bold = def.value.get('bold'); - if (bold) utils.assertBoolean(bold); - const color = def.value.get('color'); - if (color) utils.assertString(color); - const font = def.value.get('font'); - if (font) assertStringAndIsIn(font, FONTS); - - return { - text: text?.value, - size: size?.value, - bold: bold?.value, - color: color?.value, - font: font?.value, - }; -} - -function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const text = def.value.get('text'); - if (text) utils.assertString(text); - const size = def.value.get('size'); - if (size) utils.assertNumber(size); - const bold = def.value.get('bold'); - if (bold) utils.assertBoolean(bold); - const color = def.value.get('color'); - if (color) utils.assertString(color); - const font = def.value.get('font'); - if (font) assertStringAndIsIn(font, FONTS); - const onClickEv = def.value.get('onClickEv'); - if (onClickEv) utils.assertFunction(onClickEv); - - return { - text: text?.value, - size: size?.value, - bold: bold?.value, - color: color?.value, - font: font?.value, - onClickEv: async (evId: string) => { - if (onClickEv) await call(onClickEv, [values.STR(evId)]); - }, - }; -} - -function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const onInput = def.value.get('onInput'); - if (onInput) utils.assertFunction(onInput); - const defaultValue = def.value.get('default'); - if (defaultValue) utils.assertString(defaultValue); - const label = def.value.get('label'); - if (label) utils.assertString(label); - const caption = def.value.get('caption'); - if (caption) utils.assertString(caption); - - return { - onInput: async (v) => { - if (onInput) await call(onInput, [utils.jsToVal(v)]); - }, - default: defaultValue?.value, - label: label?.value, - caption: caption?.value, - }; -} - -function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const onInput = def.value.get('onInput'); - if (onInput) utils.assertFunction(onInput); - const defaultValue = def.value.get('default'); - if (defaultValue) utils.assertString(defaultValue); - const label = def.value.get('label'); - if (label) utils.assertString(label); - const caption = def.value.get('caption'); - if (caption) utils.assertString(caption); - - return { - onInput: async (v) => { - if (onInput) await call(onInput, [utils.jsToVal(v)]); - }, - default: defaultValue?.value, - label: label?.value, - caption: caption?.value, - }; -} - -function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const onInput = def.value.get('onInput'); - if (onInput) utils.assertFunction(onInput); - const defaultValue = def.value.get('default'); - if (defaultValue) utils.assertNumber(defaultValue); - const label = def.value.get('label'); - if (label) utils.assertString(label); - const caption = def.value.get('caption'); - if (caption) utils.assertString(caption); - - return { - onInput: async (v) => { - if (onInput) await call(onInput, [utils.jsToVal(v)]); - }, - default: defaultValue?.value, - label: label?.value, - caption: caption?.value, - }; -} - -function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const text = def.value.get('text'); - if (text) utils.assertString(text); - const onClick = def.value.get('onClick'); - if (onClick) utils.assertFunction(onClick); - const primary = def.value.get('primary'); - if (primary) utils.assertBoolean(primary); - const rounded = def.value.get('rounded'); - if (rounded) utils.assertBoolean(rounded); - const disabled = def.value.get('disabled'); - if (disabled) utils.assertBoolean(disabled); - - return { - text: text?.value, - onClick: async () => { - if (onClick) await call(onClick, []); - }, - primary: primary?.value, - rounded: rounded?.value, - disabled: disabled?.value, - }; -} - -function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const buttons = def.value.get('buttons'); - if (buttons) utils.assertArray(buttons); - - return { - buttons: buttons ? buttons.value.map(button => { - utils.assertObject(button); - const text = button.value.get('text'); - utils.assertString(text); - const onClick = button.value.get('onClick'); - utils.assertFunction(onClick); - const primary = button.value.get('primary'); - if (primary) utils.assertBoolean(primary); - const rounded = button.value.get('rounded'); - if (rounded) utils.assertBoolean(rounded); - const disabled = button.value.get('disabled'); - if (disabled) utils.assertBoolean(disabled); - - return { - text: text.value, - onClick: async () => { - await call(onClick, []); - }, - primary: primary?.value, - rounded: rounded?.value, - disabled: disabled?.value, - }; - }) : [], - }; -} - -function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const onChange = def.value.get('onChange'); - if (onChange) utils.assertFunction(onChange); - const defaultValue = def.value.get('default'); - if (defaultValue) utils.assertBoolean(defaultValue); - const label = def.value.get('label'); - if (label) utils.assertString(label); - const caption = def.value.get('caption'); - if (caption) utils.assertString(caption); - - return { - onChange: async (v) => { - if (onChange) await call(onChange, [utils.jsToVal(v)]); - }, - default: defaultValue?.value, - label: label?.value, - caption: caption?.value, - }; -} - -function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const items = def.value.get('items'); - if (items) utils.assertArray(items); - const onChange = def.value.get('onChange'); - if (onChange) utils.assertFunction(onChange); - const defaultValue = def.value.get('default'); - if (defaultValue) utils.assertString(defaultValue); - const label = def.value.get('label'); - if (label) utils.assertString(label); - const caption = def.value.get('caption'); - if (caption) utils.assertString(caption); - - return { - items: items ? items.value.map(item => { - utils.assertObject(item); - const text = item.value.get('text'); - utils.assertString(text); - const value = item.value.get('value'); - if (value) utils.assertString(value); - return { - text: text.value, - value: value ? value.value : text.value, - }; - }) : [], - onChange: async (v) => { - if (onChange) await call(onChange, [utils.jsToVal(v)]); - }, - default: defaultValue?.value, - label: label?.value, - caption: caption?.value, - }; -} - -function getFolderOptions(def: values.Value | undefined): Options { - utils.assertObject(def); - - const children = def.value.get('children'); - if (children) utils.assertArray(children); - const title = def.value.get('title'); - if (title) utils.assertString(title); - const opened = def.value.get('opened'); - if (opened) utils.assertBoolean(opened); - - return { - children: children ? children.value.map(v => { - utils.assertObject(v); - const id = v.value.get('id'); - utils.assertString(id); - return id.value; - }) : [], - title: title?.value ?? '', - opened: opened?.value ?? true, - }; -} - -function getPostFormProps(form: values.VObj): PostFormPropsForAsUi { - const text = form.value.get('text'); - utils.assertString(text); - const cw = form.value.get('cw'); - if (cw) utils.assertString(cw); - const visibility = form.value.get('visibility'); - if (visibility) utils.assertString(visibility); - const localOnly = form.value.get('localOnly'); - if (localOnly) utils.assertBoolean(localOnly); - - return { - text: text.value, - cw: cw?.value, - visibility: (visibility?.value && (Misskey.noteVisibilities as readonly string[]).includes(visibility.value)) ? visibility.value as typeof Misskey.noteVisibilities[number] : undefined, - localOnly: localOnly?.value, - }; -} - -function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const text = def.value.get('text'); - if (text) utils.assertString(text); - const primary = def.value.get('primary'); - if (primary) utils.assertBoolean(primary); - const rounded = def.value.get('rounded'); - if (rounded) utils.assertBoolean(rounded); - const form = def.value.get('form'); - if (form) utils.assertObject(form); - - return { - text: text?.value, - primary: primary?.value, - rounded: rounded?.value, - form: form ? getPostFormProps(form) : { - text: '', - }, - }; -} - -function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise): Options { - utils.assertObject(def); - - const form = def.value.get('form'); - if (form) utils.assertObject(form); - - return { - form: form ? getPostFormProps(form) : { - text: '', - }, - }; -} - -export function registerAsUiLib(components: Ref[], done: (root: Ref) => void) { - type OptionsConverter = (def: values.Value | undefined, call: C) => Options; - - const instances = {}; - - function createComponentInstance( - type: T['type'], - def: values.Value | undefined, - id: values.Value | undefined, - getOptions: OptionsConverter, - call: C, - ) { - if (id) utils.assertString(id); - const _id = id?.value ?? uuid(); - const component = ref({ - ...getOptions(def, call), - type, - id: _id, - } as T); - components.push(component); - const instance = values.OBJ(new Map([ - ['id', values.STR(_id)], - ['update', values.FN_NATIVE(([def], opts) => { - utils.assertObject(def); - const updates = getOptions(def, call); - for (const update of def.value.keys()) { - if (!Object.hasOwn(updates, update)) continue; - component.value[update] = updates[update]; - } - })], - ])); - instances[_id] = instance; - return instance; - } - - const rootInstance = createComponentInstance('root', utils.jsToVal({ children: [] }), utils.jsToVal('___root___'), getRootOptions, () => {}); - const rootComponent = components[0] as Ref; - done(rootComponent); - - return { - 'Ui:root': rootInstance, - - 'Ui:patch': values.FN_NATIVE(([id, val], opts) => { - utils.assertString(id); - utils.assertArray(val); - // patch(id.value, val.value, opts.call); // TODO - }), - - 'Ui:get': values.FN_NATIVE(([id], opts) => { - utils.assertString(id); - const instance = instances[id.value]; - if (instance) { - return instance; - } else { - return values.NULL; - } - }), - - // Ui:root.update({ children: [...] }) の糖衣構文 - 'Ui:render': values.FN_NATIVE(([children], opts) => { - utils.assertArray(children); - - rootComponent.value.children = children.value.map(v => { - utils.assertObject(v); - const id = v.value.get('id'); - utils.assertString(id); - return id.value; - }); - }), - - 'Ui:C:container': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('container', def, id, getContainerOptions, opts.topCall); - }), - - 'Ui:C:text': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('text', def, id, getTextOptions, opts.topCall); - }), - - 'Ui:C:mfm': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('mfm', def, id, getMfmOptions, opts.topCall); - }), - - 'Ui:C:textarea': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('textarea', def, id, getTextareaOptions, opts.topCall); - }), - - 'Ui:C:textInput': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('textInput', def, id, getTextInputOptions, opts.topCall); - }), - - 'Ui:C:numberInput': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('numberInput', def, id, getNumberInputOptions, opts.topCall); - }), - - 'Ui:C:button': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('button', def, id, getButtonOptions, opts.topCall); - }), - - 'Ui:C:buttons': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('buttons', def, id, getButtonsOptions, opts.topCall); - }), - - 'Ui:C:switch': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('switch', def, id, getSwitchOptions, opts.topCall); - }), - - 'Ui:C:select': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('select', def, id, getSelectOptions, opts.topCall); - }), - - 'Ui:C:folder': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('folder', def, id, getFolderOptions, opts.topCall); - }), - - 'Ui:C:postFormButton': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('postFormButton', def, id, getPostFormButtonOptions, opts.topCall); - }), - - 'Ui:C:postForm': values.FN_NATIVE(([def, id], opts) => { - return createComponentInstance('postForm', def, id, getPostFormOptions, opts.topCall); - }), - }; -} diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts deleted file mode 100644 index f2feb29dfc..0000000000 --- a/packages/frontend/src/scripts/array.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -type EndoRelation = (a: T, b: T) => boolean; -type Predicate = (x: T) => boolean; - -/** - * Count the number of elements that satisfy the predicate - */ - -export function countIf(f: Predicate, xs: T[]): number { - return xs.filter(f).length; -} - -/** - * Count the number of elements that is equal to the element - */ -export function count(a: T, xs: T[]): number { - return countIf(x => x === a, xs); -} - -/** - * Concatenate an array of arrays - */ -export function concat(xss: T[][]): T[] { - return ([] as T[]).concat(...xss); -} - -/** - * Intersperse the element between the elements of the array - * @param sep The element to be interspersed - */ -export function intersperse(sep: T, xs: T[]): T[] { - return concat(xs.map(x => [sep, x])).slice(1); -} - -/** - * Returns the array of elements that is not equal to the element - */ -export function erase(a: T, xs: T[]): T[] { - return xs.filter(x => x !== a); -} - -/** - * Finds the array of all elements in the first array not contained in the second array. - * The order of result values are determined by the first array. - */ -export function difference(xs: T[], ys: T[]): T[] { - return xs.filter(x => !ys.includes(x)); -} - -/** - * Remove all but the first element from every group of equivalent elements - */ -export function unique(xs: T[]): T[] { - return [...new Set(xs)]; -} - -export function uniqueBy(values: TValue[], keySelector: (value: TValue) => TKey): TValue[] { - const map = new Map(); - - for (const value of values) { - const key = keySelector(value); - if (!map.has(key)) map.set(key, value); - } - - return [...map.values()]; -} - -export function sum(xs: number[]): number { - return xs.reduce((a, b) => a + b, 0); -} - -export function maximum(xs: number[]): number { - return Math.max(...xs); -} - -/** - * Compare two arrays by lexicographical order - */ -export function lessThan(xs: number[], ys: number[]): boolean { - for (let i = 0; i < Math.min(xs.length, ys.length); i++) { - if (xs[i] < ys[i]) return true; - if (xs[i] > ys[i]) return false; - } - return xs.length < ys.length; -} - -/** - * Returns the longest prefix of elements that satisfy the predicate - */ -export function takeWhile(f: Predicate, xs: T[]): T[] { - const ys: T[] = []; - for (const x of xs) { - if (f(x)) { - ys.push(x); - } else { - break; - } - } - return ys; -} - -export function cumulativeSum(xs: number[]): number[] { - const ys = Array.from(xs); // deep copy - for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1]; - return ys; -} - -export function toArray(x: T | T[] | undefined): T[] { - return Array.isArray(x) ? x : x != null ? [x] : []; -} - -export function toSingle(x: T | T[] | undefined): T | undefined { - return Array.isArray(x) ? x[0] : x; -} diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts deleted file mode 100644 index 9a603b848c..0000000000 --- a/packages/frontend/src/scripts/autocomplete.ts +++ /dev/null @@ -1,323 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { nextTick, ref, defineAsyncComponent } from 'vue'; -import getCaretCoordinates from 'textarea-caret'; -import { toASCII } from 'punycode.js'; -import type { Ref } from 'vue'; -import { popup } from '@/os.js'; - -export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam'; - -export class Autocomplete { - private suggestion: { - x: Ref; - y: Ref; - q: Ref; - close: () => void; - } | null; - private textarea: HTMLInputElement | HTMLTextAreaElement; - private currentType: string; - private textRef: Ref; - private opening: boolean; - private onlyType: SuggestionType[]; - - private get text(): string { - // Use raw .value to get the latest value - // (Because v-model does not update while composition) - return this.textarea.value; - } - - private set text(text: string) { - // Use ref value to notify other watchers - // (Because .value setter never fires input/change events) - this.textRef.value = text; - } - - /** - * 対象のテキストエリアを与えてインスタンスを初期化します。 - */ - constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref, onlyType?: SuggestionType[]) { - //#region BIND - this.onInput = this.onInput.bind(this); - this.complete = this.complete.bind(this); - this.close = this.close.bind(this); - //#endregion - - this.suggestion = null; - this.textarea = textarea; - this.textRef = textRef; - this.opening = false; - this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam']; - - this.attach(); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを開始します。 - */ - public attach() { - this.textarea.addEventListener('input', this.onInput); - } - - /** - * このインスタンスにあるテキストエリアの入力のキャプチャを解除します。 - */ - public detach() { - this.textarea.removeEventListener('input', this.onInput); - this.close(); - } - - /** - * テキスト入力時 - */ - private onInput() { - const caretPos = this.textarea.selectionStart; - const text = this.text.substring(0, caretPos).split('\n').pop()!; - - const mentionIndex = text.lastIndexOf('@'); - const hashtagIndex = text.lastIndexOf('#'); - const emojiIndex = text.lastIndexOf(':'); - const mfmTagIndex = text.lastIndexOf('$'); - const mfmParamIndex = text.lastIndexOf('.'); - - const max = Math.max( - mentionIndex, - hashtagIndex, - emojiIndex, - mfmTagIndex); - - if (max === -1) { - this.close(); - return; - } - - const afterLastMfmParam = text.split(/\$\[[a-zA-Z]+/).pop(); - - const isMention = mentionIndex !== -1; - const isHashtag = hashtagIndex !== -1; - const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam.includes(' '); - const isMfmTag = mfmTagIndex !== -1 && !isMfmParam; - const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); - - let opened = false; - - if (isMention && this.onlyType.includes('user')) { - // ユーザのサジェスト中に@を入力すると、その位置から新たにユーザ名を取りなおそうとしてしまう - // この動きはリモートユーザのサジェストを阻害するので、@を検知したらその位置よりも前の@を探し、 - // ホスト名を含むリモートのユーザ名を全て拾えるようにする - const mentionIndexAlt = text.lastIndexOf('@', mentionIndex - 1); - const username = mentionIndexAlt === -1 - ? text.substring(mentionIndex + 1) - : text.substring(mentionIndexAlt + 1); - if (username !== '' && username.match(/^[a-zA-Z0-9_@.]+$/)) { - this.open('user', username); - opened = true; - } else if (username === '') { - this.open('user', null); - opened = true; - } - } - - if (isHashtag && !opened && this.onlyType.includes('hashtag')) { - const hashtag = text.substring(hashtagIndex + 1); - if (!hashtag.includes(' ')) { - this.open('hashtag', hashtag); - opened = true; - } - } - - if (isEmoji && !opened && this.onlyType.includes('emoji')) { - const emoji = text.substring(emojiIndex + 1); - if (!emoji.includes(' ')) { - this.open('emoji', emoji); - opened = true; - } - } - - if (isMfmTag && !opened && this.onlyType.includes('mfmTag')) { - const mfmTag = text.substring(mfmTagIndex + 1); - if (!mfmTag.includes(' ')) { - this.open('mfmTag', mfmTag.replace('[', '')); - opened = true; - } - } - - 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(); - } - } - - /** - * サジェストを提示します。 - */ - private async open(type: string, q: any) { - if (type !== this.currentType) { - this.close(); - } - if (this.opening) return; - this.opening = true; - this.currentType = type; - - //#region サジェストを表示すべき位置を計算 - const caretPosition = getCaretCoordinates(this.textarea, this.textarea.selectionStart); - - const rect = this.textarea.getBoundingClientRect(); - - const x = rect.left + caretPosition.left - this.textarea.scrollLeft; - const y = rect.top + caretPosition.top - this.textarea.scrollTop; - //#endregion - - if (this.suggestion) { - this.suggestion.x.value = x; - this.suggestion.y.value = y; - this.suggestion.q.value = q; - - this.opening = false; - } else { - const _x = ref(x); - const _y = ref(y); - const _q = ref(q); - - const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), { - textarea: this.textarea, - close: this.close, - type: type, - q: _q, - x: _x, - y: _y, - }, { - done: (res) => { - this.complete(res); - }, - }); - - this.suggestion = { - q: _q, - x: _x, - y: _y, - close: () => dispose(), - }; - - this.opening = false; - } - } - - /** - * サジェストを閉じます。 - */ - private close() { - if (this.suggestion == null) return; - - this.suggestion.close(); - this.suggestion = null; - - this.textarea.focus(); - } - - /** - * オートコンプリートする - */ - private complete({ type, value }) { - this.close(); - - const caret = this.textarea.selectionStart; - - if (type === 'user') { - const source = this.text; - - const before = source.substring(0, caret); - const trimmedBefore = before.substring(0, before.lastIndexOf('@')); - const after = source.substring(caret); - - const acct = value.host === null ? value.username : `${value.username}@${toASCII(value.host)}`; - - // 挿入 - this.text = `${trimmedBefore}@${acct} ${after}`; - - // キャレットを戻す - nextTick(() => { - this.textarea.focus(); - const pos = trimmedBefore.length + (acct.length + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type === 'hashtag') { - const source = this.text; - - const before = source.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 + 2); - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type === 'emoji') { - 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; - this.textarea.setSelectionRange(pos, pos); - }); - } else if (type === 'mfmTag') { - 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 + 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/autogen/settings-search-index.ts b/packages/frontend/src/scripts/autogen/settings-search-index.ts deleted file mode 100644 index 8e2a896b17..0000000000 --- a/packages/frontend/src/scripts/autogen/settings-search-index.ts +++ /dev/null @@ -1,822 +0,0 @@ - -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// This file was automatically generated by create-search-index. -// Do not edit this file. - -import { i18n } from '@/i18n.js'; - -export type SearchIndexItem = { - id: string; - path?: string; - label: string; - keywords: string[]; - icon?: string; - children?: SearchIndexItem[]; -}; - -export const searchIndexes: SearchIndexItem[] = [ - { - id: 'flXd1LC7r', - children: [ - { - id: 'hB11H5oul', - label: i18n.ts.syncDeviceDarkMode, - keywords: ['sync', 'device', 'dark', 'light', 'mode'], - }, - { - id: 'fDbLtIKeo', - label: i18n.ts.themeForLightMode, - keywords: ['light', 'theme'], - }, - { - id: 'eLOwK5Ia2', - label: i18n.ts.themeForDarkMode, - keywords: ['dark', 'theme'], - }, - { - id: 'ujvMfyzUr', - label: i18n.ts.setWallpaper, - keywords: ['wallpaper'], - }, - ], - label: i18n.ts.theme, - keywords: ['theme'], - path: '/settings/theme', - icon: 'ti ti-palette', - }, - { - id: '6fFIRXUww', - children: [ - { - id: 'nO7NnzqiC', - label: i18n.ts.notUseSound, - keywords: ['mute'], - }, - { - id: 'oALW4ja7U', - label: i18n.ts.useSoundOnlyWhenActive, - keywords: ['active', 'mute'], - }, - { - id: 'BbJK2SKT2', - label: i18n.ts.masterVolume, - keywords: ['volume', 'master'], - }, - ], - label: i18n.ts.sounds, - keywords: ['sounds'], - path: '/settings/sounds', - icon: 'ti ti-music', - }, - { - id: '5BjnxMfYV', - children: [ - { - id: '3UqdSCaFw', - children: [ - { - id: '75QPEg57v', - label: i18n.ts.changePassword, - keywords: [], - }, - ], - label: i18n.ts.password, - keywords: ['password'], - }, - { - id: '2fa', - children: [ - { - id: 'qCXM0HtJ7', - label: i18n.ts.totp, - keywords: ['totp', 'app', i18n.ts.totpDescription], - }, - { - id: '3g1RePuD9', - label: i18n.ts.securityKeyAndPasskey, - keywords: ['security', 'key', 'passkey'], - }, - { - id: 'pFRud5u8k', - label: i18n.ts.passwordLessLogin, - keywords: ['password', 'less', 'key', 'passkey', 'login', 'signin', i18n.ts.passwordLessLoginDescription], - }, - ], - label: i18n.ts['2fa'], - keywords: ['2fa'], - }, - ], - label: i18n.ts.security, - keywords: ['security'], - path: '/settings/security', - icon: 'ti ti-lock', - }, - { - id: 'w4L6myH61', - children: [ - { - id: 'ru8DrOn3J', - label: i18n.ts._profile.changeBanner, - keywords: ['banner', 'change'], - }, - { - id: 'CCnD8Apnu', - label: i18n.ts._profile.changeAvatar, - keywords: ['avatar', 'icon', 'change'], - }, - { - id: 'yFEVCJxFX', - label: i18n.ts._profile.name, - keywords: ['name'], - }, - { - id: '2O1S5reaB', - label: i18n.ts._profile.description, - keywords: ['description', 'bio'], - }, - { - id: 'pWi4OLS8g', - label: i18n.ts.location, - keywords: ['location', 'locale'], - }, - { - id: 'oLO5X6Wtw', - label: i18n.ts.birthday, - keywords: ['birthday', 'birthdate', 'age'], - }, - { - id: 'm2trKwPgq', - label: i18n.ts.language, - keywords: ['language', 'locale'], - }, - { - id: 'kfDZxCDp9', - label: i18n.ts._profile.metadataEdit, - keywords: ['metadata'], - }, - { - id: 'uPt3MFymp', - label: i18n.ts._profile.followedMessage, - keywords: ['follow', 'message', i18n.ts._profile.followedMessageDescription], - }, - { - id: 'wuGg0tBjw', - label: i18n.ts.reactionAcceptance, - keywords: ['reaction'], - }, - { - id: 'EezPpmMnf', - children: [ - { - id: 'f2cRLh8ad', - label: i18n.ts.flagAsCat, - keywords: ['cat'], - }, - { - id: 'eVoViiF3h', - label: i18n.ts.flagAsBot, - keywords: ['bot'], - }, - ], - label: i18n.ts.advancedSettings, - keywords: [], - }, - ], - label: i18n.ts.profile, - keywords: ['profile'], - path: '/settings/profile', - icon: 'ti ti-user', - }, - { - id: '2rp9ka5Ht', - children: [ - { - id: 'qBUSKPxLW', - label: i18n.ts.makeFollowManuallyApprove, - keywords: ['follow', 'lock', i18n.ts.lockedAccountInfo], - }, - { - id: '3LZBlZCej', - label: i18n.ts.autoAcceptFollowed, - keywords: ['follow', 'auto', 'accept'], - }, - { - id: '9gOp28wKG', - label: i18n.ts.makeReactionsPublic, - keywords: ['reaction', 'public', i18n.ts.makeReactionsPublicDescription], - }, - { - id: 'CjAkqMhct', - label: i18n.ts.followingVisibility, - keywords: ['following', 'visibility'], - }, - { - id: '4nEwI6LYt', - label: i18n.ts.followersVisibility, - keywords: ['follower', 'visibility'], - }, - { - id: 'naMp37wTL', - label: i18n.ts.hideOnlineStatus, - keywords: ['online', 'status', i18n.ts.hideOnlineStatusDescription], - }, - { - id: 'p0dCVR0UP', - label: i18n.ts.noCrawle, - keywords: ['crawle', 'index', 'search', i18n.ts.noCrawleDescription], - }, - { - id: 'aceURmNPq', - label: i18n.ts.preventAiLearning, - keywords: ['crawle', 'ai', i18n.ts.preventAiLearningDescription], - }, - { - id: 'ahABA0j7u', - label: i18n.ts.makeExplorable, - keywords: ['explore', i18n.ts.makeExplorableDescription], - }, - { - id: 'cyeDbLN8N', - children: [ - { - id: 'xEYlOghao', - label: i18n.ts._accountSettings.requireSigninToViewContents, - keywords: ['login', 'signin'], - }, - { - id: 'sMmYFCS60', - label: i18n.ts._accountSettings.makeNotesFollowersOnlyBefore, - keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription], - }, - { - id: '2prkeWRSd', - label: i18n.ts._accountSettings.makeNotesHiddenBefore, - keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription], - }, - ], - label: i18n.ts.lockdown, - keywords: ['lockdown'], - }, - ], - label: i18n.ts.privacy, - keywords: ['privacy'], - path: '/settings/privacy', - icon: 'ti ti-lock-open', - }, - { - id: '3yCAv0IsZ', - children: [ - { - id: 'x1GWSQnPw', - label: i18n.ts.uiLanguage, - keywords: ['language'], - }, - { - id: 'EOSa4rtt3', - label: i18n.ts.overridedDeviceKind, - keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'], - }, - { - id: 'm9LhX8BG8', - label: i18n.ts.showFixedPostForm, - keywords: ['post', 'form', 'timeline'], - }, - { - id: 'snyCQ5oKE', - label: i18n.ts.showFixedPostFormInChannel, - keywords: ['post', 'form', 'timeline', 'channel'], - }, - { - id: '8j36S4Ev6', - label: i18n.ts.pinnedList, - keywords: ['pinned', 'list'], - }, - { - id: 'CWpyT9vLK', - label: i18n.ts.enableQuickAddMfmFunction, - keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'], - }, - { - id: '1yhown1Xc', - label: i18n.ts.rememberNoteVisibility, - keywords: ['remember', 'keep', 'note', 'visibility'], - }, - { - id: 'wUeAI5QBV', - label: i18n.ts.defaultNoteVisibility, - keywords: ['default', 'note', 'visibility'], - }, - { - id: '6kMj4HVOg', - children: [ - { - id: 'DQIcvf64G', - label: i18n.ts.collapseRenotes, - keywords: ['renote', i18n.ts.collapseRenotesDescription], - }, - { - id: 'igFN7RIUa', - label: i18n.ts.showNoteActionsOnlyHover, - keywords: ['hover', 'show', 'footer', 'action'], - }, - { - id: '9uxocbLO0', - label: i18n.ts.showClipButtonInNoteFooter, - keywords: ['footer', 'action', 'clip', 'show'], - }, - { - id: 'eaT1O1Fao', - label: i18n.ts.enableAdvancedMfm, - keywords: ['mfm', 'enable', 'show', 'advanced'], - }, - { - id: 'omxZk3eET', - label: i18n.ts.showReactionsCount, - keywords: ['reaction', 'count', 'show'], - }, - { - id: 'epvi2Nv2G', - label: i18n.ts.loadRawImages, - keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'], - }, - ], - label: i18n.ts.note, - keywords: ['note'], - }, - { - id: 'jb3HUeyrx', - children: [ - { - id: 'ykifk3NHS', - label: i18n.ts.useGroupedNotifications, - keywords: ['group'], - }, - ], - label: i18n.ts.notifications, - keywords: ['notification'], - }, - { - id: 'abEAdSpYY', - children: [ - { - id: 'lBbtAg0Hm', - label: i18n.ts.openImageInNewTab, - keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'], - }, - { - id: 'E9whefUtX', - label: i18n.ts.useReactionPickerForContextMenu, - keywords: ['reaction', 'picker', 'contextmenu', 'open'], - }, - { - id: 'iQaBbJBva', - label: i18n.ts.enableInfiniteScroll, - keywords: ['load', 'auto', 'more'], - }, - { - id: 'hgEVGgJa1', - label: i18n.ts.disableStreamingTimeline, - keywords: ['disable', 'streaming', 'timeline'], - }, - { - id: 'yxehrHZ6x', - label: i18n.ts.alwaysConfirmFollow, - keywords: ['follow', 'confirm', 'always'], - }, - { - id: 'DdoFLaSG8', - label: i18n.ts.confirmWhenRevealingSensitiveMedia, - keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'], - }, - { - id: 'uIMCIK7kG', - label: i18n.ts.confirmOnReact, - keywords: ['reaction', 'confirm'], - }, - { - id: 'zvM13vl26', - label: i18n.ts.keepCw, - keywords: ['remember', 'keep', 'note', 'cw'], - }, - { - id: 'm75VEWI3S', - label: i18n.ts.whenServerDisconnected, - keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'], - }, - { - id: 'bLO9vCyKW', - label: i18n.ts.numberOfPageCache, - keywords: ['cache', 'page'], - }, - { - id: 'iQ7Er89l5', - label: i18n.ts.dataSaver, - keywords: ['datasaver'], - }, - ], - label: i18n.ts.behavior, - keywords: ['behavior'], - }, - { - id: 'C2WYcVM1d', - children: [ - { - id: 'Cu7ErCM7C', - label: i18n.ts.forceShowAds, - keywords: ['ad', 'show'], - }, - { - id: 'BBxwy4F6E', - label: i18n.ts.hemisphere, - keywords: [], - }, - { - id: '9YdUwDC8d', - label: i18n.ts.additionalEmojiDictionary, - keywords: ['emoji', 'dictionary', 'additional', 'extra'], - }, - ], - label: i18n.ts.other, - keywords: [], - }, - ], - label: i18n.ts.preferences, - keywords: ['general', 'preferences'], - path: '/settings/preferences', - icon: 'ti ti-adjustments', - }, - { - id: 'mwkwtw83Y', - label: i18n.ts.plugins, - keywords: ['plugin'], - path: '/settings/plugin', - icon: 'ti ti-plug', - }, - { - id: 'F1uK9ssiY', - children: [ - { - id: 'msAcN6u3S', - label: i18n.ts.accountInfo, - keywords: ['account', 'info'], - }, - { - id: 'ts8DgdnZV', - label: i18n.ts.accountMigration, - keywords: ['account', 'move', 'migration'], - }, - { - id: '4BG7nBECm', - label: i18n.ts.closeAccount, - keywords: ['account', 'close', 'delete', i18n.ts._accountDelete.requestAccountDelete], - }, - { - id: '2qI6ruPgi', - label: i18n.ts.experimentalFeatures, - keywords: ['experimental', 'feature', 'flags'], - }, - { - id: 'cIeaax47o', - label: i18n.ts.developer, - keywords: ['developer', 'mode', 'debug'], - }, - ], - label: i18n.ts.other, - keywords: ['other'], - path: '/settings/other', - icon: 'ti ti-dots', - }, - { - id: '3icEvyv2D', - children: [ - { - id: 'Tyt3gZTy', - children: [ - { - id: '9b7ZURyAt', - label: i18n.ts.showMutedWord, - keywords: ['show'], - }, - ], - label: i18n.ts.wordMute, - keywords: ['note', 'word', 'soft', 'mute', 'hide'], - }, - { - id: 'kdMk41II0', - label: i18n.ts.hardWordMute, - keywords: ['note', 'word', 'hard', 'mute', 'hide'], - }, - { - id: 'mjORQamAK', - label: i18n.ts.instanceMute, - keywords: ['note', 'server', 'instance', 'host', 'federation', 'mute', 'hide'], - }, - { - id: '1ZT7S9FZd', - label: `${i18n.ts.mutedUsers} (${ i18n.ts.renote })`, - keywords: ['renote', 'mute', 'hide', 'user'], - }, - { - id: 'ANrPit3kQ', - label: i18n.ts.mutedUsers, - keywords: ['note', 'mute', 'hide', 'user'], - }, - { - id: 'bPAE4lfno', - label: i18n.ts.blockedUsers, - keywords: ['block', 'user'], - }, - ], - label: i18n.ts.muteAndBlock, - keywords: ['mute', 'block'], - path: '/settings/mute-block', - icon: 'ti ti-ban', - }, - { - id: 'qE2vLlMkF', - children: [ - { - id: 'hPPEzjvZC', - label: i18n.ts._exportOrImport.allNotes, - keywords: ['notes'], - }, - { - id: 'AFaeHsCUB', - label: i18n.ts._exportOrImport.favoritedNotes, - keywords: ['favorite', 'notes'], - }, - { - id: 'xyCPmQiRo', - label: i18n.ts._exportOrImport.clips, - keywords: ['clip', 'notes'], - }, - { - id: 'Ch7hWAGUy', - label: i18n.ts._exportOrImport.followingList, - keywords: ['following', 'users'], - }, - { - id: 'AwPgFboEx', - label: i18n.ts._exportOrImport.userLists, - keywords: ['user', 'lists'], - }, - { - id: 'nporiHshC', - label: i18n.ts._exportOrImport.muteList, - keywords: ['mute', 'users'], - }, - { - id: 'BsCzR7vNw', - label: i18n.ts._exportOrImport.blockingList, - keywords: ['block', 'users'], - }, - { - id: 'dvf4IgYrQ', - label: i18n.ts.antennas, - keywords: ['antennas'], - }, - ], - label: i18n.ts.importAndExport, - keywords: ['import', 'export', 'data'], - path: '/settings/import-export', - icon: 'ti ti-package', - }, - { - id: '3Tcxw4Fwl', - children: [ - { - id: 'iIai9O65I', - label: i18n.ts.emailAddress, - keywords: ['email', 'address'], - }, - { - id: 'i6cC6oi0m', - label: i18n.ts.receiveAnnouncementFromInstance, - keywords: ['announcement', 'email'], - }, - { - id: 'C1YTinP11', - label: i18n.ts.emailNotification, - keywords: ['notification', 'email'], - }, - ], - label: i18n.ts.email, - keywords: ['email'], - path: '/settings/email', - icon: 'ti ti-mail', - }, - { - id: 'tnYoppRiv', - children: [ - { - id: 'ncIq6TAR2', - label: i18n.ts.usageAmount, - keywords: ['capacity', 'usage'], - }, - { - id: '2c4CQSvSr', - label: i18n.ts.statistics, - keywords: ['statistics', 'usage'], - }, - { - id: 'pepHELHMt', - label: i18n.ts.uploadFolder, - keywords: ['default', 'upload', 'folder'], - }, - { - id: 'xqOWrABxV', - label: i18n.ts.keepOriginalUploading, - keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription], - }, - { - id: 'D8HUTGWE1', - label: i18n.ts.keepOriginalFilename, - keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription], - }, - { - id: '6xAvsWSZi', - label: i18n.ts.alwaysMarkSensitive, - keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'], - }, - { - id: 'csNNPF1KX', - label: i18n.ts.enableAutoSensitive, - keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription], - }, - ], - label: i18n.ts.drive, - keywords: ['drive'], - path: '/settings/drive', - icon: 'ti ti-cloud', - }, - { - id: 'gtaOSdIJB', - label: i18n.ts.avatarDecorations, - keywords: ['avatar', 'icon', 'decoration'], - path: '/settings/avatar-decoration', - icon: 'ti ti-sparkles', - }, - { - id: 'AqPvMgn3A', - children: [ - { - id: 'j5gTtuMWP', - label: i18n.ts.useBlurEffect, - keywords: ['blur'], - }, - { - id: 'C05WQNSIJ', - label: i18n.ts.useBlurEffectForModal, - keywords: ['blur', 'modal'], - }, - { - id: 'snVKNr7Bw', - label: i18n.ts.highlightSensitiveMedia, - keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'], - }, - { - id: 'DsS2CwjYE', - label: i18n.ts.squareAvatars, - keywords: ['avatar', 'icon', 'square'], - }, - { - id: 'xCcTDl651', - label: i18n.ts.showAvatarDecorations, - keywords: ['avatar', 'icon', 'decoration', 'show'], - }, - { - id: '3dHw723VD', - label: i18n.ts.showGapBetweenNotesInTimeline, - keywords: ['note', 'timeline', 'gap'], - }, - { - id: 'AWi72xbrl', - label: i18n.ts.seasonalScreenEffect, - keywords: ['effect', 'show'], - }, - { - id: 'Ces8FsJws', - label: i18n.ts.menuStyle, - keywords: ['menu', 'style', 'popup', 'drawer'], - }, - { - id: 'wDr9xSXCv', - label: i18n.ts.emojiStyle, - keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'], - }, - { - id: 'vFB0pLzck', - label: i18n.ts.fontSize, - keywords: ['font', 'size'], - }, - { - id: '23BhvYXPC', - label: i18n.ts.useSystemFont, - keywords: ['font', 'system', 'native'], - }, - { - id: 'EeNLndAOa', - children: [ - { - id: 'rAAPoaodS', - label: i18n.ts.reactionsDisplaySize, - keywords: ['reaction', 'size', 'scale', 'display'], - }, - { - id: 'qTLAvNWsc', - label: i18n.ts.limitWidthOfReaction, - keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'], - }, - { - id: '2lWgzAm13', - label: i18n.ts.mediaListWithOneImageAppearance, - keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'], - }, - { - id: 'EU7HbxOR5', - label: i18n.ts.instanceTicker, - keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'], - }, - { - id: 'AEtM0FAp1', - label: i18n.ts.displayOfSensitiveMedia, - keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'], - }, - ], - label: i18n.ts.displayOfNote, - keywords: ['note', 'display'], - }, - { - id: 'A1FMC2Zon', - children: [ - { - id: 'CB37G5ZDo', - label: i18n.ts.position, - keywords: ['position'], - }, - { - id: 'gGS2i19hS', - label: i18n.ts.stackAxis, - keywords: ['stack', 'axis', 'direction'], - }, - ], - label: i18n.ts.notificationDisplay, - keywords: ['notification', 'display'], - }, - ], - label: i18n.ts.appearance, - keywords: ['appearance'], - path: '/settings/appearance', - icon: 'ti ti-device-desktop', - }, - { - id: 'f08Mi1Uwn', - children: [ - { - id: '7ov7ceoij', - label: i18n.ts.reduceUiAnimation, - keywords: ['animation', 'motion', 'reduce'], - }, - { - id: 'cXr3tFdpa', - label: i18n.ts.disableShowingAnimatedImages, - keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'], - }, - { - id: 'Ok1UBwtP', - label: i18n.ts.enableAnimatedMfm, - keywords: ['mfm', 'enable', 'show', 'animated'], - }, - { - id: 'yPEpJigqY', - label: i18n.ts.enableHorizontalSwipe, - keywords: ['swipe', 'horizontal', 'tab'], - }, - { - id: 'h7iZtdTU3', - label: i18n.ts.keepScreenOn, - keywords: ['keep', 'screen', 'display', 'on'], - }, - { - id: 'gP1BY3PDy', - label: i18n.ts.useNativeUIForVideoAudioPlayer, - keywords: ['native', 'system', 'video', 'audio', 'player', 'media'], - }, - { - id: 'jnMK3M6rs', - label: i18n.ts._contextMenu.title, - keywords: ['contextmenu', 'system', 'native'], - }, - ], - label: i18n.ts.accessibility, - keywords: ['accessibility'], - path: '/settings/accessibility', - icon: 'ti ti-accessible', - }, -] as const; - -export type SearchIndex = typeof searchIndexes; diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts deleted file mode 100644 index 0fbdf34d5d..0000000000 --- a/packages/frontend/src/scripts/cache.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ref } from 'vue'; - -export class Cache { - private cachedAt: number | null = null; - public value = ref(); - private lifetime: number; - private fetcher: () => Promise; - - constructor(lifetime: Cache['lifetime'], fetcher: () => Promise) { - this.lifetime = lifetime; - this.fetcher = fetcher; - } - - public set(value: T): void { - this.cachedAt = Date.now(); - this.value.value = value; - } - - private get(): T | undefined { - if (this.cachedAt == null) return undefined; - if ((Date.now() - this.cachedAt) > this.lifetime) { - this.value.value = undefined; - this.cachedAt = null; - return undefined; - } - return this.value.value; - } - - public delete() { - this.cachedAt = null; - } - - /** - * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します - */ - public async fetch(): Promise { - const cachedValue = this.get(); - if (cachedValue !== undefined) { - // Cache HIT - return cachedValue; - } - - // Cache MISS - const value = await this.fetcher(); - this.set(value); - return value; - } -} diff --git a/packages/frontend/src/scripts/chart-legend.ts b/packages/frontend/src/scripts/chart-legend.ts deleted file mode 100644 index e701d18dd2..0000000000 --- a/packages/frontend/src/scripts/chart-legend.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Plugin } from 'chart.js'; -import MkChartLegend from '@/components/MkChartLegend.vue'; - -export const chartLegend = (legend: InstanceType) => ({ - id: 'htmlLegend', - afterUpdate(chart, args, options) { - // Reuse the built-in legendItems generator - const items = chart.options.plugins.legend.labels.generateLabels(chart); - - legend.update(chart, items); - }, -}) as Plugin; diff --git a/packages/frontend/src/scripts/chart-vline.ts b/packages/frontend/src/scripts/chart-vline.ts deleted file mode 100644 index 465ca591c6..0000000000 --- a/packages/frontend/src/scripts/chart-vline.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Plugin } from 'chart.js'; - -export const chartVLine = (vLineColor: string) => ({ - id: 'vLine', - beforeDraw(chart, args, options) { - if (chart.tooltip?._active?.length) { - const ctx = chart.ctx; - const xs = chart.tooltip._active.map(a => a.element.x); - const x = xs.reduce((a, b) => a + b, 0) / xs.length; - const topY = chart.scales.y.top; - const bottomY = chart.scales.y.bottom; - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(x, bottomY); - ctx.lineTo(x, topY); - ctx.lineWidth = 1; - ctx.strokeStyle = vLineColor; - ctx.stroke(); - ctx.restore(); - } - }, -}) as Plugin; diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/scripts/check-permissions.ts deleted file mode 100644 index ed86529d5b..0000000000 --- a/packages/frontend/src/scripts/check-permissions.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { instance } from '@/instance.js'; -import { $i } from '@/account.js'; - -export const notesSearchAvailable = ( - // FIXME: instance.policies would be null in Vitest - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - ($i == null && instance.policies != null && instance.policies.canSearchNotes) || - ($i != null && $i.policies.canSearchNotes) || - false -) as boolean; - -export const canSearchNonLocalNotes = ( - instance.noteSearchableScope === 'global' -); diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts deleted file mode 100644 index 281ea2520e..0000000000 --- a/packages/frontend/src/scripts/check-reaction-permissions.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import type { UnicodeEmojiDef } from '@@/js/emojilist.js'; - -export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean { - if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする; - if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能 - - 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 deleted file mode 100644 index 98fea1bced..0000000000 --- a/packages/frontend/src/scripts/check-word-mute.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import * as Misskey from 'misskey-js'; - -export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array): Array | false { - // 自分自身 - if (me && (note.userId === me.id)) return false; - - if (mutedWords.length > 0) { - const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); - - if (text === '') return false; - - const matched = mutedWords.filter(filter => { - if (Array.isArray(filter)) { - // Clean up - const filteredFilter = filter.filter(keyword => keyword !== ''); - if (filteredFilter.length === 0) return false; - - return filteredFilter.every(keyword => text.includes(keyword)); - } else { - // represents RegExp - const regexp = filter.match(/^\/(.+)\/(.*)$/); - - // This should never happen due to input sanitisation. - if (!regexp) return false; - - try { - return new RegExp(regexp[1], regexp[2]).test(text); - } catch (err) { - // This should never happen due to input sanitisation. - return false; - } - } - }); - - if (matched.length > 0) return matched; - } - - return false; -} diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts deleted file mode 100644 index 71d1232710..0000000000 --- a/packages/frontend/src/scripts/clear-cache.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { unisonReload } from '@/scripts/unison-reload.js'; -import * as os from '@/os.js'; -import { miLocalStorage } from '@/local-storage.js'; -import { fetchCustomEmojis } from '@/custom-emojis.js'; -import { fetchInstance } from '@/instance.js'; - -export async function clearCache() { - os.waiting(); - miLocalStorage.removeItem('instance'); - miLocalStorage.removeItem('instanceCachedAt'); - miLocalStorage.removeItem('locale'); - miLocalStorage.removeItem('localeVersion'); - miLocalStorage.removeItem('theme'); - miLocalStorage.removeItem('emojis'); - miLocalStorage.removeItem('lastEmojisFetchedAt'); - await fetchInstance(true); - await fetchCustomEmojis(true); - unisonReload(); -} diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts deleted file mode 100644 index f9c4bc1829..0000000000 --- a/packages/frontend/src/scripts/clicker-game.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ref, computed } from 'vue'; -import { misskeyApi } from '@/scripts/misskey-api.js'; - -type SaveData = { - gameVersion: number; - cookies: number; - totalCookies: number; - totalHandmadeCookies: number; - clicked: number; - achievements: any[]; - facilities: any[]; -}; - -export const saveData = ref(); -export const ready = computed(() => saveData.value != null); - -let prev = ''; - -export async function load() { - try { - saveData.value = await misskeyApi('i/registry/get', { - scope: ['clickerGame'], - key: 'saveData', - }); - } catch (err) { - if (err.code === 'NO_SUCH_KEY') { - saveData.value = { - gameVersion: 2, - cookies: 0, - totalCookies: 0, - totalHandmadeCookies: 0, - clicked: 0, - achievements: [], - facilities: [], - }; - save(); - return; - } - throw err; - } - - // migration - if (saveData.value.gameVersion === 1) { - saveData.value = { - gameVersion: 2, - cookies: saveData.value.cookies, - totalCookies: saveData.value.cookies, - totalHandmadeCookies: saveData.value.cookies, - clicked: saveData.value.clicked, - achievements: [], - facilities: [], - }; - save(); - } -} - -export async function save() { - const current = JSON.stringify(saveData.value); - if (current === prev) return; - - await misskeyApi('i/registry/set', { - scope: ['clickerGame'], - key: 'saveData', - value: saveData.value, - }); - - prev = current; -} diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts deleted file mode 100644 index ea8eea14b5..0000000000 --- a/packages/frontend/src/scripts/clone.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// structredCloneが遅いため -// SEE: http://var.blog.jp/archives/86038606.html -// あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった -// https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045 - -export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[]; - -export function deepClone(x: T): T { - if (typeof x === 'object') { - if (x === null) return x; - if (Array.isArray(x)) return x.map(deepClone) as T; - const obj = {} as Record; - for (const [k, v] of Object.entries(x)) { - obj[k] = v === undefined ? undefined : deepClone(v); - } - return obj as T; - } else { - return x; - } -} diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts deleted file mode 100644 index 4f2aff9d4c..0000000000 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { createHighlighterCore } from 'shiki/core'; -import { createOnigurumaEngine } from 'shiki/engine/oniguruma'; -import darkPlus from 'shiki/themes/dark-plus.mjs'; -import { bundledThemesInfo } from 'shiki/themes'; -import { bundledLanguagesInfo } from 'shiki/langs'; -import lightTheme from '@@/themes/_light.json5'; -import darkTheme from '@@/themes/_dark.json5'; -import defaultLightTheme from '@@/themes/l-light.json5'; -import defaultDarkTheme from '@@/themes/d-green-lime.json5'; -import { unique } from './array.js'; -import { deepClone } from './clone.js'; -import { deepMerge } from './merge.js'; -import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core'; -import { prefer } from '@/preferences.js'; - -let _highlighter: HighlighterCore | null = null; - -export async function getTheme(mode: 'light' | 'dark', getName: true): Promise; -export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise; -export async function getTheme(mode: 'light' | 'dark', getName = false): Promise { - const theme = deepClone(mode === 'light' ? prefer.s.lightTheme ?? defaultLightTheme : prefer.s.darkTheme ?? defaultDarkTheme); - - 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 { - if (!_highlighter) { - return await initHighlighter(); - } - return _highlighter; -} - -async function initHighlighter() { - // テーマの重複を消す - const themes = unique([ - darkPlus, - ...(await Promise.all([getTheme('light'), getTheme('dark')])), - ]); - - const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); - const highlighter = await createHighlighterCore({ - engine: createOnigurumaEngine(() => import('shiki/onig.wasm?init')), - themes, - langs: [ - ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), - async () => (await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json')).default as unknown as LanguageRegistration, - ], - }); - - // TODO - //watch('lightTheme', async () => { - // const newTheme = await getTheme('light'); - // if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { - // highlighter.loadTheme(newTheme); - // } - //}); - //watch('darkTheme', async () => { - // const newTheme = await getTheme('dark'); - // if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { - // highlighter.loadTheme(newTheme); - // } - //}); - - _highlighter = highlighter; - - return highlighter; -} diff --git a/packages/frontend/src/scripts/collect-page-vars.ts b/packages/frontend/src/scripts/collect-page-vars.ts deleted file mode 100644 index 5096c0669e..0000000000 --- a/packages/frontend/src/scripts/collect-page-vars.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -interface StringPageVar { - name: string, - type: 'string', - value: string -} - -interface NumberPageVar { - name: string, - type: 'number', - value: number -} - -interface BooleanPageVar { - name: string, - type: 'boolean', - value: boolean -} - -type PageVar = StringPageVar | NumberPageVar | BooleanPageVar; - -export function collectPageVars(content): PageVar[] { - const pageVars: PageVar[] = []; - const collect = (xs: any[]): void => { - for (const x of xs) { - if (x.type === 'textInput') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '', - }); - } else if (x.type === 'textareaInput') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '', - }); - } else if (x.type === 'numberInput') { - pageVars.push({ - name: x.name, - type: 'number', - value: x.default || 0, - }); - } else if (x.type === 'switch') { - pageVars.push({ - name: x.name, - type: 'boolean', - value: x.default || false, - }); - } else if (x.type === 'counter') { - pageVars.push({ - name: x.name, - type: 'number', - value: 0, - }); - } else if (x.type === 'radioButton') { - pageVars.push({ - name: x.name, - type: 'string', - value: x.default || '', - }); - } else if (x.children) { - collect(x.children); - } - } - }; - collect(content); - return pageVars; -} diff --git a/packages/frontend/src/scripts/color.ts b/packages/frontend/src/scripts/color.ts deleted file mode 100644 index a11255ffd1..0000000000 --- a/packages/frontend/src/scripts/color.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const alpha = (hex: string, a: number): string => { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; - const r = parseInt(result[1], 16); - const g = parseInt(result[2], 16); - const b = parseInt(result[3], 16); - return `rgba(${r}, ${g}, ${b}, ${a})`; -}; diff --git a/packages/frontend/src/scripts/confetti.ts b/packages/frontend/src/scripts/confetti.ts deleted file mode 100644 index 8e53a6ceeb..0000000000 --- a/packages/frontend/src/scripts/confetti.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import _confetti from 'canvas-confetti'; -import * as os from '@/os.js'; - -export function confetti(options: { duration?: number; } = {}) { - const duration = options.duration ?? 1000 * 4; - const animationEnd = Date.now() + duration; - const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: os.claimZIndex('high') }; - - function randomInRange(min, max) { - return Math.random() * (max - min) + min; - } - - const interval = setInterval(() => { - const timeLeft = animationEnd - Date.now(); - - if (timeLeft <= 0) { - return clearInterval(interval); - } - - const particleCount = 50 * (timeLeft / duration); - // since particles fall down, start a bit higher than random - _confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } })); - _confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } })); - }, 250); -} diff --git a/packages/frontend/src/scripts/contains.ts b/packages/frontend/src/scripts/contains.ts deleted file mode 100644 index 6137c06e85..0000000000 --- a/packages/frontend/src/scripts/contains.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export default (parent, child, checkSame = true) => { - if (checkSame && parent === child) return true; - let node = child.parentNode; - while (node) { - if (node === parent) return true; - node = node.parentNode; - } - return false; -}; diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts deleted file mode 100644 index 7e0bb25606..0000000000 --- a/packages/frontend/src/scripts/copy-to-clipboard.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * Clipboardに値をコピー(TODO: 文字列以外も対応) - */ -export function copyToClipboard(input: string | null) { - if (input) navigator.clipboard.writeText(input); -}; diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/scripts/device-kind.ts deleted file mode 100644 index 7aadb617ca..0000000000 --- a/packages/frontend/src/scripts/device-kind.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type DeviceKind = 'smartphone' | 'tablet' | 'desktop'; - -const ua = navigator.userAgent.toLowerCase(); -const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); -const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); - -export const DEFAULT_DEVICE_KIND: DeviceKind = ( - isSmartphone - ? 'smartphone' - : isTablet - ? 'tablet' - : 'desktop' -); - -export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND; - -export function updateDeviceKind(kind: DeviceKind | null) { - deviceKind = kind ?? DEFAULT_DEVICE_KIND; -} diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts deleted file mode 100644 index 7ff5863243..0000000000 --- a/packages/frontend/src/scripts/emoji-picker.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { defineAsyncComponent, ref } from 'vue'; -import type { Ref } from 'vue'; -import { popup } from '@/os.js'; -import { store } from '@/store.js'; - -/** - * 絵文字ピッカーを表示する。 - * 類似の機能として{@link ReactionPicker}が存在しているが、この機能とは動きが異なる。 - * 投稿フォームなどで絵文字を選択する時など、絵文字ピックアップ後でもダイアログが消えずに残り、 - * 一度表示したダイアログを連続で使用できることが望ましいシーンでの利用が想定される。 - */ -class EmojiPicker { - private src: Ref = ref(null); - private manualShowing = ref(false); - private onChosen?: (emoji: string) => void; - private onClosed?: () => void; - - constructor() { - // nop - } - - public async init() { - const emojisRef = store.reactiveState.pinnedEmojis; - await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { - src: this.src, - pinnedEmojis: emojisRef, - asReactionPicker: false, - manualShowing: this.manualShowing, - choseAndClose: false, - }, { - done: emoji => { - if (this.onChosen) this.onChosen(emoji); - }, - close: () => { - this.manualShowing.value = false; - }, - closed: () => { - this.src.value = null; - if (this.onClosed) this.onClosed(); - }, - }); - } - - public show( - src: HTMLElement, - onChosen?: EmojiPicker['onChosen'], - onClosed?: EmojiPicker['onClosed'], - ) { - this.src.value = src; - this.manualShowing.value = true; - this.onChosen = onChosen; - this.onClosed = onClosed; - } -} - -export const emojiPicker = new EmojiPicker(); diff --git a/packages/frontend/src/scripts/extract-mentions.ts b/packages/frontend/src/scripts/extract-mentions.ts deleted file mode 100644 index d518562053..0000000000 --- a/packages/frontend/src/scripts/extract-mentions.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// test is located in test/extract-mentions - -import * as mfm from 'mfm-js'; - -export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { - // TODO: 重複を削除 - const mentionNodes = mfm.extract(nodes, (node) => node.type === 'mention'); - const mentions = mentionNodes.map(x => x.props); - - return mentions; -} diff --git a/packages/frontend/src/scripts/extract-url-from-mfm.ts b/packages/frontend/src/scripts/extract-url-from-mfm.ts deleted file mode 100644 index d5654ba850..0000000000 --- a/packages/frontend/src/scripts/extract-url-from-mfm.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as mfm from 'mfm-js'; -import { unique } from '@/scripts/array.js'; - -// unique without hash -// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] -const removeHash = (x: string) => x.replace(/#[^#]*$/, ''); - -export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] { - const urlNodes = mfm.extract(nodes, (node) => { - return (node.type === 'url') || (node.type === 'link' && (!respectSilentFlag || !node.props.silent)); - }); - const urls: string[] = unique(urlNodes.map(x => x.props.url)); - - return urls.reduce((array, url) => { - const urlWithoutHash = removeHash(url); - if (!array.map(x => removeHash(x)).includes(urlWithoutHash)) array.push(url); - return array; - }, [] as string[]); -} diff --git a/packages/frontend/src/scripts/file-drop.ts b/packages/frontend/src/scripts/file-drop.ts deleted file mode 100644 index 4259fe25e9..0000000000 --- a/packages/frontend/src/scripts/file-drop.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type DroppedItem = DroppedFile | DroppedDirectory; - -export type DroppedFile = { - isFile: true; - path: string; - file: File; -}; - -export type DroppedDirectory = { - isFile: false; - path: string; - children: DroppedItem[]; -}; - -export async function extractDroppedItems(ev: DragEvent): Promise { - const dropItems = ev.dataTransfer?.items; - if (!dropItems || dropItems.length === 0) { - return []; - } - - const apiTestItem = dropItems[0]; - if ('webkitGetAsEntry' in apiTestItem) { - return readDataTransferItems(dropItems); - } else { - // webkitGetAsEntryに対応していない場合はfilesから取得する(ディレクトリのサポートは出来ない) - const dropFiles = ev.dataTransfer.files; - if (dropFiles.length === 0) { - return []; - } - - const droppedFiles = Array.of(); - for (let i = 0; i < dropFiles.length; i++) { - const file = dropFiles.item(i); - if (file) { - droppedFiles.push({ - isFile: true, - path: file.name, - file, - }); - } - } - - return droppedFiles; - } -} - -/** - * ドラッグ&ドロップされたファイルのリストからディレクトリ構造とファイルへの参照({@link File})を取得する。 - */ -export async function readDataTransferItems(itemList: DataTransferItemList): Promise { - async function readEntry(entry: FileSystemEntry): Promise { - if (entry.isFile) { - return { - isFile: true, - path: entry.fullPath, - file: await readFile(entry as FileSystemFileEntry), - }; - } else { - return { - isFile: false, - path: entry.fullPath, - children: await readDirectory(entry as FileSystemDirectoryEntry), - }; - } - } - - function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise { - return new Promise((resolve, reject) => { - fileSystemFileEntry.file(resolve, reject); - }); - } - - function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise { - return new Promise(async (resolve) => { - const allEntries = Array.of(); - const reader = fileSystemDirectoryEntry.createReader(); - while (true) { - const entries = await new Promise((res, rej) => reader.readEntries(res, rej)); - if (entries.length === 0) { - break; - } - allEntries.push(...entries); - } - - resolve(await Promise.all(allEntries.map(readEntry))); - }); - } - - // 扱いにくいので配列に変換 - const items = Array.of(); - for (let i = 0; i < itemList.length; i++) { - items.push(itemList[i]); - } - - return Promise.all( - items - .map(it => it.webkitGetAsEntry()) - .filter(it => it) - .map(it => readEntry(it!)), - ); -} - -/** - * {@link DroppedItem}のリストからディレクトリを再帰的に検索し、ファイルのリストを取得する。 - */ -export function flattenDroppedFiles(items: DroppedItem[]): DroppedFile[] { - const result = Array.of(); - for (const item of items) { - if (item.isFile) { - result.push(item); - } else { - result.push(...flattenDroppedFiles(item.children)); - } - } - return result; -} diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/scripts/focus-trap.ts deleted file mode 100644 index fb7caea830..0000000000 --- a/packages/frontend/src/scripts/focus-trap.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; - -const focusTrapElements = new Set(); -const ignoreElements = [ - 'script', - 'style', -]; - -function containsFocusTrappedElements(el: HTMLElement): boolean { - return Array.from(focusTrapElements).some((focusTrapElement) => { - return el.contains(focusTrapElement); - }); -} - -function getZIndex(el: HTMLElement): number { - const zIndex = parseInt(window.getComputedStyle(el).zIndex || '0', 10); - if (isNaN(zIndex)) { - return 0; - } - return zIndex; -} - -function getHighestZIndexElement(): { el: HTMLElement; zIndex: number; } | null { - let highestZIndexElement: HTMLElement | null = null; - let highestZIndex = -Infinity; - - focusTrapElements.forEach((el) => { - const zIndex = getZIndex(el); - if (zIndex > highestZIndex) { - highestZIndex = zIndex; - highestZIndexElement = el; - } - }); - - return highestZIndexElement == null ? null : { - el: highestZIndexElement, - zIndex: highestZIndex, - }; -} - -function releaseFocusTrap(el: HTMLElement): void { - focusTrapElements.delete(el); - if (el.inert === true) { - el.inert = false; - } - - const highestZIndexElement = getHighestZIndexElement(); - - if (el.parentElement != null && el !== document.body) { - el.parentElement.childNodes.forEach((siblingNode) => { - const siblingEl = getHTMLElementOrNull(siblingNode); - if (!siblingEl) return; - if ( - siblingEl !== el && - ( - highestZIndexElement == null || - siblingEl === highestZIndexElement.el || - siblingEl.contains(highestZIndexElement.el) - ) - ) { - siblingEl.inert = false; - } else if ( - highestZIndexElement != null && - siblingEl !== highestZIndexElement.el && - !siblingEl.contains(highestZIndexElement.el) && - !ignoreElements.includes(siblingEl.tagName.toLowerCase()) - ) { - siblingEl.inert = true; - } else { - siblingEl.inert = false; - } - }); - releaseFocusTrap(el.parentElement); - } -} - -export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls: boolean, parent: true): void; -export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls?: boolean, parent?: false): { release: () => void; }; -export function focusTrap(el: HTMLElement, hasInteractionWithOtherFocusTrappedEls = false, parent = false): { release: () => void; } | void { - const highestZIndexElement = getHighestZIndexElement(); - - const highestZIndex = highestZIndexElement == null ? -Infinity : highestZIndexElement.zIndex; - const zIndex = getZIndex(el); - - // If the element has a lower z-index than the highest z-index element, focus trap the highest z-index element instead - // Focus trapping for this element will be done in the release function - if (!parent && zIndex < highestZIndex) { - focusTrapElements.add(el); - if (highestZIndexElement) { - focusTrap(highestZIndexElement.el, hasInteractionWithOtherFocusTrappedEls); - } - return { - release: () => { - releaseFocusTrap(el); - }, - }; - } - - if (el.inert === true) { - el.inert = false; - } - - if (el.parentElement != null && el !== document.body) { - el.parentElement.childNodes.forEach((siblingNode) => { - const siblingEl = getHTMLElementOrNull(siblingNode); - if (!siblingEl) return; - if ( - siblingEl !== el && - ( - hasInteractionWithOtherFocusTrappedEls === false || - (!focusTrapElements.has(siblingEl) && !containsFocusTrappedElements(siblingEl)) - ) && - !ignoreElements.includes(siblingEl.tagName.toLowerCase()) - ) { - siblingEl.inert = true; - } - }); - focusTrap(el.parentElement, hasInteractionWithOtherFocusTrappedEls, true); - } - - if (!parent) { - focusTrapElements.add(el); - - return { - release: () => { - releaseFocusTrap(el); - }, - }; - } -} diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts deleted file mode 100644 index 81278b17ea..0000000000 --- a/packages/frontend/src/scripts/focus.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js'; -import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; - -type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement; - -export const isFocusable = (input: MaybeHTMLElement | null | undefined): input is HTMLElement => { - if (input == null || !(input instanceof HTMLElement)) return false; - - if (input.tabIndex < 0) return false; - if ('disabled' in input && input.disabled === true) return false; - if ('readonly' in input && input.readonly === true) return false; - - if (!input.ownerDocument.contains(input)) return false; - - const style = window.getComputedStyle(input); - if (style.display === 'none') return false; - if (style.visibility === 'hidden') return false; - if (style.opacity === '0') return false; - if (style.pointerEvents === 'none') return false; - - return true; -}; - -export const focusPrev = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { - const element = self ? input : getElementOrNull(input)?.previousElementSibling; - if (element == null) return; - if (isFocusable(element)) { - focusOrScroll(element, scroll); - } else { - focusPrev(element, false, scroll); - } -}; - -export const focusNext = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { - const element = self ? input : getElementOrNull(input)?.nextElementSibling; - if (element == null) return; - if (isFocusable(element)) { - focusOrScroll(element, scroll); - } else { - focusNext(element, false, scroll); - } -}; - -export const focusParent = (input: MaybeHTMLElement | null | undefined, self = false, scroll = true) => { - const element = self ? input : getNodeOrNull(input)?.parentElement; - if (element == null) return; - if (isFocusable(element)) { - focusOrScroll(element, scroll); - } else { - focusParent(element, false, scroll); - } -}; - -const focusOrScroll = (element: HTMLElement, scroll: boolean) => { - if (scroll) { - const scrollContainer = getScrollContainer(element) ?? document.documentElement; - const scrollContainerTop = getScrollPosition(scrollContainer); - const stickyTop = getStickyTop(element, scrollContainer); - const stickyBottom = getStickyBottom(element, scrollContainer); - const top = element.getBoundingClientRect().top; - const bottom = element.getBoundingClientRect().bottom; - - let scrollTo = scrollContainerTop; - if (top < stickyTop) { - scrollTo += top - stickyTop; - } else if (bottom > window.innerHeight - stickyBottom) { - scrollTo += bottom - window.innerHeight + stickyBottom; - } - scrollContainer.scrollTo({ top: scrollTo, behavior: 'instant' }); - } - - if (document.activeElement !== element) { - element.focus({ preventScroll: true }); - } -}; diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts deleted file mode 100644 index 1032e97ac9..0000000000 --- a/packages/frontend/src/scripts/form.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; - -type EnumItem = string | { - label: string; - value: string; -}; - -type Hidden = boolean | ((v: any) => boolean); - -export type FormItem = { - label?: string; - type: 'string'; - default?: string | null; - description?: string; - required?: boolean; - hidden?: Hidden; - multiline?: boolean; - treatAsMfm?: boolean; -} | { - label?: string; - type: 'number'; - default?: number | null; - description?: string; - required?: boolean; - hidden?: Hidden; - step?: number; -} | { - label?: string; - type: 'boolean'; - default?: boolean | null; - description?: string; - hidden?: Hidden; -} | { - label?: string; - type: 'enum'; - default?: string | null; - required?: boolean; - hidden?: Hidden; - enum: EnumItem[]; -} | { - label?: string; - type: 'radio'; - default?: unknown | null; - required?: boolean; - hidden?: Hidden; - options: { - label: string; - value: unknown; - }[]; -} | { - label?: string; - type: 'range'; - default?: number | null; - description?: string; - required?: boolean; - step?: number; - min: number; - max: number; - textConverter?: (value: number) => string; - hidden?: Hidden; -} | { - label?: string; - type: 'object'; - default?: Record | null; - hidden: Hidden; -} | { - label?: string; - type: 'array'; - default?: unknown[] | null; - hidden: Hidden; -} | { - type: 'button'; - content?: string; - hidden?: Hidden; - action: (ev: MouseEvent, v: any) => void; -} | { - type: 'drive-file'; - defaultFileId?: string | null; - hidden?: Hidden; - validate?: (v: Misskey.entities.DriveFile) => Promise; -}; - -export type Form = Record; - -type GetItemType = - Item['type'] extends 'string' ? string : - Item['type'] extends 'number' ? number : - Item['type'] extends 'boolean' ? boolean : - Item['type'] extends 'radio' ? unknown : - Item['type'] extends 'range' ? number : - Item['type'] extends 'enum' ? string : - Item['type'] extends 'array' ? unknown[] : - Item['type'] extends 'object' ? Record : - Item['type'] extends 'drive-file' ? Misskey.entities.DriveFile | undefined : - never; - -export type GetFormResultType = { - [P in keyof F]: GetItemType; -}; diff --git a/packages/frontend/src/scripts/format-time-string.ts b/packages/frontend/src/scripts/format-time-string.ts deleted file mode 100644 index d383f143e1..0000000000 --- a/packages/frontend/src/scripts/format-time-string.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const defaultLocaleStringFormats: { [index: string]: string } = { - 'weekday': 'narrow', - 'era': 'narrow', - 'year': 'numeric', - 'month': 'numeric', - 'day': 'numeric', - 'hour': 'numeric', - 'minute': 'numeric', - 'second': 'numeric', - 'timeZoneName': 'short', -}; - -function formatLocaleString(date: Date, format: string): string { - return format.replace(/\{\{(\w+)(:(\w+))?\}\}/g, (match: string, kind: string, unused?, option?: string) => { - if (['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(kind)) { - return date.toLocaleString(window.navigator.language, { [kind]: option ? option : defaultLocaleStringFormats[kind] }); - } else { - return match; - } - }); -} - -export function formatDateTimeString(date: Date, format: string): string { - return format - .replace(/yyyy/g, date.getFullYear().toString()) - .replace(/yy/g, date.getFullYear().toString().slice(-2)) - .replace(/MMMM/g, date.toLocaleString(window.navigator.language, { month: 'long' })) - .replace(/MMM/g, date.toLocaleString(window.navigator.language, { month: 'short' })) - .replace(/MM/g, (`0${date.getMonth() + 1}`).slice(-2)) - .replace(/M/g, (date.getMonth() + 1).toString()) - .replace(/dd/g, (`0${date.getDate()}`).slice(-2)) - .replace(/d/g, date.getDate().toString()) - .replace(/HH/g, (`0${date.getHours()}`).slice(-2)) - .replace(/H/g, date.getHours().toString()) - .replace(/hh/g, (`0${(date.getHours() % 12) || 12}`).slice(-2)) - .replace(/h/g, ((date.getHours() % 12) || 12).toString()) - .replace(/mm/g, (`0${date.getMinutes()}`).slice(-2)) - .replace(/m/g, date.getMinutes().toString()) - .replace(/ss/g, (`0${date.getSeconds()}`).slice(-2)) - .replace(/s/g, date.getSeconds().toString()) - .replace(/tt/g, date.getHours() >= 12 ? 'PM' : 'AM'); -} - -export function formatTimeString(date: Date, format: string): string { - return format.replace(/\[(([^\[]|\[\])*)\]|(([yMdHhmst])\4{0,3})/g, (match: string, localeformat?: string, unused?, datetimeformat?: string) => { - if (localeformat) return formatLocaleString(date, localeformat); - if (datetimeformat) return formatDateTimeString(date, datetimeformat); - return match; - }); -} diff --git a/packages/frontend/src/scripts/fullscreen.ts b/packages/frontend/src/scripts/fullscreen.ts deleted file mode 100644 index 7a0a018ef3..0000000000 --- a/packages/frontend/src/scripts/fullscreen.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -type PartiallyPartial = Omit & Partial>; - -type VideoEl = PartiallyPartial & { - webkitEnterFullscreen?(): void; - webkitExitFullscreen?(): void; -}; - -type PlayerEl = PartiallyPartial; - -type RequestFullscreenProps = { - readonly videoEl: VideoEl; - readonly playerEl: PlayerEl; - readonly options?: FullscreenOptions | null; -}; - -type ExitFullscreenProps = { - readonly videoEl: VideoEl; -}; - -export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => { - if (playerEl.requestFullscreen != null) { - playerEl.requestFullscreen(options ?? undefined); - return; - } - if (videoEl.webkitEnterFullscreen != null) { - videoEl.webkitEnterFullscreen(); - return; - } -}; - -export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (document.exitFullscreen != null) { - document.exitFullscreen(); - return; - } - if (videoEl.webkitExitFullscreen != null) { - videoEl.webkitExitFullscreen(); - return; - } -}; diff --git a/packages/frontend/src/scripts/get-account-from-id.ts b/packages/frontend/src/scripts/get-account-from-id.ts deleted file mode 100644 index 40afa10f2d..0000000000 --- a/packages/frontend/src/scripts/get-account-from-id.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { get } from '@/scripts/idb-proxy.js'; - -export async function getAccountFromId(id: string) { - const accounts = await get('accounts') as { token: string; id: string; }[]; - if (!accounts) console.log('Accounts are not recorded'); - return accounts.find(account => account.id === id); -} diff --git a/packages/frontend/src/scripts/get-appear-note.ts b/packages/frontend/src/scripts/get-appear-note.ts deleted file mode 100644 index 40ce80eac9..0000000000 --- a/packages/frontend/src/scripts/get-appear-note.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; - -export function getAppearNote(note: Misskey.entities.Note) { - return Misskey.note.isPureRenote(note) ? note.renote : note; -} diff --git a/packages/frontend/src/scripts/get-bg-color.ts b/packages/frontend/src/scripts/get-bg-color.ts deleted file mode 100644 index ccf60b454f..0000000000 --- a/packages/frontend/src/scripts/get-bg-color.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import tinycolor from 'tinycolor2'; - -export const getBgColor = (elem?: Element | null | undefined): string | null => { - if (elem == null) return null; - - const { backgroundColor: bg } = window.getComputedStyle(elem); - - if (bg && tinycolor(bg).getAlpha() !== 0) { - return bg; - } - - return getBgColor(elem.parentElement); -}; diff --git a/packages/frontend/src/scripts/get-dom-node-or-null.ts b/packages/frontend/src/scripts/get-dom-node-or-null.ts deleted file mode 100644 index fbf54675fd..0000000000 --- a/packages/frontend/src/scripts/get-dom-node-or-null.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const getNodeOrNull = (input: unknown): Node | null => { - if (input instanceof Node) return input; - return null; -}; - -export const getElementOrNull = (input: unknown): Element | null => { - if (input instanceof Element) return input; - return null; -}; - -export const getHTMLElementOrNull = (input: unknown): HTMLElement | null => { - if (input instanceof HTMLElement) return input; - return null; -}; diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts deleted file mode 100644 index 6ac0684170..0000000000 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { defineAsyncComponent } from 'vue'; -import type { MenuItem } from '@/types/menu.js'; -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 { prefer } from '@/preferences.js'; - -function rename(file: Misskey.entities.DriveFile) { - os.inputText({ - title: i18n.ts.renameFile, - placeholder: i18n.ts.inputNewFileName, - default: file.name, - }).then(({ canceled, result: name }) => { - if (canceled) return; - misskeyApi('drive/files/update', { - fileId: file.id, - name: name, - }); - }); -} - -function describe(file: Misskey.entities.DriveFile) { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { - default: file.comment ?? '', - file: file, - }, { - done: caption => { - misskeyApi('drive/files/update', { - fileId: file.id, - comment: caption.length === 0 ? null : caption, - }); - }, - closed: () => dispose(), - }); -} - -function move(file: Misskey.entities.DriveFile) { - os.selectDriveFolder(false).then(folder => { - misskeyApi('drive/files/update', { - fileId: file.id, - folderId: folder[0] ? folder[0].id : null, - }); - }); -} - -function toggleSensitive(file: Misskey.entities.DriveFile) { - misskeyApi('drive/files/update', { - fileId: file.id, - isSensitive: !file.isSensitive, - }).catch(err => { - os.alert({ - type: 'error', - title: i18n.ts.error, - text: err.message, - }); - }); -} - -function copyUrl(file: Misskey.entities.DriveFile) { - copyToClipboard(file.url); - os.success(); -} - -/* -function addApp() { - alert('not implemented yet'); -} -*/ -async function deleteFile(file: Misskey.entities.DriveFile) { - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }), - }); - - if (canceled) return; - misskeyApi('drive/files/delete', { - fileId: file.id, - }); -} - -export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { - const isImage = file.type.startsWith('image/'); - - const menuItems: MenuItem[] = []; - - menuItems.push({ - type: 'link', - to: `/my/drive/file/${file.id}`, - text: i18n.ts._fileViewer.title, - icon: 'ti ti-info-circle', - }, { type: 'divider' }, { - text: i18n.ts.rename, - icon: 'ti ti-forms', - action: () => rename(file), - }, { - text: i18n.ts.move, - icon: 'ti ti-folder-symlink', - action: () => move(file), - }, { - text: file.isSensitive ? i18n.ts.unmarkAsSensitive : i18n.ts.markAsSensitive, - icon: file.isSensitive ? 'ti ti-eye' : 'ti ti-eye-exclamation', - action: () => toggleSensitive(file), - }, { - text: i18n.ts.describeFile, - icon: 'ti ti-text-caption', - action: () => describe(file), - }); - - if (isImage) { - menuItems.push({ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () => os.cropImage(file, { - aspectRatio: NaN, - uploadFolder: folder ? folder.id : folder, - }), - }); - } - - menuItems.push({ type: 'divider' }, { - text: i18n.ts.createNoteFromTheFile, - icon: 'ti ti-pencil', - action: () => os.post({ - initialFiles: [file], - }), - }, { - text: i18n.ts.copyUrl, - icon: 'ti ti-link', - action: () => copyUrl(file), - }, { - type: 'a', - href: file.url, - target: '_blank', - text: i18n.ts.download, - icon: 'ti ti-download', - download: file.name, - }, { type: 'divider' }, { - text: i18n.ts.delete, - icon: 'ti ti-trash', - danger: true, - action: () => deleteFile(file), - }); - - if (prefer.s.devMode) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-id', - text: i18n.ts.copyFileId, - action: () => { - copyToClipboard(file.id); - }, - }); - } - - return menuItems; -} diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts deleted file mode 100644 index 158ab9c7f8..0000000000 --- a/packages/frontend/src/scripts/get-embed-code.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { defineAsyncComponent } from 'vue'; -import { v4 as uuid } from 'uuid'; -import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js'; -import { url } from '@@/js/config.js'; -import * as os from '@/os.js'; -import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js'; - -const MOBILE_THRESHOLD = 500; - -/** - * パラメータを正規化する(埋め込みコード作成用) - * @param params パラメータ - * @returns 正規化されたパラメータ - */ -export function normalizeEmbedParams(params: EmbedParams): Record { - // paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す - const normalizedParams: Record = {}; - for (const key in params) { - // デフォルトの値と同じならparamsに含めない - if (params[key] == null || params[key] === defaultEmbedParams[key]) { - continue; - } - switch (typeof params[key]) { - case 'number': - normalizedParams[key] = params[key].toString(); - break; - case 'boolean': - normalizedParams[key] = params[key] ? 'true' : 'false'; - break; - default: - normalizedParams[key] = params[key]; - break; - } - } - return normalizedParams; -} - -/** - * 埋め込みコードを生成(iframe IDの発番もやる) - */ -export function getEmbedCode(path: string, params?: EmbedParams): string { - const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく - - let paramString = ''; - if (params) { - const searchParams = new URLSearchParams(normalizeEmbedParams(params)); - paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString(); - } - - const iframeCode = [ - ``, - ``, - ]; - return iframeCode.join('\n'); -} - -/** - * 埋め込みコードを生成してコピーする(カスタマイズ機能つき) - * - * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください - */ -export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { - const _params = { ...params }; - - if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { - _params.maxHeight = 700; - } - - // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー - if (window.innerWidth < MOBILE_THRESHOLD) { - copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); - os.success(); - } else { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { - entity, - id, - params: _params, - }, { - closed: () => dispose(), - }); - } -} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts deleted file mode 100644 index 8ce4a81bd4..0000000000 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ /dev/null @@ -1,685 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { defineAsyncComponent } from 'vue'; -import type { Ref, ShallowRef } from 'vue'; -import * as Misskey from 'misskey-js'; -import { url } from '@@/js/config.js'; -import { claimAchievement } from './achievements.js'; -import type { MenuItem } from '@/types/menu.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 { store, noteActions } from '@/store.js'; -import { miLocalStorage } from '@/local-storage.js'; -import { getUserMenu } from '@/scripts/get-user-menu.js'; -import { clipsCache, favoritedChannelsCache } from '@/cache.js'; -import MkRippleEffect from '@/components/MkRippleEffect.vue'; -import { isSupportShare } from '@/scripts/navigator.js'; -import { getAppearNote } from '@/scripts/get-appear-note.js'; -import { genEmbedCode } from '@/scripts/get-embed-code.js'; -import { prefer } from '@/preferences.js'; - -export async function getNoteClipMenu(props: { - note: Misskey.entities.Note; - isDeleted: Ref; - currentClip?: Misskey.entities.Clip; -}) { - function getClipName(clip: Misskey.entities.Clip) { - if ($i && clip.userId === $i.id && clip.notesCount != null) { - return `${clip.name} (${clip.notesCount}/${$i.policies.noteEachClipsLimit})`; - } else { - return clip.name; - } - } - - const appearNote = getAppearNote(props.note); - - const clips = await clipsCache.fetch(); - const menu: MenuItem[] = [...clips.map(clip => ({ - text: getClipName(clip), - action: () => { - claimAchievement('noteClipped1'); - os.promiseDialog( - 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.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }), - }); - if (!confirm.canceled) { - os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }).then(() => { - clipsCache.set(clips.map(c => { - if (c.id === clip.id) { - return { - ...c, - notesCount: Math.max(0, ((c.notesCount ?? 0) - 1)), - }; - } else { - return c; - } - })); - }); - if (props.currentClip?.id === clip.id) props.isDeleted.value = true; - } - } else if (err.id === 'f0dba960-ff73-4615-8df4-d6ac5d9dc118') { - os.alert({ - type: 'error', - text: i18n.ts.clipNoteLimitExceeded, - }); - } else { - os.alert({ - type: 'error', - text: err.message + '\n' + err.id, - }); - } - }, - ).then(() => { - clipsCache.set(clips.map(c => { - if (c.id === clip.id) { - return { - ...c, - notesCount: (c.notesCount ?? 0) + 1, - }; - } else { - return c; - } - })); - }); - }, - })), { type: 'divider' }, { - icon: 'ti ti-plus', - text: i18n.ts.createNew, - action: async () => { - const { canceled, result } = await os.form(i18n.ts.createNewClip, { - name: { - type: 'string', - default: null, - label: i18n.ts.name, - }, - description: { - type: 'string', - required: false, - default: null, - multiline: true, - label: i18n.ts.description, - }, - isPublic: { - type: 'boolean', - label: i18n.ts.public, - default: false, - }, - }); - if (canceled) return; - - const clip = await os.apiWithDialog('clips/create', result); - - clipsCache.delete(); - - claimAchievement('noteClipped1'); - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); - }, - }]; - - return menu; -} - -export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem { - return { - icon: 'ti ti-exclamation-circle', - text, - action: (): void => { - 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`; - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { - user: note.user, - initialComment: `${noteInfo}-----\n`, - }, { - closed: () => dispose(), - }); - }, - }; -} - -export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): MenuItem { - return { - icon: 'ti ti-link', - text, - action: (): void => { - copyToClipboard(`${url}/notes/${note.id}`); - os.success(); - }, - }; -} - -function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { - if (note.url != null || note.uri != null) return undefined; - if (['specified', 'followers'].includes(note.visibility)) return undefined; - - return { - icon: 'ti ti-code', - text, - action: (): void => { - genEmbedCode('notes', note.id); - }, - }; -} - -export function getNoteMenu(props: { - note: Misskey.entities.Note; - translation: Ref; - translating: Ref; - isDeleted: Ref; - currentClip?: Misskey.entities.Clip; -}) { - const appearNote = getAppearNote(props.note); - - const cleanups = [] as (() => void)[]; - - function del(): void { - os.confirm({ - type: 'warning', - text: i18n.ts.noteDeleteConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - misskeyApi('notes/delete', { - noteId: appearNote.id, - }); - - if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) { - claimAchievement('noteDeletedWithin1min'); - } - }); - } - - function delEdit(): void { - os.confirm({ - type: 'warning', - text: i18n.ts.deleteAndEditConfirm, - }).then(({ canceled }) => { - if (canceled) return; - - misskeyApi('notes/delete', { - noteId: appearNote.id, - }); - - os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel }); - - if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) { - claimAchievement('noteDeletedWithin1min'); - } - }); - } - - function toggleFavorite(favorite: boolean): void { - claimAchievement('noteFavorited1'); - os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: appearNote.id, - }); - } - - function toggleThreadMute(mute: boolean): void { - os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', { - noteId: appearNote.id, - }); - } - - function copyContent(): void { - copyToClipboard(appearNote.text); - os.success(); - } - - function togglePin(pin: boolean): void { - os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { - noteId: appearNote.id, - }, undefined, { - '72dab508-c64d-498f-8740-a8eec1ba385a': { - text: i18n.ts.pinLimitExceeded, - }, - }); - } - - async function unclip(): Promise { - if (!props.currentClip) return; - os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id }); - props.isDeleted.value = true; - } - - async function promote(): Promise { - const { canceled, result: days } = await os.inputNumber({ - title: i18n.ts.numberOfDays, - }); - - if (canceled || days == null) return; - - os.apiWithDialog('admin/promo/create', { - noteId: appearNote.id, - expiresAt: Date.now() + (86400000 * days), - }); - } - - function share(): void { - navigator.share({ - title: i18n.tsx.noteOf({ user: appearNote.user.name ?? appearNote.user.username }), - text: appearNote.text ?? '', - url: `${url}/notes/${appearNote.id}`, - }); - } - - function openDetail(): void { - os.pageWindow(`/notes/${appearNote.id}`); - } - - async function translate(): Promise { - if (props.translation.value != null) return; - props.translating.value = true; - const res = await misskeyApi('notes/translate', { - noteId: appearNote.id, - targetLang: miLocalStorage.getItem('lang') ?? navigator.language, - }); - props.translating.value = false; - props.translation.value = res; - } - - const menuItems: MenuItem[] = []; - - if ($i) { - const statePromise = misskeyApi('notes/state', { - noteId: appearNote.id, - }); - - if (props.currentClip?.userId === $i.id) { - menuItems.push({ - icon: 'ti ti-backspace', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, { type: 'divider' }); - } - - menuItems.push({ - icon: 'ti ti-info-circle', - text: i18n.ts.details, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); - - if (appearNote.url || appearNote.uri) { - menuItems.push({ - icon: 'ti ti-link', - text: i18n.ts.copyRemoteLink, - action: () => { - copyToClipboard(appearNote.url ?? appearNote.uri); - os.success(); - }, - }, { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); - }, - }); - } else { - menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); - } - - if (isSupportShare()) { - menuItems.push({ - icon: 'ti ti-share', - text: i18n.ts.share, - action: share, - }); - } - - if ($i.policies.canUseTranslator && instance.translatorAvailable) { - menuItems.push({ - icon: 'ti ti-language-hiragana', - text: i18n.ts.translate, - action: translate, - }); - } - - menuItems.push({ type: 'divider' }); - - menuItems.push(statePromise.then(state => state.isFavorited ? { - icon: 'ti ti-star-off', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'ti ti-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - })); - - menuItems.push({ - type: 'parent', - icon: 'ti ti-paperclip', - text: i18n.ts.clip, - children: () => getNoteClipMenu(props), - }); - - menuItems.push(statePromise.then(state => state.isMutedThread ? { - icon: 'ti ti-message-off', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false), - } : { - icon: 'ti ti-message-off', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true), - })); - - if (appearNote.userId === $i.id) { - if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) { - menuItems.push({ - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => togglePin(false), - }); - } else { - menuItems.push({ - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => togglePin(true), - }); - } - } - - menuItems.push({ - type: 'parent', - icon: 'ti ti-user', - text: i18n.ts.user, - children: async () => { - const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); - const { menu, cleanup } = getUserMenu(user); - cleanups.push(cleanup); - return menu; - }, - }); - - if (appearNote.userId !== $i.id) { - menuItems.push({ type: 'divider' }); - menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse)); - } - - if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) { - menuItems.push({ type: 'divider' }); - menuItems.push({ - type: 'parent', - 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; - }, - }); - } - - if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) { - menuItems.push({ type: 'divider' }); - if (appearNote.userId === $i.id) { - menuItems.push({ - icon: 'ti ti-edit', - text: i18n.ts.deleteAndEdit, - action: delEdit, - }); - } - menuItems.push({ - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: del, - }); - } - } else { - menuItems.push({ - icon: 'ti ti-info-circle', - text: i18n.ts.details, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); - - if (appearNote.url || appearNote.uri) { - menuItems.push({ - icon: 'ti ti-link', - text: i18n.ts.copyRemoteLink, - action: () => { - copyToClipboard(appearNote.url ?? appearNote.uri); - os.success(); - }, - }, { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); - }, - }); - } else { - menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); - } - } - - if (noteActions.length > 0) { - menuItems.push({ type: 'divider' }); - - menuItems.push(...noteActions.map(action => ({ - icon: 'ti ti-plug', - text: action.title, - action: () => { - action.handler(appearNote); - }, - }))); - } - - if (prefer.s.devMode) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-id', - text: i18n.ts.copyNoteId, - action: () => { - copyToClipboard(appearNote.id); - os.success(); - }, - }); - } - - const cleanup = () => { - if (_DEV_) console.log('note menu cleanup', cleanups); - for (const cl of cleanups) { - cl(); - } - }; - - return { - menu: menuItems, - cleanup, - }; -} - -type Visibility = (typeof Misskey.noteVisibilities)[number]; - -function smallerVisibility(a: Visibility, b: Visibility): Visibility { - if (a === 'specified' || b === 'specified') return 'specified'; - if (a === 'followers' || b === 'followers') return 'followers'; - if (a === 'home' || b === 'home') return 'home'; - // if (a === 'public' || b === 'public') - return 'public'; -} - -export function getRenoteMenu(props: { - note: Misskey.entities.Note; - renoteButton: ShallowRef; - mock?: boolean; -}) { - const appearNote = getAppearNote(props.note); - - const channelRenoteItems: MenuItem[] = []; - const normalRenoteItems: MenuItem[] = []; - const normalExternalChannelRenoteItems: MenuItem[] = []; - - if (appearNote.channel) { - channelRenoteItems.push(...[{ - text: i18n.ts.inChannelRenote, - icon: 'ti ti-repeat', - action: () => { - const el = props.renoteButton.value; - if (el && prefer.s.animation) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - const { dispose } = os.popup(MkRippleEffect, { x, y }, { - end: () => dispose(), - }); - } - - if (!props.mock) { - misskeyApi('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, - }).then(() => { - os.toast(i18n.ts.renoted); - }); - } - }, - }, { - text: i18n.ts.inChannelQuote, - icon: 'ti ti-quote', - action: () => { - if (!props.mock) { - os.post({ - renote: appearNote, - channel: appearNote.channel, - }); - } - }, - }]); - } - - if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) { - normalRenoteItems.push(...[{ - text: i18n.ts.renote, - icon: 'ti ti-repeat', - action: () => { - const el = props.renoteButton.value; - if (el && prefer.s.animation) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - const { dispose } = os.popup(MkRippleEffect, { x, y }, { - end: () => dispose(), - }); - } - - const configuredVisibility = prefer.s.rememberNoteVisibility ? store.state.visibility : prefer.s.defaultNoteVisibility; - const localOnly = prefer.s.rememberNoteVisibility ? store.state.localOnly : prefer.s.defaultNoteLocalOnly; - - let visibility = appearNote.visibility; - visibility = smallerVisibility(visibility, configuredVisibility); - if (appearNote.channel?.isSensitive) { - visibility = smallerVisibility(visibility, 'home'); - } - - if (!props.mock) { - misskeyApi('notes/create', { - localOnly, - visibility, - renoteId: appearNote.id, - }).then(() => { - os.toast(i18n.ts.renoted); - }); - } - }, - }, (props.mock) ? undefined : { - text: i18n.ts.quote, - icon: 'ti ti-quote', - action: () => { - os.post({ - renote: appearNote, - }); - }, - }]); - - normalExternalChannelRenoteItems.push({ - type: 'parent', - icon: 'ti ti-repeat', - text: appearNote.channel ? i18n.ts.renoteToOtherChannel : i18n.ts.renoteToChannel, - children: async () => { - const channels = await favoritedChannelsCache.fetch(); - return channels.filter((channel) => { - if (!appearNote.channelId) return true; - return channel.id !== appearNote.channelId; - }).map((channel) => ({ - text: channel.name, - action: () => { - const el = props.renoteButton.value; - if (el && prefer.s.animation) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - const { dispose } = os.popup(MkRippleEffect, { x, y }, { - end: () => dispose(), - }); - } - - if (!props.mock) { - misskeyApi('notes/create', { - renoteId: appearNote.id, - channelId: channel.id, - }).then(() => { - os.toast(i18n.tsx.renotedToX({ name: channel.name })); - }); - } - }, - })); - }, - }); - } - - const renoteItems = [ - ...normalRenoteItems, - ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [], - ...channelRenoteItems, - ...(normalExternalChannelRenoteItems.length > 0 && (normalRenoteItems.length > 0 || channelRenoteItems.length > 0)) ? [{ type: 'divider' }] as MenuItem[] : [], - ...normalExternalChannelRenoteItems, - ]; - - return { - menu: renoteItems, - }; -} diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts deleted file mode 100644 index 6fd9947ac1..0000000000 --- a/packages/frontend/src/scripts/get-note-summary.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { i18n } from '@/i18n.js'; - -/** - * 投稿を表す文字列を取得します。 - * @param {*} note (packされた)投稿 - */ -export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { - if (note == null) { - return ''; - } - - if (note.deletedAt) { - return `(${i18n.ts.deletedNote})`; - } - - if (note.isHidden) { - return `(${i18n.ts.invisibleNote})`; - } - - let summary = ''; - - // 本文 - if (note.cw != null) { - summary += note.cw; - } else { - summary += note.text ? note.text : ''; - } - - // ファイルが添付されているとき - if ((note.files || []).length !== 0) { - summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`; - } - - // 投票が添付されているとき - if (note.poll) { - summary += ` (${i18n.ts.poll})`; - } - - // 返信のとき - if (note.replyId) { - if (note.reply) { - summary += `\n\nRE: ${getNoteSummary(note.reply)}`; - } else { - summary += '\n\nRE: ...'; - } - } - - // Renoteのとき - if (note.renoteId) { - if (note.renote) { - summary += `\n\nRN: ${getNoteSummary(note.renote)}`; - } else { - summary += '\n\nRN: ...'; - } - } - - return summary.trim(); -}; diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts deleted file mode 100644 index 6892c3a4e4..0000000000 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { toUnicode } from 'punycode.js'; -import { defineAsyncComponent, ref, watch } from 'vue'; -import * as Misskey from 'misskey-js'; -import { host, url } from '@@/js/config.js'; -import type { IRouter } from '@/nirax.js'; -import type { MenuItem } from '@/types/menu.js'; -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 { userActions } from '@/store.js'; -import { $i, iAmModerator } from '@/account.js'; -import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js'; -import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; -import { mainRouter } from '@/router/main.js'; -import { genEmbedCode } from '@/scripts/get-embed-code.js'; -import { prefer } from '@/preferences.js'; - -export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { - const meId = $i ? $i.id : null; - - const cleanups = [] as (() => void)[]; - - async function toggleMute() { - if (user.isMuted) { - os.apiWithDialog('mute/delete', { - userId: user.id, - }).then(() => { - user.isMuted = false; - }); - } else { - const { canceled, result: period } = await os.select({ - title: i18n.ts.mutePeriod, - items: [{ - value: 'indefinitely', text: i18n.ts.indefinitely, - }, { - value: 'tenMinutes', text: i18n.ts.tenMinutes, - }, { - value: 'oneHour', text: i18n.ts.oneHour, - }, { - value: 'oneDay', text: i18n.ts.oneDay, - }, { - value: 'oneWeek', text: i18n.ts.oneWeek, - }], - default: 'indefinitely', - }); - if (canceled) return; - - const expiresAt = period === 'indefinitely' ? null - : period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10) - : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : null; - - os.apiWithDialog('mute/create', { - userId: user.id, - expiresAt, - }).then(() => { - user.isMuted = true; - }); - } - } - - async function toggleRenoteMute() { - os.apiWithDialog(user.isRenoteMuted ? 'renote-mute/delete' : 'renote-mute/create', { - userId: user.id, - }).then(() => { - user.isRenoteMuted = !user.isRenoteMuted; - }); - } - - async function toggleBlock() { - if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return; - - os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { - userId: user.id, - }).then(() => { - user.isBlocking = !user.isBlocking; - }); - } - - async function toggleNotify() { - os.apiWithDialog('following/update', { - userId: user.id, - notify: user.notify === 'normal' ? 'none' : 'normal', - }).then(() => { - user.notify = user.notify === 'normal' ? 'none' : 'normal'; - }); - } - - function reportAbuse() { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { - user: user, - }, { - closed: () => dispose(), - }); - } - - async function getConfirmed(text: string): Promise { - const confirm = await os.confirm({ - type: 'warning', - title: 'confirm', - text, - }); - - return !confirm.canceled; - } - - async function userInfoUpdate() { - os.apiWithDialog('federation/update-remote-user', { - userId: user.id, - }); - } - - async function invalidateFollow() { - if (!await getConfirmed(i18n.ts.breakFollowConfirm)) return; - - os.apiWithDialog('following/invalidate', { - userId: user.id, - }).then(() => { - user.isFollowed = !user.isFollowed; - }); - } - - async function editMemo(): Promise { - const userDetailed = await misskeyApi('users/show', { - userId: user.id, - }); - const { canceled, result } = await os.form(i18n.ts.editMemo, { - memo: { - type: 'string', - required: true, - multiline: true, - label: i18n.ts.memo, - default: userDetailed.memo, - }, - }); - if (canceled) return; - - os.apiWithDialog('users/update-memo', { - memo: result.memo, - userId: user.id, - }); - } - - const menuItems: MenuItem[] = []; - - menuItems.push({ - icon: 'ti ti-at', - text: i18n.ts.copyUsername, - action: () => { - copyToClipboard(`@${user.username}@${user.host ?? host}`); - }, - }); - - if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) { - menuItems.push({ - icon: 'ti ti-search', - text: i18n.ts.searchThisUsersNotes, - action: () => { - router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); - }, - }); - } - - if (iAmModerator) { - menuItems.push({ - icon: 'ti ti-user-exclamation', - text: i18n.ts.moderation, - action: () => { - router.push(`/admin/user/${user.id}`); - }, - }); - } - - menuItems.push({ - icon: 'ti ti-rss', - text: i18n.ts.copyRSS, - action: () => { - copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); - }, - }); - - if (user.host != null && user.url != null) { - menuItems.push({ - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - if (user.url == null) return; - window.open(user.url, '_blank', 'noopener'); - }, - }); - } else { - menuItems.push({ - icon: 'ti ti-code', - text: i18n.ts.genEmbedCode, - type: 'parent', - children: [{ - text: i18n.ts.noteOfThisUser, - action: () => { - genEmbedCode('user-timeline', user.id); - }, - }], // TODO: ユーザーカードの埋め込みなど - }); - } - - menuItems.push({ - icon: 'ti ti-share', - text: i18n.ts.copyProfileUrl, - action: () => { - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; - copyToClipboard(`${url}/${canonical}`); - }, - }); - - if ($i) { - menuItems.push({ - icon: 'ti ti-mail', - text: i18n.ts.sendMessage, - action: () => { - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; - os.post({ specified: user, initialText: `${canonical} ` }); - }, - }, { type: 'divider' }, { - icon: 'ti ti-pencil', - text: i18n.ts.editMemo, - action: editMemo, - }, { - type: 'parent', - icon: 'ti ti-list', - text: i18n.ts.addToList, - children: async () => { - const lists = await userListsCache.fetch(); - return lists.map(list => { - const isListed = ref(list.userIds?.includes(user.id) ?? false); - cleanups.push(watch(isListed, () => { - if (isListed.value) { - os.apiWithDialog('users/lists/push', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds?.push(user.id); - }); - } else { - os.apiWithDialog('users/lists/pull', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds?.splice(list.userIds.indexOf(user.id), 1); - }); - } - })); - - return { - type: 'switch', - text: list.name, - ref: isListed, - }; - }); - }, - }, { - type: 'parent', - icon: 'ti ti-antenna', - text: i18n.ts.addToAntenna, - children: async () => { - const antennas = await antennasCache.fetch(); - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; - return antennas.filter((a) => a.src === 'users').map(antenna => ({ - text: antenna.name, - action: async () => { - await os.apiWithDialog('antennas/update', { - antennaId: antenna.id, - name: antenna.name, - keywords: antenna.keywords, - excludeKeywords: antenna.excludeKeywords, - src: antenna.src, - userListId: antenna.userListId, - users: [...antenna.users, canonical], - caseSensitive: antenna.caseSensitive, - withReplies: antenna.withReplies, - withFile: antenna.withFile, - notify: antenna.notify, - }); - antennasCache.delete(); - }, - })); - }, - }); - } - - if ($i && meId !== user.id) { - if (iAmModerator) { - menuItems.push({ - type: 'parent', - icon: 'ti ti-badges', - text: i18n.ts.roles, - children: async () => { - const roles = await rolesCache.fetch(); - - return roles.filter(r => r.target === 'manual').map(r => ({ - text: r.name, - action: async () => { - const { canceled, result: period } = await os.select({ - title: i18n.ts.period + ': ' + r.name, - items: [{ - value: 'indefinitely', text: i18n.ts.indefinitely, - }, { - value: 'oneHour', text: i18n.ts.oneHour, - }, { - value: 'oneDay', text: i18n.ts.oneDay, - }, { - value: 'oneWeek', text: i18n.ts.oneWeek, - }, { - value: 'oneMonth', text: i18n.ts.oneMonth, - }], - default: 'indefinitely', - }); - if (canceled) return; - - const expiresAt = period === 'indefinitely' ? null - : period === 'oneHour' ? Date.now() + (1000 * 60 * 60) - : period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24) - : period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7) - : period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30) - : null; - - os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt }); - }, - })); - }, - }); - } - - // フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため - //if (user.isFollowing) { - const withRepliesRef = ref(user.withReplies ?? false); - - menuItems.push({ - type: 'switch', - icon: 'ti ti-messages', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withRepliesRef, - }, { - icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off', - text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes, - action: toggleNotify, - }); - - watch(withRepliesRef, (withReplies) => { - misskeyApi('following/update', { - userId: user.id, - withReplies, - }).then(() => { - user.withReplies = withReplies; - }); - }); - //} - - menuItems.push({ type: 'divider' }, { - icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', - text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, - action: toggleMute, - }, { - icon: user.isRenoteMuted ? 'ti ti-repeat' : 'ti ti-repeat-off', - text: user.isRenoteMuted ? i18n.ts.renoteUnmute : i18n.ts.renoteMute, - action: toggleRenoteMute, - }, { - icon: 'ti ti-ban', - text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, - action: toggleBlock, - }); - - if (user.isFollowed) { - menuItems.push({ - icon: 'ti ti-link-off', - text: i18n.ts.breakFollow, - action: invalidateFollow, - }); - } - - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }); - } - - if (user.host !== null) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-refresh', - text: i18n.ts.updateRemoteUser, - action: userInfoUpdate, - }); - } - - if (prefer.s.devMode) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-id', - text: i18n.ts.copyUserId, - action: () => { - copyToClipboard(user.id); - }, - }); - } - - if ($i && meId === user.id) { - menuItems.push({ type: 'divider' }, { - icon: 'ti ti-pencil', - text: i18n.ts.editProfile, - action: () => { - router.push('/settings/profile'); - }, - }); - } - - if (userActions.length > 0) { - menuItems.push({ type: 'divider' }, ...userActions.map(action => ({ - icon: 'ti ti-plug', - text: action.title, - action: () => { - action.handler(user); - }, - }))); - } - - return { - menu: menuItems, - cleanup: () => { - if (_DEV_) console.log('user menu cleanup', cleanups); - for (const cl of cleanups) { - cl(); - } - }, - }; -} diff --git a/packages/frontend/src/scripts/get-user-name.ts b/packages/frontend/src/scripts/get-user-name.ts deleted file mode 100644 index 56e91abba0..0000000000 --- a/packages/frontend/src/scripts/get-user-name.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export default function(user: { name?: string | null, username: string }): string { - return user.name === '' ? user.username : user.name ?? user.username; -} diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts deleted file mode 100644 index 04fb235694..0000000000 --- a/packages/frontend/src/scripts/hotkey.ts +++ /dev/null @@ -1,172 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { getHTMLElementOrNull } from "@/scripts/get-dom-node-or-null.js"; - -//#region types -export type Keymap = Record; - -type CallbackFunction = (ev: KeyboardEvent) => unknown; - -type CallbackObject = { - callback: CallbackFunction; - allowRepeat?: boolean; -}; - -type Pattern = { - which: string[]; - ctrl: boolean; - alt: boolean; - shift: boolean; -}; - -type Action = { - patterns: Pattern[]; - callback: CallbackFunction; - options: Required>; -}; -//#endregion - -//#region consts -const KEY_ALIASES = { - 'esc': 'Escape', - 'enter': 'Enter', - 'space': ' ', - 'up': 'ArrowUp', - 'down': 'ArrowDown', - 'left': 'ArrowLeft', - 'right': 'ArrowRight', - 'plus': ['+', ';'], -}; - -const MODIFIER_KEYS = ['ctrl', 'alt', 'shift']; - -const IGNORE_ELEMENTS = ['input', 'textarea']; -//#endregion - -//#region store -let latestHotkey: Pattern & { callback: CallbackFunction } | null = null; -//#endregion - -//#region impl -export const makeHotkey = (keymap: Keymap) => { - const actions = parseKeymap(keymap); - return (ev: KeyboardEvent) => { - if ('pswp' in window && window.pswp != null) return; - if (document.activeElement != null) { - if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return; - if (getHTMLElementOrNull(document.activeElement)?.isContentEditable) return; - } - for (const action of actions) { - if (matchPatterns(ev, action)) { - ev.preventDefault(); - ev.stopPropagation(); - action.callback(ev); - storePattern(ev, action.callback); - } - } - }; -}; - -const parseKeymap = (keymap: Keymap) => { - return Object.entries(keymap).map(([rawPatterns, rawCallback]) => { - const patterns = parsePatterns(rawPatterns); - const callback = parseCallback(rawCallback); - const options = parseOptions(rawCallback); - return { patterns, callback, options } as const satisfies Action; - }); -}; - -const parsePatterns = (rawPatterns: keyof Keymap) => { - return rawPatterns.split('|').map(part => { - const keys = part.split('+').map(trimLower); - const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x))); - const ctrl = keys.includes('ctrl'); - const alt = keys.includes('alt'); - const shift = keys.includes('shift'); - return { which, ctrl, alt, shift } as const satisfies Pattern; - }); -}; - -const parseCallback = (rawCallback: Keymap[keyof Keymap]) => { - if (typeof rawCallback === 'object') { - return rawCallback.callback; - } - return rawCallback; -}; - -const parseOptions = (rawCallback: Keymap[keyof Keymap]) => { - const defaultOptions = { - allowRepeat: false, - } as const satisfies Action['options']; - if (typeof rawCallback === 'object') { - const { callback, ...rawOptions } = rawCallback; - const options = { ...defaultOptions, ...rawOptions }; - return { ...options } as const satisfies Action['options']; - } - return { ...defaultOptions } as const satisfies Action['options']; -}; - -const matchPatterns = (ev: KeyboardEvent, action: Action) => { - const { patterns, options, callback } = action; - if (ev.repeat && !options.allowRepeat) return false; - const key = ev.key.toLowerCase(); - return patterns.some(({ which, ctrl, shift, alt }) => { - if ( - options.allowRepeat === false && - latestHotkey != null && - latestHotkey.which.includes(key) && - latestHotkey.ctrl === ctrl && - latestHotkey.alt === alt && - latestHotkey.shift === shift && - latestHotkey.callback === callback - ) { - return false; - } - if (!which.includes(key)) return false; - if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false; - if (alt !== ev.altKey) return false; - if (shift !== ev.shiftKey) return false; - return true; - }); -}; - -let lastHotKeyStoreTimer: number | null = null; - -const storePattern = (ev: KeyboardEvent, callback: CallbackFunction) => { - if (lastHotKeyStoreTimer != null) { - clearTimeout(lastHotKeyStoreTimer); - } - - latestHotkey = { - which: [ev.key.toLowerCase()], - ctrl: ev.ctrlKey || ev.metaKey, - alt: ev.altKey, - shift: ev.shiftKey, - callback, - }; - - lastHotKeyStoreTimer = window.setTimeout(() => { - latestHotkey = null; - }, 500); -}; - -const parseKeyCode = (input?: string | null) => { - if (input == null) return []; - const raw = getValueByKey(KEY_ALIASES, input); - if (raw == null) return [input]; - if (typeof raw === 'string') return [trimLower(raw)]; - return raw.map(trimLower); -}; - -const getValueByKey = < - T extends Record, - K extends keyof T | keyof any, - R extends K extends keyof T ? T[K] : T[keyof T] | undefined, ->(obj: T, key: K) => { - return obj[key] as R; -}; - -const trimLower = (str: string) => str.trim().toLowerCase(); -//#endregion diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts deleted file mode 100644 index 20f51660c7..0000000000 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// FirefoxのプライベートモードなどではindexedDBが使用不可能なので、 -// indexedDBが使えない環境ではlocalStorageを使う -import { - get as iget, - set as iset, - del as idel, -} from 'idb-keyval'; -import { miLocalStorage } from '@/local-storage.js'; - -const PREFIX = 'idbfallback::'; - -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; - -// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 -// バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと -// see https://github.com/misskey-dev/misskey/issues/13605#issuecomment-2053652123 -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error -if (window.Cypress) { - idbAvailable = false; - console.log('Cypress detected. It will use localStorage.'); -} - -if (idbAvailable) { - await iset('idb-test', 'test') - .catch(err => { - console.error('idb error', err); - console.error('indexedDB is unavailable. It will use localStorage.'); - idbAvailable = false; - }); -} else { - console.error('indexedDB is unavailable. It will use localStorage.'); -} - -export async function get(key: string) { - if (idbAvailable) return iget(key); - return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); -} - -export async function set(key: string, val: any) { - if (idbAvailable) return iset(key, val); - return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); -} - -export async function del(key: string) { - if (idbAvailable) return idel(key); - return miLocalStorage.removeItem(`${PREFIX}${key}`); -} diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/scripts/idle-render.ts deleted file mode 100644 index 6adfedcb9f..0000000000 --- a/packages/frontend/src/scripts/idle-render.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const requestIdleCallback: typeof globalThis.requestIdleCallback = globalThis.requestIdleCallback ?? ((callback) => { - const start = performance.now(); - const timeoutId = setTimeout(() => { - callback({ - didTimeout: false, // polyfill でタイムアウト発火することはない - timeRemaining() { - const diff = performance.now() - start; - return Math.max(0, 50 - diff); // - }, - }); - }); - return timeoutId; -}); -const cancelIdleCallback: typeof globalThis.cancelIdleCallback = globalThis.cancelIdleCallback ?? ((timeoutId) => { - clearTimeout(timeoutId); -}); - -class IdlingRenderScheduler { - #renderers: Set; - #rafId: number; - #ricId: number; - - constructor() { - this.#renderers = new Set(); - this.#rafId = 0; - this.#ricId = requestIdleCallback((deadline) => this.#schedule(deadline)); - } - - #schedule(deadline: IdleDeadline): void { - if (deadline.timeRemaining()) { - this.#rafId = requestAnimationFrame((time) => { - for (const renderer of this.#renderers) { - renderer(time); - } - }); - } - this.#ricId = requestIdleCallback((arg) => this.#schedule(arg)); - } - - add(renderer: FrameRequestCallback): void { - this.#renderers.add(renderer); - } - - delete(renderer: FrameRequestCallback): void { - this.#renderers.delete(renderer); - } - - dispose(): void { - this.#renderers.clear(); - cancelAnimationFrame(this.#rafId); - cancelIdleCallback(this.#ricId); - } -} - -export const defaultIdlingRenderScheduler = new IdlingRenderScheduler(); diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts deleted file mode 100644 index 037b0d9567..0000000000 --- a/packages/frontend/src/scripts/init-chart.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { - Chart, - ArcElement, - LineElement, - BarElement, - PointElement, - BarController, - LineController, - DoughnutController, - CategoryScale, - LinearScale, - TimeScale, - Legend, - Title, - Tooltip, - SubTitle, - Filler, -} from 'chart.js'; -import gradient from 'chartjs-plugin-gradient'; -import zoomPlugin from 'chartjs-plugin-zoom'; -import { MatrixController, MatrixElement } from 'chartjs-chart-matrix'; -import { store } from '@/store.js'; -import 'chartjs-adapter-date-fns'; - -export function initChart() { - Chart.register( - ArcElement, - LineElement, - BarElement, - PointElement, - BarController, - LineController, - DoughnutController, - CategoryScale, - LinearScale, - TimeScale, - Legend, - Title, - Tooltip, - SubTitle, - Filler, - MatrixController, MatrixElement, - zoomPlugin, - gradient, - ); - - // フォントカラー - Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg'); - - Chart.defaults.borderColor = store.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'; - - Chart.defaults.animation = false; -} diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts deleted file mode 100644 index 867ebf19ed..0000000000 --- a/packages/frontend/src/scripts/initialize-sw.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { lang } from '@@/js/config.js'; - -export async function initializeSw() { - if (!('serviceWorker' in navigator)) return; - - navigator.serviceWorker.register('/sw.js', { scope: '/', type: 'classic' }); - navigator.serviceWorker.ready.then(registration => { - registration.active?.postMessage({ - msg: 'initialize', - lang, - }); - }); -} diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts deleted file mode 100644 index 385f59ec39..0000000000 --- a/packages/frontend/src/scripts/intl-const.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { lang } from '@@/js/config.js'; - -export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); - -let _dateTimeFormat: Intl.DateTimeFormat; -try { - _dateTimeFormat = new Intl.DateTimeFormat(versatileLang, { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }); -} catch (err) { - console.warn(err); - if (_DEV_) console.log('[Intl] Fallback to en-US'); - - // Fallback to en-US - _dateTimeFormat = new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }); -} -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); -} catch (err) { - console.warn(err); - if (_DEV_) console.log('[Intl] Fallback to en-US'); - - // Fallback to en-US - _numberFormat = new Intl.NumberFormat('en-US'); -} -export const numberFormat = _numberFormat; diff --git a/packages/frontend/src/scripts/intl-string.ts b/packages/frontend/src/scripts/intl-string.ts deleted file mode 100644 index a5b5bbb592..0000000000 --- a/packages/frontend/src/scripts/intl-string.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { versatileLang } from '@@/js/intl-const.js'; -import type { toHiragana as toHiraganaType } from 'wanakana'; - -let toHiragana: typeof toHiraganaType = (str?: string) => str ?? ''; -let isWanakanaLoaded = false; - -/** - * ローマ字変換のセットアップ(日本語以外の環境で読み込まないのでlazy-loading) - * - * ここの比較系関数を使う際は事前に呼び出す必要がある - */ -export async function initIntlString(forceWanakana = false) { - if ((!versatileLang.includes('ja') && !forceWanakana) || isWanakanaLoaded) return; - const { toHiragana: _toHiragana } = await import('wanakana'); - toHiragana = _toHiragana; - isWanakanaLoaded = true; -} - -/** - * - 全角英数字を半角に - * - 半角カタカナを全角に - * - 濁点・半濁点がリガチャになっている(例: `か` + `゛` )ひらがな・カタカナを結合 - * - 異体字を正規化 - * - 小文字に揃える - * - 文字列のトリム - */ -export function normalizeString(str: string) { - const segmenter = new Intl.Segmenter(versatileLang, { granularity: 'grapheme' }); - return [...segmenter.segment(str)].map(({ segment }) => segment.normalize('NFKC')).join('').toLowerCase().trim(); -} - -// https://qiita.com/non-caffeine/items/77360dda05c8ce510084 -const hyphens = [ - 0x002d, // hyphen-minus - 0x02d7, // modifier letter minus sign - 0x1173, // hangul jongseong eu - 0x1680, // ogham space mark - 0x1b78, // balinese musical symbol left-hand open pang - 0x2010, // hyphen - 0x2011, // non-breaking hyphen - 0x2012, // figure dash - 0x2013, // en dash - 0x2014, // em dash - 0x2015, // horizontal bar - 0x2043, // hyphen bullet - 0x207b, // superscript minus - 0x2212, // minus sign - 0x25ac, // black rectangle - 0x2500, // box drawings light horizontal - 0x2501, // box drawings heavy horizontal - 0x2796, // heavy minus sign - 0x30fc, // katakana-hiragana prolonged sound mark - 0x3161, // hangul letter eu - 0xfe58, // small em dash - 0xfe63, // small hyphen-minus - 0xff0d, // fullwidth hyphen-minus - 0xff70, // halfwidth katakana-hiragana prolonged sound mark - 0x10110, // aegean number ten - 0x10191, // roman uncia sign -]; - -const hyphensCodePoints = hyphens.map(code => `\\u{${code.toString(16).padStart(4, '0')}}`); - -/** ハイフンを統一(ローマ字半角入力時に`ー`と`-`が判定できない問題の調整) */ -export function normalizeHyphens(str: string) { - return str.replace(new RegExp(`[${hyphensCodePoints.join('')}]`, 'ug'), '\u002d'); -} - -/** - * `normalizeString` に加えて、カタカナ・ローマ字をひらがなに揃え、ハイフンを統一 - * - * (ローマ字じゃないものもローマ字として認識され変換されるので、文字列比較の際は `normalizeString` を併用する必要あり) - */ -export function normalizeStringWithHiragana(str: string) { - return normalizeHyphens(toHiragana(normalizeString(str), { convertLongVowelMark: false })); -} - -/** aとbが同じかどうか */ -export function compareStringEquals(a: string, b: string) { - return ( - normalizeString(a) === normalizeString(b) || - normalizeStringWithHiragana(a) === normalizeStringWithHiragana(b) - ); -} - -/** baseにqueryが含まれているかどうか */ -export function compareStringIncludes(base: string, query: string) { - return ( - normalizeString(base).includes(normalizeString(query)) || - normalizeStringWithHiragana(base).includes(normalizeStringWithHiragana(query)) - ); -} diff --git a/packages/frontend/src/scripts/is-device-darkmode.ts b/packages/frontend/src/scripts/is-device-darkmode.ts deleted file mode 100644 index 4f487c7cb9..0000000000 --- a/packages/frontend/src/scripts/is-device-darkmode.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function isDeviceDarkmode() { - return window.matchMedia('(prefers-color-scheme: dark)').matches; -} diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts deleted file mode 100644 index e28e5725bc..0000000000 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { $i } from '@/account.js'; - -export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; - - if (user.followingVisibility === 'private') return false; - if (user.followingVisibility === 'followers' && !user.isFollowing) return false; - - return true; -} -export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; - - if (user.followersVisibility === 'private') return false; - if (user.followersVisibility === 'followers' && !user.isFollowing) return false; - - return true; -} diff --git a/packages/frontend/src/scripts/key-event.ts b/packages/frontend/src/scripts/key-event.ts deleted file mode 100644 index 020a6c2174..0000000000 --- a/packages/frontend/src/scripts/key-event.ts +++ /dev/null @@ -1,153 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * {@link KeyboardEvent.code} の値を表す文字列。不足分は適宜追加する - * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values - */ -export type KeyCode = ( - | 'Backspace' - | 'Tab' - | 'Enter' - | 'Shift' - | 'Control' - | 'Alt' - | 'Pause' - | 'CapsLock' - | 'Escape' - | 'Space' - | 'PageUp' - | 'PageDown' - | 'End' - | 'Home' - | 'ArrowLeft' - | 'ArrowUp' - | 'ArrowRight' - | 'ArrowDown' - | 'Insert' - | 'Delete' - | 'Digit0' - | 'Digit1' - | 'Digit2' - | 'Digit3' - | 'Digit4' - | 'Digit5' - | 'Digit6' - | 'Digit7' - | 'Digit8' - | 'Digit9' - | 'KeyA' - | 'KeyB' - | 'KeyC' - | 'KeyD' - | 'KeyE' - | 'KeyF' - | 'KeyG' - | 'KeyH' - | 'KeyI' - | 'KeyJ' - | 'KeyK' - | 'KeyL' - | 'KeyM' - | 'KeyN' - | 'KeyO' - | 'KeyP' - | 'KeyQ' - | 'KeyR' - | 'KeyS' - | 'KeyT' - | 'KeyU' - | 'KeyV' - | 'KeyW' - | 'KeyX' - | 'KeyY' - | 'KeyZ' - | 'MetaLeft' - | 'MetaRight' - | 'ContextMenu' - | 'F1' - | 'F2' - | 'F3' - | 'F4' - | 'F5' - | 'F6' - | 'F7' - | 'F8' - | 'F9' - | 'F10' - | 'F11' - | 'F12' - | 'NumLock' - | 'ScrollLock' - | 'Semicolon' - | 'Equal' - | 'Comma' - | 'Minus' - | 'Period' - | 'Slash' - | 'Backquote' - | 'BracketLeft' - | 'Backslash' - | 'BracketRight' - | 'Quote' - | 'Meta' - | 'AltGraph' -); - -/** - * 修飾キーを表す文字列。不足分は適宜追加する。 - */ -export type KeyModifier = ( - | 'Shift' - | 'Control' - | 'Alt' - | 'Meta' -); - -/** - * 押下されたキー以外の状態を表す文字列。不足分は適宜追加する。 - */ -export type KeyState = ( - | 'composing' - | 'repeat' -); - -export type KeyEventHandler = { - modifiers?: KeyModifier[]; - states?: KeyState[]; - code: KeyCode | 'any'; - handler: (event: KeyboardEvent) => void; -}; - -export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) { - function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) { - if (modifiers) { - return modifiers.every(modifier => ev.getModifierState(modifier)); - } - return true; - } - - function checkState(ev: KeyboardEvent, states?: KeyState[]) { - if (states) { - return states.every(state => ev.getModifierState(state)); - } - return true; - } - - let hit = false; - for (const handler of handlers.filter(it => it.code === event.code)) { - if (checkModifier(event, handler.modifiers) && checkState(event, handler.states)) { - handler.handler(event); - hit = true; - break; - } - } - - if (!hit) { - for (const handler of handlers.filter(it => it.code === 'any')) { - handler.handler(event); - } - } -} diff --git a/packages/frontend/src/scripts/langmap.ts b/packages/frontend/src/scripts/langmap.ts deleted file mode 100644 index b32de15963..0000000000 --- a/packages/frontend/src/scripts/langmap.ts +++ /dev/null @@ -1,671 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// TODO: sharedに置いてバックエンドのと統合したい -export const langmap = { - 'ach': { - nativeName: 'Lwo', - }, - 'ady': { - nativeName: 'Адыгэбзэ', - }, - 'af': { - nativeName: 'Afrikaans', - }, - 'af-NA': { - nativeName: 'Afrikaans (Namibia)', - }, - 'af-ZA': { - nativeName: 'Afrikaans (South Africa)', - }, - 'ak': { - nativeName: 'Tɕɥi', - }, - 'ar': { - nativeName: 'العربية', - }, - 'ar-AR': { - nativeName: 'العربية', - }, - 'ar-MA': { - nativeName: 'العربية', - }, - 'ar-SA': { - nativeName: 'العربية (السعودية)', - }, - 'ay-BO': { - nativeName: 'Aymar aru', - }, - 'az': { - nativeName: 'Azərbaycan dili', - }, - 'az-AZ': { - nativeName: 'Azərbaycan dili', - }, - 'be-BY': { - nativeName: 'Беларуская', - }, - 'bg': { - nativeName: 'Български', - }, - 'bg-BG': { - nativeName: 'Български', - }, - 'bn': { - nativeName: 'বাংলা', - }, - 'bn-IN': { - nativeName: 'বাংলা (ভারত)', - }, - 'bn-BD': { - nativeName: 'বাংলা(বাংলাদেশ)', - }, - 'br': { - nativeName: 'Brezhoneg', - }, - 'bs-BA': { - nativeName: 'Bosanski', - }, - 'ca': { - nativeName: 'Català', - }, - 'ca-ES': { - nativeName: 'Català', - }, - 'cak': { - nativeName: 'Maya Kaqchikel', - }, - 'ck-US': { - nativeName: 'ᏣᎳᎩ (tsalagi)', - }, - 'cs': { - nativeName: 'Čeština', - }, - 'cs-CZ': { - nativeName: 'Čeština', - }, - 'cy': { - nativeName: 'Cymraeg', - }, - 'cy-GB': { - nativeName: 'Cymraeg', - }, - 'da': { - nativeName: 'Dansk', - }, - 'da-DK': { - nativeName: 'Dansk', - }, - 'de': { - nativeName: 'Deutsch', - }, - 'de-AT': { - nativeName: 'Deutsch (Österreich)', - }, - 'de-DE': { - nativeName: 'Deutsch (Deutschland)', - }, - 'de-CH': { - nativeName: 'Deutsch (Schweiz)', - }, - 'dsb': { - nativeName: 'Dolnoserbšćina', - }, - 'el': { - nativeName: 'Ελληνικά', - }, - 'el-GR': { - nativeName: 'Ελληνικά', - }, - 'en': { - nativeName: 'English', - }, - 'en-GB': { - nativeName: 'English (UK)', - }, - 'en-AU': { - nativeName: 'English (Australia)', - }, - 'en-CA': { - nativeName: 'English (Canada)', - }, - 'en-IE': { - nativeName: 'English (Ireland)', - }, - 'en-IN': { - nativeName: 'English (India)', - }, - 'en-PI': { - nativeName: 'English (Pirate)', - }, - 'en-SG': { - nativeName: 'English (Singapore)', - }, - 'en-UD': { - nativeName: 'English (Upside Down)', - }, - 'en-US': { - nativeName: 'English (US)', - }, - 'en-ZA': { - nativeName: 'English (South Africa)', - }, - 'en@pirate': { - nativeName: 'English (Pirate)', - }, - 'eo': { - nativeName: 'Esperanto', - }, - 'eo-EO': { - nativeName: 'Esperanto', - }, - 'es': { - nativeName: 'Español', - }, - 'es-AR': { - nativeName: 'Español (Argentine)', - }, - 'es-419': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-CL': { - nativeName: 'Español (Chile)', - }, - 'es-CO': { - nativeName: 'Español (Colombia)', - }, - 'es-EC': { - nativeName: 'Español (Ecuador)', - }, - 'es-ES': { - nativeName: 'Español (España)', - }, - 'es-LA': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-NI': { - nativeName: 'Español (Nicaragua)', - }, - 'es-MX': { - nativeName: 'Español (México)', - }, - 'es-US': { - nativeName: 'Español (Estados Unidos)', - }, - 'es-VE': { - nativeName: 'Español (Venezuela)', - }, - 'et': { - nativeName: 'eesti keel', - }, - 'et-EE': { - nativeName: 'Eesti (Estonia)', - }, - 'eu': { - nativeName: 'Euskara', - }, - 'eu-ES': { - nativeName: 'Euskara', - }, - 'fa': { - nativeName: 'فارسی', - }, - 'fa-IR': { - nativeName: 'فارسی', - }, - 'fb-LT': { - nativeName: 'Leet Speak', - }, - 'ff': { - nativeName: 'Fulah', - }, - 'fi': { - nativeName: 'Suomi', - }, - 'fi-FI': { - nativeName: 'Suomi', - }, - 'fo': { - nativeName: 'Føroyskt', - }, - 'fo-FO': { - nativeName: 'Føroyskt (Færeyjar)', - }, - 'fr': { - nativeName: 'Français', - }, - 'fr-CA': { - nativeName: 'Français (Canada)', - }, - 'fr-FR': { - nativeName: 'Français (France)', - }, - 'fr-BE': { - nativeName: 'Français (Belgique)', - }, - 'fr-CH': { - nativeName: 'Français (Suisse)', - }, - 'fy-NL': { - nativeName: 'Frysk', - }, - 'ga': { - nativeName: 'Gaeilge', - }, - 'ga-IE': { - nativeName: 'Gaeilge', - }, - 'gd': { - nativeName: 'Gàidhlig', - }, - 'gl': { - nativeName: 'Galego', - }, - 'gl-ES': { - nativeName: 'Galego', - }, - 'gn-PY': { - nativeName: 'Avañe\'ẽ', - }, - 'gu-IN': { - nativeName: 'ગુજરાતી', - }, - 'gv': { - nativeName: 'Gaelg', - }, - 'gx-GR': { - nativeName: 'Ἑλληνική ἀρχαία', - }, - 'he': { - nativeName: 'עברית‏', - }, - 'he-IL': { - nativeName: 'עברית‏', - }, - 'hi': { - nativeName: 'हिन्दी', - }, - 'hi-IN': { - nativeName: 'हिन्दी', - }, - 'hr': { - nativeName: 'Hrvatski', - }, - 'hr-HR': { - nativeName: 'Hrvatski', - }, - 'hsb': { - nativeName: 'Hornjoserbšćina', - }, - 'ht': { - nativeName: 'Kreyòl', - }, - 'hu': { - nativeName: 'Magyar', - }, - 'hu-HU': { - nativeName: 'Magyar', - }, - 'hy': { - nativeName: 'Հայերեն', - }, - 'hy-AM': { - nativeName: 'Հայերեն (Հայաստան)', - }, - 'id': { - nativeName: 'Bahasa Indonesia', - }, - 'id-ID': { - nativeName: 'Bahasa Indonesia', - }, - 'is': { - nativeName: 'Íslenska', - }, - 'is-IS': { - nativeName: 'Íslenska (Iceland)', - }, - 'it': { - nativeName: 'Italiano', - }, - 'it-IT': { - nativeName: 'Italiano', - }, - 'ja': { - nativeName: '日本語', - }, - 'ja-JP': { - nativeName: '日本語 (日本)', - }, - 'jv-ID': { - nativeName: 'Basa Jawa', - }, - 'ka-GE': { - nativeName: 'ქართული', - }, - 'kk-KZ': { - nativeName: 'Қазақша', - }, - 'km': { - nativeName: 'ភាសាខ្មែរ', - }, - 'kl': { - nativeName: 'kalaallisut', - }, - 'km-KH': { - nativeName: 'ភាសាខ្មែរ', - }, - 'kab': { - nativeName: 'Taqbaylit', - }, - 'kn': { - nativeName: 'ಕನ್ನಡ', - }, - 'kn-IN': { - nativeName: 'ಕನ್ನಡ (India)', - }, - 'ko': { - nativeName: '한국어', - }, - 'ko-KR': { - nativeName: '한국어 (한국)', - }, - 'ku-TR': { - nativeName: 'Kurdî', - }, - 'kw': { - nativeName: 'Kernewek', - }, - 'la': { - nativeName: 'Latin', - }, - 'la-VA': { - nativeName: 'Latin', - }, - 'lb': { - nativeName: 'Lëtzebuergesch', - }, - 'li-NL': { - nativeName: 'Lèmbörgs', - }, - 'lt': { - nativeName: 'Lietuvių', - }, - 'lt-LT': { - nativeName: 'Lietuvių', - }, - 'lv': { - nativeName: 'Latviešu', - }, - 'lv-LV': { - nativeName: 'Latviešu', - }, - 'mai': { - nativeName: 'मैथिली, মৈথিলী', - }, - 'mg-MG': { - nativeName: 'Malagasy', - }, - 'mk': { - nativeName: 'Македонски', - }, - 'mk-MK': { - nativeName: 'Македонски (Македонски)', - }, - 'ml': { - nativeName: 'മലയാളം', - }, - 'ml-IN': { - nativeName: 'മലയാളം', - }, - 'mn-MN': { - nativeName: 'Монгол', - }, - 'mr': { - nativeName: 'मराठी', - }, - 'mr-IN': { - nativeName: 'मराठी', - }, - 'ms': { - nativeName: 'Bahasa Melayu', - }, - 'ms-MY': { - nativeName: 'Bahasa Melayu', - }, - 'mt': { - nativeName: 'Malti', - }, - 'mt-MT': { - nativeName: 'Malti', - }, - 'my': { - nativeName: 'ဗမာစကာ', - }, - 'no': { - nativeName: 'Norsk', - }, - 'nb': { - nativeName: 'Norsk (bokmål)', - }, - 'nb-NO': { - nativeName: 'Norsk (bokmål)', - }, - 'ne': { - nativeName: 'नेपाली', - }, - 'ne-NP': { - nativeName: 'नेपाली', - }, - 'nl': { - nativeName: 'Nederlands', - }, - 'nl-BE': { - nativeName: 'Nederlands (België)', - }, - 'nl-NL': { - nativeName: 'Nederlands (Nederland)', - }, - 'nn-NO': { - nativeName: 'Norsk (nynorsk)', - }, - 'oc': { - nativeName: 'Occitan', - }, - 'or-IN': { - nativeName: 'ଓଡ଼ିଆ', - }, - 'pa': { - nativeName: 'ਪੰਜਾਬੀ', - }, - 'pa-IN': { - nativeName: 'ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)', - }, - 'pl': { - nativeName: 'Polski', - }, - 'pl-PL': { - nativeName: 'Polski', - }, - 'ps-AF': { - nativeName: 'پښتو', - }, - 'pt': { - nativeName: 'Português', - }, - 'pt-BR': { - nativeName: 'Português (Brasil)', - }, - 'pt-PT': { - nativeName: 'Português (Portugal)', - }, - 'qu-PE': { - nativeName: 'Qhichwa', - }, - 'rm-CH': { - nativeName: 'Rumantsch', - }, - 'ro': { - nativeName: 'Română', - }, - 'ro-RO': { - nativeName: 'Română', - }, - 'ru': { - nativeName: 'Русский', - }, - 'ru-RU': { - nativeName: 'Русский', - }, - 'sa-IN': { - nativeName: 'संस्कृतम्', - }, - 'se-NO': { - nativeName: 'Davvisámegiella', - }, - 'sh': { - nativeName: 'српскохрватски', - }, - 'si-LK': { - nativeName: 'සිංහල', - }, - 'sk': { - nativeName: 'Slovenčina', - }, - 'sk-SK': { - nativeName: 'Slovenčina (Slovakia)', - }, - 'sl': { - nativeName: 'Slovenščina', - }, - 'sl-SI': { - nativeName: 'Slovenščina', - }, - 'so-SO': { - nativeName: 'Soomaaliga', - }, - 'sq': { - nativeName: 'Shqip', - }, - 'sq-AL': { - nativeName: 'Shqip', - }, - 'sr': { - nativeName: 'Српски', - }, - 'sr-RS': { - nativeName: 'Српски (Serbia)', - }, - 'su': { - nativeName: 'Basa Sunda', - }, - 'sv': { - nativeName: 'Svenska', - }, - 'sv-SE': { - nativeName: 'Svenska', - }, - 'sw': { - nativeName: 'Kiswahili', - }, - 'sw-KE': { - nativeName: 'Kiswahili', - }, - 'ta': { - nativeName: 'தமிழ்', - }, - 'ta-IN': { - nativeName: 'தமிழ்', - }, - 'te': { - nativeName: 'తెలుగు', - }, - 'te-IN': { - nativeName: 'తెలుగు', - }, - 'tg': { - nativeName: 'забо́ни тоҷикӣ́', - }, - 'tg-TJ': { - nativeName: 'тоҷикӣ', - }, - 'th': { - nativeName: 'ภาษาไทย', - }, - 'th-TH': { - nativeName: 'ภาษาไทย (ประเทศไทย)', - }, - 'fil': { - nativeName: 'Filipino', - }, - 'tlh': { - nativeName: 'tlhIngan-Hol', - }, - 'tr': { - nativeName: 'Türkçe', - }, - 'tr-TR': { - nativeName: 'Türkçe', - }, - 'tt-RU': { - nativeName: 'татарча', - }, - 'uk': { - nativeName: 'Українська', - }, - 'uk-UA': { - nativeName: 'Українська', - }, - 'ur': { - nativeName: 'اردو', - }, - 'ur-PK': { - nativeName: 'اردو', - }, - 'uz': { - nativeName: 'O\'zbek', - }, - 'uz-UZ': { - nativeName: 'O\'zbek', - }, - 'vi': { - nativeName: 'Tiếng Việt', - }, - 'vi-VN': { - nativeName: 'Tiếng Việt', - }, - 'xh-ZA': { - nativeName: 'isiXhosa', - }, - 'yi': { - nativeName: 'ייִדיש', - }, - 'yi-DE': { - nativeName: 'ייִדיש (German)', - }, - 'zh': { - nativeName: '中文', - }, - 'zh-Hans': { - nativeName: '中文简体', - }, - 'zh-Hant': { - nativeName: '中文繁體', - }, - 'zh-CN': { - nativeName: '中文(中国大陆)', - }, - 'zh-HK': { - nativeName: '中文(香港)', - }, - 'zh-SG': { - nativeName: '中文(新加坡)', - }, - 'zh-TW': { - nativeName: '中文(台灣)', - }, - 'zu-ZA': { - nativeName: 'isiZulu', - }, -}; diff --git a/packages/frontend/src/scripts/login-id.ts b/packages/frontend/src/scripts/login-id.ts deleted file mode 100644 index b52735caa0..0000000000 --- a/packages/frontend/src/scripts/login-id.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function getUrlWithLoginId(url: string, loginId: string) { - const u = new URL(url, origin); - u.searchParams.append('loginId', loginId); - return u.toString(); -} - -export function getUrlWithoutLoginId(url: string) { - const u = new URL(url); - u.searchParams.delete('loginId'); - return u.toString(); -} diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts deleted file mode 100644 index 02f589c7ca..0000000000 --- a/packages/frontend/src/scripts/lookup.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 { Router } from '@/nirax.js'; -import { mainRouter } from '@/router/main.js'; - -export async function lookup(router?: Router) { - const _router = router ?? mainRouter; - - const { canceled, result: temp } = await os.inputText({ - title: i18n.ts.lookup, - }); - const query = temp ? temp.trim() : ''; - if (canceled || query.length <= 1) return; - - if (query.startsWith('@') && !query.includes(' ')) { - _router.push(`/${query}`); - return; - } - - if (query.startsWith('#')) { - _router.push(`/tags/${encodeURIComponent(query.substring(1))}`); - return; - } - - if (query.startsWith('https://')) { - const res = await apLookup(query); - - if (res.type === 'User') { - _router.push(`/@${res.object.username}@${res.object.host}`); - } else if (res.type === 'Note') { - _router.push(`/notes/${res.object.id}`); - } - - return; - } -} - -export async function apLookup(query: string) { - const promise = misskeyApi('ap/show', { - uri: query, - }); - - os.promiseDialog(promise, null, (err) => { - let title = i18n.ts.somethingHappened; - let text = err.message + '\n' + err.id; - - switch (err.id) { - case '974b799e-1a29-4889-b706-18d4dd93e266': - title = i18n.ts._remoteLookupErrors._federationNotAllowed.title; - text = i18n.ts._remoteLookupErrors._federationNotAllowed.description; - break; - case '1a5eab56-e47b-48c2-8d5e-217b897d70db': - title = i18n.ts._remoteLookupErrors._uriInvalid.title; - text = i18n.ts._remoteLookupErrors._uriInvalid.description; - break; - case '81b539cf-4f57-4b29-bc98-032c33c0792e': - title = i18n.ts._remoteLookupErrors._requestFailed.title; - text = i18n.ts._remoteLookupErrors._requestFailed.description; - break; - case '70193c39-54f3-4813-82f0-70a680f7495b': - title = i18n.ts._remoteLookupErrors._responseInvalid.title; - text = i18n.ts._remoteLookupErrors._responseInvalid.description; - break; - case 'dc94d745-1262-4e63-a17d-fecaa57efc82': - title = i18n.ts._remoteLookupErrors._noSuchObject.title; - text = i18n.ts._remoteLookupErrors._noSuchObject.description; - break; - } - - os.alert({ - type: 'error', - title, - text, - }); - }, i18n.ts.fetchingAsApObject); - - return await promise; -} diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/scripts/media-has-audio.ts deleted file mode 100644 index 4bf3ee5d97..0000000000 --- a/packages/frontend/src/scripts/media-has-audio.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export default async function hasAudio(media: HTMLMediaElement) { - const cloned = media.cloneNode() as HTMLMediaElement; - cloned.muted = (cloned as typeof cloned & Partial).playsInline = true; - cloned.play(); - await new Promise((resolve) => cloned.addEventListener('playing', resolve)); - const result = !!(cloned as any).audioTracks?.length || (cloned as any).mozHasAudio || !!(cloned as any).webkitAudioDecodedByteCount; - cloned.remove(); - return result; -} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts deleted file mode 100644 index 78eba35ead..0000000000 --- a/packages/frontend/src/scripts/media-proxy.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { MediaProxy } from '@@/js/media-proxy.js'; -import { url } from '@@/js/config.js'; -import { instance } from '@/instance.js'; - -let _mediaProxy: MediaProxy | null = null; - -export function getProxiedImageUrl(...args: Parameters): string { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); - } - - return _mediaProxy.getProxiedImageUrl(...args); -} - -export function getProxiedImageUrlNullable(...args: Parameters): string | null { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); - } - - return _mediaProxy.getProxiedImageUrlNullable(...args); -} - -export function getStaticImageUrl(...args: Parameters): string { - if (_mediaProxy == null) { - _mediaProxy = new MediaProxy(instance, url); - } - - return _mediaProxy.getStaticImageUrl(...args); -} diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts deleted file mode 100644 index 004b6d42a4..0000000000 --- a/packages/frontend/src/scripts/merge.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { deepClone } from './clone.js'; -import type { Cloneable } from './clone.js'; - -export type DeepPartial = { - [P in keyof T]?: T[P] extends Record ? DeepPartial : T[P]; -}; - -function isPureObject(value: unknown): value is Record { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -/** - * valueにないキーをdefからもらう(再帰的)\ - * nullはそのまま、undefinedはdefの値 - **/ -export function deepMerge>(value: DeepPartial, 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>; - result[k] = deepMerge(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 deleted file mode 100644 index a2f777f623..0000000000 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { nextTick } from 'vue'; -import type { Ref } from 'vue'; -import * as os from '@/os.js'; -import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@@/js/const.js'; -import type { MenuItem } from '@/types/menu.js'; - -/** - * MFMの装飾のリストを表示する - */ -export function mfmFunctionPicker(src: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref) { - os.popupMenu([{ - text: i18n.ts.addMfmFunction, - type: 'label', - }, ...getFunctionList(textArea, textRef)], src); -} - -function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref): MenuItem[] { - return MFM_TAGS.map(tag => ({ - text: tag, - icon: 'ti ti-icons', - action: () => add(textArea, textRef, tag), - })); -} - -function add(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref, type: string) { - const caretStart: number = textArea.selectionStart as number; - const caretEnd: number = textArea.selectionEnd as number; - - MFM_TAGS.forEach(tag => { - if (type === tag) { - if (caretStart === caretEnd) { - // 単純にFunctionを追加 - const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ]${textRef.value.substring(caretEnd)}`; - textRef.value = trimmedText; - } else { - // 選択範囲を囲むようにFunctionを追加 - const trimmedText = `${textRef.value.substring(0, caretStart)}$[${type} ${textRef.value.substring(caretStart, caretEnd)}]${textRef.value.substring(caretEnd)}`; - textRef.value = trimmedText; - } - } - }); - - const nextCaretStart: number = caretStart + 3 + type.length; - const nextCaretEnd: number = caretEnd + 3 + type.length; - - // キャレットを戻す - nextTick(() => { - textArea.focus(); - textArea.setSelectionRange(nextCaretStart, nextCaretEnd); - }); -} diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/scripts/misskey-api.ts deleted file mode 100644 index dc07ad477b..0000000000 --- a/packages/frontend/src/scripts/misskey-api.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { ref } from 'vue'; -import { apiUrl } from '@@/js/config.js'; -import { $i } from '@/account.js'; -export const pendingApiRequestsCount = ref(0); - -export type Endpoint = keyof Misskey.Endpoints; - -export type Request = Misskey.Endpoints[E]['req']; - -export type AnyRequest = - (E extends Endpoint ? Request : never) | object; - -export type Response> = - E extends Endpoint - ? P extends Request ? Misskey.api.SwitchCaseResponseType : never - : object; - -// Implements Misskey.api.ApiClient.request -export function misskeyApi< - ResT = void, - E extends Endpoint | NonNullable = Endpoint, - P extends AnyRequest = E extends Endpoint ? Request : never, - _ResT = ResT extends void ? Response : ResT, ->( - endpoint: E, - data: P & { i?: string | null; } = {} as any, - token?: string | null | undefined, - signal?: AbortSignal, -): Promise<_ResT> { - if (endpoint.includes('://')) throw new Error('invalid endpoint'); - pendingApiRequestsCount.value++; - - const onFinally = () => { - pendingApiRequestsCount.value--; - }; - - const promise = new Promise<_ResT>((resolve, reject) => { - // Append a credential - if ($i) data.i = $i.token; - if (token !== undefined) data.i = token; - - // Send request - window.fetch(`${apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: 'omit', - cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - }, - signal, - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(undefined as _ResT); // void -> undefined - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; -} - -// Implements Misskey.api.ApiClient.request -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 : ResT, ->( - endpoint: E, - data: P = {} as any, -): Promise<_ResT> { - pendingApiRequestsCount.value++; - - const onFinally = () => { - pendingApiRequestsCount.value--; - }; - - const query = new URLSearchParams(data as any); - - const promise = new Promise<_ResT>((resolve, reject) => { - // Send request - window.fetch(`${apiUrl}/${endpoint}?${query}`, { - method: 'GET', - credentials: 'omit', - cache: 'default', - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(undefined as _ResT); // void -> undefined - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; -} diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/scripts/navigator.ts deleted file mode 100644 index ffc0a457f4..0000000000 --- a/packages/frontend/src/scripts/navigator.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function isSupportShare(): boolean { - return 'share' in navigator; -} diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts deleted file mode 100644 index 671751147c..0000000000 --- a/packages/frontend/src/scripts/page-metadata.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { inject, isRef, onActivated, onBeforeUnmount, provide, ref, toValue, watch } from 'vue'; -import type { MaybeRefOrGetter, Ref } from 'vue'; - -export type PageMetadata = { - title: string; - subtitle?: string; - icon?: string | null; - avatar?: Misskey.entities.User | null; - userName?: Misskey.entities.User | null; - needWideArea?: boolean; -}; - -type PageMetadataGetter = () => PageMetadata; -type PageMetadataReceiver = (getter: PageMetadataGetter) => void; - -const RECEIVER_KEY = Symbol('ReceiverKey'); -const setReceiver = (v: PageMetadataReceiver): void => { - provide(RECEIVER_KEY, v); -}; -const getReceiver = (): PageMetadataReceiver | undefined => { - return inject(RECEIVER_KEY); -}; - -const METADATA_KEY = Symbol('MetadataKey'); -const setMetadata = (v: Ref): void => { - provide>(METADATA_KEY, v); -}; -const getMetadata = (): Ref | undefined => { - return inject>(METADATA_KEY); -}; - -export const definePageMetadata = (maybeRefOrGetterMetadata: MaybeRefOrGetter): void => { - const metadataRef = ref(toValue(maybeRefOrGetterMetadata)); - const metadataGetter = () => metadataRef.value; - const receiver = getReceiver(); - - // setup handler - receiver?.(metadataGetter); - - // update handler - onBeforeUnmount(watch( - () => toValue(maybeRefOrGetterMetadata), - (metadata) => { - metadataRef.value = metadata; - receiver?.(metadataGetter); - }, - { deep: true }, - )); - onActivated(() => { - receiver?.(metadataGetter); - }); -}; - -export const provideMetadataReceiver = (receiver: PageMetadataReceiver): void => { - setReceiver(receiver); -}; - -export const provideReactiveMetadata = (metadataRef: Ref): void => { - setMetadata(metadataRef); -}; - -export const injectReactiveMetadata = (): Ref => { - 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 deleted file mode 100644 index 8a4e9319b3..0000000000 --- a/packages/frontend/src/scripts/physics.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Matter from 'matter-js'; - -export function physics(container: HTMLElement) { - const containerWidth = container.offsetWidth; - const containerHeight = container.offsetHeight; - const containerCenterX = containerWidth / 2; - - // サイズ固定化(要らないかも?) - container.style.position = 'relative'; - container.style.boxSizing = 'border-box'; - container.style.width = `${containerWidth}px`; - container.style.height = `${containerHeight}px`; - - // create engine - const engine = Matter.Engine.create({ - constraintIterations: 4, - positionIterations: 8, - velocityIterations: 8, - }); - - const world = engine.world; - - // create renderer - const render = Matter.Render.create({ - engine: engine, - //element: document.getElementById('debug'), - options: { - width: containerWidth, - height: containerHeight, - background: 'transparent', // transparent to hide - wireframeBackground: 'transparent', // transparent to hide - }, - }); - - // Disable to hide debug - Matter.Render.run(render); - - // create runner - const runner = Matter.Runner.create(); - Matter.Runner.run(runner, engine); - - const groundThickness = 1024; - const ground = Matter.Bodies.rectangle(containerCenterX, containerHeight + (groundThickness / 2), containerWidth, groundThickness, { - isStatic: true, - restitution: 0.1, - friction: 2, - }); - - //const wallRight = Matter.Bodies.rectangle(window.innerWidth+50, window.innerHeight/2, 100, window.innerHeight, wallopts); - //const wallLeft = Matter.Bodies.rectangle(-50, window.innerHeight/2, 100, window.innerHeight, wallopts); - - Matter.World.add(world, [ - ground, - //wallRight, - //wallLeft, - ]); - - const objEls = Array.from(container.children) as HTMLElement[]; - const objs: Matter.Body[] = []; - for (const objEl of objEls) { - const left = objEl.dataset.physicsX ? parseInt(objEl.dataset.physicsX) : objEl.offsetLeft; - const top = objEl.dataset.physicsY ? parseInt(objEl.dataset.physicsY) : objEl.offsetTop; - - let obj: Matter.Body; - if (objEl.classList.contains('_physics_circle_')) { - obj = Matter.Bodies.circle( - left + (objEl.offsetWidth / 2), - top + (objEl.offsetHeight / 2), - Math.max(objEl.offsetWidth, objEl.offsetHeight) / 2, - { - restitution: 0.5, - }, - ); - } else { - const style = window.getComputedStyle(objEl); - obj = Matter.Bodies.rectangle( - left + (objEl.offsetWidth / 2), - top + (objEl.offsetHeight / 2), - objEl.offsetWidth, - objEl.offsetHeight, - { - chamfer: { radius: parseInt(style.borderRadius || '0', 10) }, - restitution: 0.5, - }, - ); - } - objEl.id = obj.id.toString(); - objs.push(obj); - } - - Matter.World.add(engine.world, objs); - - // Add mouse control - - const mouse = Matter.Mouse.create(container); - const mouseConstraint = Matter.MouseConstraint.create(engine, { - mouse: mouse, - constraint: { - stiffness: 0.1, - render: { - visible: false, - }, - }, - }); - - Matter.World.add(engine.world, mouseConstraint); - - // keep the mouse in sync with rendering - render.mouse = mouse; - - for (const objEl of objEls) { - objEl.style.position = 'absolute'; - objEl.style.top = '0'; - objEl.style.left = '0'; - objEl.style.margin = '0'; - } - - window.requestAnimationFrame(update); - - let stop = false; - - function update() { - for (const objEl of objEls) { - const obj = objs.find(obj => obj.id.toString() === objEl.id.toString()); - if (obj == null) continue; - - const x = (obj.position.x - objEl.offsetWidth / 2); - const y = (obj.position.y - objEl.offsetHeight / 2); - const angle = obj.angle; - objEl.style.transform = `translate(${x}px, ${y}px) rotate(${angle}rad)`; - } - - if (!stop) { - window.requestAnimationFrame(update); - } - } - - // 奈落に落ちたオブジェクトは消す - const intervalId = window.setInterval(() => { - for (const obj of objs) { - if (obj.position.y > (containerHeight + 1024)) Matter.World.remove(world, obj); - } - }, 1000 * 10); - - return { - stop: () => { - stop = true; - Matter.Runner.stop(runner); - window.clearInterval(intervalId); - }, - }; -} diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/scripts/player-url-transform.ts deleted file mode 100644 index 39c6df6500..0000000000 --- a/packages/frontend/src/scripts/player-url-transform.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import { hostname } from '@@/js/config.js'; - -export function transformPlayerUrl(url: string): string { - const urlObj = new URL(url); - if (!['https:', 'http:'].includes(urlObj.protocol)) throw new Error('Invalid protocol'); - - const urlParams = new URLSearchParams(urlObj.search); - - if (urlObj.hostname === 'player.twitch.tv') { - // TwitchはCSPの制約あり - // https://dev.twitch.tv/docs/embed/video-and-clips/ - urlParams.set('parent', hostname); - urlParams.set('allowfullscreen', ''); - urlParams.set('autoplay', 'true'); - } else { - urlParams.set('autoplay', '1'); - urlParams.set('auto_play', '1'); - } - urlObj.search = urlParams.toString(); - - return urlObj.toString(); -} diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts deleted file mode 100644 index a8a330eb6d..0000000000 --- a/packages/frontend/src/scripts/please-login.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { defineAsyncComponent } from 'vue'; -import { $i } from '@/account.js'; -import { instance } from '@/instance.js'; -import { i18n } from '@/i18n.js'; -import { popup } from '@/os.js'; - -export type OpenOnRemoteOptions = { - /** - * 外部のMisskey Webで特定のパスを開く - */ - type: 'web'; - - /** - * 内部パス(例: `/settings`) - */ - path: string; -} | { - /** - * 外部のMisskey Webで照会する - */ - type: 'lookup'; - - /** - * 照会したいエンティティのURL - * - * (例: `https://misskey.example.com/notes/abcdexxxxyz`) - */ - url: string; -} | { - /** - * 外部のMisskeyでノートする - */ - type: 'share'; - - /** - * `/share` ページに渡すクエリストリング - * - * @see https://go.misskey-hub.net/spec/share/ - */ - params: Record; -}; - -export function pleaseLogin(opts: { - path?: string; - message?: string; - openOnRemote?: OpenOnRemoteOptions; -} = {}) { - if ($i) return; - - let _openOnRemote: OpenOnRemoteOptions | undefined = undefined; - - // 連合できる場合と、(連合ができなくても)共有する場合は外部連携オプションを設定 - if (opts.openOnRemote != null && (instance.federation !== 'none' || opts.openOnRemote.type === 'share')) { - _openOnRemote = opts.openOnRemote; - } - - const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { - autoSet: true, - message: opts.message ?? (_openOnRemote ? i18n.ts.signinOrContinueOnRemote : i18n.ts.signinRequired), - openOnRemote: _openOnRemote, - }, { - cancelled: () => { - if (opts.path) { - window.location.href = opts.path; - } - }, - closed: () => dispose(), - }); - - throw new Error('signin required'); -} diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts deleted file mode 100644 index 5b141222e8..0000000000 --- a/packages/frontend/src/scripts/popout.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { appendQuery } from '@@/js/url.js'; -import * as config from '@@/js/config.js'; - -export function popout(path: string, w?: HTMLElement) { - let url = path.startsWith('http://') || path.startsWith('https://') ? path : config.url + path; - url = appendQuery(url, 'zen'); - if (w) { - const position = w.getBoundingClientRect(); - const width = parseInt(getComputedStyle(w, '').width, 10); - const height = parseInt(getComputedStyle(w, '').height, 10); - const x = window.screenX + position.left; - const y = window.screenY + position.top; - window.open(url, url, - `width=${width}, height=${height}, top=${y}, left=${x}`); - } else { - const width = 400; - const height = 500; - const x = window.top.outerHeight / 2 + window.top.screenY - (height / 2); - const y = window.top.outerWidth / 2 + window.top.screenX - (width / 2); - window.open(url, url, - `width=${width}, height=${height}, top=${x}, left=${y}`); - } -} diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts deleted file mode 100644 index 3dad41a8b3..0000000000 --- a/packages/frontend/src/scripts/popup-position.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function calcPopupPosition(el: HTMLElement, props: { - anchorElement?: HTMLElement | null; - innerMargin: number; - direction: 'top' | 'bottom' | 'left' | 'right'; - align: 'top' | 'bottom' | 'left' | 'right' | 'center'; - alignOffset?: number; - x?: number; - y?: number; -}): { top: number; left: number; transformOrigin: string; } { - const contentWidth = el.offsetWidth; - const contentHeight = el.offsetHeight; - - let rect: DOMRect; - - if (props.anchorElement) { - rect = props.anchorElement.getBoundingClientRect(); - } - - const calcPosWhenTop = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.scrollY - contentHeight) - props.innerMargin; - } else { - left = props.x; - top = (props.y - contentHeight) - props.innerMargin; - } - - left -= (el.offsetWidth / 2); - - if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - 1; - } - - return [left, top]; - }; - - const calcPosWhenBottom = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = rect.left + window.scrollX + (props.anchorElement.offsetWidth / 2); - top = (rect.top + window.scrollY + props.anchorElement.offsetHeight) + props.innerMargin; - } else { - left = props.x; - top = (props.y) + props.innerMargin; - } - - left -= (el.offsetWidth / 2); - - if (left + contentWidth - window.scrollX > window.innerWidth) { - left = window.innerWidth - contentWidth + window.scrollX - 1; - } - - return [left, top]; - }; - - const calcPosWhenLeft = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = (rect.left + window.scrollX - contentWidth) - props.innerMargin; - top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2); - } else { - left = (props.x - contentWidth) - props.innerMargin; - top = props.y; - } - - top -= (el.offsetHeight / 2); - - if (top + contentHeight - window.scrollY > window.innerHeight) { - top = window.innerHeight - contentHeight + window.scrollY - 1; - } - - return [left, top]; - }; - - const calcPosWhenRight = () => { - let left: number; - let top: number; - - if (props.anchorElement) { - left = (rect.left + props.anchorElement.offsetWidth + window.scrollX) + props.innerMargin; - - if (props.align === 'top') { - top = rect.top + window.scrollY; - if (props.alignOffset != null) top += props.alignOffset; - } else if (props.align === 'bottom') { - // TODO - } else { // center - top = rect.top + window.scrollY + (props.anchorElement.offsetHeight / 2); - top -= (el.offsetHeight / 2); - } - } else { - left = props.x + props.innerMargin; - top = props.y; - top -= (el.offsetHeight / 2); - } - - if (top + contentHeight - window.scrollY > window.innerHeight) { - top = window.innerHeight - contentHeight + window.scrollY - 1; - } - - return [left, top]; - }; - - const calc = (): { - left: number; - top: number; - transformOrigin: string; - } => { - switch (props.direction) { - case 'top': { - const [left, top] = calcPosWhenTop(); - - // ツールチップを上に向かって表示するスペースがなければ下に向かって出す - if (top - window.scrollY < 0) { - const [left, top] = calcPosWhenBottom(); - return { left, top, transformOrigin: 'center top' }; - } - - return { left, top, transformOrigin: 'center bottom' }; - } - - case 'bottom': { - const [left, top] = calcPosWhenBottom(); - // TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す - return { left, top, transformOrigin: 'center top' }; - } - - case 'left': { - const [left, top] = calcPosWhenLeft(); - - // ツールチップを左に向かって表示するスペースがなければ右に向かって出す - if (left - window.scrollX < 0) { - const [left, top] = calcPosWhenRight(); - return { left, top, transformOrigin: 'left center' }; - } - - return { left, top, transformOrigin: 'right center' }; - } - - case 'right': { - const [left, top] = calcPosWhenRight(); - // TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す - return { left, top, transformOrigin: 'left center' }; - } - } - }; - - return calc(); -} diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts deleted file mode 100644 index 11b6f52ddd..0000000000 --- a/packages/frontend/src/scripts/post-message.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const postMessageEventTypes = [ - 'misskey:shareForm:shareCompleted', -] as const; - -export type PostMessageEventType = typeof postMessageEventTypes[number]; - -export type MiPostMessageEvent = { - type: PostMessageEventType; - payload?: any; -}; - -/** - * 親フレームにイベントを送信 - */ -export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.parent.postMessage({ - type, - payload, - }, '*'); -} diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts deleted file mode 100644 index 81f6c02dcf..0000000000 --- a/packages/frontend/src/scripts/reaction-picker.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as Misskey from 'misskey-js'; -import { defineAsyncComponent, ref } from 'vue'; -import type { Ref } from 'vue'; -import { popup } from '@/os.js'; -import { store } from '@/store.js'; - -class ReactionPicker { - private src: Ref = ref(null); - private manualShowing = ref(false); - private targetNote: Ref = ref(null); - private onChosen?: (reaction: string) => void; - private onClosed?: () => void; - - constructor() { - // nop - } - - public async init() { - const reactionsRef = store.reactiveState.reactions; - await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { - src: this.src, - pinnedEmojis: reactionsRef, - asReactionPicker: true, - targetNote: this.targetNote, - manualShowing: this.manualShowing, - }, { - done: reaction => { - if (this.onChosen) this.onChosen(reaction); - }, - close: () => { - this.manualShowing.value = false; - }, - closed: () => { - this.src.value = null; - if (this.onClosed) this.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; - } -} - -export const reactionPicker = new ReactionPicker(); diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/scripts/reload-ask.ts deleted file mode 100644 index 733d91b85a..0000000000 --- a/packages/frontend/src/scripts/reload-ask.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { i18n } from '@/i18n.js'; -import * as os from '@/os.js'; -import { unisonReload } from '@/scripts/unison-reload.js'; - -let isReloadConfirming = false; - -export async function reloadAsk(opts: { - unison?: boolean; - reason?: string; -}) { - if (isReloadConfirming) { - return; - } - - isReloadConfirming = true; - - const { canceled } = await os.confirm(opts.reason == null ? { - type: 'info', - text: i18n.ts.reloadConfirm, - } : { - type: 'info', - title: i18n.ts.reloadConfirm, - text: opts.reason, - }).finally(() => { - isReloadConfirming = false; - }); - - if (canceled) return; - - if (opts.unison) { - unisonReload(); - } else { - location.reload(); - } -} diff --git a/packages/frontend/src/scripts/search-emoji.ts b/packages/frontend/src/scripts/search-emoji.ts deleted file mode 100644 index 371f69b9a7..0000000000 --- a/packages/frontend/src/scripts/search-emoji.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export type EmojiDef = { - emoji: string; - name: string; - url: string; - aliasOf?: string; -} | { - emoji: string; - name: string; - aliasOf?: string; - isCustomEmoji?: true; -}; -type EmojiScore = { emoji: EmojiDef, score: number }; - -export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] { - if (!query) { - return []; - } - - const matched = new Map(); - // 完全一致(エイリアスなし) - emojiDb.some(x => { - if (x.name === query && !x.aliasOf) { - matched.set(x.name, { emoji: x, score: query.length + 3 }); - } - return matched.size === max; - }); - - // 完全一致(エイリアス込み) - if (matched.size < max) { - emojiDb.some(x => { - 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.has(x.name)) { - matched.set(x.name, { emoji: x, score: query.length + 1 }); - } - return matched.size === max; - }); - } - - // 前方一致(エイリアス込み) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) { - matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length }); - } - return matched.size === max; - }); - } - - // 部分一致(エイリアス込み) - if (matched.size < max) { - emojiDb.some(x => { - if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) { - matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 }); - } - return matched.size === max; - }); - } - - // 簡易あいまい検索(3文字以上) - if (matched.size < max && query.length > 3) { - const queryChars = [...query]; - const hitEmojis = new Map(); - - for (const x of emojiDb) { - // 文字列の位置を進めながら、クエリの文字を順番に探す - - let pos = 0; - let hit = 0; - for (const c of queryChars) { - pos = x.name.indexOf(c, pos); - if (pos <= -1) break; - hit++; - } - - // 半分以上の文字が含まれていればヒットとする - if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) { - hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 }); - } - } - - // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分) - [...hitEmojis.values()] - .sort((x, y) => y.score - x.score) - .slice(0, 6) - .forEach(it => matched.set(it.emoji.name, it)); - } - - return [...matched.values()] - .sort((x, y) => y.score - x.score) - .slice(0, max) - .map(it => it.emoji); -} diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts deleted file mode 100644 index 42b34f54f5..0000000000 --- a/packages/frontend/src/scripts/select-file.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 { uploadFile } from '@/scripts/upload.js'; -import { prefer } from '@/preferences.js'; - -export function chooseFileFromPc( - multiple: boolean, - options?: { - uploadFolder?: string | null; - keepOriginal?: boolean; - nameConverter?: (file: File) => string | undefined; - }, -): Promise { - const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder; - const keepOriginal = options?.keepOriginal ?? prefer.s.keepOriginalUploading; - const nameConverter = options?.nameConverter ?? (() => undefined); - - return new Promise((res, rej) => { - const input = document.createElement('input'); - input.type = 'file'; - input.multiple = multiple; - input.onchange = () => { - if (!input.files) return res([]); - const promises = Array.from( - input.files, - file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal), - ); - - Promise.all(promises).then(driveFiles => { - res(driveFiles); - }).catch(err => { - // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない - }); - - // 一応廃棄 - (window as any).__misskey_input_ref__ = null; - }; - - // https://qiita.com/fukasawah/items/b9dc732d95d99551013d - // iOS Safari で正常に動かす為のおまじない - (window as any).__misskey_input_ref__ = input; - - input.click(); - }); -} - -export function chooseFileFromDrive(multiple: boolean): Promise { - return new Promise((res, rej) => { - os.selectDriveFile(multiple).then(files => { - res(files); - }); - }); -} - -export function chooseFileFromUrl(): Promise { - return new Promise((res, rej) => { - os.inputText({ - title: i18n.ts.uploadFromUrl, - type: 'url', - placeholder: i18n.ts.uploadFromUrlDescription, - }).then(({ canceled, result: url }) => { - if (canceled) return; - - const marker = Math.random().toString(); // TODO: UUIDとか使う - - const connection = useStream().useChannel('main'); - connection.on('urlUploadFinished', urlResponse => { - if (urlResponse.marker === marker) { - res(urlResponse.file); - connection.dispose(); - } - }); - - misskeyApi('drive/files/upload-from-url', { - url: url, - folderId: prefer.s.uploadFolder, - marker, - }); - - os.alert({ - title: i18n.ts.uploadFromUrlRequested, - text: i18n.ts.uploadFromUrlMayTakeTime, - }); - }); - }); -} - -function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise { - return new Promise((res, rej) => { - const keepOriginal = ref(prefer.s.keepOriginalUploading); - - os.popupMenu([label ? { - text: label, - type: 'label', - } : undefined, { - type: 'switch', - text: i18n.ts.keepOriginalUploading, - ref: keepOriginal, - }, { - text: i18n.ts.upload, - icon: 'ti ti-upload', - action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)), - }, { - text: i18n.ts.fromDrive, - icon: 'ti ti-cloud', - action: () => chooseFileFromDrive(multiple).then(files => res(files)), - }, { - text: i18n.ts.fromUrl, - icon: 'ti ti-link', - action: () => chooseFileFromUrl().then(file => res([file])), - }], src); - }); -} - -export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise { - return select(src, label, false).then(files => files[0]); -} - -export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise { - return select(src, label, true); -} diff --git a/packages/frontend/src/scripts/show-moved-dialog.ts b/packages/frontend/src/scripts/show-moved-dialog.ts deleted file mode 100644 index 35b3ef79d8..0000000000 --- a/packages/frontend/src/scripts/show-moved-dialog.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as os from '@/os.js'; -import { $i } from '@/account.js'; -import { i18n } from '@/i18n.js'; - -export function showMovedDialog() { - if (!$i) return; - if (!$i.movedTo) return; - - os.alert({ - type: 'error', - title: i18n.ts.accountMovedShort, - text: i18n.ts.operationForbidden, - }); - - throw new Error('account moved'); -} diff --git a/packages/frontend/src/scripts/show-suspended-dialog.ts b/packages/frontend/src/scripts/show-suspended-dialog.ts deleted file mode 100644 index 8b89dbb936..0000000000 --- a/packages/frontend/src/scripts/show-suspended-dialog.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as os from '@/os.js'; -import { i18n } from '@/i18n.js'; - -export function showSuspendedDialog() { - return os.alert({ - type: 'error', - title: i18n.ts.yourAccountSuspendedTitle, - text: i18n.ts.yourAccountSuspendedDescription, - }); -} diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts deleted file mode 100644 index 1f6ef1928c..0000000000 --- a/packages/frontend/src/scripts/shuffle.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/** - * 配列をシャッフル (破壊的) - */ -export function shuffle(array: T): T { - let currentIndex = array.length; - let randomIndex: number; - - // While there remain elements to shuffle. - while (currentIndex !== 0) { - // Pick a remaining element. - randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex--; - - // And swap it with the current element. - [array[currentIndex], array[randomIndex]] = [ - array[randomIndex], array[currentIndex]]; - } - - return array; -} diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts deleted file mode 100644 index d88bdb6660..0000000000 --- a/packages/frontend/src/scripts/snowfall-effect.ts +++ /dev/null @@ -1,490 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class SnowfallEffect { - private VERTEX_SOURCE = `#version 300 es - in vec4 a_position; - in vec4 a_color; - in vec3 a_rotation; - in vec3 a_speed; - in float a_size; - out vec4 v_color; - out float v_rotation; - uniform float u_time; - uniform mat4 u_projection; - 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 * u_spin_factor) * a_rotation.y; - - vec3 pos = a_position.xyz; - - 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 * 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; - } - `; - - private FRAGMENT_SOURCE = `#version 300 es - precision highp float; - - in vec4 v_color; - in float v_rotation; - uniform sampler2D u_texture; - out vec4 out_color; - - void main() { - vec2 rotated = vec2( - cos(v_rotation) * (gl_PointCoord.x - 0.5) + sin(v_rotation) * (gl_PointCoord.y - 0.5) + 0.5, - cos(v_rotation) * (gl_PointCoord.y - 0.5) - sin(v_rotation) * (gl_PointCoord.x - 0.5) + 0.5 - ); - - vec4 snowflake = texture(u_texture, rotated); - - out_color = vec4(snowflake.rgb * v_color.xyz, snowflake.a * v_color.a); - } - `; - - private gl: WebGLRenderingContext; - private program: WebGLProgram; - private canvas: HTMLCanvasElement; - private buffers: Record; - private uniforms: Record; - private texture: WebGLTexture; - private camera: { - fov: number; - near: number; - far: number; - aspect: number; - z: number; - }; - private wind: { - current: number; - force: number; - target: number; - min: number; - max: number; - easing: number; - }; - private time: { - start: number; - previous: number; - } = { - start: 0, - previous: 0, - }; - private raf = 0; - - private density: number = 1 / 90; - private depth = 100; - private count = 1000; - private gravity = 100; - private speed: number = 1 / 10000; - private color: number[] = [1, 1, 1]; - private opacity = 1; - private size = 4; - private snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAErRJREFUeAHdmgnYlmPax5MShaxRKRElPmXJXpaSsRxDU0bTZ+kt65RloiRDltEMQsxYKmS+zzYjxCCamCzV2LchResMIxFRQ1G93+93Pdf5dL9v7zuf4/hm0fc/jt9znddy3/e1nNd53c/7vHXq/AtVWVnZA/bzkaQjoWG298DeMdvrmP6/EIOqC4fBsbAx7Arz4TaYBPXgWVDnO2jSBrB2T0IMIA9mCmmoE8aonPkR6WPZHlp9xSlfeyeBzq9bHBD5feEdUGfDXBgBqnde+a2wvw/dYdNctvZNAp1PnTaFttA6JgP7eVgBM0CNzgO9HNvy0AcYDda6SaDTdXOnz8X+IkZDugAGQmOYA+ob6Ah/MIOMDRPhJjgJ6uV7pXtWt81/50SnY/Wvwn4ZDHAvwJ9ATYcxyaqsnEnqZCyCPaE80BgYZXG/5A3VyyP/b08LHa11z9KmFUwA5eqruRBHYX1s8WSI1Xcbme8Mt8PWUCU+kF8XbFN+dtH+p06OD4IU8EjD/VOZ5bnezq0XHcHuC2oV7BDlkVIWq56uIX8UjAO31GRIMYW0Vo/xXtSXJyTuXVO6xk1qalRTmQ9AfqzEvog2XYpllnsd6Qr4unCPT7NtByu0uU7vuAaOoy1JuvfXpJdTvSX0gI1gCXwGZdFmEFxoQb7Wid8s7lNu+I8wuHGsTqz2zpQ9DAa5R6HC55A2gvCMXthvwi25bjx26H0M9/9f4Rnok9s0zulFlC2HzzP9cnld8nH/p7DVrbmuIfYs6JLz9U3/z+KGadDeCDsmwre7GyEifn/su8HVSsL2HeBn8CK8AW+B7u9R5yrPgyOjvSn5DWAaXAG2UU7CE9Ayt4k4sR1lX4LaLdd9gn2ftsL+Vtuh1Dp/elH1C8lvCdUj8kDK3gbP8XdhCnSC86rcsNSR9pQvhc/gVlB9bUfqoFNAy/mLrUROrpMwCtpBxBbTtLqkF4K6IF9rf57I9pnYekx5AS0P1VhopXso9pR5buC7+kewU86nFcB+BT4EXdIvNO73sRBubGTXLZtTtgp+DEb++bACdqBuJOlAaMMzLVM3whegNznQDtCb+pW5b8YY76euB5+7pxm0IbzCfS8m3Zf2q4T8/+4JNArXGoptpxz8LqDmQJq0Qnostt/sfIn5GygD4/Zeq7B7wljQO2yjB/QGj0Pjxz4wGdqXrkjXtCT/ISyDa6EPpHrSraFjvnecFpMoMx40Br3xSlD262rYObevddHTs2kYwWUG9uP5It/f1eU5Xw9btwoXPALbwYXcg+unG/KB3Rq8n9ddAOpn4Kr8BAaBcltcDo9D7Ouavig1o34x7F94xqPk74eLQH0MH8HvwS3SLPe9iheEG6f70KiuLpZv6sxG/Va5bFJOabaO7ucAvGEbeAH+AN1hV7iDOidQFz4A2oJb6D1YDhXZHkTqpL8EbqHDYRtwW20AsdIb8syl5N2e6dTAPB2mWYa+hE4Qk7I59iMwFZ70GlJlfyuTVfygs7Hyw7HbwI0w3Tak14BqEtdg7wVdIx8pZbtBUbrjZeA3vUPBANkU+sEehev8O4Db6QpwYm+D8II0KPKHwUFeQ3oLDIMN4WgID1yOPQ+MAXMhNAtju3ztmtuAypiAw7EXwo/Am+0NfUG5mknYc6GfGVIjsoFNuyuoh8COuDcd2LmwA9jWE8bB3Q7N4XrwWAz5XOXR+Tx4n6FgdHeB6sF/w2QwhlSXdXvl/jixx4NH8GW5LDzb7GrR4ES4F5QddB99CieAwStOAPegdUZ2B71F3AXbQSn3vJ1bYaYWrayh3NUPTcbYFExVW3CfXwlvgfoavMbnDAY9dxGo6dCt0LeaB54H4UydDEPA2R4PDlrFLB9XuNmTlO+Xr7X9ZNBr9J4+EN8AMcv6ButpMND9FM6EnTOHkLrSnvtzwbbq3vwMB2ow/qWFSC8ZC++ZQaldbquH2afQWbl8TdcvVtC6LtipifAuOKt6gA9Tzqgzb5R2gP1hX3DVtZVHVvdklY5DA5beIkVPuZn8LOgAnWEfeAaUkxCan/voBNkfF+U5cFu5z5XlxZU20OmZtgm1K45VO4naNCukrcBZVk/CD+E/YBjoYjXJY8Zg9DxsDrbbBHTRotxOrug4eBs+hHgWZtKzfHrdXHBi9gDvqzxFHNA5KVfyBCf0ExgB7nkXStLLEKkniNf0AzUs5+ublkVFKiC9FBZAvGxshT0NnN3zoSUYSJQPcjAvm0HmjcIPemNS96F6E36drFLwugx7EEzNZV/l9IjoEPkW4B7eFtYH9QKcBcfA/aCWgpPQOT+zMbb9fS3nDbYR2MdgV0S5aVlUhLs0w45IHi7sqnnGJ2E7CXqHWgZXgJ1y8KqpDUmfSLmSV5yB/XrpDqVP8ofmehNdOv7I0ShfP4yyJdl2a4SchI1gCXgkHgljYfvc1i3cs/SU1A9jQRpfri/b0Sal1RrtSj4ULyHprY5C6+6E1+EBULq0E+DK7A96iwqX0z4td8B3dCdob5gD3UB3j9fUcNuDKFOvgc+bZAZFf4Zgu/q/AGPMgfm+5ShPWay+k6I31BwAvVDRYL2cuqfUVTkfnTqvVFx5ai7/MXn3tp1UrtRkDWRsaAMjzaD08uJ1irz7+8ps/6ZYj90V3FKrQBkvmubULbN7vs7tZRyJV9w0ePLbQ4PcJspqXnkbhbgoGk/AVptZRxpB0hU7Mpc1x34cdgKPm1dzeTts9XPwlFAO5Au4BDbO7ZycO7J9A/Zh2b4A2+ucALefWpTrflDKVq4kHQBOoi9PO1qvsDeGd6AxXAJbQ5VxlFrW8EnDcJlTsOPcjElxL7WNy7AduC4f2+A/rSN/Hyg7YMBTxgqPUT3F2HAqtIb58GvQW86GqyG+ff4UWz0FBuH4UhaTal1vmAGfg98dfP4d4HPGwmwYAg+D2/J7uU0ap/YaolHZVbBj5d1DaSK8ADsmqiH2JIhgNRhbPZrbhSdZ5heVJGw7477VfYuaagMK2sM8iMloga1HXAt/AeWELgQnR/0Z7k3W6pe3xTn/JamTFPGnPMZSj6p90rA8YOziwHcnH/EgTovJlJ0LPSHkyrTKmZNJ+8KrYKBsCQeB0pWdBFNleieMgzjL44jejTK1CPSY0CiMdyOT09g6ni5O3Ceg51U4VNLaPSA3SDNEwwiKFdgHgANNrpjb7UVejYTYCuZ92DR42HYh8gfDJfAMqBi4dqxk+RrKGkD0YXNsA6AT5qCUXhBe5CR0gPCC4dhqKFwI1m1qX0hr94CotDE4aAd3PCyBX4Jyn+sNL5tBDsRAp3S7b5KVYwa2A0nHaO5AXBeDtnlMxizsW+HomLh8zX9R5sTeBSEn/cqc2Tvak9eDXCyP2PgbYWzn2gefHxT7+0Qu/h18DO7XmPWYcYqSXuHz2myb6G7RNs7meLgeMxXugbiPA3clQx0xtgNPGN819L7+oCzvm6zSx+EkI+Du3Pe0LbOd/jqc7dhG9Wib+mJ5jaJBuL8e4B5aAMpAomKlb8d+KZWUVnw+dgzKSdDtvKaLDyJ1ReZB7O0J2EV5Xwd8OsTJExNpu7Q1SJ8zgy7K93UCX4P4mr4udoyhPGDKygOP+tomIFarMw2d+cfgF2DnDVAGoBvzw33YTHgPDoXQ7Fx/Wy6YkdMrcrmrehO4Pz3WvP90cIVPgonwITg4973yu0XTZK0+ZQaQd+K816twVAwKO71ZRj9zeg7lcVzXHghpVN4n2G3BAHQ1NILx4MBjoppgLwL3Ww8IHZsf6vGk3O8fwx9heK7rhD0o2zdg75JtT6GzQQ8KzcZwElSr3M5J85ktYCzEG+Gx2NNzm/Cm5pSp+K2gfLrZbg3RcB2IQcZN1qPM3+l06SjbAltX/TiXe1wtg7+AdR+AcgIs7xUPw94XxuTrnOD4E1bEoe9Rptw+DWGOGeQi7JOs1SfKKfk+epcakPNxbI8uFVdem8vT6aJdq7jASYjOFPdQDP4Q6t+Em8HVutmbkbYH9Tv4LcQW+H6ujy9Wrtxc6A7vQnznb5TbHUPZ0mw7CeoaOBAegmfBIKw8WZzs34M/oNiPGPzB2KHdrVMUlD29VFLLpw2jMWmnaIbdDNxXur+dWgVumTMglI4zMgbUEV5LmjqW7XnRkDS9qhbu/xZlZ8LWuc3UfM22Of80aVcYDJ/lstdIWxXu0TGXm/TO19vveHWuOglUxOo6iMfyBe7JOEp01ech9puuuBCMA8pVcUUNUB5lqgMYwJyE1oXOGTh9v1gO6kmogKEwHtREMHYofz5zAl3lJ2AWqJfgfohJiKB8HWWfg54YA9Zr1fn5Xmm80SdvHhNwVmq2umF8vWxA+WRwwE9BPNhOulrq0nxz97j6Go6DF8HYcBfYyer6MwWuoINeDG6roq4iE97QCtsJuxWc2JrkCeKEbgX7waOgnLiavxdQEWfohtgRwCrygIoxoQv1K0FNgR7gAKPTB+dr5lAWMliqmbAb7AzbgCs42vYK21NmOiwHJ9atpdxqDlhdA75QdYJT4XUYDfbBiVRe5ySoZTAbBpeekp6T4lo5uFnBz0fpJ6P8E9SJufEdXHipdRA/mw2hzmvfhrfgfjCKPwJnwn2g3igldb4hNaD5a6/fz7eHVuAb2wPwPs+4DB7E/hTagd64BbgoC6Ab9IAfgn+OX0p/ppAaGxZjnw6+Ep8DK8Cj0IDrmHw3GaeN9EZ/AlxFfk1RuVGUYu8K00D9Fa6EvrAUVKzO29gXg9vC1VW3g540w0xBcU2hKJnz+FxYvTCXWaduK/StuTZlLcD6JjnfEvsb6A56m32z78q4FMGw1gA4lEa60WmwMeiSnsljIBSDmEOBE3RdfvggbMuMIbNhItgJtbyUpE9ddjA0Bid1sderXDaQ1OdPAO9zH6hDcpuG2Ml7SQfArHRx6Xpf3JTluySrsrIP6Seg9/iMqsEvF6YZoXIDeAZCRmpneAHEnnLQnaEuXATX53schR3n/e7YyuvOT1bpnyV107Io3xZ6QWs4EirAyXkEqqvK3xa9CQ0c5C5xQ+zN8kWjcr2xZxTsBHfmsipbP671ZmW3wHYA58DdEPobhtwVF2HfBE9H3pT8xjkdja3iiDK4PQBO8Dx4B9wiH8JKeANcKTUW9IITwKNMeYrcArfDhVDsb1pVyty26le5D97/zWzrzVUGXyVjI0WjHUgq4CjoAuGiRuuJkN7mSJX7cn+uaZNyfBBgDHZqXvqsU2cZ6aPwChgE/ap8M9wLbSH+0DKOaw18z8N12GPAyf4BfADbwBmwCbxAHY9NvxQXx2GgVLZXPvurZDE0rqk5+NmAm8U2aIbdH9yDalgpSS80ltlB29fPqW9c8XLUHnsIuGquqt8gN7edwtazrOsAn4MysLryX8BD4Ap3y+0dZROIwPsl9h/hHjgit4lXdrdvHN8dc91wyk7JdvIS7VpF46Jb2ZGz4WJIRyBpBKQW3oR8lZuSvwQMhKtAfQUpYuf27cgbNx6EEeDAzgMHPwYMYi2gEcSfxC7B9qicDMoo/1vQI8p9IG88WAY/yeVpYrJdHpf5vytu4Ky7X46xIamrvjDb52OrG3K+HrZt4xq9wYEZPGPVfp7bhsdE2os2ylV6J1n5mbYPUX4S7AkGX+OAk2t6mm1Iw3PtQ+O4LuooK26RYvW3s7nBLZDiAGlbUHYiRV/S5AWk28DTEFqB4eo+B+n1M55Ivhu4kspj92uYCm6Px0Gv61lor0fcDQNBrQQnOr71lVeYsm894L/bkBuFe/u93eBngJtJMlwTDIDKyfDt6n3se8Dt8jHoNU0o70waq34obZ8lPx4coG+LbifrP6Pt0aQvwn65LFzcAHY8ZUtgAnwExp2WoMpeQLvaA12p7bf/pLPFmS3a/ajr750cfE43wX4YYmU9wi7IddHBCsrc69vm8uuwQydYVhQVvmsUn7s+ebfD0GhXrI+yf2jqA4oPKdo+iHxMwHbYRmgjta4cUTqCWXkg0UHatIR4SxxWKK9PeXhgKiZfxWOthzXuGff4p6b54bH3Y3W3pNxJcK8ebgdI44iys0G0N/8qKGOAGg9Ni50n3yjy2GkxSKtMRtT/21I7Fg/H9lRIX6qK5YX6zSjvDL4BGiBfBnUNmFdzwfKX4Ct40OtJv1sDj0Hlzrk6xbM3tob7uCf4amyk96VHvQg7gltGzQG9wpcwX6BCesfJ3/kJiMmgs+Gm4errUeZqF+Up4IoOzoWLcmqETyLve/2BsKkFpGUvK7VYCz6j06RbQx+ogHhN3Qdb3QF+a/wVKF94OhSHR77sWcXytcKm82usHGW9QE2B3skq/QB7APaqnJ9NuvaufnF1GIhxYH3LSAeA+hM0hMfgNzATdHvjgDHDv+qkP8gW77XW2gwmYsJe2F3zZDgxI7NteTo+/1WD/B9Au3Zjh2RyrgAAAABJRU5ErkJggg=='; - private mode = 'snow'; - - private INITIAL_BUFFERS = () => ({ - position: { size: 3, value: [] }, - color: { size: 4, value: [] }, - size: { size: 1, value: [] }, - rotation: { size: 3, value: [] }, - speed: { size: 3, value: [] }, - }); - - private INITIAL_UNIFORMS = () => ({ - time: { type: 'float', value: 0 }, - 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], - }, - }); - - private UNIFORM_SETTERS = { - int: 'uniform1i', - float: 'uniform1f', - vec2: 'uniform2fv', - vec3: 'uniform3fv', - vec4: 'uniform4fv', - mat2: 'uniformMatrix2fv', - mat3: 'uniformMatrix3fv', - mat4: 'uniformMatrix4fv', - }; - - private CAMERA = { - fov: 60, - near: 5, - far: 10000, - aspect: 1, - z: 100, - }; - - private WIND = { - current: 0, - force: 0.01, - target: 0.01, - min: 0, - max: 0.125, - easing: 0.0005, - }; - /** - * @throws {Error} - Thrown when it fails to get WebGL context for the canvas - */ - constructor(options: { - sakura?: boolean; - }) { - if (options.sakura) { - this.mode = 'sakura'; - this.snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDItMDFUMTQ6Mzk6NTYrMDkwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyNC0wMi0wMVQxNDozOTo1NiswOTAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI2NCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjY0IgogICBleGlmOkNvbG9yU3BhY2U9IjEiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iNjQiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjY0IgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIgogICB0aWZmOlhSZXNvbHV0aW9uPSI3Mi8xIgogICB0aWZmOllSZXNvbHV0aW9uPSI3Mi8xIj4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0icHJvZHVjZWQiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IFBob3RvIDIgMi4zLjEiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/PhldI30AAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWRu0sDQRCHP6Mh4oOIWlhYBPHRJBIjiDYWEV+gFjGCr+ZyuUuEJB53JyK2gq2gINr4KvQv0FawFgRFEcTaWtFG5ZwzgQQxs+zst7+dGXZnwRPPqFmrKgzZnG3GRqOB2bn5gO8FDxV46aJRUS1jcnokTln7uJdYsduQW6t83L9Wm9QsFSqqhQdVw7SFx4QnVm3D5R3hZjWtJIXPhIOmXFD4ztUTeX5xOZXnL5fNeGwIPA3CgVQJJ0pYTZtZYXk57dnMilq4j/uSOi03My1rm8xWLGKMEiXAOMMM0UcPA+L7CBGhW3aUyQ//5k+xLLmqeIM1TJZIkcYmKOqKVNdk1UXXZGRYc/v/t6+W3hvJV6+LgvfZcd46wLcN31uO83nkON/HUPkEl7li/vIh9L+LvlXU2g/AvwHnV0UtsQsXm9DyaCim8itVyvToOryeQv0cNN1AzUK+Z4VzTh4gvi5fdQ17+9Ap8f7FHyc6Z8kcDq1+AAAACXBIWXMAAAsTAAALEwEAmpwYAAADwElEQVR4nO2bT4hWVRjGf75TkhoEkhSa/9ocRIIwCsrE1pVnLbkYdFdGgQRS6caVm3CVy2oRuqmQ2yJXKTJh4GqCGs/CJCcLccAJ/yDpnGnxHYeZ4TrNfOc55y78nuWdc3/ve57v+b65f86BgQaqotiE5bEJKxYx7onYhOU1egKwGkViE/YCN4Cx2ITNC4xbDVwAJmMT9tXobVnpArEJe4CvZx0aB7aZdxPzxhkwArw66/Ae8+5Eyf6KJiA2YRPw+bzD64EjLcP3MXfyAMdjEzYWaG1GxRIQmzAEnAVeb/nzFPCSeTeaxj4FBOCZlrEjwBvm3VSJPksm4BPaJw8wBHwXm/BibMIW4HvaJ09ifFygP6BQAtKkfgEeEyHvAy+YdxdFvBmVSsBBdJMnsQ4KeTOSJyA2YT1wCXhcjL4HPG/e/amElkjAAfSTJzEPqKHSBKQLmSvAKiV3lm4BG8y7GyqgOgHvU27yAE+mGjLJEhCbsBL4A3haxXyIJoCN5t0dBUyZgF2UnzypxtsqmNKAt4SsarUkX4F0I3ONOgkAuA48a97FXJAqAa9Qb/IAa4CXFSCVATXjL635yBuQ/RsQm7AWuCroZamaBtaZd3/nQBQJeFPA6EfLFLUVBrwmYPSr7bkAhQHPCRj9al0uQGHAWgGjs9oKA7I/hS5rZ/0XSC86JDclGVph3t3t9+TcBHT56T9QVg+5BnT5/X+grB4GCcgs/sgnYCjzfIWyesg14Hrm+Qpl9ZBrwMT/DymurB4GCeiyuEidGnCN3n15V5pOPfStLAPMu1vAWA4jU7+Zd7dzAIqboREBo7PaCgN+EjA6qz1IQDbAu9/prQeorUvm3eVciOqx+JcizlL0hQKiMuAreiu/amkq1cyWxADz7ipwWsFapH4w7/5SgJRvh+cviCyp4yqQeonMOWCHktmic+bdThVMvUSmyFK2kjWkBph354FTSuY8nTLvflYCSyyT+xD4pwB3EvhADZUbYN5dAfarucB+825cDS25WvwksFuEO2nevSNizVHJ1eLvAoplrePAewJOq4oZYN5NAsPkPTCZBoYTq4iK7hgx734EjmUgjpl3Z1T9tKnGpqlP6e+p0Vg6t6iKG5De3A6ztJul+/Si3/db38WqyrY58+4CcHQJpxxN5xRXFQOSjgCjixg3SvuusiKqZoB59y+964KbCwy7Cew27+7V6apuAkibnhbaEbq3xMaohVTVAADz7hvgMHN/FKeAQ+bdt7X7Kb519mGKTdgKfEbvYucj8+7XLvr4DxAA134c0w/5AAAAAElFTkSuQmCC'; - 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'); - - document.body.append(canvas); - - this.canvas = canvas; - this.gl = gl; - this.program = this.initProgram(); - this.buffers = this.initBuffers(); - this.uniforms = this.initUniforms(); - this.texture = this.initTexture(); - this.camera = this.initCamera(); - this.wind = this.initWind(); - - this.resize = this.resize.bind(this); - this.update = this.update.bind(this); - - window.addEventListener('resize', () => this.resize()); - } - - private initCanvas(): HTMLCanvasElement { - const canvas = document.createElement('canvas'); - - Object.assign(canvas.style, { - position: 'fixed', - top: 0, - left: 0, - width: '100vw', - height: '100vh', - background: 'transparent', - 'pointer-events': 'none', - 'z-index': 2147483647, - }); - - return canvas; - } - - private initCamera() { - return { ...this.CAMERA }; - } - - private initWind() { - return { ...this.WIND }; - } - - private initShader(type, source): WebGLShader { - const { gl } = this; - const shader = gl.createShader(type); - if (shader == null) throw new Error('Failed to create shader'); - - gl.shaderSource(shader, source); - gl.compileShader(shader); - - return shader; - } - - private initProgram(): WebGLProgram { - const { gl } = this; - const vertex = this.initShader(gl.VERTEX_SHADER, this.VERTEX_SOURCE); - const fragment = this.initShader(gl.FRAGMENT_SHADER, this.FRAGMENT_SOURCE); - const program = gl.createProgram(); - if (program == null) throw new Error('Failed to create program'); - - gl.attachShader(program, vertex); - gl.attachShader(program, fragment); - gl.linkProgram(program); - gl.useProgram(program); - - return program; - } - - private initBuffers(): SnowfallEffect['buffers'] { - const { gl, program } = this; - const buffers = this.INITIAL_BUFFERS() as unknown as SnowfallEffect['buffers']; - - for (const [name, buffer] of Object.entries(buffers)) { - buffer.location = gl.getAttribLocation(program, `a_${name}`); - buffer.ref = gl.createBuffer()!; - - gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref); - gl.enableVertexAttribArray(buffer.location); - gl.vertexAttribPointer( - buffer.location, - buffer.size, - gl.FLOAT, - false, - 0, - 0, - ); - } - - return buffers; - } - - private updateBuffers() { - const { buffers } = this; - - for (const name of Object.keys(buffers)) { - this.setBuffer(name); - } - } - - private setBuffer(name: string, value?) { - const { gl, buffers } = this; - const buffer = buffers[name]; - - buffer.value = new Float32Array(value ?? buffer.value); - - gl.bindBuffer(gl.ARRAY_BUFFER, buffer.ref); - gl.bufferData(gl.ARRAY_BUFFER, buffer.value, gl.STATIC_DRAW); - } - - private initUniforms(): SnowfallEffect['uniforms'] { - const { gl, program } = this; - const uniforms = this.INITIAL_UNIFORMS() as unknown as SnowfallEffect['uniforms']; - - for (const [name, uniform] of Object.entries(uniforms)) { - uniform.location = gl.getUniformLocation(program, `u_${name}`)!; - } - - return uniforms; - } - - private updateUniforms() { - const { uniforms } = this; - - for (const name of Object.keys(uniforms)) { - this.setUniform(name); - } - } - - private setUniform(name: string, value?) { - const { gl, uniforms } = this; - const uniform = uniforms[name]; - const setter = this.UNIFORM_SETTERS[uniform.type]; - const isMatrix = /^mat[2-4]$/i.test(uniform.type); - - uniform.value = value ?? uniform.value; - - if (isMatrix) { - gl[setter](uniform.location, false, uniform.value); - } else { - gl[setter](uniform.location, uniform.value); - } - } - - private initTexture() { - const { gl } = this; - const texture = gl.createTexture(); - if (texture == null) throw new Error('Failed to create texture'); - const image = new Image(); - - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - 1, - 1, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - new Uint8Array([0, 0, 0, 0]), - ); - - image.onload = () => { - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA, - gl.RGBA, - gl.UNSIGNED_BYTE, - image, - ); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); - }; - - image.src = this.snowflake; - - return texture; - } - - private initSnowflakes(vw: number, vh: number, dpi: number) { - const position: number[] = []; - const color: number[] = []; - const size: number[] = []; - const rotation: number[] = []; - const speed: number[] = []; - - const height = 1 / this.density; - const width = (vw / vh) * height; - const depth = this.depth; - const count = this.count; - const length = (vw / vh) * count; - - for (let i = 0; i < length; ++i) { - position.push( - -width + Math.random() * width * 2, - -height + Math.random() * height * 2, - Math.random() * depth * 2, - ); - - speed.push(1 + Math.random(), 1 + Math.random(), Math.random() * 10); - - rotation.push( - Math.random() * 2 * Math.PI, - Math.random() * 20, - Math.random() * 10, - ); - - color.push(...this.color, 0.1 + Math.random() * this.opacity); - //size.push((this.size * Math.random() * this.size * vh * dpi) / 1000); - size.push((this.size * vh * dpi) / 1000); - } - - this.setUniform('worldSize', [width, height, depth]); - - this.setBuffer('position', position); - this.setBuffer('color', color); - this.setBuffer('rotation', rotation); - this.setBuffer('size', size); - this.setBuffer('speed', speed); - } - - private setProjection(aspect: number) { - const { camera } = this; - - camera.aspect = aspect; - - const fovRad = (camera.fov * Math.PI) / 180; - const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad); - const rangeInv = 1.0 / (camera.near - camera.far); - - const m0 = f / camera.aspect; - const m5 = f; - const m10 = (camera.near + camera.far) * rangeInv; - const m11 = -1; - const m14 = camera.near * camera.far * rangeInv * 2 + camera.z; - const m15 = camera.z; - - return [m0, 0, 0, 0, 0, m5, 0, 0, 0, 0, m10, m11, 0, 0, m14, m15]; - } - - public render() { - const { gl } = this; - - gl.enable(gl.BLEND); - gl.enable(gl.CULL_FACE); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE); - gl.disable(gl.DEPTH_TEST); - - this.updateBuffers(); - this.updateUniforms(); - this.resize(true); - - this.time = { - start: window.performance.now(), - previous: window.performance.now(), - }; - - if (this.raf) window.cancelAnimationFrame(this.raf); - this.raf = window.requestAnimationFrame(this.update); - - return this; - } - - private resize(updateSnowflakes = false) { - const { canvas, gl } = this; - const vw = canvas.offsetWidth; - const vh = canvas.offsetHeight; - const aspect = vw / vh; - const dpi = window.devicePixelRatio; - - canvas.width = vw * dpi; - canvas.height = vh * dpi; - - gl.viewport(0, 0, vw * dpi, vh * dpi); - gl.clearColor(0, 0, 0, 0); - - if (updateSnowflakes === true) { - this.initSnowflakes(vw, vh, dpi); - } - - this.setUniform('projection', this.setProjection(aspect)); - } - - private update(timestamp: number) { - const { gl, buffers, wind } = this; - const elapsed = (timestamp - this.time.start) * this.speed; - const delta = timestamp - this.time.previous; - - gl.clear(gl.COLOR_BUFFER_BIT); - gl.drawArrays( - gl.POINTS, - 0, - buffers.position.value.length / buffers.position.size, - ); - - if (Math.random() > 0.995) { - wind.target = - (wind.min + Math.random() * (wind.max - wind.min)) * - (Math.random() > 0.5 ? -1 : 1); - } - - wind.force += (wind.target - wind.force) * wind.easing; - wind.current += wind.force * (delta * 0.2); - - this.setUniform('wind', wind.current); - this.setUniform('time', elapsed); - - this.time.previous = timestamp; - - this.raf = window.requestAnimationFrame(this.update); - } -} diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts deleted file mode 100644 index 436c2b75f0..0000000000 --- a/packages/frontend/src/scripts/sound.ts +++ /dev/null @@ -1,258 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { SoundStore } from '@/preferences/def.js'; -import { prefer } from '@/preferences.js'; -import { PREF_DEF } from '@/preferences/def.js'; - -let ctx: AudioContext; -const cache = new Map(); -let canPlay = true; - -export const soundsTypes = [ - // 音声なし - null, - - // ドライブの音声 - '_driveFile_', - - // プリインストール - 'syuilo/n-aec', - 'syuilo/n-aec-4va', - 'syuilo/n-aec-4vb', - 'syuilo/n-aec-8va', - 'syuilo/n-aec-8vb', - 'syuilo/n-cea', - 'syuilo/n-cea-4va', - 'syuilo/n-cea-4vb', - 'syuilo/n-cea-8va', - 'syuilo/n-cea-8vb', - 'syuilo/n-eca', - 'syuilo/n-eca-4va', - 'syuilo/n-eca-4vb', - 'syuilo/n-eca-8va', - 'syuilo/n-eca-8vb', - 'syuilo/n-ea', - 'syuilo/n-ea-4va', - 'syuilo/n-ea-4vb', - 'syuilo/n-ea-8va', - 'syuilo/n-ea-8vb', - 'syuilo/n-ea-harmony', - 'syuilo/up', - 'syuilo/down', - 'syuilo/pope1', - 'syuilo/pope2', - 'syuilo/waon', - 'syuilo/popo', - 'syuilo/triple', - 'syuilo/bubble1', - 'syuilo/bubble2', - 'syuilo/poi1', - 'syuilo/poi2', - 'syuilo/pirori', - 'syuilo/pirori-wet', - 'syuilo/pirori-square-wet', - 'syuilo/square-pico', - 'syuilo/reverved', - 'syuilo/ryukyu', - 'syuilo/kick', - 'syuilo/snare', - 'syuilo/queue-jammed', - 'aisha/1', - 'aisha/2', - 'aisha/3', - 'noizenecio/kick_gaba1', - 'noizenecio/kick_gaba2', - 'noizenecio/kick_gaba3', - 'noizenecio/kick_gaba4', - 'noizenecio/kick_gaba5', - 'noizenecio/kick_gaba6', - 'noizenecio/kick_gaba7', -] as const; - -export const operationTypes = [ - 'noteMy', - 'note', - 'notification', - 'reaction', -] as const; - -/** サウンドの種類 */ -export type SoundType = typeof soundsTypes[number]; - -/** スプライトの種類 */ -export type OperationType = typeof operationTypes[number]; - -/** - * 音声を読み込む - * @param url url - * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする - */ -export async function loadAudio(url: string, options?: { useCache?: boolean; }) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (ctx == null) { - ctx = new AudioContext(); - - window.addEventListener('beforeunload', () => { - ctx.close(); - }); - } - if (options?.useCache ?? true) { - if (cache.has(url)) { - return cache.get(url) as AudioBuffer; - } - } - - let response: Response; - - try { - response = await fetch(url); - } catch (err) { - return; - } - - const arrayBuffer = await response.arrayBuffer(); - const audioBuffer = await ctx.decodeAudioData(arrayBuffer); - - if (options?.useCache ?? true) { - cache.set(url, audioBuffer); - } - - return audioBuffer; -} - -/** - * 既定のスプライトを再生する - * @param type スプライトの種類を指定 - */ -export function playMisskeySfx(operationType: OperationType) { - const sound = prefer.s[`sound.on.${operationType}`]; - playMisskeySfxFile(sound).then((succeed) => { - if (!succeed && sound.type === '_driveFile_') { - // ドライブファイルが存在しない場合はデフォルトのサウンドを再生する - const soundName = PREF_DEF[`sound_${operationType}`].default.type as Exclude; - if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`); - playMisskeySfxFileInternal({ - type: soundName, - volume: sound.volume, - }); - } - }); -} - -/** - * サウンド設定形式で指定された音声を再生する - * @param soundStore サウンド設定 - */ -export async function playMisskeySfxFile(soundStore: SoundStore): Promise { - // 連続して再生しない - if (!canPlay) return false; - // ユーザーアクティベーションが必要な場合はそれがない場合は再生しない - if ('userActivation' in navigator && !navigator.userActivation.hasBeenActive) return false; - // サウンドがない場合は再生しない - if (soundStore.type === null || soundStore.type === '_driveFile_' && !soundStore.fileUrl) return false; - - canPlay = false; - return await playMisskeySfxFileInternal(soundStore).finally(() => { - // ごく短時間に音が重複しないように - setTimeout(() => { - canPlay = true; - }, 25); - }); -} - -async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise { - if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { - return false; - } - const masterVolume = prefer.s['sound.masterVolume']; - if (isMute() || masterVolume === 0 || soundStore.volume === 0) { - return true; // ミュート時は成功として扱う - } - const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; - const buffer = await loadAudio(url).catch(() => { - return undefined; - }); - if (!buffer) return false; - const volume = soundStore.volume * masterVolume; - createSourceNode(buffer, { volume }).soundSource.start(); - return true; -} - -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 = opts.volume ?? 1; - - const soundSource = ctx.createBufferSource(); - soundSource.buffer = buffer; - soundSource.playbackRate.value = opts.playbackRate ?? 1; - soundSource - .connect(panNode) - .connect(gainNode) - .connect(ctx.destination); - - return { soundSource, panNode, gainNode }; -} - -/** - * 音声の長さをミリ秒で取得する - * @param file ファイルのURL(ドライブIDではない) - */ -export async function getSoundDuration(file: string): Promise { - const audioEl = document.createElement('audio'); - audioEl.src = file; - return new Promise((resolve) => { - const si = setInterval(() => { - if (audioEl.readyState > 0) { - resolve(audioEl.duration * 1000); - clearInterval(si); - audioEl.remove(); - } - }, 100); - }); -} - -/** - * ミュートすべきかどうかを判断する - */ -export function isMute(): boolean { - if (prefer.s['sound.notUseSound']) { - // サウンドを出力しない - return true; - } - - // noinspection RedundantIfStatementJS - if (prefer.s['sound.useSoundOnlyWhenActive'] && document.visibilityState === 'hidden') { - // ブラウザがアクティブな時のみサウンドを出力する - return true; - } - - return false; -} diff --git a/packages/frontend/src/scripts/sticky-sidebar.ts b/packages/frontend/src/scripts/sticky-sidebar.ts deleted file mode 100644 index 50f1e6ecc8..0000000000 --- a/packages/frontend/src/scripts/sticky-sidebar.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export class StickySidebar { - private lastScrollTop = 0; - private container: HTMLElement; - private el: HTMLElement; - private spacer: HTMLElement; - private marginTop: number; - private isTop = false; - private isBottom = false; - private offsetTop: number; - private globalHeaderHeight = 59; - - constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) { - this.container = container; - this.el = this.container.children[0] as HTMLElement; - this.el.style.position = 'sticky'; - this.spacer = document.createElement('div'); - this.container.prepend(this.spacer); - this.marginTop = marginTop; - this.offsetTop = this.container.getBoundingClientRect().top; - this.globalHeaderHeight = globalHeaderHeight; - } - - public calc(scrollTop: number) { - if (scrollTop > this.lastScrollTop) { // downscroll - const overflow = Math.max(0, this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight); - this.el.style.bottom = null; - this.el.style.top = `${-overflow + this.marginTop + this.globalHeaderHeight}px`; - - this.isBottom = (scrollTop + window.innerHeight) >= (this.el.offsetTop + this.el.clientHeight); - - if (this.isTop) { - this.isTop = false; - this.spacer.style.marginTop = `${Math.max(0, this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop)}px`; - } - } else { // upscroll - const overflow = this.globalHeaderHeight + (this.el.clientHeight + this.marginTop) - window.innerHeight; - this.el.style.top = null; - this.el.style.bottom = `${-overflow}px`; - - this.isTop = scrollTop + this.marginTop + this.globalHeaderHeight <= this.el.offsetTop; - - if (this.isBottom) { - this.isBottom = false; - this.spacer.style.marginTop = `${this.globalHeaderHeight + this.lastScrollTop + this.marginTop - this.offsetTop - overflow}px`; - } - } - - this.lastScrollTop = scrollTop <= 0 ? 0 : scrollTop; - } -} diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts deleted file mode 100644 index 9b1b368de4..0000000000 --- a/packages/frontend/src/scripts/stream-mock.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { EventEmitter } from 'eventemitter3'; -import * as Misskey from 'misskey-js'; -import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; - -type AnyOf> = T[keyof T]; -type OmitFirst = T extends [any, ...infer R] ? R : never; - -/** - * Websocket無効化時に使うStreamのモック(なにもしない) - */ -export class StreamMock extends EventEmitter implements IStream { - public readonly state = 'initializing'; - - constructor(...args: ConstructorParameters) { - super(); - // do nothing - } - - public useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock { - return new ChannelConnectionMock(this, channel, name); - } - - public removeSharedConnection(connection: any): void { - // do nothing - } - - public removeSharedConnectionPool(pool: any): void { - // do nothing - } - - public disconnectToChannel(): void { - // do nothing - } - - public send(typeOrPayload: string): void; - public send(typeOrPayload: string, payload: any): void; - public send(typeOrPayload: Record | any[]): void; - public send(typeOrPayload: string | Record | any[], payload?: any): void { - // do nothing - } - - public ping(): void { - // do nothing - } - - public heartbeat(): void { - // do nothing - } - - public close(): void { - // do nothing - } -} - -class ChannelConnectionMock = any> extends EventEmitter implements IChannelConnection { - public id = ''; - public name?: string; // for debug - public inCount = 0; // for debug - public outCount = 0; // for debug - public channel: string; - - constructor(stream: IStream, ...args: OmitFirst>>) { - super(); - - this.channel = args[0]; - this.name = args[1]; - } - - public send(type: T, body: Channel['receives'][T]): void { - // do nothing - } - - public dispose(): void { - // do nothing - } -} diff --git a/packages/frontend/src/scripts/test-utils.ts b/packages/frontend/src/scripts/test-utils.ts deleted file mode 100644 index 52bb2d94e0..0000000000 --- a/packages/frontend/src/scripts/test-utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export async function tick(): Promise { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never); -} diff --git a/packages/frontend/src/scripts/theme-editor.ts b/packages/frontend/src/scripts/theme-editor.ts deleted file mode 100644 index 0206e378bf..0000000000 --- a/packages/frontend/src/scripts/theme-editor.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { v4 as uuid } from 'uuid'; - -import { themeProps } from './theme.js'; -import type { Theme } from './theme.js'; - -export type Default = null; -export type Color = string; -export type FuncName = 'alpha' | 'darken' | 'lighten'; -export type Func = { type: 'func'; name: FuncName; arg: number; value: string; }; -export type RefProp = { type: 'refProp'; key: string; }; -export type RefConst = { type: 'refConst'; key: string; }; -export type Css = { type: 'css'; value: string; }; - -export type ThemeValue = Color | Func | RefProp | RefConst | Css | Default; - -export type ThemeViewModel = [ string, ThemeValue ][]; - -export const fromThemeString = (str?: string) : ThemeValue => { - if (!str) return null; - if (str.startsWith(':')) { - const parts = str.slice(1).split('<'); - const name = parts[0] as FuncName; - const arg = parseFloat(parts[1]); - const value = parts[2].startsWith('@') ? parts[2].slice(1) : ''; - return { type: 'func', name, arg, value }; - } else if (str.startsWith('@')) { - return { - type: 'refProp', - key: str.slice(1), - }; - } else if (str.startsWith('$')) { - return { - type: 'refConst', - key: str.slice(1), - }; - } else if (str.startsWith('"')) { - return { - type: 'css', - value: str.substring(1).trim(), - }; - } else { - return str; - } -}; - -export const toThemeString = (value: Color | Func | RefProp | RefConst | Css) => { - if (typeof value === 'string') return value; - switch (value.type) { - case 'func': return `:${value.name}<${value.arg}<@${value.value}`; - case 'refProp': return `@${value.key}`; - case 'refConst': return `$${value.key}`; - case 'css': return `" ${value.value}`; - } -}; - -export const convertToMisskeyTheme = (vm: ThemeViewModel, name: string, desc: string, author: string, base: 'dark' | 'light'): Theme => { - const props = { } as { [key: string]: string }; - for (const [key, value] of vm) { - if (value === null) continue; - props[key] = toThemeString(value); - } - - return { - id: uuid(), - name, desc, author, props, base, - }; -}; - -export const convertToViewModel = (theme: Theme): ThemeViewModel => { - const vm: ThemeViewModel = []; - // プロパティの登録 - vm.push(...themeProps.map(key => [key, fromThemeString(theme.props[key])] as [ string, ThemeValue ])); - - // 定数の登録 - const consts = Object - .keys(theme.props) - .filter(k => k.startsWith('$')) - .map(k => [k, fromThemeString(theme.props[k])] as [ string, ThemeValue ]); - - vm.push(...consts); - return vm; -}; diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts deleted file mode 100644 index 851ba41e61..0000000000 --- a/packages/frontend/src/scripts/theme.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ref } from 'vue'; -import tinycolor from 'tinycolor2'; -import lightTheme from '@@/themes/_light.json5'; -import darkTheme from '@@/themes/_dark.json5'; -import JSON5 from 'json5'; -import { deepClone } from './clone.js'; -import type { BundledTheme } from 'shiki/themes'; -import { globalEvents } from '@/events.js'; -import { miLocalStorage } from '@/local-storage.js'; -import { addTheme, getThemes } from '@/theme-store.js'; - -export type Theme = { - id: string; - name: string; - author: string; - desc?: string; - base?: 'dark' | 'light'; - props: Record; - codeHighlighter?: { - base: BundledTheme; - overrides?: Record; - } | { - base: '_none_'; - overrides: Record; - }; -}; - -export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); - -export const getBuiltinThemes = () => Promise.all( - [ - 'l-light', - 'l-coffee', - 'l-apricot', - 'l-rainy', - 'l-botanical', - 'l-vivid', - 'l-cherry', - 'l-sushi', - 'l-u0', - - 'd-dark', - 'd-persimmon', - 'd-astro', - 'd-future', - 'd-botanical', - 'd-green-lime', - 'd-green-orange', - 'd-cherry', - 'd-ice', - 'd-u0', - ].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)), -); - -export const getBuiltinThemesRef = () => { - const builtinThemes = ref([]); - getBuiltinThemes().then(themes => builtinThemes.value = themes); - return builtinThemes; -}; - -let timeout: number | null = null; - -export function applyTheme(theme: Theme, persist = true) { - if (timeout) window.clearTimeout(timeout); - - document.documentElement.classList.add('_themeChanging_'); - - timeout = window.setTimeout(() => { - document.documentElement.classList.remove('_themeChanging_'); - }, 1000); - - const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; - - document.documentElement.dataset.colorScheme = colorScheme; - - // Deep copy - const _theme = deepClone(theme); - - if (_theme.base) { - const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); - if (base) _theme.props = Object.assign({}, base.props, _theme.props); - } - - const props = compile(_theme); - - for (const tag of document.head.children) { - if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { - tag.setAttribute('content', props['htmlThemeColor']); - break; - } - } - - for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); - } - - document.documentElement.style.setProperty('color-scheme', colorScheme); - - if (persist) { - miLocalStorage.setItem('theme', JSON.stringify(props)); - miLocalStorage.setItem('themeId', theme.id); - miLocalStorage.setItem('colorScheme', colorScheme); - } - - // 色計算など再度行えるようにクライアント全体に通知 - globalEvents.emit('themeChanged'); -} - -function compile(theme: Theme): Record { - function getColor(val: string): tinycolor.Instance { - if (val[0] === '@') { // ref (prop) - return getColor(theme.props[val.substring(1)]); - } else if (val[0] === '$') { // ref (const) - return getColor(theme.props[val]); - } else if (val[0] === ':') { // func - const parts = val.split('<'); - const func = parts.shift().substring(1); - const arg = parseFloat(parts.shift()); - const color = getColor(parts.join('<')); - - switch (func) { - case 'darken': return color.darken(arg); - case 'lighten': return color.lighten(arg); - case 'alpha': return color.setAlpha(arg); - case 'hue': return color.spin(arg); - case 'saturate': return color.saturate(arg); - } - } - - // other case - return tinycolor(val); - } - - const props = {}; - - for (const [k, v] of Object.entries(theme.props)) { - if (k.startsWith('$')) continue; // ignore const - - props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); - } - - return props; -} - -function genValue(c: tinycolor.Instance): string { - return c.toRgbString(); -} - -export function validateTheme(theme: Record): boolean { - if (theme.id == null || typeof theme.id !== 'string') return false; - if (theme.name == null || typeof theme.name !== 'string') return false; - if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false; - if (theme.props == null || typeof theme.props !== 'object') return false; - return true; -} - -export function parseThemeCode(code: string): Theme { - let theme; - - try { - theme = JSON5.parse(code); - } catch (err) { - throw new Error('Failed to parse theme json'); - } - if (!validateTheme(theme)) { - throw new Error('This theme is invaild'); - } - if (getThemes().some(t => t.id === theme.id)) { - throw new Error('This theme is already installed'); - } - - return theme; -} - -export function previewTheme(code: string): void { - const theme = parseThemeCode(code); - if (theme) applyTheme(theme, false); -} - -export async function installTheme(code: string): Promise { - const theme = parseThemeCode(code); - if (!theme) return; - await addTheme(theme); -} diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/scripts/time.ts deleted file mode 100644 index 275b67ed00..0000000000 --- a/packages/frontend/src/scripts/time.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const dateTimeIntervals = { - 'day': 86400000, - 'hour': 3600000, - 'ms': 1, -}; - -export function dateUTC(time: number[]): Date { - const d = - time.length === 2 ? Date.UTC(time[0], time[1]) - : time.length === 3 ? Date.UTC(time[0], time[1], time[2]) - : time.length === 4 ? Date.UTC(time[0], time[1], time[2], time[3]) - : time.length === 5 ? Date.UTC(time[0], time[1], time[2], time[3], time[4]) - : time.length === 6 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5]) - : time.length === 7 ? Date.UTC(time[0], time[1], time[2], time[3], time[4], time[5], time[6]) - : null; - - if (!d) throw new Error('wrong number of arguments'); - - return new Date(d); -} - -export function isTimeSame(a: Date, b: Date): boolean { - return a.getTime() === b.getTime(); -} - -export function isTimeBefore(a: Date, b: Date): boolean { - return (a.getTime() - b.getTime()) < 0; -} - -export function isTimeAfter(a: Date, b: Date): boolean { - return (a.getTime() - b.getTime()) > 0; -} - -export function addTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { - return new Date(x.getTime() + (value * dateTimeIntervals[span])); -} - -export function subtractTime(x: Date, value: number, span: keyof typeof dateTimeIntervals = 'ms'): Date { - return new Date(x.getTime() - (value * dateTimeIntervals[span])); -} diff --git a/packages/frontend/src/scripts/timezones.ts b/packages/frontend/src/scripts/timezones.ts deleted file mode 100644 index c7582e06da..0000000000 --- a/packages/frontend/src/scripts/timezones.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const timezones = [{ - name: 'UTC', - abbrev: 'UTC', - offset: 0, -}, { - name: 'Europe/Berlin', - abbrev: 'CET', - offset: 60, -}, { - name: 'Asia/Tokyo', - abbrev: 'JST', - offset: 540, -}, { - name: 'Asia/Seoul', - abbrev: 'KST', - offset: 540, -}, { - name: 'Asia/Shanghai', - abbrev: 'CST', - offset: 480, -}, { - name: 'Australia/Sydney', - abbrev: 'AEST', - offset: 600, -}, { - name: 'Australia/Darwin', - abbrev: 'ACST', - offset: 570, -}, { - name: 'Australia/Perth', - abbrev: 'AWST', - offset: 480, -}, { - name: 'America/New_York', - abbrev: 'EST', - offset: -300, -}, { - name: 'America/Mexico_City', - abbrev: 'CST', - offset: -360, -}, { - name: 'America/Phoenix', - abbrev: 'MST', - offset: -420, -}, { - name: 'America/Los_Angeles', - abbrev: 'PST', - offset: -480, -}]; diff --git a/packages/frontend/src/scripts/touch.ts b/packages/frontend/src/scripts/touch.ts deleted file mode 100644 index 13c9d648dc..0000000000 --- a/packages/frontend/src/scripts/touch.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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; - -export let isTouchUsing = deviceKind === 'tablet' || deviceKind === 'smartphone'; - -if (isTouchSupported && !isTouchUsing) { - window.addEventListener('touchstart', () => { - // maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも - // タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする - 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 deleted file mode 100644 index a24941d02e..0000000000 --- a/packages/frontend/src/scripts/unison-reload.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -// SafariがBroadcastChannel未実装なのでライブラリを使う -import { BroadcastChannel } from 'broadcast-channel'; - -export const reloadChannel = new BroadcastChannel('reload'); - -// BroadcastChannelを用いて、クライアントが一斉にreloadするようにします。 -export function unisonReload(path?: string) { - if (path !== undefined) { - reloadChannel.postMessage(path); - location.href = path; - } else { - reloadChannel.postMessage(null); - location.reload(); - } -} diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts deleted file mode 100644 index d105a318a7..0000000000 --- a/packages/frontend/src/scripts/upload.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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 { v4 as uuid } from 'uuid'; -import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; -import { apiUrl } from '@@/js/config.js'; -import { getCompressionConfig } from './upload/compress-config.js'; -import { $i } from '@/account.js'; -import { alert } from '@/os.js'; -import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; -import { prefer } from '@/preferences.js'; - -type Uploading = { - id: string; - name: string; - progressMax: number | undefined; - progressValue: number | undefined; - img: string; -}; -export const uploads = ref([]); - -const mimeTypeMap = { - 'image/webp': 'webp', - 'image/jpeg': 'jpg', - 'image/png': 'png', -} as const; - -export function uploadFile( - file: File, - folder?: string | Misskey.entities.DriveFolder, - name?: string, - keepOriginal: boolean = prefer.s.keepOriginalUploading, -): Promise { - if ($i == null) throw new Error('Not logged in'); - - const _folder = typeof folder === 'string' ? folder : folder?.id; - - if (file.size > instance.maxFileSize) { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, - }); - return Promise.reject(); - } - - return new Promise((resolve, reject) => { - const id = uuid(); - - const reader = new FileReader(); - reader.onload = async (): Promise => { - const filename = name ?? file.name ?? 'untitled'; - const extension = filename.split('.').length > 1 ? '.' + filename.split('.').pop() : ''; - - const ctx = reactive({ - id, - name: prefer.s.keepOriginalFilename ? filename : id + extension, - progressMax: undefined, - progressValue: undefined, - img: window.URL.createObjectURL(file), - }); - - uploads.value.push(ctx); - - const config = !keepOriginal ? await getCompressionConfig(file) : undefined; - let resizedImage: Blob | undefined; - if (config) { - try { - const resized = await readAndCompressImage(file, config); - if (resized.size < file.size || file.type === 'image/webp') { - // The compression may not always reduce the file size - // (and WebP is not browser safe yet) - resizedImage = resized; - } - if (_DEV_) { - const saved = ((1 - resized.size / file.size) * 100).toFixed(2); - console.log(`Image compression: before ${file.size} bytes, after ${resized.size} bytes, saved ${saved}%`); - } - - ctx.name = file.type !== config.mimeType ? `${ctx.name}.${mimeTypeMap[config.mimeType]}` : ctx.name; - } catch (err) { - console.error('Failed to resize image', err); - } - } - - const formData = new FormData(); - formData.append('i', $i!.token); - formData.append('force', 'true'); - formData.append('file', resizedImage ?? file); - formData.append('name', ctx.name); - if (_folder) formData.append('folderId', _folder); - - const xhr = new XMLHttpRequest(); - xhr.open('POST', apiUrl + '/drive/files/create', true); - xhr.onload = ((ev: ProgressEvent) => { - if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { - // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい - uploads.value = uploads.value.filter(x => x.id !== id); - - if (xhr.status === 413) { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseExceedsFileSizeLimit, - }); - } else if (ev.target?.response) { - const res = JSON.parse(ev.target.response); - if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseInappropriate, - }); - } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: i18n.ts.cannotUploadBecauseNoFreeSpace, - }); - } else { - alert({ - type: 'error', - title: i18n.ts.failedToUpload, - text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, - }); - } - } else { - alert({ - type: 'error', - title: 'Failed to upload', - text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, - }); - } - - reject(); - return; - } - - const driveFile = JSON.parse(ev.target.response); - - resolve(driveFile); - - uploads.value = uploads.value.filter(x => x.id !== id); - }) as (ev: ProgressEvent) => any; - - xhr.upload.onprogress = ev => { - if (ev.lengthComputable) { - ctx.progressMax = ev.total; - ctx.progressValue = ev.loaded; - } - }; - - xhr.send(formData); - }; - reader.readAsArrayBuffer(file); - }); -} diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts deleted file mode 100644 index 3046b7f518..0000000000 --- a/packages/frontend/src/scripts/upload/compress-config.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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 { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer'; - -const compressTypeMap = { - 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' }, - 'image/png': { quality: 1, mimeType: 'image/webp' }, - 'image/webp': { quality: 0.90, mimeType: 'image/webp' }, - 'image/svg+xml': { quality: 1, mimeType: 'image/webp' }, -} as const; - -const compressTypeMapFallback = { - 'image/jpeg': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/png': { quality: 1, mimeType: 'image/png' }, - 'image/webp': { quality: 0.85, mimeType: 'image/jpeg' }, - 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, -} as const; - -export async function getCompressionConfig(file: File): Promise { - const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type]; - if (!imgConfig || await isAnimated(file)) { - return; - } - - return { - maxWidth: 2048, - maxHeight: 2048, - debug: true, - ...imgConfig, - }; -} diff --git a/packages/frontend/src/scripts/upload/isWebpSupported.ts b/packages/frontend/src/scripts/upload/isWebpSupported.ts deleted file mode 100644 index 2511236ecc..0000000000 --- a/packages/frontend/src/scripts/upload/isWebpSupported.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -let isWebpSupportedCache: boolean | undefined; -export function isWebpSupported() { - if (isWebpSupportedCache === undefined) { - const canvas = document.createElement('canvas'); - canvas.width = 1; - canvas.height = 1; - isWebpSupportedCache = canvas.toDataURL('image/webp').startsWith('data:image/webp'); - } - return isWebpSupportedCache; -} diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts deleted file mode 100644 index bba64fc6ee..0000000000 --- a/packages/frontend/src/scripts/use-chart-tooltip.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onUnmounted, onDeactivated, ref } from 'vue'; -import * as os from '@/os.js'; -import MkChartTooltip from '@/components/MkChartTooltip.vue'; - -export function useChartTooltip(opts: { position: 'top' | 'middle' } = { position: 'top' }) { - const tooltipShowing = ref(false); - const tooltipX = ref(0); - const tooltipY = ref(0); - const tooltipTitle = ref(null); - const tooltipSeries = ref<{ - backgroundColor: string; - borderColor: string; - text: string; - }[] | null>(null); - const { dispose: disposeTooltipComponent } = os.popup(MkChartTooltip, { - showing: tooltipShowing, - x: tooltipX, - y: tooltipY, - title: tooltipTitle, - series: tooltipSeries, - }, {}); - - onUnmounted(() => { - disposeTooltipComponent(); - }); - - onDeactivated(() => { - tooltipShowing.value = false; - }); - - function handler(context) { - if (context.tooltip.opacity === 0) { - tooltipShowing.value = false; - return; - } - - tooltipTitle.value = context.tooltip.title[0]; - tooltipSeries.value = context.tooltip.body.map((b, i) => ({ - backgroundColor: context.tooltip.labelColors[i].backgroundColor, - borderColor: context.tooltip.labelColors[i].borderColor, - text: b.lines[0], - })); - - const rect = context.chart.canvas.getBoundingClientRect(); - - tooltipShowing.value = true; - tooltipX.value = rect.left + window.scrollX + context.tooltip.caretX; - if (opts.position === 'top') { - tooltipY.value = rect.top + window.scrollY; - } else if (opts.position === 'middle') { - tooltipY.value = rect.top + window.scrollY + context.tooltip.caretY; - } - } - - return { - handler, - }; -} diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/scripts/use-form.ts deleted file mode 100644 index 26cca839c3..0000000000 --- a/packages/frontend/src/scripts/use-form.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { computed, reactive, watch } from 'vue'; -import type { Reactive } from 'vue'; - -function copy(v: T): T { - return JSON.parse(JSON.stringify(v)); -} - -function unwrapReactive(v: Reactive): T { - return JSON.parse(JSON.stringify(v)); -} - -export function useForm>(initialState: T, save: (newState: T) => Promise) { - const currentState = reactive(copy(initialState)); - const previousState = reactive(copy(initialState)); - - const modifiedStates = reactive>({} as any); - for (const key in currentState) { - modifiedStates[key] = false; - } - const modified = computed(() => Object.values(modifiedStates).some(v => v)); - const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length); - - watch([currentState, previousState], () => { - for (const key in modifiedStates) { - modifiedStates[key] = currentState[key] !== previousState[key]; - } - }, { deep: true }); - - async function _save() { - await save(unwrapReactive(currentState)); - for (const key in currentState) { - previousState[key] = copy(currentState[key]); - } - } - - function discard() { - for (const key in currentState) { - currentState[key] = copy(previousState[key]); - } - } - - return { - state: currentState, - savedState: previousState, - modifiedStates, - modified, - modifiedCount, - save: _save, - discard, - }; -} diff --git a/packages/frontend/src/scripts/use-leave-guard.ts b/packages/frontend/src/scripts/use-leave-guard.ts deleted file mode 100644 index 395c12a756..0000000000 --- a/packages/frontend/src/scripts/use-leave-guard.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { Ref } from 'vue'; - -export function useLeaveGuard(enabled: Ref) { - /* TODO - const setLeaveGuard = inject('setLeaveGuard'); - - if (setLeaveGuard) { - setLeaveGuard(async () => { - if (!enabled.value) return false; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return canceled; - }); - } else { - onBeforeRouteLeave(async (to, from) => { - if (!enabled.value) return true; - - const { canceled } = await os.confirm({ - type: 'warning', - text: i18n.ts.leaveConfirm, - }); - - return !canceled; - }); - } - */ - - /* - function onBeforeLeave(ev: BeforeUnloadEvent) { - if (enabled.value) { - ev.preventDefault(); - ev.returnValue = ''; - } - } - - window.addEventListener('beforeunload', onBeforeLeave); - onUnmounted(() => { - window.removeEventListener('beforeunload', onBeforeLeave); - }); - */ -} diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts deleted file mode 100644 index 0bc10e90e4..0000000000 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onUnmounted } from 'vue'; -import type { 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: ShallowRef; - note: Ref; - pureNote: Ref; - isDeletedRef: Ref; -}) { - const note = props.note; - const pureNote = props.pureNote; - const connection = $i ? useStream() : null; - - function onStreamNoteUpdated(noteData): void { - const { type, id, body } = noteData; - - if ((id !== note.value.id) && (id !== pureNote.value.id)) return; - - switch (type) { - case 'reacted': { - const reaction = body.reaction; - - if (body.emoji && !(body.emoji.name in note.value.reactionEmojis)) { - note.value.reactionEmojis[body.emoji.name] = body.emoji.url; - } - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (note.value.reactions || {})[reaction] || 0; - - note.value.reactions[reaction] = currentCount + 1; - note.value.reactionCount += 1; - - if ($i && (body.userId === $i.id)) { - note.value.myReaction = reaction; - } - break; - } - - case 'unreacted': { - const reaction = body.reaction; - - // TODO: reactionsプロパティがない場合ってあったっけ? なければ || {} は消せる - const currentCount = (note.value.reactions || {})[reaction] || 0; - - note.value.reactions[reaction] = Math.max(0, currentCount - 1); - note.value.reactionCount = Math.max(0, note.value.reactionCount - 1); - if (note.value.reactions[reaction] === 0) delete note.value.reactions[reaction]; - - if ($i && (body.userId === $i.id)) { - note.value.myReaction = null; - } - break; - } - - case 'pollVoted': { - const choice = body.choice; - - const choices = [...note.value.poll.choices]; - choices[choice] = { - ...choices[choice], - votes: choices[choice].votes + 1, - ...($i && (body.userId === $i.id) ? { - isVoted: true, - } : {}), - }; - - note.value.poll.choices = choices; - break; - } - - case 'deleted': { - props.isDeletedRef.value = true; - break; - } - } - } - - function capture(withHandler = false): void { - if (connection) { - // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - connection.send(document.body.contains(props.rootEl.value ?? 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); - } - } - - function decapture(withHandler = false): void { - if (connection) { - connection.send('un', { - id: note.value.id, - }); - if (pureNote.value.id !== note.value.id) { - connection.send('un', { - id: pureNote.value.id, - }); - } - if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated); - } - } - - function onStreamConnected() { - capture(false); - } - - capture(true); - if (connection) { - connection.on('_connected_', onStreamConnected); - } - - onUnmounted(() => { - decapture(true); - if (connection) { - connection.off('_connected_', onStreamConnected); - } - }); -} diff --git a/packages/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/scripts/use-tooltip.ts deleted file mode 100644 index d9ddfc8b5d..0000000000 --- a/packages/frontend/src/scripts/use-tooltip.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ref, watch, onUnmounted } from 'vue'; -import type { Ref } from 'vue'; - -export function useTooltip( - elRef: Ref, - onShow: (showing: Ref) => void, - delay = 300, -): void { - let isHovering = false; - - // iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ - // 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる - // TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...? - let shouldIgnoreMouseover = false; - - let timeoutId: number; - - let changeShowingState: (() => void) | null; - - let autoHidingTimer; - - const open = () => { - close(); - if (!isHovering) return; - if (elRef.value == null) return; - const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; - if (!document.body.contains(el)) return; // openしようとしたときに既に元要素がDOMから消えている場合があるため - - const showing = ref(true); - onShow(showing); - changeShowingState = () => { - showing.value = false; - }; - - autoHidingTimer = window.setInterval(() => { - if (elRef.value == null || !document.body.contains(elRef.value instanceof Element ? elRef.value : elRef.value.$el)) { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - close(); - window.clearInterval(autoHidingTimer); - } - }, 1000); - }; - - const close = () => { - if (changeShowingState != null) { - changeShowingState(); - changeShowingState = null; - } - }; - - const onMouseover = () => { - if (isHovering) return; - if (shouldIgnoreMouseover) return; - isHovering = true; - timeoutId = window.setTimeout(open, delay); - }; - - const onMouseleave = () => { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - window.clearInterval(autoHidingTimer); - close(); - }; - - const onTouchstart = () => { - shouldIgnoreMouseover = true; - if (isHovering) return; - isHovering = true; - timeoutId = window.setTimeout(open, delay); - }; - - const onTouchend = () => { - if (!isHovering) return; - isHovering = false; - window.clearTimeout(timeoutId); - window.clearInterval(autoHidingTimer); - close(); - }; - - const stop = watch(elRef, () => { - if (elRef.value) { - stop(); - const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el; - el.addEventListener('mouseover', onMouseover, { passive: true }); - el.addEventListener('mouseleave', onMouseleave, { passive: true }); - el.addEventListener('touchstart', onTouchstart, { passive: true }); - el.addEventListener('touchend', onTouchend, { passive: true }); - el.addEventListener('click', close, { passive: true }); - } - }, { - immediate: true, - flush: 'post', - }); - - onUnmounted(() => { - close(); - }); -} -- cgit v1.2.3-freya