Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FireMonkey] Userscript Compatibility #429

Closed
erosman opened this issue Feb 26, 2022 · 273 comments
Closed

[FireMonkey] Userscript Compatibility #429

erosman opened this issue Feb 26, 2022 · 273 comments
Labels
addon: FireMonkey userscript: compatibility userScript/userCSS Compatibility

Comments

@erosman
Copy link
Owner

erosman commented Feb 26, 2022

FireMonkey 2.44 should be compatible with 98% of userscripts.
Please read Help for more information.

Please post any userscript issues here for further investigation.

@erosman erosman pinned this issue Feb 26, 2022
@linsui
Copy link

linsui commented Mar 6, 2022

https://github.com/syhyz1990/baiduyun This doesn't work with FireMonkey. I guess you don't have a baiduyun account so if there is any info needed I'm more than happy to provide it. I guess GM_cookie is the problem. Thanks!

@erosman
Copy link
Owner Author

erosman commented Mar 6, 2022

https://github.com/syhyz1990/baiduyun This doesn't work with FireMonkey. I guess you don't have a baiduyun account so if there is any info needed I'm more than happy to provide it. I guess GM_cookie is the problem. Thanks!

GM_cookie is used by 49 / 63,435 (0.07%) scripts and only supported by Tampermonkey.
The code should work even without it.
https://github.com/syhyz1990/baiduyun/blob/c8bdfc76656d2a00f4684fc462efbe35a0189b75/baiduyun.user.js#L525-L531

Has the developer commented about compatibility with FM?
Does it work with GM or VM?

@linsui
Copy link

linsui commented Mar 6, 2022

It works with VM but not with GM. In the README they say that VM or TM is required.

@erosman
Copy link
Owner Author

erosman commented Mar 6, 2022

VM doesn't support GM_cookie, so that should not be the problem. At a glance, I dont see any issues. Have you asked them?
If they point out the incompatibility, I will try to work on it.

@linsui
Copy link

linsui commented Mar 6, 2022

I'll report this issue there and come back. Thanks!

@Ghost-BD
Copy link

Ghost-BD commented Mar 9, 2022

https://greasyfork.org/en/scripts/432387-general-url-cleaner-revived doesn't work as intended. It might be a problem of how Firemonkey doesn't inject on all frames or whatever. Developer of this script is willing to make it Firemonkey compatible. ReCAPTCHA Solver, Hcaptcha Solver and other like them also victim of this.
https://greasyfork.org/en/scripts/428651-tabview-youtube does not work with Firemonkey.
https://greasyfork.org/en/scripts/433360-nova-youtube doesn't work before the last update but now looks like this load but doesn't show its icon in YouTube header. So, you can check it.
https://greasyfork.org/en/scripts/405614-youtube-polymer-engine-fixes doesn't save user settings.
https://greasyfork.org/en/scripts/429143-auto-set-youtube-volume can't set volume. Is there any option in Firemonkey to manually set value in Firemonkey like this script require? If not, I hope you will provide one. Because I have another problem with Nova YouTube that it stuck in loading options page with any user script manager in Firefox (might need to report there). So, I set its settings value manually in TM and VM but cannot in FM.
Also, you said to consider UserCSS custom settings if there is a popular demand, then consider me as one.
Firemonkey is an incredibly good user script and user styles manager. I like it and hope this to get recognition it deserves. No need to scratch your head over thus, just reported as you seem to seek to make Firemonky compatible with all user scripts which is very good. Spare me for my bad English. Thank you for your good work.

@erosman
Copy link
Owner Author

erosman commented Mar 10, 2022

https://greasyfork.org/en/scripts/432387-general-url-cleaner-revived doesn't work as intended. It might be a problem of how Firemonkey doesn't inject on all frames or whatever. Developer of this script is willing to make it Firemonkey compatible.

Replied to the topic


ReCAPTCHA Solver, Hcaptcha Solver and other like them also victim of this.

At a glance, I dont see any issues. It has to be checked by the developers.


https://greasyfork.org/en/scripts/433360-nova-youtube doesn't work before the last update but now looks like this load but doesn't show its icon in YouTube header. So, you can check it.

At a glance, I dont see any issues. Too large to debug.
Although not a problem ... dev may consider

// ==/UserScript==
/*jshint esversion: 6 */
window.nova_plugins = [];

Results in:

 'Optional chaining' is only available in ES11 (use 'esversion: 11').

Declares but never uses

// @grant           GM_addStyle
// @grant           GM_getResourceText
// @grant           GM_getResourceURL
// @grant           GM_addValueChangeListener
// @grant           GM_removeValueChangeListener
// @grant           GM_listValues
// @grant           GM_deleteValue
// @grant           GM_unregisterMenuCommand
// @grant           GM_openInTab

https://greasyfork.org/en/scripts/405614-youtube-polymer-engine-fixes doesn't save user settings.

At a glance, I dont see any issues. Does it work with Greasemonkey?

https://greasyfork.org/en/scripts/429143-auto-set-youtube-volume can't set volume. Is there any option in Firemonkey to manually set value in Firemonkey like this script require? If not, I hope you will provide one. Because I have another problem with Nova YouTube that it stuck in loading options page with any user script manager in Firefox (might need to report there). So, I set its settings value manually in TM and VM but cannot in FM.

Userscript should have the UI for the users tos et values. TM has the UI to view/update storage values, but that should not be a replaemnet for userscript's own UI.

Anyway, since the userscript is very small, users can manually set values here, but the code doesnt updates its value, so it has to be uninstalled and reinstalled.

GM_setValue('Default_Volume', 20); //Save the Default YT Volume as 20%

TBH, the code uses static value which makes GM_setValue/GM_getValue redundant. It is the same as:

// ==UserScript==
// @name         Auto Set Youtube Volume
// @namespace    YTVol
// @version      0.2
// @description  Choose the default volume for YouTube videos!
// @author       hacker09
// @match        https://www.youtube.com/embed/*
// @match        https://www.youtube.com/watch?v=*
// @icon         https://www.youtube.com/s/desktop/03f86491/img/favicon.ico
// @run-at       document-start
// ==/UserScript==

(function() {
  'use strict';
  const defaultVolume = 20;
  const data = {
    data: {
      volume: defaultVolume,
      muted: false,
      creation: Date.now()
    }
  };

  window.sessionStorage.setItem('yt-player-volume', JSON.stringify(data)); //Set the Default YT Volume
})();

User can manually change the 20 by editing the userscript.

📌 AFA feature to edit userscript storage, let me think about it 🤔


Also, you said to consider UserCSS custom settings if there is a #293, then consider me as one.

As per that topic, there is way to have custom setting by using @require to import them.
You can also use @import in CSS which should work fine.
Creating the User Interface will involve more work. let's see.

@Ghost-BD
Copy link

Sorry I might not be clear first time. For general URL cleaner revived, if you go any site where they use disqus.com for comment, FM doesn't inject that script where TM and VM do. In case of ReCAPTCHA Solver, when you trigger captcha in sites TM and VM run that script but not FM. This happens with similar scripts across different websites. So, reporting all of them to make Firemonkey compatible might not be a viable solution and all developers might not be friendly like you. Instead, if you inspect the problem in your spare time to make Firemonkey support most script might be better.
General URL Cleaner Revived with FM
.
General URL Cleaner Revived with VM
recaptcha with FM
recaptcha with VM

https://greasyfork.org/en/scripts/428651-tabview-youtube doesn't work at all.
https://greasyfork.org/en/scripts/405614-youtube-polymer-engine-fixes doesn't work properly. It doesn't act according to user setting instead always load default settings (try to change any setting). Looks like doesn't work with Greasemonkey either. At this point I can't say this script works properly with TM or VM 😊 as I don't use it anymore (use Nova YouTube now).
"Userscript should have the UI for the users to set values."- Totally agree with you. As you see not all userscript have this and had to search long for that one to find as an example 😰. Thanks for your consideration to implement userscript storage.
Yep, asking about UserCSS custom settings 'User Interface' if you can manage all of this. Thank you again.

@erosman
Copy link
Owner Author

erosman commented Mar 10, 2022

For general URL cleaner revived, if you go any site where they use disqus.com for comment, FM doesn't inject that script where TM and VM do. In case of ReCAPTCHA Solver, when you trigger captcha in sites TM and VM run that script but not FM.

