Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Smart Web Extractor</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Custom CSS for things Tailwind can't handle */ | |
| .dark-mode { | |
| background-color: #1a202c; | |
| color: #f7fafc; | |
| } | |
| .light-mode { | |
| background-color: #f7fafc; | |
| color: #1a202c; | |
| } | |
| .popup-container { | |
| width: 350px; | |
| min-height: 200px; | |
| max-height: 600px; | |
| overflow-y: auto; | |
| } | |
| .tab-active { | |
| border-bottom: 3px solid #4299e1; | |
| } | |
| .highlight { | |
| background-color: rgba(255, 255, 0, 0.4); | |
| } | |
| .domain-badge { | |
| font-size: 0.75rem; | |
| border-radius: 9999px; | |
| } | |
| .slide-fade-enter-active { | |
| transition: all 0.3s ease-out; | |
| } | |
| .slide-fade-leave-active { | |
| transition: all 0.3s ease-in; | |
| } | |
| .slide-fade-enter-from, | |
| .slide-fade-leave-to { | |
| transform: translateY(10px); | |
| opacity: 0; | |
| } | |
| </style> | |
| </head> | |
| <body class="font-sans text-sm"> | |
| <div id="app" class="popup-container transition-colors duration-200" :class="isDarkMode ? 'dark-mode' : 'light-mode'"> | |
| <!-- Header with logo and theme toggle --> | |
| <div class="flex justify-between items-center p-4 border-b"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-globe-americas text-blue-500 text-xl mr-2"></i> | |
| <h1 class="font-bold text-lg">Smart Web Extractor</h1> | |
| </div> | |
| <button @click="toggleTheme" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"> | |
| <i :class="isDarkMode ? 'fas fa-sun text-yellow-300' : 'fas fa-moon text-gray-600'"></i> | |
| </button> | |
| </div> | |
| <!-- Main content area with tabs --> | |
| <div class="p-4"> | |
| <!-- Tab navigation --> | |
| <div class="flex border-b mb-4"> | |
| <button @click="activeTab = 'extract'" class="px-4 py-2 font-medium" :class="{'tab-active text-blue-500': activeTab === 'extract'}"> | |
| <i class="fas fa-download mr-1"></i> Extract | |
| </button> | |
| <button @click="activeTab = 'results'" class="px-4 py-2 font-medium" :class="{'tab-active text-blue-500': activeTab === 'results'}"> | |
| <i class="fas fa-list mr-1"></i> Results | |
| </button> | |
| <button @click="activeTab = 'settings'" class="px-4 py-2 font-medium" :class="{'tab-active text-blue-500': activeTab === 'settings'}"> | |
| <i class="fas fa-cog mr-1"></i> Settings | |
| </button> | |
| </div> | |
| <!-- Extract Tab --> | |
| <div v-if="activeTab === 'extract'" class="transition-all duration-300"> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium mb-1">Extraction Scope</label> | |
| <div class="flex space-x-2"> | |
| <button @click="scope = 'visible'" class="flex-1 py-2 px-3 rounded border" :class="scope === 'visible' ? 'bg-blue-100 border-blue-300 dark:bg-blue-900 dark:border-blue-700' : 'hover:bg-gray-100 dark:hover:bg-gray-700'"> | |
| <i class="fas fa-eye mr-1"></i> Visible Content | |
| </button> | |
| <button @click="scope = 'full'" class="flex-1 py-2 px-3 rounded border" :class="scope === 'full' ? 'bg-blue-100 border-blue-300 dark:bg-blue-900 dark:border-blue-700' : 'hover:bg-gray-100 dark:hover:bg-gray-700'"> | |
| <i class="fas fa-file-alt mr-1"></i> Full Page | |
| </button> | |
| <button @click="scope = 'multi'" class="flex-1 py-2 px-3 rounded border" :class="scope === 'multi' ? 'bg-blue-100 border-blue-300 dark:bg-blue-900 dark:border-blue-700' : 'hover:bg-gray-100 dark:hover:bg-gray-700'"> | |
| <i class="fas fa-copy mr-1"></i> Multi-Page | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium mb-1">Filter Options</label> | |
| <input v-model="filterKeyword" type="text" placeholder="Keywords (comma separated)" class="w-full p-2 border rounded mb-2 dark:bg-gray-800 dark:border-gray-600"> | |
| <input v-model="filterRegex" type="text" placeholder="Regular expression" class="w-full p-2 border rounded dark:bg-gray-800 dark:border-gray-600"> | |
| <div class="flex items-center mt-2"> | |
| <input v-model="removeDuplicates" type="checkbox" id="removeDuplicates" class="mr-2"> | |
| <label for="removeDuplicates">Remove duplicates</label> | |
| </div> | |
| <div class="flex items-center mt-1"> | |
| <input v-model="groupByDomain" type="checkbox" id="groupByDomain" class="mr-2"> | |
| <label for="groupByDomain">Group by domain</label> | |
| </div> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button @click="extractContent" class="flex-1 bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded flex items-center justify-center"> | |
| <i class="fas fa-play mr-2"></i> Extract Now | |
| </button> | |
| <button @click="showAdvanced = !showAdvanced" class="bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 py-2 px-3 rounded"> | |
| <i class="fas fa-sliders-h"></i> | |
| </button> | |
| </div> | |
| <div v-if="showAdvanced" class="mt-4 p-3 bg-gray-100 dark:bg-gray-800 rounded"> | |
| <h3 class="font-medium mb-2">Advanced Options</h3> | |
| <div class="mb-2"> | |
| <label class="block text-xs mb-1">Tab delay (ms)</label> | |
| <input v-model="tabDelay" type="number" min="0" class="w-full p-2 border rounded dark:bg-gray-700 dark:border-gray-600"> | |
| </div> | |
| <div class="flex items-center"> | |
| <input v-model="keepHighlights" type="checkbox" id="keepHighlights" class="mr-2"> | |
| <label for="keepHighlights">Keep highlights</label> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Results Tab --> | |
| <div v-if="activeTab === 'results'" class="transition-all duration-300"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <h3 class="font-medium">Extracted Items ({{filteredResults.length}})</h3> | |
| <div class="flex space-x-1"> | |
| <button @click="exportResults('csv')" class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded" title="Export to CSV"> | |
| <i class="fas fa-file-csv text-green-500"></i> | |
| </button> | |
| <button @click="exportResults('excel')" class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded" title="Export to Excel"> | |
| <i class="fas fa-file-excel text-green-600"></i> | |
| </button> | |
| <button @click="exportResults('text')" class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded" title="Export to Text"> | |
| <i class="fas fa-file-alt text-blue-500"></i> | |
| </button> | |
| <button @click="exportResults('bookmarks')" class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded" title="Save as Bookmarks"> | |
| <i class="fas fa-bookmark text-yellow-500"></i> | |
| </button> | |
| <button @click="copyToClipboard" class="p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded" title="Copy to Clipboard"> | |
| <i class="fas fa-copy text-purple-500"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div v-if="filteredResults.length === 0" class="text-center py-8 text-gray-500"> | |
| <i class="fas fa-inbox text-3xl mb-2"></i> | |
| <p>No extracted content yet</p> | |
| </div> | |
| <div v-else class="space-y-2 max-h-96 overflow-y-auto"> | |
| <div v-for="(result, index) in filteredResults" :key="index" class="p-3 border rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"> | |
| <div class="flex justify-between items-start"> | |
| <div class="flex-1 min-w-0"> | |
| <p class="truncate font-medium">{{ result.title || 'Untitled' }}</p> | |
| <p class="text-xs text-gray-500 dark:text-gray-400 truncate">{{ result.url }}</p> | |
| <div v-if="result.domain" class="mt-1"> | |
| <span class="domain-badge px-2 py-0.5 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300">{{ result.domain }}</span> | |
| </div> | |
| </div> | |
| <button @click="openUrl(result.url)" class="ml-2 p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded" title="Open in new tab"> | |
| <i class="fas fa-external-link-alt text-xs"></i> | |
| </button> | |
| </div> | |
| <div v-if="result.content" class="mt-2 text-sm text-gray-700 dark:text-gray-300"> | |
| <p class="line-clamp-2">{{ result.content }}</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Tab --> | |
| <div v-if="activeTab === 'settings'" class="transition-all duration-300"> | |
| <div class="mb-4"> | |
| <h3 class="font-medium mb-2">Keyboard Shortcuts</h3> | |
| <div class="space-y-2"> | |
| <div v-for="(shortcut, name) in shortcuts" :key="name" class="flex items-center"> | |
| <label class="w-32 text-sm">{{ name.replace(/_/g, ' ') }}</label> | |
| <input v-model="shortcuts[name]" type="text" class="flex-1 p-2 border rounded dark:bg-gray-800 dark:border-gray-600"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <h3 class="font-medium mb-2">Blacklisted Sites</h3> | |
| <div class="flex mb-2"> | |
| <input v-model="newBlacklistItem" type="text" placeholder="example.com" class="flex-1 p-2 border rounded-l dark:bg-gray-800 dark:border-gray-600"> | |
| <button @click="addToBlacklist" class="bg-blue-500 hover:bg-blue-600 text-white px-3 rounded-r"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| </div> | |
| <div class="max-h-32 overflow-y-auto border rounded dark:border-gray-600"> | |
| <div v-for="(site, index) in blacklist" :key="index" class="flex items-center justify-between p-2 hover:bg-gray-100 dark:hover:bg-gray-800"> | |
| <span>{{ site }}</span> | |
| <button @click="removeFromBlacklist(index)" class="text-red-500 hover:text-red-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <h3 class="font-medium mb-2">Appearance</h3> | |
| <div class="flex items-center"> | |
| <input v-model="autoTheme" type="checkbox" id="autoTheme" class="mr-2"> | |
| <label for="autoTheme">Auto light/dark mode</label> | |
| </div> | |
| </div> | |
| <div class="flex justify-end"> | |
| <button @click="saveSettings" class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded"> | |
| Save Settings | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Status bar --> | |
| <div class="p-2 border-t text-xs text-gray-500 dark:text-gray-400 flex justify-between"> | |
| <span v-if="statusMessage">{{ statusMessage }}</span> | |
| <span v-else>Ready</span> | |
| <span v-if="isProcessing" class="text-blue-500"> | |
| <i class="fas fa-spinner fa-spin mr-1"></i> Processing... | |
| </span> | |
| </div> | |
| </div> | |
| <script> | |
| // Vue.js-like simple reactive framework for the extension | |
| const app = { | |
| data() { | |
| return { | |
| activeTab: 'extract', | |
| isDarkMode: false, | |
| autoTheme: true, | |
| scope: 'visible', | |
| filterKeyword: '', | |
| filterRegex: '', | |
| removeDuplicates: true, | |
| groupByDomain: true, | |
| showAdvanced: false, | |
| tabDelay: 500, | |
| keepHighlights: true, | |
| isProcessing: false, | |
| statusMessage: '', | |
| results: [], | |
| shortcuts: { | |
| extract_content: 'Ctrl+Shift+E', | |
| open_results: 'Ctrl+Shift+R', | |
| toggle_theme: 'Ctrl+Shift+T' | |
| }, | |
| blacklist: [], | |
| newBlacklistItem: '' | |
| } | |
| }, | |
| computed: { | |
| filteredResults() { | |
| let results = [...this.results]; | |
| // Filter by keyword if provided | |
| if (this.filterKeyword) { | |
| const keywords = this.filterKeyword.split(',').map(k => k.trim().toLowerCase()); | |
| results = results.filter(item => { | |
| const content = (item.title + ' ' + item.content + ' ' + item.url).toLowerCase(); | |
| return keywords.some(k => content.includes(k)); | |
| }); | |
| } | |
| // Filter by regex if provided | |
| if (this.filterRegex) { | |
| try { | |
| const regex = new RegExp(this.filterRegex, 'i'); | |
| results = results.filter(item => { | |
| return regex.test(item.title) || regex.test(item.content) || regex.test(item.url); | |
| }); | |
| } catch (e) { | |
| this.statusMessage = 'Invalid regular expression'; | |
| setTimeout(() => this.statusMessage = '', 3000); | |
| } | |
| } | |
| // Remove duplicates if enabled | |
| if (this.removeDuplicates) { | |
| const uniqueUrls = new Set(); | |
| results = results.filter(item => { | |
| if (uniqueUrls.has(item.url)) { | |
| return false; | |
| } | |
| uniqueUrls.add(item.url); | |
| return true; | |
| }); | |
| } | |
| // Group by domain if enabled | |
| if (this.groupByDomain) { | |
| const domainMap = {}; | |
| results.forEach(item => { | |
| const domain = this.extractDomain(item.url); | |
| if (!domainMap[domain]) { | |
| domainMap[domain] = []; | |
| } | |
| domainMap[domain].push(item); | |
| }); | |
| // Flatten back to array but with domain property | |
| results = []; | |
| for (const domain in domainMap) { | |
| domainMap[domain].forEach(item => { | |
| results.push({ | |
| ...item, | |
| domain | |
| }); | |
| }); | |
| } | |
| } | |
| return results; | |
| } | |
| }, | |
| methods: { | |
| toggleTheme() { | |
| this.isDarkMode = !this.isDarkMode; | |
| if (!this.autoTheme) { | |
| this.saveSettings(); | |
| } | |
| }, | |
| checkSystemTheme() { | |
| if (this.autoTheme) { | |
| const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); | |
| this.isDarkMode = darkModeMediaQuery.matches; | |
| } | |
| }, | |
| extractContent() { | |
| this.isProcessing = true; | |
| this.statusMessage = 'Extracting content...'; | |
| // Simulate extraction (in a real extension, this would use chrome APIs) | |
| setTimeout(() => { | |
| // Mock data for demonstration | |
| const mockResults = [ | |
| { | |
| title: 'Example Page 1', | |
| url: 'https://example.com/page1', | |
| content: 'This is some example content from page 1 with relevant information.' | |
| }, | |
| { | |
| title: 'Example Page 2', | |
| url: 'https://example.com/page2', | |
| content: 'More example content here, this time from page 2 with different data.' | |
| }, | |
| { | |
| title: 'SEO Best Practices', | |
| url: 'https://seo-example.org/best-practices', | |
| content: 'Learn about the latest SEO techniques and how to implement them.' | |
| }, | |
| { | |
| title: 'Marketing Trends 2023', | |
| url: 'https://marketing-site.com/trends', | |
| content: 'The top marketing trends you need to know for 2023 and beyond.' | |
| }, | |
| { | |
| title: 'Academic Research Paper', | |
| url: 'https://academic.edu/research/important-study', | |
| content: 'A groundbreaking study in the field of computer science.' | |
| } | |
| ]; | |
| // Add domain to each result | |
| mockResults.forEach(item => { | |
| item.domain = this.extractDomain(item.url); | |
| }); | |
| this.results = [...this.results, ...mockResults]; | |
| this.isProcessing = false; | |
| this.statusMessage = `Extracted ${mockResults.length} items`; | |
| this.activeTab = 'results'; | |
| setTimeout(() => { | |
| this.statusMessage = ''; | |
| }, 3000); | |
| }, 1500); | |
| }, | |
| extractDomain(url) { | |
| try { | |
| const domain = new URL(url).hostname.replace('www.', ''); | |
| return domain; | |
| } catch { | |
| return 'unknown'; | |
| } | |
| }, | |
| openUrl(url) { | |
| // In a real extension: chrome.tabs.create({ url }); | |
| this.statusMessage = `Opening: ${url}`; | |
| setTimeout(() => { | |
| this.statusMessage = ''; | |
| }, 2000); | |
| }, | |
| exportResults(format) { | |
| this.statusMessage = `Exporting to ${format}...`; | |
| setTimeout(() => { | |
| this.statusMessage = `Exported ${this.filteredResults.length} items to ${format}`; | |
| setTimeout(() => { | |
| this.statusMessage = ''; | |
| }, 3000); | |
| }, 1000); | |
| }, | |
| copyToClipboard() { | |
| // In a real extension: navigator.clipboard.writeText() | |
| this.statusMessage = 'Copied to clipboard'; | |
| setTimeout(() => { | |
| this.statusMessage = ''; | |
| }, 2000); | |
| }, | |
| addToBlacklist() { | |
| if (this.newBlacklistItem.trim() && !this.blacklist.includes(this.newBlacklistItem.trim())) { | |
| this.blacklist.push(this.newBlacklistItem.trim()); | |
| this.newBlacklistItem = ''; | |
| } | |
| }, | |
| removeFromBlacklist(index) { | |
| this.blacklist.splice(index, 1); | |
| }, | |
| saveSettings() { | |
| // In a real extension: chrome.storage.sync.set() | |
| this.statusMessage = 'Settings saved'; | |
| setTimeout(() => { | |
| this.statusMessage = ''; | |
| }, 2000); | |
| }, | |
| loadSettings() { | |
| // In a real extension: chrome.storage.sync.get() | |
| // Mock loading settings | |
| setTimeout(() => { | |
| this.shortcuts = { | |
| extract_content: 'Ctrl+Shift+E', | |
| open_results: 'Ctrl+Shift+R', | |
| toggle_theme: 'Ctrl+Shift+T' | |
| }; | |
| this.blacklist = ['socialmedia.com', 'news.site']; | |
| this.autoTheme = true; | |
| this.checkSystemTheme(); | |
| }, 500); | |
| } | |
| }, | |
| mounted() { | |
| this.loadSettings(); | |
| // Listen for system theme changes | |
| window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { | |
| this.checkSystemTheme(); | |
| }); | |
| // Check theme on startup | |
| this.checkSystemTheme(); | |
| } | |
| }; | |
| // Initialize the app | |
| const { createApp } = Vue; | |
| createApp(app).mount('#app'); | |
| </script> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MikePy/web-extractor" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |