state.js 10 KB

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