I see ... in that case, it seems to be the issue of @allFrames. FireMonkey by default adheres to Firefox API defaults which is to inject into top frame only, while GM|VM|TM by default inject into all frames. (for more info check Help under defaults).

In majority of cases, the top frame is enough and avoids extra overheads of injecting into sub-frames unnecessarily. However, captcha, disqus, etc are usually included as iframes. Therefore, userscript dealing with them should be set to inject into all frames.

Set @allFrames true in User Metadata and try.

@Ghost-BD
Copy link

Already tried that with URL cleaner from another issue you solved but doesn't work here. Solved ReCAPTCHA Solver though.

@erosman
Copy link
Owner Author

erosman commented Mar 10, 2022

Already tried that with URL cleaner from another issue you solved but doesn't work here. Solved ReCAPTCHA Solver though.

URL cleaner issue doesn't relate to @allFrames as per my reply to the mentioned topic.

@Ghost-BD
Copy link

Ok then, hope he will solve the issue. Looking forward to seeing future feature implementation and better scripts compatibility in firemonkey. Thank you.

@erosman
Copy link
Owner Author

erosman commented Mar 11, 2022

📌 AFA feature to edit userscript storage, let me think about it 🤔

v2.45
Added storage view/edit feature

@lightinwater
Copy link

https://greasyfork.org/en/scripts/7543-google-search-extra-buttons

Doesn't load unless explicitly asked to via the run command, probably something to do with run-on but beyond my ken I'm sorry

@erosman
Copy link
Owner Author

erosman commented Mar 15, 2022

https://greasyfork.org/en/scripts/7543-google-search-extra-buttons
Doesn't load unless explicitly asked to via the run command,

The patterns have issues e.g.
https://www.google.*/* also matches https://www.google.*/search* so no need for https://www.google.*/search*
https://encrypted.google.*/* also matches https://encrypted.google.*/search* so no need for https://encrypted.google.*/search*

I found the reason for not injecting which is due to the processing order when patterns like https://www.google.*/* are used. I will fix it in v2.45.

For now, if you only use google.com, add this to the User Metadata

@disable-include
@match      *://www.google.com/*
@match      https://encrypted.google.com/*
@match      https://spmbt.github.io/googleSearchExtraButtons/saveYourLocalStorage.html
@match      https://www.gstatic.com/sites/p/b9356d/system/services/test.html
@match      https://www.gstatic.com/index.html

@zurlafaga
Copy link

zurlafaga commented Mar 15, 2022

https://greasyfork.org/en/scripts/436115-return-youtube-dislike

The userscript is not working in FireMonkey v2.44.

@erosman
Copy link
Owner Author

erosman commented Mar 15, 2022

https://greasyfork.org/en/scripts/436115-return-youtube-dislike

The userscript in not working in FireMonkey v2.44.

Unfortunately it is due to the CORS restriction Bug 1715249 in userScript context when it is using fetch on line 219.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://returnyoutubedislikeapi.com/votes?videoId=DkYQksqpFyg. (Reason: CORS request did not succeed). Status code: (null).

GM.fetch/GM.xmlHttpRequest would work if developers decides to update it. I will see if I can work out anything until the Firefox bug is fixed.

@zurlafaga
Copy link

Unfortunately it is due to the CORS restriction Bug 1715249 in userScript context when it is using fetch on line 219.

Just for reference, ViolentMonkey and TamperMonkey are working with the script at https://greasyfork.org/en/scripts/436115-return-youtube-dislike

@erosman
Copy link
Owner Author

erosman commented Mar 15, 2022

Just for reference, ViolentMonkey and TamperMonkey are working with the script at https://greasyfork.org/en/scripts/436115-return-youtube-dislike

GM|TM|VM inject into content context while FireMonkey injects into secure userScript context. The bug relates to userScript context.

Note: Script declares @grant GM.xmlHttpRequest but never uses it.

Here is the same script, with added compatibility with FireMonkey as well (line 219-234 no other changes):

// ==UserScript==
// @name         Return YouTube Dislike
// @namespace    https://www.returnyoutubedislike.com/
// @homepage     https://www.returnyoutubedislike.com/
// @version      0.9.0
// @encoding     utf-8
// @description  Return of the YouTube Dislike, Based off https://www.returnyoutubedislike.com/
// @icon         https://github.com/Anarios/return-youtube-dislike/raw/main/Icons/Return%20Youtube%20Dislike%20-%20Transparent.png
// @author       Anarios & JRWR
// @match        *://*.youtube.com/*
// @exclude      *://music.youtube.com/*
// @exclude      *://*.music.youtube.com/*
// @compatible   chrome
// @compatible   firefox
// @compatible   opera
// @compatible   safari
// @compatible   edge
// @grant        GM.xmlHttpRequest
// @connect      youtube.com
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==
const LIKED_STATE = "LIKED_STATE";
const DISLIKED_STATE = "DISLIKED_STATE";
const NEUTRAL_STATE = "NEUTRAL_STATE";
let previousState = 3; //1=LIKED, 2=DISLIKED, 3=NEUTRAL
let likesvalue = 0;
let dislikesvalue = 0;

let isMobile = location.hostname == "m.youtube.com";
let mobileDislikes = 0;
function cLog(text, subtext = "") {
  subtext = subtext.trim() === "" ? "" : `(${subtext})`;
  console.log(`[Return YouTube Dislikes] ${text} ${subtext}`);
}

function getButtons() {
  if (isMobile) {
    return document.querySelector(".slim-video-action-bar-actions");
  }
  if (document.getElementById("menu-container")?.offsetParent === null) {
    return document.querySelector("ytd-menu-renderer.ytd-watch-metadata > div");
  } else {
    return document
      .getElementById("menu-container")
      ?.querySelector("#top-level-buttons-computed");
  }
}

function getLikeButton() {
  return getButtons().children[0];
}

function getDislikeButton() {
  return getButtons().children[1];
}

function isVideoLiked() {
  if (isMobile) {
    return (
      getLikeButton().querySelector("button").getAttribute("aria-label") ==
      "true"
    );
  }
  return getLikeButton().classList.contains("style-default-active");
}

function isVideoDisliked() {
  if (isMobile) {
    return (
      getDislikeButton().querySelector("button").getAttribute("aria-label") ==
      "true"
    );
  }
  return getDislikeButton().classList.contains("style-default-active");
}

function isVideoNotLiked() {
  if (isMobile) {
    return !isVideoLiked();
  }
  return getLikeButton().classList.contains("style-text");
}

function isVideoNotDisliked() {
  if (isMobile) {
    return !isVideoDisliked();
  }
  return getDislikeButton().classList.contains("style-text");
}

function checkForUserAvatarButton() {
  if (isMobile) {
    return;
  }
  if (document.querySelector('#avatar-btn')) {
    return true
  } else {
    return false
  }
}

function getState() {
  if (isVideoLiked()) {
    return LIKED_STATE;
  }
  if (isVideoDisliked()) {
    return DISLIKED_STATE;
  }
  return NEUTRAL_STATE;
}

function setLikes(likesCount) {
  if (isMobile) {
    getButtons().children[0].querySelector(".button-renderer-text").innerText =
      likesCount;
    return;
  }
  getButtons().children[0].querySelector("#text").innerText = likesCount;
}

function setDislikes(dislikesCount) {
  if (isMobile) {
    mobileDislikes = dislikesCount;
    return;
  }
  getButtons().children[1].querySelector("#text").innerText = dislikesCount;
}

(typeof GM_addStyle != "undefined"
  ? GM_addStyle
  : (styles) => {
      let styleNode = document.createElement("style");
      styleNode.type = "text/css";
      styleNode.innerText = styles;
      document.head.appendChild(styleNode);
    })(`
    #return-youtube-dislike-bar-container {
      background: var(--yt-spec-icon-disabled);
      border-radius: 2px;
    }

    #return-youtube-dislike-bar {
      background: var(--yt-spec-text-primary);
      border-radius: 2px;
      transition: all 0.15s ease-in-out;
    }

    .ryd-tooltip {
      position: relative;
      display: block;
      height: 2px;
      top: 9px;
    }

    .ryd-tooltip-bar-container {
      width: 100%;
      height: 2px;
      position: absolute;
      padding-top: 6px;
      padding-bottom: 28px;
      top: -6px;
    }
  `);

