Skip to content

Instantly share code, notes, and snippets.

@kosorin
Last active April 15, 2020 13:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kosorin/17750e5281f03942712c6022158b190a to your computer and use it in GitHub Desktop.
Save kosorin/17750e5281f03942712c6022158b190a to your computer and use it in GitHub Desktop.
SerialZone.cz UserScript
// ==UserScript==
// @name SerialZone.cz PLUS
// @namespace https://www.serialzone.cz/
// @version 3.1.1
// @author David Kosorin
// @match https://www.serialzone.cz/*
// @grant none
// ==/UserScript==
try {
console.log("========== SerialZone.cz <PLUS> ========== BEGIN");
(function () {
'use strict';
const classPrefix = "szp-";
/**
* @typedef {number} SeriesId
*/
/**
* @readonly
* @enum {number}
*/
const SeriesState = {
None: 0,
Watch: 1 << 1,
Follow: 1 << 2,
Love: 1 << 3,
Ignore: 1 << 4,
};
/**
* @readonly
* @enum {number}
*/
const ActionType = {
Watch: 1,
Follow: 2,
Love: 3,
Ignore: 4,
};
class ActionData {
/**
* @type {Test}
*/
action;
/**
* @type {string}
*/
icon;
/**
* @type {string}
*/
title;
/**
* @param {string} action
* @param {string} icon
* @param {string} title
*/
constructor(action, icon, title) {
this.action = action;
this.icon = icon;
this.title = title;
}
}
class ActionButton {
/**
* @type {ActionType}
*/
type;
/**
* @type {SeriesId}
*/
seriesId;
/**
* @type {boolean}
*/
confirmAction;
/**
* @type {function (ActionButton): void}
*/
onAction;
/**
* @type {boolean}
*/
currentState;
/**
* @type {*}
*/
data;
/**
* @type {ActionData}
*/
get currentData() {
return this.data[this.currentState];
}
/**
* @type {HTMLElement}
*/
element;
/**
* @param {ActionType} type
* @param {SeriesId} seriesId
* @param {boolean} initialState
* @param {*} data
* @param {function} [onAction]
* @param {boolean} [confirmAction]
*/
constructor(type, seriesId, initialState, data, onAction, confirmAction) {
let root = document.createElement("span");
let link = document.createElement("a");
link.setAttribute("href", "#");
link.addEventListener("click", e => {
e.preventDefault();
if (this.currentState && this.confirmAction && !confirm(this.currentData.title + "?")) {
return;
}
$.ajax({
type: "POST",
url: "https://www.serialzone.cz/ajax/moje-serialy/",
data: {
serial: this.seriesId,
akce: this.currentData.action,
},
cache: false,
}).done(() => {
this.toggleState();
if (this.onAction) {
this.onAction(this);
}
});
});
root.appendChild(link);
this.type = type;
this.seriesId = parseInt(seriesId);
this.confirmAction = confirmAction;
this.onAction = onAction;
this.data = data;
this.element = root;
this.setState(initialState);
}
/**
* @param {boolean} state
* @param {boolean} [visible]
* @returns {void}
*/
setState(state, visible) {
this.currentState = state;
let link = this.element.firstChild;
link.innerHTML = this.currentData.icon;
link.setAttribute("title", this.currentData.title);
link.setAttribute("alt", this.currentData.title);
if (visible !== undefined) {
show(this.element, visible);
}
}
/**
* @returns {void}
*/
toggleState() {
this.setState(!this.currentState);
}
}
function show(element, value) {
element.style.display = value ? "block" : "none";
}
function getUserUrl() {
let root = document.querySelector("img.user-ico-user");
if (!root) {
return null;
}
return root.parentNode.href;
}
function processMenu(userUrl) {
function normalizeUrl(url, originUrl, append) {
url = url.replace(originUrl, "");
url = url.substring(0, url.length - 1);
if (append) {
url = url + "/" + append
}
return url;
}
let menu = document.querySelector("ul.menu");
if (!menu) {
return;
}
if (!userUrl) {
let originUrl = location.origin + "/";
let links = menu.querySelectorAll("li.fl-left > a");
for (let i = 0; i < links.length; i++) {
let link = links[i];
let current = normalizeUrl(link.href, originUrl);
switch (current) {
case "serialy":
case "clanky":
case "forum":
break;
default:
link.parentNode.remove();
break;
}
}
return;
}
function setLinkStyle(link) {
link.style.fontWeight = "bolder";
}
function setOtherLinkStyle(link) {
link.style.fontSize = "smaller";
}
function addLink(url, text, beforeLink, afterLink) {
let link = document.createElement("a");
link.innerHTML = text;
link.setAttribute("href", url);
if (location.href == url) {
link.setAttribute("class", "navactive");
}
setLinkStyle(link);
let item = document.createElement("li");
item.setAttribute("class", "fl-left");
item.appendChild(link);
if (beforeLink) {
menu.insertBefore(item, beforeLink.parentNode);
} else if (afterLink) {
menu.insertBefore(item, afterLink.parentNode.nextSibling);
} else {
menu.appendChild(item);
}
return link;
}
let serialsLink;
let calendarLink;
let watchlistLink;
let originUrl = location.origin + "/";
let links = menu.querySelectorAll("li.fl-left > a");
for (let i = 0; i < links.length; i++) {
let link = links[i];
let current = normalizeUrl(link.href, originUrl);
switch (current) {
case "serialy":
setOtherLinkStyle(link);
serialsLink = link;
break;
case "kalendar":
setLinkStyle(link);
calendarLink = link;
break;
case "watchlist":
setLinkStyle(link);
watchlistLink = link;
break;
case "hry":
case "playlist":
case "timeline":
case normalizeUrl(userUrl, originUrl, "moje"):
show(link.parentNode, false);
break;
default:
setOtherLinkStyle(link);
break;
}
}
calendarLink.parentNode.remove();
watchlistLink.parentNode.remove();
menu.appendChild(calendarLink.parentNode);
menu.appendChild(watchlistLink.parentNode);
let sawLink = addLink(userUrl + "rozkoukane/", "Rozkoukané", null, watchlistLink);
let favoritesLink = addLink(userUrl + "moje/", "Oblíbené", null, sawLink);
let wannaSeeLink = addLink(userUrl + "chci-videt/", "Chci vidět", null, favoritesLink);
}
function processCalendar() {
let root = document.querySelector("#calendar");
if (!root) {
return;
}
let today = root.querySelector("td.linked-day");
if (!today) {
return;
}
today.style.border = "2px solid red";
}
function processWatchlist(userUrl) {
if (!userUrl) {
return;
}
let root = document.querySelector("#watchlist");
if (!root) {
return;
}
/**
* @readonly
* @enum {string}
*/
const GroupId = {
Bind: "V poslední době odškrtávané",
Follow: "Sledované",
Love: "Zamilované",
Ignore: "Ignorované",
};
/**
* @readonly
* @enum {SeriesState}
*/
const GroupSeriesState = {
[GroupId.Bind]: SeriesState.None,
[GroupId.Follow]: SeriesState.Follow,
[GroupId.Love]: SeriesState.Follow | SeriesState.Love,
[GroupId.Ignore]: SeriesState.Follow | SeriesState.Ignore,
};
/**
* @readonly
* @enum {number}
*/
const CategoryId = {
Other: 1,
Follow: 2,
Ignore: 3,
/**
* @param {SeriesState} seriesState
* @returns {CategoryId}
*/
getFromSeriesState(seriesState) {
if (!seriesState) {
return CategoryId.Other;
}
if ((seriesState & SeriesState.Ignore) != 0) {
return CategoryId.Ignore;
} else if ((seriesState & SeriesState.Watch) != 0) {
return CategoryId.Other;
} else {
return CategoryId.Follow;
}
}
};
/**
* @readonly
* @enum {string}
*/
const CategoryTitle = {
[CategoryId.Other]: "Ostatní",
[CategoryId.Follow]: "Sledované",
[CategoryId.Ignore]: "Ignorované",
};
class Category {
/**
* @type {number}
*/
categoryId;
/**
* @type {string}
*/
title;
/**
* @type {HTMLElement}
*/
element;
/**
* @type {HTMLElement}
*/
seriesElement;
/**
* @param {number} categoryId
*/
constructor(categoryId) {
this.categoryId = categoryId;
this.title = CategoryTitle[categoryId];
let seriesElementClass = classPrefix + "category-series";
this.element = $.parseHTML(`
<div>
<div class="list-h list-h-lightblue mto20 mbo10" data-kategorie-m="${this.categoryId}">
<div class="lhhead">${this.title}</div>
<div class="cleaner"></div>
</div>
<div class="${seriesElementClass}">
</div>
</div>
`)[1];
this.seriesElement = this.element.querySelector("." + seriesElementClass);
}
}
class Series {
/**
* @type {SeriesId}
*/
seriesId;
/**
* @type {SeriesState}
*/
state;
/**
* @type {CategoryId}
*/
get categoryId() {
return CategoryId.getFromSeriesState(this.state);
}
/** @type {Episode} */
episode;
buttons;
/**
* @type {HTMLElement}
*/
element;
/**
* @type {HTMLElement}
*/
buttonsElement;
/**
* @param {SeriesId} seriesId
* @param {HTMLElement} element
*/
constructor(seriesId, element) {
this.seriesId = seriesId;
this.buttonsElement = document.createElement("div");
this.buttonsElement.classList.add(classPrefix + "buttons");
this.buttonsElement.setAttribute("style", "float: right; width: 48px;");
this.element = element;
this.element.querySelector("div.watchlist-content > div").parentNode.appendChild(this.buttonsElement);
this.updateEpisode();
}
/**
* @param {Series} a
* @param {Series} b
* @returns {number}
*/
static compare(a, b) {
let result = 0;
// Kategorie
result = a.categoryId - b.categoryId;
if (result) {
return result;
}
// Epizoda
result = Episode.compare(a.episode, b.episode);
if (result) {
return result;
}
return result;
}
/**
* @returns {void}
*/
updateEpisode() {
this.episode = new Episode(this.seriesId, this.element);
this.updateOpacity();
}
/**
* @param {boolean} [opaque]
* @returns {void}
*/
updateOpacity(opaque) {
this.element.style.opacity = (opaque || (this.episode && this.episode.isReady)) ? 1 : 0.4;
}
/**
* @param {Category} category
* @returns {void}
*/
onRender(category) {
this.element.dataset.kategorie = category.categoryId;
this.createButtons();
this.element.addEventListener("mouseover", () => this.updateOpacity(true));
this.element.addEventListener("mouseout", () => this.updateOpacity(false));
this.element.addEventListener("DOMSubtreeModified", e => {
for (let target = e.target; target && target != this.element; target = target.parentNode) {
if (target.matches(".eh" + this.seriesId)) {
this.updateEpisode();
break;
}
}
}, false);
}
/**
* @returns {void}
*/
createButtons() {
if (this.buttons) {
return;
}
this.buttons = {};
this.buttons[ActionType.Love] = new ActionButton(ActionType.Love, this.seriesId, (this.state & SeriesState.Love) != 0, {
[false]: new ActionData("love", "🤍", "Do zamilovaných"),
[true]: new ActionData("unlove", "❤️", "Odebrat ze zamilovaných"),
})
this.buttonsElement.appendChild(this.buttons[ActionType.Love].element);
this.buttons[ActionType.Ignore] = new ActionButton(ActionType.Ignore, this.seriesId, (this.state & SeriesState.Ignore) != 0, {
[false]: new ActionData("ignore", "👁️", "Do ignorovaných"),
[true]: new ActionData("unignore", "🔕", "Odebrat z ignorovaných"),
});
this.buttonsElement.appendChild(this.buttons[ActionType.Ignore].element);
this.buttons[ActionType.Follow] = new ActionButton(ActionType.Follow, this.seriesId, (this.state & SeriesState.Follow) != 0, {
[false]: new ActionData("pridat", "➕", "Přidat do sledovaných"),
[true]: new ActionData("odebrat", "➖", "Odebrat ze sledovaných"),
}, button => {
this.buttons[ActionType.Love].setState(false, button.currentState);
this.buttons[ActionType.Ignore].setState(false, button.currentState);
}, true);
this.buttonsElement.appendChild(this.buttons[ActionType.Follow].element);
if (!this.buttons[ActionType.Follow].currentState) {
this.buttons[ActionType.Love].setState(false, false);
this.buttons[ActionType.Ignore].setState(false, false);
}
}
}
class Episode {
/**
* @type {SeriesId}
*/
seriesId;
/**
* @type {number}
*/
serie;
/**
* @type {number}
*/
episode;
/**
* @type {string}
*/
title;
/**
* @type {Date}
*/
date;
/**
* @type {boolean}
*/
get isReady() {
if (!this.date) {
return false;
}
const today = new Date().setHours(0, 0, 0, 0);
return this.date <= today;
}
/**
* @param {SeriesId} seriesId
* @param {HTMLElement} element
*/
constructor(seriesId, element) {
this.seriesId = seriesId;
// Serie + Episode
let serieEpisodeParagraph = element.querySelector("div.watchlist-epnr > div").innerText;
if (serieEpisodeParagraph.match(/^\s*$/)) {
return null;
}
[this.serie, this.episode] = serieEpisodeParagraph.split("×").map(x => parseInt(x));
// Title
this.title = element.querySelector("div.watchlist-content a.dfl2-name").innerText;
// LikeBox
let likeBoxElement = element.querySelector("div.likebox");
if (likeBoxElement) {
likeBoxElement.remove();
}
// Date
try {
let dateElement = element.querySelectorAll("div.watchlist-epname div.wep-name")[1];
let { groups: { day, month, year } } = /:\s*((?<day>\d+)\.\s*(?<month>\d+)\.\s*(?<year>\d+))/.exec(dateElement.innerText);
this.date = new Date(year, month - 1, day);
dateElement.querySelector("span.greentext,span.redtext").parentNode.classList.remove("font11");
} catch (error) {
this.date = null;
}
// Voting button
for (const button of element.querySelectorAll("div.wlp-in > a")) {
if (button.getAttribute("href") == "#nic" && this.isReady) {
button.classList.add("opacity40");
button.style.margin = "0";
button.style.marginLeft = "44px";
} else {
button.remove();
}
}
}
/**
* @param {Episode} a
* @param {Episode} b
* @returns {number}
*/
static compare(a, b) {
const maxDate = new Date(8640000000000000);
let result = 0;
// Datum
result = (a.date || maxDate) - (b.date || maxDate);
if (result) {
return result;
}
// Název
result = a.title.localeCompare(b.title);
return result;
}
}
class Watchlist {
/**
*
*/
series;
/**
*
*/
categories;
/**
* @type {HTMLElement}
*/
element;
/**
* @param {HTMLElement} element
*/
constructor(element) {
this.element = element;
this.series = {};
let parseGroupId = function (element) {
return element.querySelector("div.lhhead").innerHTML;
};
let parseSeriesId = function (element) {
return parseInt(element.querySelector("div.wlp-in").dataset["sid"]);
};
let currentGroupId;
for (let element of this.element.children) {
if (element.classList.contains("list-h")) {
currentGroupId = parseGroupId(element);
} else if (element.classList.contains("def-list2-sz3")) {
let seriesId = parseSeriesId(element);
let series = this.series[seriesId];
if (!series) {
series = new Series(seriesId, element);
this.series[seriesId] = series;
}
series.state |= GroupSeriesState[currentGroupId];
}
}
}
/**
* @returns {void}
*/
render() {
this.categories = {};
this.element.innerHTML = "";
this.element.classList.add("mbo40");
this.renderCategory(CategoryId.Other);
this.renderCategory(CategoryId.Follow);
this.renderCategory(CategoryId.Ignore);
this.renderFilter();
}
/**
* @param {CategoryId} categoryId
* @returns {void}
*/
renderCategory(categoryId) {
/** @type {[Series]} */
let categorySeries = Object.values(this.series).filter(x => x.categoryId == categoryId).sort(Series.compare);
if (categorySeries.length == 0) {
return;
}
let category = new Category(categoryId);
this.categories[categoryId] = category;
this.element.appendChild(category.element);
for (const series of categorySeries) {
series.onRender(category);
category.seriesElement.appendChild(series.element);
}
}
/**
* @returns {void}
*/
renderFilter() {
let filterIgnoredElementClass = classPrefix + "filter-ignored";
let filterElement = $.parseHTML(`
<div class="list-h list-h-grey mto10">
<div class="check-head">
<a href="#" class="filtr-videni ${filterIgnoredElementClass}">zobrazit ignorované <div class="ch-uncheck" id="filter-ignored"></div></a>
</div>
<div class="cleaner"></div>
</div>
`)[1];
this.element.parentNode.insertBefore(filterElement, this.element);
let filterIgnoredElement = filterElement.querySelector("." + filterIgnoredElementClass);
filterIgnoredElement.addEventListener("click", e => {
e.preventDefault();
let checkElement = filterIgnoredElement.querySelector("div");
checkElement.classList.toggle("ch-check");
checkElement.classList.toggle("ch-uncheck");
let showIgnored = checkElement.classList.contains("ch-check");
this.filter(CategoryId.Ignore, showIgnored);
});
this.filter(CategoryId.Ignore, false);
}
/**
* @param {CategoryId} categoryId
* @param {boolean} value
* @returns {void}
*/
filter(categoryId, value) {
show(this.categories[categoryId].element, value);
}
}
new Watchlist(root).render();
}
let userUrl = getUserUrl();
processMenu(userUrl);
processCalendar();
processWatchlist(userUrl);
})();
} catch (e) {
console.log("========== SerialZone.cz <PLUS> ========== ERROR");
console.log(e);
} finally {
console.log("========== SerialZone.cz <PLUS> ========== END");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment