Libib - Stile indicatore stato personalizzato

Modifica i colori e lo stile dell'indicatore dello stato di un oggetto di libib.com

  1. // ==UserScript==
  2. // @name Libib - Custom status indicator style
  3. // @name:it Libib - Stile indicatore stato personalizzato
  4. // @description Set a custom color and style for libib.com item status indicator and more
  5. // @description:it Modifica i colori e lo stile dell'indicatore dello stato di un oggetto di libib.com
  6. // @author JetpackCat
  7. // @namespace https://github.com/JetpackCat-IT/libib-custom-status-style
  8. // @supportURL https://github.com/JetpackCat-IT/libib-custom-status-style/issues
  9. // @icon https://github.com/JetpackCat-IT/libib-custom-status-style/raw/v1.0.0/img/icon_64.png
  10. // @version 2.0.0
  11. // @license GPL-3.0-or-later; https://raw.githubusercontent.com/JetpackCat-IT/libib-custom-status-style/master/LICENSE
  12. // @match https://www.libib.com/library
  13. // @icon https://www.libib.com/img/favicon.png
  14. // @run-at document-idle
  15. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant GM.getValue
  19. // @grant GM.setValue
  20. // ==/UserScript==
  21.  
  22. (function () {
  23. "use strict";
  24.  
  25. // Get libib sidebar menu. The settings button will be added to the sidebar
  26. const libib_sidebar_menu = document.getElementById("primary-menu");
  27.  
  28. // Create the element, it needs to be an <a> tag inside an <li> tag
  29. const settings_button_a = document.createElement("a");
  30. settings_button_a.appendChild(
  31. document.createTextNode("Libib status settings")
  32. );
  33.  
  34. // Create <li> element and insert <a> element inside
  35. const settings_button_li = document.createElement("li");
  36. settings_button_li.appendChild(settings_button_a);
  37.  
  38. // Assign click event handler to open the menu settings
  39. settings_button_li.addEventListener("click", function () {
  40. gmc.open();
  41. });
  42.  
  43. // Add <li> element to the sidebar
  44. libib_sidebar_menu.appendChild(settings_button_li);
  45.  
  46. // Create a container for the configuration elements
  47. const config_container = document.createElement("div");
  48. document.body.appendChild(config_container);
  49.  
  50. const copyToClipboard = (text) => {
  51. const textarea = document.createElement('textarea');
  52. textarea.value = text;
  53. textarea.style.position = 'fixed';
  54. document.body.appendChild(textarea);
  55. textarea.focus();
  56. textarea.select();
  57. try {
  58. document.execCommand('copy');
  59. } catch (err) {
  60. console.error('Failed to copy text: ', err);
  61. }
  62. document.body.removeChild(textarea);
  63. }
  64.  
  65. const readFromClipboard = async () => {
  66. return await navigator.clipboard.readText();
  67. }
  68.  
  69. // For cover blur
  70. let blur_groups = [];
  71.  
  72. // Adapt container background color and shadow based on libib theme (dark/light)
  73. const is_dark_scheme = document.body.classList.contains("dark");
  74. let background_color = "#fefefe";
  75. let shadow_color = "#838383";
  76.  
  77. if (is_dark_scheme) {
  78. background_color = "#1b1b1b";
  79. shadow_color = "#e7e7e7";
  80. }
  81. const config_panel_css = `#libib_status_config{padding: 20px !important; background-color: ${background_color}; box-shadow: 0px 0px 9px 3px ${shadow_color}}; `;
  82.  
  83. let gmc = new GM_config({
  84. id: "libib_status_config", // The id used for this instance of GM_config
  85. title: "Script Settings", // Panel Title
  86. types: {
  87. // Create color input type
  88. color: {
  89. default: null,
  90. toNode: function () {
  91. var field = this.settings,
  92. value = this.value,
  93. id = this.id,
  94. create = this.create,
  95. slash = null,
  96. retNode = create("div", {
  97. className: "config_var",
  98. id: this.configId + "_" + id + "_var",
  99. title: field.title || "",
  100. });
  101.  
  102. // Create the field lable
  103. retNode.appendChild(
  104. create("label", {
  105. innerHTML: field.label,
  106. id: this.configId + "_" + id + "_field_label",
  107. for: this.configId + "_field_" + id,
  108. className: "field_label",
  109. })
  110. );
  111. // Create the actual input element
  112. var props = {
  113. id: this.configId + "_field_" + id,
  114. type: "color",
  115. value: value ?? "",
  116. };
  117. // Actually create and append the input element
  118. retNode.appendChild(create("input", props));
  119. return retNode;
  120. },
  121. toValue: function () {
  122. let input = document.getElementById(
  123. `${this.configId}_field_${this.id}`
  124. );
  125. if(input != null) return input.value;
  126. },
  127. reset: function () {
  128. let input = document.getElementById(
  129. `${this.configId}_field_${this.id}`
  130. );
  131. input.value = this.default;
  132. },
  133. },
  134. number: {
  135. default: null,
  136. toNode: function () {
  137. var field = this.settings,
  138. value = this.value,
  139. id = this.id,
  140. create = this.create,
  141. slash = null,
  142. retNode = create("div", {
  143. className: "config_var",
  144. id: this.configId + "_" + id + "_var",
  145. title: field.title || "",
  146. });
  147.  
  148. // Create the field lable
  149. retNode.appendChild(
  150. create("label", {
  151. innerHTML: field.label,
  152. id: this.configId + "_" + id + "_field_label",
  153. for: this.configId + "_field_" + id,
  154. className: "field_label",
  155. })
  156. );
  157. // Create the actual input element
  158. var props = {
  159. id: this.configId + "_field_" + id,
  160. type: "number",
  161. value: value ?? "",
  162. };
  163. // Actually create and append the input element
  164. retNode.appendChild(create("input", props));
  165. return retNode;
  166. },
  167. toValue: function () {
  168. let input = document.getElementById(
  169. `${this.configId}_field_${this.id}`
  170. );
  171. if(input != null) return input.value;
  172. },
  173. reset: function () {
  174. let input = document.getElementById(
  175. `${this.configId}_field_${this.id}`
  176. );
  177. input.value = this.default;
  178. },
  179. },
  180. },
  181. // Fields object
  182. fields: {
  183. // This is the id of the field
  184. type: {
  185. label: "Indicator type", // Appears next to field
  186. type: "radio", // Makes this setting a radio field
  187. options: ["Triangle", "Border"], // Default = triangle
  188. default: "Triangle", // Default value if user doesn't change it
  189. },
  190. // This is the id of the field
  191. trianglePosition: {
  192. label: "Triangle position", // Appears next to field
  193. type: "select", // Makes this setting a select field
  194. options: ["Top left", "Top right", "Bottom left", "Bottom right"],
  195. default: "Top left", // Default value if user doesn't change it
  196. },
  197. // This is the id of the field
  198. borderPosition: {
  199. label: "Border position", // Appears next to field
  200. type: "select", // Makes this setting a select field
  201. options: ["Top", "Bottom"],
  202. default: "Top", // Default value if user doesn't change it
  203. },
  204. // This is the id of the field
  205. borderHeight: {
  206. label: "Border height", // Appears next to field
  207. type: "number", // Makes this setting a number field
  208. default: 5, // Default value if user doesn't change it
  209. },
  210. // This is the id of the field
  211. colorNotBegun: {
  212. label: '"Not begun" Color', // Appears next to field
  213. type: "color", // Makes this setting a color field
  214. default: "#ffffff", // Default value if user doesn't change it
  215. },
  216. // This is the id of the field
  217. colorCompleted: {
  218. label: '"Completed" Color', // Appears next to field
  219. type: "color", // Makes this setting a color field
  220. default: "#76eb99", // Default value if user doesn't change it
  221. },
  222. // This is the id of the field
  223. colorProgress: {
  224. label: '"In progress" Color', // Appears next to field
  225. type: "color", // Makes this setting a color field
  226. default: "#ffec8a", // Default value if user doesn't change it
  227. },
  228. // This is the id of the field
  229. colorAbandoned: {
  230. label: '"Abandoned" Color', // Appears next to field
  231. type: "color", // Makes this setting a color field
  232. default: "#ff7a7a", // Default value if user doesn't change it
  233. },
  234. // This is the id of the field
  235. blurGroups: {
  236. section: ['Blur (18+ content)', 'Blur all covers from specified groups (separated by ";") (ex. Naruto;One Piece)'],
  237. type: "string", // Makes this setting a text field
  238. default: "", // Default value if user doesn't change it
  239. },
  240. // This is the id of the field
  241. noBlurOnHover: {
  242. label: 'Disable blur on hover', // Appears next to field
  243. type: "checkbox", // Makes this setting a checkbox field
  244. default: false, // Default value if user doesn't change it
  245. },
  246. copySettings:
  247. {
  248. section: ['Import/Export'],
  249. 'label': 'Copy settings', // Appears on the button
  250. 'type': 'button', // Makes this setting a button input
  251. 'size': 100, // Control the size of the button (default is 25)
  252. 'click': function() { // Function to call when button is clicked
  253. const result = Object.values(gmc.fields).map(item => ({
  254. id: item.id,
  255. value: item.value
  256. }));
  257. copyToClipboard(JSON.stringify(result))
  258. }
  259. },
  260. pasteSettings:
  261. {
  262. 'label': 'Paste settings', // Appears on the button
  263. 'type': 'button', // Makes this setting a button input
  264. 'size': 100, // Control the size of the button (default is 25)
  265. 'click': async function() { // Function to call when button is clicked
  266. const settings = await readFromClipboard();
  267. let options = [];
  268.  
  269. try {
  270. options = JSON.parse(settings);
  271. } catch {
  272. return;
  273. }
  274.  
  275. // Loop each settings and save
  276. options.forEach(el => {
  277. gmc.set(el.id, el.value);
  278. });
  279. // Save settings
  280. gmc.save();
  281. }
  282. }
  283. },
  284. css: config_panel_css,
  285. frame: config_container,
  286. // Callback functions object
  287. events: {
  288. init: function () {
  289. let css = generateCSS(this);
  290. setCustomStyle(css);
  291. loadBlurredCovers(this);
  292. },
  293. save: function () {
  294. let css = generateCSS(this);
  295. setCustomStyle(css);
  296. loadBlurredCovers(this);
  297. this.close();
  298. },
  299. },
  300. });
  301.  
  302. // Apply blur to initial loaded covers
  303. const loadBlurredCovers = function(GM_settings) {
  304. if (GM_settings == null) GM_settings = gmc;
  305.  
  306. // Remove class from loaded items before apply
  307. const blurred_items = document.getElementsByClassName("cover-blur");
  308. Array.from(blurred_items).forEach(el => el.classList.remove("cover-blur"));
  309.  
  310. const blur_groups_string = GM_settings.get("blurGroups");
  311. blur_groups = blur_groups_string.split(";");
  312.  
  313. // Foreach word in blur_group, search elements
  314. blur_groups.forEach(word => {
  315. const divs = document.getElementsByClassName('item-group');
  316. Array.from(divs).forEach(item => {
  317. if (item.firstChild.textContent.trim() === word) {
  318. // Add class to first child of parent (this will probably break at some point)
  319. const parent = item.parentNode;
  320. if (parent && parent.firstChild) {
  321. parent.firstChild.classList.add("cover-blur");
  322. }
  323. }
  324. });
  325. });
  326. };
  327.  
  328. const generateCSS = function (GM_settings) {
  329. if (GM_settings == null) GM_settings = gmc;
  330.  
  331. const not_begun_color = GM_settings.get("colorNotBegun");
  332. const completed_color = GM_settings.get("colorCompleted");
  333. const in_progress_color = GM_settings.get("colorProgress");
  334. const abandoned_color = GM_settings.get("colorAbandoned");
  335. const no_blur_on_hover = GM_settings.get("noBlurOnHover");
  336.  
  337. let css_style = "";
  338. // Make libib buttons still clickable
  339. css_style += `
  340. .quick-edit-link{
  341. z-index: 10;
  342. }
  343. .quick-blur-link{
  344. position: absolute;
  345. height: 24px;
  346. width: 24px;
  347. top: 5px;
  348. left: 5px;
  349. border: none;
  350. background-color: #fff;
  351. background-image: url(/img/library/icons/icon-flag-item.svg);
  352. opacity: 0;
  353. border-radius: 100px;
  354. transition: all 0.3s ease-in-out;
  355. cursor: pointer;
  356. text-indent: -99999px;
  357. z-index: 10;
  358. }
  359. .item.cover:hover .quick-blur-link {
  360. opacity: 1;
  361. }
  362. .batch-select{
  363. z-index: 10;
  364. }
  365. .cover-blur{
  366. overflow: hidden;
  367. }
  368. .cover-blur img{
  369. filter: blur(8px);
  370. }`;
  371. // Disable blur on cover hover
  372. if(no_blur_on_hover){
  373. css_style += `
  374. .cover-blur:hover img{
  375. filter: blur(0px);
  376. }`;
  377. }
  378. // Set the save, close and reset buttons color to white id dark mode
  379. css_style += `
  380. body.dark #libib_status_config_resetLink,body.dark #libib_status_config_saveBtn,body.dark #libib_status_config_closeBtn{
  381. color:white!important
  382. }`;
  383.  
  384. // Triangle style
  385. if (GM_settings.get("type") == "Triangle") {
  386. let triangle_position = GM_settings.get("trianglePosition");
  387. if (triangle_position == "Top left") {
  388. css_style += `
  389. .cover .completed.cover-wrapper::after {
  390. border-left-color: ${completed_color};
  391. border-top-color: ${completed_color};
  392. }
  393. .cover .in-progress.cover-wrapper::after {
  394. border-left-color: ${in_progress_color};
  395. border-top-color: ${in_progress_color};
  396. }
  397. .cover .abandoned.cover-wrapper::after {
  398. border-left-color: ${abandoned_color};
  399. border-top-color: ${abandoned_color};
  400. }
  401. .cover .not-begun.cover-wrapper::after {
  402. border-left-color: ${not_begun_color};
  403. border-top-color: ${not_begun_color};
  404. }
  405. `;
  406. } else if (triangle_position == "Top right") {
  407. css_style += `
  408. .cover .cover-wrapper::after{
  409. right: 0;
  410. left: auto;
  411. }
  412. .cover .completed.cover-wrapper::after {
  413. border-left-color: transparent;
  414. border-right-color: ${completed_color};
  415. border-top-color: ${completed_color};
  416. }
  417. .cover .in-progress.cover-wrapper::after {
  418. border-left-color: transparent;
  419. border-right-color: ${in_progress_color};
  420. border-top-color: ${in_progress_color};
  421. }
  422. .cover .abandoned.cover-wrapper::after {
  423. border-left-color: transparent;
  424. border-right-color: ${abandoned_color};
  425. border-top-color: ${abandoned_color};
  426. }
  427. .cover .not-begun.cover-wrapper::after {
  428. border-left-color: transparent;
  429. border-right-color: ${not_begun_color};
  430. border-top-color: ${not_begun_color};
  431. }
  432. `;
  433. } else if (triangle_position == "Bottom left") {
  434. css_style += `
  435. .cover .cover-wrapper::after{
  436. bottom: 0;
  437. top: auto;
  438. }
  439. .cover .completed.cover-wrapper::after {
  440. border-top-color: transparent;
  441. border-left-color: ${completed_color};
  442. border-bottom-color: ${completed_color};
  443. }
  444. .cover .in-progress.cover-wrapper::after {
  445. border-top-color: transparent;
  446. border-left-color: ${in_progress_color};
  447. border-bottom-color: ${in_progress_color};
  448. }
  449. .cover .abandoned.cover-wrapper::after {
  450. border-top-color: transparent;
  451. border-left-color: ${abandoned_color};
  452. border-bottom-color: ${abandoned_color};
  453. }
  454. .cover .not-begun.cover-wrapper::after {
  455. border-top-color: transparent;
  456. border-left-color: ${not_begun_color};
  457. border-bottom-color: ${not_begun_color};
  458. }
  459. `;
  460. } else if (triangle_position == "Bottom right") {
  461. css_style += `
  462. .cover .cover-wrapper::after{
  463. bottom: 0;
  464. top: auto;
  465. left: auto;
  466. right: 0;
  467. }
  468. .cover .completed.cover-wrapper::after {
  469. border-top-color: transparent;
  470. border-left-color: transparent;
  471. border-right-color: ${completed_color};
  472. border-bottom-color: ${completed_color};
  473. }
  474. .cover .in-progress.cover-wrapper::after {
  475. border-top-color: transparent;
  476. border-left-color: transparent;
  477. border-right-color: ${in_progress_color};
  478. border-bottom-color: ${in_progress_color};
  479. }
  480. .cover .abandoned.cover-wrapper::after {
  481. border-top-color: transparent;
  482. border-left-color: transparent;
  483. border-right-color: ${abandoned_color};
  484. border-bottom-color: ${abandoned_color};
  485. }
  486. .cover .not-begun.cover-wrapper::after {
  487. border-top-color: transparent;
  488. border-left-color: transparent;
  489. border-right-color: ${not_begun_color};
  490. border-bottom-color: ${not_begun_color};
  491. }
  492. `;
  493. }
  494. } else if (GM_settings.get("type") == "Border") {
  495. let border_position = GM_settings.get("borderPosition");
  496. let border_height = GM_settings.get("borderHeight");
  497. // The box-shadow prevents the click on the item, so it needs to be hidden on hover
  498. css_style += `
  499. .cover-wrapper {
  500. --shadow-y: ${
  501. border_position == "Top" ? `` : `-`
  502. }${border_height}px;
  503. }
  504. .cover-wrapper:hover::after {
  505. display:none!important;
  506. --shadow-y: 0px;
  507. transition: all 0.25s;
  508. transition-behavior: allow-discrete;
  509. }`;
  510.  
  511. css_style += `
  512. .cover .cover-wrapper::before, .cover .cover-wrapper::after {
  513. width: 100%;
  514. height: 100%;
  515. border-radius: 4px;
  516. display: block;
  517. border: none;
  518. z-index: 0;
  519. }
  520. .cover .completed.cover-wrapper::after {
  521. box-shadow: inset 0px var(--shadow-y) ${completed_color};
  522. }
  523. .cover .in-progress.cover-wrapper::after {
  524. box-shadow: inset 0px var(--shadow-y) ${in_progress_color};
  525. }
  526. .cover .abandoned.cover-wrapper::after {
  527. box-shadow: inset 0px var(--shadow-y) ${abandoned_color};
  528. }
  529. .cover .not-begun.cover-wrapper::after {
  530. box-shadow: inset 0px var(--shadow-y) ${not_begun_color};
  531. }
  532. `;
  533. }
  534. return css_style;
  535. };
  536.  
  537. const setCustomStyle = function (css) {
  538. // Remove existing style if present
  539. const existingStyle = document.getElementById(
  540. "libib-custom-status-indicator-style"
  541. );
  542. if (existingStyle != null) {
  543. existingStyle.remove();
  544. }
  545.  
  546. // Add style tag to document
  547. document.head.append(
  548. Object.assign(document.createElement("style"), {
  549. type: "text/css",
  550. id: "libib-custom-status-indicator-style",
  551. textContent: css,
  552. })
  553. );
  554. };
  555.  
  556. // Add the item group to the 'blurGroups' if not present, if already present remove it
  557. const toggleBlurForGroup = (div) => {
  558. // Search for the span containing the item group
  559. const span = div.target.parentNode.parentNode.querySelectorAll(".item-group>span");
  560. if(span.length != 1) return;
  561.  
  562. // Create array from blurredGroups string
  563. let blurred_groups_string = gmc.get("blurGroups");
  564. if(blurred_groups_string == null) return;
  565. let blurred_groups = blurred_groups_string.split(";");
  566. let item_group = span[0].innerText;
  567. const index = blurred_groups.indexOf(item_group);
  568. // If item found, remove it
  569. if(index > -1) blurred_groups.splice(index, 1);
  570. // If not found, add to array
  571. else blurred_groups.push(item_group);
  572.  
  573. // Save to settings
  574. gmc.set("blurGroups", blurred_groups.join(";"));
  575. gmc.save();
  576. }
  577. // Create the button for flagging groups to blur
  578. const createSetBlurButton = () => {
  579. const newDiv = document.createElement("div");
  580. newDiv.classList.add("quick-blur-link");
  581. newDiv.title = "Toggle blur for group";
  582. newDiv.addEventListener("click",toggleBlurForGroup);
  583. return newDiv;
  584. }
  585. // Check if the group of this item needs to be blurred
  586. const divNeedsBlur = (div) => {
  587. const spans = div.querySelectorAll('span');
  588. return Array.from(spans).some(span => span.parentNode.classList.contains("item-group") && blur_groups.includes(span.textContent.trim()) );
  589. }
  590. const divIsCover = (div) => {
  591. if(div.classList.contains("cover")) return true;
  592. return false;
  593. }
  594.  
  595. // Run when new books get loaded on the page
  596. // Check new nodes
  597. function findDivInNode(node) {
  598. if (node.nodeType === Node.ELEMENT_NODE) {
  599. if (node.tagName.toLowerCase() === 'div' && divIsCover(node)) {
  600. node.firstChild.appendChild(createSetBlurButton());
  601. if(divNeedsBlur(node)){
  602. node.firstChild.classList.add("cover-blur");
  603. }
  604. }
  605. }
  606. }
  607.  
  608. // Setup observer
  609. const blur_observer = new MutationObserver(mutations => {
  610. for (const mutation of mutations) {
  611. for (const node of mutation.addedNodes) {
  612. findDivInNode(node);
  613. }
  614. }
  615. });
  616.  
  617. // Start observer
  618. blur_observer.observe(document.body, { childList: true, subtree: true });
  619.  
  620. })();