function createRateBar(likes, dislikes) {
  if (isMobile) {
    return;
  }
  let rateBar = document.getElementById("return-youtube-dislike-bar-container");

  const widthPx =
    getButtons().children[0].clientWidth +
    getButtons().children[1].clientWidth +
    8;

  const widthPercent =
    likes + dislikes > 0 ? (likes / (likes + dislikes)) * 100 : 50;

  if (!rateBar && !isMobile) {
    document.getElementById("menu-container").insertAdjacentHTML(
      "beforeend",
      `
        <div class="ryd-tooltip" style="width: ${widthPx}px">
        <div class="ryd-tooltip-bar-container">
           <div
              id="return-youtube-dislike-bar-container"
              style="width: 100%; height: 2px;"
              >
              <div
                 id="return-youtube-dislike-bar"
                 style="width: ${widthPercent}%; height: 100%"
                 ></div>
           </div>
        </div>
        <tp-yt-paper-tooltip position="top" id="ryd-dislike-tooltip" class="style-scope ytd-sentiment-bar-renderer" role="tooltip" tabindex="-1">
           <!--css-build:shady-->${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}
        </tp-yt-paper-tooltip>
        </div>
`
    );
  } else {
    document.getElementById(
      "return-youtube-dislike-bar-container"
    ).style.width = widthPx + "px";
    document.getElementById("return-youtube-dislike-bar").style.width =
      widthPercent + "%";

    document.querySelector(
      "#ryd-dislike-tooltip > #tooltip"
    ).innerHTML = `${likes.toLocaleString()}&nbsp;/&nbsp;${dislikes.toLocaleString()}`;
  }
}

function setState() {
  cLog("Fetching votes...");
  let statsSet = false;

  if (GM_info.scriptHandler === 'FireMonkey') {
    GM.fetch(`https://returnyoutubedislikeapi.com/votes?videoId=${getVideoId()}`, {responseType: 'json'})
    .then((response) => response.json)
    .then((json) => {
      if (json && !("traceId" in json) && !statsSet) {
        const { dislikes, likes } = json;
        cLog(`Received count: ${dislikes}`);
        likesvalue = likes;
        dislikesvalue = dislikes;
        setDislikes(numberFormat(dislikes));
        createRateBar(likes, dislikes);
      }
    });
    setState = function(){};
    return;
  }

  fetch(
    `https://returnyoutubedislikeapi.com/votes?videoId=${getVideoId()}`
  ).then((response) => {
    response.json().then((json) => {
      if (json && !("traceId" in response) && !statsSet) {
        const { dislikes, likes } = json;
        cLog(`Received count: ${dislikes}`);
        likesvalue = likes;
        dislikesvalue = dislikes;
        setDislikes(numberFormat(dislikes));
        createRateBar(likes, dislikes);
      }
    });
  });
  setState = function(){};
}

function likeClicked() {
  if (checkForUserAvatarButton() == true) {
    if (previousState == 1) {
      likesvalue--;
      createRateBar(likesvalue, dislikesvalue);
      setDislikes(numberFormat(dislikesvalue));
      previousState = 3
    } else if (previousState == 2) {
      likesvalue++;
      dislikesvalue--;
      setDislikes(numberFormat(dislikesvalue))
      createRateBar(likesvalue, dislikesvalue);
      previousState = 1
    } else if (previousState == 3) {
      likesvalue++;
      createRateBar(likesvalue, dislikesvalue)
      previousState = 1
    }
  }
}

function dislikeClicked() {
  if (checkForUserAvatarButton() == true) {
    if (previousState == 3) {
      dislikesvalue++;
      setDislikes(numberFormat(dislikesvalue));
      createRateBar(likesvalue, dislikesvalue);
      previousState = 2
    } else if (previousState == 2) {
      dislikesvalue--;
      setDislikes(numberFormat(dislikesvalue));
      createRateBar(likesvalue, dislikesvalue);
      previousState = 3
    } else if (previousState == 1) {
      likesvalue--;
      dislikesvalue++;
      setDislikes(numberFormat(dislikesvalue));
      createRateBar(likesvalue, dislikesvalue);
      previousState = 2
    }
  }
}

function setInitialState() {
  setState();
}

function getVideoId() {
  const urlObject = new URL(window.location.href);
  const pathname = urlObject.pathname;
  if (pathname.startsWith("/clip")) {
    return document.querySelector("meta[itemprop='videoId']").content;
  } else {
    return urlObject.searchParams.get("v");
  }
}

function isVideoLoaded() {
  if (isMobile) {
    return document.getElementById("player").getAttribute("loading") == "false";
  }
  const videoId = getVideoId();

  return (
    document.querySelector(`ytd-watch-flexy[video-id='${videoId}']`) !== null
  );
}

function roundDown(num) {
  if (num < 1000) return num;
  const int = Math.floor(Math.log10(num) - 2);
  const decimal = int + (int % 3 ? 1 : 0);
  const value = Math.floor(num / 10 ** decimal);
  return value * 10 ** decimal;
}

function numberFormat(numberState) {
  let userLocales;
  try {
    userLocales = new URL(
      Array.from(document.querySelectorAll("head > link[rel='search']"))
        ?.find((n) => n?.getAttribute("href")?.includes("?locale="))
        ?.getAttribute("href")
    )?.searchParams?.get("locale");
  } catch {}
  const formatter = Intl.NumberFormat(
    document.documentElement.lang || userLocales || navigator.language,
    {
      notation: "compact",
    }
  );

  return formatter.format(roundDown(numberState));
}

function setEventListeners(evt) {
  let jsInitChecktimer;

  function checkForJS_Finish(check) {
    console.log();
    if (getButtons()?.offsetParent && isVideoLoaded()) {
      clearInterval(jsInitChecktimer);
      const buttons = getButtons();

      if (!window.returnDislikeButtonlistenersSet) {
        cLog("Registering button listeners...");
        buttons.children[0].addEventListener("click", likeClicked);
        buttons.children[1].addEventListener("click", dislikeClicked);
        window.returnDislikeButtonlistenersSet = true;
      }
      setInitialState();
    }
  }

  cLog("Setting up...");
  jsInitChecktimer = setInterval(checkForJS_Finish, 111);
}

(function () {
  "use strict";
  window.addEventListener("yt-navigate-finish", setEventListeners, true);
  setEventListeners();
})();
if (isMobile) {
  let originalPush = history.pushState;
  history.pushState = function (...args) {
    window.returnDislikeButtonlistenersSet = false;
    setEventListeners(args[2]);
    return originalPush.apply(history, args);
  };
  setInterval(() => {
    getDislikeButton().querySelector(".button-renderer-text").innerText =
      mobileDislikes;
  }, 1000);
}

@ivysrono
Copy link

I write some scripts that use GM_openInTab and window.focus and window.close:
GM_openIntab return the opened tab (A), addEventLinstener to focus A and close it auto.
I'm using Violentmonkey, I know FireMonkey don't plan to suppot window.focus and window.close, however, what can I do?

@erosman
Copy link
Owner Author

erosman commented Mar 19, 2022

I write some scripts that use GM_openInTab and window.focus and window.close: GM_openIntab return the opened tab (A), addEventLinstener to focus A and close it auto. I'm using Violentmonkey, I know FireMonkey don't plan to suppot window.focus and window.close, however, what can I do?

GM_openIntab -> focus
What focus does it to make the tab active. GM.openInTab(url) creates a tab and makes it active.
Unless the script wants to make tab active later, or on some condition, that should do the job.

window.close
I have updated GM.openIntab/GM_openIntab which should allow JavaScript window.close() to work when the API is used.
It will be in v2.45
It is not fully tested but I tried it with the following test userscript and it works.

// ==UserScript==
// @name          window.close test
// @match         *://*.example.com/*
// @match         *://github.com/*
// ==/UserScript==

if (location.hostname.endsWith('github.com')) {
  window.setTimeout(() => GM.openInTab('https://example.com/'), 2000);
}
else {
  window.setTimeout(() => window.close(), 2000);
}

@ivysrono
Copy link

Unless the script wants to make tab active later

Yes, need later.

@zurlafaga
Copy link

https://greasyfork.org/en/scripts/428651-tabview-youtube

The userscript is not working in FireMonkey v2.45.

@erosman
Copy link
Owner Author

erosman commented Mar 21, 2022

https://greasyfork.org/en/scripts/428651-tabview-youtube

