state.js 9.6 KB

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