1
0
Files
Userscripts/hitomi-language-filter.user.js

268 lines
9.6 KiB
JavaScript

// ==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();
}
})();