The userscript is not working in FireMonkey v2.45.

At first glance, I don't see any issues. The script to too large and has large minified @require to debug properly.

If the developer points out the issue, I will see what can be done.

PS. I posted to Return YouTube Dislike with the code but there is no response so far.

@zurlafaga
Copy link

@erosman,

The script at https://greasyfork.org/en/scripts/436115-return-youtube-dislike with your changes is working in FireMonkey v2.45. Thank you.

Just for reference, ViolentMonkey and TamperMonkey are working with the script at https://greasyfork.org/en/scripts/428651-tabview-youtube

@Ghost-BD
Copy link

Ghost-BD commented Apr 7, 2022

https://greasyfork.org/en/scripts/419825-opera-browser-rocker-mouse-gestures-search-highlight script does not work as expected. Units and currency converters of this script not working with FM. You can use this page to check currency conversion.
Thanks.

@erosman
Copy link
Owner Author

erosman commented Apr 7, 2022

https://greasyfork.org/en/scripts/419825-opera-browser-rocker-mouse-gestures-search-highlight script does not work as expected. Units and currency converters of this script not working with FM. You can use this page to check currency conversion.
Thanks.

It could the be due to Bug 1715249 as the script uses fetch.

I am waiting for the bug to be fixed.

@Ghost-BD
Copy link

Ghost-BD commented Apr 7, 2022

Maybe. But no CORS errors are displayed in the console and enabling CORS with extension doesn't help. Also i do not understand a thing about those (or any of those hard code you write😯). So I am leaving all the hard work upon you🤣. Thanks again.

@CyberFoxar
Copy link

Yup ! But there seems to be more profound issues with the support. Stuff like the jquery $ not being injected.

I'm unsure how to debug this, tbh. But I thought it might be helpful to give you more example of bigger scripts that are incompatible.

I believe the GM_getResourceText part to be inside the downloader. I think it would not prevent the script from running outright, just the download functionality. At the moment, trying to run the script on a compatible site (like e926) will result in an error.

@erosman
Copy link
Owner Author

erosman commented Apr 4, 2023

Stuff like the jquery $ not being injected.

It could be the window as the script sets window.re621.

window in userScripts context (FM) is NOT the same window as contentScripts context (GM|TM|VM).

In my FM v2.68, all scripts are injected.

image

The console log also shows that the script is running but I don't know what it is supposed to.

image

@CyberFoxar
Copy link

Weird. On my Firefox (FireMonkey 2.97 and Firefox 111.0.1). It might just be my setup that's wrong.

Also, here's the intended show: It add a small wrench on the top right, and clicking the wrench open the shown dialog with options in it.
image

@erosman
Copy link
Owner Author

erosman commented Apr 4, 2023

The icons do appear and the popups work.

image

I tested q and it activates the search box. So keyboard shortcuts work.
I don't get any errors in the console but I am not logged in.

It might be a small issue but it takes time to pinpoint.

@CyberFoxar
Copy link

CyberFoxar commented Apr 4, 2023

Odd. I doesn't load at all on my end, either with all extensions disabled or in private browsing (with only FM active). The script just doesn't load and when I try to run it manually (using FM's popup menu) I get a

Failed to insert

$ is not defined

If you want to contact me privately / on IM, I'm on telegram @CyberFoxar ! I can also do IRC, but you'll need to give me a place x)

@erosman
Copy link
Owner Author

erosman commented Apr 4, 2023

My previous test was with Firefox Nightly 113 & FireMonkey 2.68.
I tested now with Firefox 111 and FireMonkey 2.67 and the userscript doesn't run (but no errors).

It might be something to do with the changes in the v2.68. 🤔
I suggest waiting for FM v2.68 and then try again.

@sondreamer
Copy link

sondreamer commented Apr 6, 2023

Error with this script (v 0.4.0):
https://github.com/immersive-translate/immersive-translate

Failed to sync latest adaptive rules
Failure reason:response.json is not a function
Current Rule Version:2023-04-05T23:18:19.469Z

Tampermonkey 4.18.1 works. Firemonkey version is 2.67. Firefox 102.3.0esr

@erosman
Copy link
Owner Author

erosman commented Apr 6, 2023

The script checks for GM_fetch which is supported by FireMonkey but not other managers AFAIK.
However, FireMonkey's GM fetch might not be the same as the userscript expects.

line 11,432

  function request2(options2) {
    return isWebOptionsPage() ? ask({
      method: "request",
      data: options2
    }) : isMonkey() || isDeno() ? (options2.fetchPolyfill = globalThis.GM_fetch, request(options2)) : sendMessage({
      method: "fetch",
      data: options2
    });
  }

Adding this to top of the script, eliminates the error.

Line 67

