state.js 9.7 KB

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