diff --git a/gui-hosts-app/Image_1.png b/gui-hosts-app/Image_1.png new file mode 100644 index 00000000000..dff327e8750 Binary files /dev/null and b/gui-hosts-app/Image_1.png differ diff --git a/gui-hosts-app/Image_2.png b/gui-hosts-app/Image_2.png new file mode 100644 index 00000000000..525b49acfbb Binary files /dev/null and b/gui-hosts-app/Image_2.png differ diff --git a/gui-hosts-app/lang/en.json b/gui-hosts-app/lang/en.json new file mode 100644 index 00000000000..a70792732a0 --- /dev/null +++ b/gui-hosts-app/lang/en.json @@ -0,0 +1,43 @@ +{ + "app_title": "🛡️ Hosts Generator", + "app_subtitle": "Custom hosts file generator", + "last_update": "Last update", + "open_folder": "📁 Open Folder", + "update": "🔄 Update", + "language": "🌐 Language", + "step1_title": "Hosts Sources", + "step1_description": "Download hosts sources from trusted repositories", + "checking_sources": "Checking sources...", + "download_sources": "📥 Download Sources", + "update_sources": "🔄 Update Sources", + "downloading": "Downloading...", + "preparing_download": "Preparing download...", + "step2_title": "Select Extensions", + "step2_description": "Choose which categories of sites to block", + "select_all": "✅ Select All", + "clear_all": "❌ Clear All", + "generate_title": "Generate Hosts File", + "generate_description": "Create your custom hosts file with selected extensions", + "generate_button": "⚙️ Generate Hosts File", + "generating": "Generating...", + "output_files": "Generated Files", + "no_files_generated": "No files generated yet", + "generate_first_file": "Generate your first hosts file", + "available": "✅ Available", + "missing": "❌ Missing", + "not_available": "Not available", + "all_sources_available": "✅ All sources available", + "missing_sources": "⚠️ Missing {count} sources", + "sources_downloaded": "✅ Sources downloaded successfully", + "sources_updated": "✅ Sources updated successfully", + "download_error": "❌ Download error", + "data_updated": "✅ Data updated", + "file_generated": "✅ File generated", + "select_extension_warning": "⚠️ Select at least one extension", + "error": "Error", + "base_hosts_desc": "Base hosts (adware + malware)", + "fakenews_desc": "Fake news sites", + "gambling_desc": "Gambling sites", + "porn_desc": "Adult content sites", + "social_desc": "Social media sites" +} \ No newline at end of file diff --git a/gui-hosts-app/lang/es.json b/gui-hosts-app/lang/es.json new file mode 100644 index 00000000000..2c4367aaf1e --- /dev/null +++ b/gui-hosts-app/lang/es.json @@ -0,0 +1,43 @@ +{ + "app_title": "🛡️ Host Generator", + "app_subtitle": "Generador de archivos hosts personalizados", + "last_update": "Última actualización", + "open_folder": "📁 Abrir Carpeta", + "update": "🔄 Actualizar", + "language": "🌐 Idioma", + "step1_title": "Fuentes de Hosts", + "step1_description": "Descarga las fuentes de hosts desde repositorios confiables", + "checking_sources": "Verificando fuentes...", + "download_sources": "📥 Descargar Fuentes", + "update_sources": "🔄 Actualizar Fuentes", + "downloading": "Descargando...", + "preparing_download": "Preparando descarga...", + "step2_title": "Seleccionar Extensiones", + "step2_description": "Elige qué categorías de sitios bloquear", + "select_all": "✅ Seleccionar Todo", + "clear_all": "❌ Limpiar Todo", + "generate_title": "Generar Archivo Hosts", + "generate_description": "Crea tu archivo hosts personalizado con las extensiones seleccionadas", + "generate_button": "⚙️ Generar Archivo Hosts", + "generating": "Generando...", + "output_files": "Archivos Generados", + "no_files_generated": "No hay archivos generados aún", + "generate_first_file": "Genera tu primer archivo hosts", + "available": "✅ Disponible", + "missing": "❌ Faltante", + "not_available": "No disponible", + "all_sources_available": "✅ Todas las fuentes disponibles", + "missing_sources": "⚠️ Faltan {count} fuentes", + "sources_downloaded": "✅ Fuentes descargadas exitosamente", + "sources_updated": "✅ Fuentes actualizadas exitosamente", + "download_error": "❌ Error en la descarga", + "data_updated": "✅ Datos actualizados", + "file_generated": "✅ Archivo generado", + "select_extension_warning": "⚠️ Selecciona al menos una extensión", + "error": "Error", + "base_hosts_desc": "Base hosts (adware + malware)", + "fakenews_desc": "Sitios de noticias falsas", + "gambling_desc": "Sitios de apuestas", + "porn_desc": "Contenido para adultos", + "social_desc": "Redes sociales" +} \ No newline at end of file diff --git a/gui-hosts-app/main.py b/gui-hosts-app/main.py new file mode 100644 index 00000000000..6ef1cd7ac8c --- /dev/null +++ b/gui-hosts-app/main.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Hosts Generator +Generador de archivos hosts personalizados con descarga de diferentes fuentes. +""" + +import os +import sys +import json +import requests +import webview +import threading +import time +import concurrent.futures +from pathlib import Path +from datetime import datetime +from urllib.parse import urlparse + +class HostsGenerator: + def __init__(self): + self.base_dir = Path(__file__).parent + self.data_dir = self.base_dir / "data" + self.output_dir = self.data_dir / "output" + self.sources_dir = self.data_dir / "hosts_sources" + self.lang_dir = self.base_dir / "lang" + + # URLs de fuentes de hosts (directas desde GitHub) + self.sources = { + "base": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", + "description": "Base hosts (adware + malware)" + }, + "fakenews": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts", + "description": "Fake news sites" + }, + "gambling": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/gambling/hosts", + "description": "Gambling sites" + }, + "porn": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/porn/hosts", + "description": "Adult content sites" + }, + "social": { + "url": "https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/social/hosts", + "description": "Social media sites" + } + } + + # Estado de descarga + self.download_status = { + "downloading": False, + "progress": 0, + "current_source": "", + "message": "" + } + + # Crear directorios necesarios + self.ensure_directories() + + def ensure_directories(self): + """Crear directorios necesarios""" + self.data_dir.mkdir(exist_ok=True) + self.output_dir.mkdir(exist_ok=True) + self.sources_dir.mkdir(exist_ok=True) + self.lang_dir.mkdir(exist_ok=True) + + def get_download_status(self): + """Obtener estado de descarga""" + return self.download_status + + def check_sources_exist(self): + """Verificar qué fuentes existen localmente""" + existing = [] + missing = [] + + for name in self.sources.keys(): + file_path = self.sources_dir / f"{name}.txt" + if file_path.exists(): + existing.append(name) + else: + missing.append(name) + + return { + "existing": len(existing), + "total": len(self.sources), + "missing": missing, + "all_exist": len(missing) == 0 + } + + def download_sources_async(self): + """Descargar fuentes en hilo separado""" + thread = threading.Thread(target=self.download_sources) + thread.daemon = True + thread.start() + return {"success": True, "message": "Descarga iniciada"} + + def download_sources(self): + """Descargar todas las fuentes faltantes""" + try: + self.download_status["downloading"] = True + self.download_status["progress"] = 0 + self.download_status["message"] = "Iniciando descarga..." + + # Verificar qué fuentes faltan + status = self.check_sources_exist() + if status["all_exist"]: + self.download_status["message"] = "Todas las fuentes ya están disponibles" + self.download_status["progress"] = 100 + return + + # Descargar solo las fuentes faltantes + sources_to_download = [(name, info) for name, info in self.sources.items() + if name in status["missing"]] + self._download_sources_list(sources_to_download) + + except Exception as e: + self.download_status["message"] = f"Error general: {str(e)}" + + finally: + self.download_status["downloading"] = False + + def get_available_extensions(self): + """Obtener extensiones disponibles (incluyendo base)""" + extensions = [] + for name, info in self.sources.items(): + file_path = self.sources_dir / f"{name}.txt" + extensions.append({ + "name": name, + "description": info["description"], + "available": file_path.exists(), + "size": file_path.stat().st_size if file_path.exists() else 0, + "is_base": name == "base" + }) + + return extensions + + def get_available_languages(self): + """Obtener idiomas disponibles""" + languages = [] + if self.lang_dir.exists(): + for lang_file in self.lang_dir.glob("*.json"): + lang_code = lang_file.stem + languages.append({ + "code": lang_code, + "name": "Español" if lang_code == "es" else "English" if lang_code == "en" else lang_code.upper() + }) + return languages + + def get_language_strings(self, lang_code="es"): + """Obtener strings de idioma""" + lang_file = self.lang_dir / f"{lang_code}.json" + if lang_file.exists(): + try: + with open(lang_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"Error cargando idioma {lang_code}: {e}") + + # Fallback a español si no se encuentra el idioma + fallback_file = self.lang_dir / "es.json" + if fallback_file.exists(): + try: + with open(fallback_file, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception: + pass + + # Fallback hardcoded si no hay archivos de idioma + return { + "app_title": "🛡️ Generador de Hosts", + "app_subtitle": "Generador de archivos hosts personalizados", + "error": "Error" + } + + def update_sources_async(self): + """Actualizar fuentes existentes en hilo separado""" + thread = threading.Thread(target=self.update_sources) + thread.daemon = True + thread.start() + return {"success": True, "message": "Actualización iniciada"} + + def update_sources(self): + """Actualizar todas las fuentes existentes""" + try: + self.download_status["downloading"] = True + self.download_status["progress"] = 0 + self.download_status["message"] = "Iniciando actualización..." + + # Descargar todas las fuentes (actualizar) + sources_to_download = list(self.sources.items()) + self._download_sources_list(sources_to_download) + + except Exception as e: + self.download_status["message"] = f"Error general: {str(e)}" + + finally: + self.download_status["downloading"] = False + + def _download_sources_list(self, sources_to_download): + """Método auxiliar para descargar una lista de fuentes""" + total_sources = len(sources_to_download) + completed = 0 + + def download_single_source(source_data): + nonlocal completed + name, source_info = source_data + + try: + self.download_status["current_source"] = f"Descargando {name}..." + print(f"Descargando {name} desde {source_info['url']}") + + # Configuración optimizada para requests + session = requests.Session() + session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }) + + response = session.get( + source_info['url'], + timeout=45, + stream=True, # Para archivos grandes + allow_redirects=True + ) + response.raise_for_status() + + file_path = self.sources_dir / f"{name}.txt" + with open(file_path, 'w', encoding='utf-8') as f: + f.write(response.text) + + completed += 1 + progress = int((completed / total_sources) * 100) + self.download_status["progress"] = progress + + print(f"✓ {name} descargado exitosamente ({completed}/{total_sources})") + return True + + except Exception as e: + print(f"✗ Error descargando {name}: {e}") + self.download_status["message"] = f"Error en {name}: {str(e)}" + return False + + # Descargas concurrentes (máximo 5 simultáneas) + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + futures = [executor.submit(download_single_source, source) for source in sources_to_download] + + # Esperar a que todas terminen + concurrent.futures.wait(futures) + + self.download_status["progress"] = 100 + self.download_status["current_source"] = "Descarga completada" + self.download_status["message"] = f"Descargadas {completed}/{total_sources} fuentes exitosamente" + + def generate_hosts_file(self, extensions): + """Generar archivo hosts personalizado""" + try: + # Verificar que la base existe + base_file = self.sources_dir / "base.txt" + if not base_file.exists(): + return { + "success": False, + "message": "Archivo base no encontrado. Descarga las fuentes primero." + } + + # Generar nombre único + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + if extensions: + ext_suffix = "_" + "_".join(sorted(extensions)) + else: + ext_suffix = "_base" + + output_filename = f"hosts_{timestamp}{ext_suffix}" + output_path = self.output_dir / output_filename + + # Leer archivo base + print(f"Leyendo archivo base: {base_file}") + with open(base_file, 'r', encoding='utf-8') as f: + base_content = f.read() + + # Combinar con extensiones + combined_content = base_content + extension_stats = [] + + for ext in extensions: + if ext == "base": # Skip base ya que siempre se incluye + continue + + ext_file = self.sources_dir / f"{ext}.txt" + if ext_file.exists(): + print(f"Agregando extensión: {ext}") + with open(ext_file, 'r', encoding='utf-8') as f: + ext_content = f.read() + + # Agregar separador y contenido + combined_content += f"\n\n# === {ext.upper()} EXTENSION ===\n" + combined_content += ext_content + + # Estadísticas + ext_lines = len(ext_content.splitlines()) + extension_stats.append(f"{ext}: {ext_lines:,} líneas") + else: + print(f"Advertencia: Extensión {ext} no encontrada") + + # Escribir archivo final + with open(output_path, 'w', encoding='utf-8') as f: + f.write(combined_content) + + # Estadísticas finales + file_size = output_path.stat().st_size + total_lines = len(combined_content.splitlines()) + + stats_message = f"Archivo generado exitosamente\n" + stats_message += f"Nombre: {output_filename}\n" + stats_message += f"Tamaño: {file_size:,} bytes\n" + stats_message += f"Líneas totales: {total_lines:,}\n" + if extension_stats: + stats_message += f"Extensiones: {', '.join(extension_stats)}" + else: + stats_message += "Extensiones: Solo base (adware + malware)" + + return { + "success": True, + "message": stats_message, + "filename": output_filename, + "path": str(output_path), + "size": file_size, + "lines": total_lines + } + + except Exception as e: + return { + "success": False, + "message": f"Error generando archivo: {str(e)}" + } + + def open_output_folder(self): + """Abrir carpeta de salida""" + try: + if sys.platform == "win32": + os.startfile(self.output_dir) + elif sys.platform == "darwin": + os.system(f"open '{self.output_dir}'") + else: + os.system(f"xdg-open '{self.output_dir}'") + return {"success": True, "message": "Carpeta abierta"} + except Exception as e: + return {"success": False, "message": f"Error: {str(e)}"} + + def get_output_files(self): + """Obtener lista de archivos generados""" + files = [] + if self.output_dir.exists(): + for file_path in self.output_dir.glob("hosts_*"): + if file_path.is_file(): + stat = file_path.stat() + files.append({ + "name": file_path.name, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S") + }) + + return sorted(files, key=lambda x: x["modified"], reverse=True) + +def main(): + """Función principal""" + print("🛡️ Iniciando Hosts Generator...") + + # Crear instancia de la aplicación + app = HostsGenerator() + + # Verificar archivo HTML + html_path = app.base_dir / "ui.html" + if not html_path.exists(): + print(f"Error: No se encontró ui.html en {html_path}") + input("Presiona Enter para salir...") + return + + print(f"📁 Directorio base: {app.base_dir}") + print(f"📁 Directorio de datos: {app.data_dir}") + print(f"📁 Directorio de salida: {app.output_dir}") + + # Verificar estado de fuentes + sources_status = app.check_sources_exist() + if sources_status["all_exist"]: + print(f"✅ Todas las fuentes están disponibles ({sources_status['existing']}/{sources_status['total']})") + else: + print(f"⚠️ Faltan {len(sources_status['missing'])} fuentes de {sources_status['total']}") + print(f" Faltantes: {', '.join(sources_status['missing'])}") + + # Crear ventana de la aplicación + print("🚀 Iniciando interfaz web...") + + webview.create_window( + title="🛡️ Hosts Generator", + url=str(html_path), + js_api=app, + width=1024, # Cambia este valor para el ancho (actualmente 1024) + height=900, # Cambia este valor para el alto (actualmente 750) + min_size=(800, 600), # Tamaño mínimo (actualmente 700x500) + resizable=True + ) + + webview.start(debug=False) + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n👋 Aplicación cerrada por el usuario") + except Exception as e: + print(f"❌ Error fatal: {e}") + input("Presiona Enter para salir...") \ No newline at end of file diff --git a/gui-hosts-app/requirements.txt b/gui-hosts-app/requirements.txt new file mode 100644 index 00000000000..5b0b9fca651 --- /dev/null +++ b/gui-hosts-app/requirements.txt @@ -0,0 +1,3 @@ +# Dependencias mínimas para Hosts Generator +pywebview>=4.0.0 +requests>=2.25.0 \ No newline at end of file diff --git a/gui-hosts-app/run.bat b/gui-hosts-app/run.bat new file mode 100644 index 00000000000..e9088b256c2 --- /dev/null +++ b/gui-hosts-app/run.bat @@ -0,0 +1,16 @@ +@echo off +cd /d "%~dp0" +echo 🛡️ Iniciando Hosts Generator... +echo. +python main.py + +if errorlevel 1 ( + echo. + echo ❌ Error al ejecutar la aplicación. + echo. + echo Posibles soluciones: + echo 1. Instalar dependencias: pip install -r requirements.txt + echo 2. Verificar que Python esté instalado + echo. + pause +) \ No newline at end of file diff --git a/gui-hosts-app/script.js b/gui-hosts-app/script.js new file mode 100644 index 00000000000..45ef0d46617 --- /dev/null +++ b/gui-hosts-app/script.js @@ -0,0 +1,380 @@ +class HostsGeneratorApp { + constructor() { + this.extensions = []; + this.downloadInterval = null; + this.currentLanguage = 'es'; + this.strings = {}; + this.init(); + } + + async init() { + // Esperar a que pywebview esté disponible + await this.waitForPywebview(); + await this.loadLanguage(this.currentLanguage); + this.setupEventListeners(); + this.updateLastUpdateTime(); + await this.checkSourcesStatus(); + } + + async waitForPywebview() { + return new Promise((resolve) => { + const checkPywebview = () => { + if (window.pywebview && window.pywebview.api) { + resolve(); + } else { + setTimeout(checkPywebview, 100); + } + }; + checkPywebview(); + }); + } + + setupEventListeners() { + document.getElementById('download-btn').addEventListener('click', () => this.downloadSources()); + document.getElementById('update-btn').addEventListener('click', () => this.updateSources()); + document.getElementById('language-selector').addEventListener('change', (e) => this.changeLanguage(e.target.value)); + } + + async loadLanguage(langCode) { + try { + this.strings = await pywebview.api.get_language_strings(langCode); + this.currentLanguage = langCode; + this.updateUI(); + document.getElementById('language-selector').value = langCode; + } catch (error) { + console.error('Error loading language:', error); + } + } + + async changeLanguage(langCode) { + await this.loadLanguage(langCode); + this.showNotification(this.getString('data_updated'), 'success'); + } + + getString(key, params = {}) { + let str = this.strings[key] || key; + Object.keys(params).forEach(param => { + str = str.replace(`{${param}}`, params[param]); + }); + return str; + } + + updateUI() { + document.querySelectorAll('[data-i18n]').forEach(element => { + const key = element.getAttribute('data-i18n'); + element.textContent = this.getString(key); + }); + + // Actualizar placeholders y otros textos dinámicos + if (this.extensions.length > 0) { + this.renderExtensions(); + } + } + + updateLastUpdateTime() { + const now = new Date(); + const timeString = now.toLocaleTimeString('es-ES', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); + document.getElementById('last-update').innerHTML = `${this.getString('last_update')}: ${timeString}`; + } + + async checkSourcesStatus() { + try { + const status = await pywebview.api.check_sources_exist(); + const sourcesStep = document.getElementById('sources-step'); + const sourcesStatus = document.getElementById('sources-status'); + const downloadBtn = document.getElementById('download-btn'); + const updateBtn = document.getElementById('update-btn'); + + if (status.all_exist) { + sourcesStep.className = 'step-card success'; + sourcesStatus.textContent = `${this.getString('all_sources_available')} (${status.existing}/${status.total})`; + downloadBtn.style.display = 'none'; + updateBtn.style.display = 'inline-flex'; + this.showExtensionsSection(); + } else { + sourcesStep.className = 'step-card warning'; + sourcesStatus.textContent = `${this.getString('missing_sources', {count: status.missing.length})}: ${status.missing.join(', ')}`; + downloadBtn.style.display = 'inline-flex'; + updateBtn.style.display = 'none'; + } + } catch (error) { + this.showNotification(this.getString('error') + ': ' + error.message, 'error'); + } + } + + async downloadSources() { + await this.performDownload('download_sources_async', this.getString('downloading')); + } + + async updateSources() { + await this.performDownload('update_sources_async', this.getString('downloading')); + } + + async performDownload(apiMethod, buttonText) { + const downloadBtn = document.getElementById('download-btn'); + const updateBtn = document.getElementById('update-btn'); + const progressDiv = document.getElementById('download-progress'); + + downloadBtn.disabled = true; + updateBtn.disabled = true; + downloadBtn.innerHTML = ` ${buttonText}`; + updateBtn.innerHTML = ` ${buttonText}`; + progressDiv.style.display = 'block'; + + try { + await pywebview.api[apiMethod](); + + this.downloadInterval = setInterval(async () => { + const status = await pywebview.api.get_download_status(); + + document.getElementById('download-message').textContent = status.current_source; + document.getElementById('progress-fill').style.width = status.progress + '%'; + document.getElementById('progress-text').textContent = status.progress + '%'; + + if (!status.downloading) { + clearInterval(this.downloadInterval); + + if (status.progress === 100) { + const message = apiMethod === 'update_sources_async' ? + this.getString('sources_updated') : this.getString('sources_downloaded'); + this.showNotification(message, 'success'); + setTimeout(() => this.checkSourcesStatus(), 1000); + } else { + this.showNotification(this.getString('download_error') + ': ' + status.message, 'error'); + } + + downloadBtn.disabled = false; + updateBtn.disabled = false; + downloadBtn.innerHTML = this.getString('download_sources'); + updateBtn.innerHTML = this.getString('update_sources'); + progressDiv.style.display = 'none'; + } + }, 250); + + } catch (error) { + this.showNotification(this.getString('error') + ': ' + error.message, 'error'); + downloadBtn.disabled = false; + updateBtn.disabled = false; + downloadBtn.innerHTML = this.getString('download_sources'); + updateBtn.innerHTML = this.getString('update_sources'); + progressDiv.style.display = 'none'; + } + } + + async showExtensionsSection() { + document.getElementById('extensions-section').classList.remove('hidden'); + document.getElementById('generate-section').classList.remove('hidden'); + await this.loadExtensions(); + this.loadOutputFiles(); + } + + async loadExtensions() { + try { + this.extensions = await pywebview.api.get_available_extensions(); + this.renderExtensions(); + } catch (error) { + this.showNotification(this.getString('error') + ': ' + error.message, 'error'); + } + } + + renderExtensions() { + const grid = document.getElementById('extensions-grid'); + grid.innerHTML = ''; + + // Ordenar extensiones: base primero, luego el resto + const sortedExtensions = this.extensions.sort((a, b) => { + if (a.is_base) return -1; + if (b.is_base) return 1; + return a.name.localeCompare(b.name); + }); + + sortedExtensions.forEach(ext => { + const card = document.createElement('div'); + const cardClasses = ['extension-card']; + + if (!ext.available) cardClasses.push('unavailable'); + if (ext.is_base) cardClasses.push('base-extension'); + + card.className = cardClasses.join(' '); + + // Convertir tamaño a MB + const sizeText = ext.available ? `${(ext.size / (1024 * 1024)).toFixed(2)} MB` : this.getString('not_available'); + const statusText = ext.available ? this.getString('available') : this.getString('missing'); + + card.innerHTML = ` +
+ + ${ext.name} + ${ext.is_base ? 'BASE' : ''} +
+
${this.getExtensionDescription(ext.name)}
+
+ 📊 ${sizeText} + ${statusText} +
+ `; + + if (ext.available) { + if (ext.is_base) { + card.classList.add('selected'); + card.addEventListener('click', () => { + const checkbox = card.querySelector('input'); + checkbox.checked = !checkbox.checked; + card.classList.toggle('selected', checkbox.checked); + }); + } else { + card.addEventListener('click', () => { + const checkbox = card.querySelector('input'); + checkbox.checked = !checkbox.checked; + card.classList.toggle('selected', checkbox.checked); + }); + } + } + + grid.appendChild(card); + }); + } + + getExtensionDescription(name) { + const descriptions = { + 'base': this.getString('base_hosts_desc'), + 'fakenews': this.getString('fakenews_desc'), + 'gambling': this.getString('gambling_desc'), + 'porn': this.getString('porn_desc'), + 'social': this.getString('social_desc') + }; + return descriptions[name] || name; + } + + selectAllExtensions() { + document.querySelectorAll('.extension-checkbox:not(:disabled)').forEach(checkbox => { + checkbox.checked = true; + checkbox.closest('.extension-card').classList.add('selected'); + }); + } + + clearAllExtensions() { + document.querySelectorAll('.extension-checkbox:not([id="ext-base"])').forEach(checkbox => { + checkbox.checked = false; + checkbox.closest('.extension-card').classList.remove('selected'); + }); + // Mantener base seleccionado + const baseCheckbox = document.getElementById('ext-base'); + if (baseCheckbox) { + baseCheckbox.checked = false; + baseCheckbox.closest('.extension-card').classList.add('selected'); + } + } + + async generateHostsFile() { + const selectedExtensions = []; + document.querySelectorAll('.extension-checkbox:checked').forEach(checkbox => { + selectedExtensions.push(checkbox.id.replace('ext-', '')); + }); + + if (selectedExtensions.length === 0) { + this.showNotification(this.getString('select_extension_warning'), 'warning'); + return; + } + + const generateBtn = document.getElementById('generate-btn'); + generateBtn.disabled = true; + generateBtn.innerHTML = ` ${this.getString('generating')}`; + + try { + const result = await pywebview.api.generate_hosts_file(selectedExtensions); + + if (result.success) { + this.showNotification(`${this.getString('file_generated')}: ${result.filename}`, 'success'); + this.loadOutputFiles(); + } else { + this.showNotification(this.getString('error') + ': ' + result.message, 'error'); + } + } catch (error) { + this.showNotification(this.getString('error') + ': ' + error.message, 'error'); + } finally { + generateBtn.disabled = false; + generateBtn.innerHTML = this.getString('generate_button'); + } + } + + async loadOutputFiles() { + try { + const files = await pywebview.api.get_output_files(); + const container = document.getElementById('output-files'); + + if (files.length === 0) { + container.innerHTML = ` +
+
+ 📄 +
+
${this.getString('no_files_generated')}
+
${this.getString('generate_first_file')}
+
+
+
+ `; + return; + } + + container.innerHTML = files.map(file => { + const sizeMB = (file.size / (1024 * 1024)).toFixed(2); + return ` +
+
+ 📄 +
+
${file.name}
+
${file.modified} • ${sizeMB} MB
+
+
+
+ `; + }).join(''); + } catch (error) { + this.showNotification(this.getString('error') + ': ' + error.message, 'error'); + } + } + + async openOutputFolder() { + try { + await pywebview.api.open_output_folder(); + } catch (error) { + this.showNotification(this.getString('error') + ': ' + error.message, 'error'); + } + } + + async refreshAll() { + this.updateLastUpdateTime(); + await this.checkSourcesStatus(); + if (this.extensions.length > 0) { + await this.loadExtensions(); + } + await this.loadOutputFiles(); + this.showNotification(this.getString('data_updated'), 'success'); + } + + showNotification(message, type = 'success') { + const notification = document.getElementById('notification'); + notification.textContent = message; + notification.className = `notification ${type}`; + notification.classList.add('show'); + + setTimeout(() => { + notification.classList.remove('show'); + }, 4000); + } +} + +// Crear instancia global de la aplicación +let app; + +// Inicializar cuando el DOM esté listo +document.addEventListener('DOMContentLoaded', () => { + app = new HostsGeneratorApp(); +}); \ No newline at end of file diff --git a/gui-hosts-app/styles.css b/gui-hosts-app/styles.css new file mode 100644 index 00000000000..6b06caa0378 --- /dev/null +++ b/gui-hosts-app/styles.css @@ -0,0 +1,598 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; + display: flex; + flex-direction: column; +} + +.container { + max-width: 1200px; + margin: 0 auto; + background: white; + border-radius: 15px; + box-shadow: 0 20px 40px rgba(0,0,0,0.15); + overflow: hidden; + flex: 1; + display: flex; + flex-direction: column; +} + +.header { + background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); + color: white; + padding: 25px 30px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.header h1 { + font-size: 1.8em; + font-weight: 600; +} + +.header p { + opacity: 0.9; + font-size: 0.95em; + margin-top: 2px; +} + +.header-right { + display: flex; + gap: 10px; + align-items: center; +} + +.last-update { + font-size: 0.8em; + opacity: 0.8; + margin-right: 15px; +} + +.header-btn { + background: rgba(255,255,255,0.2); + border: 1px solid rgba(255,255,255,0.3); + color: white; + padding: 8px 16px; + border-radius: 6px; + font-size: 0.85em; + cursor: pointer; + transition: all 0.2s; +} + +.header-btn:hover { + background: rgba(255,255,255,0.3); +} + +.language-selector { + background: rgba(255,255,255,0.2); + border: 1px solid rgba(255,255,255,0.3); + color: #303030; + padding: 8px 12px; + border-radius: 6px; + font-size: 0.85em; + cursor: pointer; + transition: all 0.2s; +} + +.language-selector:hover { + background: rgba(255,255,255,0.3); +} + +.content { + padding: 60px; + flex: 1; + display: flex; + flex-direction: column; +} + +.step-card { + background: #f8f9fa; + border-radius: 12px; + padding: 25px; + border-left: 4px solid #3498db; + margin-bottom: 20px; + transition: all 0.3s; +} + +.step-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0,0,0,0.1); +} + +.step-card.success { + border-left-color: #27ae60; + background: #eafaf1; +} + +.step-card.warning { + border-left-color: #f39c12; + background: #fef9e7; +} + +.step-card.error { + border-left-color: #e74c3c; + background: #fdf2f2; +} + +.step-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 15px; +} + +.step-number { + background: #3498db; + color: white; + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.9em; +} + +.step-card.success .step-number { + background: #27ae60; +} + +.step-card.warning .step-number { + background: #f39c12; +} + +.step-title { + font-size: 1.1em; + font-weight: 600; + color: #2c3e50; +} + +.step-description { + color: #5a6c7d; + margin-bottom: 15px; + line-height: 1.5; +} + +.step-status { + font-size: 0.9em; + margin-bottom: 15px; + padding: 8px 12px; + border-radius: 6px; + background: rgba(52, 152, 219, 0.1); + color: #2980b9; +} + +.step-card.success .step-status { + background: rgba(39, 174, 96, 0.1); + color: #27ae60; +} + +.step-card.warning .step-status { + background: rgba(243, 156, 18, 0.1); + color: #f39c12; +} + +.btn { + background: #3498db; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + font-size: 0.9em; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + display: inline-flex; + align-items: center; + gap: 8px; + margin-right: 10px; +} + +.btn:hover:not(:disabled) { + background: #2980b9; + transform: translateY(-1px); +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.btn-success { + background: #27ae60; +} + +.btn-success:hover:not(:disabled) { + background: #229954; +} + +.btn-secondary { + background: #95a5a6; +} + +.btn-secondary:hover:not(:disabled) { + background: #7f8c8d; +} + +.btn-warning { + background: #f39c12; +} + +.btn-warning:hover:not(:disabled) { + background: #e67e22; +} + +.extensions-section { + background: white; + border-radius: 12px; + border: 1px solid #e9ecef; + overflow: hidden; + margin-bottom: 20px; +} + +.extensions-header { + background: #f8f9fa; + padding: 20px 25px; + border-bottom: 1px solid #e9ecef; + display: flex; + justify-content: space-between; + align-items: center; +} + +.extensions-title { + font-size: 1.2em; + font-weight: 600; + color: #2c3e50; + display: flex; + align-items: center; + gap: 10px; +} + +.extensions-actions { + display: flex; + gap: 10px; + align-items: center; +} + +.extensions-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 15px; + padding: 25px; +} + +.extension-card { + border: 2px solid #e9ecef; + border-radius: 8px; + padding: 18px; + transition: all 0.2s; + cursor: pointer; + background: white; +} + +.extension-card:hover { + border-color: #3498db; + box-shadow: 0 4px 12px rgba(52, 152, 219, 0.15); +} + +.extension-card.selected { + border-color: #27ae60; + background: #f8fff9; + box-shadow: 0 4px 12px rgba(39, 174, 96, 0.15); +} + +.extension-card.unavailable { + opacity: 0.5; + cursor: not-allowed; + background: #f8f9fa; +} + +.extension-card.base-extension { + border-color: #e74c3c; + background: #fef9f9; +} + +.extension-card.base-extension.selected { + border-color: #c0392b; + background: #fdf2f2; +} + +.extension-header { + display: flex; + align-items: center; + gap: 15px; /* Aumentado de 12px */ + margin-bottom: 15px; /* Aumentado de 12px */ +} + +.extension-description { + color: #5a6c7d; + font-size: 0.9em; + margin-bottom: 15px; /* Aumentado de 10px */ + line-height: 1.6; /* Aumentado de 1.4 */ + padding: 0 5px; /* Añadir padding lateral */ +} + +.extension-stats { + display: flex; + justify-content: space-between; + font-size: 0.8em; + color: #7f8c8d; + margin-top: 8px; /* Añadir margen superior */ + padding: 0 5px; /* Añadir padding lateral */ +} + +.extension-card { + border: 2px solid #e9ecef; + border-radius: 8px; + padding: 22px; /* Aumentado de 18px */ + transition: all 0.2s; + cursor: pointer; + background: white; +} + +.extension-card:hover { + border-color: #3498db; + box-shadow: 0 4px 12px rgba(52, 152, 219, 0.15); +} + +.extension-card.selected { + border-color: #27ae60; + background: #f8fff9; + box-shadow: 0 4px 12px rgba(39, 174, 96, 0.15); +} + +.extension-card.unavailable { + opacity: 0.5; + cursor: not-allowed; + background: #f8f9fa; +} + +.extension-card.base-extension { + border-color: #e74c3c; + background: #fef9f9; +} + +.extension-card.base-extension.selected { + border-color: #c0392b; + background: #fdf2f2; +} + +.extension-checkbox { + width: 18px; + height: 18px; + accent-color: #27ae60; +} + +.extension-name { + font-weight: 600; + color: #2c3e50; + text-transform: capitalize; + font-size: 1.05em; +} + +.extension-description { + color: #5a6c7d; + font-size: 0.9em; + margin-bottom: 10px; + line-height: 1.4; +} + +.extension-stats { + display: flex; + justify-content: space-between; + font-size: 0.8em; + color: #7f8c8d; +} + +.base-badge { + background: #e74c3c; + color: white; + font-size: 0.7em; + padding: 2px 6px; + border-radius: 3px; + margin-left: 8px; +} + +.generate-section { + background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%); + color: white; + padding: 25px; + border-radius: 12px; + text-align: center; + margin-bottom: 20px; +} + +.generate-section h3 { + font-size: 1.3em; + margin-bottom: 10px; +} + +.generate-section p { + opacity: 0.9; + margin-bottom: 20px; +} + +.generate-btn { + background: white; + color: #27ae60; + font-size: 1em; + padding: 15px 35px; + font-weight: 600; + border-radius: 25px; /* Más redondeado */ + box-shadow: 0 4px 15px rgba(0,0,0,0.1); /* Sombra suave */ + transition: all 0.3s ease; + border: 2px solid transparent; +} + +.generate-btn:hover:not(:disabled) { + background: #f8f9fa; + transform: translateY(-3px); + box-shadow: 0 6px 20px rgba(0,0,0,0.15); + border-color: rgba(255,255,255,0.3); +} + +.progress-section { + background: #f8f9fa; + border-radius: 10px; + padding: 20px; + margin: 20px 0; + display: none; +} + +.progress-bar { + background: #e9ecef; + border-radius: 10px; + height: 12px; + overflow: hidden; + margin: 15px 0; +} + +.progress-fill { + background: linear-gradient(90deg, #3498db, #2980b9); + height: 100%; + width: 0%; + transition: width 0.3s ease; + border-radius: 10px; +} + +.progress-text { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9em; + color: #5a6c7d; +} + +.output-section { + background: #f8f9fa; + border-radius: 12px; + border: 1px solid #e9ecef; + padding: 20px; + margin-top: auto; +} + +.output-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.output-title { + font-size: 1.1em; + font-weight: 600; + color: #2c3e50; +} + +.file-item { + background: white; + padding: 15px; + border-radius: 8px; + border: 1px solid #e9ecef; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.file-info { + display: flex; + align-items: center; + gap: 12px; +} + +.file-name { + font-weight: 500; + color: #2c3e50; +} + +.file-meta { + font-size: 0.85em; + color: #7f8c8d; +} + +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + border-radius: 8px; + color: white; + font-weight: 500; + z-index: 1000; + transform: translateX(100%); + transition: transform 0.3s ease; + max-width: 400px; +} + +.notification.show { + transform: translateX(0); +} + +.notification.success { + background: #27ae60; +} + +.notification.error { + background: #e74c3c; +} + +.notification.warning { + background: #f39c12; +} + +.footer { + background: #2c3e50; + color: white; + text-align: center; + padding: 15px; + font-size: 0.9em; + opacity: 0.9; +} + +.loading { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid #f3f3f3; + border-top: 2px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.hidden { + display: none !important; +} + +@media (max-width: 768px) { + .extensions-grid { + grid-template-columns: 1fr; + } + + .header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .extensions-header { + flex-direction: column; + gap: 15px; + } +} \ No newline at end of file diff --git a/gui-hosts-app/ui.html b/gui-hosts-app/ui.html new file mode 100644 index 00000000000..ccfa077741c --- /dev/null +++ b/gui-hosts-app/ui.html @@ -0,0 +1,113 @@ + + + + + + Hosts Generator + + + +
+
+
+

🛡️ Host Generator

+

Generador de archivos hosts personalizados

+
+
+
Última actualización: --
+ + + +
+
+ +
+ +
+
+
1
+
Fuentes de Hosts
+
+
+ Descarga las fuentes de hosts desde repositorios confiables +
+
+ Verificando fuentes... +
+
+ + +
+ +
+
+ Preparando descarga... + 0% +
+
+
+
+
+
+ + + + + + + + +
+
+
Archivos Generados
+
+
+
+
+ 📄 +
+
No hay archivos generados aún
+
Genera tu primer archivo hosts
+
+
+
+
+
+
+ + +
+ + +
+ + + + \ No newline at end of file diff --git a/readme_template.md b/readme_template.md index 3141d92c8d8..c0b2407e9f1 100644 --- a/readme_template.md +++ b/readme_template.md @@ -82,9 +82,11 @@ maintain and provide for you. ## Generate your own unified hosts file -You have three options to generate your own hosts file. You can use our -container image, build your own image, or do it in your own environment. Option -#1 is easiest if you have Linux with Docker installed. +You have five options to generate your own hosts file. You can use our +container image, build your own image, do it in your own environment, use our +GUI application, or use Google Colab. Option #1 is easiest if you have Linux +with Docker installed, while Option #4 provides a user-friendly graphical +interface for all platforms. ### Option 1: Use our container image (Linux only) @@ -150,7 +152,34 @@ pip3 install --user -r requirements.txt at the user level. More information about it can be found on pip [documentation](https://pip.pypa.io/en/stable/reference/pip_install/?highlight=--user#cmdoption-user). -### Option 4: Generate it in Google Colab +### Option 4: Use the GUI Hosts Generator Application + +For users who prefer a graphical interface, we provide a user-friendly GUI application that simplifies the process of generating custom hosts files. + +**Features:** +- Easy-to-use graphical interface +- Select extensions (gambling, porn, fakenews, social) with checkboxes + +**Requirements:** +- Python 3.6 or later +- pywebview (for the web-based GUI interface) +- requests (for downloading hosts sources) + +**Installation and Usage:** + +1. Navigate to the `gui-hosts-app` directory in this repository +2. Install dependencies: + ```sh + pip install -r requirements.txt + ``` +3. Run the GUI application: + ```sh + python main.py + ``` + +The application will guide you through the process with an intuitive interface, making it perfect for users who prefer visual tools over command-line operations. + +### Option 5: Generate it in Google Colab Spin up a free remote [Google Colab](https://colab.research.google.com/drive/1tYWXpU2iuPDqN_o03JW9ml3ExO80eBLq?usp=sharing) environment.