(() => { globalThis.GM_fetch = null;

I haven't checked it thoroughly though.

@sondreamer
Copy link

sondreamer commented Apr 8, 2023

It works, thanks. If you're interested, this is what script author said about the issue:

it seems that Firemonkey changed the native globalThis.Response, they should not change this.

It seems that teddit.net doesn't work tho, Failed to sync latest adaptive rules
Failure reason:NetworkError when attempting to fetch resource.

@sondreamer
Copy link

https://greasyfork.org/en/scripts/437291-open-source-alternative-redirector
v11.2.0, nitter.net won't work. Authors response about nitter issue on Tampermonkey:

that's Tampermonkey's fault, if CSP (Content Security Policy) enabled in website, then Tampermonkey doesn't work

But Tampermonkey works for me, weirdly.

@erosman
Copy link
Owner Author

erosman commented Apr 8, 2023

It works, thanks. If you're interested, this is what script author said about the issue:

it seems that Firemonkey changed the native globalThis.Response, they should not change this.

It seems that teddit.net doesn't work tho, Failed to sync latest adaptive rules Failure reason:NetworkError when attempting to fetch resource.

I am guessing that he is not aware that FireMonkey injects into the dedicated & isolated userScripts context, which is not the same as contentScripts context used in GM|TM|VM.

AFA teddit error, TBH, the script is too large for me to test & debug. The script developer can look into it.

https://greasyfork.org/en/scripts/437291-open-source-alternative-redirector v11.2.0, nitter.net won't work. Authors response about nitter issue on Tampermonkey:

that's Tampermonkey's fault, if CSP (Content Security Policy) enabled in website, then Tampermonkey doesn't work

But Tampermonkey works for me, weirdly.

// @grant none

  • VM by defaults injects into page context
  • TM injects into page context when there is // @grant none
  • Read Quick Fixes for FireMonkey fix

Content Security Policy (CSP)

  • TM removes CSP and therefore able to inject userscripts into pages with strict CSP
  • AFAIK, none of the other managers do that

See also

Update

I had a quick look over Open-Source Alternative Redirector and its function is quite simple (and can be done with fewer lines of code).

I tested it without any modification on reddit & google and the redirect was working.

@erosman
Copy link
Owner Author

erosman commented Apr 12, 2023

I've got a problem with the latest version (0.12.0.) of PotcFdk/SteamDiscoveryQueueAutoSkipper: Auto-advances in Steam Discovery Queues. not running.

The script itself is here. The previous version used to work with FireMonkey.

@eberhardweber I came across PotcFdk/SteamDiscoveryQueueAutoSkipper#11 and had another look at the script.

The main problem is return in lines 47 & 147

SyntaxError: return not in function

As per FireMonkey Help: Quick Fixes, "Use Wrap code in IIFE". That would eliminate the error.

If the script still doesn't run as expected, "Try setting @inject-into page in User Metadata".

@GunGunGun
Copy link

GunGunGun commented Apr 17, 2023

This script doesn't work but even wierder using it together with Firemonkey breaks network connection of many websites, causing them to break/not fully loaded, do you have any idea ? Doesn't happen with Violentmonkey, Tampermonkey... I used a new profile to test, so it can't be my profile issues.

https://greasyfork.org/en/scripts/449581-m3u8%E8%A7%86%E9%A2%91%E4%BE%A6%E6%B5%8B%E4%B8%8B%E8%BD%BD%E5%99%A8-%E8%87%AA%E5%8A%A8%E5%97%85%E6%8E%A2

@erosman
Copy link
Owner Author

erosman commented Apr 17, 2023

@GunGunGun re: M3U8 Video Detector and Downloader

At a glance, these are some of the issues which you can share with the developer if you want.

1

Since FireMonkey 2.42, if both GM.* & GM_* @grant are requested, only GM.* are kept.
The script checks for this in lines 44-58 which is fine.

However, script starts using GM_* in lines 392-397 directly.
JavaScript parsing breaks with an error:

ReferenceError: GM_getValue is not defined

2

There might be an issue with unsafeWindow.fetch on line 124
Due to a bug in userScripts context (Bug 1715249), FireMonkey maps fetch to window.fetch.
See also: #340

I don't know how it will end up

// in FireMoneky
fetch -> window.fetch
unsafeWindow -> window.wrappedJSObject;

// result in M3U8 Video Detector and Downloader
unsafeWindow.fetch -> window.wrappedJSObject.fetch

3

Although not a problem, line 61 has a return so the codes on lines 63-68 are useless

@GunGunGun
Copy link

GunGunGun commented Apr 18, 2023

@erosman Thanks, I tried to fix the script and it looks like I've found the issue, it's because of this code to override xmlHttprequest.prototype.open:

        const _open = unsafeWindow.XMLHttpRequest.prototype.open;
        unsafeWindow.XMLHttpRequest.prototype.open = function (...args) {
            this.addEventListener("load", () => {
                try {
                    let content = this.responseText;
                    if (checkContent(content)) doM3U({ url: args[1], content });
                } catch { }
            });
            // checkUrl(args[1]);
            return _open.apply(this, args);
        }

Removing this part fixed the network issue, but it works on all other userscript Managers like VM, TM...

But I don't really want to remove above part because it affects ability to grab m3u8 from xmlHttprequest, result in script grabbing less video urls.

  1. The fetch part isn't important, I can remove it without issue because it's for leeching video from 3rd party server, and it doesn't affect the script.
  2. This part is probably not important and not needed.

There must be something differences between FM and VM/TM ?

Here's my modified code, I replaced all GM_set/get with GM.:

// ==UserScript==
// @name         m3u8视频侦测下载器【自动嗅探】
// @name:zh-CN   m3u8视频侦测下载器【自动嗅探】
// @name:zh-TW   m3u8視頻偵測下載器【自動嗅探】
// @name:en      M3U8 Video Detector and Downloader
// @version      1.4.1
// @description  自动检测页面m3u8视频并进行完整下载。检测到m3u8链接后会自动出现在页面右上角位置,点击下载即可跳转到m3u8下载器。
// @description:zh-CN  自动检测页面m3u8视频并进行完整下载。检测到m3u8链接后会自动出现在页面右上角位置,点击下载即可跳转到m3u8下载器。
// @description:zh-TW  自動檢測頁面m3u8視頻並進行完整下載。檢測到m3u8鏈接後會自動出現在頁面右上角位置,點擊下載即可跳轉到m3u8下載器。
// @description:en  Automatically detect the m3u8 video of the page and download it completely. Once detected the m3u8 link, it will appear in the upper right corner of the page. Click download to jump to the m3u8 downloader.
// @icon         https://tools.thatwind.com/favicon.png
// @author       allFull
// @namespace    https://tools.thatwind.com/
// @homepage     https://tools.thatwind.com/tool/m3u8downloader
// @match        *://*/*
// @exclude      *://www.diancigaoshou.com/*
// @require      https://cdn.jsdelivr.net/npm/m3u8-parser@4.7.1/dist/m3u8-parser.min.js
// @connect      *
// @grant        unsafeWindow
// @grant        GM_openInTab
// @grant        GM.openInTab
// @grant        GM_getValue
// @grant        GM.getValue
// @grant        GM_setValue
// @grant        GM.setValue
// @grant        GM_deleteValue
// @grant        GM.deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_download
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const mgmapi = {

        addStyle(s) {
            let style = document.createElement("style");
            style.innerHTML = s;
            document.documentElement.appendChild(style);
        },
        async getValue(name, defaultVal) {
            return await ((typeof GM_getValue === "function") ? GM_getValue : GM.getValue)(name, defaultVal);
        },
        async setValue(name, value) {
            return await ((typeof GM_setValue === "function") ? GM_setValue : GM.setValue)(name, value);
        },
        async deleteValue(name) {
            return await ((typeof GM_deleteValue === "function") ? GM_deleteValue : GM.deleteValue)(name);
        },
        openInTab(url, open_in_background = false) {
            return ((typeof GM_openInTab === "function") ? GM_openInTab : GM.openInTab)(url, open_in_background);
        },
        xmlHttpRequest(details) {
            return ((typeof GM_xmlhttpRequest === "function") ? GM_xmlhttpRequest : GM.xmlHttpRequest)(details);
        },
        download(details) {

            return this.openInTab(details.url);

            if (typeof GM_download === "function") {
                this.message("下载中,请留意浏览器下载弹窗\nDownloading, pay attention to the browser's download pop-up.", 3000);
                return GM_download(details);
            } else {
                this.openInTab(details.url);
            }
        },
        copyText(text) {
            copyTextToClipboard(text);
            function copyTextToClipboard(text) {
                // 复制文本
                var copyFrom = document.createElement("textarea");
                copyFrom.textContent = text;
                document.body.appendChild(copyFrom);
                copyFrom.select();
                document.execCommand('copy');
                copyFrom.blur();
                document.body.removeChild(copyFrom);
            }
        },
        message(text, disappearTime = 5000) {
            const id = "f8243rd238-gm-message-panel";
            let p = document.querySelector(`#${id}`);
            if (!p) {
                p = document.createElement("div");
                p.id = id;
                p.style = `
                    position: fixed;
                    bottom: 20px;
                    right: 20px;
                    display: flex;
                    flex-direction: column;
                    align-items: end;
                    z-index: 999999999999999;
                `;
                (document.body || document.documentElement).appendChild(p);
            }
            let mdiv = document.createElement("div");
            mdiv.innerText = text;
            mdiv.style = `
                padding: 3px 8px;
                border-radius: 5px;
                background: black;
                box-shadow: #000 1px 2px 5px;
                margin-top: 10px;
                font-size: small;
                color: #fff;
                text-align: right;
            `;
            p.appendChild(mdiv);
            setTimeout(() => {
                p.removeChild(mdiv);
            }, disappearTime);
        }
    };


    if (location.host === "tools.thatwind.com" || location.host === "localhost:3000") {
        mgmapi.addStyle("#userscript-tip{display:none !important;}");

        // 对请求做代理
        const _fetch = unsafeWindow.fetch;
        unsafeWindow.fetch = async function (...args) {
            try {
                let response = await _fetch(...args);
                if (response.status !== 200) throw new Error(response.status);
                return response;
            } catch (e) {
                // 失败请求使用代理
                if (args.length == 1) {
                    console.log(`请求代理:${args[0]}`);
                    return await new Promise((resolve, reject) => {
                        let referer = new URLSearchParams(location.hash.slice(1)).get("referer");
                        let headers = {};
                        if (referer) {
                            referer = new URL(referer);
                            headers = {
                                "origin": referer.origin,
                                "referer": referer.href
                            };
                        }
                        mgmapi.xmlHttpRequest({
                            method: "GET",
                            url: args[0],
                            responseType: 'arraybuffer',
                            headers,
                            onload(r) {
                                resolve({
                                    status: r.status,
                                    headers: new Headers(r.responseHeaders.split("\n").filter(n => n).map(s => s.split(/:\s*/)).reduce((all, [a, b]) => { all[a] = b; return all; }, {})),
                                    async text() {
                                        return r.responseText;
                                    },
                                    async arrayBuffer() {
                                        return r.response;
                                    }
                                });
                            },
                            onerror() {
                                reject(new Error());
                            }
                        });
                    });
                } else {
                    throw e;
                }
            }
        }

        return;
    }


    // iframe 信息交流
    // 目前只用于获取顶部标题
    window.addEventListener("message", async (e) => {
        if (e.data === "3j4t9uj349-gm-get-title") {
            let name = `top-title-${Date.now()}`;
            await mgmapi.setValue(name, document.title);
            e.source.postMessage(`3j4t9uj349-gm-top-title-name:${name}`, "*");
        }
    });

    function getTopTitle() {
        return new Promise(resolve => {
            window.addEventListener("message", async function l(e) {
                if (typeof e.data === "string") {
                    if (e.data.startsWith("3j4t9uj349-gm-top-title-name:")) {
                        let name = e.data.slice("3j4t9uj349-gm-top-title-name:".length);
                        await new Promise(r => setTimeout(r, 5)); // 等5毫秒 确定 setValue 已经写入
                        resolve(await mgmapi.getValue(name));
                        mgmapi.deleteValue(name);
                        window.removeEventListener("message", l);
                    }
                }
            });
            window.top.postMessage("3j4t9uj349-gm-get-title", "*");
        });
    }


    {
        // 请求检测
        // const _fetch = unsafeWindow.fetch;
        // unsafeWindow.fetch = function (...args) {
        //     if (checkUrl(args[0])) doM3U({ url: args[0] });
        //     return _fetch(...args);
        // }

        const _r_text = unsafeWindow.Response.prototype.text;
        unsafeWindow.Response.prototype.text = function () {
            return new Promise((resolve, reject) => {
                _r_text.call(this).then((text) => {
                    resolve(text);
                    if (checkContent(text)) doM3U({ url: this.url, content: text });
                }).catch(reject);
            });
        }

        /*const _open = unsafeWindow.XMLHttpRequest.prototype.open;
        unsafeWindow.XMLHttpRequest.prototype.open = function (...args) {
            this.addEventListener("load", () => {
                try {
                    let content = this.responseText;
                    if (checkContent(content)) doM3U({ url: args[1], content });
                } catch { }
            });
            // checkUrl(args[1]);
            return _open.apply(this, args);
        }*/


        function checkUrl(url) {
            url = new URL(url, location.href);
            if (url.pathname.endsWith(".m3u8") || url.pathname.endsWith(".m3u")) {
                // 发现
                return true;
            }
        }

        function checkContent(content) {
            if (content.trim().startsWith("#EXTM3U")) {
                return true;
            }
        }


        // 检查纯视频
        setInterval(doVideos, 1000);

    }

    const rootDiv = document.createElement("div");
    rootDiv.style = `
        position: fixed;
        z-index: 9999999999999999;
        opacity: 0.9;
    `;
    rootDiv.style.display = "none";
    document.documentElement.appendChild(rootDiv);

    const shadowDOM = rootDiv.attachShadow({ mode: 'open' });
    const wrapper = document.createElement("div");
    shadowDOM.appendChild(wrapper);


    // 指示器
    const bar = document.createElement("div");
    bar.style = `
        text-align: right;
    `;
    bar.innerHTML = `
        <span
            class="number-indicator"
            data-number="0"
            style="
                display: inline-flex;
                width: 25px;
                height: 25px;
                background: black;
                padding: 10px;
                border-radius: 100px;
                margin-bottom: 5px;
                cursor: pointer;
                border: 3px solid #83838382;
            "
        >
            <svg
            style="
                filter: invert(1);
            "
            version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 585.913 585.913" style="enable-background:new 0 0 585.913 585.913;" xml:space="preserve">
                <g>
                    <path d="M11.173,46.2v492.311l346.22,47.402V535.33c0.776,0.058,1.542,0.109,2.329,0.109h177.39
                    c20.75,0,37.627-16.883,37.627-37.627V86.597c0-20.743-16.877-37.628-37.627-37.628h-177.39c-0.781,0-1.553,0.077-2.329,0.124V0
                    L11.173,46.2z M110.382,345.888l-1.37-38.273c-0.416-11.998-0.822-26.514-0.822-41.023l-0.415,0.01
                    c-2.867,12.767-6.678,26.956-10.187,38.567l-10.961,38.211l-15.567-0.582l-9.239-37.598c-2.801-11.269-5.709-24.905-7.725-37.361
                    l-0.25,0.005c-0.503,12.914-0.879,27.657-1.503,39.552L50.84,343.6l-17.385-0.672l5.252-94.208l25.415-0.996l8.499,32.064
                    c2.724,11.224,5.467,23.364,7.428,34.819h0.389c2.503-11.291,5.535-24.221,8.454-35.168l9.643-33.042l27.436-1.071l5.237,101.377
                    L110.382,345.888z M172.479,349.999c-12.569-0.504-23.013-4.272-28.539-8.142l4.504-17.249c3.939,2.226,13.1,6.445,22.373,6.687
                    c12.009,0.32,18.174-5.497,18.174-13.218c0-10.068-9.838-14.683-19.979-14.74l-9.253-0.052v-16.777l8.801-0.066
                    c7.708-0.208,17.646-3.262,17.646-11.905c0-6.121-4.914-10.562-14.635-10.331c-7.95,0.189-16.245,3.914-20.213,6.446l-4.52-16.693
                    c5.693-4.008,17.224-8.11,29.883-8.588c21.457-0.795,33.643,10.407,33.643,24.625c0,11.029-6.197,19.691-18.738,24.161v0.314
                    c12.229,2.216,22.266,11.663,22.266,25.281C213.89,338.188,197.866,351.001,172.479,349.999z M331.104,302.986
                    c0,36.126-19.55,52.541-51.193,51.286c-29.318-1.166-46.019-17.103-46.019-52.044v-61.104l25.711-1.006v64.201
                    c0,19.191,7.562,29.146,21.179,29.502c14.234,0.368,22.189-8.976,22.189-29.26v-66.125l28.122-1.097v65.647H331.104z
                    M359.723,70.476h177.39c8.893,0,16.125,7.236,16.125,16.126v411.22c0,8.888-7.232,16.127-16.125,16.127h-177.39
                    c-0.792,0-1.563-0.116-2.329-0.232V380.782c17.685,14.961,40.504,24.032,65.434,24.032c56.037,0,101.607-45.576,101.607-101.599
                    c0-56.029-45.581-101.603-101.607-101.603c-24.93,0-47.749,9.069-65.434,24.035V70.728
                    C358.159,70.599,358.926,70.476,359.723,70.476z M390.873,364.519V245.241c0-1.07,0.615-2.071,1.586-2.521
                    c0.981-0.483,2.13-0.365,2.981,0.307l93.393,59.623c0.666,0.556,1.065,1.376,1.065,2.215c0,0.841-0.399,1.67-1.065,2.215
                    l-93.397,59.628c-0.509,0.4-1.114,0.61-1.743,0.61l-1.233-0.289C391.488,366.588,390.873,365.585,390.873,364.519z" />
                </g>
            </svg>
        </span>
    `;

    wrapper.appendChild(bar);

    // 样式
    const style = document.createElement("style");

    style.innerHTML = `
        .number-indicator{
            position:relative;
        }

        .number-indicator::after{
            content: attr(data-number);
            position: absolute;
            bottom: 0;
            right: 0;
            color: #40a9ff;
            font-size: 14px;
            font-weight: bold;
            background: #000;
            border-radius: 10px;
            padding: 3px 5px;
        }

        .copy-link:active{
            color: #ccc;
        }

        .download-btn:hover{
            text-decoration: underline;
        }
        .download-btn:active{
            opacity: 0.9;
        }

        .m3u8-item{
            color: white;
            margin-bottom: 5px;
            display: flex;
            flex-direction: row;
            background: black;
            padding: 3px 10px;
            border-radius: 3px;
            font-size: 14px;
            user-select: none;
        }

        [data-shown="false"] {
            opacity: 0.8;
            zoom: 0.8;
        }

        [data-shown="false"]:hover{
            opacity: 1;
        }

        [data-shown="false"] .m3u8-item{
            display: none;
        }

    `;

    wrapper.appendChild(style);




    const barBtn = bar.querySelector(".number-indicator");

    // 关于显隐和移动

    (async function () {

        let shown = await GM.getValue("shown", true);
        wrapper.setAttribute("data-shown", shown);


        let x = await GM.getValue("x", 10);
        let y = await GM.getValue("y", 10);

        x = Math.min(innerWidth - 50, x);
        y = Math.min(innerHeight - 50, y);

        if (x < 0) x = 0;
        if (y < 0) y = 0;

        rootDiv.style.top = `${y}px`;
        rootDiv.style.right = `${x}px`;

        barBtn.addEventListener("mousedown", e => {
            let startX = e.pageX;
            let startY = e.pageY;

            let moved = false;

            let mousemove = e => {
                let offsetX = e.pageX - startX;
                let offsetY = e.pageY - startY;
                if (moved || (Math.abs(offsetX) + Math.abs(offsetY)) > 5) {
                    moved = true;
                    rootDiv.style.top = `${y + offsetY}px`;
                    rootDiv.style.right = `${x - offsetX}px`;
                }
            };
            let mouseup = e => {

                let offsetX = e.pageX - startX;
                let offsetY = e.pageY - startY;

                if (moved) {
                    x -= offsetX;
                    y += offsetY;
                    mgmapi.setValue("x", x);
                    mgmapi.setValue("y", y);
                } else {
                    shown = !shown;
                    mgmapi.setValue("shown", shown);
                    wrapper.setAttribute("data-shown", shown);
                }

                removeEventListener("mousemove", mousemove);
                removeEventListener("mouseup", mouseup);
            }
            addEventListener("mousemove", mousemove);
            addEventListener("mouseup", mouseup);
        });
    })();






    let count = 0;
    let shownUrls = [];


    function doVideos() {
        for (let v of Array.from(document.querySelectorAll("video"))) {
            if (v.duration && v.src && v.src.startsWith("http") && (!shownUrls.includes(v.src))) {
                const src = v.src;

                shownUrls.push(src);
                showVideo({
                    type: "video",
                    url: new URL(src),
                    duration: `${Math.ceil(v.duration * 10 / 60) / 10} mins`,
                    download() {
                        const details = {
                            url: src,
                            name: (() => {
                                let name = new URL(src).pathname.split("/").slice(-1)[0];
                                if (!/\.\w+$/.test(name)) {
                                    if (name.match(/^\s*$/)) name = Date.now();
                                    name = name + ".mp4";
                                }
                                return name;
                            })(),
                            headers: {
                                // referer: location.origin, // 不允许该头
                                origin: location.origin
                            },
                            onerror(e) {
                                mgmapi.openInTab(src);
                            }
                        };
                        mgmapi.download(details);
                    }
                })
            }
        }
    }

    async function doM3U({ url, content }) {

        url = new URL(url);

        if (shownUrls.includes(url.href)) return;

        // 解析 m3u
        content = content || await (await fetch(url)).text();

        const parser = new m3u8Parser.Parser();
        parser.push(content);
        parser.end();
        const manifest = parser.manifest;

        if (manifest.segments) {
            let duration = 0;
            manifest.segments.forEach((segment) => {
                duration += segment.duration;
            });
            manifest.duration = duration;
        }

        showVideo({
            type: "m3u8",
            url,
            duration: manifest.duration ? `${Math.ceil(manifest.duration * 10 / 60) / 10} mins` : manifest.playlists ? `多(Multi)(${manifest.playlists.length})` : "未知(unknown)",
            async download() {
                mgmapi.openInTab(
                    `https://tools.thatwind.com/tool/m3u8downloader#${new URLSearchParams({
                        m3u8: url.href,
                        referer: location.href,
                        filename: (await getTopTitle()) || ""
                    })}`
                );
            }
        })

    }



    async function showVideo({
        type,
        url,
        duration,
        download
    }) {
        let div = document.createElement("div");
        div.className = "m3u8-item";
        div.innerHTML = `
            <span>${type}</span>
            <span
                class="copy-link"
                title="${url}"
                style="
                    max-width: 200px;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                    overflow: hidden;
                    margin-left: 10px;
                "
            >${url.pathname}</span>
            <span 
                style="
                    margin-left: 10px;
                    flex-grow: 1;
                "
            >${duration}</span>
            <span
                class="download-btn"
                style="
                    margin-left: 10px;
                    cursor: pointer;
            ">下载(Download)</span>
        `;

        div.querySelector(".copy-link").addEventListener("click", () => {
            // 复制链接
            mgmapi.copyText(url.href);
            mgmapi.message("已复制链接 (link copied)", 2000);
        });

        div.querySelector(".download-btn").addEventListener("click", download);

        rootDiv.style.display = "block";

        count++;

        shownUrls.push(url.href);

        bar.querySelector(".number-indicator").setAttribute("data-number", count);

        wrapper.appendChild(div);
    }

})();

