// ==UserScript== // @name Hitomi.la Language Filter // @namespace https://git.ponywave.de/Akamaru/Userscripts // @version 2.0 // @description Filter manga by language on hitomi.la with improved integration // @author Akamaru // @match https://hitomi.la/* // @icon https://www.google.com/s2/favicons?domain=hitomi.la&sz=32 // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @updateURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/hitomi-language-filter.user.js // @downloadURL https://git.ponywave.de/Akamaru/Userscripts/raw/branch/master/hitomi-language-filter.user.js // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // Available languages (matching hitomi.la's language options) const languages = { 'all': 'All Languages', 'english': 'English', 'japanese': 'Japanese', 'chinese': 'Chinese', 'korean': 'Korean', 'spanish': 'Spanish', 'french': 'French', 'german': 'German', 'russian': 'Russian', 'italian': 'Italian', 'portuguese': 'Portuguese', 'thai': 'Thai', 'vietnamese': 'Vietnamese', 'polish': 'Polish', 'indonesian': 'Indonesian', 'turkish': 'Turkish', 'dutch': 'Dutch', 'norwegian': 'Norwegian', 'finnish': 'Finnish', 'swedish': 'Swedish', 'tagalog': 'Tagalog', 'arabic': 'Arabic', 'hebrew': 'Hebrew' }; // Get saved language preference (default: all) let selectedLanguage = GM_getValue('selectedLanguage', 'all'); // Create language selector UI integrated into the page function createLanguageSelector() { // Find the list-title section where "Order by" dropdown is located const listTitle = document.querySelector('.list-title'); if (!listTitle) { console.warn('Hitomi Language Filter: .list-title not found'); return; } // Make list-title use flexbox for horizontal layout listTitle.style.cssText = ` display: flex !important; flex-direction: row !important; align-items: center !important; justify-content: space-between !important; gap: 15px !important; flex-wrap: nowrap !important; `; // Fix h3 margin to prevent layout issues const h3 = listTitle.querySelector('h3'); if (h3) { h3.style.marginBottom = '0'; } // Create a wrapper for the dropdowns to keep them together on the right let dropdownWrapper = document.querySelector('.header-sort-wrapper'); if (!dropdownWrapper) { dropdownWrapper = document.createElement('div'); dropdownWrapper.className = 'header-sort-wrapper'; dropdownWrapper.style.cssText = ` display: flex !important; align-items: center !important; gap: 10px !important; `; } // Fix position and float of existing header-sort-select to work with flexbox const existingSort = document.querySelector('.header-sort-select'); if (existingSort) { existingSort.style.cssText = ` position: relative !important; float: none !important; top: auto !important; right: auto !important; `; // Move existing sort into wrapper if not already there if (existingSort.parentElement !== dropdownWrapper) { dropdownWrapper.appendChild(existingSort); } } // Create container for the filter const filterContainer = document.createElement('div'); filterContainer.id = 'language-filter-container'; filterContainer.className = 'header-sort-select'; filterContainer.style.cssText = ` position: relative !important; float: none !important; top: auto !important; right: auto !important; `; // Create select element (no label) const select = document.createElement('select'); select.id = 'language-filter-dropdown'; // Add options for (const [code, name] of Object.entries(languages)) { const option = document.createElement('option'); option.value = code; option.textContent = name; if (code === selectedLanguage) { option.selected = true; } select.appendChild(option); } // Handle language change select.addEventListener('change', function() { selectedLanguage = this.value; GM_setValue('selectedLanguage', selectedLanguage); filterGalleries(); updateVisibleCount(); }); filterContainer.appendChild(select); // Add filter to the dropdown wrapper dropdownWrapper.appendChild(filterContainer); // Add wrapper to list-title if not already there if (!dropdownWrapper.parentElement) { listTitle.appendChild(dropdownWrapper); } } // Get all gallery elements (including all types) function getAllGalleries() { return document.querySelectorAll('.gallery, .dj, .manga, .acg, .imageset'); } // Filter galleries based on selected language function filterGalleries() { const galleries = getAllGalleries(); galleries.forEach(gallery => { if (selectedLanguage === 'all') { gallery.style.display = ''; return; } // Check for language link in format /index-LANGUAGE.html const languageTag = gallery.querySelector('a[href*="/index-"][href$=".html"]'); if (!languageTag) { // Hide if no language tag found gallery.style.display = 'none'; return; } const href = languageTag.getAttribute('href'); const langMatch = href.match(/\/index-([^.]+)\.html/); if (langMatch && langMatch[1]) { const galleryLang = langMatch[1].toLowerCase(); gallery.style.display = (galleryLang === selectedLanguage) ? '' : 'none'; } else { gallery.style.display = 'none'; } }); } // Update visible gallery count (optional visual feedback) function updateVisibleCount() { const galleries = getAllGalleries(); const visible = Array.from(galleries).filter(g => g.style.display !== 'none').length; const total = galleries.length; // Update or create count display let countDisplay = document.getElementById('language-filter-count'); if (!countDisplay) { countDisplay = document.createElement('span'); countDisplay.id = 'language-filter-count'; countDisplay.style.cssText = 'margin-left: 10px; color: #666; font-size: 0.9em;'; const filterContainer = document.getElementById('language-filter-container'); if (filterContainer) { filterContainer.appendChild(countDisplay); } } if (selectedLanguage === 'all') { countDisplay.textContent = ''; } else { countDisplay.textContent = `(${visible}/${total})`; } } // Register menu command for quick access GM_registerMenuCommand('Reset Language Filter', function() { selectedLanguage = 'all'; GM_setValue('selectedLanguage', selectedLanguage); const select = document.getElementById('language-filter-dropdown'); if (select) { select.value = 'all'; } filterGalleries(); updateVisibleCount(); }); // Initialize function init() { // Wait a bit for the page to fully load setTimeout(() => { createLanguageSelector(); filterGalleries(); updateVisibleCount(); // Watch for dynamic content loading (e.g., infinite scroll) const observer = new MutationObserver(function(mutations) { // Only filter if gallery content changes for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { const hasGalleryNodes = Array.from(mutation.addedNodes).some(node => { if (node.nodeType === 1) { // Element node return node.classList.contains('dj') || node.classList.contains('manga') || node.classList.contains('acg') || node.classList.contains('imageset') || node.classList.contains('gallery'); } return false; }); if (hasGalleryNodes) { filterGalleries(); updateVisibleCount(); break; } } } }); const galleryContent = document.querySelector('.gallery-content'); if (galleryContent) { observer.observe(galleryContent, { childList: true, subtree: true }); } }, 100); } // Wait for page to load if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();