CSFD Movie Preview

Při najetí myší na odkaz na film se zobrazí náhled jeho profilu.

当前为 2021-08-13 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name CSFD Movie Preview
  3. // @namespace http://csfd.cz
  4. // @description Při najetí myší na odkaz na film se zobrazí náhled jeho profilu.
  5. // @match https://www.csfd.cz/*
  6. // @match https://www.csfd.sk/*
  7. // @exclude https://www.csfd.cz/uzivatel/*/editace/
  8. // @exclude https://www.csfd.sk/uzivatel/*/editace/
  9. // @require https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
  10. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
  11. // @grant GM_registerMenuCommand
  12. // @grant GM.registerMenuCommand
  13. // @grant GM_xmlhttpRequest
  14. // @grant GM.xmlHttpRequest
  15. // @grant GM_getValue
  16. // @grant GM.getValue
  17. // @grant GM_setValue
  18. // @grant GM.setValue
  19. // @version 2.4
  20. // ==/UserScript==
  21.  
  22. // CHANGES
  23. // -------
  24. // 2.4 - do náhledu vrácen název filmu
  25. // 2.3 - opraveno načítání náhledů u epizod seriálů, úprava URL adres
  26. // 2.2 - úpravy kvůli novému designu webu
  27. // 2.1 - opraveno přepínání automatického nahrávání náhledů filmů
  28. // 2.0 - GM_* funkce nahrazeny novými kvůli změně API v GreaseMonkey 4.0+
  29. // 1.3 - upravena hlavička skriptu kvůli přechodu ČSFD na https
  30. // 1.2 - doplněna podpora dynamicky přidávaných odkazů
  31. // 1.1 - výměna jQuery.ajax(), který ve Firefoxu přestal fungovat, za GM_xmlhttpRequest()
  32. // 1.0 - první verze
  33.  
  34. $ = this.jQuery = jQuery.noConflict(true);
  35.  
  36. $('<div id="movie-preview" style="display: none; z-index: 999; width: 420px; background-color: #efefef; padding: 6px; ' +
  37. 'border-radius: 4px; box-shadow: 0 0 10px 4px #777777"><table border="0"><tr><td id="movie-preview-poster" width="152" ' +
  38. 'style="text-align: center"></td><td id="movie-preview-content" style="vertical-align: top; padding-left: 7px"></td>' +
  39. '</tr></table></div>').appendTo('body');
  40.  
  41. var cacheExpires = 7; // days
  42.  
  43. var movieBox = $('div#movie-preview');
  44. var movieBoxPoster = movieBox.find('#movie-preview-poster');
  45. var movieBoxContent = movieBox.find('#movie-preview-content');
  46.  
  47. var movieLinkSelector = 'a[href*="/film/"], a[href*="/film.php"]';
  48.  
  49. var thisPageMovieId = parseMovieId(window.location.href);
  50. var currentMovieId = null;
  51. var movies = [];
  52.  
  53. var timerId = -1;
  54.  
  55. // Greasmonkey-only section start
  56.  
  57. if (typeof GM.registerMenuCommand == 'function' && isStorageSupported()) {
  58. GM.registerMenuCommand("Přepnout automatické nahrávání náhledů filmů", function() {
  59. GM.getValue("doPrefetch", false).then(function(doPrefetch) {
  60. GM.setValue("doPrefetch", !doPrefetch);
  61.  
  62. alert("Automatické nahrávání náhledů filmů " + (doPrefetch? "vypnuto": "zapnuto") + ".\nZměna nastavení se projeví po obnovení stránky.");
  63. });
  64. });
  65. }
  66.  
  67. // Greasmonkey-only section end
  68.  
  69. function isStorageSupported() {
  70. return typeof(Storage) !== void(0);
  71. }
  72.  
  73. function parseMovieId(movieURL) {
  74. var match = movieURL.match(/\/film(?:\.php\?)?(?:\/[\d]+)?.*\/([\d]+)/);
  75. return match && match.length >= 2? 'm' + match[1]: null;
  76. }
  77.  
  78. function getDiffDays(date1, date2) {
  79. return Math.round(Math.abs(date1 - date2) / (1000 * 3600 * 24));
  80. }
  81.  
  82. var storage = isStorageSupported()?
  83. { // local storage
  84. getStoredItem: function(movieURL) {
  85. return localStorage[parseMovieId(movieURL)];
  86. },
  87. setStoredItem: function(movieURL, value) {
  88. try {
  89. localStorage[parseMovieId(movieURL)] = value;
  90. } catch (ex) {
  91. // "Persistent storage maximum size reached" -> remove 10 random items
  92. for (var i=0; i < 10; i++) {
  93. var index = Math.floor(Math.random() * localStorage.length);
  94. var key = localStorage.key(index);
  95.  
  96. localStorage.removeItem(key);
  97. }
  98.  
  99. return this.setStoredItem(movieURL, value);
  100. }
  101. },
  102. cleanExpiredData: function() {
  103. var lastCleanup = localStorage["last-cleanup"]? Date.parse(localStorage["last-cleanup"]): new Date(0);
  104.  
  105. // run cleanup only once per day
  106. if (getDiffDays(new Date(), lastCleanup) < 1) return;
  107.  
  108. for(var key in localStorage) {
  109. if (key.match(/m\d+/)) {
  110. var cached = JSON.parse(localStorage[key]);
  111. if (getDiffDays(new Date(), Date.parse(cached.timestamp)) > cacheExpires) {
  112. localStorage.removeItem(key);
  113. }
  114. }
  115. }
  116.  
  117. localStorage["last-cleanup"] = new Date();
  118. }
  119. }:
  120. { // dummy storage
  121. getStoredItem: function(movieURL) {
  122. return null;
  123. },
  124. setStoredItem: function(movieURL, value) {
  125. // noop
  126. },
  127. cleanExpiredData: function() {
  128. // noop
  129. }
  130. };
  131.  
  132. function getMovieBoxPosition(event) {
  133. var boxWidth = movieBox.width() + 10;
  134. var tPosX = boxWidth - event.clientX + 30 > 0? event.pageX + 30: event.pageX - boxWidth - 30;
  135. var tPosY = event.pageY + event.clientY;
  136.  
  137. if (event.clientY > 30) {
  138. var winHeight = $(window).height();
  139. var boxHeight = movieBox.height() > winHeight? winHeight - 60: movieBox.height();
  140. var overflowY = event.clientY + boxHeight - winHeight;
  141. tPosY = overflowY > 0? event.pageY - overflowY - 50: event.pageY - 30;
  142. }
  143.  
  144. return { X: tPosX, Y: tPosY };
  145. }
  146. function showMovieBox(event, profile, rating) {
  147. var poster = profile.find(".film-posters img");
  148. var title = "<h1 style='font-size: 22px; padding-bottom: 12px'>" + profile.find(".film-header-name h1").text().trim() + "</h1>";
  149. var genre = profile.find(".genres");
  150. var origin = profile.find(".origin");
  151. var creators = profile.find(".creators");
  152.  
  153. movieBoxPoster.html('');
  154. movieBoxPoster.append(poster.css('width', 140));
  155. movieBoxPoster.append('<h1 style="font-size: 32px; margin-top: 12px">' + rating + '</h1>');
  156.  
  157. movieBoxContent.html('');
  158. movieBoxContent.append(title);
  159. movieBoxContent.append(genre.css('font-weight', 'bold'));
  160. movieBoxContent.append(origin.css('font-weight', 'bold'));
  161. movieBoxContent.append('<br>');
  162. movieBoxContent.append(creators);
  163.  
  164. var pos = getMovieBoxPosition(event);
  165. movieBox.css({ 'position': 'absolute', 'top': pos.Y, 'left': pos.X }).show();
  166. }
  167.  
  168. function getCachedData(movieURL) {
  169. var cached = storage.getStoredItem(movieURL);
  170.  
  171. if (cached) {
  172. cached = JSON.parse(cached);
  173.  
  174. if (getDiffDays(new Date(), Date.parse(cached.timestamp)) <= cacheExpires)
  175. return { "profile": $(cached.profile), "rating": cached.rating };
  176. }
  177.  
  178. return null;
  179. }
  180. function loadMovieBox(movieURL, doneCallback, errorCallback, redirectMovieURL) {
  181. if (!redirectMovieURL) redirectMovieURL = movieURL;
  182.  
  183. console.log("[CSFD Movie Preview] Loading movie page: " + redirectMovieURL);
  184.  
  185. GM.xmlHttpRequest({
  186. method: "GET",
  187. url: redirectMovieURL,
  188. onload: function(response) {
  189. try {
  190. if (false /* TODO: handle redirect */) {
  191. loadMovieBox(movieURL, doneCallback, errorCallback, response.redirect);
  192. } else {
  193. response = $(response.responseText);
  194.  
  195. var profile = response.find(".film-info").html().replace(/[\t\n]+/mg, ' ');
  196. var rating = response.find(".film-info .rating-average").text().trim();
  197. storage.setStoredItem(movieURL, JSON.stringify({ "profile": profile, "rating": rating, "timestamp": new Date() }));
  198. if (doneCallback) doneCallback($(profile), rating);
  199. }
  200. } catch(ex) {
  201. console.log("[CSFD Movie Preview] Error in AJAX handler: " + ex.message);
  202.  
  203. if (errorCallback) errorCallback();
  204. }
  205. },
  206. onerror: function(response) {
  207. if (errorCallback) errorCallback();
  208. }
  209. });
  210. }
  211.  
  212. function prefetchMovies() {
  213. if (!isStorageSupported()) return;
  214. GM.getValue("doPrefetch", false).then(function(doPrefetch) {
  215. var movieURL;
  216.  
  217. if (doPrefetch && (movieURL = movies.shift())) {
  218. setTimeout(function() {
  219. if (!getCachedData(movieURL)) {
  220. loadMovieBox(movieURL, prefetchMovies, prefetchMovies);
  221. } else {
  222. prefetchMovies();
  223. }
  224. }, 300);
  225. }
  226. });
  227. }
  228.  
  229. function addHoverHandler(element) {
  230. element.hover(function(event) {
  231. var movieURL = $(this).attr("href").trim();
  232. var movieId = parseMovieId(movieURL);
  233.  
  234. // prevent previews of the movie on its page
  235. if (thisPageMovieId == movieId) return;
  236.  
  237. currentMovieId = movieId;
  238.  
  239. var cached = getCachedData(movieURL);
  240. if (cached) {
  241. showMovieBox(event, cached.profile, cached.rating);
  242. } else {
  243. clearTimeout(timerId);
  244.  
  245. timerId = setTimeout(function() {
  246. loadMovieBox(movieURL, function(profile, rating) {
  247. if (currentMovieId == movieId) showMovieBox(event, profile, rating);
  248. });
  249. }, 30);
  250. }
  251. }, function() {
  252. clearTimeout(timerId);
  253. timerId = -1;
  254. currentMovieId = null;
  255.  
  256. movieBox.hide();
  257. });
  258. }
  259.  
  260. function setupMutationObserver() {
  261. var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  262.  
  263. var observer = new MutationObserver(function(mutations) {
  264. mutations.forEach(function(mutation) {
  265. for (var i=0; i < mutation.addedNodes.length; i++) {
  266. $(mutation.addedNodes[i]).find("a").each(function() {
  267. if (this.href && this.href.match(/\/film/)) {
  268. addHoverHandler($(this));
  269.  
  270. var movieURL = this.href.trim();
  271. movies.push(movieURL);
  272. }
  273. });
  274. }
  275. });
  276.  
  277. prefetchMovies();
  278. });
  279.  
  280. observer.observe(document.querySelector("body"), {
  281. childList: true,
  282. subtree: true
  283. });
  284. }
  285.  
  286. // program start
  287.  
  288. storage.cleanExpiredData();
  289.  
  290. $(movieLinkSelector).each(function() {
  291. addHoverHandler($(this));
  292.  
  293. var movieURL = $(this).attr("href").trim();
  294. movies.push(movieURL);
  295. });
  296.  
  297. setupMutationObserver();
  298.  
  299. prefetchMovies();
  300.  
  301. // program end