diff --git a/.env.dev b/.env.dev
index db9ad56e8..b5939db6f 100644
--- a/.env.dev
+++ b/.env.dev
@@ -3,3 +3,4 @@ VITE_APP_API="http://localhost:3100/v3"
VITE_APP_API_GQL="http://localhost:3000/v3/gql"
VITE_APP_API_REST_OLD="http://localhost:3100/v2"
VITE_APP_API_EVENTS="ws://localhost:3700/v3"
+VITE_APP_HOST="http://localhost:8080"
diff --git a/.env.production b/.env.production
index ede96af97..54dab3655 100644
--- a/.env.production
+++ b/.env.production
@@ -3,3 +3,4 @@ VITE_APP_API="https://7tv.io/v3"
VITE_APP_API_GQL="https://7tv.io/v3/gql"
VITE_APP_API_REST_OLD="https://7tv.io/v2"
VITE_APP_API_EVENTS="wss://events.7tv.io/v3"
+VITE_APP_HOST="https://extension.7tv.gg"
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index ab241047c..a74d35c88 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -28,6 +28,11 @@ on:
default: false
description: "Upload to CWS/AMO"
+ deploy:
+ type: boolean
+ default: false
+ description: "Deploy Hosted Build"
+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -110,6 +115,10 @@ jobs:
- name: Structure Files
run: |
+ if [ -f ${{ steps.web-ext-build.outputs.target }} ]; then
+ mv ${{ steps.web-ext-build.outputs.target }} dist/ext.xpi
+ fi
+
mkdir -p dist/manifest
cp dist/mv2/manifest.json dist/manifest/manifest.mv2.json
cp dist/mv3/manifest.json dist/manifest/manifest.mv3.json
@@ -211,6 +220,41 @@ jobs:
name: v${{ fromJson(env.PACKAGE_JSON).version }}
tag: v${{ fromJson(env.PACKAGE_JSON).version }}
+ deploy:
+ name: Deploy Hosted Build
+ runs-on: aws-runner
+ needs: [ci, release]
+ if: ${{ inputs.deploy }}
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: "18"
+
+ - name: Install Yarn
+ run: npm install -g yarn
+
+ - name: Install Dependencies
+ run: |
+ yarn
+
+ - name: Build (Hosted)
+ env:
+ BRANCH: ${{ inputs.nightly && 'nightly' || '' }}
+ run: |
+ yarn build-hosted:prod
+
+ - name: Upload to Host
+ uses: shallwefootball/s3-upload-action@master
+ with:
+ endpoint: ${{ secrets.R2_API_ENDPOINT }}
+ aws_key_id: ${{ secrets.R2_API_AK }}
+ aws_secret_access_key: ${{ secrets.R2_API_SECRET }}
+ aws_bucket: 7tv-extension
+ source_dir: "dist-hosted/"
+ destination_dir: ""
+
push:
name: Submit to CWS/AMO
runs-on: aws-runner
@@ -224,12 +268,18 @@ jobs:
node-version: "18"
# Retrieve the non-CRX MV3 zip to be uploaded to CWS
- - name: Download Artifact (Chromium)
+ - name: Download Artifact (Build)
uses: actions/download-artifact@v3
with:
name: build
path: ext
+ - name: Download Artifact (Installable)
+ uses: actions/download-artifact@v3
+ with:
+ name: installable
+ path: ext
+
# Get the XPI File for Firefox
# It will be uploaded to AMO
- name: Download Manifest
@@ -257,6 +307,7 @@ jobs:
--refresh-token "$(sed '3q;d' c)"
- name: Upload to AMO
+ if: always()
uses: trmcnvn/firefox-addon@v1
with:
uuid: ${{ env.NIGHTLY_EXTENSION_ID_AMO }}
diff --git a/.gitignore b/.gitignore
index bb24689b9..173641977 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ lerna-debug.log*
node_modules
dist
dist-ssr
+dist-hosted
*.local
# Editor directories and files
diff --git a/CHANGELOG-nightly.md b/CHANGELOG-nightly.md
index 3b0495e48..5327d30d7 100644
--- a/CHANGELOG-nightly.md
+++ b/CHANGELOG-nightly.md
@@ -1,5 +1,8 @@
### Untitled Version
+**The changes listed here are not assigned to an official release**.
+
+- Added hot-patching functionality to the extension
- Fixed an issue which caused flickering when hovering on a deleted message
### Version 3.0.3.1000
diff --git a/package.json b/package.json
index 0d0a85728..a1ca7fca6 100644
--- a/package.json
+++ b/package.json
@@ -3,17 +3,20 @@
"displayName": "7TV",
"description": "Improve your viewing experience on Twitch & YouTube with new features, emotes, vanity and performance.",
"private": true,
- "version": "3.0.3",
- "dev_version": "1.0",
+ "version": "3.0.4",
+ "dev_version": "3.0",
"scripts": {
"start": "NODE_ENV=dev yarn build:dev && NODE_ENV=dev vite --mode dev",
"build:section:app": "vite build --config vite.config.ts",
"build:section:background": "vite build --config vite.config.background.ts",
"build:section:content": "vite build --config vite.config.content.ts",
"build:section:worker": "vite build -c vite.config.worker.ts",
+ "build-hosted:section:worker": "vite build --config vite.config.worker.ts",
+ "build-hosted:section:site": "vite build --config vite.config.hosted.ts",
"build:dev": "NODE_ENV=dev run-s build:section:*",
"build:prod": "NODE_ENV=production vue-tsc --noEmit && run-s build:section:*",
- "build:mv2:prod": "vue-tsc --noEmit && MV2=true vite build --minify es2021 && vite build -c vite.config.worker.ts --minify es2021",
+ "build-hosted:dev": "NODE_ENV=dev run-s build-hosted:section:*",
+ "build-hosted:prod": "NODE_ENV=production vue-tsc --noEmit && run-s build-hosted:section:*",
"watch:section:background": "vite build --config vite.config.background.ts --watch",
"watch:section:content": "vite build --config vite.config.content.ts --watch",
"watch:section:worker": "vite build --config vite.config.worker.ts --watch",
diff --git a/src/assets/blob/emoji.json b/public/assets/emoji/emoji.json
similarity index 100%
rename from src/assets/blob/emoji.json
rename to public/assets/emoji/emoji.json
diff --git a/src/assets/blob/emojis0.svg b/public/assets/emoji/emojis0.svg
similarity index 100%
rename from src/assets/blob/emojis0.svg
rename to public/assets/emoji/emojis0.svg
diff --git a/src/assets/blob/emojis1.svg b/public/assets/emoji/emojis1.svg
similarity index 100%
rename from src/assets/blob/emojis1.svg
rename to public/assets/emoji/emojis1.svg
diff --git a/src/assets/blob/emojis10.svg b/public/assets/emoji/emojis10.svg
similarity index 100%
rename from src/assets/blob/emojis10.svg
rename to public/assets/emoji/emojis10.svg
diff --git a/src/assets/blob/emojis2.svg b/public/assets/emoji/emojis2.svg
similarity index 100%
rename from src/assets/blob/emojis2.svg
rename to public/assets/emoji/emojis2.svg
diff --git a/src/assets/blob/emojis3.svg b/public/assets/emoji/emojis3.svg
similarity index 100%
rename from src/assets/blob/emojis3.svg
rename to public/assets/emoji/emojis3.svg
diff --git a/src/assets/blob/emojis4.svg b/public/assets/emoji/emojis4.svg
similarity index 100%
rename from src/assets/blob/emojis4.svg
rename to public/assets/emoji/emojis4.svg
diff --git a/src/assets/blob/emojis5.svg b/public/assets/emoji/emojis5.svg
similarity index 100%
rename from src/assets/blob/emojis5.svg
rename to public/assets/emoji/emojis5.svg
diff --git a/src/assets/blob/emojis6.svg b/public/assets/emoji/emojis6.svg
similarity index 100%
rename from src/assets/blob/emojis6.svg
rename to public/assets/emoji/emojis6.svg
diff --git a/src/assets/blob/emojis7.svg b/public/assets/emoji/emojis7.svg
similarity index 100%
rename from src/assets/blob/emojis7.svg
rename to public/assets/emoji/emojis7.svg
diff --git a/src/assets/blob/emojis8.svg b/public/assets/emoji/emojis8.svg
similarity index 100%
rename from src/assets/blob/emojis8.svg
rename to public/assets/emoji/emojis8.svg
diff --git a/src/assets/blob/emojis9.svg b/public/assets/emoji/emojis9.svg
similarity index 100%
rename from src/assets/blob/emojis9.svg
rename to public/assets/emoji/emojis9.svg
diff --git a/src/assets/svg/icons/CloudIcon.vue b/src/assets/svg/icons/CloudIcon.vue
new file mode 100644
index 000000000..90a6315f1
--- /dev/null
+++ b/src/assets/svg/icons/CloudIcon.vue
@@ -0,0 +1,8 @@
+
+
+
diff --git a/src/composable/chat/useChatEmotes.ts b/src/composable/chat/useChatEmotes.ts
index 1fd003e13..1f19560c2 100644
--- a/src/composable/chat/useChatEmotes.ts
+++ b/src/composable/chat/useChatEmotes.ts
@@ -2,28 +2,6 @@ import { reactive } from "vue";
import type { ChannelContext } from "@/composable/channel/useChannelContext";
import { useEmoji } from "@/composable/useEmoji";
-const { emojiList } = useEmoji();
-const emojiSets = {} as Record;
-const emojis = {} as Record;
-emojiList.forEach((e) => {
- const es = emojiSets[e.group];
- if (!es) {
- emojiSets[e.group] = {
- id: e.group,
- name: e.group,
- provider: "EMOJI",
- emotes: [],
- tags: [],
- immutable: true,
- privileged: true,
- flags: 0,
- };
- }
-
- emojiSets[e.group].emotes.push(e.emote);
- emojis[e.emote.name] = e.emote;
-});
-
interface ChatEmotes {
active: Record;
sets: Record;
@@ -32,6 +10,8 @@ interface ChatEmotes {
}
const m = new WeakMap();
+const emojiSets = {} as Record;
+const emojis = {} as Record;
export function useChatEmotes(ctx: ChannelContext) {
let x = m.get(ctx);
@@ -102,3 +82,25 @@ export function useChatEmotes(ctx: ChannelContext) {
return r;
}
+
+export function convertEmojis(): void {
+ const { emojiList } = useEmoji();
+ emojiList.forEach((e) => {
+ const es = emojiSets[e.group];
+ if (!es) {
+ emojiSets[e.group] = {
+ id: e.group,
+ name: e.group,
+ provider: "EMOJI",
+ emotes: [],
+ tags: [],
+ immutable: true,
+ privileged: true,
+ flags: 0,
+ };
+ }
+
+ emojiSets[e.group].emotes.push(e.emote);
+ emojis[e.emote.name] = e.emote;
+ });
+}
diff --git a/src/composable/useEmoji.ts b/src/composable/useEmoji.ts
index 8c15d78db..5dab2d056 100644
--- a/src/composable/useEmoji.ts
+++ b/src/composable/useEmoji.ts
@@ -1,5 +1,5 @@
-import { reactive } from "vue";
-import emojiList from "@/assets/blob/emoji.json";
+import { inject, reactive } from "vue";
+import { SITE_ASSETS_URL } from "@/common/Constant";
export interface Emoji {
codes: string;
@@ -14,25 +14,35 @@ export interface Emoji {
const emojiByName = new Map();
const emojiByCode = new Map();
-for (const e of emojiList) {
- const emoji = e as Emoji;
+const cached = [] as Emoji[];
+export async function loadEmojiList() {
+ if (cached.length) return cached;
- emoji.emote = {
- id: emoji.codes,
- name: emoji.name,
- unicode: emoji.char,
- provider: "EMOJI",
- flags: 0,
- } as SevenTV.ActiveEmote;
+ const assetsBase = inject(SITE_ASSETS_URL, "");
+ const data = (await (await fetch(assetsBase + "/emoji/emoji.json")).json().catch(() => void 0)) as Emoji[];
- emojiByName.set(emoji.name, emoji);
- emojiByCode.set(emoji.char, emoji);
+ for (const e of data) {
+ const emoji = e as Emoji;
+
+ emoji.emote = {
+ id: emoji.codes,
+ name: emoji.name,
+ unicode: emoji.char,
+ provider: "EMOJI",
+ flags: 0,
+ } as SevenTV.ActiveEmote;
+
+ emojiByName.set(emoji.name, emoji);
+ emojiByCode.set(emoji.char, emoji);
+ }
+
+ cached.push(...data);
}
-export const useEmoji = () => {
+export function useEmoji() {
return reactive({
- emojiList: emojiList as Emoji[],
+ emojiList: cached,
emojiByCode,
emojiByName,
});
-};
+}
diff --git a/src/composable/useWorker.ts b/src/composable/useWorker.ts
index 04bc3d412..07d22799c 100644
--- a/src/composable/useWorker.ts
+++ b/src/composable/useWorker.ts
@@ -44,9 +44,11 @@ async function init(bc: BroadcastChannel, originURL: string): Promise {
- log.error("Unable to fetch worker data", err);
- });
+ const data = await fetch(workerURL || "")
+ .then((r) => r.blob())
+ .catch((err) => {
+ log.error("Unable to fetch worker data", err);
+ });
if (!data) return Promise.reject("There was an error fetching worker data");
log.info("Received worker data", `(${data.size} bytes)`);
diff --git a/src/content/content.ts b/src/content/content.ts
index bbf7de4c1..989794b33 100644
--- a/src/content/content.ts
+++ b/src/content/content.ts
@@ -1,4 +1,5 @@
import { APP_BROADCAST_CHANNEL } from "@/common/Constant";
+import { insertEmojiVectors } from "./emoji";
// Inject extension into site
const inject = () => {
@@ -25,6 +26,11 @@ const inject = () => {
}
(document.head || document.documentElement).appendChild(script);
+
+ // Insert emojis
+ setTimeout(() => {
+ insertEmojiVectors();
+ }, 1e3);
};
const bc = new BroadcastChannel(APP_BROADCAST_CHANNEL);
diff --git a/src/content/emoji.ts b/src/content/emoji.ts
new file mode 100644
index 000000000..d8db600c6
--- /dev/null
+++ b/src/content/emoji.ts
@@ -0,0 +1,27 @@
+/**
+ * Inserts the emoji vectors into the DOM.
+ */
+export async function insertEmojiVectors(): Promise {
+ const container = document.createElement("div");
+ container.id = "seventv-emoji-container";
+ container.style.display = "none";
+ container.style.position = "fixed";
+ container.style.top = "-1px";
+ container.style.left = "-1px";
+
+ // Get path to emoji blocks in assets
+ const base = chrome.runtime.getURL("assets/emoji");
+ const blocks = 11;
+
+ for (let i = 0; i < blocks; i++) {
+ const data = (await fetch(base + "/emojis" + i + ".svg")).text();
+
+ const element = document.createElement("div");
+ element.id = "emojis" + i;
+ element.innerHTML = await data;
+
+ container.appendChild(element);
+ }
+
+ document.head.appendChild(container);
+}
diff --git a/src/site/App.vue b/src/site/App.vue
index d073244df..32dda03e1 100644
--- a/src/site/App.vue
+++ b/src/site/App.vue
@@ -4,10 +4,6 @@
-
-
-
-
@@ -24,14 +20,15 @@ import { markRaw, onMounted, ref } from "vue";
import { APP_BROADCAST_CHANNEL, SITE_WORKER_URL } from "@/common/Constant";
import { log } from "@/common/Logger";
import { db } from "@/db/idb";
+import { convertEmojis } from "@/composable/chat/useChatEmotes";
+import { loadEmojiList } from "@/composable/useEmoji";
import { useFrankerFaceZ } from "@/composable/useFrankerFaceZ";
import { fillSettings, useConfig, useSettings } from "@/composable/useSettings";
import { useWorker } from "@/composable/useWorker";
import Global from "./global/Global.vue";
-import YouTubeSite from "./youtube.com/YouTubeSite.vue";
-const EmojiContainer = defineAsyncComponent(() => import("@/site/EmojiContainer.vue"));
const TwitchSite = defineAsyncComponent(() => import("@/site/twitch.tv/TwitchSite.vue"));
+const YouTubeSite = defineAsyncComponent(() => import("@/site/youtube.com/YouTubeSite.vue"));
if (import.meta.hot) {
import.meta.hot.on("full-reload", () => {
@@ -89,6 +86,9 @@ bc.addEventListener("message", (ev) => {
}
});
+// Load emojis
+loadEmojiList().then(() => convertEmojis());
+
log.setContextName(`site/${domain}`);
onMounted(() => {
diff --git a/src/site/EmojiContainer.vue b/src/site/EmojiContainer.vue
deleted file mode 100644
index 4d79751f3..000000000
--- a/src/site/EmojiContainer.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/src/site/global/settings/SettingsViewHome.vue b/src/site/global/settings/SettingsViewHome.vue
index 9d5fd932c..b7a2600d0 100644
--- a/src/site/global/settings/SettingsViewHome.vue
+++ b/src/site/global/settings/SettingsViewHome.vue
@@ -9,7 +9,12 @@
@@ -28,6 +33,7 @@
import { storeToRefs } from "pinia";
import { useStore } from "@/store/main";
import Changelog from "@/site/global/Changelog.vue";
+import CloudIcon from "@/assets/svg/icons/CloudIcon.vue";
import UiScrollable from "@/ui/UiScrollable.vue";
const { theme } = storeToRefs(useStore());
@@ -36,6 +42,7 @@ const appName = import.meta.env.VITE_APP_NAME;
const appContainer = import.meta.env.VITE_APP_CONTAINER ?? "Extension";
const appServer = import.meta.env.VITE_APP_API ?? "Offline";
const version = import.meta.env.VITE_APP_VERSION;
+const isRemote = seventv.remote || false;
const twitterScript = document.createElement("script");
twitterScript.async = true;
@@ -75,6 +82,13 @@ document.head.appendChild(twitterScript);
align-items: center;
color: hsla(0deg, 0%, 50%, 90%);
}
+
+ .seventv-version-remote {
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 0.5rem;
+ color: rgba(70, 225, 150, 100%);
+ }
}
}
diff --git a/src/site/site.app.ts b/src/site/site.app.ts
new file mode 100644
index 000000000..e54bc7e0a
--- /dev/null
+++ b/src/site/site.app.ts
@@ -0,0 +1,76 @@
+import { createApp, h, provide } from "vue";
+import { createPinia } from "pinia";
+import { SITE_ASSETS_URL, SITE_EXT_OPTIONS_URL, SITE_WORKER_URL } from "@/common/Constant";
+import App from "@/site/App.vue";
+import { apolloClient } from "@/apollo/apollo";
+import { TextPaintDirective } from "@/directive/TextPaintDirective";
+import { TooltipDirective } from "@/directive/TooltipDirective";
+import { setupI18n } from "@/i18n";
+import { ApolloClients } from "@vue/apollo-composable";
+
+if (!("seventv" in window)) {
+ (window as Window & { seventv?: SeventvGlobalScope }).seventv = { host_manifest: null };
+}
+
+const appID = Date.now().toString();
+
+// Sanity Check
+//
+// Detect duplicate instances
+const roots = document.querySelectorAll("#seventv-root, script#seventv");
+
+let dupeCount = 0;
+for (let i = 0; i < roots.length; i++) {
+ const root = roots.item(i);
+ if (!root) continue;
+
+ const rootID = root.getAttribute("data-app-id");
+ if (root.tagName === "script" || (rootID && rootID === appID)) continue;
+
+ dupeCount++;
+}
+
+if (dupeCount > 0) {
+ const oldTitle = document.title;
+ document.title = "BIG PROBLEM - 7TV - Twitch";
+ alert(
+ "[7TV] Woah there! It seems you're running multiple different instances of 7TV. Please disable any other version of the extension and try again.",
+ );
+
+ document.title = oldTitle;
+ throw new Error("Duplicate 7TV instances detected, aborting");
+}
+
+// Create Vue App
+const root = document.createElement("div");
+root.id = "seventv-root";
+root.setAttribute("data-app-id", appID);
+
+document.body.append(root);
+
+const scr = document.querySelector("script#seventv-extension");
+
+const app = createApp({
+ setup() {
+ provide(ApolloClients, {
+ default: apolloClient,
+ });
+ },
+ render: () => h(App),
+});
+
+app.provide("app-id", appID);
+
+const extensionOrigin = scr?.getAttribute("extension_origin") ?? "";
+app.provide(
+ SITE_WORKER_URL,
+ seventv.remote ? seventv.host_manifest?.worker_file ?? null : null ?? scr?.getAttribute("worker_url"),
+);
+app.provide(SITE_ASSETS_URL, extensionOrigin + "assets");
+app.provide(SITE_EXT_OPTIONS_URL, extensionOrigin + "index.html");
+
+app.use(createPinia())
+ .use(setupI18n())
+ .directive("tooltip", TooltipDirective)
+ .directive("cosmetic-paint", TextPaintDirective)
+ .mount("#seventv-root");
diff --git a/src/site/site.ts b/src/site/site.ts
index 9f775a3fd..75db47eac 100644
--- a/src/site/site.ts
+++ b/src/site/site.ts
@@ -1,69 +1,45 @@
-import { createApp, h, provide } from "vue";
-import { createPinia } from "pinia";
-import { SITE_ASSETS_URL, SITE_EXT_OPTIONS_URL, SITE_WORKER_URL } from "@/common/Constant";
-import App from "@/site/App.vue";
-import { apolloClient } from "@/apollo/apollo";
-import { TextPaintDirective } from "@/directive/TextPaintDirective";
-import { TooltipDirective } from "@/directive/TooltipDirective";
-import { setupI18n } from "@/i18n";
-import { ApolloClients } from "@vue/apollo-composable";
-
-const appID = Date.now().toString();
-
-// Sanity Check
-//
-// Detect duplicate instances
-const roots = document.querySelectorAll("#seventv-root, script#seventv");
-
-let dupeCount = 0;
-for (let i = 0; i < roots.length; i++) {
- const root = roots.item(i);
- if (!root) continue;
-
- const rootID = root.getAttribute("data-app-id");
- if (root.tagName === "script" || (rootID && rootID === appID)) continue;
-
- dupeCount++;
-}
-
-if (dupeCount > 0) {
- const oldTitle = document.title;
- document.title = "BIG PROBLEM - 7TV - Twitch";
- alert(
- "[7TV] Woah there! It seems you're running multiple different instances of 7TV. Please disable any other version of the extension and try again.",
- );
-
- document.title = oldTitle;
- throw new Error("Duplicate 7TV instances detected, aborting");
-}
-
-// Create Vue App
-const root = document.createElement("div");
-root.id = "seventv-root";
-root.setAttribute("data-app-id", appID);
-
-document.body.append(root);
-
-const scr = document.querySelector("script#seventv-extension");
-
-const app = createApp({
- setup() {
- provide(ApolloClients, {
- default: apolloClient,
- });
- },
- render: () => h(App),
-});
-
-app.provide("app-id", appID);
-
-const extensionOrigin = scr?.getAttribute("extension_origin") ?? "";
-app.provide(SITE_WORKER_URL, scr?.getAttribute("worker_url"));
-app.provide(SITE_ASSETS_URL, extensionOrigin + "assets");
-app.provide(SITE_EXT_OPTIONS_URL, extensionOrigin + "index.html");
-
-app.use(createPinia())
- .use(setupI18n())
- .directive("tooltip", TooltipDirective)
- .directive("cosmetic-paint", TextPaintDirective)
- .mount("#seventv-root");
+import { log } from "@/common/Logger";
+import { semanticVersionToNumber } from "@/common/Transform";
+
+(async () => {
+ const manifestURL = `${import.meta.env.VITE_APP_HOST}/manifest${
+ import.meta.env.VITE_APP_VERSION_BRANCH ? "." + import.meta.env.VITE_APP_VERSION_BRANCH.toLowerCase() : ""
+ }.json`;
+
+ const manifest = await fetch(manifestURL)
+ .then((res) => res.json())
+ .catch((err) => log.error("", "Failed to fetch host manifest", err.message));
+
+ const localVersion = semanticVersionToNumber(import.meta.env.VITE_APP_VERSION);
+ const hostedVersion = manifest ? semanticVersionToNumber(manifest.version) : 0;
+
+ (window as Window & { seventv?: SeventvGlobalScope }).seventv = {
+ host_manifest: manifest ?? null,
+ };
+
+ if (manifest && hostedVersion > localVersion) {
+ seventv.remote = true;
+
+ const scr = document.createElement("script");
+ scr.id = "seventv-site-hosted";
+ scr.src = manifest.index_file;
+ scr.type = "module";
+
+ const style = document.createElement("link");
+ style.rel = "stylesheet";
+ style.type = "text/css";
+ style.href = manifest.stylesheet_file;
+ style.setAttribute("charset", "utf-8");
+ style.setAttribute("content", "text/html");
+ style.setAttribute("http-equiv", "content-type");
+ style.id = "seventv-stylesheet";
+
+ document.head.appendChild(style);
+ document.head.appendChild(scr);
+
+ log.info("", "Using Hosted Mode,", "v" + manifest.version);
+ } else {
+ import("./site.app");
+ log.info("", "Using Local Mode,", "v" + import.meta.env.VITE_APP_VERSION);
+ }
+})();
diff --git a/src/types/app.d.ts b/src/types/app.d.ts
index 7762b7703..702f609be 100644
--- a/src/types/app.d.ts
+++ b/src/types/app.d.ts
@@ -1,3 +1,15 @@
+declare interface SeventvGlobalScope {
+ host_manifest: null | {
+ version: string;
+ worker_file: string;
+ stylesheet_file: string;
+ index_file: string;
+ };
+ remote?: boolean;
+}
+
+declare const seventv: SeventvGlobalScope;
+
declare namespace SevenTV {
interface Emote {
id: ObjectID;
diff --git a/vite.config.hosted.ts b/vite.config.hosted.ts
new file mode 100644
index 000000000..90aa3a756
--- /dev/null
+++ b/vite.config.hosted.ts
@@ -0,0 +1,111 @@
+import { appName, getFullVersion, r } from "./vite.utils";
+import vuei18n from "@intlify/unplugin-vue-i18n/vite";
+import vue from "@vitejs/plugin-vue";
+import fs from "fs-extra";
+import path from "path";
+import { defineConfig, loadEnv } from "vite";
+
+export default defineConfig(() => {
+ const mode = process.env.NODE_ENV ?? "";
+ const isNightly = process.env.BRANCH === "nightly";
+ const outDir = process.env.OUT_DIR || "";
+ const fullVersion = getFullVersion(isNightly);
+
+ const stylesheetFileName = `seventv.style.${fullVersion}.css`;
+ const indexFileName = `index.${fullVersion}.js`;
+
+ process.env = {
+ ...process.env,
+ ...loadEnv(mode, process.cwd()),
+ VITE_APP_NAME: appName,
+ VITE_APP_VERSION: fullVersion,
+ VITE_APP_VERSION_BRANCH: process.env.BRANCH,
+ VITE_APP_STYLESHEET_NAME: stylesheetFileName,
+ VITE_APP_CHANGELOG: fs.readFileSync(
+ r(
+ {
+ nightly: "CHANGELOG-nightly.md",
+ }[process.env.BRANCH ?? ""] ?? "CHANGELOG.md",
+ ),
+ "utf-8",
+ ),
+ };
+
+ return {
+ mode,
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "src"),
+ "@locale": path.resolve(__dirname, "locale"),
+ "vue-i18n": "vue-i18n/dist/vue-i18n.runtime.esm-bundler.js",
+ },
+ },
+ define: {
+ "process.env": {},
+ },
+ build: {
+ emptyOutDir: true,
+ outDir: "dist-hosted" + "/" + outDir,
+ lib: {
+ formats: ["es"],
+ entry: r("src/site/site.app.ts"),
+ fileName: () => `v${fullVersion}/index.${fullVersion}.js`,
+ name: "seventv-site",
+ },
+ rollupOptions: {
+ output: {
+ inlineDynamicImports: false,
+ assetFileNames: `v${fullVersion}/seventv.[name].${fullVersion}[extname]`,
+ chunkFileNames: `v${fullVersion}/seventv.[name].${fullVersion}.js`,
+
+ sanitizeFileName: (name: string) => name.toLowerCase(),
+
+ manualChunks: {
+ "site.twitch.tv": ["src/site/twitch.tv/TwitchSite.vue"],
+ "site.youtube.com": ["src/site/youtube.com/YouTubeSite.vue"],
+ "site.global": ["src/site/global/Global.vue"],
+ },
+ },
+ },
+ },
+
+ plugins: [
+ vue(),
+ vuei18n({
+ include: r("./locale/*"),
+ }),
+
+ // Create hosted manifest
+ {
+ name: "create-hosted-manifest",
+ async writeBundle(this) {
+ const man = {
+ version: getFullVersion(isNightly),
+ index_file: `${process.env.VITE_APP_HOST}/v${fullVersion}/${indexFileName}`,
+ stylesheet_file: `${process.env.VITE_APP_HOST}/v${fullVersion}/${stylesheetFileName}`,
+ worker_file: `${process.env.VITE_APP_HOST}/v${fullVersion}/worker.${fullVersion}.js`,
+ };
+
+ const manifestName = process.env.BRANCH
+ ? `manifest.${process.env.BRANCH.toLowerCase()}.json`
+ : "manifest.json";
+
+ setTimeout(() => {
+ const p = r("dist-hosted") + (outDir ? "/" + outDir : "");
+
+ // Copy worker to version scope (if it's there)
+ const workerPath = r("dist/worker.js");
+ if (fs.existsSync(workerPath)) {
+ fs.copySync(workerPath, `${p}/v${fullVersion}/worker.${fullVersion}.js`);
+ } else {
+ man.worker_file = "";
+ }
+
+ // Set up manifest
+ fs.writeJSONSync(p + "/" + manifestName, man);
+ });
+ },
+ },
+ ],
+ };
+});
diff --git a/vite.config.ts b/vite.config.ts
index bdd92c9ce..9181af0e7 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -22,7 +22,7 @@ const ignoreHMR = [
"SettingsModule.vue",
];
-const alwaysHot = ["src/background/background.ts"];
+const alwaysHot = ["src/background/background.ts", "src/content/content.ts", "src/content/emoji.ts"];
// https://vitejs.dev/config/
export default defineConfig(() => {