(function () {
    'use strict';

    const reg = /magnet:\?xt=urn:btih:\w{10,}([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;

    let l = navigator.language || "en";
    if (l.startsWith("en-")) l = "en";
    else if (l.startsWith("zh-")) l = "zh-CN";
    else l = "en";

    const T = {
        "en": {
            play: "Play"
        },
        "zh-CN": {
            play: '播放'
        }
    }[l];

    whenDOMReady(() => {
        addStyle(`
            button[data-wtmzjk-mag-url]{
                all: initial;
                border: none;
                outline: none;
                background: none;
                background: #f7d308;
                background: #08a6f7;
                margin: 2px 8px;
                border-radius: 3px;
                color: white;
                cursor: pointer;
                display: inline-flex;
                height: 1.6em;
                padding: 0 .8em;
                align-items: center;
                justify-content: center;
                transition: background .15s;
                text-decoration: none;
                border-radius: 0.8em;
                font-size: small;
            }
            button[data-wtmzjk-mag-url]>svg{
                height: 60%;
                fill: white;
                pointer-events: none;
            }
            button[data-wtmzjk-mag-url]:hover{
                background: #fae157;
                background: #39b9f9;
            }
            button[data-wtmzjk-mag-url]:active{
                background: #dfbe07;
                background: #0797df;
            }
            button[data-wtmzjk-mag-url]>span{
                pointer-events: none;
                font-size: small;margin-right: .5em;font-weight:bold;color:white !important;
            }
        `);
        window.addEventListener("click", onEvents, true);
        window.addEventListener("mousedown", onEvents, true);
        window.addEventListener("mouseup", onEvents, true);

        watchBodyChange(work);
    });

    function onEvents(e) {
        if (e.target.hasAttribute('data-wtmzjk-mag-url')) {
            e.preventDefault();
            e.stopPropagation();
            if (e.type == "click") {
                let a = document.createElement('a');
                a.href = 'https://www.diancigaoshou.com/#' + new URLSearchParams({ url: e.target.getAttribute('data-wtmzjk-mag-url') });
                a.target = "_blank";
                a.click();
            }
        }
    }



    function createWatchButton(url, isForPlain = false) {
        let button = document.createElement("button");
        button.setAttribute('data-wtmzjk-mag-url', url);
        if (isForPlain) button.setAttribute('data-wtmzjk-button-for-plain', '');
        button.innerHTML = `<span>${T.play}</span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>`;
        return button;
    }

    function hasPlainMagUrlThatNotHandled() {
        let m = document.body.textContent.match(new RegExp(reg, 'g'));
        return document.querySelectorAll(`[data-wtmzjk-button-for-plain]`).length != (m ? m.length : 0);
    }

    function work() {
        if (!document.body) return;
        if (hasPlainMagUrlThatNotHandled()) {
            for (let node of getAllTextNodes(document.body)) {
                if (node.nextSibling && node.nextSibling.hasAttribute && node.nextSibling.hasAttribute('data-wtmzjk-mag-url')) continue;
                let text = node.nodeValue;
                if (!reg.test(text)) continue;
                let match = text.match(reg);
                if (match) {
                    let url = match[0];
                    let p = node.parentNode;
                    p.insertBefore(document.createTextNode(text.slice(0, match.index + url.length)), node);
                    p.insertBefore(createWatchButton(url, true), node);
                    p.insertBefore(document.createTextNode(text.slice(match.index + url.length)), node);
                    p.removeChild(node);
                }
            }
        }
        for (let a of Array.from(document.querySelectorAll(
            ['href', 'value', 'data-clipboard-text', 'data-value', 'title', 'alt', 'data-url', 'data-magnet', 'data-copy'].map(n => `[${n}*="magnet:?xt=urn:btih:"]`).join(',')
        ))) {
            if (a.nextSibling && a.nextSibling.hasAttribute && a.nextSibling.hasAttribute('data-wtmzjk-mag-url')) continue; // 已经添加
            if (reg.test(a.textContent)) continue;
            for (let attr of a.getAttributeNames()) {
                let val = a.getAttribute(attr);
                if (!reg.test(val)) continue;
                let url = val.match(reg)[0];
                a.parentNode.insertBefore(createWatchButton(url), a.nextSibling);
            }
        }
    }


    function watchBodyChange(onchange) {
        let timeout;
        let observer = new MutationObserver(() => {
            if (!timeout) {
                timeout = setTimeout(() => {
                    timeout = null;
                    onchange();
                }, 200);
            }
        });
        observer.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            characterData: true
        });

    }

    function getAllTextNodes(parent) {
        var re = [];
        if (["STYLE", "SCRIPT", "BASE", "COMMAND", "LINK", "META", "TITLE", "XTRANS-TXT", "XTRANS-TXT-GROUP", "XTRANS-POPUP"].includes(parent.tagName)) return re;
        for (let node of parent.childNodes) {
            if (node.childNodes.length) re = re.concat(getAllTextNodes(node));
            else if (Text.prototype.isPrototypeOf(node) && (!node.nodeValue.match(/^\s*$/))) re.push(node);
        }
        return re;
    }

    function whenDOMReady(f) {
        if (document.body) f();
        else window.addEventListener("DOMContentLoaded", f);
    }

    function addStyle(s) {
        let style = document.createElement("style");
        style.innerHTML = s;
        document.documentElement.appendChild(style);
    }

})();

