├── .editorconfig ├── .env.dev ├── .env.production ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── dependabot.yml └── workflows │ ├── check-changelog.yaml │ ├── ci.md │ ├── ci.yaml │ └── crowdin.yaml ├── .gitignore ├── .old.env.dev ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG-nightly.md ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── index.html ├── locale └── en_US.yaml ├── manifest.config.ts ├── mozilla.template.json ├── package.json ├── public ├── assets │ ├── emoji │ │ ├── emoji.json │ │ ├── emojis0.svg │ │ ├── emojis1.svg │ │ ├── emojis10.svg │ │ ├── emojis2.svg │ │ ├── emojis3.svg │ │ ├── emojis4.svg │ │ ├── emojis5.svg │ │ ├── emojis6.svg │ │ ├── emojis7.svg │ │ ├── emojis8.svg │ │ └── emojis9.svg │ ├── picture │ │ ├── cgl_display_avif.svg │ │ ├── cgl_display_chat.webm │ │ ├── cgl_display_dynamic_cosmetics.avif │ │ ├── cgl_display_emote_carousel.avif │ │ ├── cgl_display_emote_menu.avif │ │ ├── cgl_display_highlights.avif │ │ ├── cgl_display_personal_emotes.avif │ │ └── cgl_display_settings_menu.avif │ └── sound │ │ └── ping.ogg ├── icon │ ├── icon-128.png │ ├── icon-16.png │ ├── icon-48.png │ ├── icon-512.png │ ├── nightly-icon-128.png │ ├── nightly-icon-16.png │ ├── nightly-icon-48.png │ └── nightly-icon-512.png └── logo.svg ├── script ├── compile-emojis.ts ├── generate-emojis.ts └── script.tsconfig.json ├── src ├── apollo │ └── apollo.ts ├── app │ ├── chat │ │ ├── Badge.vue │ │ ├── BadgeTooltip.vue │ │ ├── Chat.vue │ │ ├── ChatData.vue │ │ ├── Emote.vue │ │ ├── EmoteAliasButton.vue │ │ ├── EmoteLinkEmbed.vue │ │ ├── EmoteTooltip.vue │ │ ├── MessageTokenLink.vue │ │ ├── MessageTokenMention.vue │ │ ├── RichEmbed.vue │ │ ├── UserCard.vue │ │ ├── UserCardActions.vue │ │ ├── UserCardMessageList.vue │ │ ├── UserCardMod.vue │ │ ├── UserCardTabs.vue │ │ ├── UserMessage.vue │ │ ├── UserMessageButtons.vue │ │ ├── UserTag.vue │ │ └── msg │ │ │ ├── 0.NormalMessage.vue │ │ │ ├── 11.Connected.vue │ │ │ ├── 14.SubscriptionMessage.vue │ │ │ ├── 15.Resubscription.vue │ │ │ ├── 21.SubGift.vue │ │ │ ├── 26.Raid.vue │ │ │ ├── 35.SubMysteryGift.vue │ │ │ ├── 41.BitsBadgeTier.vue │ │ │ ├── 43.PointsReward.vue │ │ │ ├── 47.CommunityIntroduction.vue │ │ │ ├── 49.AnnouncementMessage.vue │ │ │ ├── 52.ViewerMilestone.vue │ │ │ ├── BasicSystemMessage.vue │ │ │ └── EmoteSetUpdateMessage.vue │ ├── emote-menu │ │ ├── EmoteMenu.vue │ │ ├── EmoteMenuContext.ts │ │ ├── EmoteMenuSet.vue │ │ └── EmoteMenuTab.vue │ ├── options │ │ ├── Options.vue │ │ ├── keys.ts │ │ ├── options.ts │ │ ├── router │ │ │ └── router.ts │ │ └── views │ │ │ ├── Compat │ │ │ └── Compat.vue │ │ │ ├── Onboarding │ │ │ ├── Onboarding.ts │ │ │ ├── Onboarding.vue │ │ │ ├── OnboardingChangelog.vue │ │ │ ├── OnboardingCompat.vue │ │ │ ├── OnboardingConfig.vue │ │ │ ├── OnboardingEnd.vue │ │ │ ├── OnboardingPlatforms.vue │ │ │ ├── OnboardingPromo.vue │ │ │ └── OnboardingStart.vue │ │ │ └── Popup │ │ │ ├── Popup.vue │ │ │ └── PopupInner.vue │ ├── paint-tool │ │ ├── PaintTool.vue │ │ ├── PaintToolGradient.vue │ │ ├── PaintToolGradientStop.vue │ │ ├── PaintToolList.vue │ │ ├── PaintToolMaker.vue │ │ ├── PaintToolPaintCard.vue │ │ └── PaintToolShadow.vue │ ├── settings │ │ ├── CategoryDropdown.vue │ │ ├── Settings.ts │ │ ├── SettingsConfigHighlights.vue │ │ ├── SettingsMenu.vue │ │ ├── SettingsNode.vue │ │ ├── SettingsUpdateButton.vue │ │ ├── SettingsViewBackup.vue │ │ ├── SettingsViewCompat.vue │ │ ├── SettingsViewConfig.vue │ │ ├── SettingsViewConfigCat.vue │ │ ├── SettingsViewHome.vue │ │ ├── SettingsViewProfile.vue │ │ └── control │ │ │ ├── FormCheckbox.vue │ │ │ ├── FormColor.vue │ │ │ ├── FormDropdown.vue │ │ │ ├── FormInput.vue │ │ │ ├── FormSelect.vue │ │ │ ├── FormSlider.vue │ │ │ └── FormToggle.vue │ └── store │ │ ├── Store.vue │ │ ├── StoreSubscribeButton.vue │ │ └── egvault.ts ├── assets │ ├── gql │ │ ├── seventv.user.gql.ts │ │ ├── tw.chat-bans.gql.ts │ │ ├── tw.chat-delete.gql.ts │ │ ├── tw.chat-pin.gql.ts │ │ ├── tw.chat-replies.gql.ts │ │ ├── tw.emote-card.gql.ts │ │ ├── tw.fragment.gql.ts │ │ ├── tw.gql.d.ts │ │ ├── tw.mod-user.gql.ts │ │ └── tw.user-card.gql.ts │ ├── style │ │ ├── flair.scss │ │ ├── global.scss │ │ ├── icon-direction.scss │ │ ├── shape.scss │ │ └── tw-tooltip.scss │ └── svg │ │ ├── emoji │ │ └── SingleEmoji.vue │ │ ├── icons │ │ ├── ArrowIcon.vue │ │ ├── BanIcon.vue │ │ ├── BellIcon.vue │ │ ├── BellSlashIcon.vue │ │ ├── BellsIcon.vue │ │ ├── CakeIcon.vue │ │ ├── CaretIcon.vue │ │ ├── ChatIcon.vue │ │ ├── ChevronIcon.vue │ │ ├── CloseIcon.vue │ │ ├── CloudIcon.vue │ │ ├── CompactDiscIcon.vue │ │ ├── CopyIcon.vue │ │ ├── DeleteIcon.vue │ │ ├── DownloadIcon.vue │ │ ├── DropdownIcon.vue │ │ ├── EmojiIcon.vue │ │ ├── EmotesIcon.vue │ │ ├── FileExportIcon.vue │ │ ├── ForwardIcon.vue │ │ ├── GaugeIcon.vue │ │ ├── GavelIcon.vue │ │ ├── GearsIcon.vue │ │ ├── HeartIcon.vue │ │ ├── HomeIcon.vue │ │ ├── IconFillDrip.vue │ │ ├── IconForSettings.vue │ │ ├── IconUpRightFromSquare.vue │ │ ├── LogoutIcon.vue │ │ ├── ModLogsIcon.vue │ │ ├── OpenLinkIcon.vue │ │ ├── PaintIcon.vue │ │ ├── PauseIcon.vue │ │ ├── PinIcon.vue │ │ ├── PlayerIcon.vue │ │ ├── PlusIcon.vue │ │ ├── PuzzlePieceIcon.vue │ │ ├── ReplyIcon.vue │ │ ├── SearchIcon.vue │ │ ├── ShieldIcon.vue │ │ ├── SiteLayoutIcon.vue │ │ ├── StarIcon.vue │ │ ├── SwordIcon.vue │ │ ├── TimerIcon.vue │ │ ├── TvIcon.vue │ │ ├── WarningIcon.vue │ │ ├── YouTubeIcon.vue │ │ └── ppL.vue │ │ ├── logos │ │ ├── Logo.vue │ │ ├── Logo7TV.vue │ │ ├── LogoAVIF.vue │ │ ├── LogoBTTV.vue │ │ ├── LogoBrandDiscord.vue │ │ ├── LogoBrandKick.vue │ │ ├── LogoBrandTwitch.vue │ │ ├── LogoBrandTwitter.vue │ │ ├── LogoBrandYouTube.vue │ │ ├── LogoFFZ.vue │ │ ├── LogoKick.vue │ │ └── LogoTwitch.vue │ │ ├── seventv │ │ ├── BgBadge3.vue │ │ └── VectorBadge.vue │ │ └── twitch │ │ ├── TwAnnounce.vue │ │ ├── TwAnnouncement.vue │ │ ├── TwBits.vue │ │ ├── TwChannelPoints.vue │ │ ├── TwChatModBan.vue │ │ ├── TwChatModDelete.vue │ │ ├── TwChatModTimeout.vue │ │ ├── TwChatModWarn.vue │ │ ├── TwChatReply.vue │ │ ├── TwClose.vue │ │ ├── TwFlame.vue │ │ ├── TwGift.vue │ │ ├── TwGreet.vue │ │ ├── TwMassGift.vue │ │ ├── TwPrime.vue │ │ ├── TwReply.vue │ │ ├── TwStar.vue │ │ └── TwSus.vue ├── background │ ├── background.ts │ ├── messaging.ts │ └── sync.ts ├── common │ ├── Async.ts │ ├── Color.ts │ ├── Constant.ts │ ├── Decode.ts │ ├── EventTarget.ts │ ├── Flags.ts │ ├── Image.ts │ ├── Input.ts │ ├── Jwt.ts │ ├── Logger.ts │ ├── Rand.ts │ ├── ReactHooks.ts │ ├── Reflection.ts │ ├── Roles.ts │ ├── Tokenize.ts │ ├── Transform.ts │ ├── chat │ │ ├── ChatMessage.ts │ │ └── Tokenizer.ts │ └── type-predicates │ │ ├── MessageTokens.ts │ │ └── Messages.ts ├── composable │ ├── channel │ │ └── useChannelContext.ts │ ├── chat │ │ ├── useChatEmotes.ts │ │ ├── useChatHighlights.ts │ │ ├── useChatMessages.ts │ │ ├── useChatModeration.ts │ │ ├── useChatProperties.ts │ │ ├── useChatScroller.ts │ │ └── useChatTools.ts │ ├── useActor.ts │ ├── useApollo.ts │ ├── useCookies.ts │ ├── useCosmetics.ts │ ├── useEmoji.ts │ ├── useExtensionPermission.ts │ ├── useFloatContext.ts │ ├── useFrankerFaceZ.ts │ ├── useLiveQuery.ts │ ├── useModule.ts │ ├── usePubSub.ts │ ├── useSetMutation.ts │ ├── useSettings.ts │ ├── useSound.ts │ ├── useTooltip.ts │ ├── useUpdater.ts │ ├── useUserAgent.ts │ └── useWorker.ts ├── content │ ├── content.ts │ └── emoji.ts ├── db │ ├── idb.ts │ └── versions.idb.ts ├── directive │ ├── ElementLifecycleDirective.ts │ ├── TextPaintDirective.ts │ └── TooltipDirective.ts ├── i18n │ └── index.ts ├── site │ ├── App.vue │ ├── global │ │ ├── Changelog.vue │ │ ├── FloatContext.vue │ │ ├── FloatScreen.vue │ │ ├── Global.vue │ │ ├── GlobalSettings.ts │ │ ├── ModuleWrapper.vue │ │ ├── Tooltip.vue │ │ └── components │ │ │ ├── EmoteCard.vue │ │ │ ├── FormCheckbox.vue │ │ │ └── FormInput.vue │ ├── kick.com │ │ ├── KickSite.vue │ │ ├── composable │ │ │ └── useUserdata.ts │ │ ├── index.ts │ │ └── modules │ │ │ ├── auth │ │ │ ├── Auth.ts │ │ │ ├── AuthButton.vue │ │ │ └── AuthModule_disabled.vue │ │ │ ├── chat-input │ │ │ ├── ChatInput.vue │ │ │ └── ChatInputModule.vue │ │ │ ├── chat │ │ │ ├── ChatController.vue │ │ │ ├── ChatMessage.vue │ │ │ ├── ChatModule.vue │ │ │ ├── ChatObserver.vue │ │ │ └── ChatUserCard.vue │ │ │ ├── emote-menu │ │ │ ├── EmoteMenu.vue │ │ │ └── EmoteMenuModule.vue │ │ │ └── settings │ │ │ ├── SettingsChatHook.vue │ │ │ └── SettingsModule.vue │ ├── site.app.ts │ ├── site.normal.ts │ ├── site.ts │ ├── twitch.tv │ │ ├── TwitchSite.vue │ │ ├── index.ts │ │ └── modules │ │ │ ├── autoclaim │ │ │ └── AutoclaimModule.vue │ │ │ ├── avatars │ │ │ └── AvatarsModule.vue │ │ │ ├── chat-input-controller │ │ │ └── ChatInputControllerModule.vue │ │ │ ├── chat-input │ │ │ ├── ChatInput.vue │ │ │ ├── ChatInputCarousel.vue │ │ │ ├── ChatInputModule.vue │ │ │ └── ChatSpam.vue │ │ │ ├── chat-vod │ │ │ ├── ChatVod.vue │ │ │ └── ChatVodModule.vue │ │ │ ├── chat │ │ │ ├── ChatController.vue │ │ │ ├── ChatList.vue │ │ │ ├── ChatMessageUnhandled.vue │ │ │ ├── ChatModule.vue │ │ │ ├── ChatPubSub.vue │ │ │ └── components │ │ │ │ ├── mod │ │ │ │ ├── ModIcons.vue │ │ │ │ ├── ModSlider.vue │ │ │ │ └── ModSliderBackend.ts │ │ │ │ └── tray │ │ │ │ ├── ChatTray.ts │ │ │ │ ├── ChatTray.vue │ │ │ │ ├── ReplyTray.vue │ │ │ │ └── Tray.vue │ │ │ ├── custom-commands │ │ │ ├── CommandModule.vue │ │ │ └── Commands │ │ │ │ ├── Dashboard.vue │ │ │ │ ├── Nuke.vue │ │ │ │ ├── Refresh.vue │ │ │ │ ├── Song.vue │ │ │ │ └── components │ │ │ │ └── EnableTray.vue │ │ │ ├── emote-menu │ │ │ ├── EmoteMenu.vue │ │ │ ├── EmoteMenuButton.vue │ │ │ └── EmoteMenuModule.vue │ │ │ ├── hidden-elements │ │ │ ├── HiddenElementsModule.vue │ │ │ └── hiddenElements.ts │ │ │ ├── mod-logs │ │ │ ├── ModLogs.vue │ │ │ ├── ModLogsAuthorityMessages.vue │ │ │ ├── ModLogsButton.vue │ │ │ ├── ModLogsModule.vue │ │ │ ├── ModLogsRecentActions.vue │ │ │ ├── ModLogsRecentActionsItem.vue │ │ │ └── ModLogsStore.ts │ │ │ ├── player │ │ │ ├── PlayerContentWarning.vue │ │ │ ├── PlayerController.vue │ │ │ ├── PlayerModule.vue │ │ │ ├── PlayerStatsTooltip.vue │ │ │ └── PlayerStreamInfo.vue │ │ │ ├── settings │ │ │ ├── SettingsMenuButton.vue │ │ │ └── SettingsModule.vue │ │ │ └── sidebar-previews │ │ │ ├── SidebarCard.vue │ │ │ └── SidebarPreviewsModule.vue │ └── youtube.com │ │ ├── YouTubeSite.vue │ │ └── modules │ │ └── chat │ │ ├── ChatAutocomplete.vue │ │ ├── ChatController.vue │ │ ├── ChatData.vue │ │ └── ChatModule.vue ├── store │ └── main.ts ├── types │ ├── app.d.ts │ ├── kick.d.ts │ ├── kick.module.d.ts │ ├── react-extended.d.ts │ ├── react.d.ts │ ├── tw.module.d.ts │ ├── twitch.d.ts │ ├── twitch.messages.d.ts │ ├── vite-env.d.ts │ ├── youtube.d.ts │ └── yt.module.d.ts ├── ui │ ├── UiButton.vue │ ├── UiConfirmPrompt.vue │ ├── UiCopiedMessageToast.vue │ ├── UiDraggable.vue │ ├── UiFloating.vue │ ├── UiLazy.ts │ ├── UiLazy.vue │ ├── UiLazyList.vue │ ├── UiScrollable.vue │ └── UiSuperHint.vue └── worker │ ├── event-handlers │ ├── cosmetic.handler.ts │ ├── emote-set.handler.ts │ ├── entitlement.handler.ts │ ├── handler.ts │ └── user.handler.ts │ ├── index.ts │ ├── worker.driver.ts │ ├── worker.events.ts │ ├── worker.http.ts │ ├── worker.port.ts │ └── worker.root.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.background.mts ├── vite.config.content.mts ├── vite.config.hosted.mts ├── vite.config.mts ├── vite.config.worker.mts ├── vite.utils.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{yaml,yml}] 13 | indent_style = space 14 | indent_size = 2 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VITE_APP_SITE="https://7tv.app" 3 | VITE_APP_API="https://7tv.io/v3" 4 | VITE_APP_API_GQL="https://7tv.io/v3/gql" 5 | VITE_APP_API_EVENTS="wss://events.7tv.io/v3" 6 | VITE_APP_API_EGVAULT="https://7tv.io/egvault/v1" 7 | VITE_APP_HOST="https://extension.7tv.gg" 8 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | VITE_APP_SITE="https://7tv.app" 3 | VITE_APP_API="https://7tv.io/v3" 4 | VITE_APP_API_GQL="https://7tv.io/v3/gql" 5 | VITE_APP_API_EVENTS="wss://events.7tv.io/v3" 6 | VITE_APP_API_EGVAULT="https://7tv.io/egvault/v1" 7 | VITE_APP_HOST="https://extension.7tv.gg" 8 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es2021: true, 7 | }, 8 | plugins: ["prettier"], 9 | extends: [ 10 | "plugin:vue/vue3-recommended", 11 | "eslint:recommended", 12 | "@vue/typescript/recommended", 13 | // Add under other rules 14 | "@vue/prettier", 15 | ], 16 | parserOptions: { 17 | ecmaVersion: 2021, 18 | }, 19 | ignorePatterns: ["locale/*.ts"], 20 | rules: { 21 | "prettier/prettier": "error", 22 | "no-console": "warn", 23 | "no-debugger": "error", 24 | "no-undef": "off", 25 | quotes: [1, "double"], 26 | "@typescript-eslint/no-unused-vars": "error", 27 | "@typescript-eslint/explicit-module-boundary-types": "off", 28 | "@typescript-eslint/no-namespace": "off", 29 | "vue/valid-template-root": "off", 30 | "vue/multi-word-component-names": "off", 31 | "vue/require-default-prop": "off", 32 | "vue/no-dupe-keys": "off", 33 | "@typescript-eslint/no-non-null-assertion": "off", 34 | }, 35 | globals: { 36 | defineEmits: "readonly", 37 | defineProps: "readonly", 38 | NodeJS: "readonly", 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: File a bug report 3 | title: "[BUG] " 4 | labels: bug 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Is there an existing issue for this? 9 | description: Please [search here](https://github.com/SevenTV/Extension/issues) to see if an issue already exists for your problem. 10 | options: 11 | - label: I have searched the existing issues 12 | required: true 13 | - type: checkboxes 14 | attributes: 15 | label: This issue exists in the latest nightly version 16 | description: Please make sure you have installed the latest nightly version and verified it is still an issue. 17 | options: 18 | - label: I am using the latest nightly version 19 | required: true 20 | - type: dropdown 21 | attributes: 22 | label: What browsers are you seeing the problem on? 23 | multiple: true 24 | options: 25 | - Chrome 26 | - Firefox 27 | - Opera 28 | - Microsoft Edge 29 | - Other 30 | validations: 31 | required: true 32 | - type: textarea 33 | attributes: 34 | label: Current Behavior 35 | description: A clear & concise description of what you're experiencing. 36 | validations: 37 | required: true 38 | - type: textarea 39 | attributes: 40 | label: Expected Behavior 41 | description: A clear & concise description of what you expected to happen. 42 | validations: 43 | required: true 44 | - type: textarea 45 | attributes: 46 | label: Steps To Reproduce 47 | description: Steps to reproduce the behavior. 48 | validations: 49 | required: true 50 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 📄 Feature Request 2 | description: File a feature request 3 | title: "[REQUEST] <title>" 4 | labels: enhancement 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Is there an existing issue for this? 9 | description: Please [search here](https://github.com/SevenTV/Extension/issues) to see if an issue already exists for your feature. 10 | options: 11 | - label: I have searched the existing issues 12 | required: true 13 | - type: checkboxes 14 | attributes: 15 | label: This feature does not exist in the latest nightly version 16 | description: Please make sure you have installed the latest nightly version and verified it is not a feature already. 17 | options: 18 | - label: I have checked the latest nightly version 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Feature Description 23 | description: A clear & concise description of what you're requesting. 24 | validations: 25 | required: true 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | open-pull-requests-limit: 0 5 | schedule: 6 | interval: daily 7 | time: "00:00" 8 | directory: "/" 9 | target-branch: dev 10 | - package-ecosystem: github-actions 11 | directory: "/" 12 | target-branch: dev 13 | schedule: 14 | interval: daily 15 | time: "00:00" 16 | open-pull-requests-limit: 0 17 | -------------------------------------------------------------------------------- /.github/workflows/check-changelog.yaml: -------------------------------------------------------------------------------- 1 | name: Changelog Check 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] 7 | 8 | env: 9 | COMMENT_BODY: "Please add a changelog entry for your changes, under `CHANGELOG-nightly.md`." 10 | COMMENT_BODY_SKIP: "This PR has been marked with the `skip changelog check` label, so no changelog entry is required." 11 | HAS_SKIP_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'skip changelog check') }} 12 | 13 | jobs: 14 | check-changelog: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Changelog check 19 | id: check 20 | uses: dangoslen/changelog-enforcer@v3 21 | with: 22 | changeLogPath: "CHANGELOG-nightly.md" 23 | skipLabels: "skip changelog check" 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.md: -------------------------------------------------------------------------------- 1 | # Jobs 2 | 3 | ## Build job (`ci`) 4 | 5 | - Builds the chrome extension with `yarn build:prod` and firefox extension with `MOZILLA_ID=moz-addon-prod@7tv.app MV2=true yarn build:prod` 6 | - Both builds get zipped 7 | - For chrome: Create CRX from zip with action `cardinalby/webext-buildtools-chrome-crx-action@v2` and private key `secrets.WEB_EXTENSION_CRX` 8 | - For Firefox: Create XPI from zip with action `kewisch/action-web-ext@v1` 9 | - CRX and XPI files uploaded as artifact `installable` 10 | - Chrome zip and Firefox zip uploaded as artifact `build` 11 | - Both manifest jsons uploaded as artifact `manifest` 12 | 13 | ## Release job (`release`) 14 | 15 | - Creates Github releases and tags 16 | 17 | ## Side loading deploy job (`deploy`) 18 | 19 | - Builds with `yarn build-hosted:prod` 20 | - Uploads to Cloudflare R2 with action `shallwefootball/s3-upload-action@master` 21 | - Endpoint: `secrets.R2_API_ENDPOINT` 22 | - Access Key: `secrets.R2_API_AK` 23 | - Secret Key: `secrets.R2_API_SECRET` 24 | - Bucket: `7tv-extension` 25 | 26 | ## Push job (`push`) 27 | 28 | - Upload zip file (not crx) to chrome web store (cws) with npm package `chrome-webstore-upload-cli` 29 | - Extension id: `ammjkodgmmoknidbanneddgankgfejfh` 30 | - Client id, client secret, refresh token in `secrets.CWS` 31 | - Sign XPI file with action `kewisch/action-web-ext@v1` 32 | - API key: `secrets.AMO_API_KEY` 33 | - API secret: `secrets.AMO_API_SECRET` 34 | - upload signed XPI as artifact `installable` 35 | - Update Github release 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | dist-hosted 14 | *.local 15 | 16 | # Editor directories and files 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | 25 | # Manifest.json is generated from manifest.config.json 26 | src/manifest.json 27 | msg_types.json 28 | 29 | crx/* 30 | script/*.json 31 | 32 | -------------------------------------------------------------------------------- /.old.env.dev: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | VITE_APP_SITE="http://localhost:4200" 3 | VITE_APP_API="http://localhost:3100/v3" 4 | VITE_APP_API_GQL="http://localhost:3000/v3/gql" 5 | VITE_APP_API_EVENTS="ws://localhost:3700/v3" 6 | VITE_APP_API_EGVAULT="http://localhost:3444/v1" 7 | VITE_APP_HOST="http://localhost:8080" 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /Typings 4 | /public/assets/emoji/emoji.json 5 | /.github/* 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 3 | "trailingComma": "all", 4 | "tabWidth": 4, 5 | "useTabs": true, 6 | "semi": true, 7 | "singleQuote": false, 8 | "printWidth": 120, 9 | "importOrder": [ 10 | "^vue", 11 | "^@vueuse/(.*)$", 12 | "^pinia", 13 | "^@/store/(.*)$", 14 | "^@/common/(.*)$", 15 | "^@/db/(.*)$", 16 | "^@/composable/(.*)$", 17 | "^@/site/(.*)$", 18 | "^@/worker/(.*)$", 19 | "^@/assets/(.*)$", 20 | "^./(.*)$", 21 | "<THIRD_PARTY_MODULES>" 22 | ], 23 | "importOrderGroupNamespaceSpecifiers": true, 24 | "importOrderSeparation": false, 25 | "importOrderSortSpecifiers": true 26 | } 27 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | plugins: ["stylelint-scss"] 2 | extends: 3 | - "stylelint-config-standard" 4 | rules: 5 | color-function-notation: "legacy" 6 | declaration-block-no-redundant-longhand-properties: 7 | - null 8 | ignoreFiles: ["locale/*.ts"] 9 | overrides: 10 | - files: ["**/*.scss"] 11 | extends: 12 | - "stylelint-config-standard-scss" 13 | - files: ["**/*.vue"] 14 | customSyntax: "postcss-html" 15 | extends: 16 | - "stylelint-config-standard-scss" 17 | - "stylelint-config-standard-vue/scss" 18 | - files: ["**/*.{vue,scss}"] 19 | rules: {} 20 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["vue.volar", "stylelint.vscode-stylelint", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.preferences.importModuleSpecifier": "non-relative", 3 | "javascript.preferences.quoteStyle": "double", 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "css.validate": false, 7 | "scss.validate": false, 8 | "stylelint.enable": true, 9 | "stylelint.configFile": ".stylelintrc", 10 | "stylelint.validate": ["css", "scss", "vue"], 11 | "stylelint.packageManager": "yarn", 12 | "stylelint.configBasedir": "${workspaceFolder}", 13 | "stylelint.config": { 14 | "rules": { 15 | "color-function-notation": "legacy", 16 | "declaration-block-no-redundant-longhand-properties": null 17 | }, 18 | "overrides": [ 19 | { 20 | "files": ["**/*.scss"], 21 | "extends": ["stylelint-config-standard-scss"] 22 | }, 23 | { 24 | "files": ["**/*.vue"], 25 | "extends": ["stylelint-config-standard-scss", "stylelint-config-standard-vue/scss"] 26 | }, 27 | { 28 | "files": ["**/*.{vue,scss}"], 29 | "rules": {} 30 | } 31 | ] 32 | }, 33 | "[typescript]": { 34 | "editor.defaultFormatter": "esbenp.prettier-vscode" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | prod: production 3 | production: 4 | yarn build:prod 5 | 6 | dev: 7 | yarn build:dev 8 | 9 | stage: 10 | yarn build:stage 11 | 12 | format: 13 | yarn format 14 | 15 | deps: 16 | yarn 17 | 18 | lint: 19 | yarn lint 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en" data-seventv-app> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 | <meta name="viewport" content="width=device-width,initial-scale=1.0" /> 7 | <link rel="preconnect" href="https://fonts.googleapis.com" /> 8 | <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 9 | <link 10 | href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,400&display=swap" 11 | rel="stylesheet" 12 | /> 13 | <link href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@700&display=swap" rel="stylesheet" /> 14 | <link 15 | href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@100;600;900&display=swap" 16 | rel="stylesheet" 17 | /> 18 | </head> 19 | <body data-seventv-app> 20 | <noscript> 21 | <strong 22 | >We're sorry but 7TV doesn't work properly without JavaScript enabled. Please enable it to 23 | continue.</strong 24 | > 25 | </noscript> 26 | <div id="app"></div> 27 | <script type="module" src="src/app/options/options.ts"></script> 28 | </body> 29 | </html> 30 | -------------------------------------------------------------------------------- /mozilla.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": { 3 | "moz-addon-prod@7tv.app": { 4 | "updates": [ 5 | { 6 | "version": "stable#version", 7 | "update_link": "stable#update_link" 8 | } 9 | ] 10 | }, 11 | "moz-addon@7tv.app": { 12 | "updates": [ 13 | { 14 | "version": "nightly#version", 15 | "update_link": "nightly#update_link" 16 | } 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /public/assets/picture/cgl_display_avif.svg: -------------------------------------------------------------------------------- 1 | <svg 2 | version="1.1" 3 | xmlns="http://www.w3.org/2000/svg" 4 | xmlns:xlink="http://www.w3.org/1999/xlink" 5 | viewBox="0 0 760 568" 6 | > 7 | <polygon fill="#FBAC30" points="470.5,567.31 0.47,284 470.5,0.69" /> 8 | <polygon fill="#12B17D" points="470.5,567.31 356.27,466.45 470.5,414.65" /> 9 | <polygon fill="#12B17D" points="356.27,101.55 470.5,0.69 470.5,153.35" /> 10 | <polygon fill="#BB255C" points="759.53,284 356.27,466.45 356.27,101.55" /> 11 | <path 12 | d="M189.48,294.52h-24.31l12.16-41.96L189.48,294.52z M246.02,344.09l-45.31-130.19h-46.78l-45.31,130.19h42.18l4.42-15.22 13 | h44.2l4.42,15.22H246.02z" 14 | /> 15 | <polygon 16 | points="309.28,344.09 354.59,213.91 312.41,213.91 285.89,301.54 259.37,213.91 217.19,213.91 262.5,344.09" 17 | /> 18 | <polygon 19 | fill="#FFFFFF" 20 | points="459.92,344.09 459.92,295.49 514.62,295.49 514.62,261.14 459.92,261.14 459.92,248.26 21 | 515.91,248.26 515.91,213.91 422.71,213.91 422.71,344.09" 22 | /> 23 | <rect x="368.33" y="213.91" fill="#FFFFFF" width="37.2" height="130.19" /> 24 | </svg> 25 | -------------------------------------------------------------------------------- /public/assets/picture/cgl_display_chat.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/assets/picture/cgl_display_chat.webm -------------------------------------------------------------------------------- /public/assets/picture/cgl_display_dynamic_cosmetics.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/assets/picture/cgl_display_dynamic_cosmetics.avif -------------------------------------------------------------------------------- /public/assets/picture/cgl_display_emote_carousel.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/assets/picture/cgl_display_emote_carousel.avif -------------------------------------------------------------------------------- /public/assets/picture/cgl_display_emote_menu.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/assets/picture/cgl_display_emote_menu.avif -------------------------------------------------------------------------------- /public/assets/picture/cgl_display_highlights.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/assets/picture/cgl_display_highlights.avif -------------------------------------------------------------------------------- /public/assets/picture/cgl_display_personal_emotes.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/assets/picture/cgl_display_personal_emotes.avif -------------------------------------------------------------------------------- /public/assets/picture/cgl_display_settings_menu.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/assets/picture/cgl_display_settings_menu.avif -------------------------------------------------------------------------------- /public/assets/sound/ping.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/assets/sound/ping.ogg -------------------------------------------------------------------------------- /public/icon/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/icon/icon-128.png -------------------------------------------------------------------------------- /public/icon/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/icon/icon-16.png -------------------------------------------------------------------------------- /public/icon/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/icon/icon-48.png -------------------------------------------------------------------------------- /public/icon/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/icon/icon-512.png -------------------------------------------------------------------------------- /public/icon/nightly-icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/icon/nightly-icon-128.png -------------------------------------------------------------------------------- /public/icon/nightly-icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/icon/nightly-icon-16.png -------------------------------------------------------------------------------- /public/icon/nightly-icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/icon/nightly-icon-48.png -------------------------------------------------------------------------------- /public/icon/nightly-icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SevenTV/Extension/4b2e208b96c3355132563ac7c3e1adb42a14bfc2/public/icon/nightly-icon-512.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 109.6 80.9" width="1em"> 2 | <g> 3 | <path d="M84.1,22.2l5-8.7,2.7-4.6L86.8.2V0H60.1l5,8.7,5,8.7,2.8,4.8H84.1" fill="currentColor" /> 4 | <path 5 | d="M29,80.6l5-8.7,5-8.7,5-8.7,5-8.7,5-8.7,5-8.7L62.7,22l-5-8.7-5-8.7L49.9.1H7.7l-5,8.7L0,13.4l5,8.7v.2h32l-5,8.7-5,8.7-5,8.7-5,8.7-5,8.7L8.5,72l5,8.7v.2H29" 6 | fill="currentColor" 7 | /> 8 | <path 9 | d="M70.8,80.6H86.1l5-8.7,5-8.7,5-8.7,5-8.7,3.5-6-5-8.7v-.2H89.2l-5,8.7-5,8.7-.7,1.3-5-8.7-5-8.7-.7-1.3-5,8.7-5,8.7L55,53.1l5,8.7,5,8.7,5,8.7.8,1.4" 10 | fill="currentColor" 11 | /> 12 | </g> 13 | </svg> 14 | -------------------------------------------------------------------------------- /script/script.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "resolveJsonModule": true, 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/apollo/apollo.ts: -------------------------------------------------------------------------------- 1 | import { decodeJWT } from "@/common/Jwt"; 2 | import { useConfig } from "@/composable/useSettings"; 3 | import { ApolloClient, ApolloLink, InMemoryCache, createHttpLink } from "@apollo/client/core"; 4 | 5 | export const httpLink = createHttpLink({ 6 | uri: import.meta.env.VITE_APP_API_GQL, 7 | }); 8 | 9 | const token = useConfig<string>("app.7tv.token"); 10 | const authLink = new ApolloLink((op, next) => { 11 | const jwt = decodeJWT(token.value); 12 | if (!jwt || jwt.exp * 1000 < Date.now() || !jwt.sub) { 13 | token.value = ""; 14 | return next(op); 15 | } 16 | op.setContext({ 17 | headers: { 18 | Authorization: `Bearer ${token.value}`, 19 | }, 20 | }); 21 | return next(op); 22 | }); 23 | 24 | const link = ApolloLink.from([authLink, httpLink]); 25 | 26 | const cache = new InMemoryCache(); 27 | 28 | export const apolloClient = new ApolloClient({ 29 | link, 30 | cache, 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/chat/BadgeTooltip.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div ref="tooltip" class="seventv-tooltip" tooltip-type="badge"> 3 | <p>{{ alt }}</p> 4 | </div> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | defineProps<{ 9 | alt: string; 10 | }>(); 11 | </script> 12 | 13 | <style scoped lang="scss"> 14 | .seventv-tooltip { 15 | background-color: rgba(0, 0, 0, 0.65%); 16 | 17 | @at-root .seventv-transparent & { 18 | backdrop-filter: blur(0.25em); 19 | } 20 | 21 | border-radius: 0.33em; 22 | padding: 0.25em; 23 | } 24 | </style> 25 | -------------------------------------------------------------------------------- /src/app/chat/Chat.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div /> 3 | </template> 4 | 5 | <script setup lang="ts"> 6 | void 0; 7 | </script> 8 | -------------------------------------------------------------------------------- /src/app/chat/EmoteAliasButton.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="alias-button" :shrink="!active"> 3 | <input 4 | ref="aliasRef" 5 | :value="alias" 6 | :active="active" 7 | :placeholder="active ? 'Alias' : '...'" 8 | @input="emit('update:alias', aliasRef!.value)" 9 | @focus="active = true" 10 | @blur="if (alias === '') active = false;" 11 | /> 12 | </div> 13 | </template> 14 | <script setup lang="ts"> 15 | import { ref } from "vue"; 16 | 17 | defineProps<{ 18 | alias: string; 19 | }>(); 20 | 21 | const emit = defineEmits<{ 22 | (event: "update:alias", value: string): void; 23 | }>(); 24 | const active = ref(false); 25 | const aliasRef = ref<HTMLInputElement>(); 26 | </script> 27 | 28 | <style scoped lang="scss"> 29 | [invalid="true"] { 30 | input { 31 | border-color: red !important; 32 | outline-color: red !important; 33 | } 34 | } 35 | 36 | .alias-button { 37 | height: 3rem; 38 | max-width: 6rem; 39 | display: flex; 40 | align-items: center; 41 | justify-content: end; 42 | flex-grow: 1; 43 | 44 | input { 45 | width: 3rem; 46 | height: 3rem; 47 | padding: 0.5rem; 48 | color: var(--color-text-base); 49 | box-sizing: border-box; 50 | transition: width 0.2s; 51 | border: 0.1rem solid transparent; 52 | border-radius: 0.5rem; 53 | background-color: var(--color-background-input); 54 | 55 | &[active="true"] { 56 | width: 100%; 57 | border-color: var(--color-border-base); 58 | } 59 | 60 | &[active="false"] { 61 | text-align: center; 62 | cursor: pointer; 63 | 64 | &:hover { 65 | background-color: var(--color-background-button-text-hover); 66 | } 67 | } 68 | } 69 | } 70 | </style> 71 | -------------------------------------------------------------------------------- /src/app/chat/MessageTokenLink.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <a :href="token.content.url" target="_blank" class="link-part"> 3 | {{ token.content.displayText }} 4 | </a> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import type { LinkToken } from "@/common/chat/ChatMessage"; 9 | 10 | defineProps<{ 11 | token: LinkToken; 12 | }>(); 13 | </script> 14 | -------------------------------------------------------------------------------- /src/app/chat/MessageTokenMention.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="mention-token"> 3 | <UserTag 4 | :user=" 5 | token.content.user ?? { 6 | id: uuid(), 7 | username: tag.toLowerCase(), 8 | displayName: tag, 9 | color: '', 10 | } 11 | " 12 | is-mention 13 | :hide-at="!hasAt" 14 | hide-badges 15 | /> 16 | </span> 17 | </template> 18 | 19 | <script setup lang="ts"> 20 | import type { ChatMessage, MentionToken } from "@/common/chat/ChatMessage"; 21 | import UserTag from "./UserTag.vue"; 22 | import { v4 as uuid } from "uuid"; 23 | 24 | const props = defineProps<{ 25 | token: MentionToken; 26 | msg?: ChatMessage; 27 | }>(); 28 | 29 | const hasAt = props.token.content.displayText.charAt(0) === "@"; 30 | const tag = hasAt ? props.token.content.displayText.slice(1) : props.token.content.displayText; 31 | </script> 32 | 33 | <style scoped lang="scss"> 34 | .mention-token { 35 | cursor: pointer; 36 | font-weight: bold; 37 | } 38 | </style> 39 | -------------------------------------------------------------------------------- /src/app/chat/UserCardActions.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="seventv-user-card-actions" /> 3 | </template> 4 | 5 | <script setup lang="ts"></script> 6 | 7 | <style scoped lang="scss"> 8 | .seventv-user-card-actions { 9 | grid-area: actions; 10 | display: grid; 11 | grid-auto-flow: row; 12 | grid-template-areas: "leftactions rightactions"; 13 | grid-template-columns: 1fr 1fr; 14 | grid-template-rows: 1fr; 15 | gap: 0 1rem; 16 | } 17 | 18 | .rightactions { 19 | grid-area: rightactions; 20 | } 21 | 22 | .leftactions { 23 | grid-area: leftactions; 24 | display: grid; 25 | grid-template-columns: 1fr 1fr 1fr; 26 | grid-template-rows: 1fr; 27 | grid-auto-flow: row; 28 | grid-template-areas: ". . ."; 29 | } 30 | </style> 31 | -------------------------------------------------------------------------------- /src/app/chat/msg/0.NormalMessage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="seventv-chat-message-container"> 3 | <div class="seventv-chat-message-background" tabindex="0"> 4 | <div v-if="msgData.reply" class="seventv-reply-part"> 5 | <div class="seventv-chat-reply-icon"> 6 | <TwChatReply /> 7 | </div> 8 | <div 9 | v-tooltip="`Replying to @${msgData.reply.parentDisplayName}: ${msgData.reply.parentMessageBody}`" 10 | class="seventv-reply-message-part" 11 | > 12 | {{ `Replying to @${msgData.reply.parentDisplayName}: ${msgData.reply.parentMessageBody}` }} 13 | </div> 14 | </div> 15 | <slot /> 16 | </div> 17 | </div> 18 | </template> 19 | 20 | <script setup lang="ts"> 21 | import type { ChatMessage } from "@/common/chat/ChatMessage"; 22 | import TwChatReply from "@/assets/svg/twitch/TwChatReply.vue"; 23 | 24 | defineProps<{ 25 | msg: ChatMessage; 26 | msgData: Twitch.ChatMessage; 27 | }>(); 28 | </script> 29 | <style scoped lang="scss"> 30 | .seventv-chat-message-container { 31 | display: block; 32 | position: relative; 33 | overflow-wrap: anywhere; 34 | 35 | .seventv-chat-message-background { 36 | position: relative; 37 | padding: 0.5rem var(--seventv-chat-padding, 1rem); 38 | 39 | .seventv-reply-part { 40 | display: flex; 41 | font-size: 1.2rem; 42 | color: var(--color-text-alt-2); 43 | overflow: clip; 44 | 45 | .seventv-chat-reply-icon { 46 | align-items: center; 47 | fill: currentcolor; 48 | display: inline-flex; 49 | } 50 | 51 | .seventv-reply-message-part { 52 | text-overflow: ellipsis; 53 | overflow: clip; 54 | white-space: nowrap; 55 | margin-left: 0.5rem; 56 | } 57 | } 58 | } 59 | 60 | &:hover, 61 | &:focus-within { 62 | .seventv-chat-message-background { 63 | border-radius: 0.25rem; 64 | background: hsla(0deg, 0%, 60%, 24%); 65 | } 66 | 67 | .seventv-buttons-container { 68 | visibility: visible; 69 | } 70 | 71 | :deep(.seventv-chat-message-buttons) { 72 | visibility: visible; 73 | } 74 | } 75 | } 76 | </style> 77 | -------------------------------------------------------------------------------- /src/app/chat/msg/11.Connected.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-welcome-message"> Welcome to the chat room! </span> 3 | </template> 4 | 5 | <script setup lang="ts"> 6 | defineProps<{ 7 | msgData: Twitch.AnyMessage; 8 | }>(); 9 | </script> 10 | 11 | <style scoped lang="scss"> 12 | span.seventv-welcome-message { 13 | padding: 0.5rem 2rem; 14 | color: var(--seventv-muted); 15 | } 16 | </style> 17 | -------------------------------------------------------------------------------- /src/app/chat/msg/14.SubscriptionMessage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-sub-message-container seventv-highlight"> 3 | <div class="sub-part"> 4 | <div class="sub-message-icon"> 5 | <TwPrime v-if="plan == 'Prime'" /> 6 | <TwStar v-else /> 7 | </div> 8 | <div class="sub-message-text"> 9 | <span v-if="msg.author" class="sub-name bold"> 10 | {{ msg.author.displayName }} 11 | </span> 12 | <span class="bold">Subscribed</span> 13 | with 14 | {{ plan }} 15 | <template v-if="msgData.sourceData"> to {{ msgData.sourceData.displayName }} </template> 16 | <span>.</span> 17 | </div> 18 | </div> 19 | 20 | <slot v-if="msg.body" /> 21 | </span> 22 | </template> 23 | 24 | <script setup lang="ts"> 25 | import type { ChatMessage } from "@/common/chat/ChatMessage"; 26 | import TwPrime from "@/assets/svg/twitch/TwPrime.vue"; 27 | import TwStar from "@/assets/svg/twitch/TwStar.vue"; 28 | 29 | const props = defineProps<{ 30 | msg: ChatMessage; 31 | msgData: Twitch.SubMessage; 32 | }>(); 33 | 34 | const plan = props.msgData.methods?.plan == "Prime" ? "Prime" : "Tier " + props.msgData.methods?.plan.charAt(0); 35 | </script> 36 | 37 | <style scoped lang="scss"> 38 | .seventv-sub-message-container { 39 | display: block; 40 | padding: 0.5rem 2rem; 41 | margin-top: 0.5rem; 42 | margin-bottom: 0.5rem; 43 | overflow-wrap: anywhere; 44 | background-color: hsla(0deg, 0%, 50%, 10%); 45 | } 46 | 47 | .seventv-highlight { 48 | border-left: 0.4rem solid var(--seventv-channel-accent); 49 | padding-left: 1.6rem !important; 50 | } 51 | 52 | .sub-part { 53 | display: flex; 54 | 55 | .sub-message-text { 56 | margin-left: 0.25rem; 57 | 58 | .bold { 59 | font-weight: 700; 60 | } 61 | 62 | .sub-name { 63 | display: block; 64 | color: var(--color-text-link); 65 | } 66 | } 67 | } 68 | 69 | .message-part { 70 | margin-top: 0.5rem; 71 | } 72 | </style> 73 | -------------------------------------------------------------------------------- /src/app/chat/msg/21.SubGift.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-sub-message-container seventv-highlight"> 3 | <div class="sub-part"> 4 | <div class="gift-icon"> 5 | <TwGift /> 6 | </div> 7 | <div class="sub-message-text"> 8 | <span class="sub-name bold"> 9 | {{ msgData.user.displayName }} 10 | </span> 11 | Gifted a 12 | <span class="bold"> Tier {{ msgData.methods?.plan.charAt(0) }} </span> 13 | Sub to 14 | <span class="bold"> {{ msgData.recipientDisplayName }} </span> 15 | <template v-if="msgData.sourceData"> in {{ msgData.sourceData.displayName }}'s channel! </template> 16 | </div> 17 | </div> 18 | </span> 19 | </template> 20 | 21 | <script setup lang="ts"> 22 | import TwGift from "@/assets/svg/twitch/TwGift.vue"; 23 | 24 | defineProps<{ 25 | msgData: Twitch.SubMessage; 26 | }>(); 27 | </script> 28 | 29 | <style scoped lang="scss"> 30 | .seventv-sub-message-container { 31 | display: block; 32 | padding: 0.5rem 2rem; 33 | margin-top: 0.5rem; 34 | margin-bottom: 0.5rem; 35 | overflow-wrap: anywhere; 36 | background-color: hsla(0deg, 0%, 50%, 10%); 37 | } 38 | 39 | .seventv-highlight { 40 | border-left: 0.4rem solid var(--seventv-channel-accent); 41 | padding-left: 1.6rem !important; 42 | } 43 | 44 | .sub-part { 45 | display: flex; 46 | 47 | .gift-icon { 48 | display: flex; 49 | flex-shrink: 0; 50 | padding-right: 1.6rem; 51 | margin: auto 0; 52 | } 53 | 54 | .sub-message-text { 55 | margin-left: 0.25rem; 56 | 57 | .bold { 58 | font-weight: 700; 59 | } 60 | 61 | .sub-name { 62 | display: block; 63 | color: var(--color-text-link); 64 | } 65 | } 66 | } 67 | </style> 68 | -------------------------------------------------------------------------------- /src/app/chat/msg/26.Raid.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-raid-message-container seventv-highlight"> 3 | <span class="bold">{{ msgData.params.displayName }}</span> 4 | raided 5 | <template v-if="msgData.sourceData"> {{ msgData.sourceData.displayName }}'s channel </template> 6 | with a viewer count of 7 | <span class="bold"> {{ msgData.params.viewerCount }}</span> 8 | <span>.</span> 9 | </span> 10 | </template> 11 | 12 | <script setup lang="ts"> 13 | defineProps<{ 14 | msgData: Twitch.RaidMessage; 15 | }>(); 16 | </script> 17 | 18 | <style scoped lang="scss"> 19 | .seventv-raid-message-container { 20 | display: block; 21 | padding: 0.5rem 2rem; 22 | margin-top: 0.5rem; 23 | margin-bottom: 0.5rem; 24 | overflow-wrap: anywhere; 25 | background-color: hsla(0deg, 0%, 50%, 5%); 26 | 27 | .bold { 28 | font-weight: 700; 29 | } 30 | } 31 | 32 | .seventv-highlight { 33 | border-left: 0.4rem solid var(--seventv-channel-accent); 34 | padding-left: 1.6rem !important; 35 | } 36 | </style> 37 | -------------------------------------------------------------------------------- /src/app/chat/msg/35.SubMysteryGift.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-sub-message-container seventv-highlight"> 3 | <div class="sub-part"> 4 | <div class="gift-icon"> 5 | <TwMassGift /> 6 | </div> 7 | <div class="sub-message-text"> 8 | <span class="sub-name-big"> 9 | {{ msgData.user.displayName }} 10 | </span> 11 | is gifting {{ msgData.massGiftCount }} Tier {{ msgData.plan.charAt(0) }} Sub{{ 12 | msgData.massGiftCount > 1 ? "s" : "" 13 | }} 14 | <template v-if="msgData.sourceData"> 15 | <span>to {{ msgData.sourceData.displayName }}'s community</span> 16 | </template> 17 | <span>.</span> 18 | <template v-if="msgData.senderCount == msgData.massGiftCount"> 19 | It's their first Gift Sub in the channel! 20 | </template> 21 | <template v-else> They've gifted a total of {{ msgData.senderCount }} Subs in the channel! </template> 22 | </div> 23 | </div> 24 | </span> 25 | </template> 26 | 27 | <script setup lang="ts"> 28 | import TwMassGift from "@/assets/svg/twitch/TwMassGift.vue"; 29 | 30 | defineProps<{ 31 | msgData: Twitch.MassGiftMessage; 32 | }>(); 33 | </script> 34 | 35 | <style scoped lang="scss"> 36 | .seventv-sub-message-container { 37 | display: block; 38 | padding: 0.5rem 2rem; 39 | margin-top: 0.5rem; 40 | margin-bottom: 0.5rem; 41 | overflow-wrap: anywhere; 42 | background-color: hsla(0deg, 0%, 50%, 10%); 43 | } 44 | 45 | .seventv-highlight { 46 | border-left: 0.4rem solid var(--seventv-channel-accent); 47 | padding-left: 1.6rem !important; 48 | } 49 | 50 | .sub-part { 51 | display: flex; 52 | 53 | .gift-icon { 54 | display: flex; 55 | flex-shrink: 0; 56 | padding-right: 1.6rem; 57 | margin: auto 0; 58 | } 59 | 60 | .sub-message-text { 61 | margin-left: 0.25rem; 62 | 63 | .bold { 64 | font-weight: 700; 65 | } 66 | 67 | .sub-name-big { 68 | display: block; 69 | font-size: larger; 70 | font-weight: 700; 71 | } 72 | } 73 | } 74 | </style> 75 | -------------------------------------------------------------------------------- /src/app/chat/msg/41.BitsBadgeTier.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-bits-badge-message-container seventv-highlight"> 3 | <div class="bits-badge-header"> 4 | <div class="bits-icon"> 5 | <TwBits /> 6 | </div> 7 | <div class="bits-text"> 8 | <span class="bits-username bold"> 9 | {{ msgData.user.displayName }} 10 | </span> 11 | just earned a new 12 | <span class="bold">{{ msgData.threshold }} Bits badge</span> 13 | <template v-if="msgData.sourceData"> 14 | <span> in {{ msgData.sourceData.displayName }}'s channel</span> 15 | </template> 16 | <span>!</span> 17 | </div> 18 | </div> 19 | <div v-if="msg.body" class="bits-badge-message"> 20 | <slot /> 21 | </div> 22 | </span> 23 | </template> 24 | 25 | <script setup lang="ts"> 26 | import { ChatMessage } from "@/common/chat/ChatMessage"; 27 | import TwBits from "@/assets/svg/twitch/TwBits.vue"; 28 | 29 | defineProps<{ 30 | msg: ChatMessage; 31 | msgData: Twitch.BitsBadgeTierMessage; 32 | }>(); 33 | </script> 34 | 35 | <style scoped lang="scss"> 36 | .seventv-bits-badge-message-container { 37 | display: block; 38 | border-image-slice: 1; 39 | margin-top: 0.5rem; 40 | margin-bottom: 0.5rem; 41 | overflow-wrap: anywhere; 42 | background-color: hsla(0deg, 0%, 50%, 5%); 43 | 44 | .bits-badge-header { 45 | padding: 0.5rem 0.5rem 0.5rem 1rem; 46 | display: flex; 47 | align-items: center; 48 | 49 | .bits-icon { 50 | display: inline-flex; 51 | padding: 0 0.5rem; 52 | color: var(--color-fill-alt-2); 53 | } 54 | 55 | .bits-text { 56 | .bold { 57 | font-weight: 700; 58 | } 59 | } 60 | } 61 | 62 | .bits-badge-message { 63 | padding: 0.5rem 1.2rem; 64 | } 65 | } 66 | 67 | .seventv-highlight { 68 | border-left: 0.4rem solid var(--seventv-channel-accent); 69 | } 70 | </style> 71 | -------------------------------------------------------------------------------- /src/app/chat/msg/47.CommunityIntroduction.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-introduction-message-container seventv-highlight"> 3 | <div class="introduction-header"> 4 | <div class="introduction-icon"> 5 | <TwGreet /> 6 | </div> 7 | New Introduction 8 | </div> 9 | <div class="introduction-message"> 10 | <slot /> 11 | </div> 12 | </span> 13 | </template> 14 | 15 | <script setup lang="ts"> 16 | import TwGreet from "@/assets/svg/twitch/TwGreet.vue"; 17 | 18 | defineProps<{ 19 | msgData: Twitch.CommunityIntroductionMessage; 20 | }>(); 21 | </script> 22 | 23 | <style scoped lang="scss"> 24 | .seventv-introduction-message-container { 25 | display: block; 26 | border-image-slice: 1; 27 | margin-top: 0.5rem; 28 | margin-bottom: 0.5rem; 29 | overflow-wrap: anywhere; 30 | background-color: hsla(0deg, 0%, 50%, 5%); 31 | 32 | .introduction-header { 33 | padding: 0.5rem 0.5rem 0.5rem 1rem; 34 | display: flex; 35 | 36 | .introduction-icon { 37 | display: inline-flex; 38 | padding: 0 0.5rem; 39 | } 40 | } 41 | 42 | .introduction-message { 43 | padding: 0.5rem 1.2rem; 44 | } 45 | } 46 | 47 | .seventv-highlight { 48 | border-left: 0.4rem solid #ff75e6; 49 | } 50 | </style> 51 | -------------------------------------------------------------------------------- /src/app/chat/msg/49.AnnouncementMessage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-announce-message-container" :class="className"> 3 | <div class="announce-header"> 4 | <div class="announce-icon"> 5 | <TwAnnounce /> 6 | </div> 7 | <div class="announce-title"> 8 | <template v-if="msgData.sourceData"> {{ msgData.sourceData.displayName }}'s </template> 9 | Announcement 10 | </div> 11 | </div> 12 | <div class="announce-message"> 13 | <slot /> 14 | </div> 15 | </span> 16 | </template> 17 | 18 | <script setup lang="ts"> 19 | import { useChannelContext } from "@/composable/channel/useChannelContext"; 20 | import { useChatProperties } from "@/composable/chat/useChatProperties"; 21 | import TwAnnounce from "@/assets/svg/twitch/TwAnnounce.vue"; 22 | 23 | const props = defineProps<{ 24 | msgData: Twitch.AnnouncementMessage; 25 | }>(); 26 | 27 | const ctx = useChannelContext(); 28 | const properties = useChatProperties(ctx); 29 | 30 | const className = 31 | "announcement-line--" + 32 | (props.msgData.color == "PRIMARY" && properties.primaryColorHex == null 33 | ? "purple" 34 | : props.msgData.color.toLowerCase()); 35 | </script> 36 | 37 | <style scoped lang="scss"> 38 | .seventv-announce-message-container { 39 | display: block; 40 | border-image-slice: 1; 41 | border-left: 0.8rem solid; 42 | border-right: 0.8rem solid; 43 | margin-top: 0.5rem; 44 | margin-bottom: 0.5rem; 45 | overflow-wrap: anywhere; 46 | background-color: hsla(0deg, 0%, 50%, 5%); 47 | 48 | .announce-header { 49 | background-color: hsla(0deg, 0%, 50%, 15%); 50 | padding: 0.5rem 0.5rem 0.5rem 1rem; 51 | display: flex; 52 | 53 | .announce-icon { 54 | display: inline-flex; 55 | padding: 0 0.5rem; 56 | } 57 | 58 | .announce-title { 59 | font-weight: 600; 60 | } 61 | } 62 | 63 | .announce-message { 64 | padding: 0.5rem 1.2rem; 65 | } 66 | } 67 | 68 | /* stylelint-disable-next-line selector-class-pattern */ 69 | .announcement-line--primary { 70 | border-color: var(--seventv-channel-accent, currentColor); 71 | } 72 | </style> 73 | -------------------------------------------------------------------------------- /src/app/chat/msg/BasicSystemMessage.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span class="seventv-basic-system-message"> {{ text }} </span> 3 | </template> 4 | 5 | <script setup lang="ts"> 6 | defineProps<{ 7 | text: string; 8 | }>(); 9 | </script> 10 | 11 | <style scoped lang="scss"> 12 | span.seventv-basic-system-message { 13 | display: block; 14 | padding: 0.5rem 2rem; 15 | color: var(--seventv-muted); 16 | } 17 | </style> 18 | -------------------------------------------------------------------------------- /src/app/emote-menu/EmoteMenuContext.ts: -------------------------------------------------------------------------------- 1 | import { inject, provide, reactive } from "vue"; 2 | 3 | export const EMOTE_MENU_CTX = Symbol("seventv-emote-menu-context"); 4 | 5 | interface EmoteMenuContext { 6 | open: boolean; 7 | filter: string; 8 | channelID: string; 9 | } 10 | 11 | export function useEmoteMenuContext(): EmoteMenuContext { 12 | let data = inject<EmoteMenuContext | null>(EMOTE_MENU_CTX, null); 13 | if (!data) { 14 | data = reactive<EmoteMenuContext>({ 15 | open: false, 16 | filter: "", 17 | channelID: "", 18 | }); 19 | 20 | provide(EMOTE_MENU_CTX, data); 21 | } 22 | 23 | return data; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/options/keys.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey } from "vue"; 2 | 3 | export const OPTIONS_CONTEXT_KEY = Symbol() as InjectionKey<boolean>; 4 | -------------------------------------------------------------------------------- /src/app/options/options.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createHead } from "@vueuse/head"; 3 | import { SITE_ASSETS_URL } from "@/common/Constant"; 4 | import Options from "@/app/options/Options.vue"; 5 | import { router } from "@/app/options/router/router"; 6 | import { TooltipDirective } from "@/directive/TooltipDirective"; 7 | import "@/i18n"; 8 | import { setupI18n } from "@/i18n"; 9 | 10 | const app = createApp(Options); 11 | const head = createHead({ 12 | titleTemplate(title) { 13 | return title ? `${title} — 7TV` : "7TV"; 14 | }, 15 | }); 16 | 17 | app.directive("tooltip", TooltipDirective); 18 | app.provide(SITE_ASSETS_URL, chrome.runtime.getURL("/assets")); 19 | app.use(router).use(head).use(setupI18n()).mount("#app"); 20 | -------------------------------------------------------------------------------- /src/app/options/router/router.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw, createRouter, createWebHashHistory } from "vue-router"; 2 | 3 | const routes = [ 4 | { 5 | path: "", 6 | redirect: "/onboarding/start", 7 | children: [ 8 | { 9 | path: "onboarding/:step?", 10 | name: "Onboarding", 11 | component: () => import("../views/Onboarding/Onboarding.vue"), 12 | }, 13 | ], 14 | }, 15 | { 16 | path: "/popup", 17 | name: "Popup", 18 | component: () => import("../views/Popup/Popup.vue"), 19 | }, 20 | { 21 | path: "/compat", 22 | name: "Compat", 23 | component: () => import("../views/Compat/Compat.vue"), 24 | }, 25 | ] as RouteRecordRaw[]; 26 | 27 | export const router = createRouter({ 28 | history: createWebHashHistory(), 29 | strict: true, 30 | routes, 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/options/views/Onboarding/Onboarding.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey, Ref, inject, provide, reactive } from "vue"; 2 | import { tryOnUnmounted } from "@vueuse/core"; 3 | 4 | const ONBOARDING_KEY = Symbol() as InjectionKey<OnboardingData>; 5 | export const ONBOARDING_UPGRADED = Symbol() as InjectionKey<Ref<boolean>>; 6 | 7 | interface OnboardingData { 8 | activeStep: OnboardingStep | null; 9 | steps: Map<string, OnboardingStep>; 10 | sortedSteps: OnboardingStep[]; 11 | onMove?: () => void; 12 | } 13 | 14 | export interface OnboardingStep { 15 | name: string; 16 | component: ComponentFactory; 17 | order: number; 18 | locked: boolean; 19 | completed: boolean; 20 | active: boolean; 21 | color?: string; 22 | } 23 | 24 | export type OnboardingStepRoute = Pick<OnboardingStep, "name" | "order" | "color">; 25 | 26 | export function createOnboarding(): OnboardingData { 27 | let o = inject(ONBOARDING_KEY, null); 28 | if (!o) { 29 | o = reactive<OnboardingData>({ 30 | activeStep: null, 31 | steps: new Map(), 32 | sortedSteps: [], 33 | }); 34 | 35 | provide(ONBOARDING_KEY, o); 36 | } 37 | 38 | return o; 39 | } 40 | 41 | export function useOnboarding(stepName: string) { 42 | const ctx = inject(ONBOARDING_KEY); 43 | if (!ctx) { 44 | throw new Error("Onboarding not in context"); 45 | } 46 | 47 | const step = ctx.steps.get(stepName); 48 | if (!step) { 49 | throw new Error("Unknown Step"); 50 | } 51 | 52 | step.active = true; 53 | 54 | function setCompleted(value: boolean): void { 55 | if (step) { 56 | step.completed = value; 57 | } 58 | } 59 | 60 | function setLock(value: boolean, onMove?: () => void): void { 61 | if (!ctx || !step) return; 62 | 63 | step.locked = value; 64 | ctx.onMove = onMove; 65 | if (onMove) { 66 | tryOnUnmounted(() => { 67 | ctx.onMove = undefined; 68 | }); 69 | } 70 | } 71 | 72 | return { 73 | setCompleted, 74 | setLock, 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/app/options/views/Onboarding/OnboardingChangelog.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main class="onboarding-changelog"> 3 | <Changelog :no-header="true" /> 4 | </main> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import Changelog from "@/site/global/Changelog.vue"; 9 | 10 | const { setCompleted } = useOnboarding("changelog"); 11 | 12 | onDeactivated(() => { 13 | setCompleted(true); 14 | }); 15 | </script> 16 | 17 | <script lang="ts"> 18 | import { onDeactivated } from "vue"; 19 | import { OnboardingStepRoute, useOnboarding } from "./Onboarding"; 20 | 21 | export const step: OnboardingStepRoute = { 22 | name: "changelog", 23 | order: 0.5, 24 | }; 25 | </script> 26 | 27 | <style scoped lang="scss"> 28 | main { 29 | display: grid; 30 | justify-content: center; 31 | max-width: 44rem; 32 | margin: 0 20%; 33 | 34 | @media screen and (width <= 1000px) { 35 | margin: 0 1rem; 36 | } 37 | } 38 | </style> 39 | -------------------------------------------------------------------------------- /src/app/options/views/Onboarding/OnboardingCompat.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main class="onboarding-compat"> 3 | <div class="header"> 4 | <h1 v-t="'onboarding.compat_title'" /> 5 | <p v-t="'onboarding.compat_subtitle'" /> 6 | </div> 7 | 8 | <div class="compat"> 9 | <Compat :internal="true" @skip="emit('completed')" /> 10 | </div> 11 | </main> 12 | </template> 13 | 14 | <script setup lang="ts"> 15 | import { onDeactivated } from "vue"; 16 | import Compat from "../Compat/Compat.vue"; 17 | import { OnboardingStepRoute, useOnboarding } from "./Onboarding"; 18 | 19 | const emit = defineEmits<{ 20 | (e: "completed"): void; 21 | }>(); 22 | 23 | const ctx = useOnboarding("compatibility"); 24 | 25 | onDeactivated(() => { 26 | ctx.setCompleted(true); 27 | }); 28 | </script> 29 | 30 | <script lang="ts"> 31 | export const step: OnboardingStepRoute = { 32 | name: "compatibility", 33 | order: 4, 34 | }; 35 | </script> 36 | 37 | <style scoped lang="scss"> 38 | main.onboarding-compat { 39 | width: 100%; 40 | display: grid; 41 | grid-template-rows: 20% 80%; 42 | grid-template-areas: 43 | "header" 44 | "compat"; 45 | 46 | .header { 47 | grid-area: header; 48 | justify-self: center; 49 | align-self: center; 50 | text-align: center; 51 | max-width: 40vw; 52 | 53 | h1 { 54 | font-size: 4vw; 55 | } 56 | 57 | p { 58 | font-size: 1vw; 59 | } 60 | } 61 | 62 | .compat { 63 | grid-area: compat; 64 | margin: 0 10%; 65 | } 66 | } 67 | </style> 68 | -------------------------------------------------------------------------------- /src/app/options/views/Popup/Popup.vue: -------------------------------------------------------------------------------- 1 | import PopupInner from './PopupInner.vue'; 2 | 3 | <template> 4 | <main class="seventv-popup"> 5 | <div> 6 | <PopupInner /> 7 | </div> 8 | </main> 9 | </template> 10 | 11 | <script setup lang="ts"> 12 | import PopupInner from "./PopupInner.vue"; 13 | </script> 14 | 15 | <style scoped lang="scss"> 16 | main { 17 | display: block; 18 | background-color: var(--seventv-background-shade-1); 19 | 20 | > div { 21 | height: 600px; 22 | width: 800px; 23 | } 24 | } 25 | </style> 26 | -------------------------------------------------------------------------------- /src/app/paint-tool/PaintToolPaintCard.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="paint-tool-paint-card"> 3 | <div for="name"> 4 | <span v-cosmetic-paint="paint.id">{{ paint.data.name }}</span> 5 | </div> 6 | <div for="colorball"> 7 | <div class="seventv-paint" :data-seventv-paint-id="paint.id" /> 8 | </div> 9 | </div> 10 | </template> 11 | 12 | <script setup lang="ts"> 13 | defineProps<{ 14 | paint: SevenTV.Cosmetic<"PAINT">; 15 | }>(); 16 | </script> 17 | 18 | <style scoped lang="scss"> 19 | .paint-tool-paint-card { 20 | cursor: pointer; 21 | width: 24rem; 22 | display: grid; 23 | grid-template-columns: 1fr 1fr 1fr; 24 | grid-template-rows: 1fr; 25 | grid-template-areas: 26 | "name name colorball" 27 | "metadata metadata metadata"; 28 | background-color: var(--seventv-background-shade-2); 29 | padding: 0.5rem 1rem; 30 | border-radius: 0.25rem; 31 | align-items: center; 32 | 33 | &:hover { 34 | background-color: var(--seventv-highlight-neutral-1); 35 | } 36 | 37 | div[for="name"] { 38 | grid-area: name; 39 | display: grid; 40 | align-items: center; 41 | justify-content: start; 42 | 43 | > span { 44 | font-size: 1.25rem; 45 | font-weight: 700; 46 | display: inline-block; 47 | white-space: nowrap; 48 | text-overflow: ellipsis; 49 | overflow: hidden; 50 | } 51 | } 52 | 53 | div[for="colorball"] { 54 | grid-area: colorball; 55 | display: grid; 56 | justify-content: end; 57 | 58 | > div { 59 | width: 3rem; 60 | height: 3rem; 61 | clip-path: circle(50%); 62 | } 63 | } 64 | 65 | div[for="metadata"] { 66 | grid-area: metadata; 67 | display: grid; 68 | } 69 | } 70 | </style> 71 | -------------------------------------------------------------------------------- /src/app/settings/Settings.ts: -------------------------------------------------------------------------------- 1 | import { defineAsyncComponent, markRaw, reactive } from "vue"; 2 | import { LOCAL_STORAGE_KEYS } from "@/common/Constant"; 3 | import SettingsViewBackupVue from "./SettingsViewBackup.vue"; 4 | import SettingsViewCompatVue from "./SettingsViewCompat.vue"; 5 | import SettingsViewConfigVue from "./SettingsViewConfig.vue"; 6 | import SettingsViewHomeVue from "./SettingsViewHome.vue"; 7 | import SettingsViewProfileVue from "./SettingsViewProfile.vue"; 8 | 9 | const PaintTool = defineAsyncComponent(() => import("@/app/paint-tool/PaintTool.vue")); 10 | const Store = defineAsyncComponent(() => import("@/app/store/Store.vue")); 11 | 12 | class SettingsMenuContext { 13 | open = false; 14 | view: AnyInstanceType | null = null; 15 | 16 | category = ""; 17 | scrollpoint = ""; 18 | intersectingSubcategory = ""; 19 | seen = [] as string[]; 20 | 21 | mappedNodes: Record<string, Record<string, SevenTV.SettingNode[]>> = reactive({ 22 | Home: {}, 23 | }); 24 | 25 | constructor() { 26 | this.switchView("home"); 27 | 28 | const keys = localStorage.getItem(LOCAL_STORAGE_KEYS.SEEN_SETTINGS); 29 | if (keys) { 30 | for (const key of keys.split(",")) { 31 | this.seen.push(key); 32 | } 33 | } 34 | } 35 | 36 | toggle(): void { 37 | this.open = !this.open; 38 | } 39 | 40 | switchView(name: keyof typeof views): void { 41 | this.view = markRaw(views[name]); 42 | } 43 | 44 | markSettingAsSeen(...keys: string[]): void { 45 | for (const key of keys) { 46 | if (this.seen.indexOf(key) !== -1) continue; 47 | this.seen.push(key); 48 | } 49 | 50 | localStorage.setItem(LOCAL_STORAGE_KEYS.SEEN_SETTINGS, this.seen.join(",")); 51 | } 52 | } 53 | 54 | const views = { 55 | home: SettingsViewHomeVue, 56 | config: SettingsViewConfigVue, 57 | profile: SettingsViewProfileVue, 58 | compat: SettingsViewCompatVue, 59 | backup: SettingsViewBackupVue, 60 | store: Store, 61 | paint: PaintTool, 62 | }; 63 | 64 | const inst = reactive(new SettingsMenuContext()); 65 | export function useSettingsMenu(): SettingsMenuContext { 66 | return inst; 67 | } 68 | -------------------------------------------------------------------------------- /src/app/settings/SettingsViewCompat.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main class="seventv-settings-compat"> 3 | <h3> 4 | Compatibility 5 | <p>Scan your extensions for compatibility issues</p> 6 | </h3> 7 | 8 | <iframe v-if="supportsAPI" :src="optionsURL + '#/compat?noheader=1'" /> 9 | </main> 10 | </template> 11 | 12 | <script setup lang="ts"> 13 | import { inject } from "vue"; 14 | import { SITE_EXT_OPTIONS_URL } from "@/common/Constant"; 15 | import { useUserAgent } from "@/composable/useUserAgent"; 16 | 17 | const { browser } = useUserAgent(); 18 | const optionsURL = inject(SITE_EXT_OPTIONS_URL, ""); 19 | const supportsAPI = browser.name !== "Firefox"; 20 | 21 | // firefox doesn't support using browser apis from an iframe 22 | // so we open a new tab instead 23 | if (!supportsAPI) { 24 | window.open(optionsURL + "#/compat", "_blank"); 25 | } 26 | </script> 27 | 28 | <style scoped lang="scss"> 29 | main.seventv-settings-compat { 30 | display: grid; 31 | grid-template-rows: 6rem auto 1rem; 32 | width: 100%; 33 | height: 100%; 34 | 35 | h3 { 36 | display: grid; 37 | margin: 1rem; 38 | align-items: center; 39 | text-align: center; 40 | font-size: 2rem; 41 | } 42 | 43 | iframe { 44 | height: 100%; 45 | width: inherit; 46 | } 47 | } 48 | </style> 49 | -------------------------------------------------------------------------------- /src/app/settings/SettingsViewConfig.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-if="ctx.mappedNodes[ctx.category]" class="seventv-settings-view-container"> 3 | <UiScrollable> 4 | <div 5 | v-for="[key, nodes] of Object.entries(ctx.mappedNodes[ctx.category])" 6 | :key="key" 7 | class="seventv-settings-subcategory" 8 | > 9 | <SettingsViewConfigCat ref="subCats" :name="key" :nodes="nodes" /> 10 | </div> 11 | </UiScrollable> 12 | </div> 13 | </template> 14 | 15 | <script setup lang="ts"> 16 | import { ref, watch } from "vue"; 17 | import { useSettingsMenu } from "./Settings"; 18 | import SettingsViewConfigCat from "./SettingsViewConfigCat.vue"; 19 | import UiScrollable from "@/ui/UiScrollable.vue"; 20 | 21 | const ctx = useSettingsMenu(); 22 | const subCats = ref<InstanceType<typeof SettingsViewConfigCat>[]>([]); 23 | 24 | watch( 25 | () => ctx.scrollpoint, 26 | () => { 27 | // handle click on subcategory: scroll into view 28 | subCats.value.find((r) => r.name == ctx.scrollpoint)?.scrollIntoView(); 29 | }, 30 | ); 31 | </script> 32 | <style scoped lang="scss"> 33 | .seventv-settings-view-container { 34 | display: flex; 35 | flex-direction: column; 36 | height: 100%; 37 | width: 100%; 38 | 39 | > :first-child { 40 | flex-grow: 1; 41 | } 42 | } 43 | 44 | .seventv-settings-subcategory:last-child { 45 | margin-bottom: 30%; 46 | } 47 | </style> 48 | -------------------------------------------------------------------------------- /src/app/settings/control/FormCheckbox.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <input id="checkbox" v-model="setting" type="checkbox" /> 4 | </div> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import { useConfig } from "@/composable/useSettings"; 9 | 10 | const props = defineProps<{ 11 | node: SevenTV.SettingNode<boolean>; 12 | }>(); 13 | 14 | const setting = useConfig<boolean>(props.node.key); 15 | </script> 16 | -------------------------------------------------------------------------------- /src/app/settings/control/FormColor.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <input :id="node.key" v-model="setting" type="color" /> 4 | </div> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import { useConfig } from "@/composable/useSettings"; 9 | 10 | const props = defineProps<{ 11 | node: SevenTV.SettingNode<string, "COLOR">; 12 | }>(); 13 | 14 | const setting = useConfig<string>(props.node.key); 15 | </script> 16 | 17 | <style scoped lang="scss"> 18 | input { 19 | &::-webkit-color-swatch-wrapper { 20 | padding: 0; 21 | } 22 | 23 | &::-webkit-color-swatch { 24 | border: none; 25 | } 26 | } 27 | </style> 28 | -------------------------------------------------------------------------------- /src/app/settings/control/FormDropdown.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <select :id="node.key" v-model="setting"> 3 | <option v-for="([option, value], i) of node.options" :key="i" :value="value ?? option"> 4 | {{ option }} 5 | </option> 6 | </select> 7 | </template> 8 | 9 | <script setup lang="ts"> 10 | import { useConfig } from "@/composable/useSettings"; 11 | 12 | const props = defineProps<{ 13 | node: SevenTV.SettingNode<SevenTV.SettingType, "DROPDOWN">; 14 | }>(); 15 | 16 | const setting = useConfig(props.node.key); 17 | </script> 18 | 19 | <style scoped lang="scss"> 20 | select { 21 | background-color: var(--seventv-input-background); 22 | padding: 0.5rem 1rem; 23 | border-radius: 0.25rem; 24 | border: 0.01rem solid var(--seventv-input-border); 25 | color: var(--seventv-text-color-normal); 26 | } 27 | </style> 28 | -------------------------------------------------------------------------------- /src/app/settings/control/FormInput.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="inputbox"> 3 | <input 4 | :id="node.key" 5 | v-model="temp" 6 | autocomplete="noidontthinkso" 7 | data-form-type="other" 8 | :valid="isValid" 9 | :placeholder="node.options?.placeholder" 10 | :type="node.options?.type ?? 'inputbox'" 11 | @input="onInput" 12 | /> 13 | </div> 14 | </template> 15 | 16 | <script setup lang="ts"> 17 | import { ref, watch } from "vue"; 18 | import { useConfig } from "@/composable/useSettings"; 19 | 20 | const props = defineProps<{ 21 | node: SevenTV.SettingNode<string, "INPUT">; 22 | }>(); 23 | 24 | const setting = useConfig<string>(props.node.key); 25 | const temp = ref(setting.value); 26 | const isValid = ref(true); 27 | 28 | watch(setting, (v) => (temp.value = v)); 29 | 30 | const onInput = () => { 31 | isValid.value = props.node.predicate ? props.node.predicate(temp.value) : true; 32 | if (isValid.value) setting.value = temp.value; 33 | }; 34 | </script> 35 | 36 | <style scoped lang="scss"> 37 | input { 38 | background-color: var(--seventv-input-background); 39 | padding: 0.5rem 1rem; 40 | border-radius: 0.25rem; 41 | border: 0.01rem solid var(--seventv-input-border); 42 | color: var(--seventv-text-color-normal); 43 | 44 | &[valid="false"] { 45 | outline-color: red !important; 46 | background-color: #f004; 47 | } 48 | } 49 | </style> 50 | -------------------------------------------------------------------------------- /src/app/settings/control/FormSelect.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <template v-for="([option], i) of props.node.options" :key="i"> 4 | <input v-model="setting" class="radio-button" type="text" /> 5 | {{ option }} 6 | <br /> 7 | </template> 8 | </div> 9 | </template> 10 | 11 | <script setup lang="ts"> 12 | import { ref, watch } from "vue"; 13 | import { useConfig } from "@/composable/useSettings"; 14 | 15 | const props = defineProps<{ 16 | node: SevenTV.SettingNode<string, "SELECT">; 17 | }>(); 18 | 19 | const setting = useConfig<string>(props.node.key); 20 | const value = ref<unknown>(setting.value); 21 | 22 | watch(value, (v) => { 23 | if (typeof v !== "string") return; 24 | 25 | setting.value = v; 26 | }); 27 | </script> 28 | <style scoped lang="scss"> 29 | .radio-button { 30 | margin-right: 1rem; 31 | } 32 | </style> 33 | -------------------------------------------------------------------------------- /src/app/store/Store.vue: -------------------------------------------------------------------------------- 1 | <template></template> 2 | 3 | <script setup lang="ts"></script> 4 | -------------------------------------------------------------------------------- /src/app/store/StoreSubscribeButton.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <UiButton class="seventv-button-subscribe" @click="openStorePage"> 3 | <Logo7TV /> 4 | <span v-t="'onboarding.promo_subscribe'" /> 5 | </UiButton> 6 | </template> 7 | 8 | <script setup lang="ts"> 9 | import Logo7TV from "@/assets/svg/logos/Logo7TV.vue"; 10 | import UiButton from "@/ui/UiButton.vue"; 11 | 12 | function openStorePage() { 13 | window.open(import.meta.env.VITE_APP_SITE + "/store", "_blank"); 14 | } 15 | </script> 16 | 17 | <style scoped lang="scss"> 18 | .seventv-button-subscribe { 19 | display: grid; 20 | place-items: center; 21 | place-content: center; 22 | align-items: center; 23 | align-content: center; 24 | grid-template-columns: repeat(2, auto); 25 | column-gap: 0.25em; 26 | box-shadow: none; 27 | background-image: linear-gradient(90deg, rgb(250, 170, 0) 0, rgb(255, 200, 50) 10%, rgb(250, 170, 0) 50%); 28 | animation-duration: 2s; 29 | animation-fill-mode: forwards; 30 | animation-iteration-count: infinite; 31 | animation-name: bg; 32 | animation-timing-function: linear; 33 | background-size: 200% 0.1rem; 34 | color: rgb(0, 0, 0); 35 | 36 | &:hover { 37 | filter: brightness(120%); 38 | } 39 | 40 | @keyframes bg { 41 | 0% { 42 | background-position: 0 0; 43 | } 44 | 45 | 100% { 46 | background-position: 200% 100%; 47 | } 48 | } 49 | } 50 | </style> 51 | -------------------------------------------------------------------------------- /src/assets/gql/tw.chat-bans.gql.ts: -------------------------------------------------------------------------------- 1 | import { TwTypeChatBanStatus } from "./tw.gql"; 2 | import gql from "graphql-tag"; 3 | 4 | export const twitchBanUserQuery = gql` 5 | mutation Chat_BanUserFromChatRoom($input: BanUserFromChatRoomInput!) { 6 | banUserFromChatRoom(input: $input) { 7 | ban { 8 | bannedUser { 9 | id 10 | login 11 | displayName 12 | } 13 | createdAt 14 | expiresAt 15 | isPermanent 16 | moderator { 17 | id 18 | login 19 | displayName 20 | } 21 | reason 22 | } 23 | error { 24 | code 25 | minTimeoutDurationSeconds 26 | maxTimeoutDurationSeconds 27 | } 28 | } 29 | } 30 | `; 31 | 32 | export namespace twitchBanUserQuery { 33 | export interface Variables { 34 | input: { 35 | channelID: string; 36 | bannedUserLogin: string; 37 | expiresIn: string | null; 38 | reason?: string; 39 | }; 40 | } 41 | export interface Result { 42 | banUserFromChatRoom: { 43 | ban: null | TwTypeChatBanStatus; 44 | error: null | { 45 | code: string; 46 | }; 47 | }; 48 | } 49 | } 50 | 51 | export const twitchUnbanUserQuery = gql` 52 | mutation Chat_UnbanUserFromChatRoom($input: UnbanUserFromChatRoomInput!) { 53 | unbanUserFromChatRoom(input: $input) { 54 | ban { 55 | bannedUser { 56 | id 57 | login 58 | displayName 59 | } 60 | createdAt 61 | expiresAt 62 | isPermanent 63 | moderator { 64 | id 65 | login 66 | displayName 67 | } 68 | } 69 | error { 70 | code 71 | } 72 | } 73 | } 74 | `; 75 | 76 | export namespace twitchUnbanUserQuery { 77 | export interface Variables { 78 | input: { 79 | channelID: string; 80 | bannedUserLogin: string; 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/assets/gql/tw.chat-delete.gql.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export const twitchDeleteMessageQuery = gql` 4 | mutation Chat_DeleteChatMessage($input: DeleteChatMessageInput!) { 5 | deleteChatMessage(input: $input) { 6 | responseCode 7 | message { 8 | id 9 | sender { 10 | id 11 | login 12 | displayName 13 | } 14 | content { 15 | text 16 | } 17 | } 18 | } 19 | } 20 | `; 21 | 22 | export namespace twitchDeleteMessageQuery { 23 | export interface Variables { 24 | input: { 25 | channelID: string; 26 | messageID: string; 27 | }; 28 | } 29 | export interface Result { 30 | id: string; 31 | sender: null | object; 32 | content: null | object; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/assets/gql/tw.chat-pin.gql.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export const twitchPinMessageQuery = gql` 4 | mutation PinChatMessage($input: PinChatMessageInput!) { 5 | pinChatMessage(input: $input) { 6 | pinnedChatMessage { 7 | id 8 | pinnedMessage { 9 | id 10 | } 11 | } 12 | error { 13 | code 14 | } 15 | } 16 | } 17 | `; 18 | 19 | export namespace twitchPinMessageQuery { 20 | export interface Variables { 21 | input: { 22 | channelID: string; 23 | messageID: string; 24 | durationSeconds: number; 25 | type: "MOD"; 26 | }; 27 | } 28 | export interface Result { 29 | id: string; 30 | pinnedChatMessage: null | object; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/gql/tw.chat-replies.gql.ts: -------------------------------------------------------------------------------- 1 | import { twitchMessageFragments } from "./tw.fragment.gql"; 2 | import gql from "graphql-tag"; 3 | 4 | export const twitchChatReplyQuery = gql` 5 | query ChatReplies($messageID: ID!, $channelID: ID!) { 6 | message(id: $messageID) { 7 | ...messageFields 8 | replies { 9 | nodes { 10 | ...messageFields 11 | } 12 | totalCount 13 | } 14 | } 15 | } 16 | 17 | ${twitchMessageFragments} 18 | `; 19 | -------------------------------------------------------------------------------- /src/assets/gql/tw.emote-card.gql.ts: -------------------------------------------------------------------------------- 1 | import { twitchSubProductFragment, twitchSubProductsFragments } from "./tw.fragment.gql"; 2 | import { TwTypeEmote } from "./tw.gql"; 3 | import { gql } from "graphql-tag"; 4 | 5 | export const emoteCardQuery = gql` 6 | query EmoteCard($emoteID: ID!, $octaneEnabled: Boolean!, $artistEnabled: Boolean!) { 7 | emote(id: $emoteID) { 8 | id 9 | type 10 | subscriptionTier @include(if: $octaneEnabled) 11 | token 12 | setID 13 | artist @include(if: $artistEnabled) { 14 | id 15 | login 16 | displayName 17 | profileImageURL(width: 70) 18 | } 19 | owner { 20 | id 21 | login 22 | displayName 23 | profileImageURL(width: 70) 24 | channel { 25 | id 26 | localEmoteSets { 27 | id 28 | emotes { 29 | id 30 | token 31 | } 32 | } 33 | } 34 | stream { 35 | id 36 | type 37 | } 38 | self { 39 | follower { 40 | followedAt 41 | } 42 | subscriptionBenefit { 43 | id 44 | tier 45 | } 46 | } 47 | subscriptionProducts { 48 | id 49 | displayName 50 | tier 51 | name 52 | url 53 | emotes { 54 | id 55 | token 56 | } 57 | priceInfo { 58 | id 59 | currency 60 | price 61 | } 62 | } 63 | } 64 | subscriptionProduct @skip(if: $octaneEnabled) { 65 | ...subProduct 66 | } 67 | subscriptionSummaries @include(if: $octaneEnabled) { 68 | ...subSummary 69 | } 70 | bitsBadgeTierSummary { 71 | threshold 72 | self { 73 | isUnlocked 74 | numberOfBitsUntilUnlock 75 | } 76 | } 77 | type 78 | } 79 | } 80 | 81 | ${twitchSubProductFragment} 82 | ${twitchSubProductsFragments} 83 | `; 84 | 85 | export namespace emoteCardQuery { 86 | export interface Result { 87 | emote: TwTypeEmote; 88 | } 89 | 90 | export interface Variables { 91 | emoteID: string; 92 | octaneEnabled: boolean; 93 | artistEnabled: boolean; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/assets/gql/tw.mod-user.gql.ts: -------------------------------------------------------------------------------- 1 | import { TwTypeUser } from "./tw.gql"; 2 | import gql from "graphql-tag"; 3 | 4 | export const twitchModUserMut = gql` 5 | mutation ModUser($input: ModUserInput!) { 6 | result: modUser(input: $input) { 7 | channel { 8 | id 9 | login 10 | } 11 | target { 12 | id 13 | login 14 | } 15 | error { 16 | code 17 | } 18 | } 19 | } 20 | `; 21 | 22 | export const twitchUnmodUserMut = gql` 23 | mutation UnmodUser($input: UnmodUserInput!) { 24 | result: unmodUser(input: $input) { 25 | channel { 26 | id 27 | login 28 | } 29 | target { 30 | id 31 | login 32 | } 33 | error { 34 | code 35 | } 36 | } 37 | } 38 | `; 39 | 40 | export namespace ModOrUnmodUser { 41 | export interface Variables { 42 | input: { 43 | channelID: string; 44 | targetID?: string; 45 | targetLogin?: string; 46 | }; 47 | } 48 | 49 | export interface Response { 50 | result: { 51 | channel: Pick<TwTypeUser, "id" | "login">; 52 | target: Pick<TwTypeUser, "id" | "login">; 53 | error: { 54 | code: 55 | | "FORBIDDEN" 56 | | "TARGET_NOT_FOUND" 57 | | "CHANNEL_NOT_FOUND" 58 | | "TARGET_NOT_MOD" 59 | | "TARGET_ALREADY_MOD" 60 | | "TARGET_IS_CHAT_BANNED"; 61 | }; 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/assets/style/flair.scss: -------------------------------------------------------------------------------- 1 | %seventv-flair-pulsating { 2 | &::before { 3 | content: ""; 4 | position: absolute; 5 | display: block; 6 | width: 100%; 7 | height: 100%; 8 | background-color: var(--seventv-primary); 9 | clip-path: circle(50% at 50% 50%); 10 | animation: seventv-pulse-ring 2s ease infinite; 11 | } 12 | 13 | &::after { 14 | content: ""; 15 | position: absolute; 16 | display: block; 17 | width: 100%; 18 | height: 100%; 19 | background-color: var(--seventv-primary); 20 | clip-path: circle(50% at 50% 50%); 21 | animation: seventv-pulse-dot 2s ease -0.4s infinite; 22 | } 23 | } 24 | 25 | @mixin flair-pulsating($color: white) { 26 | @extend %seventv-flair-pulsating; 27 | 28 | &::before { 29 | background-color: $color; 30 | } 31 | 32 | &::after { 33 | background-color: $color; 34 | } 35 | } 36 | 37 | @keyframes seventv-pulse-ring { 38 | 0% { 39 | transform: scale(0); 40 | } 41 | 42 | 80%, 43 | 100% { 44 | opacity: 0; 45 | transform: scale(5); 46 | } 47 | } 48 | 49 | @keyframes seventv-pulse-dot { 50 | 0% { 51 | transform: scale(1); 52 | } 53 | 54 | 50% { 55 | transform: scale(1.35); 56 | } 57 | 58 | 100% { 59 | transform: scale(1); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/assets/style/icon-direction.scss: -------------------------------------------------------------------------------- 1 | svg[direction="up"] { 2 | transform: rotate(180deg); 3 | } 4 | 5 | svg[direction="left"] { 6 | transform: rotate(90deg); 7 | } 8 | 9 | svg[direction="right"] { 10 | transform: rotate(-90deg); 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/style/shape.scss: -------------------------------------------------------------------------------- 1 | @function create-bevel($angle: 0.5em) { 2 | @return polygon( 3 | $angle 0%, 4 | calc(100% - $angle) 0%, 5 | 100% $angle, 6 | 100% calc(100% - $angle), 7 | calc(100% - $angle) 100%, 8 | $angle 100%, 9 | 0% calc(100% - $angle), 10 | 0% $angle 11 | ); 12 | } 13 | 14 | @function create-diamond($angle: 0.5em) { 15 | @return polygon($angle 0%, calc(100% - $angle) $angle, $angle calc(100% - $angle), 0% $angle); 16 | } 17 | -------------------------------------------------------------------------------- /src/assets/svg/emoji/SingleEmoji.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"> 3 | <use :xlink:href="'#' + id" /> 4 | </svg> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | defineProps<{ 9 | id: string; 10 | }>(); 11 | </script> 12 | -------------------------------------------------------------------------------- /src/assets/svg/icons/ArrowIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg 3 | xmlns="http://www.w3.org/2000/svg" 4 | viewBox="0 0 448 512" 5 | :direction="direction || 'down'" 6 | width="1em" 7 | height="1em" 8 | fill="currentColor" 9 | > 10 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 11 | <path 12 | d="M214.6 454.6L192 477.3l-22.6-22.6-144-144L2.7 288 48 242.8l22.6 22.6L160 354.8 160 64l0-32 64 0 0 32 0 290.7 89.4-89.4L336 242.8 381.3 288l-22.6 22.6-144 144z" 13 | /> 14 | </svg> 15 | </template> 16 | 17 | <script setup lang="ts"> 18 | defineProps<{ 19 | direction: "up" | "down" | "left" | "right"; 20 | }>(); 21 | </script> 22 | 23 | <style scoped lang="scss"> 24 | @import "@/assets/style/icon-direction"; 25 | </style> 26 | -------------------------------------------------------------------------------- /src/assets/svg/icons/BanIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"> 5 | <path 6 | d="M367.2 412.5L99.5 144.8C77.1 176.1 64 214.5 64 256c0 106 86 192 192 192c41.5 0 79.9-13.1 111.2-35.5zm45.3-45.3C434.9 335.9 448 297.5 448 256c0-106-86-192-192-192c-41.5 0-79.9 13.1-111.2 35.5L412.5 367.2zM512 256c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/BellIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free 2 | Copyright 2024 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor"> 5 | <path 6 | d="M224 0c-17.7 0-32 14.3-32 32V51.2C119 66 64 130.6 64 208v18.8c0 47-17.3 92.4-48.5 127.6l-7.4 8.3c-8.4 9.4-10.4 22.9-5.3 34.4S19.4 416 32 416H416c12.6 0 24-7.4 29.2-18.9s3.1-25-5.3-34.4l-7.4-8.3C401.3 319.2 384 273.9 384 226.8V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32zm45.3 493.3c12-12 18.7-28.3 18.7-45.3H224 160c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/BellSlashIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free 2 | Copyright 2024 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor"> 5 | <path 6 | d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7l-90.2-70.7c.2-.4 .4-.9 .6-1.3c5.2-11.5 3.1-25-5.3-34.4l-7.4-8.3C497.3 319.2 480 273.9 480 226.8V208c0-77.4-55-142-128-156.8V32c0-17.7-14.3-32-32-32s-32 14.3-32 32V51.2c-42.6 8.6-79 34.2-102 69.3L38.8 5.1zM406.2 416L160 222.1v4.8c0 47-17.3 92.4-48.5 127.6l-7.4 8.3c-8.4 9.4-10.4 22.9-5.3 34.4S115.4 416 128 416H406.2zm-40.9 77.3c12-12 18.7-28.3 18.7-45.3H320 256c0 17 6.7 33.3 18.7 45.3s28.3 18.7 45.3 18.7s33.3-6.7 45.3-18.7z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/BellsIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor"> 5 | <path 6 | d="M293.1 41.2c-71.1 42-104.5 130-75.2 210.4l35.3 97.1-11.3 44.4L0 305l12.9-35.6 67.2-48 36.4-100c25.7-70.5 103.6-106.8 174-81.2c.9 .3 1.7 .6 2.5 1zM97.5 374.6l117.9 42.9c-11.3 18.3-31.5 30.4-54.5 30.4c-35.4 0-64.1-28.7-64.1-64c0-3.2 .2-6.3 .7-9.3zm181.3 94.5l-13.7-37.6L286.7 347 248 240.6c-27-74.2 11.2-156.2 85.4-183.2s156.3 11.3 183.3 85.5l38.7 106.4 70.9 50.9L640 337.7 278.8 469.1zm150-16.3L545 410.6c.1 1.8 .2 3.6 .2 5.4c0 35.4-28.7 64-64.1 64c-21.6 0-40.8-10.7-52.4-27.1z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/CakeIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="1em" height="1em" fill="currentColor"> 3 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path 5 | d="M96 0L63.9 44.9C58.8 52.1 56 60.8 56 69.6V72c0 22.1 17.9 40 40 40s40-17.9 40-40V69.6c0-8.8-2.8-17.5-7.9-24.6L96 0zM224 0L191.9 44.9c-5.1 7.2-7.9 15.8-7.9 24.6V72c0 22.1 17.9 40 40 40s40-17.9 40-40V69.6c0-8.8-2.8-17.5-7.9-24.6L224 0zm95.9 44.9c-5.1 7.2-7.9 15.8-7.9 24.6V72c0 22.1 17.9 40 40 40s40-17.9 40-40V69.6c0-8.8-2.8-17.5-7.9-24.6L352 0 319.9 44.9zM128 176V144H64v32 48H0V350.8l29.2 15.3 60-28.6 7.1-3.4 7 3.5L160 366.1l56.8-28.4 7.2-3.6 7.2 3.6L288 366.1l56.8-28.4 7-3.5 7 3.4 60 28.6L448 350.8V224H384V176 144H320v32 48H256V176 144H192v32 48H128V176zM448 386.9l-21.3 11.2-7.1 3.7-7.2-3.4-60.2-28.6-57 28.5-7.2 3.6-7.2-3.6L224 369.9l-56.8 28.4-7.2 3.6-7.2-3.6-57-28.5L35.7 398.4l-7.2 3.4-7.1-3.7L0 386.9V512H448V386.9z" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/CaretIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" width="1em" height="1em" fill="currentColor"> 5 | <path d="M320 240L160 384 0 240l0-48 320 0 0 48z" /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/icons/ChatIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor"> 5 | <path d="M0 0H416V320H202.7L96 384V320H0V0zM256 448V352H448V128H640V448H544v64L437.3 448H256z" /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/icons/ChevronIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg 5 | xmlns="http://www.w3.org/2000/svg" 6 | viewBox="0 0 512 512" 7 | :direction="direction || 'down'" 8 | width="1em" 9 | height="1em" 10 | fill="currentColor" 11 | > 12 | <path 13 | d="M238 429.3l22.6-22.6 192-192L475.3 192 430 146.7l-22.6 22.6L238 338.7 68.6 169.4 46 146.7 .7 192l22.6 22.6 192 192L238 429.3z" 14 | /> 15 | </svg> 16 | </template> 17 | 18 | <script setup lang="ts"> 19 | defineProps<{ 20 | direction: "up" | "down" | "left" | "right"; 21 | }>(); 22 | </script> 23 | 24 | <style scoped lang="scss"> 25 | @import "@/assets/style/icon-direction"; 26 | </style> 27 | -------------------------------------------------------------------------------- /src/assets/svg/icons/CloseIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="1em" height="1em" fill="currentColor"> 5 | <!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 6 | <path 7 | d="M68.6 57.4L46 34.7 .7 80l22.6 22.6L176.7 256 23.4 409.4 .7 432 46 477.3l22.6-22.6L222 301.3 375.4 454.6 398 477.3 443.3 432l-22.6-22.6L267.3 256 420.6 102.6 443.3 80 398 34.7 375.4 57.4 222 210.7 68.6 57.4z" 8 | /> 9 | </svg> 10 | </template> 11 | -------------------------------------------------------------------------------- /src/assets/svg/icons/CloudIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="1em" height="1em" fill="currentColor"> 3 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path 5 | d="M0 480H144 512 640V352c0-59.6-40.8-109.8-96-124V192c0-53-43-96-96-96c-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32C167.6 32 96 103.6 96 192v8.2C40.1 220 0 273.3 0 336V480z" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/CompactDiscIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"> 3 | <!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path 5 | d="M0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm256 32a32 32 0 1 1 0-64 32 32 0 1 1 0 64zm-96-32a96 96 0 1 0 192 0 96 96 0 1 0 -192 0zM96 240c0-35 17.5-71.1 45.2-98.8S205 96 240 96c8.8 0 16-7.2 16-16s-7.2-16-16-16c-45.4 0-89.2 22.3-121.5 54.5S64 194.6 64 240c0 8.8 7.2 16 16 16s16-7.2 16-16z" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/CopyIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="1em" height="1em" fill="currentColor"> 3 | <!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --> 4 | <path 5 | d="M320 448v40c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24V120c0-13.255 10.745-24 24-24h72v296c0 30.879 25.121 56 56 56h168zm0-344V0H152c-13.255 0-24 10.745-24 24v368c0 13.255 10.745 24 24 24h272c13.255 0 24-10.745 24-24V128H344c-13.2 0-24-10.8-24-24zm120.971-31.029L375.029 7.029A24 24 0 0 0 358.059 0H352v96h96v-6.059a24 24 0 0 0-7.029-16.97z" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/DeleteIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="1em" height="1em" fill="currentColor"> 3 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path d="M144 0L128 32H0V96H448V32H320L304 0H144zM416 128H32L56 512H392l24-384z" /> 5 | </svg> 6 | </template> 7 | -------------------------------------------------------------------------------- /src/assets/svg/icons/DownloadIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor" width="1em" height="1em"> 5 | <path 6 | d="M64 480V416H0v64H64zm96 0V416H96v64h64zm32 0h64V416H192v64zm160 0V416H288v64h64zm32 0h64V416H384v64zM256 160v48h48 28.1L224 316.1 115.9 208H144h48V160 80h64v80zm1.9 190.1L384 224V208 160H352 336 304V112 80 32H256 192 144V80v32 48H112 96 64v48 16L190.1 350.1 224 384l33.9-33.9z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/DropdownIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="0.6666em" viewBox="0 0 9 6" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> 3 | <path 4 | d="M0.221974 4.46565L3.93498 0.251908C4.0157 0.160305 4.10314 0.0955723 4.19731 0.0577097C4.29148 0.0192364 4.39238 5.49454e-08 4.5 5.3662e-08C4.60762 5.23786e-08 4.70852 0.0192364 4.80269 0.0577097C4.89686 0.0955723 4.9843 0.160305 5.06502 0.251908L8.77803 4.46565C8.92601 4.63359 9 4.84733 9 5.10687C9 5.36641 8.92601 5.58015 8.77803 5.74809C8.63005 5.91603 8.4417 6 8.213 6C7.98431 6 7.79596 5.91603 7.64798 5.74809L4.5 2.17557L1.35202 5.74809C1.20404 5.91603 1.0157 6 0.786996 6C0.558296 6 0.369956 5.91603 0.221974 5.74809C0.0739918 5.58015 6.39938e-08 5.36641 6.08988e-08 5.10687C5.78038e-08 4.84733 0.0739918 4.63359 0.221974 4.46565Z" 5 | /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/icons/EmojiIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"> 5 | <path 6 | d="M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512zM129.7 327.2l28.6-14.3c8.7 17.5 41.3 55.2 97.7 55.2s88.9-37.7 97.7-55.2l28.6 14.3C369.7 352.3 327.6 400 256 400s-113.7-47.7-126.3-72.8zM208.4 208c0 17.7-14.3 32-32 32s-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32zm128 32c-17.7 0-32-14.3-32-32s14.3-32 32-32s32 14.3 32 32s-14.3 32-32 32z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/EmotesIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 | <path 4 | d="M8.5 16C12.6421 16 16 12.6421 16 8.5C16 4.35786 12.6421 1 8.5 1C4.35786 1 1 4.35786 1 8.5C1 12.6421 4.35786 16 8.5 16Z" 5 | stroke="currentColor" 6 | stroke-width="1.33333" 7 | stroke-linecap="round" 8 | stroke-linejoin="round" 9 | /> 10 | <path 11 | d="M6 6.83334H6.00833M11 6.83334H11.0083M6.41667 11C6.68823 11.2772 7.01237 11.4974 7.37011 11.6477C7.72784 11.798 8.11197 11.8754 8.5 11.8754C8.88803 11.8754 9.27216 11.798 9.62989 11.6477C9.98763 11.4974 10.3118 11.2772 10.5833 11" 12 | stroke="currentColor" 13 | stroke-width="1.33333" 14 | stroke-linecap="round" 15 | stroke-linejoin="round" 16 | /> 17 | </svg> 18 | </template> 19 | -------------------------------------------------------------------------------- /src/assets/svg/icons/FileExportIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Free 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" fill="currentColor" width="1em" height="1em"> 5 | <path 6 | d="M0 64C0 28.7 28.7 0 64 0H224V128c0 17.7 14.3 32 32 32H384V288H216c-13.3 0-24 10.7-24 24s10.7 24 24 24H384V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zM384 336V288H494.1l-39-39c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l80 80c9.4 9.4 9.4 24.6 0 33.9l-80 80c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l39-39H384zm0-208H256V0L384 128z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/ForwardIcon.vue: -------------------------------------------------------------------------------- 1 | /*Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial 2 | License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"> 5 | <path 6 | d="M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416V96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4L224 214.3V256v41.7L52.5 440.6zM256 352V256 128 96c0-12.4 7.2-23.7 18.4-29s24.5-3.6 34.1 4.4l192 160c7.3 6.1 11.5 15.1 11.5 24.6s-4.2 18.5-11.5 24.6l-192 160c-9.5 7.9-22.8 9.7-34.1 4.4s-18.4-16.6-18.4-29V352z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/GaugeIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" height="1em" width="1em"> 5 | <path 6 | d="M0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm320 96c0-26.9-16.5-49.9-40-59.3V88 64H232V88 292.7c-23.5 9.5-40 32.5-40 59.3c0 35.3 28.7 64 64 64s64-28.7 64-64zM144 176a32 32 0 1 0 0-64 32 32 0 1 0 0 64zm-16 80a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm288 32a32 32 0 1 0 0-64 32 32 0 1 0 0 64zM400 144a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/GavelIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 3 | 4 | <svg mlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="1em" height="1em" fill="currentColor"> 5 | <path 6 | v-if="slashed" 7 | d="M408 56L384 80c37.3 37.3 74.7 74.7 112 112c8-8 16-16 24-24l56 56c-37.5 37.5-74.9 74.9-112.4 112.4c59.1 45.9 118.2 91.7 177.3 137.6c-9.8 12.6-19.6 25.3-29.4 37.9C407.9 353.9 204.5 195.9 1 38C10.8 25.3 20.6 12.7 30.4 .1c60.3 46.8 120.7 93.7 181 140.5C258.3 93.7 305.1 46.9 352 0c18.7 18.7 37.3 37.3 56 56zM288 176l-13.5 13.5c40.3 31.3 80.7 62.7 121 94C359.7 247.7 323.8 211.8 288 176zm-9.4 166.7c5.8 5.7 11.6 11.5 17.4 17.3c-50.7 50.7-101.3 101.3-152 152c-26.7-26.7-53.3-53.3-80-80c50.7-50.7 101.3-101.3 152-152c5.8 5.8 11.6 11.6 17.4 17.4c3.3-3.3 6.5-6.5 9.8-9.8c16.9 13.3 33.8 26.6 50.6 39.9c-5.1 5.1-10.1 10.1-15.2 15.2z" 8 | /> 9 | <path 10 | v-else 11 | d="M344 56L320 80 432 192l24-24 56 56L368 368l-56-56 24-24L224 176l-24 24-56-56L288 0l56 56zM214.6 342.6L232 360 80 512 0 432 152 280l17.4 17.4L234.7 232 280 277.3l-65.4 65.4z" 12 | /> 13 | </svg> 14 | </template> 15 | 16 | <script setup lang="ts"> 17 | defineProps<{ 18 | slashed?: boolean; 19 | }>(); 20 | </script> 21 | -------------------------------------------------------------------------------- /src/assets/svg/icons/GearsIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor" height="1em" width="1em"> 5 | <path 6 | d="M125 8h70l10 48.1c13.8 5.2 26.5 12.7 37.5 22L285.6 64 320 123.4l-33.9 30.3c1.3 7.3 1.9 14.7 1.9 22.3s-.7 15.1-1.9 22.3L320 228.6 285.6 288l-43.1-14.2c-11.1 9.3-23.7 16.8-37.5 22L195 344H125l-10-48.1c-13.8-5.2-26.5-12.7-37.5-22L34.4 288 0 228.6l33.9-30.3C32.7 191.1 32 183.6 32 176s.7-15.1 1.9-22.3L0 123.4 34.4 64 77.5 78.2c11.1-9.3 23.7-16.8 37.5-22L125 8zm83 168c0-26.5-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48s48-21.5 48-48zM632 386.4l-47.8 9.8c-4.9 13.4-12 25.8-20.9 36.7l15 44.8L517.7 512l-30.9-34c-7.4 1.3-15 2-22.7 2s-15.4-.7-22.7-2l-30.9 34-60.6-34.4 15-44.8c-8.9-10.9-16-23.3-20.9-36.7L296 386.4V317.6l47.8-9.8c4.9-13.4 12-25.8 20.9-36.7l-15-44.8L410.3 192l30.9 34c7.4-1.3 15-2 22.7-2s15.4 .7 22.7 2l30.9-34 60.6 34.4-15 44.8c8.9 10.9 16 23.3 20.9 36.7l47.8 9.8v68.7zM464 400c26.5 0 48-21.5 48-48s-21.5-48-48-48s-48 21.5-48 48s21.5 48 48 48z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/HeartIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor"> 3 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path 5 | d="M39.8 263.8L64 288 256 480 448 288l24.2-24.2c25.5-25.5 39.8-60 39.8-96C512 92.8 451.2 32 376.2 32c-36 0-70.5 14.3-96 39.8L256 96 231.8 71.8c-25.5-25.5-60-39.8-96-39.8C60.8 32 0 92.8 0 167.8c0 36 14.3 70.5 39.8 96z" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/HomeIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" fill="currentColor"> 5 | <path d="M511.8 287.6H576V240L288.4 0 0 240v47.6H64.1V512H224V352H352V512H512.8l-1-224.4z" /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/icons/IconFillDrip.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" fill="currentcolor" width="1em" height="1em"> 3 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path 5 | d="M0 272l45.3 45.3L194.7 466.7 240 512l45.3-45.3L466.7 285.3 512 240l-45.3-45.3L317.3 45.3 272 0 226.7 45.3l-60.1 60.1-80-80L64 2.7 18.7 48 41.4 70.6l80 80L45.3 226.7 0 272zm185.4-57.4L208 237.3 253.3 192l-22.6-22.6-18.7-18.7L272 90.5 421.5 240l-48 48h-267l-16-16 76.1-76.1 18.7 18.7zM512 320l-55.7 97.4c-5.5 9.6-8.3 20.4-8.3 31.4c0 34.9 28.3 63.2 63.2 63.2h1.6c34.9 0 63.2-28.3 63.2-63.2c0-11-2.9-21.8-8.3-31.4L512 320z" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/IconForSettings.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <component :is="c as AnyInstanceType" /> 3 | </template> 4 | 5 | <script setup lang="ts"> 6 | import BellsIcon from "./BellsIcon.vue"; 7 | import ChatIcon from "./ChatIcon.vue"; 8 | import EmotesIcon from "./EmotesIcon.vue"; 9 | import FileExportIcon from "./FileExportIcon.vue"; 10 | import GearsIcon from "./GearsIcon.vue"; 11 | import HomeIcon from "./HomeIcon.vue"; 12 | import IconFillDrip from "./IconFillDrip.vue"; 13 | import PaintIcon from "./PaintIcon.vue"; 14 | import PlayerIcon from "./PlayerIcon.vue"; 15 | import PuzzlePieceIcon from "./PuzzlePieceIcon.vue"; 16 | import SiteLayoutIcon from "./SiteLayoutIcon.vue"; 17 | import TvIcon from "./TvIcon.vue"; 18 | import YouTubeIcon from "./YouTubeIcon.vue"; 19 | 20 | const props = withDefaults( 21 | defineProps<{ 22 | name: string; 23 | }>(), 24 | { name: "general" }, 25 | ); 26 | 27 | const c = { 28 | Home: HomeIcon, 29 | Chat: ChatIcon, 30 | Emotes: EmotesIcon, 31 | Appearance: PaintIcon, 32 | Highlights: BellsIcon, 33 | General: GearsIcon, 34 | Channel: TvIcon, 35 | Player: PlayerIcon, 36 | Compatibility: PuzzlePieceIcon, 37 | Backup: FileExportIcon, 38 | "Paint Tool": IconFillDrip, 39 | "Site Layout": SiteLayoutIcon, 40 | 41 | "Enable YouTube": YouTubeIcon, 42 | }[props.name]; 43 | </script> 44 | -------------------------------------------------------------------------------- /src/assets/svg/icons/IconUpRightFromSquare.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentcolor"> 3 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path 5 | d="M321.9 81.9L288 48l33.9-33.9L336 0H464h48V48 176l-14.1 14.1L464 224l-33.9-33.9L393 153 241 305l-17 17L190.1 288l17-17L359 119 321.9 81.9zM464 156.1V48H355.9L464 156.1zM0 32H24 200h24V80H200 48V464H432V312 288h48v24V488v24H456 24 0V488 56 32z" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/LogoutIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"> 5 | <path 6 | d="M160 96h32V32H160 32 0V64 448v32H32 160h32V416H160 64L64 96l96 0zM352 416L512 256 352 96H320v96H160l0 128H320v96h32z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/ModLogsIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor"> 5 | <path 6 | d="M344 56L320 80c37.3 37.3 74.7 74.7 112 112c8-8 16-16 24-24l24.7 24.7c-75.6 6.5-137.5 60.8-155.4 132.6c-4.4-4.4-8.8-8.8-13.2-13.2c8-8 16-16 24-24c-37.3-37.3-74.7-74.7-112-112l-24 24c-18.7-18.7-37.3-37.3-56-56C192 96 240 48 288 0c18.7 18.7 37.3 37.3 56 56zM214.6 342.7c5.8 5.7 11.6 11.5 17.4 17.3C181.3 410.7 130.7 461.3 80 512c-26.7-26.7-53.3-53.3-80-80c50.7-50.7 101.3-101.3 152-152c5.8 5.8 11.6 11.6 17.4 17.4c21.8-21.8 43.5-43.6 65.3-65.4c15.1 15.1 30.2 30.2 45.3 45.3c-21.8 21.8-43.6 43.6-65.4 65.4zM352 368c0-79.5 64.5-144 144-144s144 64.5 144 144s-64.5 144-144 144s-144-64.5-144-144zm160-80l-32 0c0 32 0 64 0 96c26.7 0 53.3 0 80 0c0-10.7 0-21.3 0-32c-16 0-32 0-48 0c0-21.3 0-42.7 0-64z" 7 | ></path> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/OpenLinkIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"> 5 | <path 6 | d="M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32H320zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/PaintIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" fill="currentColor"> 5 | <path 6 | d="M575.2 80l-39.6 39.6L306.9 348.3l-79.2-79.2L456.4 40.4 496 .8 575.2 80zM205.1 291.7l79.2 79.2-.1 .1c2.5 9.3 3.8 19 3.8 29c0 61.9-50.1 112-112 112H0V448H64V400c0-61.9 50.1-112 112-112c10 0 19.8 1.3 29 3.8l.1-.1z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/PauseIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512" fill="currentColor" width="1em" height="1em"> 5 | <path d="M128 64H0V448H128V64zm192 0H192V448H320V64z" /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/icons/PinIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="1em" height="1em" fill="currentColor"> 5 | <path 6 | d="M64 0H32V64H64 93.5L82.1 212.1C23.7 240.7 0 293 0 320v32H384V320c0-22.5-23.7-76.5-82.1-106.7L290.5 64H320h32V0H320 64zm96 480v32h64V480 384H160v96z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/PlayerIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="1em" height="1em" fill="currentColor"> 5 | <path 6 | d="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/PlusIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="1em" height="1em" fill="currentcolor"> 3 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path d="M256 80V48H192V80 224H48 16v64H48 192V432v32h64V432 288H400h32V224H400 256V80z" /> 5 | </svg> 6 | </template> 7 | -------------------------------------------------------------------------------- /src/assets/svg/icons/PuzzlePieceIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"> 5 | <path 6 | d="M192 89.6c-19.1-8.3-32-23.8-32-41.6c0-26.5 28.7-48 64-48s64 21.5 64 48c0 17.8-12.9 33.3-32 41.6V128H384V256h38.4c8.3-19.1 23.8-32 41.6-32c26.5 0 48 28.7 48 64s-21.5 64-48 64c-17.8 0-33.3-12.9-41.6-32H384V512H256V473.6c19.1-8.3 32-23.8 32-41.6c0-26.5-28.7-48-64-48s-64 21.5-64 48c0 17.8 12.9 33.3 32 41.6V512H0V320H38.4c8.3 19.1 23.8 32 41.6 32c26.5 0 48-28.7 48-64s-21.5-64-48-64c-17.8 0-33.3 12.9-41.6 32H0V128H192V89.6z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/ReplyIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor"> 5 | <path 6 | d="M0 208L192 384h32V288h80c61.9 0 112 50.1 112 112c0 48-32 80-32 80s128-48 128-176c0-97.2-78.8-176-176-176H224V32H192L0 208z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/SearchIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 | <path 4 | d="M11.3733 5.68667C11.3733 6.94156 10.966 8.10077 10.2797 9.04125L13.741 12.5052C14.0827 12.8469 14.0827 13.4019 13.741 13.7437C13.3992 14.0854 12.8442 14.0854 12.5025 13.7437L9.04125 10.2797C8.10077 10.9687 6.94156 11.3733 5.68667 11.3733C2.54533 11.3733 0 8.828 0 5.68667C0 2.54533 2.54533 0 5.68667 0C8.828 0 11.3733 2.54533 11.3733 5.68667ZM5.68667 9.62359C7.86018 9.62359 9.62359 7.86018 9.62359 5.68667C9.62359 3.51316 7.86018 1.74974 5.68667 1.74974C3.51316 1.74974 1.74974 3.51316 1.74974 5.68667C1.74974 7.86018 3.51316 9.62359 5.68667 9.62359Z" 5 | fill="currentColor" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/ShieldIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 3 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="1em" height="1em" fill="currentColor"> 4 | <path 5 | v-if="slashed" 6 | d="M496.8 363l124.1 96.3 19 14.7-29.4 37.9-19-14.7L19 52.7 0 38 29.4 .1l19 14.7 77.8 60.4L308.4 4.5 320 0l11.6 4.5L539.1 85l19.2 7.4 1.2 20.5c2.9 50-4.9 126.3-37.3 200.9c-7.2 16.5-15.6 32.9-25.3 49zM170.4 109.5L458.6 333.3c7.4-12.6 13.9-25.5 19.5-38.5C505 232.9 512.9 169.5 512 126L320 51.5l-149.6 58zm-8.5 185.2c28.2 64.9 77 127.7 158.1 164.8c30-13.7 55.6-31 77.3-50.5l38.2 30.1c-28.1 26.5-62 49.7-102.8 67.3L320 512l-12.7-5.5c-98.4-42.6-156.7-117.3-189.4-192.6c-23.5-54.1-34.1-109-37-154.2l53.3 42c5.2 29.5 13.9 61.5 27.7 93z" 7 | /> 8 | <path 9 | v-else 10 | d="M267.6 4.5L256 0 244.4 4.5 36.9 85 17.8 92.5 16.6 113c-2.9 49.9 4.9 126.3 37.3 200.9c32.7 75.3 91 150 189.4 192.6L256 512l12.7-5.5c98.4-42.6 156.7-117.3 189.4-192.6c32.4-74.7 40.2-151 37.3-200.9l-1.2-20.5L475.1 85 267.6 4.5zM256 68.7l0 0L432 137c-.5 40.9-8.8 96.8-32.6 151.5c-26.2 60.3-70.6 118-143.5 153.5V68.7z" 11 | /> 12 | </svg> 13 | </template> 14 | 15 | <script setup lang="ts"> 16 | defineProps<{ 17 | slashed?: boolean; 18 | }>(); 19 | </script> 20 | -------------------------------------------------------------------------------- /src/assets/svg/icons/SiteLayoutIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" fill="currentColor"> 3 | <!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --> 4 | <path 5 | d="M64 0C28.7 0 0 28.7 0 64V352c0 35.3 28.7 64 64 64H240l-10.7 32H160c-17.7 0-32 14.3-32 32s14.3 32 32 32H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H346.7L336 416H512c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H64zM512 64V352H64V64H512z" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/icons/StarIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2023 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="1em" height="1em" fill="currentColor"> 5 | <path 6 | d="M288.1 0l86.5 164 182.7 31.6L428 328.5 454.4 512 288.1 430.2 121.7 512l26.4-183.5L18.9 195.6 201.5 164 288.1 0z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/SwordIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"> 5 | <path 6 | d="M400 16L166.6 249.4l96 96L496 112 512 0 400 16zM0 416l96 96 32-32-16-32 56-56 88 56 32-32L96 224 64 256l56 88L64 400 32 384 0 416z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/TimerIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor" width="1em" height="1em"> 5 | <path 6 | d="M256 0H224V32 96v32h64V96 66.7C378.8 81.9 448 160.9 448 256c0 106-86 192-192 192S64 362 64 256c0-53.7 22-102.3 57.6-137.1l22.9-22.4L99.7 50.7 76.8 73.1C29.5 119.6 0 184.4 0 256C0 397.4 114.6 512 256 512s256-114.6 256-256S397.4 0 256 0zM193 159l-17-17L142.1 176l17 17 80 80 17 17L289.9 256l-17-17-80-80z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/TvIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"> 5 | <path 6 | d="M185 23l-17-17L134.1 40l17 17 39 39H0V512H512V96H321.9l39-39 17-17L344 6.1 327 23l-71 71L185 23zM472 232c0 13.3-10.7 24-24 24s-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24zM448 336c-13.3 0-24-10.7-24-24s10.7-24 24-24s24 10.7 24 24s-10.7 24-24 24zM64 160H384V448H64V160z" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/icons/WarningIcon.vue: -------------------------------------------------------------------------------- 1 | /* Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license 2 | (Commercial License) Copyright 2022 Fonticons, Inc. */ 3 | <template> 4 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="currentColor" width="1em" height="1em"> 5 | <path d="M448 32H0V480H448V32zM248 128v24V264v24H200V264 152 128h48zM200 320h48v48H200V320z" /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/icons/YouTubeIcon.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" width="1em" height="1em" fill="currentColor"> 3 | <path 4 | d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z" 5 | /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/logos/Logo.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <component :is="c" /> 3 | </template> 4 | 5 | <script setup lang="ts"> 6 | import { useStore } from "@/store/main"; 7 | import Logo7TV from "@/assets/svg/logos/Logo7TV.vue"; 8 | import LogoBTTV from "@/assets/svg/logos/LogoBTTV.vue"; 9 | import LogoFFZ from "@/assets/svg/logos/LogoFFZ.vue"; 10 | import LogoTwitch from "@/assets/svg/logos/LogoTwitch.vue"; 11 | import LogoKick from "./LogoKick.vue"; 12 | import EmojiIcon from "../icons/EmojiIcon.vue"; 13 | 14 | const props = withDefaults( 15 | defineProps<{ 16 | provider: SevenTV.Provider; 17 | }>(), 18 | { provider: "7TV" }, 19 | ); 20 | 21 | const { platform } = useStore(); 22 | 23 | const c = { 24 | "7TV": Logo7TV, 25 | FFZ: LogoFFZ, 26 | BTTV: LogoBTTV, 27 | PLATFORM: ( 28 | { 29 | TWITCH: LogoTwitch, 30 | YOUTUBE: LogoTwitch, 31 | KICK: LogoKick, 32 | } as Record<Platform, AnyInstanceType> 33 | )[platform], 34 | EMOJI: EmojiIcon, 35 | }[props.provider]; 36 | </script> 37 | -------------------------------------------------------------------------------- /src/assets/svg/logos/Logo7TV.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33 23.551"> 3 | <path 4 | d="M2.383,0,0,4.127,1.473,6.676H11.7L3.426,21,4.9,23.551H9.66Q14.532,15.113,19.4,6.676L15.549,0ZM18.492,0l3.856,6.676h2.945l2.381-4.125L26.2,0Zm2.383,9.225L17.021,15.9l4.417,7.649H26.2L33,11.775l-1.473-2.55H26.764l-2.944,5.1Z" 5 | /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoAVIF.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg 3 | version="1.1" 4 | xmlns="http://www.w3.org/2000/svg" 5 | xmlns:xlink="http://www.w3.org/1999/xlink" 6 | width="1em" 7 | viewBox="0 0 760 568" 8 | > 9 | <polygon fill="#FBAC30" points="470.5,567.31 0.47,284 470.5,0.69" /> 10 | <polygon fill="#12B17D" points="470.5,567.31 356.27,466.45 470.5,414.65" /> 11 | <polygon fill="#12B17D" points="356.27,101.55 470.5,0.69 470.5,153.35" /> 12 | <polygon fill="#BB255C" points="759.53,284 356.27,466.45 356.27,101.55" /> 13 | <path 14 | d="M189.48,294.52h-24.31l12.16-41.96L189.48,294.52z M246.02,344.09l-45.31-130.19h-46.78l-45.31,130.19h42.18l4.42-15.22 15 | h44.2l4.42,15.22H246.02z" 16 | /> 17 | <polygon 18 | points="309.28,344.09 354.59,213.91 312.41,213.91 285.89,301.54 259.37,213.91 217.19,213.91 262.5,344.09" 19 | /> 20 | <polygon 21 | fill="#FFFFFF" 22 | points="459.92,344.09 459.92,295.49 514.62,295.49 514.62,261.14 459.92,261.14 459.92,248.26 23 | 515.91,248.26 515.91,213.91 422.71,213.91 422.71,344.09" 24 | /> 25 | <rect x="368.33" y="213.91" fill="#FFFFFF" width="37.2" height="130.19" /> 26 | </svg> 27 | </template> 28 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoBTTV.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="-25 -25 350 350"> 3 | <path 4 | d="M150 1.74C68.409 1.74 1.74 68.41 1.74 150S68.41 298.26 150 298.26h148.26V150.17h-.004c0-.057.004-.113.004-.17C298.26 68.409 231.59 1.74 150 1.74zm0 49c55.11 0 99.26 44.15 99.26 99.26 0 55.11-44.15 99.26-99.26 99.26-55.11 0-99.26-44.15-99.26-99.26 0-55.11 44.15-99.26 99.26-99.26z" 5 | fill="currentColor" 6 | /> 7 | <path 8 | d="M161.388 70.076c-10.662 0-19.42 7.866-19.42 17.67 0 9.803 8.758 17.67 19.42 17.67 10.662 0 19.42-7.867 19.42-17.67 0-9.804-8.758-17.67-19.42-17.67zm45.346 24.554-.02.022-.004.002c-5.402 2.771-11.53 6.895-18.224 11.978l-.002.002-.004.002c-25.943 19.766-60.027 54.218-80.344 80.33h-.072l-1.352 1.768c-5.114 6.69-9.267 12.762-12.098 18.006l-.082.082.022.021v.002l.004.002.174.176.052-.053.102.053-.07.072c30.826 30.537 81.213 30.431 111.918-.273 30.783-30.784 30.8-81.352.04-112.152l-.005-.004zM87.837 142.216c-9.803 0-17.67 8.758-17.67 19.42 0 10.662 7.867 19.42 17.67 19.42 9.804 0 17.67-8.758 17.67-19.42 0-10.662-7.866-19.42-17.67-19.42z" 9 | fill="currentColor" 10 | /> 11 | </svg> 12 | </template> 13 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoBrandDiscord.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="1em" height="1em" fill="currentColor"> 3 | <path 4 | d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z" 5 | /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoBrandKick.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1539.02 1539.02"> 3 | <rect width="1539.02" height="1539.02" /> 4 | <polygon 5 | fill="#53fc19" 6 | fill-rule="evenodd" 7 | points="278.26 216.86 646.7 216.86 646.7 462.48 769.51 462.48 769.51 339.67 892.32 339.67 892.32 216.86 1260.75 216.86 1260.75 585.29 1137.94 585.29 1137.94 708.1 1015.13 708.1 1015.13 830.91 1137.94 830.91 1137.94 953.72 1260.75 953.72 1260.75 1322.16 892.32 1322.16 892.32 1199.35 769.51 1199.35 769.51 1076.54 646.7 1076.54 646.7 1322.16 278.26 1322.16 278.26 216.86" 8 | /> 9 | </svg> 10 | </template> 11 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoBrandTwitch.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2400 2800" width="1em" height="1em"> 3 | <polygon fill="#ffffff" points="2200,1300 1800,1700 1400,1700 1050,2050 1050,1700 600,1700 600,200 2200,200" /> 4 | <path 5 | fill="#9146ff" 6 | d="M500,0L0,500v1800h600v500l500-500h400l900-900V0H500z M2200,1300l-400,400h-400l-350,350v-350H600V200h1600 7 | V1300z" 8 | /> 9 | <rect x="1700" y="550" fill="#9146ff" width="200" height="600" /> 10 | <rect x="1150" y="550" fill="#9146ff" width="200" height="600" /> 11 | </svg> 12 | </template> 13 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoBrandTwitter.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="1em" height="1em" fill="currentColor"> 3 | <path 4 | d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z" 5 | /> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoBrandYouTube.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="-35.20005 -41.33325 305.0671 247.9995"> 3 | <path 4 | d="M229.763 25.817c-2.699-10.162-10.65-18.165-20.748-20.881C190.716 0 117.333 0 117.333 0S43.951 0 25.651 4.936C15.553 7.652 7.6 15.655 4.903 25.817 0 44.236 0 82.667 0 82.667s0 38.429 4.903 56.85C7.6 149.68 15.553 157.681 25.65 160.4c18.3 4.934 91.682 4.934 91.682 4.934s73.383 0 91.682-4.934c10.098-2.718 18.049-10.72 20.748-20.882 4.904-18.421 4.904-56.85 4.904-56.85s0-38.431-4.904-56.85" 5 | fill="red" 6 | /> 7 | <path d="M93.333 117.559l61.333-34.89-61.333-34.894z" fill="#fff" /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoFFZ.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 | <path 4 | d="M40.0716 67.9018C33.7085 68.5686 28.834 65.7635 24.3565 61.822C23.9566 61.4691 23.1627 61.4811 22.5614 61.514C22.1644 61.5349 21.7996 61.9266 21.3968 62.1001C19.6805 62.8388 18.8194 62.5038 17.5526 61.2209C16.2917 59.9439 14.8177 58.8853 13.4283 57.7399C10.4423 55.2816 7.3629 52.946 5.381 49.4381C4.42069 47.7395 3.76103 46.0379 4.08211 44.0671C4.50826 41.4354 4.49075 38.5854 5.54445 36.2408C6.84043 33.355 8.86903 30.7442 10.8947 28.289C13.1101 25.6035 15.5503 26.1537 17.2432 29.2101C18.8078 32.0361 18.9012 34.916 18.2357 37.9843C17.9321 39.3779 17.8474 40.8463 17.8679 42.2788C17.8737 42.7722 18.4225 43.5497 18.8603 43.6664C19.2923 43.78 20.1592 43.3673 20.3781 42.9397C22.074 39.6501 23.8866 36.3934 25.1855 32.9363C25.9064 31.0164 25.74 28.7376 25.921 26.6143C26.0611 24.9844 25.9123 23.3037 26.2538 21.7277C27.1878 17.4094 30.0454 14.8554 34.0179 13.4648C34.9841 13.1269 35.9269 12.7142 36.9076 12.4361C40.1446 11.518 43.2882 12.0473 45.8977 14.045C48.3787 15.944 49.771 18.7641 50.7167 21.8533C51.4202 24.1441 52.4213 26.387 53.6239 28.4505C54.4821 29.9248 55.1008 31.3752 55.0921 33.041C55.0775 35.8012 55.7284 38.3611 57.0448 40.7386C57.2842 41.1723 57.8008 41.4474 58.189 41.7973C58.3992 41.2829 58.8195 40.7536 58.7844 40.2601C58.6152 37.8827 58.2532 35.5201 58.0927 33.1456C57.9234 30.6665 61.2626 26.5365 63.6122 25.9534C65.8335 25.4031 66.572 27.2812 67.6986 28.4236C71.5165 32.2963 73.2328 37.3773 74.9141 42.4044C75.6117 44.4918 75.9006 46.7825 75.997 48.9985C76.0758 50.8198 74.6076 52.01 73.332 53.0806C70.095 55.793 66.8084 58.4217 62.9964 60.2938C61.7062 60.9278 60.6496 62.0433 59.4091 62.8118C58.8282 63.1707 57.8854 63.5954 57.4243 63.3561C55.0541 62.115 53.2561 63.5714 51.4669 64.5972C47.8504 66.6756 44.1872 68.3982 40.0775 67.9018H40.0716Z" 5 | fill="currentColor" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoKick.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1539.02 1539.02" width="1em" height="1em" fill="none"> 3 | <polygon 4 | fill="currentColor" 5 | fill-rule="evenodd" 6 | points="278.26 216.86 646.7 216.86 646.7 462.48 769.51 462.48 769.51 339.67 892.32 339.67 892.32 216.86 1260.75 216.86 1260.75 585.29 1137.94 585.29 1137.94 708.1 1015.13 708.1 1015.13 830.91 1137.94 830.91 1137.94 953.72 1260.75 953.72 1260.75 1322.16 892.32 1322.16 892.32 1199.35 769.51 1199.35 769.51 1076.54 646.7 1076.54 646.7 1322.16 278.26 1322.16 278.26 216.86" 7 | /> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/logos/LogoTwitch.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg"> 3 | <path 4 | d="M38.7818 25.362H43.6208V39.914H38.7818M52.0805 25.362H56.9195V39.914H52.0805M23.0805 12L11 24.138V67.862H25.4831V80L37.5974 67.862H47.2415L69 46V12M64.161 43.586L54.5169 53.276H44.839L36.3792 61.776V53.276H25.4831V16.862H64.161V43.586Z" 5 | fill="currentColor" 6 | /> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwAnnounce.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg fill="var(--color-fill-current)" width="18px" height="20px" version="1.1" viewBox="0 0 20 20" x="0px" y="0px"> 3 | <path 4 | fill-rule="evenodd" 5 | clip-rule="evenodd" 6 | d="M11 14l7 4V2l-7 4H4a2 2 0 00-2 2v4a2 2 0 002 2h2v4h2v-4h3zm1-6.268l4-2.286v9.108l-4-2.286V7.732zM10 12H4V8h6v4z" 7 | ></path> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwAnnouncement.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="18px" height="20px" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="var(--color-fill-current)"> 3 | <path 4 | fill-rule="evenodd" 5 | clip-rule="evenodd" 6 | d="M11 14l7 4V2l-7 4H4a2 2 0 00-2 2v4a2 2 0 002 2h2v4h2v-4h3zm1-6.268l4-2.286v9.108l-4-2.286V7.732zM10 12H4V8h6v4z" 7 | ></path> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwBits.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="20px" height="20px" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="var(--color-fill-current)"> 3 | <g> 4 | <path 5 | fill-rule="evenodd" 6 | d="M3 12l7-10 7 10-7 6-7-6zm2.678-.338L10 5.487l4.322 6.173-.85.728L10 11l-3.473 1.39-.849-.729z" 7 | clip-rule="evenodd" 8 | ></path> 9 | </g> 10 | </svg> 11 | </template> 12 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwChannelPoints.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg type="color-fill-current" width="1em" height="1em" version="1.1" viewBox="0 0 20 20" fill="currentColor"> 3 | <g> 4 | <path d="M10 6a4 4 0 014 4h-2a2 2 0 00-2-2V6z"></path> 5 | <path 6 | fill-rule="evenodd" 7 | d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-2 0a6 6 0 11-12 0 6 6 0 0112 0z" 8 | clip-rule="evenodd" 9 | ></path> 10 | </g> 11 | </svg> 12 | </template> 13 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwChatModBan.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="currentColor"> 3 | <g> 4 | <path 5 | fill-rule="evenodd" 6 | d="M2 10a8 8 0 1116 0 8 8 0 01-16 0zm8 6a6 6 0 01-4.904-9.458l8.362 8.362A5.972 5.972 0 0110 16zm4.878-2.505a6 6 0 00-8.372-8.372l8.372 8.372z" 7 | clip-rule="evenodd" 8 | ></path> 9 | </g> 10 | </svg> 11 | </template> 12 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwChatModDelete.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="currentColor"> 3 | <g> 4 | <path d="M12 2H8v1H3v2h14V3h-5V2zM4 7v9a2 2 0 002 2h8a2 2 0 002-2V7h-2v9H6V7H4z"></path> 5 | <path d="M11 7H9v7h2V7z"></path> 6 | </g> 7 | </svg> 8 | </template> 9 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwChatModTimeout.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="currentColor"> 3 | <g> 4 | <path d="M11 9.586V6H9v4.414l2.293 2.293 1.414-1.414L11 9.586z"></path> 5 | <path 6 | fill-rule="evenodd" 7 | d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-2 0a6 6 0 11-12 0 6 6 0 0112 0z" 8 | clip-rule="evenodd" 9 | ></path> 10 | </g> 11 | </svg> 12 | </template> 13 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwChatModWarn.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="currentColor"> 3 | <g> 4 | <path 5 | fill-rule="evenodd" 6 | d="M10.954 3.543c-.422-.724-1.486-.724-1.908 0l-6.9 11.844c-.418.719.11 1.613.955 1.613h13.798c.844 0 1.373-.894.955-1.613l-6.9-11.844zM11 15H9v-2h2v2zm0-3H9V7h2v5z" 7 | clip-rule="evenodd" 8 | ></path> 9 | </g> 10 | </svg> 11 | </template> 12 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwChatReply.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg fill="currenColor" width="1em" height="1em" version="1.1" viewBox="0 0 16 16" x="0px" y="0px"> 3 | <g> 4 | <path d="M5 6H7V8H5V6Z"></path> 5 | <path d="M9 6H11V8H9V6Z"></path> 6 | <path 7 | fill-rule="evenodd" 8 | clip-rule="evenodd" 9 | d="M8 14L10 12H13C13.5523 12 14 11.5523 14 11V3C14 2.44772 13.5523 2 13 2H3C2.44772 2 2 2.44772 2 3V11C2 11.5523 2.44772 12 3 12H6L8 14ZM6.82843 10H4V4H12V10H9.17157L8 11.1716L6.82843 10Z" 10 | ></path> 11 | </g> 12 | </svg> 13 | </template> 14 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwClose.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="currentColor"> 3 | <g> 4 | <path 5 | d="M8.5 10L4 5.5 5.5 4 10 8.5 14.5 4 16 5.5 11.5 10l4.5 4.5-1.5 1.5-4.5-4.5L5.5 16 4 14.5 8.5 10z" 6 | ></path> 7 | </g> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwFlame.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="20px" height="20px" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="var(--color-fill-current)"> 3 | <g> 4 | <path 5 | fill-rule="evenodd" 6 | d="M11 4.5 9 2 4.8 6.9A7.48 7.48 0 0 0 3 11.77C3 15.2 5.8 18 9.23 18h1.65A6.12 6.12 0 0 0 17 11.88c0-1.86-.65-3.66-1.84-5.1L12 3l-1 1.5ZM6.32 8.2 9 5l2 2.5L12 6l1.62 2.07A5.96 5.96 0 0 1 15 11.88c0 2.08-1.55 3.8-3.56 4.08.36-.47.56-1.05.56-1.66 0-.52-.18-1.02-.5-1.43L10 11l-1.5 1.87c-.32.4-.5.91-.5 1.43 0 .6.2 1.18.54 1.64A4.23 4.23 0 0 1 5 11.77c0-1.31.47-2.58 1.32-3.57Z" 7 | clip-rule="evenodd" 8 | ></path> 9 | </g> 10 | </svg> 11 | </template> 12 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwGift.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="25px" height="20px" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="var(--color-fill-current)"> 3 | <g> 4 | <path 5 | fill-rule="evenodd" 6 | d="M16 6h2v6h-1v6H3v-6H2V6h2V4.793c0-2.507 3.03-3.762 4.803-1.99.131.131.249.275.352.429L10 4.5l.845-1.268a2.81 2.81 0 01.352-.429C12.969 1.031 16 2.286 16 4.793V6zM6 4.793V6h2.596L7.49 4.341A.814.814 0 006 4.793zm8 0V6h-2.596l1.106-1.659a.814.814 0 011.49.451zM16 8v2h-5V8h5zm-1 8v-4h-4v4h4zM9 8v2H4V8h5zm0 4H5v4h4v-4z" 7 | clip-rule="evenodd" 8 | ></path> 9 | </g> 10 | </svg> 11 | </template> 12 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwGreet.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="20px" height="20px" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="var(--color-fill-current)"> 3 | <g> 4 | <path 5 | fill-rule="evenodd" 6 | d="M18 2v5.646a3 3 0 0 1-1.886 2.785L13 11.677V18h-2v-5.557L5.649 14.45a1 1 0 0 0-.649.936V18H3v-2.614a3 3 0 0 1 1.947-2.809L7 11.807V9.874A4.002 4.002 0 0 1 8 2a4 4 0 0 1 1 7.874v1.183l2.639-.99 3.732-1.493A1 1 0 0 0 16 7.646V2h2ZM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4Z" 7 | clip-rule="evenodd" 8 | ></path> 9 | </g> 10 | </svg> 11 | </template> 12 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwMassGift.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <img 3 | alt="mystery gift" 4 | class="mystery-gift-theme__image" 5 | src="https://static-cdn.jtvnw.net/subs-image-assets/gift-illus.png" 6 | /> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwPrime.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="20px" height="20px" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="var(--color-fill-brand)"> 3 | <g> 4 | <path 5 | fill-rule="evenodd" 6 | clip-rule="evenodd" 7 | d="M18 5v8a2 2 0 0 1-2 2H4a2.002 2.002 0 0 1-2-2V5l4 3 4-4 4 4 4-3z" 8 | ></path> 9 | </g> 10 | </svg> 11 | </template> 12 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwReply.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="1em" height="1em" version="1.1" viewBox="0 0 20 20" x="0px" y="0px"> 3 | <path 4 | d="M8.5 5.5L7 4L2 9L7 14L8.5 12.5L6 10H10C12.2091 10 14 11.7909 14 14V16H16V14C16 10.6863 13.3137 8 10 8H6L8.5 5.5Z" 5 | ></path> 6 | </svg> 7 | </template> 8 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwStar.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg width="20px" height="20px" version="1.1" viewBox="0 0 20 20" x="0px" y="0px" fill="var(--color-fill-current)"> 3 | <g> 4 | <path 5 | d="M8.944 2.654c.406-.872 1.706-.872 2.112 0l1.754 3.77 4.2.583c.932.13 1.318 1.209.664 1.853l-3.128 3.083.755 4.272c.163.92-.876 1.603-1.722 1.132L10 15.354l-3.579 1.993c-.846.47-1.885-.212-1.722-1.132l.755-4.272L2.326 8.86c-.654-.644-.268-1.723.664-1.853l4.2-.583 1.754-3.77z" 6 | ></path> 7 | </g> 8 | </svg> 9 | </template> 10 | -------------------------------------------------------------------------------- /src/assets/svg/twitch/TwSus.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <svg 3 | width="1em" 4 | height="1em" 5 | version="1.1" 6 | viewBox="0 0 20 20" 7 | x="0px" 8 | y="0px" 9 | class="ScIconSVG-sc-1q25cff-1 dSicFr" 10 | > 11 | <g> 12 | <path d="M9 11v2h2v-2H9Z"></path> 13 | <path 14 | fill-rule="evenodd" 15 | clip-rule="evenodd" 16 | d="M5.003 3.947A10 10 0 0 0 9.519 2.32L10 2l.48.32A10 10 0 0 0 16.029 4H17l-.494 5.641a9 9 0 0 1-4.044 6.751L10 18l-2.462-1.608a9 9 0 0 1-4.044-6.75L3 4h.972c.346 0 .69-.018 1.031-.053Zm.174 1.992.309 3.528a7 7 0 0 0 3.146 5.25l1.368.894 1.368-.893a7 7 0 0 0 3.146-5.25l.309-3.529A12 12 0 0 1 11 4.896V9H9V4.896a12 12 0 0 1-3.823 1.043Z" 17 | ></path> 18 | </g> 19 | </svg> 20 | </template> 21 | -------------------------------------------------------------------------------- /src/background/messaging.ts: -------------------------------------------------------------------------------- 1 | // Handle messaging from downstream 2 | 3 | let shouldReloadOnUpdate = false; 4 | 5 | chrome.runtime.onMessage.addListener((msg, _, reply) => { 6 | switch (msg.type) { 7 | case "permission-request": { 8 | const { id, origins, permissions } = msg.data; 9 | 10 | chrome.permissions.request({ origins, permissions }, (granted) => { 11 | reply({ granted, id }); 12 | 13 | if (!granted) return; 14 | 15 | chrome.runtime.sendMessage({ 16 | type: "permission-granted", 17 | data: { id }, 18 | }); 19 | }); 20 | break; 21 | } 22 | case "update-check": { 23 | if (typeof chrome.runtime.requestUpdateCheck !== "function") return; 24 | shouldReloadOnUpdate = true; 25 | 26 | /* // sim: 27 | reply({ 28 | status: "update_available", 29 | version: "3.0.0.12200", 30 | done: false, 31 | }); 32 | 33 | setTimeout(() => { 34 | if (!shouldReloadOnUpdate) return; 35 | 36 | broadcastMessage("update-ready", { 37 | version: "3.0.0.12200", 38 | }); 39 | 40 | setTimeout(() => chrome.runtime.reload(), 50); 41 | }, 1500); 42 | return; 43 | /// end sim */ 44 | 45 | chrome.runtime.requestUpdateCheck((status, details) => { 46 | reply({ 47 | status, 48 | version: details?.version ?? null, 49 | }); 50 | }); 51 | 52 | break; 53 | } 54 | } 55 | 56 | return true; 57 | }); 58 | 59 | chrome.runtime.onUpdateAvailable.addListener((details) => { 60 | if (!shouldReloadOnUpdate) return; 61 | 62 | // Notify page script to reload trigger a reload immediately 63 | broadcastMessage("update-ready", { version: details.version }); 64 | 65 | // Reload extension after a tiny delay to allow the downstream message to be sent 66 | setTimeout(() => chrome.runtime.reload(), 50); 67 | }); 68 | 69 | function broadcastMessage(type: string, data: unknown): void { 70 | chrome.tabs.query({}, (tabs) => { 71 | tabs.forEach((tab) => { 72 | if (!tab.id) return; 73 | 74 | chrome.tabs.sendMessage(tab.id, { type, data }); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /src/background/sync.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/db/idb"; 2 | import { liveQuery } from "dexie"; 3 | 4 | const settings = [] as SevenTV.Setting<SevenTV.SettingType>[]; 5 | 6 | async function main() { 7 | await db.ready(); 8 | 9 | // TODO 10 | // IDB Syncing Mechanism 11 | // 12 | // This synchronizes settings across sites 13 | liveQuery(() => db.settings.toArray()).subscribe({ 14 | next(value) { 15 | settings.length = 0; 16 | settings.push(...value); 17 | }, 18 | }); 19 | } 20 | 21 | chrome.tabs.onUpdated.addListener((id, i) => { 22 | if (i.status !== "complete") return; 23 | 24 | // wait a few seconds then send extension-stored settings 25 | // the site will decide if these nodes are more up to date than its own set 26 | setTimeout(() => { 27 | if (typeof id !== "number") return; 28 | 29 | chrome.tabs.sendMessage(id, { 30 | type: "settings-sync", 31 | data: { settings }, 32 | }); 33 | }, 5000); 34 | }); 35 | 36 | main(); 37 | -------------------------------------------------------------------------------- /src/common/Constant.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey, Ref } from "vue"; 2 | 3 | export const APP_BROADCAST_CHANNEL = "seventv-app-broadcast-channel"; 4 | 5 | export const LOCAL_STORAGE_KEYS = { 6 | WORKER_ADDR: "seventv_worker_addr", 7 | SEEN_SETTINGS: "seventv_seen_settings", 8 | APP_TOKEN: "seventv_app_token", 9 | }; 10 | 11 | export const REACT_TYPEOF_TOKEN = "$$typeof"; 12 | 13 | export const SITE_CURRENT_PLATFORM: InjectionKey<Platform> = Symbol("seventv-site-current-platform"); 14 | export const SITE_NAV_PATHNAME: InjectionKey<Ref<string>> = Symbol("seventv-site-nav-pathname"); 15 | export const SITE_WORKER_URL: InjectionKey<string> = Symbol("seventv-site-worker-url"); 16 | export const SITE_ASSETS_URL: InjectionKey<string> = Symbol("seventv-site-assets-url"); 17 | export const SITE_EXT_OPTIONS_URL: InjectionKey<string> = Symbol("seventv-site-ext-options-url"); 18 | export const SITE_ACTIVE_WINDOW: InjectionKey<Window> = Symbol("seventv-site-active-window"); 19 | 20 | export const UNICODE_TAG_0 = "\u{E0000}"; 21 | export const UNICODE_TAG_0_REGEX = new RegExp(UNICODE_TAG_0, "g"); 22 | 23 | export const TWITCH_PROFILE_IMAGE_REGEX = /(\d+x\d+)(?=\.\w{3,4}$)/; 24 | 25 | export const HOSTNAME_SUPPORTED_REGEXP = /([a-z0-9]+[.])*(youtube|kick)[.]com/; 26 | export const SEVENTV_EMOTE_LINK = new RegExp( 27 | "https?:\\/\\/(?:www\\.)?7tv.app\\/emotes\\/(?<emoteID>[0-7][0-9A-HJKMNP-TV-Z]{25})", 28 | "i", 29 | ); 30 | export const SEVENTV_EMOTE_ID = new RegExp("[0-9a-f]{24}"); 31 | export const SEVENTV_EMOTE_NAME_REGEXP = new RegExp("^[-_A-Za-z():0-9]{2,100}$"); 32 | -------------------------------------------------------------------------------- /src/common/Decode.ts: -------------------------------------------------------------------------------- 1 | export function decodeYoutubeParams(v: string): string { 2 | return ( 3 | atob(decodeURIComponent(atob(v))) 4 | .split("*'\n\u0018")?.[1] 5 | .split("\u0012\u000b")?.[0] ?? "" 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /src/common/EventTarget.ts: -------------------------------------------------------------------------------- 1 | export type TypedEventListenerOrEventListenerObject<T extends Event> = 2 | | TypedEventListener<T> 3 | | TypedEventListenerObject<T>; 4 | 5 | interface TypedEventListener<T extends Event> { 6 | (evt: T): void; 7 | } 8 | 9 | interface TypedEventListenerObject<T extends Event> { 10 | handleEvent(object: T): void; 11 | } 12 | -------------------------------------------------------------------------------- /src/common/Flags.ts: -------------------------------------------------------------------------------- 1 | export const EmoteSetFlags = { 2 | Immutable: 1 << 0, 3 | Privileged: 1 << 1, 4 | Personal: 1 << 2, 5 | Commercial: 1 << 3, 6 | }; 7 | 8 | export const ActiveEmoteFlags = { 9 | ZeroWidth: 1 << 0, 10 | Pending: 1 << 8, 11 | OverrideTwitchGlobal: 1 << 16, 12 | OverrideTwitchSubscriber: 1 << 17, 13 | OverrideBetterTTV: 1 << 18, 14 | OverrideFrankerFaceZ: 1 << 19, 15 | }; 16 | 17 | class BitFieldOps<T extends Record<string, number>> { 18 | constructor( 19 | private e: T, 20 | public sum: number, 21 | ) {} 22 | 23 | has(bit: keyof T): boolean { 24 | return (this.sum & this.e[bit]) === this.e[bit]; 25 | } 26 | 27 | add(bit: keyof T): this { 28 | this.sum |= this.e[bit]; 29 | return this; 30 | } 31 | 32 | remove(bit: keyof T): this { 33 | this.sum &= ~this.e[bit]; 34 | return this; 35 | } 36 | 37 | toggle(bit: keyof T): this { 38 | this.sum ^= this.e[bit]; 39 | return this; 40 | } 41 | 42 | valueOf(): number { 43 | return this.sum; 44 | } 45 | } 46 | 47 | export function BitField<T extends Record<string, number>>(e: T, n: number): BitFieldOps<T> { 48 | return new BitFieldOps(e, n); 49 | } 50 | -------------------------------------------------------------------------------- /src/common/Input.ts: -------------------------------------------------------------------------------- 1 | export function getSearchRange(text: string, position: number): [number, number] { 2 | let start = 0; 3 | let end = 0; 4 | 5 | for (let i = position; ; i--) { 6 | if (i < 1 || (text.charAt(i - 1) === " " && i !== position)) { 7 | start = i; 8 | break; 9 | } 10 | } 11 | 12 | for (let i = position + 1; ; i++) { 13 | if (i > text.length || text.charAt(i - 1) === " ") { 14 | end = i - 1; 15 | break; 16 | } 17 | } 18 | 19 | return [start, end]; 20 | } 21 | 22 | export interface TabToken { 23 | token: string; 24 | priority: number; 25 | item?: SevenTV.ActiveEmote; 26 | } 27 | -------------------------------------------------------------------------------- /src/common/Jwt.ts: -------------------------------------------------------------------------------- 1 | export function decodeJWT(token: string): SevenTV.JWT | undefined { 2 | try { 3 | return JSON.parse(atob(token.split(".")[1])); 4 | } catch { 5 | return; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/common/Logger.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | private static instance: Logger; 3 | 4 | pipe: ((type: LogType, text: string[], extraCSS: string[], objects?: object[]) => void) | null = null; 5 | 6 | static Get(): Logger { 7 | return this.instance ?? (Logger.instance = new Logger()); 8 | } 9 | 10 | private ctx = "context unset"; 11 | 12 | private getPrefix() { 13 | return { 14 | text: "%c[7TV]", 15 | css: "color:#ef9234;", 16 | }; 17 | } 18 | 19 | private print(type: LogType, text: string[], extraCSS: string[], objects?: object[]): void { 20 | if (this.pipe) { 21 | this.pipe(type, text, extraCSS, objects); 22 | return; 23 | } 24 | 25 | const prefix = this.getPrefix(); 26 | // eslint-disable-next-line no-console 27 | console[type]( 28 | prefix.text + " " + text.join(" ") + ` (${this.ctx})`, 29 | prefix.css, 30 | ...extraCSS, 31 | ...(objects ?? []), 32 | ); 33 | } 34 | 35 | setContextName(name: string): void { 36 | this.ctx = name; 37 | } 38 | 39 | debug(...text: string[]): void { 40 | return this.print("debug", ["%c[DEBUG]%c", ...text], ["color:#32c8e6;", "color:grey"]); 41 | } 42 | 43 | debugWithObjects(text: string[], objects: object[]): void { 44 | return this.print("debug", ["%c[DEBUG]%c", ...text], ["color:#32c8e6;", "color:grey"], objects); 45 | } 46 | 47 | info(...text: string[]): void { 48 | return this.print("info", ["%c[INFO]%c", ...text], ["color:#3cf051;", "color:reset;"]); 49 | } 50 | 51 | warn(...text: string[]): void { 52 | return this.print("warn", ["%c[WARN]%c", ...text], ["color:#fac837;", "color:reset;"]); 53 | } 54 | 55 | error(...text: string[]): void { 56 | return this.print("error", ["%c[ERROR]%c", ...text], ["color:#e63232;", "color:reset;"]); 57 | } 58 | } 59 | 60 | export const log = new Logger(); 61 | 62 | export type LogType = "error" | "warn" | "debug" | "info"; 63 | -------------------------------------------------------------------------------- /src/common/Rand.ts: -------------------------------------------------------------------------------- 1 | export function getRandomInt(min: number, max: number) { 2 | min = Math.ceil(min); 3 | max = Math.floor(max); 4 | return Math.floor(Math.random() * (max - min + 1)) + min; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/Roles.ts: -------------------------------------------------------------------------------- 1 | export enum SevenTVRoles { 2 | ADMIN = "01FBQX1CXG000AP6N15FV4HKYD", 3 | MODERATOR = "01F2Z8C8M8000EJFC2HFW8B1W9", 4 | } 5 | -------------------------------------------------------------------------------- /src/common/type-predicates/MessageTokens.ts: -------------------------------------------------------------------------------- 1 | import { AnyToken, EmoteToken, LinkToken, MentionToken, TextToken } from "../chat/ChatMessage"; 2 | 3 | export function IsTextToken(part: AnyToken): part is TextToken { 4 | return part.kind === "TEXT"; 5 | } 6 | 7 | export function IsLinkToken(part: AnyToken): part is LinkToken { 8 | return part.kind === "LINK"; 9 | } 10 | 11 | export function IsEmoteToken(part: AnyToken): part is EmoteToken { 12 | return part.kind === "EMOTE"; 13 | } 14 | 15 | export function IsMentionToken(part: AnyToken): part is MentionToken { 16 | return part.kind === "MENTION"; 17 | } 18 | -------------------------------------------------------------------------------- /src/common/type-predicates/Messages.ts: -------------------------------------------------------------------------------- 1 | export function IsChatMessage(msg: Twitch.AnyMessage): msg is Twitch.ChatMessage { 2 | const x = msg as Twitch.ChatMessage; 3 | 4 | return Array.isArray(x.messageParts) && typeof x.messageType === "number"; 5 | } 6 | 7 | export function IsDisplayableMessage(msg: Twitch.AnyMessage): msg is Twitch.DisplayableMessage { 8 | const x = msg as Twitch.DisplayableMessage; 9 | 10 | return Array.isArray(x.messageParts) || typeof x.message === "object"; 11 | } 12 | 13 | export function IsLowTrustUserMessage(msg: Twitch.AnyMessage): msg is Twitch.RestrictedLowTrustUserMessage { 14 | const x = msg as Twitch.RestrictedLowTrustUserMessage; 15 | 16 | return typeof x.sender === "string" && typeof x.sent_at === "string"; 17 | } 18 | 19 | export function IsModerationMessage(msg: Twitch.AnyMessage): msg is Twitch.ModerationMessage { 20 | const x = msg as Twitch.ModerationMessage; 21 | 22 | return typeof x.moderationType === "number"; 23 | } 24 | 25 | export function IsSubMessage(msg: Twitch.AnyMessage): msg is Twitch.SubMessage { 26 | const x = msg as Twitch.SubMessage; 27 | 28 | return typeof x.methods === "object" && typeof x.user === "string" && typeof x.months === "number"; 29 | } 30 | -------------------------------------------------------------------------------- /src/composable/chat/useChatProperties.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { ChannelContext } from "../channel/useChannelContext"; 3 | 4 | interface ChatProperties { 5 | fontAprilFools: string; 6 | isDarkTheme: number; 7 | primaryColorHex: string | null; 8 | useHighContrastColors: boolean; 9 | showTimestamps: boolean; 10 | showModerationIcons: boolean; 11 | hovering: boolean; 12 | pauseReason: Set<ChatPauseReason>; 13 | currentChannel: CurrentChannel; 14 | imageFormat: SevenTV.ImageFormat; 15 | twitchBadgeSets: Twitch.BadgeSets | null; 16 | blockedUsers: Set<string>; 17 | } 18 | 19 | type ChatPauseReason = "MOUSEOVER" | "SCROLL" | "ALTKEY"; 20 | 21 | const m = new WeakMap<ChannelContext, ChatProperties>(); 22 | 23 | export function useChatProperties(ctx: ChannelContext) { 24 | let data = m.get(ctx); 25 | if (!data) { 26 | data = reactive<ChatProperties>({ 27 | isDarkTheme: 1, 28 | primaryColorHex: null as string | null, 29 | useHighContrastColors: true, 30 | showTimestamps: false, 31 | showModerationIcons: false, 32 | hovering: false, 33 | pauseReason: new Set<ChatPauseReason>(["SCROLL"]), 34 | currentChannel: {} as CurrentChannel, 35 | imageFormat: "WEBP" as SevenTV.ImageFormat, 36 | twitchBadgeSets: {} as Twitch.BadgeSets | null, 37 | blockedUsers: new Set<string>(), 38 | fontAprilFools: "unset", 39 | }); 40 | 41 | m.set(ctx, data); 42 | } 43 | 44 | return data; 45 | } 46 | -------------------------------------------------------------------------------- /src/composable/chat/useChatTools.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { ChannelContext } from "../channel/useChannelContext"; 3 | 4 | interface ChatTools { 5 | TWITCH: { 6 | onShowViewerCard: Twitch.ViewerCardComponent["onShowViewerCard"]; 7 | onShowViewerWarnPopover: ( 8 | userId: string, 9 | userLogin: string, 10 | placement: Twitch.WarnUserPopoverPlacement, 11 | ) => void; 12 | }; 13 | YOUTUBE: Record<string, never>; 14 | KICK: Record<string, never>; 15 | UNKNOWN: Record<string, never>; 16 | } 17 | 18 | const m = new WeakMap<ChannelContext, ChatTools>(); 19 | 20 | export function useChatTools(ctx: ChannelContext) { 21 | let data = m.get(ctx); 22 | if (!data) { 23 | data = reactive<ChatTools>({ 24 | TWITCH: { 25 | onShowViewerCard: () => void 0, 26 | onShowViewerWarnPopover: () => void 0, 27 | }, 28 | YOUTUBE: {}, 29 | KICK: {}, 30 | UNKNOWN: {}, 31 | }); 32 | 33 | m.set(ctx, data); 34 | } 35 | 36 | function update<P extends Platform>(platform: P, key: keyof ChatTools[P], value: ChatTools[P][keyof ChatTools[P]]) { 37 | if (!data) return; 38 | 39 | data[platform][key] = value; 40 | } 41 | 42 | function openViewerCard(e: MouseEvent, username: string, msgID: string) { 43 | if (!data || !e || !e.currentTarget || !username) return false; 44 | 45 | const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); 46 | data[ctx.platform].onShowViewerCard(username, 0, msgID, rect.bottom); 47 | return true; 48 | } 49 | 50 | function openViewerWarnPopover(userId: string, userLogin: string, placement: Twitch.WarnUserPopoverPlacement) { 51 | if (!data || !userId || !userLogin) return false; 52 | 53 | data[ctx.platform].onShowViewerWarnPopover(userId, userLogin, placement); 54 | return true; 55 | } 56 | 57 | return { 58 | update, 59 | openViewerCard, 60 | openViewerWarnPopover, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/composable/useApollo.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import type { ApolloClient } from "@apollo/client"; 3 | 4 | const client = ref<ApolloClient<object> | null>(null); 5 | 6 | export function useApollo() { 7 | return client; 8 | } 9 | -------------------------------------------------------------------------------- /src/composable/useCookies.ts: -------------------------------------------------------------------------------- 1 | class CookieMap extends Map<string, string> { 2 | refresh(): void { 3 | super.clear(); 4 | 5 | const cookies = document.cookie.split(";").map((cookie) => cookie.trim()); 6 | 7 | for (const cookie of cookies) { 8 | const [key, value] = cookie.split("="); 9 | super.set(key, decodeURIComponent(value)); 10 | } 11 | } 12 | 13 | get(key: string): string | undefined { 14 | this.refresh(); 15 | 16 | return super.get(key); 17 | } 18 | } 19 | 20 | const cookieMap = new CookieMap(); 21 | 22 | export function useCookies() { 23 | cookieMap.refresh(); 24 | 25 | return cookieMap; 26 | } 27 | -------------------------------------------------------------------------------- /src/composable/useEmoji.ts: -------------------------------------------------------------------------------- 1 | import { inject, reactive } from "vue"; 2 | import { SITE_ASSETS_URL } from "@/common/Constant"; 3 | 4 | export interface Emoji { 5 | codes: string; 6 | char: string; 7 | name: string; 8 | category: string; 9 | group: string; 10 | subgroup: string; 11 | emote: SevenTV.ActiveEmote; 12 | } 13 | 14 | const emojiByName = new Map<string, Emoji>(); 15 | const emojiByCode = new Map<string, Emoji>(); 16 | 17 | const cached = [] as Emoji[]; 18 | export async function loadEmojiList() { 19 | if (cached.length) return cached; 20 | 21 | const assetsBase = inject(SITE_ASSETS_URL, ""); 22 | const data = (await (await fetch(assetsBase + "/emoji/emoji.json")).json().catch(() => void 0)) as Emoji[]; 23 | 24 | for (const e of data) { 25 | const emoji = e as Emoji; 26 | 27 | emoji.emote = { 28 | id: emoji.codes, 29 | name: emoji.name, 30 | unicode: emoji.char, 31 | provider: "EMOJI", 32 | flags: 0, 33 | } as SevenTV.ActiveEmote; 34 | 35 | emojiByName.set(emoji.name, emoji); 36 | emojiByCode.set(emoji.char, emoji); 37 | } 38 | 39 | cached.push(...data); 40 | } 41 | 42 | export function useEmoji() { 43 | return reactive({ 44 | emojiList: cached, 45 | emojiByCode, 46 | emojiByName, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/composable/useLiveQuery.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from "vue"; 2 | import { MaybeRef, tryOnUnmounted } from "@vueuse/core"; 3 | import { liveQuery } from "dexie"; 4 | 5 | export function useLiveQuery<T>( 6 | queryFn: () => T | Promise<T | undefined> | undefined, 7 | onResult?: (result: T) => void, 8 | opt: LiveQueryOptions = {}, 9 | ) { 10 | const value = ref<T>(); 11 | 12 | let queryStop = () => {}; 13 | let watchStop = () => {}; 14 | const stop = () => { 15 | queryStop(); 16 | watchStop(); 17 | }; 18 | 19 | const handleResult = (result: T | undefined) => { 20 | if (!result) return; 21 | if (typeof opt.count === "number" && opt.count-- <= 0) { 22 | stop(); 23 | } 24 | 25 | value.value = result; 26 | onResult?.(result); 27 | }; 28 | 29 | queryStop = liveQuery(queryFn).subscribe(handleResult).unsubscribe; 30 | 31 | if (opt.reactives) { 32 | watchStop = watch(opt.reactives, () => { 33 | queryStop(); 34 | queryStop = liveQuery(queryFn).subscribe(handleResult).unsubscribe; 35 | }); 36 | } 37 | 38 | tryOnUnmounted(stop); 39 | 40 | opt.until?.then(stop); 41 | 42 | return value; 43 | } 44 | 45 | export interface LiveQueryOptions { 46 | count?: number; 47 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 48 | reactives?: MaybeRef<any>[]; 49 | until?: Promise<boolean>; 50 | } 51 | -------------------------------------------------------------------------------- /src/composable/useSound.ts: -------------------------------------------------------------------------------- 1 | import { tryOnUnmounted } from "@vueuse/core"; 2 | 3 | const soundMap = new Map<string, HTMLAudioElement>(); 4 | 5 | /** 6 | * Play a sound 7 | * 8 | * @param path the path to the sound file, prepended by the assets URL set by the loader 9 | * @param reuse whether to reuse the audio element for the same path 10 | */ 11 | export function useSound(path: string, reuse = true) { 12 | const existingAudioElement = soundMap.get(path); 13 | const audio = reuse && existingAudioElement ? existingAudioElement : new Audio(); 14 | 15 | const play = (volume: number = 1) => { 16 | if (reuse && existingAudioElement) { 17 | existingAudioElement.volume = volume; 18 | existingAudioElement.play(); 19 | return; 20 | } 21 | 22 | fetchAudio(path).then((blobUrl) => { 23 | audio.src = blobUrl; 24 | audio.volume = volume; 25 | audio.play(); 26 | soundMap.set(path, audio); 27 | }); 28 | }; 29 | 30 | const stop = () => audio.pause(); 31 | 32 | tryOnUnmounted(() => { 33 | audio.remove(); 34 | }); 35 | 36 | return { play, stop, audio }; 37 | } 38 | 39 | async function fetchAudio(path: string): Promise<string> { 40 | const response = await fetch(path); 41 | if (!response.ok) { 42 | throw new Error(`HTTP error: ${response.status} - ${response.statusText}`); 43 | } 44 | return URL.createObjectURL(await response.blob()); 45 | } 46 | 47 | export interface Sound { 48 | play: (volume?: number) => void; 49 | stop: () => void; 50 | audio: HTMLAudioElement; 51 | } 52 | -------------------------------------------------------------------------------- /src/composable/useTooltip.ts: -------------------------------------------------------------------------------- 1 | import { Component, markRaw, nextTick, reactive } from "vue"; 2 | import { Placement, computePosition, shift } from "@floating-ui/dom"; 3 | 4 | export const tooltip = reactive({ 5 | x: 0, 6 | y: 0, 7 | content: null as Component | string | null, 8 | contentProps: {} as Record<string, unknown>, 9 | container: null as HTMLElement | null, 10 | }); 11 | 12 | /** 13 | * useTooltip() is a composable function to display tooltips efficiently 14 | * 15 | * @param content text, or a component, to display as the tooltip content 16 | * @param props if content is a component, these are the props to pass to it 17 | * @returns 18 | */ 19 | export function useTooltip(content?: string | Component, props?: Record<string, unknown>, opt?: TooltipOptions) { 20 | // this shows the tooltip 21 | function show(el: HTMLElement | undefined): void { 22 | if (!el) return; 23 | 24 | // Set the content, this is necessary to calculate the tooltip's positioning 25 | if (content !== undefined) { 26 | tooltip.content = typeof content !== "string" ? markRaw(content) : content === "" ? null : content; 27 | tooltip.contentProps = props ?? {}; 28 | } 29 | 30 | // on the next tick we will update the position of the tooltip container 31 | nextTick(() => { 32 | computePosition(el, tooltip.container as HTMLElement, { 33 | placement: opt?.placement ?? "top", 34 | middleware: [shift({ padding: 8, crossAxis: true, mainAxis: true })], 35 | }).then(({ x: xVal, y: yVal }) => { 36 | tooltip.x = xVal; 37 | tooltip.y = yVal; 38 | }); 39 | }); 40 | } 41 | 42 | // this hides the tooltip 43 | function hide(): void { 44 | tooltip.content = null; 45 | tooltip.contentProps = {}; 46 | } 47 | 48 | return { show, hide }; 49 | } 50 | 51 | interface TooltipOptions { 52 | placement?: Placement; 53 | } 54 | -------------------------------------------------------------------------------- /src/composable/useUserAgent.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import { IBrowser, UAParser, UAParserInstance } from "ua-parser-js"; 3 | 4 | interface UserAgentHelper { 5 | agent: UAParserInstance; 6 | browser: IBrowser; 7 | avif: boolean; 8 | preferredFormat: SevenTV.ImageFormat; 9 | } 10 | 11 | const agent = new UAParser(); 12 | const browser = agent.getBrowser(); 13 | const data = reactive<UserAgentHelper>({ 14 | agent, 15 | browser, 16 | avif: 17 | (browser.name === "Chrome" && parseInt(browser.version as string, 10) >= 100) || 18 | (browser.name === "Firefox" && parseInt(browser.version as string, 10) >= 113), 19 | preferredFormat: "WEBP", 20 | }); 21 | 22 | export function useUserAgent() { 23 | return data; 24 | } 25 | -------------------------------------------------------------------------------- /src/content/emoji.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inserts the emoji vectors into the DOM. 3 | */ 4 | export async function insertEmojiVectors(): Promise<void> { 5 | const container = document.createElement("div"); 6 | container.id = "seventv-emoji-container"; 7 | container.style.display = "none"; 8 | container.style.position = "fixed"; 9 | container.style.top = "-1px"; 10 | container.style.left = "-1px"; 11 | 12 | // Get path to emoji blocks in assets 13 | const base = chrome.runtime.getURL("assets/emoji"); 14 | const blocks = 11; 15 | 16 | for (let i = 0; i < blocks; i++) { 17 | const data = (await fetch(base + "/emojis" + i + ".svg")).text(); 18 | 19 | const element = document.createElement("div"); 20 | element.id = "emojis" + i; 21 | element.innerHTML = await data; 22 | 23 | container.appendChild(element); 24 | } 25 | 26 | document.head.appendChild(container); 27 | } 28 | -------------------------------------------------------------------------------- /src/db/versions.idb.ts: -------------------------------------------------------------------------------- 1 | import type { Dexie7 } from "./idb"; 2 | 3 | export function defineVersions(db: Dexie7) { 4 | db.version(2.4).stores({ 5 | channels: "id,timestamp", 6 | emoteSets: "id,timestamp,priority,provider,scope", 7 | emotes: "id,timestamp,name,owner.id", 8 | cosmetics: "id,timestamp", 9 | entitlements: "id,scope,timestamp,user_id", 10 | settings: "key", 11 | }); 12 | 13 | db.version(2.5).stores({ 14 | channels: "id,timestamp", 15 | emoteSets: "id,timestamp,priority,provider,scope", 16 | emotes: "id,timestamp,name,owner.id", 17 | cosmetics: "id,timestamp,kind", 18 | entitlements: "id,scope,timestamp,user_id", 19 | settings: "key", 20 | }); 21 | db.version(2.6).stores({ 22 | channels: "id,timestamp", 23 | emoteSets: "id,timestamp,priority,provider,scope", 24 | emotes: "id,timestamp,name,owner.id", 25 | cosmetics: "id,timestamp,kind", 26 | entitlements: "id,scope,timestamp,user_id,platform_id", 27 | settings: "key", 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/directive/ElementLifecycleDirective.ts: -------------------------------------------------------------------------------- 1 | import type { Directive, DirectiveBinding } from "vue"; 2 | 3 | export const ElementLifecycleDirective = { 4 | mounted(el: HTMLElement, binding: DirectiveBinding<Callback>) { 5 | const { value: cb } = binding; 6 | 7 | cb("mounted", el); 8 | }, 9 | updated(el: HTMLElement, binding: DirectiveBinding<Callback>) { 10 | const { value: cb } = binding; 11 | 12 | cb("updated", el); 13 | }, 14 | beforeUnmount(el: HTMLElement, binding: DirectiveBinding<Callback>) { 15 | const { value: cb } = binding; 16 | 17 | cb("unmounted", el); 18 | }, 19 | } as Directive<HTMLElement, Callback>; 20 | 21 | type Callback = (state: ElementLifecycle, el: HTMLElement) => void; 22 | 23 | export type ElementLifecycle = "mounted" | "updated" | "unmounted"; 24 | -------------------------------------------------------------------------------- /src/directive/TextPaintDirective.ts: -------------------------------------------------------------------------------- 1 | import type { Directive, DirectiveBinding } from "vue"; 2 | 3 | const ATTR_SEVENTV_PAINT_ID = "data-seventv-paint-id"; 4 | const ATTR_SEVENTV_TEXT = "data-seventv-painted-text"; 5 | 6 | export const TextPaintDirective = { 7 | mounted(el: HTMLElement, binding: DirectiveBinding<string | null>) { 8 | const { value: paint } = binding; 9 | 10 | updateElementStyles(el, paint); 11 | }, 12 | updated(el: HTMLElement, binding: DirectiveBinding<string | null>) { 13 | const { value: paint } = binding; 14 | 15 | updateElementStyles(el, paint); 16 | }, 17 | } as Directive<HTMLElement, string | null>; 18 | 19 | type PaintedElement = HTMLElement & { 20 | __seventv_backup_style?: { backgroundImage: string; filter: string; color: string }; 21 | }; 22 | export function updateElementStyles(el: PaintedElement, paintID: string | null): void { 23 | const hasPaint = el.hasAttribute(ATTR_SEVENTV_PAINT_ID); 24 | const newPaint = hasPaint && paintID !== el.getAttribute(ATTR_SEVENTV_PAINT_ID); 25 | 26 | if (!hasPaint) { 27 | el.__seventv_backup_style = { 28 | backgroundImage: el.style.backgroundImage, 29 | filter: el.style.filter, 30 | color: el.style.color, 31 | }; 32 | } 33 | 34 | if (hasPaint && newPaint) { 35 | const backup = el.__seventv_backup_style; 36 | el.style.backgroundImage = backup?.backgroundImage ?? ""; 37 | el.style.filter = backup?.filter ?? ""; 38 | el.style.color = backup?.color ?? ""; 39 | 40 | el.classList.remove("seventv-painted-content"); 41 | el.removeAttribute(ATTR_SEVENTV_TEXT); 42 | el.removeAttribute(ATTR_SEVENTV_PAINT_ID); 43 | } 44 | if (!paintID) return; 45 | 46 | el.classList.add("seventv-painted-content", "seventv-paint"); 47 | el.setAttribute(ATTR_SEVENTV_TEXT, "true"); 48 | el.setAttribute(ATTR_SEVENTV_PAINT_ID, paintID); 49 | } 50 | -------------------------------------------------------------------------------- /src/directive/TooltipDirective.ts: -------------------------------------------------------------------------------- 1 | import type { Directive, DirectiveBinding } from "vue"; 2 | import { useTooltip } from "@/composable/useTooltip"; 3 | import { Placement } from "@floating-ui/dom"; 4 | 5 | export const TooltipDirective = { 6 | mounted(el: HTMLElement, binding: DirectiveBinding) { 7 | handleTooltip(el, binding); 8 | }, 9 | updated(el: HTMLElement, binding: DirectiveBinding) { 10 | handleTooltip(el, binding); 11 | }, 12 | beforeUnmount() { 13 | useTooltip().hide(); 14 | }, 15 | } as Directive; 16 | 17 | function handleTooltip(el: HTMLElement, binding: DirectiveBinding) { 18 | const tooltipText = binding.value || ""; 19 | 20 | switch (binding.arg) { 21 | case "position": 22 | el.setAttribute("data-tooltip-position", binding.value); 23 | break; 24 | default: { 25 | const { show, hide } = useTooltip(tooltipText, undefined, { 26 | placement: (el.getAttribute("data-tooltip-position") as Placement) || binding.arg, 27 | }); 28 | 29 | el.addEventListener("mouseenter", () => show(el)); 30 | el.addEventListener("mouseleave", hide); 31 | break; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from "vue-i18n"; 2 | 3 | // Import locales 4 | const locale = {} as Record<string, Record<string, object>>; 5 | const importedLocales = import.meta.glob("@locale/*.yaml", { eager: true, import: "default" }); 6 | 7 | for (const [path, mod] of Object.entries(importedLocales)) { 8 | const lang = path.replace("/locale/", "").replace(".yaml", ""); 9 | locale[lang] = mod as Record<string, object>; 10 | } 11 | 12 | // Create i18n instance 13 | export function setupI18n() { 14 | const inst = createI18n({ 15 | locale: "en_US", 16 | legacy: false, 17 | globalInjection: true, 18 | messages: locale, 19 | }); 20 | 21 | return inst; 22 | } 23 | -------------------------------------------------------------------------------- /src/site/global/FloatContext.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main id="seventv-float-context"> 3 | <div v-for="screen of ctx.screens" :key="screen.sym" class="seventv-float-screen-parent"> 4 | <FloatScreen :screen="screen" @container-created="onContainerCreated" /> 5 | </div> 6 | </main> 7 | </template> 8 | 9 | <script setup lang="ts"> 10 | import { useFloatContext } from "@/composable/useFloatContext"; 11 | import FloatScreen from "./FloatScreen.vue"; 12 | 13 | const ctx = useFloatContext(); 14 | 15 | function onContainerCreated(screenID: symbol, container: Element): void { 16 | const screen = ctx.screenMap[screenID]; 17 | if (!screen) return; 18 | 19 | screen.teleportContainer = container; 20 | } 21 | </script> 22 | -------------------------------------------------------------------------------- /src/site/global/FloatScreen.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <UiFloating 3 | v-if="screen.anchor" 4 | :anchor="screen.anchor" 5 | :placement="screen.placement" 6 | :middleware="screen.middleware" 7 | > 8 | <div ref="teleportContainer" class="seventv-float-screen" /> 9 | </UiFloating> 10 | </template> 11 | 12 | <script setup lang="ts"> 13 | import { ref, watch } from "vue"; 14 | import { FloatScreen } from "@/composable/useFloatContext"; 15 | import UiFloating from "@/ui/UiFloating.vue"; 16 | 17 | const props = defineProps<{ 18 | screen: FloatScreen; 19 | }>(); 20 | 21 | const emit = defineEmits<{ 22 | (e: "container-created", sym: symbol, container: Element): void; 23 | }>(); 24 | 25 | const teleportContainer = ref<HTMLElement>(); 26 | 27 | watch(teleportContainer, (container) => { 28 | if (!container) return; 29 | 30 | emit("container-created", props.screen.sym, container); 31 | }); 32 | </script> 33 | -------------------------------------------------------------------------------- /src/site/global/Global.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <Tooltip /> 3 | <FloatContext /> 4 | <Transition name="settings-menu" appear> 5 | <SettingsMenu v-if="settingsMenuCtx.open" /> 6 | </Transition> 7 | </template> 8 | 9 | <script setup lang="ts"> 10 | import { nextTick, watch } from "vue"; 11 | import { useConfig, useSettings } from "@/composable/useSettings"; 12 | import useUpdater from "@/composable/useUpdater"; 13 | import { useWorker } from "@/composable/useWorker"; 14 | import FloatContext from "./FloatContext.vue"; 15 | import { dataSettings, globalSettings } from "./GlobalSettings"; 16 | import Tooltip from "./Tooltip.vue"; 17 | import { useSettingsMenu } from "@/app/settings/Settings"; 18 | import SettingsMenu from "@/app/settings/SettingsMenu.vue"; 19 | 20 | const settingsMenuCtx = useSettingsMenu(); 21 | 22 | const { register } = useSettings(); 23 | 24 | register(dataSettings); 25 | register(globalSettings); 26 | 27 | const updater = useUpdater(); 28 | const version = useConfig("app.version"); 29 | 30 | const { target } = useWorker(); 31 | target.addEventListener("config", (cfg) => { 32 | const { version } = cfg.detail; 33 | if (!version) return; 34 | 35 | updater.latestVersion = version; 36 | 37 | // check for updates 38 | updater.checkUpdate(); 39 | }); 40 | 41 | const stop = watch( 42 | version, 43 | (v) => { 44 | if (version.value === null || updater.runtimeVersion === v) return; 45 | 46 | version.value = updater.runtimeVersion; 47 | 48 | nextTick(() => stop()); 49 | }, 50 | { immediate: true }, 51 | ); 52 | </script> 53 | -------------------------------------------------------------------------------- /src/site/global/ModuleWrapper.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <Suspense> 3 | <component :is="mod" ref="modRef" /> 4 | </Suspense> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import { onMounted, ref } from "vue"; 9 | 10 | defineProps<{ 11 | mod: unknown; 12 | }>(); 13 | 14 | const emit = defineEmits<{ 15 | (e: "mounted", inst: InstanceType<ComponentFactory>): void; 16 | }>(); 17 | 18 | const modRef = ref(); 19 | 20 | onMounted(() => { 21 | emit("mounted", modRef.value); 22 | }); 23 | </script> 24 | -------------------------------------------------------------------------------- /src/site/global/Tooltip.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div 3 | id="seventv-tooltip-container" 4 | ref="tooltipContainer" 5 | :active="!!tooltip.content" 6 | :style="{ left: `${tooltip.x}px`, top: `${tooltip.y}px` }" 7 | > 8 | <template v-if="typeof tooltip.content === 'string'"> 9 | <span class="text-only-tooltip">{{ tooltip.content }}</span> 10 | </template> 11 | <template v-else> 12 | <component :is="tooltip.content" v-bind="tooltip.contentProps" /> 13 | </template> 14 | <div id="seventv-tooltip-arrow" ref="arrowEl" /> 15 | </div> 16 | </template> 17 | 18 | <script setup lang="ts"> 19 | import { onMounted, ref } from "vue"; 20 | import { tooltip } from "@/composable/useTooltip"; 21 | 22 | // Tooltip positioning data 23 | const tooltipContainer = ref<HTMLDivElement | null>(null); 24 | const arrowEl = ref<HTMLDivElement | null>(null); 25 | 26 | onMounted(() => { 27 | if (tooltipContainer.value) { 28 | tooltip.container = tooltipContainer.value; 29 | } 30 | }); 31 | </script> 32 | 33 | <style scoped lang="scss"> 34 | #seventv-tooltip-container { 35 | all: unset; 36 | display: grid; 37 | z-index: 99999; 38 | position: absolute; 39 | pointer-events: none; 40 | top: 0; 41 | left: 0; 42 | background-color: var(--seventv-background-transparent-2); 43 | 44 | @at-root .seventv-transparent & { 45 | backdrop-filter: blur(0.88em); 46 | } 47 | 48 | border-radius: 0.25rem; 49 | 50 | &[active="true"] { 51 | outline: 0.1em solid var(--seventv-border-transparent-1); 52 | } 53 | 54 | .text-only-tooltip { 55 | padding: 0.25rem; 56 | } 57 | } 58 | 59 | #seventv-tooltip-arrow { 60 | position: absolute; 61 | } 62 | </style> 63 | -------------------------------------------------------------------------------- /src/site/global/components/FormCheckbox.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div> 3 | <input id="checkbox" :checked="checked" :disabled="disabled" type="checkbox" @change="onInput" /> 4 | </div> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | defineProps<{ 9 | checked: boolean; 10 | disabled?: boolean; 11 | }>(); 12 | 13 | const emit = defineEmits<{ 14 | (e: "update:checked", value: boolean): void; 15 | (e: "update:modelValue", value: boolean): void; 16 | }>(); 17 | 18 | function onInput(ev: Event): void { 19 | const checked = (ev.target as HTMLInputElement).checked; 20 | 21 | emit("update:checked", checked); 22 | emit("update:modelValue", checked); 23 | } 24 | </script> 25 | -------------------------------------------------------------------------------- /src/site/global/components/FormInput.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <input 3 | ref="inputEl" 4 | :type="type" 5 | autocomplete="noidontthinkso" 6 | data-form-type="other" 7 | :error="error" 8 | :autofocus="autofocus" 9 | :value="modelValue" 10 | :placeholder="label" 11 | @input="onInput" 12 | @blur="emit('blur')" 13 | /> 14 | </template> 15 | 16 | <script setup lang="ts"> 17 | import { onMounted, ref } from "vue"; 18 | 19 | const props = withDefaults( 20 | defineProps<{ 21 | label?: string; 22 | modelValue?: string | number; 23 | type?: "text" | "password" | "email" | "number" | "url"; 24 | icon?: string; 25 | error?: boolean; 26 | width?: string; 27 | appearance?: "flat" | "outline"; 28 | autofocus?: boolean; 29 | }>(), 30 | { 31 | type: "text", 32 | appearance: "outline", 33 | }, 34 | ); 35 | 36 | const emit = defineEmits<{ 37 | (e: "update:modelValue", value: string | number): void; 38 | (e: "blur"): void; 39 | }>(); 40 | 41 | const onInput = (event: Event) => emit("update:modelValue", (event.target as HTMLInputElement).value); 42 | 43 | const inputEl = ref<HTMLInputElement | null>(null); 44 | 45 | defineExpose({ 46 | focus: () => inputEl.value?.focus(), 47 | }); 48 | 49 | onMounted(() => { 50 | if (inputEl.value && props.autofocus) { 51 | inputEl.value.focus(); 52 | } 53 | }); 54 | </script> 55 | 56 | <style scoped lang="scss"> 57 | input { 58 | all: unset; 59 | width: v-bind(width); 60 | transition: border-color 140ms ease-in-out; 61 | background-color: transparent; 62 | border-bottom: 0.1rem solid var(--seventv-input-border); 63 | color: inherit; 64 | padding: 0.5rem; 65 | font-size: 1em; 66 | font-weight: 500; 67 | 68 | &:focus { 69 | outline: unset; 70 | border-color: var(--seventv-primary); 71 | 72 | & ~ label { 73 | font-weight: 600; 74 | transform: translateY(-1.35em) translateX(-0.25em) scale(0.8); 75 | } 76 | } 77 | } 78 | </style> 79 | -------------------------------------------------------------------------------- /src/site/kick.com/composable/useUserdata.ts: -------------------------------------------------------------------------------- 1 | export async function useUserdata( 2 | auth: string, 3 | sessionToken: string | undefined, 4 | ): Promise<{ 5 | id: number; 6 | username: string; 7 | bio: string; 8 | email: string; 9 | streamer_channel: { 10 | slug: string; 11 | }; 12 | discord?: string; 13 | facebook?: string; 14 | twitter?: string; 15 | youtube?: string; 16 | tiktok?: string; 17 | instagram?: string; 18 | }> { 19 | const data = await fetch("https://kick.com/api/v1/user", { 20 | headers: { 21 | Authorization: "Bearer " + sessionToken, 22 | "X-XSRF-TOKEN": auth, 23 | }, 24 | method: "GET", 25 | }); 26 | 27 | return await data.json(); 28 | } 29 | -------------------------------------------------------------------------------- /src/site/kick.com/index.ts: -------------------------------------------------------------------------------- 1 | import { InjectionKey } from "vue"; 2 | 3 | export interface ChatRoom { 4 | chatroom: ChatRoomData | null; 5 | currentChannelSlug: string; 6 | currentMessage: string; 7 | } 8 | 9 | export interface ChatRoomData { 10 | id: number; 11 | } 12 | 13 | export interface KickChannelInfo { 14 | active: boolean; 15 | slug: string; 16 | currentMessage: string; 17 | } 18 | 19 | export const KICK_CHANNEL_KEY = Symbol() as InjectionKey<KickChannelInfo>; 20 | -------------------------------------------------------------------------------- /src/site/kick.com/modules/auth/Auth.ts: -------------------------------------------------------------------------------- 1 | import { useCookies } from "@/composable/useCookies"; 2 | 3 | const tokenWrapRegexp = /\[7TV:[0-9a-fA-F]+\]/g; 4 | 5 | export async function setBioCode(identity: KickIdentity, code: string, cookies: ReturnType<typeof useCookies>) { 6 | if (!identity) return; 7 | 8 | const tokenWrap = `[7TV:${code}]`; 9 | 10 | const auth = cookies.get("XSRF-TOKEN"); 11 | if (!auth) return; 12 | 13 | const headers = new Headers(); 14 | headers.set("x-xsrf-token", auth ?? ""); 15 | headers.set("Content-Type", "application/json"); 16 | 17 | const cleanBio = identity.bio?.replace(tokenWrapRegexp, "").trim() ?? ""; 18 | const newBio = code ? (identity.bio ? `${cleanBio} ${tokenWrap}` : tokenWrap) : cleanBio; 19 | 20 | return fetch("https://kick.com/update_profile", { 21 | headers: { 22 | accept: "application/json, text/plain, */*", 23 | "accept-language": "en-US", 24 | "content-type": "application/json", 25 | "x-xsrf-token": auth, 26 | }, 27 | referrer: "https://kick.com/dashboard/settings/profile", 28 | referrerPolicy: "strict-origin-when-cross-origin", 29 | body: JSON.stringify({ 30 | id: identity.numID, 31 | email: identity.email, 32 | bio: newBio, 33 | discord: identity.discord, 34 | facebook: identity.facebook, 35 | twitter: identity.twitter, 36 | youtube: identity.youtube, 37 | tiktok: identity.tiktok, 38 | instagram: identity.instagram, 39 | }), 40 | method: "POST", 41 | mode: "cors", 42 | }).then((resp) => { 43 | if (!resp.ok) return; 44 | 45 | identity.bio = newBio; 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/site/kick.com/modules/chat/ChatModule.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <Suspense> 3 | <ChatController ref="controller" :channel-id="channelID" :slug="slug" /> 4 | </Suspense> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import { ref, watch } from "vue"; 9 | import { Logger } from "@/common/Logger"; 10 | import { useElementFiberHook } from "@/common/ReactHooks"; 11 | import { useChannelContext } from "@/composable/channel/useChannelContext"; 12 | import { declareModule } from "@/composable/useModule"; 13 | import ChatController from "./ChatController.vue"; 14 | 15 | const { markAsReady } = declareModule<"KICK">("chat", { 16 | name: "Chat", 17 | depends_on: [], 18 | }); 19 | 20 | const slug = ref(""); 21 | const channelID = ref(""); 22 | useChannelContext(channelID.value); 23 | watch( 24 | slug, 25 | async (v) => { 26 | if (!v) return; 27 | const resp = await fetch(`https://kick.com/api/v2/channels/${v}`).catch((err) => { 28 | Logger.Get().error("failed to fetch channel data", err); 29 | }); 30 | if (!resp) return; 31 | const { user_id: id } = await resp.json(); 32 | if (!id) return; 33 | channelID.value = id.toString() as string; 34 | }, 35 | { immediate: true }, 36 | ); 37 | 38 | useElementFiberHook<{ channelSlug: string }>( 39 | { 40 | childSelector: "#chatroom-messages", 41 | maxDepth: 5, 42 | predicate: (n) => !!n.memoizedProps?.channelSlug, 43 | }, 44 | { 45 | hooks: { 46 | render(old, props, ref) { 47 | slug.value = props.channelSlug; 48 | 49 | return old?.call(this, props, ref) ?? null; 50 | }, 51 | }, 52 | }, 53 | ); 54 | 55 | markAsReady(); 56 | </script> 57 | -------------------------------------------------------------------------------- /src/site/kick.com/modules/chat/ChatUserCard.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <Teleport :to="badgeContainer"> 3 | <span v-if="cosmetics.badges.size" class="seventv-badge-list"> 4 | <Badge 5 | v-for="[id, badge] of cosmetics.badges" 6 | :key="id" 7 | :badge="badge" 8 | type="app" 9 | :alt="badge.data.tooltip" 10 | /> 11 | </span> 12 | </Teleport> 13 | </template> 14 | 15 | <script setup lang="ts"> 16 | import { onUnmounted, watchEffect } from "vue"; 17 | import { useCosmetics } from "@/composable/useCosmetics"; 18 | import type { ChatMessageBinding } from "./ChatMessage.vue"; 19 | import Badge from "@/app/chat/Badge.vue"; 20 | import { updateElementStyles } from "@/directive/TextPaintDirective"; 21 | 22 | const props = defineProps<{ 23 | el: HTMLDivElement; 24 | bind: ChatMessageBinding; 25 | }>(); 26 | 27 | const cosmetics = useCosmetics(props.bind.authorID); 28 | 29 | const badgeContainer = document.createElement("seventv-container"); 30 | badgeContainer.id = "seventv-badge-container"; 31 | badgeContainer.style.width = "100%"; 32 | 33 | watchEffect(() => { 34 | const infoBlock = props.el.querySelector<HTMLDivElement>(".information"); 35 | const badgeBlock = props.el.querySelector<HTMLDivElement>(".badges-container"); 36 | const username = props.el.querySelector<HTMLAnchorElement>("a.username"); 37 | 38 | if (badgeBlock) { 39 | badgeBlock.appendChild(badgeContainer); 40 | } else if (infoBlock) { 41 | infoBlock.insertAdjacentElement("afterend", badgeContainer); 42 | } 43 | 44 | if (username && cosmetics.paints.size) { 45 | username.style.width = "fit-content"; 46 | updateElementStyles(username, Array.from(cosmetics.paints.values())[0].id); 47 | } 48 | }); 49 | 50 | onUnmounted(() => { 51 | badgeContainer.remove(); 52 | }); 53 | </script> 54 | 55 | <style scoped lang="scss"> 56 | :deep(.seventv-chat-badge > img) { 57 | height: 1.25rem; 58 | } 59 | </style> 60 | -------------------------------------------------------------------------------- /src/site/kick.com/modules/emote-menu/EmoteMenuModule.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <EmoteMenu :container="mod?.instance?.container ?? null" :on-pick-emote="onPickEmote" /> 3 | </template> 4 | 5 | <script setup lang="ts"> 6 | import { watch } from "vue"; 7 | import { log } from "@/common/Logger"; 8 | import { convertKickEmoteSet } from "@/common/Transform"; 9 | import { useChannelContext } from "@/composable/channel/useChannelContext"; 10 | import { useChatEmotes } from "@/composable/chat/useChatEmotes"; 11 | import { declareModule, getModuleRef } from "@/composable/useModule"; 12 | import EmoteMenu from "./EmoteMenu.vue"; 13 | 14 | const { markAsReady } = declareModule("emote-menu", { 15 | name: "Emote Menu", 16 | depends_on: [], 17 | }); 18 | 19 | const mod = getModuleRef<"KICK", "chat-input">("chat-input"); 20 | 21 | const ctx = useChannelContext(); 22 | const emotes = useChatEmotes(ctx); 23 | 24 | function onPickEmote(emote: SevenTV.ActiveEmote) { 25 | if (emote.provider === "EMOJI") { 26 | mod?.value?.instance?.appendText(emote.unicode ?? emote.name); 27 | } else { 28 | mod?.value?.instance?.appendText(emote.name); 29 | } 30 | } 31 | 32 | watch( 33 | () => ctx.id, 34 | async (id, oldID) => { 35 | if (id === oldID) return; 36 | 37 | const resp = await fetch(`https://kick.com/emotes/${ctx.username}`).catch((err) => { 38 | log.error("failed to fetch channel emote data", err); 39 | }); 40 | if (!resp) throw new Error("failed to fetch channel emote data"); 41 | 42 | const emoteSets = (await resp.json()) as Kick.KickEmoteSet[]; 43 | 44 | for (const emoteSet of emoteSets) { 45 | emotes.providers.PLATFORM ??= {}; 46 | 47 | if ("user" in emoteSet && emoteSet.user_id.toString() == ctx.id) continue; 48 | emotes.providers.PLATFORM[emoteSet.id] = convertKickEmoteSet(emoteSet); 49 | } 50 | }, 51 | ); 52 | 53 | markAsReady(); 54 | </script> 55 | -------------------------------------------------------------------------------- /src/site/kick.com/modules/settings/SettingsModule.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <template v-if="chatSettingsPopup"> 3 | <SettingsChatHook :el="chatSettingsPopup" @open-settings="ctx.open = true" /> 4 | </template> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import { ref } from "vue"; 9 | import { useEventListener } from "@vueuse/core"; 10 | import { declareModule } from "@/composable/useModule"; 11 | import SettingsChatHook from "./SettingsChatHook.vue"; 12 | import { useSettingsMenu } from "@/app/settings/Settings"; 13 | 14 | declareModule<"KICK">("settings", { 15 | name: "Settings", 16 | depends_on: [], 17 | }); 18 | 19 | const ctx = useSettingsMenu(); 20 | 21 | // Acquire vue app 22 | 23 | const chatSettingsPopup = ref<HTMLElement | null>(null); 24 | 25 | function handle(): void { 26 | chatSettingsPopup.value = document.querySelector(".chat-actions-popup"); 27 | } 28 | 29 | useEventListener(window, "popstate", () => setTimeout(handle, 0)); 30 | useEventListener(document, "click", () => setTimeout(handle, 0), { 31 | capture: true, 32 | }); 33 | </script> 34 | -------------------------------------------------------------------------------- /src/site/site.normal.ts: -------------------------------------------------------------------------------- 1 | export function loadSite() { 2 | import("./site.app"); 3 | } 4 | -------------------------------------------------------------------------------- /src/site/site.ts: -------------------------------------------------------------------------------- 1 | import { log } from "@/common/Logger"; 2 | import { semanticVersionToNumber } from "@/common/Transform"; 3 | import { loadSite } from "./site.normal"; 4 | 5 | (async () => { 6 | const host: string = import.meta.env.VITE_APP_HOST; 7 | const versionBranch: string = import.meta.env.VITE_APP_VERSION_BRANCH; 8 | 9 | const manifestURL = `${host}/manifest${versionBranch ? "." + versionBranch.toLowerCase() : ""}.json`; 10 | 11 | const manifest = await fetch(manifestURL) 12 | .then((res) => res.json()) 13 | .catch((err) => log.error("<Site>", "Failed to fetch host manifest", err.message)); 14 | 15 | const localVersion = semanticVersionToNumber(import.meta.env.VITE_APP_VERSION); 16 | const hostedVersion = manifest ? semanticVersionToNumber(manifest.version) : 0; 17 | 18 | (window as Window & { seventv?: SeventvGlobalScope }).seventv = { 19 | host_manifest: manifest ?? null, 20 | }; 21 | 22 | if (!manifest || hostedVersion <= localVersion) { 23 | log.info("<Site>", "Using Local Mode,", "v" + import.meta.env.VITE_APP_VERSION); 24 | loadSite(); 25 | } else { 26 | seventv.hosted = true; 27 | 28 | const v1 = document.createElement("script"); 29 | v1.id = "seventv-site-hosted"; 30 | v1.src = manifest.index_file; 31 | v1.type = "module"; 32 | 33 | const v2 = document.createElement("link"); 34 | v2.rel = "stylesheet"; 35 | v2.type = "text/css"; 36 | v2.href = manifest.stylesheet_file; 37 | v2.setAttribute("charset", "utf-8"); 38 | v2.setAttribute("content", "text/html"); 39 | v2.setAttribute("http-equiv", "content-type"); 40 | v2.id = "seventv-stylesheet"; 41 | 42 | document.head.appendChild(v2); 43 | document.head.appendChild(v1); 44 | 45 | log.info("<Site>", "Using Hosted Mode,", "v" + manifest.version); 46 | } 47 | })(); 48 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/chat/ChatMessageUnhandled.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <span :id="msg.id" ref="msgContainer" class="seventv-unhandled-message-ref" /> 3 | </template> 4 | 5 | <script setup lang="ts"> 6 | import { onMounted, ref } from "vue"; 7 | import type { ChatMessage } from "@/common/chat/ChatMessage"; 8 | 9 | const props = defineProps<{ 10 | msg: ChatMessage; 11 | }>(); 12 | 13 | const msgContainer = ref<HTMLSpanElement | null>(null); 14 | 15 | onMounted(() => { 16 | if (!msgContainer.value || !props.msg.wrappedNode) return; 17 | 18 | msgContainer.value.appendChild(props.msg.wrappedNode); 19 | }); 20 | </script> 21 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/chat/components/tray/ChatTray.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <template v-for="({ parent, component, props }, i) of trayElements" :key="i"> 3 | <Teleport v-if="parent.current" :to="parent.current"> 4 | <Component :is="component" v-bind="props" /> 5 | </Teleport> 6 | </template> 7 | </template> 8 | <script setup lang="ts"> 9 | import { trayElements } from "./ChatTray"; 10 | </script> 11 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/chat/components/tray/Tray.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <Teleport v-if="tray.bodyRef" :to="tray.bodyRef"> 3 | <slot /> 4 | </Teleport> 5 | </template> 6 | <script setup lang="ts"> 7 | import { onMounted, onUnmounted } from "vue"; 8 | import { useTrayRef } from "./ChatTray"; 9 | 10 | const props = withDefaults( 11 | defineProps<{ 12 | inputValueOverride?: string; 13 | sendButtonOverride?: string; 14 | disableCommands?: boolean; 15 | disableBits?: boolean; 16 | disablePaidPinnedChat?: boolean; 17 | onClose?: (v?: string) => void; 18 | disableChat?: boolean; 19 | messageHandler?: (v: string) => void; 20 | placeholder?: string; 21 | modifier?: boolean; 22 | floating?: boolean; 23 | }>(), 24 | { 25 | modifier: false, 26 | }, 27 | ); 28 | 29 | const tray = useTrayRef(props, props.modifier); 30 | 31 | onMounted(tray.open); 32 | onUnmounted(tray.close); 33 | </script> 34 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/custom-commands/CommandModule.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <template v-if="ch?.instances"> 3 | <template v-for="i of ch.instances" :key="i"> 4 | <template v-for="cmd of commands" :key="cmd"> 5 | <component :is="cmd" v-bind="{ add: i.component.addCommand, remove: i.component.removeCommand }" /> 6 | </template> 7 | </template> 8 | </template> 9 | </template> 10 | <script setup lang="ts"> 11 | import type { Component } from "vue"; 12 | import { until } from "@vueuse/core"; 13 | import { useComponentHook } from "@/common/ReactHooks"; 14 | import { declareModule } from "@/composable/useModule"; 15 | import Dashboard from "./Commands/Dashboard.vue"; 16 | import Nuke from "./Commands/Nuke.vue"; 17 | import Reresh from "./Commands/Refresh.vue"; 18 | import Song from "./Commands/Song.vue"; 19 | 20 | const { dependenciesMet, markAsReady } = declareModule("command-manager", { 21 | name: "Command Manager", 22 | depends_on: ["chat", "chat-input"], 23 | }); 24 | 25 | let ch: ReturnType<typeof useComponentHook<Twitch.ChatCommandComponent>> | undefined = undefined; 26 | 27 | defineExpose({ 28 | addCommand: (c: Twitch.ChatCommand) => ch?.instances[0]?.component.addCommand(c), 29 | removeCommand: (c: Twitch.ChatCommand) => ch?.instances[0]?.component.removeCommand(c), 30 | }); 31 | 32 | await until(dependenciesMet).toBe(true); 33 | 34 | const commands = [Dashboard, Song, Reresh, Nuke] as Component[]; 35 | 36 | useComponentHook<Twitch.ChatCommandGrouperComponent>( 37 | { 38 | parentSelector: ".chat-input__textarea", 39 | predicate: (n) => n.determineGroup, 40 | maxDepth: 50, 41 | }, 42 | { 43 | functionHooks: { 44 | determineGroup(this, old, command) { 45 | return command.group ? command.group : old.call(this, command) ?? "Twitch"; 46 | }, 47 | }, 48 | }, 49 | ); 50 | 51 | ch = useComponentHook<Twitch.ChatCommandComponent>({ 52 | childSelector: ".stream-chat", 53 | predicate: (n) => n.addCommand, 54 | maxDepth: 50, 55 | }); 56 | markAsReady(); 57 | </script> 58 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/custom-commands/Commands/Refresh.vue: -------------------------------------------------------------------------------- 1 | <template /> 2 | <script setup lang="ts"> 3 | import { onUnmounted } from "vue"; 4 | import { useChannelContext } from "@/composable/channel/useChannelContext"; 5 | import { useChatMessages } from "@/composable/chat/useChatMessages"; 6 | 7 | const ctx = useChannelContext(); 8 | 9 | const props = defineProps<{ 10 | add: (c: Twitch.ChatCommand) => void; 11 | remove: (c: Twitch.ChatCommand) => void; 12 | }>(); 13 | 14 | const { reload } = useChatMessages(ctx); 15 | 16 | const doRefresh = async () => { 17 | await ctx.fetch(true); 18 | reload(); 19 | return { notice: "Emotes refreshed" }; 20 | }; 21 | 22 | const handler: Twitch.ChatCommand.Handler = () => { 23 | return { 24 | deferred: doRefresh(), 25 | }; 26 | }; 27 | 28 | const command: Twitch.ChatCommand = { 29 | name: "refresh", 30 | description: "Refresh chat emotes", 31 | helpText: 32 | "Refresh chat emotes, this might fix missing emotes sets, but will not update emotes that have been added very recently.", 33 | permissionLevel: 0, 34 | handler: handler, 35 | group: "7TV", 36 | }; 37 | 38 | props.add(command); 39 | onUnmounted(() => props.remove(command)); 40 | </script> 41 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/emote-menu/EmoteMenuButton.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-if="isEnabled" class="seventv-tw-button seventv-emote-menu-button" @click="onClick"> 3 | <button> 4 | <Logo provider="7TV" class="icon" /> 5 | </button> 6 | <span class="tooltip-over">Emote Menu</span> 7 | <div v-if="!updater.isUpToDate && !ctx.open" class="seventv-emote-menu-update-flair" /> 8 | </div> 9 | </template> 10 | 11 | <script setup lang="ts"> 12 | import { ref, watch } from "vue"; 13 | import { useConfig } from "@/composable/useSettings"; 14 | import useUpdater from "@/composable/useUpdater"; 15 | import Logo from "@/assets/svg/logos/Logo.vue"; 16 | import { useEmoteMenuContext } from "@/app/emote-menu/EmoteMenuContext"; 17 | 18 | defineProps<{ 19 | onClick?: () => void; 20 | }>(); 21 | 22 | const ctx = useEmoteMenuContext(); 23 | const updater = useUpdater(); 24 | const isEnabled = ref(false); 25 | const placement = useConfig<string>("ui.emote_menu.button_placement"); 26 | 27 | watch( 28 | placement, 29 | (v) => { 30 | isEnabled.value = v === "below"; 31 | }, 32 | { immediate: true }, 33 | ); 34 | </script> 35 | 36 | <style scoped lang="scss"> 37 | @import "@/assets/style/tw-tooltip"; 38 | @import "@/assets/style/flair"; 39 | 40 | .seventv-emote-menu-update-flair { 41 | position: absolute; 42 | top: 0.5rem; 43 | right: 0.5rem; 44 | width: 0.75rem; 45 | height: 0.75rem; 46 | 47 | @include flair-pulsating(#3eed58); 48 | } 49 | 50 | .seventv-tw-button.seventv-emote-menu-button { 51 | svg { 52 | width: 100%; 53 | height: 100%; 54 | } 55 | } 56 | </style> 57 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/mod-logs/ModLogsAuthorityMessages.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main class="seventv-mod-log-authority-messages"> 3 | <template v-for="item of localStore.modMessages" :key="item.id"> 4 | <UserMessage :msg="item" :force-timestamp="true" :emotes="emotes.active" /> 5 | </template> 6 | </main> 7 | </template> 8 | 9 | <script setup lang="ts"> 10 | import { useChannelContext } from "@/composable/channel/useChannelContext"; 11 | import { useChatEmotes } from "@/composable/chat/useChatEmotes"; 12 | import { useModLogsStore } from "./ModLogsStore"; 13 | import UserMessage from "@/app/chat/UserMessage.vue"; 14 | 15 | const localStore = useModLogsStore(); 16 | const ctx = useChannelContext(); 17 | const emotes = useChatEmotes(ctx); 18 | </script> 19 | 20 | <style scoped lang="scss"> 21 | .seventv-mod-log-authority-messages { 22 | display: flex; 23 | flex-direction: column; 24 | row-gap: 1em; 25 | padding: 0.5rem 0.35rem; 26 | } 27 | </style> 28 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/mod-logs/ModLogsButton.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div v-if="isEnabled" class="seventv-tw-button seventv-mod-logs-button" @click="onClick"> 3 | <button> 4 | <ModLogsIcon class="icon" /> 5 | </button> 6 | <span class="tooltip-over">Mod Logs</span> 7 | </div> 8 | </template> 9 | 10 | <script setup lang="ts"> 11 | import { useConfig } from "@/composable/useSettings"; 12 | import ModLogsIcon from "@/assets/svg/icons/ModLogsIcon.vue"; 13 | 14 | defineProps<{ 15 | onClick: () => void; 16 | }>(); 17 | 18 | const isEnabled = useConfig<boolean>("chat.mod_logs.enabled"); 19 | </script> 20 | 21 | <style scoped lang="scss"> 22 | @import "@/assets/style/tw-tooltip"; 23 | </style> 24 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/mod-logs/ModLogsRecentActions.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main class="seventv-mod-log-recent-actions"> 3 | <template v-for="item of messages.moderated" :key="item.id"> 4 | <template v-if="item.mod"> 5 | <ModLogsRecentActionsItem 6 | :messages="item.messages" 7 | :victim="item.victim" 8 | :mod="item.mod" 9 | :emotes="emotes.active" 10 | /> 11 | </template> 12 | </template> 13 | </main> 14 | </template> 15 | 16 | <script setup lang="ts"> 17 | import { useChannelContext } from "@/composable/channel/useChannelContext"; 18 | import { useChatEmotes } from "@/composable/chat/useChatEmotes"; 19 | import { useChatMessages } from "@/composable/chat/useChatMessages"; 20 | import ModLogsRecentActionsItem from "./ModLogsRecentActionsItem.vue"; 21 | 22 | const ctx = useChannelContext(); 23 | const messages = useChatMessages(ctx); 24 | const emotes = useChatEmotes(ctx); 25 | </script> 26 | 27 | <style scoped lang="scss"> 28 | .seventv-mod-log-recent-actions { 29 | display: flex; 30 | flex-direction: column; 31 | row-gap: 1em; 32 | padding: 0.5rem 0.35rem; 33 | } 34 | </style> 35 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/mod-logs/ModLogsStore.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from "vue"; 2 | import type { ChatMessage } from "@/common/chat/ChatMessage"; 3 | 4 | const data = reactive({ 5 | modMessages: [] as ChatMessage[], 6 | }); 7 | 8 | export function useModLogsStore() { 9 | return data; 10 | } 11 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/player/PlayerContentWarning.vue: -------------------------------------------------------------------------------- 1 | <template /> 2 | 3 | <script setup lang="ts"> 4 | import { log } from "@/common/Logger"; 5 | import { HookedInstance } from "@/common/ReactHooks"; 6 | import { definePropertyHook } from "@/common/Reflection"; 7 | 8 | const props = defineProps<{ 9 | inst: HookedInstance<Twitch.VideoPlayerContentRestriction>; 10 | }>(); 11 | 12 | definePropertyHook(props.inst.component, "props", { 13 | value(v) { 14 | const fn = v.children?.props?.onAcknowledge; 15 | if (typeof fn === "function") { 16 | fn(); 17 | 18 | log.info("<Player>", "Acknowledged content warning", `(${v.restriction})`); 19 | } 20 | }, 21 | }); 22 | </script> 23 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/player/PlayerStatsTooltip.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <div class="seventv-tooltip" tooltip-type="video-stats"> 3 | <div class="details"> 4 | <p>Video: {{ props.width }}x{{ props.height }}</p> 5 | <p>Framerate: {{ props.framerate }}</p> 6 | <p>Bitrate: {{ props.bitrate }} kbps</p> 7 | <p>Dropped Frames: {{ props.droppedFrames }}</p> 8 | <p>Playback Rate: {{ props.playbackRate.toFixed(2) }}x</p> 9 | <p>Buffer Size: {{ props.bufferSize.toFixed(2) }}s</p> 10 | </div> 11 | <div class="tooltip-title"> 12 | <p>Stream Latency</p> 13 | </div> 14 | </div> 15 | </template> 16 | 17 | <script setup lang="ts"> 18 | const props = defineProps<{ 19 | bitrate: string; 20 | playbackRate: number; 21 | droppedFrames: number; 22 | height: number; 23 | width: number; 24 | framerate: number; 25 | bufferSize: number; 26 | }>(); 27 | </script> 28 | 29 | <style scoped lang="scss"> 30 | .seventv-tooltip { 31 | text-align: center; 32 | padding: 0.5em 1.15em; 33 | min-width: 12em; 34 | } 35 | 36 | .details { 37 | padding-bottom: 0.25rem; 38 | margin-bottom: 0.25rem; 39 | } 40 | 41 | .tooltip-title { 42 | padding-top: 0.25rem; 43 | margin-top: 0.25rem; 44 | } 45 | </style> 46 | -------------------------------------------------------------------------------- /src/site/twitch.tv/modules/settings/SettingsModule.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <SettingsMenuButton @toggle="ctx.open = !ctx.open" /> 3 | </template> 4 | 5 | <script setup lang="ts"> 6 | import { declareModule } from "@/composable/useModule"; 7 | import SettingsMenuButton from "./SettingsMenuButton.vue"; 8 | import { useSettingsMenu } from "@/app/settings/Settings"; 9 | 10 | const { markAsReady } = declareModule("settings", { 11 | name: "Settings", 12 | depends_on: [], 13 | }); 14 | 15 | const ctx = useSettingsMenu(); 16 | 17 | markAsReady(); 18 | </script> 19 | 20 | <style scoped lang="scss"> 21 | .settings-menu-enter-active, 22 | .settings-menu-leave-active { 23 | transition: opacity 120ms; 24 | } 25 | 26 | .settings-menu-enter-from, 27 | .settings-menu-leave-to { 28 | opacity: 0; 29 | } 30 | </style> 31 | -------------------------------------------------------------------------------- /src/site/youtube.com/YouTubeSite.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <template v-for="[key, mod] of Object.entries(modules)" :key="key"> 3 | <ModuleWrapper :mod="mod" @mounted="onModuleUpdate(key as unknown as YtModuleID, $event)" /> 4 | </template> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import { defineAsyncComponent } from "vue"; 9 | import { useStore } from "@/store/main"; 10 | import { getModule } from "@/composable/useModule"; 11 | import { useUserAgent } from "@/composable/useUserAgent"; 12 | import { YtModuleID } from "@/types/yt.module"; 13 | 14 | const ModuleWrapper = defineAsyncComponent(() => import("@/site/global/ModuleWrapper.vue")); 15 | 16 | const store = useStore(); 17 | const ua = useUserAgent(); 18 | 19 | ua.preferredFormat = store.avifSupported ? "AVIF" : "WEBP"; 20 | store.setPreferredImageFormat(ua.preferredFormat); 21 | store.setPlatform("YOUTUBE", ["7TV", "FFZ", "BTTV"], []); 22 | 23 | // Import modules 24 | const modules = import.meta.glob("./modules/**/*Module.vue", { eager: true, import: "default" }); 25 | 26 | for (const key in modules) { 27 | const modPath = key.split("/"); 28 | const modKey = modPath.splice(modPath.length - 2, 1).pop(); 29 | 30 | modules[modKey!] = modules[key]; 31 | delete modules[key]; 32 | } 33 | 34 | function onModuleUpdate(mod: YtModuleID, inst: InstanceType<ComponentFactory>) { 35 | const modInst = getModule(mod); 36 | if (!modInst) return; 37 | 38 | modInst.instance = inst; 39 | } 40 | </script> 41 | -------------------------------------------------------------------------------- /src/site/youtube.com/modules/chat/ChatData.vue: -------------------------------------------------------------------------------- 1 | <template /> 2 | 3 | <script setup lang="ts"> 4 | import { toRef } from "vue"; 5 | import { db } from "@/db/idb"; 6 | import { useChannelContext } from "@/composable/channel/useChannelContext"; 7 | import { useChatEmotes } from "@/composable/chat/useChatEmotes"; 8 | import { useLiveQuery } from "@/composable/useLiveQuery"; 9 | 10 | const ctx = useChannelContext(); 11 | const emotes = useChatEmotes(ctx); 12 | 13 | const channelID = toRef(ctx, "id"); 14 | 15 | // query the channel's emote set bindings 16 | const channelSets = useLiveQuery( 17 | () => 18 | db.channels 19 | .where("id") 20 | .equals(ctx.id ?? "") 21 | .first() 22 | .then((c) => c?.set_ids ?? []), 23 | () => { 24 | // reset the third-party emote providers 25 | emotes.providers["7TV"] = {}; 26 | emotes.providers["FFZ"] = {}; 27 | emotes.providers["BTTV"] = {}; 28 | }, 29 | { 30 | reactives: [channelID], 31 | }, 32 | ); 33 | 34 | // query the channel's active emote sets 35 | useLiveQuery( 36 | () => 37 | db.emoteSets 38 | .where("id") 39 | .anyOf(channelSets.value ?? []) 40 | .or("scope") 41 | .equals("GLOBAL") 42 | .sortBy("priority"), 43 | (sets) => { 44 | if (!sets) return; 45 | 46 | for (const set of sets) { 47 | const provider = (set.provider ?? "UNKNOWN") as SevenTV.Provider; 48 | 49 | if (!emotes.providers[provider]) emotes.providers[provider] = {}; 50 | emotes.providers[provider][set.id] = set; 51 | 52 | emotes.sets[set.id] = set; 53 | } 54 | 55 | const o = {} as Record<SevenTV.ObjectID, SevenTV.ActiveEmote>; 56 | for (const emote of sets.flatMap((set) => set.emotes)) { 57 | if (!emote) return; 58 | o[emote.name] = emote; 59 | } 60 | 61 | for (const e in emotes.emojis) { 62 | const emoji = emotes.emojis[e]; 63 | if (!emoji || !emoji.unicode) continue; 64 | 65 | o[emoji.unicode] = emoji; 66 | } 67 | 68 | for (const e in o) { 69 | emotes.active[e] = o[e]; 70 | } 71 | }, 72 | { 73 | reactives: [channelSets], 74 | }, 75 | ); 76 | </script> 77 | -------------------------------------------------------------------------------- /src/types/kick.module.d.ts: -------------------------------------------------------------------------------- 1 | // import AuthModuleVue from "@/site/kick.com/modules/auth/AuthModule_disabled.vue"; 2 | import ChatInputModuleVue from "@/site/kick.com/modules/chat-input/ChatInputModule.vue"; 3 | import ChatModuleVue from "@/site/kick.com/modules/chat/ChatModule.vue"; 4 | import EmoteMenuModuleVue from "@/site/kick.com/modules/emote-menu/EmoteMenuModule.vue"; 5 | import SettingsModuleVue from "@/site/kick.com/modules/settings/SettingsModule.vue"; 6 | 7 | declare type KickModuleID = keyof KickModuleComponentMap; 8 | 9 | declare type KickModuleComponentMap = { 10 | auth: typeof AuthModuleVue; 11 | chat: typeof ChatModuleVue; 12 | settings: typeof SettingsModuleVue; 13 | "chat-input": typeof ChatInputModuleVue; 14 | "emote-menu": typeof EmoteMenuModuleVue; 15 | }; 16 | -------------------------------------------------------------------------------- /src/types/react-extended.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | /* eslint-disable @typescript-eslint/prefer-namespace-keyword */ 4 | 5 | type MaybeElement = Element | null; 6 | 7 | declare namespace ReactExtended { 8 | type AnyReactComponent = WritableComponent<any, any> & { [x: string]: any }; 9 | 10 | type WritableComponent<P = {}, S = {}, SS = any> = React.Component<P, S, SS> & { 11 | props: Writeable<React.Component<P, S, SS>["props"]>; 12 | state: Writeable<React.Component<P, S, SS>["state"]>; 13 | }; 14 | 15 | type Writeable<T> = { -readonly [P in keyof T]: Writeable<T[P]> }; 16 | 17 | interface ReactVNode<P = {} | null> { 18 | alternate: ReactVNode | null; 19 | child: ReactVNode | null; 20 | childExpirationTime: number | null; 21 | effectTag: number | null; 22 | elementType: React.ElementType<P> | null; 23 | expirationTime: number | null; 24 | index: number | null; 25 | key: Key | null; 26 | mode: number | null; 27 | return: ReactVNode | null; 28 | sibling: ReactVNode | null; 29 | stateNode: React.ReactInstance | null; 30 | tag: number | null; 31 | type: React.ElementType<P> | null; 32 | pendingProps: P; 33 | memoizedProps: P; 34 | } 35 | 36 | interface ReactFunctionalFiber<P = {}> extends ReactVNode<P> { 37 | elementType: { render: (props: P) => ReactExtended.ReactVNode }; 38 | pendingProps: P; 39 | } 40 | 41 | interface ReactRuntimeElement extends React.ReactElement { 42 | $$typeof: symbol; 43 | ref: { current: MaybeElement } | ((e: MaybeElement) => void) | null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/types/tw.module.d.ts: -------------------------------------------------------------------------------- 1 | import type AutoclaimModuleVue from "@/site/twitch.tv/modules/avatars/AutoclaimModuleVue.vue"; 2 | import type AvatarsModuleVue from "@/site/twitch.tv/modules/avatars/AvatarsModule.vue"; 3 | import type ChatInputControllerComponent from "@/site/twitch.tv/modules/chat-input-controller/ChatInputControllerModule.vue"; 4 | import type ChatInputModuleVue from "@/site/twitch.tv/modules/chat-input/ChatInputModule.vue"; 5 | import type ChatVodModuleVue from "@/site/twitch.tv/modules/chat-vod/ChatVodModule.vue"; 6 | import type ChatModuleVue from "@/site/twitch.tv/modules/chat/ChatModule.vue"; 7 | import type EmoteMenuModuleVue from "@/site/twitch.tv/modules/emote-menu/EmoteMenuModule.vue"; 8 | import type HiddenElementsModuleVue from "@/site/twitch.tv/modules/hidden-elements/HiddenElementsModule.vue"; 9 | import type ModLogsModule from "@/site/twitch.tv/modules/mod-logs/ModLogsModule.vue"; 10 | import type PlayerModule from "@/site/twitch.tv/modules/player/PlayerModule.vue"; 11 | import type SettingsModuleVue from "@/site/twitch.tv/modules/settings/SettingsModule.vue"; 12 | import type SidebarPreviewsModuleVue from "@/site/twitch.tv/modules/sidebar-previews/SidebarPreviewsModule.vue"; 13 | 14 | declare type TwModuleID = keyof TwModuleComponentMap; 15 | 16 | declare type TwModuleComponentMap = { 17 | "chat-input-controller": typeof ChatInputControllerComponent; 18 | "chat-input": typeof ChatInputModuleVue; 19 | "chat-vod": typeof ChatVodModuleVue; 20 | "emote-menu": typeof EmoteMenuModuleVue; 21 | "hidden-elements": typeof HiddenElementsModuleVue; 22 | "mod-logs": typeof ModLogsModule; 23 | "sidebar-previews": typeof SidebarPreviewsModuleVue; 24 | autoclaim: typeof AutoclaimModuleVue; 25 | avatars: typeof AvatarsModuleVue; 26 | chat: typeof ChatModuleVue; 27 | player: PlayerModule; 28 | settings: typeof SettingsModuleVue; 29 | }; 30 | -------------------------------------------------------------------------------- /src/types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | // eslint-disable-next-line prettier/prettier 3 | import type { DefineComponent } from "vue"; 4 | 5 | declare module "*.vue" { 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | const component: DefineComponent<Record<string, never>, Record<string, never>, any>; 8 | export default component; 9 | } 10 | 11 | declare module "*?sharedworker&inline" { 12 | const WorkerFactory: new () => SharedWorker; 13 | export default WorkerFactory; 14 | } 15 | -------------------------------------------------------------------------------- /src/types/yt.module.d.ts: -------------------------------------------------------------------------------- 1 | import type ChatModuleVue from "@/site/youtube.com/modules/chat/ChatModule.vue"; 2 | 3 | declare type YtModuleID = keyof YtModuleComponentMap; 4 | 5 | declare type YtModuleComponentMap = { 6 | chat: typeof ChatModuleVue; 7 | }; 8 | -------------------------------------------------------------------------------- /src/ui/UiButton.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <button> 3 | <slot /> 4 | 5 | <span class="ui-button-icon"> 6 | <slot name="icon" /> 7 | </span> 8 | </button> 9 | </template> 10 | 11 | <script setup lang="ts"> 12 | void 0; 13 | </script> 14 | 15 | <style scoped lang="scss"> 16 | button { 17 | all: unset; 18 | height: 100%; 19 | border-radius: 0.25rem; 20 | background: var(--seventv-input-background); 21 | outline: 0.01em solid var(--seventv-input-border); 22 | padding: 0 0.5em; 23 | white-space: nowrap; 24 | text-align: center; 25 | font-weight: 600; 26 | font-size: 1.25em; 27 | 28 | &:hover { 29 | cursor: pointer; 30 | } 31 | 32 | transition: opacity 170ms ease-in-out; 33 | 34 | &:focus-within { 35 | opacity: 0.65; 36 | } 37 | 38 | &.ui-button-important { 39 | background: var(--seventv-text-color-normal); 40 | color: var(--seventv-background-shade-1); 41 | box-shadow: 0.15rem 0.15rem 0.1rem var(--seventv-text-color-normal); 42 | 43 | &:hover { 44 | filter: brightness(1.5); 45 | } 46 | } 47 | 48 | &.ui-button-hollow { 49 | background: unset; 50 | outline-width: 0.15em; 51 | 52 | &:hover { 53 | background: var(--seventv-input-background); 54 | } 55 | } 56 | 57 | .ui-button-icon { 58 | display: inline-grid; 59 | 60 | > :only-child { 61 | margin-left: 0.5rem; 62 | } 63 | } 64 | 65 | * { 66 | display: inline-block; 67 | vertical-align: middle; 68 | } 69 | 70 | &[disabled] { 71 | cursor: not-allowed; 72 | opacity: 0.25 !important; 73 | 74 | &:hover { 75 | filter: unset; 76 | } 77 | } 78 | } 79 | </style> 80 | -------------------------------------------------------------------------------- /src/ui/UiCopiedMessageToast.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main ref="copiedMessageToastRef" class="seventv-copied-message-toast"> 3 | <div class="seventv-copied-message-toast-body"> 4 | <p v-if="message">{{ message }}</p> 5 | <slot v-else /> 6 | </div> 7 | </main> 8 | </template> 9 | 10 | <script setup lang="ts"> 11 | import { ref } from "vue"; 12 | import { onClickOutside } from "@vueuse/core"; 13 | 14 | const copiedMessageToastRef = ref<HTMLElement>(); 15 | 16 | defineProps<{ 17 | message?: string; 18 | }>(); 19 | 20 | const emit = defineEmits<{ 21 | (event: "close"): void; 22 | }>(); 23 | 24 | onClickOutside(copiedMessageToastRef, () => { 25 | emit("close"); 26 | }); 27 | </script> 28 | 29 | <style scoped lang="scss"> 30 | main.seventv-copied-message-toast { 31 | background: var(--seventv-background-transparent-1); 32 | backdrop-filter: blur(1rem); 33 | border: 0.15rem solid var(--seventv-border-transparent-1); 34 | border-radius: 0.25rem; 35 | max-width: 18rem; 36 | z-index: 100; 37 | opacity: 1; 38 | 39 | .seventv-copied-message-toast-body { 40 | padding: 0.5rem 1rem; 41 | border-bottom: 0.1rem solid var(--seventv-border-transparent-1); 42 | text-align: center; 43 | 44 | p { 45 | font-size: 1.25rem; 46 | } 47 | } 48 | } 49 | </style> 50 | -------------------------------------------------------------------------------- /src/ui/UiLazy.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted, reactive } from "vue"; 2 | 3 | const instances = reactive<Map<symbol | string, number>>(new Map()); 4 | 5 | export function useUiLazy(id: symbol | string) { 6 | function increment(incr = 1) { 7 | if (!instances.has(id)) { 8 | instances.set(id, 0); 9 | } else { 10 | instances.set(id, instances.get(id)! + incr); 11 | } 12 | 13 | return instances.get(id)!; 14 | } 15 | 16 | onUnmounted(() => instances.delete(id)); 17 | 18 | return { 19 | increment, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/ui/UiLazy.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <template v-if="ok"> 3 | <slot /> 4 | </template> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import { nextTick, ref } from "vue"; 9 | 10 | const ok = ref(false); 11 | 12 | nextTick(() => { 13 | ok.value = true; 14 | }); 15 | </script> 16 | -------------------------------------------------------------------------------- /src/ui/UiLazyList.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <template v-if="show"> 3 | <slot /> 4 | </template> 5 | </template> 6 | 7 | <script setup lang="ts"> 8 | import { ref } from "vue"; 9 | import { useTimeoutFn } from "@vueuse/shared"; 10 | import { useUiLazy } from "./UiLazy"; 11 | 12 | const props = defineProps<{ 13 | inst: symbol | string; 14 | incr?: number; 15 | }>(); 16 | 17 | const { increment } = useUiLazy(props.inst); 18 | const show = ref(false); 19 | 20 | if (props.inst) { 21 | useTimeoutFn(() => { 22 | show.value = true; 23 | }, increment(props.incr)); 24 | } 25 | </script> 26 | -------------------------------------------------------------------------------- /src/ui/UiSuperHint.vue: -------------------------------------------------------------------------------- 1 | <template> 2 | <main class="seventv-super-hint"> 3 | <div class="seventv-super-hint-heading"> 4 | <Logo provider="7TV" /> 5 | <span>{{ title }}</span> 6 | </div> 7 | 8 | <div class="seventv-super-hint-content"> 9 | <slot /> 10 | </div> 11 | </main> 12 | </template> 13 | 14 | <script setup lang="ts"> 15 | import Logo from "@/assets/svg/logos/Logo.vue"; 16 | 17 | defineProps<{ 18 | title: string; 19 | }>(); 20 | </script> 21 | 22 | <style scoped lang="scss"> 23 | main.seventv-super-hint { 24 | background: var(--seventv-background-transparent-1); 25 | backdrop-filter: blur(1rem); 26 | border-radius: 0.25rem; 27 | outline: 0.01rem solid var(--seventv-border-transparent-1); 28 | overflow: clip; 29 | max-width: 24rem; 30 | 31 | .seventv-super-hint-heading { 32 | display: grid; 33 | padding: 0.5rem; 34 | grid-template-columns: 2rem auto; 35 | gap: 0.5rem; 36 | background: var(--seventv-background-transparent-2); 37 | backdrop-filter: blur(1rem); 38 | align-items: center; 39 | text-align: center; 40 | font-size: 1.25rem; 41 | font-weight: 600; 42 | width: 100%; 43 | border-bottom: 0.1rem solid var(--seventv-border-transparent-1); 44 | 45 | > svg { 46 | width: 100%; 47 | font-size: 2rem; 48 | } 49 | } 50 | } 51 | </style> 52 | -------------------------------------------------------------------------------- /src/worker/event-handlers/cosmetic.handler.ts: -------------------------------------------------------------------------------- 1 | import { log } from "@/common/Logger"; 2 | import { ChangeMap, EventContext } from ".."; 3 | 4 | export function onCosmeticCreate(ctx: EventContext, cm: ChangeMap<SevenTV.ObjectKind.COSMETIC>) { 5 | if (!cm.object) return; 6 | 7 | // Insert the cosmetic into the database 8 | 9 | cm.object.provider = "7TV"; 10 | ctx.db 11 | .withErrorFallback(ctx.db.cosmetics.put(cm.object), () => 12 | ctx.db.cosmetics.where("id").equals(cm.object.id).modify(cm.object), 13 | ) 14 | .catch((err) => log.error("<EventAPI>", "Failed to insert cosmetic", err)); 15 | 16 | for (const port of ctx.driver.ports.values()) { 17 | port.postMessage("COSMETIC_CREATED", cm.object as SevenTV.Cosmetic<"AVATAR" | "BADGE" | "PAINT">); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/worker/event-handlers/user.handler.ts: -------------------------------------------------------------------------------- 1 | import { log } from "@/common/Logger"; 2 | import { iterateChangeMap } from "./handler"; 3 | import { ChangeField, ChangeMap, EventContext, TypedWorkerMessage } from ".."; 4 | 5 | export async function onUserUpdate(ctx: EventContext, cm: ChangeMap<SevenTV.ObjectKind.USER>) { 6 | const emission = [] as TypedWorkerMessage<"USER_UPDATED">[]; 7 | 8 | await iterateChangeMap<SevenTV.ObjectKind.USER>(cm, { 9 | connections: { 10 | async updated(fields: ChangeField[], _, cur?: ChangeField) { 11 | if (!cur) return; 12 | 13 | for (const f of fields) { 14 | switch (f.key) { 15 | case "emote_set": { 16 | const [oldSet, newSet] = [f.old_value, f.value] as SevenTV.EmoteSet[]; 17 | 18 | // fetch the new set's emotes 19 | if (newSet) { 20 | const set = await ctx.driver.http 21 | .API() 22 | .seventv.loadEmoteSet(newSet.id) 23 | .catch((err) => 24 | log.warn( 25 | "<Net/EventAPI>", 26 | `failed to fetch emote set (id: '${newSet.id}') during user connection update`, 27 | err, 28 | ), 29 | ); 30 | if (!set) return; 31 | 32 | newSet.emotes = set.emotes; 33 | ctx.db.emoteSets.put(set); 34 | } 35 | 36 | emission.push({ 37 | id: cm.id, 38 | actor: cm.actor, 39 | emote_set: { 40 | connection_index: cur.index ?? 0, 41 | old_set: oldSet, 42 | new_set: newSet, 43 | }, 44 | }); 45 | 46 | break; 47 | } 48 | } 49 | } 50 | }, 51 | }, 52 | }); 53 | 54 | for (const e of emission) { 55 | for (const port of ctx.driver.ports.values()) { 56 | port.postMessage("USER_UPDATED", e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/worker/worker.root.ts: -------------------------------------------------------------------------------- 1 | import { WorkerDriver } from "./worker.driver"; 2 | import "./worker.http"; 3 | 4 | const w = self as unknown as SharedWorkerGlobalScope; 5 | 6 | (() => new WorkerDriver(w))(); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "useDefineForClassFields": true, 7 | "strict": true, 8 | "jsx": "preserve", 9 | "jsxFactory": "VueJSX", 10 | "sourceMap": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM", "WebWorker"], 14 | "types": ["vite/client", "chrome"], 15 | "skipLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./src/*"], 19 | "CHANGELOG": ["./CHANGELOG.md"] 20 | } 21 | }, 22 | "include": [ 23 | "src/*.ts", 24 | "src/**/*.ts", 25 | "src/types/*.d.ts", 26 | "src/**/*.vue", 27 | "manifest.config.ts", 28 | "src/site/twitch.tv/modules/**/*.d.ts" 29 | ], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true, 7 | "resolveJsonModule": true 8 | }, 9 | "include": [ 10 | "vite.utils.ts", 11 | "vite.config.mts", 12 | "vite.config.worker.mts", 13 | "vite.config.background.mts", 14 | "vite.config.content.mts", 15 | "manifest.config.ts", 16 | "package.json" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /vite.config.background.mts: -------------------------------------------------------------------------------- 1 | import { appName, getFullVersion, r } from "./vite.utils"; 2 | import path from "path"; 3 | import { defineConfig, loadEnv } from "vite"; 4 | 5 | export default defineConfig(() => { 6 | const mode = process.env.NODE_ENV ?? ""; 7 | const isNightly = process.env.BRANCH === "nightly"; 8 | const outDir = process.env.OUT_DIR || ""; 9 | 10 | process.env = { 11 | ...process.env, 12 | ...loadEnv(mode, process.cwd()), 13 | VITE_APP_NAME: appName, 14 | VITE_APP_VERSION: getFullVersion(isNightly), 15 | VITE_APP_VERSION_BRANCH: process.env.BRANCH || "", 16 | }; 17 | 18 | return { 19 | mode, 20 | resolve: { 21 | alias: { 22 | "@": path.resolve(__dirname, "src"), 23 | }, 24 | }, 25 | build: { 26 | emptyOutDir: false, 27 | outDir: "dist" + "/" + outDir, 28 | lib: { 29 | formats: ["iife"], 30 | entry: r("src/background/background.ts"), 31 | name: "seventv-background", 32 | }, 33 | rollupOptions: { 34 | output: { 35 | entryFileNames: "background.js", 36 | extend: true, 37 | }, 38 | }, 39 | }, 40 | }; 41 | }); 42 | -------------------------------------------------------------------------------- /vite.config.content.mts: -------------------------------------------------------------------------------- 1 | import { appName, getFullVersion, r } from "./vite.utils"; 2 | import path from "path"; 3 | import { defineConfig, loadEnv } from "vite"; 4 | 5 | export default defineConfig(() => { 6 | const mode = process.env.NODE_ENV ?? ""; 7 | const isDev = process.env.NODE_ENV === "dev"; 8 | const isNightly = process.env.BRANCH === "nightly"; 9 | const outDir = process.env.OUT_DIR || ""; 10 | const fullVersion = getFullVersion(isNightly); 11 | 12 | process.env = { 13 | ...process.env, 14 | ...loadEnv(mode, process.cwd()), 15 | VITE_APP_NAME: appName, 16 | VITE_APP_VERSION: fullVersion, 17 | VITE_APP_VERSION_BRANCH: process.env.BRANCH || "", 18 | VITE_APP_STYLESHEET_NAME: `seventv.style.${fullVersion}.css`, 19 | }; 20 | 21 | return { 22 | mode, 23 | resolve: { 24 | alias: { 25 | "@": path.resolve(__dirname, "src"), 26 | }, 27 | }, 28 | base: isDev ? "http://localhost:4777/" : "./", 29 | build: { 30 | emptyOutDir: false, 31 | outDir: "dist" + "/" + outDir, 32 | lib: { 33 | formats: ["iife"], 34 | entry: r("src/content/content.ts"), 35 | name: "seventv-content", 36 | }, 37 | rollupOptions: { 38 | output: { 39 | entryFileNames: "content.js", 40 | extend: true, 41 | }, 42 | }, 43 | }, 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /vite.config.worker.mts: -------------------------------------------------------------------------------- 1 | import { displayName as name } from "./package.json"; 2 | import { getFullVersion } from "./vite.utils"; 3 | import path from "path"; 4 | import { defineConfig, loadEnv } from "vite"; 5 | 6 | const r = (...args: string[]) => path.resolve(__dirname, ...args); 7 | 8 | export default defineConfig(() => { 9 | const mode = process.env.NODE_ENV; 10 | const outDir = process.env.OUT_DIR || ""; 11 | const isNightly = process.env.BRANCH === "nightly"; 12 | const fullVersion = getFullVersion(isNightly); 13 | 14 | process.env = { 15 | ...process.env, 16 | ...loadEnv(mode, process.cwd()), 17 | VITE_APP_NAME: name, 18 | VITE_APP_VERSION: fullVersion, 19 | VITE_APP_VERSION_BRANCH: process.env.BRANCH || "", 20 | }; 21 | 22 | process.stdout.write("Building worker...\n"); 23 | 24 | return { 25 | mode, 26 | resolve: { 27 | alias: { 28 | "@": path.resolve(__dirname, "src"), 29 | }, 30 | }, 31 | 32 | root: ".", 33 | build: { 34 | outDir: "dist" + "/" + outDir, 35 | emptyOutDir: false, 36 | write: true, 37 | rollupOptions: { 38 | input: { 39 | worker: r("src/worker/worker.root.ts"), 40 | }, 41 | output: { 42 | entryFileNames: "worker.js", 43 | }, 44 | }, 45 | }, 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /vite.utils.ts: -------------------------------------------------------------------------------- 1 | import { dev_version, displayName as name, version as packagedVersion } from "./package.json"; 2 | import path from "path"; 3 | 4 | export const r = (...args: string[]) => path.resolve(__dirname, ...args); 5 | 6 | export const appName = name; 7 | export const version = packagedVersion; 8 | export const getFullVersion = (nightly?: boolean, stage?: boolean) => { 9 | let v = version; 10 | if (stage) { 11 | v += "-dev-" + Date.now().toString(); 12 | } else if (nightly) { 13 | v += "." + (parseFloat(dev_version) * 1000).toFixed(0); 14 | } 15 | return v; 16 | }; 17 | 18 | const versionSplit = version.split("."); 19 | export const versionID = 20 | versionSplit.slice(0, 3).join("") + (versionSplit[3] ? `-${parseInt(versionSplit[3]) / 1000}` : ""); 21 | --------------------------------------------------------------------------------