[
  {
    "path": ".gitignore",
    "content": "extension.zip\r\n.DS_Store\r\nnode_modules\r\n"
  },
  {
    "path": "PrivacyPolicyForEdgeWebstore.md",
    "content": "# Privacy Policy\n\nI don't collect any data.\n\nThere are no network requests done by the extension nor any data read from websites.\nYou can look at the source, there is no way I could get your data 🤷‍♀️\n"
  },
  {
    "path": "README.md",
    "content": "# ![Windowed Logo](extension/Icons/Icon_32.png) Windowed\n\n[Install in Chrome Webstore](https://chrome.google.com/webstore/detail/windowed-floating-youtube/gibipneadnbflmkebnmcbgjdkngkbklb)  \n[Install in Firefox](https://addons.mozilla.org/firefox/addon/windowed/)  \n[Install in Edge](https://microsoftedge.microsoft.com/addons/detail/windowed-floating-youtu/kfaokmgjemianbbeadblgppcedfihdnb)\n\nA small extension for chrome and firefox I made because I don't really like normal fullscreen. It injects itself into every page, and replaces `HTMLElement.prototype.requestFullscreen` (or the browser specific version) with a popup to get into Windowed, In-window or picture in picture mode. Important for me is that the video/page does not reload when switching to Windowed.\n\n![Chromestore screenshot](Chromewebstore%20screenshot%20%231.png)\n\n### Modes\n\nEven though I want to create a very minimalist extension, I still provide some choice every time you want to go into fullscreen.\n\n- **Windowed** <kbd>w</kbd>  \n  Obviously my favourite, hence the namesake of this extension. Puts video in a seperate window with title bar. I can't get rid of the title bar because of restrictions in Chrome extensions. Other extensions do put videos in title-bar-less windows, but that requires a reload of the page, and I really really really don't like that. Also it does not float. Floating also, is exclusive to reload\n- **In-Window** <kbd>i</kbd>  \n  Does the same as Windowed, but doesn't pop the window out. So it will fill the full window that as it is right now.\n- **Fullscreen** <kbd>f</kbd>  \n  Get out, ya joker\n- **Picture in Picture** <kbd>p</kbd>  \n  This only shows up if a video element is found. Will put the video into native Picture-in-Picture mode, with only browser controls. This is amazing in most cases, butttttt here is one big disadvantage, the only reason I'm still working on this extension even, is that it **only works on videos**, and only **without any website specific controls**. For youtube, that doesn't really stack up against the floating-on-top feature for me. Still, there are many websites that do put something else (not a video) in fullscreen, which picture-in-picture does not support.  \n  Either picture-in-picture needs to work for arbitrary elements, which would be really awesome, or chrome extensions should be able to make floating-on-top windows (they used to). Until then, we are stuck with two modes for just slightly different situations\n\n### Why so much code\n\nThere are actually a lot of things that the websites need when I'm fullscreen-ing them.\n\n1. There is mostly a lot of code for the popup that allows you choose between Windowed and fullscreen.\n2. Some of them remove the component we want to fullscreen as soon as we resize, in which case I copy the element and try to show it anyway.\n3. When clicking inside a frame, it should fullscreen that frame inside the parent window.\n4. Need to be able to disable domain by clicking the Windowed toolbar button, and I want the icon to update accordingly on every tab.\n5. Even just selecting the right window to restore the tab to after going out of windowed mode again takes more lines then you'd expect.\n6. Firefox has some restrictions on requesting fullscreen from async functions, so had to work around that by using sync functions first where necessary.\n\n### Hey I got a request or even got something that might be useful\n\nNice. Please open an issue or a PR. That'd be really cool. 😎\n\n---\n\nThanks for reading,  \nMichiel Dral\n"
  },
  {
    "path": "Testing.md",
    "content": "# Testing\n\nIn both browsers currently supported (Chrome and Firefox)\nI should test these things:\n\n### Basic usage\n1. Activate the extension\n2. click a fullscreen button (eg. on youtube)\n3. Try all three options (Windowed, in window and fullscreen)\n4. Every option should transform the window into the right \"format\"\n5. Click the \"un-fullscreen\" button, and it should transform back to a normal tab again.\n\n### Disabling\n1. Activate the extension\n2. Go to youtube\n3. Press the \"Browser action\" (thus, disabling the extension on youtube)\n4. Press the fullscreen button\n5. No option menu should appear now, it goes into normal fullscreen\n6. Go out of fullscreen again\n7. Refresh the page\n8. Press the fullscreen button\n9. No option menu should appear now, it goes into normal fullscreen\n10. Go out of fullscreen again\n"
  },
  {
    "path": "extension/Background/BackgroundModule.js",
    "content": "// Import is not yet allowed in firefox, so for now I put tint_image in manifest.json\nimport { icon_theme_color_chrome } from \"./theme-color/chrome.js\";\nimport { icon_theme_color_firefox } from \"./theme-color/firefox.js\";\nimport { tint_image } from \"./tint_image.js\";\n// import { browser } from \"../Vendor/Browser.js\";\n\n/**\n * @param {import(\"webextension-polyfill\").Tabs.Tab} tab\n * @returns {Promise<string>}\n */\nlet icon_theme_color = async (tab) => {\n  if (await is_firefox) {\n    return await icon_theme_color_firefox(tab);\n  } else {\n    return await icon_theme_color_chrome(tab);\n  }\n};\n\n/** @type {import(\"webextension-polyfill\").Browser} */\n// @ts-ignore\nlet browser = chrome;\n\nlet BROWSERACTION_ICON = \"/Images/Icon_Windowed_Mono@1x.png\";\n\nlet browser_info_promise = browser.runtime.getBrowserInfo\n  ? browser.runtime.getBrowserInfo()\n  : Promise.resolve({ name: \"Chrome\" });\nlet is_firefox = browser_info_promise.then(\n  (browser_info) => browser_info.name === \"Firefox\",\n);\n\n/**\n * @param {import(\"webextension-polyfill\").Windows.Window} window\n */\nlet is_valid_window = (window) => {\n  return (\n    window.incognito === false &&\n    window.type === \"normal\" &&\n    window.state !== \"minimized\"\n  );\n};\n\n/**\n * Firefox can't take the `focused` property to browser.windows.create/update\n * So I just take it out when using firefox 🤷‍♀️\n * @param {import(\"webextension-polyfill\").Windows.CreateCreateDataType} window_properties\n * @returns {Promise<import(\"webextension-polyfill\").Windows.CreateCreateDataType>}\n */\nlet firefix_window = async (window_properties) => {\n  let is_it_firefox = await is_firefox;\n  if (is_it_firefox) {\n    let { focused, ...good_properties } = window_properties;\n    return good_properties;\n  } else {\n    return window_properties;\n  }\n};\n\n// Get a window to put our tab on: either the last focussed, a random, or none;\n// In case of none being found, null is returned and the caller should make a new window himself (with the tab attached)\n/**\n * @param {number} windowId\n * @returns {Promise<import(\"webextension-polyfill\").Windows.Window>}\n */\nconst get_fallback_window = async (windowId) => {\n  const first_fallback_window = await browser.windows.getLastFocused({\n    // @ts-ignore\n    windowTypes: [\"normal\"],\n  });\n\n  if (\n    first_fallback_window.id !== windowId &&\n    is_valid_window(first_fallback_window)\n  ) {\n    return first_fallback_window;\n  } else {\n    const windows = await browser.windows.getAll({ windowTypes: [\"normal\"] });\n    const right_window = windows\n      .filter((x) => is_valid_window(x))\n      .filter((x) => x.id !== windowId)\n      .sort((a, b) => a.tabs.length - b.tabs.length)[0];\n\n    if (right_window) {\n      return right_window;\n    } else {\n      return null;\n    }\n  }\n};\n\n// TODO Instead of using this static height, I can maybe \"ping\" the page I'm popup-izing\n// after it is done becoming a popup: then it can figure out it's position itself\n// (and check the size of it's current header itself)\nconst Chrome_Popup_Menubar_Height = 22; // Do `window.outerHeight - window.innerHeight` in a popup tab\n\n/**\n * @typedef WindowedMode\n * @type {\"fullscreen\" | \"windowed\" | \"in-window\" | \"fullscreen\" | \"ask\"}\n */\n\n/**\n * @param {string} mode\n * @param {boolean} disabled\n * @returns {WindowedMode}\n */\nlet clean_mode = (mode, disabled) => {\n  // Any other mode than the known ones are ignored\n  if (mode == \"fullscreen\" || mode == \"windowed\" || mode == \"in-window\") {\n    return mode;\n  }\n  return disabled === true ? \"fullscreen\" : \"ask\";\n};\n\nlet ALL_MODE = \"mode(*)\";\nlet ALL_PIP = \"pip(*)\";\n\n/** @param {import(\"webextension-polyfill\").Tabs.Tab} tab */\nlet get_host_config = async (tab) => {\n  let host = new URL(tab.url).host;\n  let host_mode = `mode(${host})`;\n  let host_pip = `pip(${host})`;\n  let {\n    [host_mode]: mode,\n    [host]: disabled,\n    [host_pip]: pip,\n    [ALL_MODE]: all_mode,\n    [ALL_PIP]: all_pip,\n  } = /** @type {any} */ (\n    await browser.storage.sync.get([\n      host_mode,\n      host,\n      host_pip,\n      ALL_MODE,\n      ALL_PIP,\n    ])\n  ) ?? {};\n\n  return {\n    mode: clean_mode(mode ?? all_mode, disabled),\n    pip: (pip ?? all_pip) === true,\n    all_mode: clean_mode(all_mode, false),\n    all_pip: all_pip === true,\n  };\n};\n\n/**\n * Wrapper to do some basic routing on extension messaging\n * @param {string} type\n * @param {(message: any, sender: import(\"webextension-polyfill\").Runtime.MessageSender) => Promise<any>} fn\n * @return {void}\n */\nlet onMessage = (type, fn) => {\n  browser.runtime.onMessage.addListener((message, sender, sendResponse) => {\n    // @ts-ignore\n    if (message?.type === type) {\n      fn(message, sender)\n        .then((result) => {\n          return { type: \"resolve\", value: result };\n        })\n        .catch((err) => {\n          return {\n            type: \"reject\",\n            value: { message: err.message, stack: err.stack },\n          };\n        })\n        .then((x) => sendResponse(x));\n      return true;\n    }\n  });\n};\n\nonMessage(\"update_windowed_button\", async (message, sender) => {\n  let tabs = message.id\n    ? [await browser.tabs.get(message.id)]\n    : await browser.tabs.query(message.query);\n  for (let tab of tabs) {\n    await update_button_on_tab(tab);\n  }\n});\n\nonMessage(\"get_windowed_config\", async (message, sender) => {\n  return await get_host_config(sender.tab);\n});\n\nonMessage(\"get_theme\", async (message, sender) => {\n  return await browser.theme.getCurrent();\n});\n\n/** Detatch the current tab and put it into a standalone popup window */\nonMessage(\"please_make_me_a_popup\", async (message, sender) => {\n  // TODO Save windowId and index inside that window,\n  // so when you \"pop\" it back, it will go where you opened it\n  let {\n    left: screenLeft,\n    top: screenTop,\n    type: windowType,\n  } = await browser.windows.get(sender.tab.windowId);\n\n  await TabOrigins.set(sender.tab.id, {\n    windowId: sender.tab.windowId,\n    index: sender.tab.index,\n    pinned: sender.tab.pinned,\n  });\n\n  // TODO Check possible 'panel' support in firefox\n  let frame = message.position;\n  if (windowType === \"popup\") {\n    // Already a popup, no need to re-create the window\n    await browser.windows.update(\n      sender.tab.windowId,\n      await firefix_window({\n        focused: true,\n        left: Math.round(screenLeft + frame.left),\n        top: Math.round(screenTop + frame.top - Chrome_Popup_Menubar_Height),\n        width: Math.round(frame.width),\n        height: Math.round(frame.height + Chrome_Popup_Menubar_Height),\n      }),\n    );\n    return;\n  }\n\n  const created_window = await browser.windows.create(\n    await firefix_window({\n      tabId: sender.tab.id,\n      type: \"popup\",\n      focused: true,\n      left: Math.round(screenLeft + frame.left),\n      top: Math.round(screenTop + frame.top - Chrome_Popup_Menubar_Height),\n      width: Math.round(frame.width),\n      height: Math.round(frame.height + Chrome_Popup_Menubar_Height),\n    }),\n  );\n\n  if (await is_firefox) {\n    await browser.windows.update(created_window.id, {\n      titlePreface: \"Windowed: \",\n      focused: true,\n    });\n  }\n  return;\n});\n\n/**\n * @typedef TabOrigin\n * @type {{\n *   windowId: import(\"webextension-polyfill\").Windows.Window[\"id\"]\n *   pinned: import(\"webextension-polyfill\").Tabs.Tab[\"pinned\"]\n *   index: import(\"webextension-polyfill\").Tabs.Tab[\"index\"]\n * }}\n */\nlet TabOrigins = {\n  /**\n   * @param {import(\"webextension-polyfill\").Tabs.Tab[\"id\"]} tabId\n   * @returns {string}\n   */\n  key: (tabId) => `tab_origin(${tabId})`,\n  /**\n   * @param {import(\"webextension-polyfill\").Tabs.Tab[\"id\"]} tabId\n   * @returns {Promise<TabOrigin | undefined>}\n   */\n  get: async (tabId) => {\n    let key = TabOrigins.key(tabId);\n    let { [key]: data } = await browser.storage.session.get(key);\n    return /** @type {any} */ (data);\n  },\n  /**\n   * @param {import(\"webextension-polyfill\").Tabs.Tab[\"id\"]} tabId\n   * @param {TabOrigin} tab_origin\n   */\n  set: async (tabId, tab_origin) => {\n    let key = TabOrigins.key(tabId);\n    await browser.storage.session.set({ [key]: tab_origin });\n  },\n};\n\n/**\n * Take the current tab, and put it into a tab-ed window again.\n * 1. Last focussed window\n * 2. Other tab-containing window (not popups without tab bar)\n * 3. New window we create\n */\nonMessage(\"please_make_me_a_tab_again\", async (message, sender) => {\n  let { type: windowType } = await browser.windows.get(sender.tab.windowId);\n  if (windowType === \"normal\") {\n    return;\n  }\n\n  let origin = await TabOrigins.get(sender.tab.id);\n  if (origin != undefined) {\n    let window = await browser.windows.get(origin.windowId).catch(() => null);\n    if (window) {\n      await browser.tabs.move(sender.tab.id, {\n        windowId: origin.windowId,\n        index: origin.index,\n      });\n      await browser.tabs.update(sender.tab.id, {\n        active: true,\n        pinned: origin.pinned,\n      });\n      return;\n    }\n  }\n\n  let fallback_window = await get_fallback_window(sender.tab.windowId);\n\n  if (fallback_window) {\n    await browser.tabs.move(sender.tab.id, {\n      windowId: fallback_window.id,\n      index: -1,\n    });\n    await browser.tabs.update(sender.tab.id, { active: true });\n  } else {\n    // No other window open: create a new window with tabs\n    let create_window_with_tabs = await browser.windows.create({\n      tabId: sender.tab.id,\n      type: \"normal\",\n    });\n  }\n});\n\n/** @type {{ [tabid: number]: Promise<boolean> }} */\nlet current_port_promises = {};\n/**\n * Check if we can connect with the Windowed content script in a tab\n * @param {number} tabId\n * @returns {Promise<boolean>}\n */\nlet ping_content_script = async (tabId) => {\n  try {\n    if (current_port_promises[tabId] != null) {\n      return await current_port_promises[tabId];\n    } else {\n      current_port_promises[tabId] = new Promise((resolve, reject) => {\n        let port = browser.tabs.connect(tabId);\n        port.onMessage.addListener((message) => {\n          resolve(true);\n          port.disconnect();\n        });\n        port.onDisconnect.addListener((p) => {\n          /// Just need to check for errors so chrome doesn't show a warning\n          browser.runtime.lastError;\n          resolve(false);\n        });\n      });\n      return await current_port_promises[tabId];\n    }\n  } finally {\n    delete current_port_promises[tabId];\n  }\n};\n\n/**\n * This looks a bit weird (and honestly it is) but it opens a port to the content script on a page,\n * and then the page knows it should reload it's settings.\n * TODO? Should I close the port?\n * @param {number} tabId\n * @param {any} properties\n */\nlet notify_tab_state = async (tabId, properties) => {\n  let port = browser.tabs.connect(tabId);\n  // port.postMessage(JSON.stringify({ method: 'notify', data: properties }))\n};\n\n/**\n * Shorthand for setting the browser action icon on a tab\n * @param {number} tabId\n * @param {{ icon: ImageData, title: string }} action\n */\nlet apply_browser_action = async (tabId, action) => {\n  try {\n    await browser.action.setIcon({\n      tabId: tabId,\n      // @ts-ignore\n      imageData: action.icon,\n    });\n    await browser.action.setTitle({\n      tabId: tabId,\n      title: action.title,\n    });\n  } catch (error) {\n    console.log(`Setting browser action ERROR:`, error);\n  }\n};\n\n/**\n * @param {import(\"webextension-polyfill\").Tabs.Tab} tab\n */\nlet update_button_on_tab = async (tab) => {\n  let has_contentscript_active =\n    tab.status === \"complete\" && (await ping_content_script(tab.id));\n\n  // A specific exception for about:blank so, on firefox,\n  // when you customize your menu bar, windowed is at it's most beautiful.\n  if (has_contentscript_active === false && tab.url === \"about:blank\") {\n    await apply_browser_action(tab.id, {\n      icon: await tint_image(BROWSERACTION_ICON, await icon_theme_color(tab)),\n      title: `Windowed`,\n    });\n    return;\n  }\n\n  // On some domains windowed will never work, because it is blocked by the browser.\n  // To avoid user confusion with the \"You have to reload\" message,\n  // I show a descriptive error 💁‍♀️\n  if (\n    has_contentscript_active === false &&\n    (tab.url.match(/^about:/) ||\n      tab.url.match(/^chrome:\\/\\//) ||\n      tab.url.match(/^edge:\\/\\//) ||\n      tab.url.match(/^https?:\\/\\/chrome\\.google\\.com/) ||\n      tab.url.match(/^https?:\\/\\/support\\.mozilla\\.org/) ||\n      tab.url.match(/^https?:\\/\\/addons.mozilla.org/) ||\n      tab.url === \"\")\n  ) {\n    await apply_browser_action(tab.id, {\n      icon: await tint_image(BROWSERACTION_ICON, \"rgba(208, 2, 27, .22)\"),\n      title: `For security reasons, windowed is not supported on this domain (${tab.url}).`,\n    });\n    return;\n  }\n\n  // So if the tab is loaded, and it is not an extra secure domain,\n  // it means windowed is not loaded for some reason. So I tell that.\n  if (tab.status === \"complete\" && has_contentscript_active === false) {\n    await apply_browser_action(tab.id, {\n      icon: await tint_image(BROWSERACTION_ICON, \"#D0021B\"),\n      title:\n        \"This page needs to be reloaded for Windowed to activate. Click here to reload.\",\n    });\n    return;\n  }\n\n  // From here I figure out what the user has configured for Windowed on this domain,\n  // and show a specific icon and title for each of those.\n  let host = new URL(tab.url).host;\n  let config = await get_host_config(tab);\n  if (config.mode === \"fullscreen\" && config.pip === false) {\n    // ONLY FULLSCREEN - Dimmed icon because it is basically disabled\n    await apply_browser_action(tab.id, {\n      icon: await tint_image(BROWSERACTION_ICON, \"rgba(133, 133, 133, 0.5)\"),\n      title: `Windowed is disabled on ${host}, click to re-activate`,\n    });\n    await notify_tab_state(tab.id, { disabled: true });\n  } else if (config.mode === config.all_mode && config.pip === config.all_pip) {\n    // SAME AS DEFAULT - This used to be config.mode === \"ask\" && config.pip === false,\n    // but now you can change the default... this is the color for default\n    await apply_browser_action(tab.id, {\n      icon: await tint_image(BROWSERACTION_ICON, await icon_theme_color(tab)),\n      title: `Windowed is enabled on ${host}`,\n    });\n    await notify_tab_state(tab.id, { disabled: false });\n  } else {\n    // SOMETHING SPECIFIC - Light blue because now the extension is really changing your browsing explicitly\n    await apply_browser_action(tab.id, {\n      icon: await tint_image(BROWSERACTION_ICON, \"#16a8a8\"),\n      title: `Windowed is enabled on ${host}`,\n    });\n    await notify_tab_state(tab.id, { disabled: false });\n  }\n};\n\n// Events where I refresh the browser action button\nbrowser.runtime.onInstalled.addListener(async () => {\n  let all_tabs = await browser.tabs.query({});\n  for (let tab of all_tabs) {\n    await update_button_on_tab(tab);\n  }\n});\nbrowser.tabs.onUpdated.addListener(async (tabId, changed, tab) => {\n  if (changed.url != null || changed.status != null) {\n    await update_button_on_tab(tab);\n  }\n});\n// Not sure if I need this one -\n// only reason I need it is for when one would toggle Enabled/Disabled\nbrowser.tabs.onActivated.addListener(async ({ tabId }) => {\n  let tab = await browser.tabs.get(tabId);\n  await update_button_on_tab(tab);\n});\n// Because I load this as a module now, I am making sure this code is ran as much as possible\n(async () => {\n  let all_tabs = await browser.tabs.query({});\n  for (let tab of all_tabs) {\n    await update_button_on_tab(tab);\n  }\n})();\n"
  },
  {
    "path": "extension/Background/theme-color/chrome-offscreen.html",
    "content": "<script src=\"./chrome-offscreen.js\"></script>\n"
  },
  {
    "path": "extension/Background/theme-color/chrome-offscreen.js",
    "content": "/** @type {import(\"webextension-polyfill\").Browser} */\n// @ts-ignore\nlet browser = chrome;\n\nbrowser.runtime.onMessage.addListener(\n  (/** @type {any} */ message, sender, sendResponse) => {\n    // Return early if this message isn't meant for the offscreen document.\n    if (message.target !== \"offscreen\") {\n      return;\n    }\n\n    if (message.type === \"matchMedia\") {\n      const result = window.matchMedia(message.data);\n      sendResponse({\n        matches: result.matches,\n        media: result.media,\n      });\n      return true;\n    }\n  },\n);\n"
  },
  {
    "path": "extension/Background/theme-color/chrome.js",
    "content": "/** @type {import(\"webextension-polyfill\").Browser} */\n// @ts-ignore\nlet browser = chrome;\n\nlet creating; // A global promise to avoid concurrency issues\n/**\n *\n * @param {string} path\n * @returns {Promise}\n */\nasync function setupOffscreenDocument(path) {\n  // Check all windows controlled by the service worker to see if one\n  // of them is the offscreen document with the given path\n  const offscreenUrl = browser.runtime.getURL(path);\n  const existingContexts = await browser.runtime.getContexts({\n    // @ts-ignore\n    contextTypes: [\"OFFSCREEN_DOCUMENT\"],\n    documentUrls: [offscreenUrl],\n  });\n\n  if (existingContexts.length > 0) {\n    return;\n  }\n\n  // create offscreen document\n  if (creating) {\n    await creating;\n  } else {\n    // @ts-ignore\n    creating = browser.offscreen.createDocument({\n      url: path,\n      reasons: [\"MATCH_MEDIA\"],\n      justification: \"Use window.matchMedia to determine the theme color.\",\n    });\n\n    await creating;\n    creating = null;\n  }\n}\n\n/**\n * @param {string} query\n * @returns {Promise<MediaQueryList>}\n */\nlet matchMedia = async (query) => {\n  await setupOffscreenDocument(\"Background/theme-color/chrome-offscreen.html\");\n\n  // Send message to offscreen document\n  return await browser.runtime.sendMessage({\n    type: \"matchMedia\",\n    target: \"offscreen\",\n    data: query,\n  });\n};\n\n/**\n * Tries to figure out the default icon color\n * - Tries to use the current theme on firefox\n * - Else defaults to light and dark mode\n * @param {import(\"webextension-polyfill\").Tabs.Tab} tab\n * @returns {Promise<string>}\n */\nexport let icon_theme_color_chrome = async (tab) => {\n  let x = (await matchMedia(\"(prefers-color-scheme: dark)\")).matches\n    ? \"rgba(255,255,255,0.8)\"\n    : \"#5f6368\";\n  return x;\n};\n"
  },
  {
    "path": "extension/Background/theme-color/firefox.js",
    "content": "import { themecolor_to_string } from \"../../utilities/themecolor-to-string.js\";\n\n/** @type {import(\"webextension-polyfill\").Browser} */\n// @ts-ignore\nlet browser = chrome;\n\n/**\n * Tries to figure out the default icon color\n * - Tries to use the current theme on firefox\n * - Else defaults to light and dark mode\n * @param {import(\"webextension-polyfill\").Tabs.Tab} tab\n * @returns {Promise<string>}\n */\nexport let icon_theme_color_firefox = async (tab) => {\n  let theme = await browser.theme.getCurrent(tab.windowId);\n\n  if (theme?.colors?.icons != undefined) {\n    return themecolor_to_string(theme.colors.icons);\n  }\n  if (theme?.colors?.toolbar_field_border_focus != undefined) {\n    return themecolor_to_string(theme.colors.toolbar_field_border_focus);\n  }\n  return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n    ? \"rgba(255,255,255,0.8)\"\n    : \"rgb(250, 247, 252)\";\n};\n"
  },
  {
    "path": "extension/Background/tint_image.js",
    "content": "/**\n * @param {string} color\n * @returns {{ color: string, alpha: number }}\n */\nlet find_and_replace_alpha = (color) => {\n  let match = null;\n  if (\n    (match = color\n      .replace(/ /g, \"\")\n      .match(/^(rgba?\\([^,]+,[^,]+,[^,]+,)([^,]+)(\\))$/))\n  ) {\n    let [fullmatch, before, transparency, after] = match;\n    let alpha = Number(transparency);\n    if (!Number.isNaN(alpha)) {\n      return {\n        color: `${before}1${after}`,\n        alpha: alpha,\n      };\n    } else {\n      return { color, alpha: 1 };\n    }\n  } else if (\n    (match = color.replace(/ /g, \"\").match(/^(#\\d\\d\\d\\d\\d\\d)(\\d\\d)$/))\n  ) {\n    let [fullmatch, before, transparency] = match;\n    let alpha = Number(`0x${transparency}`) / 255;\n    if (!Number.isNaN(alpha)) {\n      return {\n        color: `${before}`,\n        alpha: alpha,\n      };\n    } else {\n      return { color, alpha: 1 };\n    }\n  } else {\n    return { color, alpha: 1 };\n  }\n};\n\n/** @type {Map<string, Promise<ImageData>>} */\nlet color_icon_cache = new Map();\n\n/**\n * Colorize an image with the use of a canvas\n * @param {string} url\n * @param {string} color\n * @returns {Promise<ImageData>}\n */\nexport let tint_image = async (url, color) => {\n  let identifier = `${url}@${color}`;\n  if (color_icon_cache.has(identifier)) {\n    return color_icon_cache.get(identifier);\n  } else {\n    let icon = _color_icon(url, color);\n    color_icon_cache.set(identifier, icon);\n    return await icon;\n  }\n};\n\n/**\n * @param {string} url\n * @param {string} _color\n */\nlet _color_icon = async (url, _color) => {\n  let { color, alpha } = find_and_replace_alpha(_color);\n\n  const blob = await fetch(url).then((r) => r.blob());\n  const fg = await createImageBitmap(blob);\n\n  let canvas = new OffscreenCanvas(fg.height, fg.width);\n\n  // Initaliase a 2-dimensional drawing context\n  let ctx = canvas.getContext(\"2d\");\n  let width = ctx.canvas.width;\n  let height = ctx.canvas.height;\n\n  // create offscreen buffer,\n  let buffer = new OffscreenCanvas(fg.width, fg.height);\n\n  let bx = buffer.getContext(\"2d\");\n  // fill offscreen buffer with the tint color\n  bx.fillStyle = color;\n  bx.fillRect(0, 0, buffer.width, buffer.height);\n  // destination atop makes a result with an alpha channel identical to fg, but with all pixels retaining their original color *as far as I can tell*\n  bx.globalCompositeOperation = \"destination-atop\";\n  bx.drawImage(fg, 0, 0);\n\n  // to tint the image, draw it first\n  ctx.drawImage(fg, 0, 0);\n  ctx.drawImage(buffer, 0, 0);\n\n  ctx.globalAlpha = alpha;\n  ctx.globalCompositeOperation = \"copy\";\n  ctx.drawImage(canvas, 0, 0, width, height);\n  ctx.globalAlpha = 1.0;\n\n  return ctx.getImageData(0, 0, width, height);\n};\n"
  },
  {
    "path": "extension/Content.js",
    "content": "// This file is by far the most important of the whole extension.\n// This gets loaded into every single page you open, so I have to keep it as light as possible.\n// Sounds a bit a weird, for a file with 1200 lines, but I want it to so light that I need\n// more rather than less code. No modules or anything fance like that\n\n/**\n * From https://github.com/fabiospampinato/noop-tag/blob/master/src/index.ts\n *\n * @param {TemplateStringsArray} strings\n * @param {unknown[]} expressions\n * @returns {string}\n */\nconst noop_template = (strings, ...expressions) => {\n  let result = strings[0];\n  for (let i = 1, l = strings.length; i < l; i++) {\n    result += expressions[i - 1];\n    result += strings[i];\n  }\n  return result;\n};\nlet css = noop_template;\nlet html = noop_template;\n\n// @ts-ignore\nconst browser = /** @type {import(\"webextension-polyfill\").Browser} */ (\n  globalThis.browser\n);\n\nconst fullscreen_id_namespace = `windowed_long_id_that_does_not_conflict`;\n\nconst fullscreen_select = `${fullscreen_id_namespace}_select`;\nconst fullscreen_active = `${fullscreen_id_namespace}_active`;\nconst fullscreen_element_cloned = `${fullscreen_id_namespace}_ugly_hacky_cloned`;\nconst fullscreen_parent = `${fullscreen_id_namespace}_parent`;\nconst body_class = `${fullscreen_id_namespace}_body`;\nconst shadowdom_trail = `${fullscreen_id_namespace}_shadowdom`;\n\nconst max_z_index = \"2147483647\";\n\n/**\n * @typedef PictureInPictureVideoElement\n * @type {HTMLVideoElement & {\n *  requestPictureInPicture(): Promise<void>,\n *  disablePictureInPicture: boolean,\n * }}\n */\n\n// Aliasses for different browsers (rest of aliasses are in the inserted script)\nlet fullscreenchange_aliasses = [\n  \"fullscreenchange\",\n  \"webkitfullscreenchange\",\n  \"mozfullscreenchange\",\n  \"MSFullscreenChange\",\n];\nlet requestFullscreen_aliasses = [\n  \"requestFullscreen\",\n  \"mozRequestFullScreen\",\n  \"webkitRequestFullscreen\",\n  \"webkitRequestFullScreen\",\n  \"msRequestFullscreen\",\n];\nlet exitFullscreen_aliasses = [\n  \"exitFullscreen\",\n  \"webkitExitFullscreen\",\n  \"webkitCancelFullScreen\",\n  \"mozCancelFullScreen\",\n  \"msExitFullscreen\",\n];\nlet fullscreenelement_aliasses = [\n  \"fullscreenElement\",\n  \"webkitFullscreenElement\",\n  \"mozFullscreenElement\",\n  \"mozFullScreenElement\",\n  \"msFullscreenElement\",\n  \"webkitCurrentFullScreenElement\",\n];\n\nlet external_functions = {};\nlet next_id = 1;\n\nlet on_webpage = (strings, ...values) => {\n  let result = strings[0];\n\n  let value_index = 1;\n  for (let value of values) {\n    if (typeof value === \"string\") {\n      result = result + value;\n    }\n    if (typeof value === \"object\") {\n      result = result + JSON.stringify(value);\n    }\n    if (typeof value === \"function\") {\n      external_functions[next_id] = value;\n      result = result + `external_function(${next_id});`;\n      next_id = next_id + 1;\n    }\n    result = result + strings[value_index];\n    value_index = value_index + 1;\n  }\n\n  return result;\n};\n\nlet all_communication_id = 0;\nlet external_function_parent =\n  (function_id) =>\n  async (...args) => {\n    let request_id = `FROM_CONTENT:${all_communication_id}`;\n    all_communication_id = all_communication_id + 1;\n\n    if (window.parent === window) {\n      return;\n    }\n\n    window.parent.postMessage(\n      {\n        type: \"CUSTOM_WINDOWED_FROM_PAGE\",\n        request_id: request_id,\n        function_id: function_id,\n        args: args,\n      },\n      \"*\",\n    );\n\n    return new Promise((resolve, reject) => {\n      let listener = (event) => {\n        // We only accept messages from ourselves\n        if (event.source != window.parent) return;\n        if (event.data == null) return;\n\n        if (event.data.type === \"CUSTOM_WINDOWED_TO_PAGE\") {\n          if (event.data.request_id === request_id) {\n            window.removeEventListener(\"message\", listener);\n            resolve(event.data.result);\n          }\n        }\n      };\n      window.addEventListener(\"message\", listener);\n    });\n  };\n\nlet enable_selector = (element, key) => {\n  element.dataset[key] = true;\n};\nlet disable_selector = (element, key) => {\n  delete element.dataset[key];\n};\n\n/**\n * @returns {Promise<{\n *  mode: import(\"./Background/BackgroundModule\").WindowedMode,\n *  pip: boolean,\n * }>}\n */\nlet get_host_config_local = async () => {\n  return await send_chrome_message({\n    type: \"get_windowed_config\",\n  });\n};\n\n/**\n * @param {ParentNode} root\n * @returns {HTMLElement}\n */\nlet get_fullscreen_select_element = (root = document) => {\n  /** @type {HTMLElement} */\n  let found_in_lightdom = root.querySelector(`[data-${fullscreen_select}]`);\n  if (found_in_lightdom) return found_in_lightdom;\n\n  // If not found in the lightdom, we follow the trail down the shadows\n  let path_with_breadcrumb = root.querySelector(`[data-${shadowdom_trail}]`);\n  if (path_with_breadcrumb?.shadowRoot != null) {\n    return get_fullscreen_select_element(path_with_breadcrumb.shadowRoot);\n  } else {\n    throw new Error(\"Could not find the element that wants to be fullscreened\");\n  }\n};\n\nlet Button = ({ icon, title, text, target }) => `\n  <button data-target=\"${target}\" title=\"${title}\">\n    <div class=\"icon mask-icon\" style=\"mask-image: url(${icon})\"></div>\n    <span>${text}</span>\n  </button>\n`;\n\n/**\n * @param {import(\"webextension-polyfill\").Manifest.ThemeColor} color\n * @returns {string}\n */\nlet themecolor_to_string = (color) => {\n  if (typeof color === \"string\") {\n    return color;\n  } else if (Array.isArray(color)) {\n    if (color.length === 3) {\n      return `rgb(${color[0]}, ${color[1]}, ${color[2]})`;\n    } else if (color.length === 4) {\n      return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`;\n    } else {\n      // prettier-ignore\n      throw new Error(`Invalid theme color array: ${JSON.stringify(color)}`);\n    }\n  } else {\n    throw new Error(`Invalid theme color: ${color}`);\n  }\n};\n\n// Insert requestFullScreen mock\nconst code_to_insert_in_page = on_webpage`{\n  // Alliases for different browsers\n  let requestFullscreen_aliasses = ${JSON.stringify(\n    requestFullscreen_aliasses,\n  )};\n  let exitFullscreen_aliasses = ${JSON.stringify(exitFullscreen_aliasses)};\n  let fullscreenelement_aliasses = ${JSON.stringify(\n    fullscreenelement_aliasses,\n  )};\n  let fullscreenchange_aliasses = ${JSON.stringify(fullscreenchange_aliasses)};\n\n  const send_event = (element, type) => {\n    const event = new Event(type, {\n      bubbles: true,\n      cancelBubble: false,\n      cancelable: false,\n    });\n    // if (element[\\`on\\${type}\\`]) {\n    //   element[\\`on\\${type}\\`](event);\n    // }\n    element.dispatchEvent(event);\n  };\n\n  const send_fullscreen_events = (element) => {\n    for (let fullscreenchange of fullscreenchange_aliasses) {\n      send_event(document, fullscreenchange);\n    }\n    send_event(window, 'resize');\n  };\n\n  let all_communication_id = 0;\n  let external_function = (function_id) => async (...args) => {\n    let request_id = all_communication_id;\n    all_communication_id = all_communication_id + 1;\n\n    window.postMessage({\n      type: 'CUSTOM_WINDOWED_FROM_PAGE',\n      request_id: request_id,\n      function_id: function_id,\n      args: args,\n    }, '*');\n\n    return new Promise((resolve, reject) => {\n      let listener = (event) => {\n        // We only accept messages from ourselves\n        if (event.source != window) return;\n        if (event.data == null) return;\n\n        if (event.data.type === 'CUSTOM_WINDOWED_TO_PAGE') {\n          if (event.data.request_id === request_id) {\n            window.removeEventListener('message', listener);\n            if (event.data.resultType === 'resolve') {\n              resolve(event.data.result);\n            } else {\n              let err = new Error(event.data.result.message);\n              err.stack = event.data.result.stack;\n              reject(err);\n            }\n          }\n        }\n      }\n      window.addEventListener('message', listener);\n    });\n  }\n\n  let overwrite = (object, property, value) => {\n    try {\n      if (property in object) {\n        Object.defineProperty(object, property, {\n          value: value,\n          configurable: true,\n          writable: true,\n        });\n      }\n    } catch (err) {\n      // Nothing\n    }\n  }\n\n  let set_fullscreen_element = (element = null) => {\n    if (element == null) {\n      throw new Error('WINDOWED: Got null in set_fullscreen_element');\n    }\n\n    overwrite(document, 'webkitIsFullScreen', true); // Old old old\n    overwrite(document, 'fullscreen', true); // Old old old\n    for (let fullscreenelement_alias of fullscreenelement_aliasses) {\n      overwrite(document, fullscreenelement_alias, element);\n    }\n  }\n\n  let make_tab_go_fullscreen = ${async () => {\n    await go_into_fullscreen();\n  }}\n  let make_tab_go_inwindow = ${async () => {\n    await go_in_window();\n  }}\n\n  let create_popup = ${async () => {\n    clear_popup();\n\n    let is_fullscreen = await external_function_parent(\"is_fullscreen\")();\n    if (is_fullscreen) {\n      await go_out_of_fullscreen();\n      return;\n    }\n\n    let element = get_fullscreen_select_element();\n\n    // Find possible picture-in-picture video element\n    let video_element = /** @type {PictureInPictureVideoElement} */ (\n      element.querySelector(\"video:not([disablepictureinpicture])\")\n    );\n    video_element =\n      video_element != null &&\n      video_element.requestPictureInPicture != null &&\n      video_element.readyState >= 1 &&\n      !video_element.disablePictureInPicture\n        ? video_element\n        : null;\n\n    let { mode, pip } = is_shift_pressed\n      ? /// Force the mode to be \"ask\" when shift is pressed\n        { mode: \"ask\", pip: false }\n      : await get_host_config_local();\n    if (pip === true && video_element != null) {\n      video_element.requestPictureInPicture();\n      onEscapePress(() => {\n        // @ts-ignore\n        document.exitPictureInPicture();\n      });\n      return \"PICTURE-IN-PICTURE\";\n    }\n\n    if (mode === \"fullscreen\" || mode === \"windowed\" || mode === \"in-window\") {\n      if (mode === \"fullscreen\") {\n        let element = get_fullscreen_select_element();\n        disable_selector(element, fullscreen_select);\n        element.requestFullscreen();\n        return \"FULLSCREEN\";\n      }\n      if (mode === \"windowed\") {\n        await go_into_fullscreen();\n        return \"WINDOWED\";\n      }\n      if (mode === \"in-window\") {\n        await go_in_window();\n        return \"IN-WINDOW\";\n      }\n    }\n\n    let popup_div = document.createElement(\"div\");\n    let shadowRoot = popup_div.attachShadow({ mode: \"open\" });\n\n    if (theme?.colors != undefined) {\n      let root = popup_div;\n      for (let [key, value] of Object.entries(theme.colors)) {\n        if (value != undefined) {\n          root.style.setProperty(`--theme-${key}`, themecolor_to_string(value));\n        }\n      }\n    }\n\n    shadowRoot.appendChild(\n      createElementFromHTML(`<style>${popup_css}</style>`),\n    );\n\n    let clicked_element_still_exists =\n      last_click_y != null && last_click_x != null; // && document.elementsFromPoint(last_click_x, last_click_y).includes(last_click_element)\n    if (\n      clicked_element_still_exists &&\n      Date.now() - last_click_timestamp <\n        CLICK_IS_CONSIDERED_FULLSCREEN_CLICK_DELAY\n    ) {\n      let top_vs_bottom =\n        last_click_y < window.innerHeight / 2\n          ? \"translateY(0px)\"\n          : \"translateY(-100%)\";\n      let left_vs_right =\n        last_click_x < window.innerWidth / 2\n          ? \"translateX(0px)\"\n          : \"translateX(-100%)\";\n\n      /// Popup that shows if there is a recent click\n      let popup = createElementFromHTML(`\n        <menu class=\"popup\" tabIndex=\"1\" style=\"\n          position: absolute;\n          top: ${last_click_y}px;\n          left: ${last_click_x}px;\n          transform: ${top_vs_bottom} ${left_vs_right};\n        \">\n          ${\n            video_element\n              ? `<li>\n                ${Button({\n                  icon: browser.runtime.getURL(\"Images/Icon_PiP@scalable.svg\"),\n                  text: \"PiP\",\n                  title: \"Picture-in-picture (p)\",\n                  target: \"picture-in-picture\",\n                })}\n              </li>`\n              : \"\"\n          }\n          <li>\n            ${Button({\n              icon: browser.runtime.getURL(\n                \"Images/Icon_Windowed_Mono@scalable.svg\",\n              ),\n              text: \"Windowed\",\n              title: \"Windowed (w)\",\n              target: \"windowed\",\n            })}\n          </li>\n          <li>\n            ${Button({\n              icon: browser.runtime.getURL(\n                \"Images/Icon_InWindow_Mono@scalable.svg\",\n              ),\n              text: \"In-window\",\n              title: \"In-window (i)\",\n              target: \"in-window\",\n            })}\n          </li>\n          <li>\n            ${Button({\n              icon: browser.runtime.getURL(\n                \"Images/Icon_EnterFullscreen@scalable.svg\",\n              ),\n              text: \"Fullscreen\",\n              title: \"Fullscreen (f)\",\n              target: \"fullscreen\",\n            })}\n          </li>\n        </menu>\n      `);\n      shadowRoot.appendChild(popup);\n    } else {\n      /// Popup that shows if there is no recent click (centered in the screen)\n      let popup = createElementFromHTML(`\n        <div>\n          <div\n            style=\"\n              position: fixed;\n              top: 0; left: 0;\n              right: 0; bottom: 0;\n              background-color: rgba(0,0,0,.8);\n              pointer-events: none;\n              z-index: ${max_z_index};\n            \"\n          ></div>\n\n          <div class=\"popup\" tabIndex=\"1\" style=\"\n            position: fixed;\n            top: 25vh;\n            left: 50vw;\n            transform: translateX(-50%) translateY(-50%);\n            font-size: 20px;\n          \">\n            <div style=\"padding: 1.25em; padding-bottom: 0.25em; padding-top: 0.25em\">Enter fullscreen</div>\n            <div style=\"height: 10px\"></div>\n            <menu style=\"display: flex; flex-direction: column; align-items: stretch;\">\n              ${\n                video_element\n                  ? `\n                    <li>\n                      <button data-target=\"picture-in-picture\" title=\"Picture in picture (p)\" class=\"flex flex-row\">\n                        <div class=\"icon mask-icon\" style=\"mask-image: url(${browser.runtime.getURL(\"Images/Icon_PiP@scalable.svg\")})\"></div>\n                        <span class=\"flex-1\">PiP</span>\n                        <kbd style=\"margin-left: 16px\">p</kbd>\n                      </button>\n                    </li>\n                  `\n                  : \"\"\n              }\n              <li>\n                <button data-target=\"windowed\" title=\"Windowed (w)\" class=\"flex flex-row\">\n                  <div class=\"icon mask-icon\" style=\"mask-image: url(${browser.runtime.getURL(\"Images/Icon_Windowed_Mono@scalable.svg\")})\"></div>\n                  <span class=\"flex-1\">Windowed</span>\n                  <kbd style=\"margin-left: 16px\">w</kbd>\n                </button>\n              </li>\n              <li>\n                <button data-target=\"in-window\" title=\"In-window (i)\" class=\"flex flex-row\">\n                  <div class=\"icon mask-icon\" style=\"mask-image: url(${browser.runtime.getURL(\"Images/Icon_InWindow_Mono@scalable.svg\")})\"></div>\n                  <span class=\"flex-1\">In-window</span>\n                  <kbd style=\"margin-left: 16px\">i</kbd>\n                </button>\n              </li>\n              <li>\n                <button data-target=\"fullscreen\" title=\"Fullscreen (f)\" class=\"flex flex-row\">\n                  <div class=\"icon mask-icon\" style=\"mask-image: url(${browser.runtime.getURL(\"Images/Icon_EnterFullscreen@scalable.svg\")})\"></div>\n                  <span class=\"flex-1\">Fullscreen</span>\n                  <kbd style=\"margin-left: 16px\">f</kbd>\n                </button>\n              </li>\n            </menu>\n          </div>\n        </div>\n      `);\n      shadowRoot.appendChild(popup);\n    }\n\n    /** @type {HTMLElement} */\n    let popup_element = shadowRoot.querySelector(`.popup`);\n    setTimeout(() => {\n      popup_element.focus();\n    }, 0);\n\n    document.body.appendChild(popup_div);\n    last_popup = popup_div;\n\n    /** @type {\"windowed\" | \"in-window\" | \"fullscreen\" | \"picture-in-picture\" | \"nothing\" | \"cool-pip\"} */\n    let result = await new Promise((resolve) => {\n      popup_element.addEventListener(\"focusout\", (event) => {\n        // @ts-ignore\n        if (!event.currentTarget.contains(event.relatedTarget)) {\n          resolve(\"nothing\");\n        }\n      });\n\n      // For people who like keyboard shortcuts\n      popup_element.addEventListener(\n        \"keydown\",\n        (/** @type {KeyboardEvent} */ event) => {\n          if (event.key === \"w\" || event.key === \"W\") {\n            event.stopPropagation();\n            resolve(\"windowed\");\n          }\n          if (event.key === \"i\" || event.key === \"I\") {\n            event.stopPropagation();\n            resolve(\"in-window\");\n          }\n          if (event.key === \"f\" || event.key === \"F\") {\n            event.stopPropagation();\n\n            // I need this check here, because I can't call the original fullscreen from a\n            // 'async' function (or anywhere async (eg. after `resolve()` is called))\n            let element = get_fullscreen_select_element();\n            disable_selector(element, fullscreen_select);\n            element.requestFullscreen();\n\n            resolve(\"fullscreen\");\n          }\n          if (event.key === \"p\" || event.key === \"P\") {\n            event.stopPropagation();\n            resolve(\"picture-in-picture\");\n          }\n          if (event.key === \"Escape\") {\n            event.stopPropagation();\n            resolve(\"nothing\");\n          }\n        },\n      );\n\n      for (let button of shadowRoot.querySelectorAll(`[data-target]`)) {\n        button.addEventListener(\"click\", (e) => {\n          // @ts-ignore\n          let target_keyword = button.dataset.target;\n          if (target_keyword === \"fullscreen\") {\n            // I need this check here, because I can't call the original fullscreen from a\n            // 'async' function (or anywhere async (eg. after `resolve()` is called))\n            let element = get_fullscreen_select_element();\n            disable_selector(element, fullscreen_select);\n            element.requestFullscreen();\n          }\n          resolve(target_keyword);\n        });\n      }\n    });\n\n    clear_popup();\n\n    if (result === \"fullscreen\") {\n      // NOTE This is now all done sync in the popup callback.\n      // .... because firefox does not like it when I call it from a promise.\n      return \"FULLSCREEN\";\n    }\n    if (result === \"windowed\") {\n      await go_into_fullscreen();\n      return \"WINDOWED\";\n    }\n    if (result === \"in-window\") {\n      await go_in_window();\n      return \"IN-WINDOW\";\n    }\n    if (result === \"picture-in-picture\") {\n      video_element.requestPictureInPicture();\n\n      onEscapePress(() => {\n        // @ts-ignore\n        document.exitPictureInPicture();\n      });\n\n      return \"PICTURE-IN-PICTURE\";\n    }\n\n    // if (result === \"cool-pip\") {\n    //   await go_in_cool_pip();\n    //   return \"COOL-PIP\";\n    // }\n\n    if (result === \"nothing\") {\n      return \"NOTHING\";\n    }\n  }}\n\n  let make_tab_exit_fullscreen = ${async () => {\n    await go_out_of_fullscreen();\n    send_fullscreen_events();\n  }}\n\n  let exitFullscreen = async function(original) {\n    let windowed_fullscreen = document.querySelector('[data-${fullscreen_active}], [data-${shadowdom_trail}]');\n\n    if (windowed_fullscreen) {\n      // If the fullscreen element is a frame, tell it to exit fullscreen too\n      if (typeof windowed_fullscreen.postMessage === 'function') {\n        document.fullscreenElement.postMessage.sendMessage({ type: \"exit_fullscreen_iframe\" });\n      }\n\n      // Reset all the variables to their browser form\n      delete window.screen.width;\n      delete window.screen.height;\n      delete document['webkitIsFullScreen'];\n      delete document['fullscreen'];\n      for (let fullscreenelement_alias of fullscreenelement_aliasses) {\n        delete document[fullscreenelement_alias];\n      }\n\n      await make_tab_exit_fullscreen();\n    } else {\n      original();\n    }\n  }\n\n  ${\"\" /* NOTE requestFullscreen */}\n  const requestFullscreen_windowed = async function(original, ...args) {\n    const element = this;\n    element.dataset['${fullscreen_select}'] = true;\n\n    let shadowroot = element.getRootNode()\n    while (shadowroot != null && shadowroot.host != null) {\n        shadowroot.host.dataset['${shadowdom_trail}'] = true;\n        shadowroot = shadowroot.host.getRootNode()\n    }\n\n    // Tell extension code (outside of this block) to go into fullscreen\n    // window.postMessage({ type: force ? \"enter_fullscreen\" : \"show_fullscreen_popup\" }, \"*\");\n    // send_windowed_event(element, force ? \"enter_fullscreen\" : \"show_fullscreen_popup\");\n    try {\n      await create_popup();\n    } catch (err) {\n      // Anything gone wrong, we default to normal fullscreen\n      console.error(err);\n      console.error('[Windowed] Something went wrong, so I default to normal fullscreen:', err.stack);\n      original();\n    }\n  }\n\n  // Because firefox is super cool, it is also super frustrating...\n  // So firefox does not allow fullscreen calls from promises, even if it is basically sync.\n  // Therefor I need to first define this as sync, and from here call the async version.\n  let MUTATE_is_windowed_enabled = true;\n  let requestFullscreen = function(original, ...args) {\n    if (MUTATE_is_windowed_enabled === false) {\n      return original()\n    } else {\n      requestFullscreen_windowed.call(this, original, ...args);\n    }\n  }\n\n  let finish_fullscreen = () => {\n    // Because youtube actually checks for those sizes?!\n    const window_width = Math.max(window.outerWidth, window.innerWidth);\n    const window_height = Math.max(window.outerHeight, window.innerHeight);\n\n    overwrite(window.screen, 'width', window_width);\n    overwrite(window.screen, 'height', window_height);\n\n    let element = document.querySelector('[data-${fullscreen_select}]');\n    if (element == null) {\n      console.warn('[WINDOWED] Strange, no fullscreen element shown');\n      return;\n    }\n\n    document.body.focus();\n    element.focus();\n    set_fullscreen_element(element || document.body);\n    send_fullscreen_events();\n  }\n\n  window.addEventListener(\"message\", (message) => {\n    // Message from content script or parent content script\n    if (message.source === message.target || message.source === window.parent) {\n      if (message.data?.type === 'WINDOWED-confirm-fullscreen') {\n        finish_fullscreen();\n      }\n\n      if (message.data?.type === 'WINDOWED-exit-fullscreen') {\n        exitFullscreen.call(document, original_exitFullscreen);\n      }\n\n      if (message.data?.type === 'WINDOWED-notify') {\n        MUTATE_is_windowed_enabled = !message.data.disabled;\n      }\n    }\n\n    // Message from frame inside the page (these are tricky not sure if I still know how this works)\n    const frame = [...document.querySelectorAll('iframe')].find(x => x.contentWindow === message.source);\n    if (frame != null) {\n      if (message.data?.type === 'enter_inwindow_iframe') {\n        frame.dataset['${fullscreen_select}'] = \"true\";\n        make_tab_go_inwindow();\n      }\n      if (message.data?.type === 'enter_fullscreen_iframe') {\n        frame.dataset['${fullscreen_select}'] = \"true\";\n        make_tab_go_fullscreen();\n      }\n      if (message.data?.type === 'exit_fullscreen_iframe') {\n        // Call my exitFullscreen on the document\n        exitFullscreen.call(document, original_exitFullscreen);\n      }\n    }\n  });\n\n  ${\n    \"\" /* NOTE Replace all the `requestFullscreen` aliasses with calls to my own version */\n  }\n  let original_requestFullscreen = null;\n  for (let requestFullscreenAlias of requestFullscreen_aliasses) {\n    if (typeof Element.prototype[requestFullscreenAlias] === 'function') {\n      let original_function = Element.prototype[requestFullscreenAlias];\n      original_requestFullscreen = original_function;\n      Element.prototype[requestFullscreenAlias] = function(...args) {\n        return requestFullscreen.call(this, original_function.bind(this), ...args);\n      };\n    }\n  }\n\n  ${\n    \"\" /* NOTE Replace all the `exitFullscreen` aliasses with calls to my own version */\n  }\n  let original_exitFullscreen = null;\n  for (let exitFullscreenAlias of exitFullscreen_aliasses) {\n    if (typeof Document.prototype[exitFullscreenAlias] === 'function') {\n      let original_function = Document.prototype[exitFullscreenAlias];\n      original_exitFullscreen = original_function;\n      Document.prototype[exitFullscreenAlias] = function(...args) {\n        return exitFullscreen.call(this, original_function.bind(this), ...args);\n      };\n    }\n  }\n}`;\n\n//// I used to insert the code directly as a <script>...</script> tag,\n//// but that doesn't work nicely with some sites CSP.\n//// Now I create this separate file with the contents of the script,\n//// which is awkward if they aren't in sync... but I need to hack hack hack.\n//// This also loads the script asynchronously.. but i think these scripts still execute in order.\n//// I still need to do the `on_webpage` call, so the message listeners are set up.\n// let elt = document.createElement(\"script\");\n// elt.innerHTML = code_to_insert_in_page;\n// document.documentElement.appendChild(elt);\n// document.documentElement.removeChild(elt);\n\n/// No longer necessary as I insert this via manifest.json\n// let elt = document.createElement(\"script\");\n// elt.src = browser.runtime.getURL(\"Windowed-inject-into-page.js\");\n// document.documentElement.appendChild(elt);\n// document.documentElement.removeChild(elt);\n\n//// This is just for myself as debugging, but it will tell me if the script that is inserted,\n//// is actually the same as the script I am expecting it to be. (because debugging could get very frustrating)\nlet async = async (async) => async();\nasync(async () => {\n  let response = await fetch(\n    browser.runtime.getURL(\"Windowed-inject-into-page.js\"),\n  );\n  let result = await response.text();\n  if (result !== code_to_insert_in_page) {\n    // prettier-ignore\n    console.error(\"[WINDOWED] HEY MICHIEL! The script I am inserting is not the same as the script I expect it to be!\");\n    console.log(\"[WINDOWED] Code should actually be:\");\n    console.log(code_to_insert_in_page);\n  }\n});\n\nconst send_event = (element, type) => {\n  const event = new Event(type, {\n    bubbles: true,\n    cancelable: false,\n  });\n  element.dispatchEvent(event);\n};\n\nconst send_fullscreen_events = () => {\n  for (let fullscreenchange of fullscreenchange_aliasses) {\n    send_event(document, fullscreenchange);\n  }\n  send_event(window, \"resize\");\n};\n\n// setTimeout(() => {\n//   for (let stylesheet of document.styleSheets) {\n//     try {\n//       for (let rule of stylesheet.cssRules) {\n//         // Remove the css rule if the media query doesn't match,\n//         // Force match it when it does\n//         if (rule.media) {\n//           if (window.matchMedia(rule.media.mediaText).matches) {\n//             // console.log(`The media (${rule.media.mediaText}) matches!`);\n//             rule.media.__WINDOWED_FALLBACK_MEDIATEXT__ = rule.media.mediaText;\n//             rule.media.mediaText = \"all\";\n//           } else {\n//             // console.log(`The media (${rule.media.mediaText}) does not match!`);\n//             rule.media.__WINDOWED_FALLBACK_MEDIATEXT__ = rule.media.mediaText;\n//             rule.media.mediaText = \"not all\";\n//           }\n//         }\n//       }\n//     } catch (err) {\n//       console.warn(`WINDOWED: Couldn't read stylesheet rules because of CORS...`);\n//       console.log(`stylesheet:`, stylesheet)\n//     }\n//   }\n// }, 1000);\n\n// console.log('Runs in proper sandbox:', document.documentElement.constructor === HTMLHtmlElement);\n// NOTE On chrome, extensions run in a proper sandbox (above will log true),\n// meaning that you can't get access to the actual prototype-s of the Document and Elements-s,\n// hence the need for the ugly script insert above.\n// On Firefox however, this is not the case, and I might (because firefox screws me with CSP)\n// need to use this quirk to work on all pages\n\nlet clear_listeners = () => {};\n\nlet delay = (ms) => {\n  return new Promise((resolve) => {\n    setTimeout(() => resolve(), ms);\n  });\n};\n\nlet popup_css = css`\n  /* Poor mans tailwind */\n  .flex {\n    display: flex;\n  }\n  .flex-col {\n    flex-direction: column;\n  }\n  .flex-row {\n    flex-direction: row;\n  }\n  .flex-1 {\n    flex: 1;\n  }\n\n  :host {\n    --theme-popup: #111;\n    --theme-popup_border: #555;\n    --theme-popup_text: white;\n    --theme-popup_highlight: rgba(255, 255, 255, 0.1);\n    --theme-popup_highlight_text: white;\n  }\n\n  .popup {\n    background-color: var(--theme-popup, white);\n    border-radius: 3px;\n    border: solid #eee 1px;\n    border-color: var(--theme-popup_border, #eee);\n    /*box-shadow: 0px 2px 4px #00000026;*/\n    padding-top: 5px;\n    padding-bottom: 5px;\n    font-size: 16px;\n    color: var(--theme-popup_text, black);\n    min-width: 150px;\n\n    display: flex;\n    flex-direction: column;\n    align-items: stretch;\n\n    font-family: sans-serif;\n\n    z-index: ${max_z_index};\n  }\n\n  .popup:focus {\n    outline: none;\n  }\n\n  /*@media (prefers-color-scheme: dark) {\n    .popup {\n      filter: invert(0.9);\n    }\n  }*/\n\n  .popup [data-target] {\n    text-align: inherit;\n    cursor: pointer;\n    padding: 1.25em;\n    padding-top: 0.25em;\n    padding-bottom: 0.25em;\n    /*background-color: white;*/\n    /*background-color: var(--theme-popup, white);*/\n    background-color: transparent;\n    color: var(--theme-popup_text, #eee);\n    /* Force black for if the page has color: white */\n\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n\n    font-size: inherit;\n    border: none;\n    box-shadow: none;\n\n    white-space: nowrap;\n\n    &:focus {\n      /*filter: brightness(0.95);*/\n      background-color: var(--theme-popup_highlight, white);\n      color: var(--theme-popup_highlight_text, black);\n    }\n    &:hover {\n      /*filter: brightness(0.9);*/\n      background-color: var(--theme-popup_highlight, white);\n      color: var(--theme-popup_highlight_text, black);\n    }\n    &:focus:not(:focus-visible) {\n      outline: none;\n    }\n\n    & > img {\n      height: 1.2em;\n      width: 1.2em;\n      margin-right: 1em;\n    }\n  }\n\n  .icon {\n    height: 1.2em;\n    width: 1.2em;\n    margin-right: 1em;\n  }\n  .mask-icon {\n    background-color: currentColor;\n    mask-size: contain;\n  }\n\n  [data-target]::-moz-focus-inner,\n  .popup::-moz-focus-inner {\n    border: 0;\n    outline: 0;\n  }\n\n  li {\n    display: contents;\n  }\n  menu {\n    margin: 0;\n    padding: 0;\n    list-style-type: none;\n  }\n\n  kbd {\n    font-size: 0.9em;\n    background-color: #c0c0c0;\n    padding-inline: 4px;\n    border-radius: 4px;\n    border: solid 1px #979797;\n  }\n`;\n\n/**\n * @param {ParentNode} root\n */\nlet create_style_rule = (root = document) => {\n  let css_text = css`\n    [data-${fullscreen_parent}] {\n      /* This thing is css black magic */\n      all: initial !important;\n      z-index: ${max_z_index} !important;\n\n      /* Debugging */\n      /* background-color: rgba(0,0,0,.1) !important; */\n    }\n\n    /* Not sure if this is necessary, but putting it here just in case */\n    [data-${fullscreen_parent}]::before,\n    [data-${fullscreen_parent}]::after {\n      display: none;\n    }\n\n    [data-${body_class}] {\n      /* Prevent scrolling */\n      overflow: hidden !important;\n\n      /* For debugging, leaving this just in here so I see when something goes wrong */\n      /* background-color: rgb(113, 0, 180); */\n    }\n\n    /* I know I want it to be generic, but still this is a youtube specific fix */\n    #player-theater-container {\n      min-height: 0 !important;\n    }\n\n    [data-${fullscreen_active}],\n    [data-${shadowdom_trail}] {\n      position: fixed !important;\n      top: 0 !important;\n      bottom: 0 !important;\n      right: 0 !important;\n      left: 0 !important;\n      width: 100% !important;\n      height: 100% !important;\n      max-width: initial !important;\n      max-height: initial !important;\n      z-index: ${max_z_index} !important;\n\n      background-color: black;\n    }\n  `;\n\n  let styleEl = document.createElement(\"style\");\n  if (root instanceof ShadowRoot) {\n    root.appendChild(styleEl);\n  } else if (root instanceof Document) {\n    root.head.appendChild(styleEl);\n  } else {\n    throw new Error(\"[WINDOWED] Could not find a place to put the style\");\n  }\n  styleEl.appendChild(document.createTextNode(css_text));\n\n  let shadowroot_maybe = root.querySelector(`[data-${shadowdom_trail}`);\n  if (shadowroot_maybe != null) {\n    console.log(`element:`, shadowroot_maybe);\n    create_style_rule(shadowroot_maybe.shadowRoot);\n  }\n};\n\n/**\n * @param {HTMLElement} element\n */\nconst parent_elements = function* (element) {\n  let el = element;\n  while (el) {\n    let root_node = el.getRootNode();\n    if (el.parentElement) {\n      el = el.parentElement;\n    } else if (root_node instanceof ShadowRoot) {\n      el = /** @type {HTMLElement} */ (root_node.host);\n    } else {\n      break;\n    }\n\n    yield el;\n  }\n};\n\n/**\n * @param {{ type: string, [key: string]: any }} message\n */\nlet send_chrome_message = async (message) => {\n  // @ts-ignore\n  let { type, value } = await browser.runtime.sendMessage(message);\n  if (type === \"resolve\") {\n    return value;\n  } else {\n    let err = new Error(value.message);\n    err.stack = value.stack;\n    // err.stack = [\n    //   ...x.value.stack.split('\\n'),\n    //   'From postMessage to background page',\n    //   ...stack,\n    // ].join('\\n');\n    throw err;\n  }\n};\n\nlet CLICK_IS_CONSIDERED_FULLSCREEN_CLICK_DELAY = 1 * 1000;\n\nlet last_click_x = null;\nlet last_click_y = null;\nlet last_click_timestamp = 0;\nlet last_click_element = null;\n\nlet is_shift_pressed = false;\n\nlet last_popup = null;\nlet is_in_fullscreen = false;\n\nlet clear_popup = () => {\n  if (last_popup != null) {\n    try {\n      document.body.removeChild(last_popup);\n    } catch (err) {}\n    last_popup = null;\n    return true;\n  }\n  return false;\n};\n\ndocument.addEventListener(\"keydown\", (event) => {\n  is_shift_pressed = event.shiftKey;\n  let { mode, pip } = current_mode;\n  window.postMessage(\n    {\n      type: \"WINDOWED-notify\",\n      disabled: is_shift_pressed\n        ? false\n        : mode === \"fullscreen\" && pip === false,\n    },\n    \"*\",\n  );\n});\ndocument.addEventListener(\"keyup\", (event) => {\n  is_shift_pressed = event.shiftKey;\n  let { mode, pip } = current_mode;\n  window.postMessage(\n    {\n      type: \"WINDOWED-notify\",\n      disabled: is_shift_pressed\n        ? false\n        : mode === \"fullscreen\" && pip === false,\n    },\n    \"*\",\n  );\n});\n\ndocument.addEventListener(\n  \"click\",\n  (event) => {\n    last_click_x = event.pageX;\n    last_click_y = event.pageY;\n    last_click_timestamp = Date.now();\n    // last_click_element = event.target;\n\n    if (\n      last_popup != null &&\n      (event.target === last_popup || last_popup.contains(event.target))\n    ) {\n      // Clicked inside popup\n    } else {\n      if (clear_popup()) {\n        send_fullscreen_events();\n      }\n    }\n  },\n  { capture: true },\n);\n\nlet exit_fullscreen_on_page = () => {\n  window.postMessage(\n    {\n      type: \"WINDOWED-exit-fullscreen\",\n    },\n    \"*\",\n  );\n};\n\nlet createElementFromHTML = (htmlString) => {\n  let div = document.createElement(\"div\");\n  div.innerHTML = htmlString.trim();\n\n  return div.firstChild;\n};\n\nlet go_in_window = async () => {\n  create_style_rule();\n\n  clear_listeners();\n  let element = get_fullscreen_select_element();\n\n  let unlisten_to_escape = onEscapePress(() => {\n    exit_fullscreen_on_page();\n  });\n\n  let beforeunload_listener = (e) => {\n    exit_fullscreen_on_page();\n  };\n  window.addEventListener(\"beforeunload\", beforeunload_listener);\n\n  clear_listeners = () => {\n    unlisten_to_escape();\n    window.removeEventListener(\"beforeunload\", beforeunload_listener);\n  };\n\n  enable_selector(element, fullscreen_active);\n  // Add fullscreen class to every parent of our fullscreen element\n  for (let parent_element of parent_elements(element)) {\n    enable_selector(parent_element, fullscreen_parent);\n  }\n\n  if (window.parent !== window) {\n    // Ask parent-windowed code to become fullscreen too\n    window.parent.postMessage({ type: \"enter_inwindow_iframe\" }, \"*\");\n  }\n\n  // Post back to in-page javascript\n  window.postMessage({ type: \"WINDOWED-confirm-fullscreen\" }, \"*\");\n\n  // Add no scroll to the body and let everything kick in\n  enable_selector(document.body, body_class);\n};\n\nlet go_into_fullscreen = async () => {\n  create_style_rule();\n  let element = get_fullscreen_select_element();\n  let cloned = element.cloneNode(true);\n\n  clear_listeners();\n  var mutationObserver = new MutationObserver(async (mutations) => {\n    for (let mutation of mutations) {\n      for (let removed of mutation.removedNodes) {\n        if (removed === element) {\n          clear_listeners();\n\n          enable_selector(cloned, fullscreen_element_cloned);\n          enable_selector(cloned, fullscreen_select);\n          document.body.appendChild(cloned);\n          go_into_fullscreen();\n\n          await delay(500);\n          if (\n            (cloned instanceof HTMLIFrameElement ||\n              cloned instanceof HTMLObjectElement) &&\n            cloned.contentWindow?.postMessage\n          ) {\n            cloned.contentWindow.postMessage(\n              { type: \"WINDOWED-confirm-fullscreen\" },\n              \"*\",\n            );\n          }\n        }\n      }\n    }\n  });\n\n  if (element.parentElement) {\n    mutationObserver.observe(element.parentElement, {\n      childList: true,\n    });\n  }\n\n  let unlisten_to_escape = onEscapePress(() => {\n    exit_fullscreen_on_page();\n  });\n\n  let beforeunload_listener = (e) => {\n    exit_fullscreen_on_page();\n  };\n  window.addEventListener(\"beforeunload\", beforeunload_listener);\n\n  clear_listeners = () => {\n    mutationObserver.disconnect();\n    unlisten_to_escape();\n    window.removeEventListener(\"beforeunload\", beforeunload_listener);\n  };\n\n  enable_selector(element, fullscreen_active);\n  // Add fullscreen class to every parent of our fullscreen element\n  for (let parent_element of parent_elements(element)) {\n    enable_selector(parent_element, fullscreen_parent);\n  }\n\n  if (window.parent !== window) {\n    // Ask parent-windowed code to become fullscreen too\n    window.parent.postMessage({ type: \"enter_fullscreen_iframe\" }, \"*\");\n  } else {\n    // Send popup command to extension\n    let menubar_size = window.outerHeight - window.innerHeight; // Asume there is just header, no browser footer\n\n    let rect = element.getBoundingClientRect();\n    let height = Math.max((rect.width * 9) / 16, rect.height);\n    let ratio_width = Math.min((height / 9) * 16, rect.width); // 16:9\n    let width_diff = rect.width - ratio_width;\n\n    await send_chrome_message({\n      type: \"please_make_me_a_popup\",\n      position: {\n        height: height,\n        width: ratio_width,\n        top: rect.top + menubar_size,\n        left: rect.left + width_diff / 2,\n      },\n    });\n  }\n\n  // Post back to the javascript we put inside the page\n  window.postMessage({ type: \"WINDOWED-confirm-fullscreen\" }, \"*\");\n\n  // Add no scroll to the body and let everything kick in\n  enable_selector(document.body, body_class);\n  window.focus(); // idk\n};\n\nlet go_out_of_fullscreen = async () => {\n  // Remove no scroll from body (and remove all styles)\n  disable_selector(document.body, body_class);\n\n  clear_listeners();\n\n  send_fullscreen_events();\n\n  const fullscreen_element = get_fullscreen_select_element();\n  disable_selector(fullscreen_element, fullscreen_select);\n  disable_selector(fullscreen_element, fullscreen_active);\n\n  // Remove fullscreen class... from everything\n  for (let parent of parent_elements(fullscreen_element)) {\n    disable_selector(parent, fullscreen_parent);\n    disable_selector(parent, shadowdom_trail);\n  }\n\n  // If we are a frame, tell the parent frame to exit fullscreen\n  // If we aren't (we are a popup), tell the background page to make me tab again\n  if (window.parent !== window) {\n    window.parent.postMessage({ type: \"exit_fullscreen_iframe\" }, \"*\");\n  } else {\n    await delay(10);\n    await send_chrome_message({ type: \"please_make_me_a_tab_again\" });\n    await delay(500);\n  }\n\n  let cloned = document.querySelector(`[data-${fullscreen_element_cloned}]`);\n  if (cloned) {\n    document.body.removeChild(cloned);\n  }\n};\n\nexternal_functions.is_fullscreen = () => {\n  const fullscreen_element = document.querySelector(\n    `[data-${fullscreen_active}]`,\n  );\n  return fullscreen_element != null;\n};\n\nwindow.addEventListener(\"message\", async (event) => {\n  // We only accept messages from ourselves\n  if (event.data == null) return;\n  if (event.data.type === \"CUSTOM_WINDOWED_FROM_PAGE\") {\n    let fn = external_functions[event.data.function_id];\n    try {\n      let result = await fn(...event.data.args);\n      // @ts-ignore\n      event.source.postMessage(\n        {\n          type: \"CUSTOM_WINDOWED_TO_PAGE\",\n          request_id: event.data.request_id,\n          resultType: \"resolve\",\n          result: result,\n        },\n        // @ts-ignore\n        \"*\",\n      );\n    } catch (err) {\n      event.source.postMessage(\n        {\n          type: \"CUSTOM_WINDOWED_TO_PAGE\",\n          request_id: event.data.request_id,\n          resultType: \"reject\",\n          result: {\n            message: err.message,\n            stack: err.stack,\n          },\n        },\n        // @ts-ignore\n        \"*\",\n      );\n    }\n  }\n});\n\nlet is_escape_locked = false;\n// TODO Ask feedback from the Stadia community to find out what an helpful solution would be\n// https://www.reddit.com/r/Stadia/comments/kibh2r/windowed_stadia_extension_what_could_i_improve/\n// WELL one response isn't too bad, is it?\n// ...\n// const keyboard_lock_injection = on_webpage`{\n//   let all_communication_id = 0;\n//   let external_function = (function_id) => async (...args) => {\n//     let request_id = all_communication_id;\n//     all_communication_id = all_communication_id + 1;\n\n//     window.postMessage({\n//       type: 'CUSTOM_WINDOWED_FROM_PAGE',\n//       request_id: request_id,\n//       function_id: function_id,\n//       args: args,\n//     }, '*');\n\n//     return new Promise((resolve, reject) => {\n//       let listener = (event) => {\n//         // We only accept messages from ourselves\n//         if (event.source != window) return;\n//         if (event.data == null) return;\n\n//         if (event.data.type === 'CUSTOM_WINDOWED_TO_PAGE') {\n//           if (event.data.request_id === request_id) {\n//             window.removeEventListener('message', listener);\n//             if (event.data.resultType === 'resolve') {\n//               resolve(event.data.result);\n//             } else {\n//               let err = new Error(event.data.result.message);\n//               err.stack = event.data.result.stack;\n//               reject(err);\n//             }\n//           }\n//         }\n//       }\n//       window.addEventListener('message', listener);\n//     });\n//   }\n\n//   let old_lock = navigator.keyboard.lock.bind(navigator.keyboard);\n//   let set_escape_locked = ${(is_locked) => {\n//     is_escape_locked = is_locked;\n//   }}\n//   navigator.keyboard.lock = async (...args) => {\n//     let keycodes = args[0]\n\n//     if (keycodes == null || keycodes.includes('Escape')) {\n//       try {\n//         set_escape_locked(true)\n//       } catch (err) {}\n//     }\n//     return old_lock(...args)\n//   }\n// }`;\n// let keyboard_lock_script_element = document.createElement(\"script\");\n// keyboard_lock_script_element.innerHTML = keyboard_lock_injection;\n// document.documentElement.appendChild(keyboard_lock_script_element);\n// document.documentElement.removeChild(keyboard_lock_script_element);\n\nlet onEscapePress = (fn) => {\n  let escape_timeout = null;\n  let escape_listener = (e) => {\n    if (!e.defaultPrevented && e.key === \"Escape\") {\n      if (is_escape_locked) {\n        escape_timeout = setTimeout(() => {\n          fn();\n        }, 1.2 * 1000);\n      } else {\n        fn();\n      }\n    }\n  };\n\n  let escape_up_listener = (e) => {\n    if (!e.defaultPrevented && e.key === \"Escape\") {\n      clearTimeout(escape_timeout);\n    }\n  };\n\n  window.addEventListener(\"keydown\", escape_listener);\n  window.addEventListener(\"keyup\", escape_up_listener);\n\n  return () => {\n    window.removeEventListener(\"keydown\", escape_listener);\n    window.removeEventListener(\"keyup\", escape_up_listener);\n  };\n};\n\nlet current_mode = { mode: \"ask\", pip: false };\n\nlet check_disabled_state = async () => {\n  try {\n    let { mode, pip } = await get_host_config_local();\n    current_mode = { mode, pip };\n    window.postMessage(\n      {\n        type: \"WINDOWED-notify\",\n        disabled: is_shift_pressed\n          ? false\n          : mode === \"fullscreen\" && pip === false,\n      },\n      \"*\",\n    );\n  } catch (err) {\n    // prettier-ignore\n    console.warn(`[Windowed] Error while checking if windowed is enabled or not`, err)\n  }\n};\n\ncheck_disabled_state();\n\nbrowser.runtime.onConnect.addListener(async (port) => {\n  port.postMessage({ type: \"I_exists_ping\" });\n  check_disabled_state();\n});\n\nlet theme = null;\nsend_chrome_message({\n  type: \"get_theme\",\n}).then((x) => {\n  theme = x;\n});\n"
  },
  {
    "path": "extension/Popup/Popup.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <style>\n      :root {\n        --theme-popup: transparent;\n        --theme-popup_text: currentColor;\n        --theme-popup_highlight: rgba(255, 255, 255, 0.1);\n        --theme-popup_highlight_text: currentColor;\n      }\n      body,\n      html {\n        margin: 0;\n        padding: 0;\n        background-color: var(--theme-popup, #1a1a1a);\n        color-scheme: light dark;\n      }\n\n      button {\n        font-size: inherit;\n        border: none;\n        box-shadow: none;\n        background-color: transparent;\n      }\n\n      kbd {\n        font-size: 0.9em;\n        padding-inline: 4px;\n        border-radius: 4px;\n\n        @media (prefers-color-scheme: dark) {\n          border: solid 1px #676767;\n          background-color: #404040;\n        }\n        @media (prefers-color-scheme: light) {\n          border: solid 1px #979797;\n          background-color: #c0c0c0;\n        }\n      }\n\n      .popup {\n        font-family: system-ui, sans-serif;\n        user-select: none;\n\n        /* border-radius: 3px; */\n        /* border: solid #eee 1px; */\n        /* box-shadow: 0px 2px 4px #00000026; */\n        padding-top: 12px;\n        padding-bottom: 12px;\n\n        .browser-firefox & {\n          padding-inline: 12px;\n        }\n\n        font-size: 14px;\n        color: var(--theme-popup_text);\n        width: min-content;\n        min-width: 210px;\n        z-index: 2147483647;\n\n        display: flex;\n        flex-direction: column;\n        align-items: stretch;\n      }\n\n      .popup:focus:not(:focus-visible) {\n        outline: none;\n      }\n\n      .popup [data-target] {\n        cursor: pointer;\n\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n\n        white-space: nowrap;\n\n        .browser-chrome & {\n          padding-inline: 1.25em;\n          padding-block: 0.5em;\n        }\n        .browser-firefox & {\n          padding-inline: 0.5em;\n          padding-block: 0.5em;\n          border-radius: 8px;\n        }\n\n        &::-moz-focus-inner,\n        .popup::-moz-focus-inner {\n          border: none;\n        }\n\n        &:hover,\n        &:focus {\n          &:not(:focus-visible) {\n            outline: none;\n          }\n\n          background-color: var(--theme-popup_highlight);\n          color: var(--theme-popup_highlight_text);\n          /*.browser-firefox & {\n          }\n          .browser-chrome & {\n            background-color: rgba(255, 255, 255, 0.2);\n          }*/\n        }\n\n        & > input[type=\"radio\"],\n        & > input[type=\"checkbox\"] {\n          margin: 0;\n          margin-right: 16px;\n        }\n        & > .icon {\n          height: 1.2em;\n          width: 1.2em;\n          margin-right: 1em;\n        }\n      }\n\n      .popup::-moz-focus-inner {\n        border: none;\n      }\n\n      .mask-icon {\n        background-color: currentColor;\n        mask-size: contain;\n        /*mask-image: url(../Images/Icon_InWindow_Mono@scalable.svg);*/\n      }\n\n      #set-as-default {\n        display: flex;\n        flex-direction: column;\n\n        font-size: 14px;\n        color: #999;\n\n        .browser-firefox & {\n          border-radius: 8px;\n        }\n        .browser-chrome & {\n          padding-block: 8px;\n        }\n\n        &:hover,\n        &:focus {\n          background-color: var(--theme-popup_highlight);\n          color: var(--theme-popup_highlight_text);\n        }\n\n        &:disabled {\n          display: none;\n        }\n      }\n    </style>\n    <style>\n      hide-if-no-picture-in-picture {\n        display: none;\n      }\n      .picture-in-picture-support hide-if-no-picture-in-picture {\n        all: inherit;\n      }\n\n      body[data-mode-selected=\"true\"] hide-if-ask {\n        display: none;\n      }\n\n      p {\n        all: unset;\n        display: block;\n        padding-block: 4px;\n        font-size: 14px;\n\n        .browser-firefox & {\n          padding-inline: 0px;\n        }\n        .browser-chrome & {\n          padding-inline: 20px;\n        }\n      }\n\n      button {\n        width: 100%;\n        min-height: 28px;\n        cursor: pointer;\n        padding: 1.25em;\n        padding-top: 0.25em;\n        padding-bottom: 0.25em;\n        /*background-color: white;*/\n\n        display: flex;\n        flex-direction: row;\n        align-items: center;\n\n        font-size: inherit;\n        border: none;\n        box-shadow: none;\n\n        white-space: nowrap;\n      }\n\n      button::-moz-focus-inner {\n        border: none;\n      }\n      button:focus {\n        filter: brightness(0.95);\n      }\n      button:focus:not(:focus-visible) {\n        outline: none;\n      }\n      button:hover {\n        filter: brightness(0.9);\n      }\n\n      button > input[type=\"radio\"],\n      button > input[type=\"checkbox\"] {\n        margin: 0;\n        margin-right: 16px;\n      }\n\n      button > img {\n        height: 1.2em;\n        width: 1.2em;\n        margin-right: 1em;\n      }\n\n      .reload {\n        &:hover,\n        &:focus {\n          background-color: var(--theme-popup_highlight);\n          color: var(--theme-popup_highlight_text);\n        }\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"popup\" tabindex=\"1\">\n      <div id=\"something-bad\">\n        <p>...</p>\n      </div>\n\n      <div id=\"disabled-because-security\" style=\"display: none\">\n        <p>For security reasons, Windowed is not supported on this domain.</p>\n      </div>\n\n      <div id=\"need-a-refresh\" style=\"display: none\">\n        <p>You need to refresh this page for Windowed to function properly</p>\n        <div style=\"height: 8px\"></div>\n        <button class=\"reload\">Reload page</button>\n      </div>\n\n      <div id=\"working\" style=\"display: none\">\n        <p>\n          Fullscreen on <i data-placeholder=\"domain\">DOMAIN.COM</i> should\n          always:\n        </p>\n        <div style=\"height: 4px\"></div>\n\n        <form>\n          <label data-target=\"ask\" title=\"Ask\">\n            <input type=\"radio\" name=\"behaviour\" value=\"ask\" checked />\n            <div class=\"icon\"></div>\n            <span>Ask</span>\n          </label>\n          <label data-target=\"windowed\" title=\"Windowed\">\n            <input type=\"radio\" name=\"behaviour\" value=\"windowed\" />\n            <div\n              class=\"icon mask-icon\"\n              data-dynamic-mask-image=\"../Images/Icon_Windowed_Mono@scalable.svg\"\n            ></div>\n            <span>Windowed</span>\n          </label>\n          <label data-target=\"in-window\" title=\"In-window\">\n            <input type=\"radio\" name=\"behaviour\" value=\"in-window\" />\n            <div\n              class=\"icon mask-icon\"\n              data-dynamic-mask-image=\"../Images/Icon_InWindow_Mono@scalable.svg\"\n            ></div>\n            <span>In-window</span>\n          </label>\n          <label data-target=\"fullscreen\" title=\"Fullscreen\">\n            <input type=\"radio\" name=\"behaviour\" value=\"fullscreen\" />\n            <div\n              class=\"icon mask-icon\"\n              data-dynamic-mask-image=\"../Images/Icon_EnterFullscreen@scalable.svg\"\n            ></div>\n            <span>Fullscreen</span>\n          </label>\n\n          <hide-if-no-picture-in-picture>\n            <div style=\"height: 8px\"></div>\n            <label\n              data-target=\"picture-in-picture\"\n              title=\"Picture-in-Picture mode, only if supported by the page. This will overwrite the preference above (even ask)\"\n            >\n              <input type=\"checkbox\" name=\"picture_in_picture\" />\n              <!--<img src=\"../Images/Icon_PiP@scalable.svg\" />-->\n              <div\n                style=\"\n                  background-color: currentColor;\n                  mask-image: url(&quot;../Images/Icon_PiP@scalable.svg&quot;);\n                \"\n              ></div>\n              <span>PiP (if possible)</span>\n            </label>\n          </hide-if-no-picture-in-picture>\n\n          <button id=\"set-as-default\" disabled style=\"margin-top: 8px\">\n            <div>Set as default</div>\n            <div>for all websites</div>\n          </button>\n\n          <hide-if-ask>\n            <p style=\"margin-top: 8px\">\n              You can always use <kbd>Shift</kbd> to trigger the ask popup.\n            </p>\n          </hide-if-ask>\n        </form>\n      </div>\n    </div>\n    <script src=\"../Javascript/browser-polyfill.min.js\"></script>\n    <script src=\"./Popup.js\" type=\"module\"></script>\n    <script src=\"./Theme-Colors.js\" type=\"module\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "extension/Popup/Popup.js",
    "content": "import { browser } from \"../Vendor/Browser.js\";\n\n/** @param {() => unknown} fn */\nlet run = (fn) => fn();\n\n/** @param {string} id */\nlet show_html_for = (id) => {\n  /** @type {NodeListOf<HTMLElement>} */\n  let popup_divs = document.querySelectorAll(\".popup > div\");\n  for (let div of popup_divs) {\n    div.style.display = \"none\";\n  }\n  /** @type {HTMLElement} */\n  let to_show = document.querySelector(`.popup > ${id}`);\n  to_show.style.display = null;\n  return to_show;\n};\n\nfor (let element of document.querySelectorAll(\"[data-dynamic-mask-image]\")) {\n  if (element instanceof HTMLElement) {\n    let mask_image_path = element.dataset.dynamicMaskImage;\n    let string = browser.runtime.getURL(mask_image_path);\n    element.style.setProperty(\"mask-image\", `url(${string})`);\n  }\n}\n\nlet browser_info = browser.runtime.getBrowserInfo\n  ? await browser.runtime.getBrowserInfo()\n  : await Promise.resolve({ name: \"Chrome\" });\nlet is_firefox = browser_info.name === \"Firefox\";\n\ndocument.documentElement.classList.add(\n  is_firefox ? \"browser-firefox\" : \"browser-chrome\",\n);\n\n/**\n * Function that works with my Extension Messaging Wrapper for nice error handling\n * @param {any} message\n * */\nlet send_chrome_message = async (message) => {\n  let { type, value } = await browser.runtime.sendMessage(message);\n  if (type === \"resolve\") {\n    return value;\n  } else {\n    let err = new Error(value.message);\n    err.stack = value.stack;\n    // err.stack = [\n    //   ...x.value.stack.split('\\n'),\n    //   'From postMessage to background page',\n    //   ...stack,\n    // ].join('\\n');\n    throw err;\n  }\n};\n\nlet ALL_MODE = \"mode(*)\";\nlet ALL_PIP = \"pip(*)\";\n\n/**\n * @param {string} mode\n * @param {boolean} disabled\n * @returns {import(\"../Background/BackgroundModule.js\").WindowedMode}\n */\nlet clean_mode = (mode, disabled) => {\n  // Any other mode than the known ones are ignored\n  if (mode == \"fullscreen\" || mode == \"windowed\" || mode == \"in-window\") {\n    return mode;\n  }\n  return disabled === true ? \"fullscreen\" : \"ask\";\n};\n/** @param {import(\"webextension-polyfill\").Tabs.Tab} tab */\nlet get_host_config = async (tab) => {\n  let host = new URL(tab.url).host;\n  let host_mode = `mode(${host})`;\n  let host_pip = `pip(${host})`;\n  let {\n    [host_mode]: mode,\n    [host]: disabled,\n    [host_pip]: pip,\n    [ALL_MODE]: all_mode,\n    [ALL_PIP]: all_pip,\n  } = await browser.storage.sync.get([\n    host_mode,\n    host,\n    host_pip,\n    ALL_MODE,\n    ALL_PIP,\n  ]);\n\n  return {\n    mode: clean_mode(mode ?? all_mode, disabled),\n    pip: (pip ?? all_pip) === true,\n    all_mode: clean_mode(all_mode, false),\n    all_pip: all_pip === true,\n  };\n};\n\n/** @type {{ [tabid: number]: Promise<boolean> }} */\nlet current_port_promises = {};\n/**\n * Check if we can connect with the Windowed content script in a tab\n * @param {number} tabId\n * @returns {Promise<boolean>}\n */\nlet ping_content_script = async (tabId) => {\n  try {\n    if (current_port_promises[tabId] != null) {\n      return await current_port_promises[tabId];\n    } else {\n      current_port_promises[tabId] = new Promise((resolve, reject) => {\n        let port = browser.tabs.connect(tabId);\n        port.onMessage.addListener((message) => {\n          resolve(true);\n          port.disconnect();\n        });\n        port.onDisconnect.addListener((p) => {\n          resolve(false);\n        });\n      });\n      return await current_port_promises[tabId];\n    }\n  } finally {\n    delete current_port_promises[tabId];\n  }\n};\n\nlet initialize_page = async () => {\n  let tabs = await browser.tabs.query({ active: true, currentWindow: true });\n  let tab = tabs[0];\n\n  if (tab.status !== \"complete\") {\n    await new Promise((resolve) => {\n      setTimeout(resolve, 100);\n    });\n    await initialize_page();\n    return;\n  }\n\n  let has_contentscript_active =\n    tab.status === \"complete\" && (await ping_content_script(tab.id));\n\n  if (\n    has_contentscript_active === false &&\n    (tab.url.match(/^about:/) ||\n      tab.url.match(/^chrome:\\/\\//) ||\n      tab.url.match(/^edge:\\/\\//) ||\n      tab.url.match(/^https?:\\/\\/chrome\\.google\\.com/) ||\n      tab.url.match(/^https?:\\/\\/support\\.mozilla\\.org/))\n  ) {\n    await show_html_for(\"#disabled-because-security\");\n    return;\n  }\n\n  if (tab.status === \"complete\" && has_contentscript_active === false) {\n    let $root = await show_html_for(\"#need-a-refresh\");\n    $root.querySelector(\".reload\").addEventListener(\"click\", async () => {\n      await browser.tabs.reload(tab.id);\n      await initialize_page();\n    });\n    return;\n  }\n\n  // @ts-ignore\n  if (HTMLVideoElement.prototype.requestPictureInPicture != null) {\n    document.body.classList.add(\"picture-in-picture-support\");\n  }\n\n  let host = new URL(tab.url).host;\n  let host_mode = `mode(${host})`;\n  let host_pip = `pip(${host})`;\n  for (let element of document.querySelectorAll(\"[data-placeholder=domain]\")) {\n    element.textContent = host;\n  }\n\n  let $root = await show_html_for(\"#working\");\n  let $form = $root.querySelector(\"form\");\n  /** @type {HTMLButtonElement} */\n  let $set_as_default_button = $root.querySelector(\"#set-as-default\");\n  // @ts-ignore\n  let behaviour_input = $form.elements.behaviour;\n  // @ts-ignore\n  let picture_in_picture_input = $form.elements.picture_in_picture;\n\n  let config = await get_host_config(tab);\n\n  document.body.setAttribute(\"data-mode-selected\", config.mode);\n  $set_as_default_button.disabled =\n    behaviour_input.value === config.all_mode &&\n    picture_in_picture_input.checked === config.all_pip;\n\n  $form.addEventListener(\"input\", async (e) => {\n    await browser.storage.sync.set({\n      [host_mode]: behaviour_input.value,\n      [host_pip]: picture_in_picture_input.checked,\n    });\n\n    initialize_page();\n    send_chrome_message({\n      type: \"update_windowed_button\",\n      id: tab.id,\n    });\n  });\n\n  $set_as_default_button.addEventListener(\"click\", async (e) => {\n    await browser.storage.sync.set({\n      [ALL_MODE]: behaviour_input.value,\n      [ALL_PIP]: picture_in_picture_input.checked,\n    });\n    config = await get_host_config(tab);\n    await send_chrome_message({\n      type: \"update_windowed_button\",\n      id: tab.id,\n    });\n  });\n\n  behaviour_input.value = config.mode;\n  picture_in_picture_input.checked = config.pip;\n};\n\nrun(initialize_page);\n\nexport {};\n"
  },
  {
    "path": "extension/Popup/Theme-Colors.js",
    "content": "import { themecolor_to_string } from \"../utilities/themecolor-to-string.js\";\nimport { browser } from \"../Vendor/Browser.js\";\n\nif (browser.theme) {\n  let theme = await browser.theme.getCurrent();\n\n  if (theme.colors != undefined) {\n    let root = document.documentElement;\n    console.log(`Object.entries(theme.colors):`, Object.entries(theme.colors));\n\n    for (let [key, value] of Object.entries(theme.colors)) {\n      console.log(`key:`, key);\n      if (value != undefined) {\n        root.style.setProperty(`--theme-${key}`, themecolor_to_string(value));\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "extension/Vendor/Browser.d.ts",
    "content": "export declare let browser: import(\"webextension-polyfill\").Browser;\n"
  },
  {
    "path": "extension/Vendor/Browser.js",
    "content": "import \"./browser-polyfill.min.js\";\n\n// @ts-ignore\nexport let browser = globalThis.browser;\n"
  },
  {
    "path": "extension/Windowed-inject-into-page.js",
    "content": "{\n  // Alliases for different browsers\n  let requestFullscreen_aliasses = [\"requestFullscreen\",\"mozRequestFullScreen\",\"webkitRequestFullscreen\",\"webkitRequestFullScreen\",\"msRequestFullscreen\"];\n  let exitFullscreen_aliasses = [\"exitFullscreen\",\"webkitExitFullscreen\",\"webkitCancelFullScreen\",\"mozCancelFullScreen\",\"msExitFullscreen\"];\n  let fullscreenelement_aliasses = [\"fullscreenElement\",\"webkitFullscreenElement\",\"mozFullscreenElement\",\"mozFullScreenElement\",\"msFullscreenElement\",\"webkitCurrentFullScreenElement\"];\n  let fullscreenchange_aliasses = [\"fullscreenchange\",\"webkitfullscreenchange\",\"mozfullscreenchange\",\"MSFullscreenChange\"];\n\n  const send_event = (element, type) => {\n    const event = new Event(type, {\n      bubbles: true,\n      cancelBubble: false,\n      cancelable: false,\n    });\n    // if (element[`on${type}`]) {\n    //   element[`on${type}`](event);\n    // }\n    element.dispatchEvent(event);\n  };\n\n  const send_fullscreen_events = (element) => {\n    for (let fullscreenchange of fullscreenchange_aliasses) {\n      send_event(document, fullscreenchange);\n    }\n    send_event(window, 'resize');\n  };\n\n  let all_communication_id = 0;\n  let external_function = (function_id) => async (...args) => {\n    let request_id = all_communication_id;\n    all_communication_id = all_communication_id + 1;\n\n    window.postMessage({\n      type: 'CUSTOM_WINDOWED_FROM_PAGE',\n      request_id: request_id,\n      function_id: function_id,\n      args: args,\n    }, '*');\n\n    return new Promise((resolve, reject) => {\n      let listener = (event) => {\n        // We only accept messages from ourselves\n        if (event.source != window) return;\n        if (event.data == null) return;\n\n        if (event.data.type === 'CUSTOM_WINDOWED_TO_PAGE') {\n          if (event.data.request_id === request_id) {\n            window.removeEventListener('message', listener);\n            if (event.data.resultType === 'resolve') {\n              resolve(event.data.result);\n            } else {\n              let err = new Error(event.data.result.message);\n              err.stack = event.data.result.stack;\n              reject(err);\n            }\n          }\n        }\n      }\n      window.addEventListener('message', listener);\n    });\n  }\n\n  let overwrite = (object, property, value) => {\n    try {\n      if (property in object) {\n        Object.defineProperty(object, property, {\n          value: value,\n          configurable: true,\n          writable: true,\n        });\n      }\n    } catch (err) {\n      // Nothing\n    }\n  }\n\n  let set_fullscreen_element = (element = null) => {\n    if (element == null) {\n      throw new Error('WINDOWED: Got null in set_fullscreen_element');\n    }\n\n    overwrite(document, 'webkitIsFullScreen', true); // Old old old\n    overwrite(document, 'fullscreen', true); // Old old old\n    for (let fullscreenelement_alias of fullscreenelement_aliasses) {\n      overwrite(document, fullscreenelement_alias, element);\n    }\n  }\n\n  let make_tab_go_fullscreen = external_function(1);\n  let make_tab_go_inwindow = external_function(2);\n\n  let create_popup = external_function(3);\n\n  let make_tab_exit_fullscreen = external_function(4);\n\n  let exitFullscreen = async function(original) {\n    let windowed_fullscreen = document.querySelector('[data-windowed_long_id_that_does_not_conflict_active], [data-windowed_long_id_that_does_not_conflict_shadowdom]');\n\n    if (windowed_fullscreen) {\n      // If the fullscreen element is a frame, tell it to exit fullscreen too\n      if (typeof windowed_fullscreen.postMessage === 'function') {\n        document.fullscreenElement.postMessage.sendMessage({ type: \"exit_fullscreen_iframe\" });\n      }\n\n      // Reset all the variables to their browser form\n      delete window.screen.width;\n      delete window.screen.height;\n      delete document['webkitIsFullScreen'];\n      delete document['fullscreen'];\n      for (let fullscreenelement_alias of fullscreenelement_aliasses) {\n        delete document[fullscreenelement_alias];\n      }\n\n      await make_tab_exit_fullscreen();\n    } else {\n      original();\n    }\n  }\n\n  \n  const requestFullscreen_windowed = async function(original, ...args) {\n    const element = this;\n    element.dataset['windowed_long_id_that_does_not_conflict_select'] = true;\n\n    let shadowroot = element.getRootNode()\n    while (shadowroot != null && shadowroot.host != null) {\n        shadowroot.host.dataset['windowed_long_id_that_does_not_conflict_shadowdom'] = true;\n        shadowroot = shadowroot.host.getRootNode()\n    }\n\n    // Tell extension code (outside of this block) to go into fullscreen\n    // window.postMessage({ type: force ? \"enter_fullscreen\" : \"show_fullscreen_popup\" }, \"*\");\n    // send_windowed_event(element, force ? \"enter_fullscreen\" : \"show_fullscreen_popup\");\n    try {\n      await create_popup();\n    } catch (err) {\n      // Anything gone wrong, we default to normal fullscreen\n      console.error(err);\n      console.error('[Windowed] Something went wrong, so I default to normal fullscreen:', err.stack);\n      original();\n    }\n  }\n\n  // Because firefox is super cool, it is also super frustrating...\n  // So firefox does not allow fullscreen calls from promises, even if it is basically sync.\n  // Therefor I need to first define this as sync, and from here call the async version.\n  let MUTATE_is_windowed_enabled = true;\n  let requestFullscreen = function(original, ...args) {\n    if (MUTATE_is_windowed_enabled === false) {\n      return original()\n    } else {\n      requestFullscreen_windowed.call(this, original, ...args);\n    }\n  }\n\n  let finish_fullscreen = () => {\n    // Because youtube actually checks for those sizes?!\n    const window_width = Math.max(window.outerWidth, window.innerWidth);\n    const window_height = Math.max(window.outerHeight, window.innerHeight);\n\n    overwrite(window.screen, 'width', window_width);\n    overwrite(window.screen, 'height', window_height);\n\n    let element = document.querySelector('[data-windowed_long_id_that_does_not_conflict_select]');\n    if (element == null) {\n      console.warn('[WINDOWED] Strange, no fullscreen element shown');\n      return;\n    }\n\n    document.body.focus();\n    element.focus();\n    set_fullscreen_element(element || document.body);\n    send_fullscreen_events();\n  }\n\n  window.addEventListener(\"message\", (message) => {\n    // Message from content script or parent content script\n    if (message.source === message.target || message.source === window.parent) {\n      if (message.data?.type === 'WINDOWED-confirm-fullscreen') {\n        finish_fullscreen();\n      }\n\n      if (message.data?.type === 'WINDOWED-exit-fullscreen') {\n        exitFullscreen.call(document, original_exitFullscreen);\n      }\n\n      if (message.data?.type === 'WINDOWED-notify') {\n        MUTATE_is_windowed_enabled = !message.data.disabled;\n      }\n    }\n\n    // Message from frame inside the page (these are tricky not sure if I still know how this works)\n    const frame = [...document.querySelectorAll('iframe')].find(x => x.contentWindow === message.source);\n    if (frame != null) {\n      if (message.data?.type === 'enter_inwindow_iframe') {\n        frame.dataset['windowed_long_id_that_does_not_conflict_select'] = \"true\";\n        make_tab_go_inwindow();\n      }\n      if (message.data?.type === 'enter_fullscreen_iframe') {\n        frame.dataset['windowed_long_id_that_does_not_conflict_select'] = \"true\";\n        make_tab_go_fullscreen();\n      }\n      if (message.data?.type === 'exit_fullscreen_iframe') {\n        // Call my exitFullscreen on the document\n        exitFullscreen.call(document, original_exitFullscreen);\n      }\n    }\n  });\n\n  \n  let original_requestFullscreen = null;\n  for (let requestFullscreenAlias of requestFullscreen_aliasses) {\n    if (typeof Element.prototype[requestFullscreenAlias] === 'function') {\n      let original_function = Element.prototype[requestFullscreenAlias];\n      original_requestFullscreen = original_function;\n      Element.prototype[requestFullscreenAlias] = function(...args) {\n        return requestFullscreen.call(this, original_function.bind(this), ...args);\n      };\n    }\n  }\n\n  \n  let original_exitFullscreen = null;\n  for (let exitFullscreenAlias of exitFullscreen_aliasses) {\n    if (typeof Document.prototype[exitFullscreenAlias] === 'function') {\n      let original_function = Document.prototype[exitFullscreenAlias];\n      original_exitFullscreen = original_function;\n      Document.prototype[exitFullscreenAlias] = function(...args) {\n        return exitFullscreen.call(this, original_function.bind(this), ...args);\n      };\n    }\n  }\n}"
  },
  {
    "path": "extension/manifest-firefox.json",
    "content": "{\n  \"name\": \"Windowed - floating Youtube/every website\",\n  \"short_name\": \"Windowed\",\n  \"description\": \"Changes fullscreen buttons to go into a popup. Works for every website that uses fullscreen, including Youtube, Vimeo, Netflix\",\n  \"developer\": {\n    \"name\": \"Michiel Dral\",\n    \"url\": \"https://dral.eu/\"\n  },\n  \"version\": \"34\",\n  \"manifest_version\": 3,\n  \"browser_specific_settings\": {\n    \"gecko\": {\n      \"id\": \"{477dbe5e-1742-4641-a2c3-b6113bb5cf6e}\"\n    }\n  },\n  \"permissions\": [\"storage\", \"tabs\", \"offscreen\"],\n  \"action\": {\n    \"default_popup\": \"Popup/Popup.html\",\n    \"default_icon\": {\n      \"32\": \"/Images/Icon_Windowed_Mono@1x.png\"\n    }\n  },\n  \"content_scripts\": [\n    {\n      \"run_at\": \"document_start\",\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"Vendor/browser-polyfill.min.js\", \"Content.js\"],\n      \"all_frames\": true\n    },\n    {\n      \"run_at\": \"document_start\",\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"Windowed-inject-into-page.js\"],\n      \"all_frames\": true,\n      \"world\": \"MAIN\"\n    }\n  ],\n  \"background\": {\n    \"scripts\": [\"Background/BackgroundModule.js\"],\n    \"service_worker\": \"Background/BackgroundModule.js\",\n    \"type\": \"module\"\n  },\n  \"web_accessible_resources\": [\n    {\n      \"resources\": [\"Images/*\"],\n      \"matches\": [\"<all_urls>\"]\n    },\n    {\n      \"resources\": [\n        \"Vendor/browser-polyfill.min.js\",\n        \"Vendor/Browser.js\",\n        \"Windowed-inject-into-page.js\"\n      ],\n      \"matches\": [\"<all_urls>\"]\n    }\n  ],\n  \"icons\": {\n    \"16\": \"Icons/Icon_16.png\",\n    \"32\": \"Icons/Icon_32.png\",\n    \"64\": \"Icons/Icon_64.png\",\n    \"128\": \"Icons/Icon_128.png\"\n  }\n}\n"
  },
  {
    "path": "extension/manifest.json",
    "content": "{\n  \"name\": \"Windowed - floating Youtube/every website\",\n  \"short_name\": \"Windowed\",\n  \"description\": \"Changes fullscreen buttons to go into a popup. Works for every website that uses fullscreen, including Youtube, Vimeo, Netflix\",\n  \"developer\": {\n    \"name\": \"Michiel Dral\",\n    \"url\": \"https://dral.eu/\"\n  },\n  \"version\": \"34\",\n  \"manifest_version\": 3,\n  \"browser_specific_settings\": {\n    \"gecko\": {\n      \"id\": \"{477dbe5e-1742-4641-a2c3-b6113bb5cf6e}\"\n    }\n  },\n  \"permissions\": [\"storage\", \"tabs\", \"offscreen\"],\n  \"action\": {\n    \"default_popup\": \"Popup/Popup.html\",\n    \"default_icon\": {\n      \"32\": \"/Images/Icon_Windowed_Mono@1x.png\"\n    }\n  },\n  \"content_scripts\": [\n    {\n      \"run_at\": \"document_start\",\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"Vendor/browser-polyfill.min.js\", \"Content.js\"],\n      \"all_frames\": true\n    },\n    {\n      \"run_at\": \"document_start\",\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"Windowed-inject-into-page.js\"],\n      \"all_frames\": true,\n      \"world\": \"MAIN\"\n    }\n  ],\n  \"background\": {\n    \"scripts\": [\"Background/BackgroundModule.js\"],\n    \"service_worker\": \"Background/BackgroundModule.js\",\n    \"type\": \"module\"\n  },\n  \"web_accessible_resources\": [\n    {\n      \"resources\": [\"Images/*\"],\n      \"matches\": [\"<all_urls>\"]\n    },\n    {\n      \"resources\": [\n        \"Vendor/browser-polyfill.min.js\",\n        \"Vendor/Browser.js\",\n        \"Windowed-inject-into-page.js\"\n      ],\n      \"matches\": [\"<all_urls>\"]\n    }\n  ],\n  \"icons\": {\n    \"16\": \"Icons/Icon_16.png\",\n    \"32\": \"Icons/Icon_32.png\",\n    \"64\": \"Icons/Icon_64.png\",\n    \"128\": \"Icons/Icon_128.png\"\n  }\n}\n"
  },
  {
    "path": "extension/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"esnext\",\n    \"lib\": [\"ES2020\", \"DOM\", \"dom.iterable\"],\n    \"allowJs\": true,\n    \"checkJs\": true,\n    \"noEmit\": true,\n    \"strict\": false,\n    \"noImplicitThis\": true,\n    \"alwaysStrict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"moduleDetection\": \"force\"\n  },\n  \"exclude\": [\"./Vendor/browser-polyfill.min.js\"]\n}\n"
  },
  {
    "path": "extension/utilities/themecolor-to-string.js",
    "content": "/**\n * @param {import(\"webextension-polyfill\").Manifest.ThemeColor} color\n * @returns {string}\n */\nexport let themecolor_to_string = (color) => {\n  if (typeof color === \"string\") {\n    return color;\n  } else if (Array.isArray(color)) {\n    if (color.length === 3) {\n      return `rgb(${color[0]}, ${color[1]}, ${color[2]})`;\n    } else if (color.length === 4) {\n      return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`;\n    } else {\n      // prettier-ignore\n      throw new Error(`Invalid theme color array: ${JSON.stringify(color)}`);\n    }\n  } else {\n    throw new Error(`Invalid theme color: ${color}`);\n  }\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"windowed\",\n  \"version\": \"1.0.0\",\n  \"description\": \"[Install in Chrome Webstore](https://chrome.google.com/webstore/detail/windowed-floating-youtube/gibipneadnbflmkebnmcbgjdkngkbklb)\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"(cd extension; zip -r ../extension.zip .)\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n    \"dev\": \"web-ext run -s extension --url \\\"https://youtu.be/Yocja_N5s1I\\\"\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/dralletje/Windowed.git\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/dralletje/Windowed/issues\"\n  },\n  \"homepage\": \"https://github.com/dralletje/Windowed#readme\",\n  \"devDependencies\": {\n    \"@types/webextension-polyfill\": \"^0.12.4\",\n    \"prettier\": \"^3.4.2\",\n    \"puppeteer\": \"^1.18.1\",\n    \"web-ext\": \"^7.11.0\"\n  },\n  \"prettier\": {},\n  \"packageManager\": \"pnpm@10.16.1\"\n}\n"
  },
  {
    "path": "testing/iframe/iframe.html",
    "content": "<html style=\"background-color: black\">\n<body>\n  <iframe id=\"thing\" src=\"about:blank\" style=\"border: none; height: 300px\"></iframe>\n  <script>\n    var iframe = document.getElementById('thing');\n    iframe.contentWindow.document.open();\n    iframe.contentWindow.document.write(`<html><body>\n      <div style=\"background-color: red; width: 200px; height: 200px;\" id=\"supersizeme\"></div>\n      <button onclick=\"console.log('hi'); document.querySelector('#supersizeme').requestFullscreen()\">Fullscreen</button>\n    </body></html>`);\n    iframe.contentWindow.document.close();\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "testing/page.html",
    "content": "<html>\n<body>\n  <div style=\"width: 200px; height: 200px; background-color: blue\"></div>\n  <div\n    class=\"video\"\n    style=\"\n      padding: 50px;\n      min-height: 300px;\n      width: 500px;\n      background-color: black;\n    \"\n  >\n    <div\n      onclick=\"document.exitFullscreen()\"\n      class=\"exit-fullscreen\"\n      style=\"\n        display: inline-block;\n        padding: 5px;\n        color: white;\n        border: 2px solid white;\n      \"\n    >Exit fullscreen</div>\n    <div\n      onclick=\"document.querySelector('.video').requestFullscreen()\"\n      class=\"fullscreen\"\n      style=\"\n        display: inline-block;\n        padding: 5px;\n        color: white;\n        border: 2px solid white;\n      \"\n    >Fullscreen</div>\n  </div>\n\n  <div style=\"width: 200px; height: 200px; background-color: red\"></div>\n</body>\n</html>\n"
  },
  {
    "path": "testing/stadia-like.html",
    "content": "<html>\n<body>\n  <div style=\"width: 200px; height: 200px; background-color: blue\"></div>\n  <div\n    class=\"video\"\n    style=\"\n      padding: 50px;\n      min-height: 300px;\n      width: 500px;\n      background-color: black;\n    \"\n  >\n    <div\n      onclick=\"document.exitFullscreen()\"\n      class=\"exit-fullscreen\"\n      style=\"\n        display: inline-block;\n        padding: 5px;\n        color: white;\n        border: 2px solid white;\n      \"\n    >Exit fullscreen</div>\n    <div\n      onclick=\"document.querySelector('.video').requestFullscreen()\"\n      class=\"fullscreen\"\n      style=\"\n        display: inline-block;\n        padding: 5px;\n        color: white;\n        border: 2px solid white;\n      \"\n    >Fullscreen</div>\n  </div>\n\n  <div style=\"width: 200px; height: 200px; background-color: red\"></div>\n\n  <script>\n    document.addEventListener('keydown', (e) => {\n      if (e.key === 'Escape') {\n        e.preventDefault();\n      }\n    });\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "testing/test.js",
    "content": "let puppeteer = require('puppeteer');\nlet path = require('path');\n\n\n\nlet extension_path = path.resolve(__dirname, '../extension');\nlet page_html = path.resolve(__dirname, 'page.html');\n\nconsole.log(`extension_path:`, extension_path)\n\nlet run = async () => {\n  try {\n    let browser = await puppeteer.launch({\n      devtools: false,\n      headless: false,\n      sloMo: true,\n      args: [\n        // I need this for CORS stuff\n        '--disable-web-security',\n\n        // Some things I saw on the internet idk if they make it better\n        \"--proxy-server='direct://'\",\n        '--proxy-bypass-list=*',\n        '--no-sandbox',\n\n        `--disable-extensions-except=${extension_path}/`,\n        `--load-extension=${extension_path}/`,\n      ],\n    });\n\n    let page = await browser.newPage();\n\n    page.setDefaultTimeout = 5000;\n    await page.setUserAgent(\n      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'\n    );\n\n    // let browser_console = make_holy_console({\n    //   name: 'From Browser',\n    //   map_fn: (line) => chalk.dim(line),\n    // });\n    page.on('console', async (msg) => {\n      let args = await Promise.all(msg.args().map((x) => x.jsonValue()));\n      console.log('BROWSER:', ...args);\n    });\n\n    let url = `file://${page_html}`;\n    page.goto(url).catch((err) => {\n      console.log(`Error from page.goto:`, err.message);\n    });\n\n    await page._client.send('Emulation.clearDeviceMetricsOverride')\n\n    await page.screenshot({\n      path: path.resolve(__dirname, `./screenshots/normal.png`),\n    });\n\n    for (let window_target of ['windowed', 'in-window', 'fullscreen']) {\n      let fullscreen_button = await page.waitForSelector(`.fullscreen`);\n      await fullscreen_button.click();\n      let windowed_button = await page.waitForSelector(`[data-target=\"${window_target}\"]`);\n      await windowed_button.click();\n\n      // await new Promise((resolve) => {\n      //   setTimeout(() => {\n      //     resolve();\n      //   }, 3000);\n      // })\n\n      let viewport = page.viewport();\n      // console.log(`viewport:`, viewport)\n      await page.screenshot({\n        path: path.resolve(__dirname, `./screenshots/${window_target}.png`),\n      });\n\n      // let innerheight = await page.evaluate(() => document.querySelector('.video').innerHeight);\n      // let innerwidth = await page.evaluate(() => document.querySelector('.video').innerWidth);\n\n      let innerheight = await page.evaluate(() => window.innerHeight);\n      let innerwidth = await page.evaluate(() => window.innerWidth);\n\n      let body_innerheight = await page.evaluate(() => document.body.offsetHeight);\n      let body_innerwidth = await page.evaluate(() => document.body.offsetWidth);\n\n      console.log(`body_innerheight:`, body_innerheight)\n      console.log(`body_innerwidth:`, body_innerwidth)\n\n      console.log(`innerheight:`, innerheight);\n      console.log(`innerwidth:`, innerwidth)\n\n      let exit_fullscreen_button = await page.waitForSelector(`.exit-fullscreen`);\n      await exit_fullscreen_button.click();\n    }\n\n    browser.close();\n    console.log('Done');\n    process.exit(0);\n  } catch (error) {\n    console.log(`error.stack:`, error.stack);\n    process.exit(1);\n  }\n}\n\nrun();\n"
  },
  {
    "path": "testing/ustream/ustream.html",
    "content": "<html>\n<body>\n<h1>Foo Heading</h1>\n<p>Foo paragraph.</p>\n<iframe width=\"480\" height=\"270\" src=\"https://ustream.tv/embed/recorded/120712252\" scrolling=\"no\" allowfullscreen webkitallowfullscreen frameborder=\"0\" style=\"border: 0 none transparent;\"></iframe>\n<p>Bar paragraph.</p>\n</body>\n</html>\n"
  }
]