@GunGunGun
Copy link

GunGunGun commented Apr 18, 2023

Because the above post is too long, I'll use this post to update what I've found:

  • I found that with Firemonkey and above script:
    image

window.XMLHttpRequest.prototype.open has become:

window.XMLHttpRequest.prototype.open
Restricted {  }

Normally it should be:

window.XMLHttpRequest.prototype.open
function open()

With Violentmonkey it became:

window.XMLHttpRequest.prototype.open
function open(args)

This should be the root of the issue.

@erosman
Copy link
Owner Author

erosman commented Apr 18, 2023

@GunGunGun Are you the developer of M3U8 Video Detector and Downloader?

There must be something differences between FM and VM/TM ?

Of course.

In current MV2 (MV3 will be different for all userScript/userStyle mangers), FM injection context and GM|TM|VM injection context are different.
See also: @inject-into: Context Comparison

Injection Context

  • FM injects int a dedicated isolated userScript context
  • GM|TM|VM inject into semi-privileged content context (the same context that is meant for extension's own code)

Window

  • window in userScript context (FM) is not the same as window in content context (GM|TM|VM)

unsafeWindow

  • FM|GM -> unsafeWindow = window.wrappedJSObject
  • TM|VM -> unsafeWindow = window

Testing in a userscript

console.log(XMLHttpRequest.prototype.open);
// function open()
console.log(unsafeWindow.XMLHttpRequest.prototype.open);
// function open()

Regarding the following test in Web Developer Tools

window.XMLHttpRequest.prototype.open
// Restricted {  }

It is due to the rewriting of XMLHttpRequest.prototype.open lines 222-232
I am not sure how it should be fixed though. 🤔

@GunGunGun
Copy link

GunGunGun commented Apr 19, 2023

@erosman I'm not the dev of this script, honestly this script is a mess, I deleted so many things from this script to make it close to work

So far what I've found

  • @inject-into page is the life saver, it makes this script works because it allowing unsafeWindow.XMLHttpRequest.prototype.open and unsafeWindow.Response.prototype.text to truly get injected into page, this script also has the similar problem, just by enabling inject-into page fixed it completely: https://greasyfork.org/en/scripts/445751-unlimited-downloader

Combining my above modified code with @inject-into page and now I have a working script. :D

Since FireMonkey 2.42, if both GM.* & GM_* @grant are requested, only GM.* are kept.

And do you think this change needs to be restored ? Because GM_s are synchronous, and GM.s are asynchronous, sometimes using both of them is better and in this case this script do.

@erosman
Copy link
Owner Author

erosman commented Apr 19, 2023

@GunGunGun Good job with the script 👍

And do you think this behaviour needs to be changed ? Because GM_s are synchronous, and GM.s are asynchronous, sometimes using both of them is better and in this case this script do.

The behaviour was set in FM 2.42.

To start with, there is no synchronous extension storage in modern browsers. The storage API is asynchronous (has been since Firefox 57).
GM3 for pre-Firefox 57, had GM_* API and its storage was synchronous. After Firefox 57, GM changed the API to GM.* to separate it from older synchronous API.

Userscript developers have been reluctant to update their userscripts and therefore, userscript managers (TM|VM) kept the GM_* API and created an imitation synchronous API by first getting the userscript storage, and then injecting the script with the static storage. The process results in a delay in userscript injection which affects run-at. Furthermore, there are issues/delays in getting the updated storage values, if storage updates (e.g. from another tab), while the script is running.

GM.getValue gets the real up-to-date value, all the time.

Due to requests, FireMonkey also implemented a workaround to imitate the synchronous GM_* storage (v2.43). Since FireMonkey supports both types of API namespace, and the process requires prior knowledge, and FM checks the @grant entries (since v2.42):

  • If @grant for only sync GM storage is requested, FM runs the scripts after getting the storage
  • If @grant for both sync & async GM storage are requested, FM removes the sync API in favour of the async API
    It is assumed that a userscript that requests both types, has the process to properly select the available API.

Updating old userscripts for the modern async JavaScript is quite easy.

// GM3 sync code
(() => {
  const value = GM_getValue('key');
  // --- some code
})();

// GM4 async code
(async() => {
  const value = await GM.getValue('key');
  // --- some code
})();

There are some who complain about the delay in await GM.getValue('key'). In fact, getting a single value asynchronously takes on average 2 milliseconds, while delaying the script injection to get the entire storage on average takes 100s times longer.
Furthermore, manually getting the storage and injecting userscripts and updating the storage, is considerably more resource intensive.

See also:

@GunGunGun
Copy link

@erosman Thank you very much for the detailed information, so it looks like getting asynchronous is a huge upgrade and if it's possible use async GM.getValue in most case to improve performance.

@erosman erosman closed this as completed Apr 22, 2023
@erosman
Copy link
Owner Author

erosman commented Apr 22, 2023

Please open individual topics for the userscript script compatibility issue.

@Orriky
Copy link

Orriky commented May 30, 2023

Hi, Super-preloader script works on 2.68 version.
However, I have problems with Google search sites. If I enable iframe the script doesn't work anymore on Google sites
I'd also like to enable new iframe globally with this custom rule

{
  "name": "GoogleAlt",
  "url": "^https?://www\\.google\\..+/search.+",
  "useiframe": true,
  "autopager": {
      "newIframe": true,
   },
}

@erosman
Copy link
Owner Author

erosman commented May 30, 2023

@Orriky It would be easier to open a new topic.

If I enable iframe the script doesn't work anymore on Google sites

Did you enable iframe in User Metadata?

I'd also like to enable new iframe globally with this custom rule

What is that rule for/from?

@cyfung1031
Copy link

@erosman
In order to maximize the compatibility ability of FireMonkey, will there be any option that can set in FireMonkey (it could be just user preference which is not controlled by the userscript) that to make the isolation environment being less strict such that

  • FM can inject into semi-privileged content context like TM|VM
  • unsafeWindow = window rather than wrappedJSObject like TM|VM

Currently there is no option to modify the non-isolated environment (some userscripts are doing the direct access to the DOM properties, modifying web APIs, prototype of the native objects like Event)
For Custom Events, if the initiator is inside FM's dedicated isolated context with object type event.detail, and the event handler is hooked in the non-isolated environment, event.detail is not accessible.

If the userscript is simple without any setValue getValue GM menu, it can be injected into the page context and problem solves.
However, most userscripts require these GM features but still they have to modify the non-isolated environment.

Therefore I think giving option to user that FireMonkey can also use GM functions in page context but bear the risks by themselves would make users happy.

@erosman
Copy link
Owner Author

erosman commented Jun 24, 2023

@cyfung1031
📌 There will be changes to the userscript environment in manifest v3 which will affect all user-script & user-style managers. (Read FireMonkey Help for more info)

  • Isolated context is the core security feature of FireMonkey
  • Firefox especially created the isolated context for the security in using userscripts
  • In Manifest 3, userscripts will be injected into an isolated context
  • All userscripts can be compatible with FireMonkey, or GM4, if userscript developers chose to update their userscripts
  • The main difference between setting unsafeWindow to wrappedJSObject and not to window, is that in the 2nd case, the webpage would also have assess to the userscript which can be a security issue
  • unsafeWindow in TM|VM is not always set to window

Please note that while most userscripts are compatible with TM|VM, there are differences between the two and some userscripts are not compatible with both. (Read FireMonkey Help for more info)

Repository owner locked and limited conversation to collaborators Jun 24, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
addon: FireMonkey userscript: compatibility userScript/userCSS Compatibility
Projects
None yet
Development

No branches or pull requests