state.js 9.5 KB

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