From f7dd8150a8ddfef3f3e51e82ad23829cf786d298 Mon Sep 17 00:00:00 2001 From: Akamaru Date: Wed, 3 Dec 2025 13:53:51 +0100 Subject: [PATCH] =?UTF-8?q?Hitomi.la=20Language=20Filter=20komplett=20?= =?UTF-8?q?=C3=BCberarbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- hitomi-language-filter.user.js | 219 ++++++++++++++++++++++++--------- 2 files changed, 164 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 7e66346..822cebb 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Sendet Magnet-Links und `.torrent`-Dateien direkt an einen Transmission-Server i Filtert Manga auf hitomi.la nach Sprache. Zeigt nur Inhalte in der ausgewählten Sprache an. **Funktionen:** -- Dropdown-Menü oben rechts zur Sprachauswahl +- Dropdown-Menü zur Sprachauswahl - Unterstützt 15+ Sprachen (Englisch, Japanisch, Koreanisch, Chinesisch, Deutsch, etc.) - Filtert Galerien automatisch und in Echtzeit - Speichert Spracheinstellung persistent (Standard: Englisch) diff --git a/hitomi-language-filter.user.js b/hitomi-language-filter.user.js index 3cb8422..ab767a0 100644 --- a/hitomi-language-filter.user.js +++ b/hitomi-language-filter.user.js @@ -1,8 +1,8 @@ // ==UserScript== // @name Hitomi.la Language Filter // @namespace https://git.ponywave.de/Akamaru/Userscripts -// @version 1.1 -// @description Filter manga by language on hitomi.la +// @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 @@ -17,7 +17,7 @@ (function() { 'use strict'; - // Available languages + // Available languages (matching hitomi.la's language options) const languages = { 'all': 'All Languages', 'english': 'English', @@ -34,39 +34,85 @@ 'vietnamese': 'Vietnamese', 'polish': 'Polish', 'indonesian': 'Indonesian', - 'turkish': 'Turkish' + 'turkish': 'Turkish', + 'dutch': 'Dutch', + 'norwegian': 'Norwegian', + 'finnish': 'Finnish', + 'swedish': 'Swedish', + 'tagalog': 'Tagalog', + 'arabic': 'Arabic', + 'hebrew': 'Hebrew' }; - // Get saved language preference (default: english) - let selectedLanguage = GM_getValue('selectedLanguage', 'english'); + // Get saved language preference (default: all) + let selectedLanguage = GM_getValue('selectedLanguage', 'all'); - // Create language selector UI + // Create language selector UI integrated into the page function createLanguageSelector() { - const container = document.createElement('div'); - container.id = 'language-filter-container'; - container.style.cssText = ` - position: fixed; - top: 10px; - right: 10px; - z-index: 10000; - background: #fff; - border: 2px solid #333; - border-radius: 5px; - padding: 10px; - box-shadow: 0 2px 10px rgba(0,0,0,0.3); + // 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; `; - const label = document.createElement('label'); - label.textContent = 'Language: '; - label.style.fontWeight = 'bold'; + // 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.style.cssText = ` - padding: 5px; - border: 1px solid #ccc; - border-radius: 3px; - margin-left: 5px; - `; + select.id = 'language-filter-dropdown'; // Add options for (const [code, name] of Object.entries(languages)) { @@ -84,17 +130,28 @@ selectedLanguage = this.value; GM_setValue('selectedLanguage', selectedLanguage); filterGalleries(); + updateVisibleCount(); }); - container.appendChild(label); - container.appendChild(select); - document.body.appendChild(container); + 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() { - // Find all gallery items (both classes used on the site) - const galleries = document.querySelectorAll('.gallery, .dj, .manga'); + const galleries = getAllGalleries(); galleries.forEach(gallery => { if (selectedLanguage === 'all') { @@ -105,6 +162,7 @@ // 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; } @@ -114,42 +172,91 @@ if (langMatch && langMatch[1]) { const galleryLang = langMatch[1].toLowerCase(); - if (galleryLang === selectedLanguage) { - gallery.style.display = ''; - } else { - gallery.style.display = 'none'; - } + gallery.style.display = (galleryLang === selectedLanguage) ? '' : 'none'; } else { gallery.style.display = 'none'; } }); } - // Register menu command for quick access - GM_registerMenuCommand('Change Language Filter', function() { - const newLang = prompt('Enter language code:\n' + - Object.entries(languages).map(([code, name]) => `${code} - ${name}`).join('\n')); - if (newLang && languages[newLang]) { - selectedLanguage = newLang; - GM_setValue('selectedLanguage', selectedLanguage); - location.reload(); + // 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() { - createLanguageSelector(); - filterGalleries(); - - // Watch for dynamic content loading - const observer = new MutationObserver(function(mutations) { + // Wait a bit for the page to fully load + setTimeout(() => { + createLanguageSelector(); filterGalleries(); - }); + updateVisibleCount(); - observer.observe(document.body, { - childList: true, - subtree: true - }); + // 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