state.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. import {
  2. getLikeButton,
  3. getDislikeButton,
  4. getButtons,
  5. getLikeTextContainer,
  6. getDislikeTextContainer,
  7. } from "./buttons";
  8. import { createRateBar } from "./bar";
  9. import {
  10. getBrowser,
  11. getVideoId,
  12. cLog,
  13. numberFormat,
  14. getColorFromTheme,
  15. } from "./utils";
  16. import { localize } from "./utils";
  17. import { createStarRating } from "./starRating";
  18. //TODO: Do not duplicate here and in ryd.background.js
  19. const apiUrl = "https://returnyoutubedislikeapi.com";
  20. const LIKED_STATE = "LIKED_STATE";
  21. const DISLIKED_STATE = "DISLIKED_STATE";
  22. const NEUTRAL_STATE = "NEUTRAL_STATE";
  23. let extConfig = {
  24. disableVoteSubmission: false,
  25. coloredThumbs: false,
  26. coloredBar: false,
  27. colorTheme: "classic",
  28. numberDisplayFormat: "compactShort",
  29. showTooltipPercentage: false,
  30. tooltipPercentageMode: "dash_like",
  31. numberDisplayReformatLikes: false,
  32. };
  33. let storedData = {
  34. likes: 0,
  35. dislikes: 0,
  36. previousState: NEUTRAL_STATE,
  37. };
  38. function isMobile() {
  39. return location.hostname == "m.youtube.com";
  40. }
  41. function isShorts() {
  42. return location.pathname.startsWith("/shorts");
  43. }
  44. function isNewDesign() {
  45. return document.getElementById("comment-teaser") !== null;
  46. }
  47. let mutationObserver = new Object();
  48. if (isShorts() && mutationObserver.exists !== true) {
  49. cLog("initializing mutation observer");
  50. mutationObserver.options = {
  51. childList: false,
  52. attributes: true,
  53. subtree: false,
  54. };
  55. mutationObserver.exists = true;
  56. mutationObserver.observer = new MutationObserver(function (
  57. mutationList,
  58. observer
  59. ) {
  60. mutationList.forEach((mutation) => {
  61. if (
  62. mutation.type === "attributes" &&
  63. mutation.target.nodeName === "TP-YT-PAPER-BUTTON" &&
  64. mutation.target.id === "button"
  65. ) {
  66. // cLog('Short thumb button status changed');
  67. if (mutation.target.getAttribute("aria-pressed") === "true") {
  68. mutation.target.style.color =
  69. mutation.target.parentElement.parentElement.id === "like-button"
  70. ? getColorFromTheme(true)
  71. : getColorFromTheme(false);
  72. } else {
  73. mutation.target.style.color = "unset";
  74. }
  75. return;
  76. }
  77. cLog(
  78. "unexpected mutation observer event: " + mutation.target + mutation.type
  79. );
  80. });
  81. });
  82. }
  83. function isLikesDisabled() {
  84. // return true if the like button's text doesn't contain any number
  85. if (isMobile()) {
  86. return /^\D*$/.test(
  87. getButtons().children[0].querySelector(".button-renderer-text").innerText
  88. );
  89. }
  90. return /^\D*$/.test(
  91. getButtons().children[0].innerText
  92. );
  93. }
  94. function isVideoLiked() {
  95. if (isMobile()) {
  96. return (
  97. getLikeButton().querySelector("button").getAttribute("aria-label") ==
  98. "true"
  99. );
  100. }
  101. return getLikeButton().classList.contains("style-default-active");
  102. }
  103. function isVideoDisliked() {
  104. if (isMobile()) {
  105. return (
  106. getDislikeButton().querySelector("button").getAttribute("aria-label") ==
  107. "true"
  108. );
  109. }
  110. return getDislikeButton().classList.contains("style-default-active");
  111. }
  112. function getState(storedData) {
  113. if (isVideoLiked()) {
  114. return { current: LIKED_STATE, previous: storedData.previousState };
  115. }
  116. if (isVideoDisliked()) {
  117. return { current: DISLIKED_STATE, previous: storedData.previousState };
  118. }
  119. return { current: NEUTRAL_STATE, previous: storedData.previousState };
  120. }
  121. //--- Sets The Likes And Dislikes Values ---//
  122. function setLikes(likesCount) {
  123. getLikeTextContainer().innerText = likesCount;
  124. }
  125. function setDislikes(dislikesCount) {
  126. getDislikeTextContainer()?.removeAttribute('is-empty');
  127. if (!isLikesDisabled()) {
  128. if (isMobile()) {
  129. getButtons().children[1].querySelector(
  130. ".button-renderer-text"
  131. ).innerText = dislikesCount;
  132. return;
  133. }
  134. getDislikeTextContainer().innerText = dislikesCount;
  135. } else {
  136. cLog("likes count disabled by creator");
  137. if (isMobile()) {
  138. getButtons().children[1].querySelector(
  139. ".button-renderer-text"
  140. ).innerText = localize("TextLikesDisabled");
  141. return;
  142. }
  143. getDislikeTextContainer().innerText = localize("TextLikesDisabled");
  144. }
  145. }
  146. function getLikeCountFromButton() {
  147. try {
  148. if (isShorts()) {
  149. //Youtube Shorts don't work with this query. It's not nessecary; we can skip it and still see the results.
  150. //It should be possible to fix this function, but it's not critical to showing the dislike count.
  151. return false;
  152. }
  153. let likesStr = getLikeButton()
  154. .querySelector("yt-formatted-string#text")
  155. .getAttribute("aria-label")
  156. .replace(/\D/g, "");
  157. return likesStr.length > 0 ? parseInt(likesStr) : false;
  158. }
  159. catch {
  160. return false;
  161. }
  162. }
  163. function processResponse(response, storedData) {
  164. const formattedDislike = numberFormat(response.dislikes);
  165. setDislikes(formattedDislike);
  166. if (extConfig.numberDisplayReformatLikes === true) {
  167. const nativeLikes = getLikeCountFromButton();
  168. if (nativeLikes !== false) {
  169. setLikes(numberFormat(nativeLikes));
  170. }
  171. }
  172. storedData.dislikes = parseInt(response.dislikes);
  173. storedData.likes = getLikeCountFromButton() || parseInt(response.likes);
  174. createRateBar(storedData.likes, storedData.dislikes);
  175. if (extConfig.coloredThumbs === true) {
  176. if (isShorts()) {
  177. // for shorts, leave deactived buttons in default color
  178. let shortLikeButton = getLikeButton().querySelector(
  179. "tp-yt-paper-button#button"
  180. );
  181. let shortDislikeButton = getDislikeButton().querySelector(
  182. "tp-yt-paper-button#button"
  183. );
  184. if (shortLikeButton.getAttribute("aria-pressed") === "true") {
  185. shortLikeButton.style.color = getColorFromTheme(true);
  186. }
  187. if (shortDislikeButton.getAttribute("aria-pressed") === "true") {
  188. shortDislikeButton.style.color = getColorFromTheme(false);
  189. }
  190. mutationObserver.observer.observe(
  191. shortLikeButton,
  192. mutationObserver.options
  193. );
  194. mutationObserver.observer.observe(
  195. shortDislikeButton,
  196. mutationObserver.options
  197. );
  198. } else {
  199. getLikeButton().style.color = getColorFromTheme(true);
  200. getDislikeButton().style.color = getColorFromTheme(false);
  201. }
  202. }
  203. //Temporary disabling this - it breaks all places where getButtons()[1] is used
  204. // createStarRating(response.rating, isMobile());
  205. }
  206. // Tells the user if the API is down
  207. function displayError(error) {
  208. getButtons().children[1].querySelector("#text").innerText = localize(
  209. "textTempUnavailable"
  210. );
  211. }
  212. async function setState(storedData) {
  213. storedData.previousState = isVideoDisliked()
  214. ? DISLIKED_STATE
  215. : isVideoLiked()
  216. ? LIKED_STATE
  217. : NEUTRAL_STATE;
  218. let statsSet = false;
  219. let videoId = getVideoId(window.location.href);
  220. let likeCount = getLikeCountFromButton() || null;
  221. let response = await fetch(
  222. `${apiUrl}/votes?videoId=${videoId}&likeCount=${likeCount || ""}`,
  223. {
  224. method: "GET",
  225. headers: {
  226. Accept: "application/json",
  227. },
  228. }
  229. )
  230. .then((response) => {
  231. if (!response.ok) displayError(response.error);
  232. return response;
  233. })
  234. .then((response) => response.json())
  235. .catch(displayError);
  236. cLog("response from api:");
  237. cLog(JSON.stringify(response));
  238. if (response !== undefined && !("traceId" in response) && !statsSet) {
  239. processResponse(response, storedData);
  240. }
  241. }
  242. function setInitialState() {
  243. setState(storedData);
  244. }
  245. function initExtConfig() {
  246. initializeDisableVoteSubmission();
  247. initializeColoredThumbs();
  248. initializeColoredBar();
  249. initializeColorTheme();
  250. initializeNumberDisplayFormat();
  251. initializeTooltipPercentage();
  252. initializeTooltipPercentageMode();
  253. initializeNumberDisplayReformatLikes();
  254. }
  255. function initializeDisableVoteSubmission() {
  256. getBrowser().storage.sync.get(["disableVoteSubmission"], (res) => {
  257. if (res.disableVoteSubmission === undefined) {
  258. getBrowser().storage.sync.set({ disableVoteSubmission: false });
  259. } else {
  260. extConfig.disableVoteSubmission = res.disableVoteSubmission;
  261. }
  262. });
  263. }
  264. function initializeColoredThumbs() {
  265. getBrowser().storage.sync.get(["coloredThumbs"], (res) => {
  266. if (res.coloredThumbs === undefined) {
  267. getBrowser().storage.sync.set({ coloredThumbs: false });
  268. } else {
  269. extConfig.coloredThumbs = res.coloredThumbs;
  270. }
  271. });
  272. }
  273. function initializeColoredBar() {
  274. getBrowser().storage.sync.get(["coloredBar"], (res) => {
  275. if (res.coloredBar === undefined) {
  276. getBrowser().storage.sync.set({ coloredBar: false });
  277. } else {
  278. extConfig.coloredBar = res.coloredBar;
  279. }
  280. });
  281. }
  282. function initializeColorTheme() {
  283. getBrowser().storage.sync.get(["colorTheme"], (res) => {
  284. if (res.colorTheme === undefined) {
  285. getBrowser().storage.sync.set({ colorTheme: false });
  286. } else {
  287. extConfig.colorTheme = res.colorTheme;
  288. }
  289. });
  290. }
  291. function initializeNumberDisplayFormat() {
  292. getBrowser().storage.sync.get(["numberDisplayFormat"], (res) => {
  293. if (res.numberDisplayFormat === undefined) {
  294. getBrowser().storage.sync.set({ numberDisplayFormat: "compactShort" });
  295. } else {
  296. extConfig.numberDisplayFormat = res.numberDisplayFormat;
  297. }
  298. });
  299. }
  300. function initializeTooltipPercentage() {
  301. getBrowser().storage.sync.get(["showTooltipPercentage"], (res) => {
  302. if (res.showTooltipPercentage === undefined) {
  303. getBrowser().storage.sync.set({ showTooltipPercentage: false });
  304. } else {
  305. extConfig.showTooltipPercentage = res.showTooltipPercentage;
  306. }
  307. });
  308. }
  309. function initializeTooltipPercentageMode() {
  310. getBrowser().storage.sync.get(["tooltipPercentageMode"], (res) => {
  311. if (res.tooltipPercentageMode === undefined) {
  312. getBrowser().storage.sync.set({ tooltipPercentageMode: "dash_like" });
  313. } else {
  314. extConfig.tooltipPercentageMode = res.tooltipPercentageMode;
  315. }
  316. });
  317. }
  318. function initializeNumberDisplayReformatLikes() {
  319. getBrowser().storage.sync.get(["numberDisplayReformatLikes"], (res) => {
  320. if (res.numberDisplayReformatLikes === undefined) {
  321. getBrowser().storage.sync.set({ numberDisplayReformatLikes: false });
  322. } else {
  323. extConfig.numberDisplayReformatLikes = res.numberDisplayReformatLikes;
  324. }
  325. });
  326. }
  327. export {
  328. isMobile,
  329. isShorts,
  330. isVideoDisliked,
  331. isVideoLiked,
  332. isNewDesign,
  333. getState,
  334. setState,
  335. setInitialState,
  336. setLikes,
  337. setDislikes,
  338. getLikeCountFromButton,
  339. LIKED_STATE,
  340. DISLIKED_STATE,
  341. NEUTRAL_STATE,
  342. extConfig,
  343. initExtConfig,
  344. storedData,
  345. isLikesDisabled,
  346. };