// ==UserScript== // @name Banana Prompt Quicker // @namespace gemini.script // @tag entertainment // @version 1.4.1 // @description Prompts quicker is ALL you 🍌 need - UserScript版 // @author Glidea // @author Johnbi // @license MIT // @match https://aistudio.google.com/* // @match https://gemini.google.com/* // @exclude https://aistudio.google.com/app/_/* // @exclude https://gemini.google.com/_/*/ // @match https://*.hf.space/* // @match https://x.com/i/grok // @match https://*.perplexity.ai/* // @icon data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAwIiBoZWlnaHQ9IjgwMCIgdmlld0JveD0iMCAwIDEyOCAxMjgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgY2xhc3M9Imljb25pZnkgaWNvbmlmeS0tbm90byI+PHBhdGggZD0iTTExOS4yMiA5NS4xMmMtMS4xNCAyLjM2LTUuOTEgMi4yMS04LjU0IDEuMzgtNi43Mi0yLjEyLTEwLjktNy45LTEwLjktNy45TDY1LjE0IDczLjlsLTEwLjYxLTUuNTdzLTIuMDYtMy41IDIuMDYtNi4xNyA3LjAxLTIuODcgNy4wMS0yLjg3bDExLjk0IDcuNnM3LjcxLTYgMTUuNTQtNi45MWM3LjQ4LS44NiAxNi43OSA1LjIyIDE5LjI0IDE2LjcgMS45OSA5LjMxIDUuMTQgMTIuNTkgNi4wOSAxMy44Ljk1IDEuMjIgMy43OSAyLjYyIDIuODEgNC42NHoiIGZpbGw9IiNmZjhlMDAiLz48cGF0aCBkPSJNNzcuNzIgMzIuMTVDNzQuNjUgMjEuOTkgNzAuNzcgMTAuMSA2NC45MSA2LjAzYy01LjE5LTMuNi0xMC4yNC0yLjk1LTEyLjY0LTEuMzggMCAwLTQuMzMgMS43Ny01LjQ3IDYuMjQtMS44OCA3LjM0LjU5IDE2LjE1IDIuMiAyNS4yNSAxLjYxIDkuMSAyLjQ1IDE1LjM2IDMuMTggMjIuNjcuMDcuNjYgMTIuMTYgMjIuMTggMTIuMTYgMjIuMThsOS42NSA1LjE3czcuMzctMTUuOTQgNy4yNi0xOC4xOWMtLjEtMi4yNi0xLjI5LTI4LjQtMy41My0zNS44MnoiIGZpbGw9IiNmZmU0YjQiLz48cGF0aCBkPSJNNTIuNTUgNjYuOTRjLS4xMyAyLjQzIDUuNzMgMjEuOCA1LjczIDIxLjhMNzQgODYuMTRzMTEuMTUtNi42IDExLjQ2LTE4LjU3Yy40OC0xOC4wNS0yLjYxLTI2LjU1LTQuODUtMzMuOTctMy4wOC0xMC4xNC05Ljk0LTIzLjUtMTUuNzUtMjcuNjQtMy40MS0yLjQzLTcuMTktMi43OS04Ljc0LTIuNDkgMCAwIDEwLjMyIDguMDggMTUuNTQgMjEuOXM2LjQ1IDI5Ljc5IDYuNDUgMjkuNzktMy44NCAyLjgzLTEyLjkgNC4xM2MtNyAxLjAxLTEzLjA0LS43LTEzLjA0LS43cy41NSA1LjI3LjM4IDguMzV6IiBmaWxsPSIjZmZlMjY1Ii8+PHBhdGggZD0iTTg1LjMzIDgzLjc1bC02LjI5LTEzLjE0LjM0LTQuNjhzMS4zNC0zLjkzLTcuMDMgMi40MmMtMy44OCAyLjk0LTYuMjIgOC4zNi02LjIyIDguMzZzLTEuMTgtMi45Mi01LjMyLTYuNzhjLTMuNjgtMy40Mi04LjYzLTUuMDgtOC42My01LjA4cy0xMC4yOC0yLTExLjQtMS4xM2MtNy4yNSA1LjY2LTEyLjU1IDEyLjU1LTEyLjU1IDEyLjU1TDIyLjkgOTYuNDJsLTguODYgNS40M3MyLjQyIDMuNTQgNS45MiAyLjIyYzIuNzMtMS4wMyA2LjExLTQuMTIgNy45My04LjAxIDIuNS01LjM1IDcuNzUtMTUuNDcgMTAuNDItMTguNjcgNS4xNi02LjE2IDEwLjIyLTguNDEgMTMuOTktNi42MiA0LjI5IDIuMDMuODYgMjIuMjkuODYgMjIuMjlsLTI3LjU4IDIzLjIxLS40NyAzLjE2czQuMDEgMy44NSAxNS4wNiAzLjQ5IDI1LjgzLTQuNDUgMzUuMTgtMTcuMmM4LjI1LTExLjI0IDkuOTgtMjEuOTcgOS45OC0yMS45N3oiIGZpbGw9IiNmZmE3MjYiLz48cGF0aCBkPSJNNjMuNTggODQuOTZjLjI0IDUuNTUtMy4yMiAxMy45Ni0xMS4yMiAyMC45Ny03Ljk5IDcuMDEtMTYuNDMgOS4zMy0yMS42NyA5LjczLTMuOC4yOS00LjkzLTEuODgtNC45My0xLjg4czcuNjYtNy4wNSAxMy4zNS0xMi43YzUuNC01LjM3IDExLjI4LTE2LjY1IDEyLjU2LTIxLjg0IDEuMjgtNS4xOS4xMi04LjY4LjEyLTguNjhzMi40OC42OSA2LjEzIDQuMzVjMy41NCAzLjU1IDUuNTMgNi45NyA1LjY2IDEwLjA1eiIgZmlsbD0iI2ZmYjgwMyIvPjxwYXRoIGQ9Ik0yOS40NSAxMTguNzRjLS44MyAxLjkzLTMuNjggMi4wMS00LjkyLjk0LTEuMTctMS4wMS0yLjMxLTMuMDItMS4xMy01LjI3Ljc4LTEuNDggMy4zNC0xLjg5IDQuNzgtLjc0czIuMDIgMy4zMiAxLjI3IDUuMDd6IiBmaWxsPSIjODc1YjU0Ii8+PHBhdGggZD0iTTIzLjc2IDk3Ljg0Yy0zLjI4IDQuNTUtNi41NyA1LjI0LTcuOTMgNS4zOS0xLjE5LjE0LTIuNDktMS4zOC0yLjM0LTIuNzguMTUtMS40IDIuNDUtMy4zNyAyLjczLTcuNjcuMjgtNC4zLS4wNi0yMC4wNyA4LjY5LTI5LjEzIDYuMTctNi4zOCAxMy43My00LjE4IDE4LjYxLTIuNDkgNS41IDEuOSA4LjY3IDMuNyA4LjY3IDMuN3MtNC41LS4zMS0xMi4yNiA1LjgxYy00LjUxIDMuNTYtNy4xNiA4LjQ1LTkuNjYgMTQuMDYtMS44IDQuMDYtNC4yIDkuOTEtNi41MSAxMy4xMXoiIGZpbGw9IiNmZWU0YjQiLz48cGF0aCBkPSJNMTExIDEwOS41OGMtLjkyIDEuODQtMy4xNyAyLjk4LTUuMTYgMi44OS00LjIxLS4xOC04LjA1LTIuMzUtMTIuMy03LjgzLTYuMDEtNy43Ni0xMS4yNC0yNi4zOC0xNS4xNi0zMS4zNC0yLjY1LTMuMzUtNS45NS01LjAxLTUuOTUtNS4wMXMxLjgyLTEuNTQgMy42NC0yLjQ5YzEuODItLjk1IDYuMjItLjUgNi4yMi0uNWwxOC43OCAzMC4xOCA5LjkzIDE0LjF6IiBmaWxsPSIjZmViODA0Ii8+PHBhdGggZD0iTTEwMC4zOSA2OC40NmM0LjEyIDUuODkgNC42NiAxMS4wNiA1LjI4IDE2LjM0LjU2IDQuNzcgMS43MSAxNC44IDMuNDQgMTguMDcgMS43NCAzLjI3IDMuNDMgNS40IDEuNTYgNy4zOXMtNy4zNyAxLjI4LTExLjMyLTMuMDJjLTMuMTgtMy40Ny00Ljk0LTcuMjUtNy4zMy0xMi44LTIuMzktNS41NS01LjkxLTE4LjY1LTEwLjQ4LTI0LjU3LTIuOTItMy43OC01LjgzLTMuODktNS44My0zLjg5czMuODUtMy4xNCAxMS4yLTMuMjJjNi41OS0uMDcgMTAuMDUuOCAxMy40OCA1Ljd6IiBmaWxsPSIjZmZlNGI0Ii8+PC9zdmc+ // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_addElement // @grant GM_log // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_addStyle // @connect raw.githubusercontent.com // @source https://github.com/bxb100/Scripts/tree/main/banana-prompt-quicker // @homepage https://github.com/bxb100/Scripts/tree/main/banana-prompt-quicker // @homepageURL https://github.com/bxb100/Scripts/tree/main/banana-prompt-quicker // @supportURL https://github.com/bxb100/Scripts/issues // @downloadURL https://update.greasyfork.icu/scripts/556866/Banana%20Prompt%20Quicker.user.js // @updateURL https://update.greasyfork.icu/scripts/556866/Banana%20Prompt%20Quicker.meta.js // ==/UserScript== // ;(function () { 'use strict' /*! * Credit by Jan Biniok * MIT License * source: https://github.com/Tampermonkey/tampermonkey/issues/1334#issuecomment-2442399033 * * Fix https://copilot.microsoft.com/ by CY Fung * source: https://greasyfork.org/zh-CN/scripts/522884-default-trusted-types-policy-for-all-pages */ ;(function () { if (typeof window != 'undefined' && 'trustedTypes' in window && 'createPolicy' in window.trustedTypes && typeof window.trustedTypes.createPolicy == 'function' && window.trustedTypes.defaultPolicy == null) { window.trustedTypes.createPolicy('default', { createScriptURL: (s) => s, createScript: (s) => s, createHTML: (s) => s }) } })() // --- Polyfills for Chrome Extension API --- // 模拟 chrome.storage 使用 GM_storage const mockStorage = { get: (keys) => new Promise((resolve) => { let result = {} const keyList = Array.isArray(keys) ? keys : [keys] keyList.forEach((key) => { result[key] = GM_getValue(key) }) resolve(result) }), set: (items) => new Promise((resolve) => { for (const [key, value] of Object.entries(items)) { GM_setValue(key, value) } resolve() }), } const chrome = { storage: { local: mockStorage, sync: mockStorage, // Tampermonkey 统一使用本地存储 }, } // 辅助函数:使用 GM_xmlhttpRequest 替代 fetch 以避免 CSP 问题 function gmFetchJson(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, onload: function (response) { if (response.status >= 200 && response.status < 300) { try { resolve(JSON.parse(response.responseText)) } catch (e) { reject(e) } } else { reject(new Error(`HTTP error! status: ${response.status}`)) } }, onerror: function (err) { reject(err) }, }) }) } // --- ConfigManager (unified prompts + config) --- const ConfigManager = (() => { const configDetails = { url: 'https://raw.githubusercontent.com/glidea/banana-prompt-quicker/main/config.json', cacheKey: 'banana_config_cache', cacheTsKey: 'banana_config_cache_time', defaultValue: null, } const promptsDetails = { url: 'https://raw.githubusercontent.com/glidea/banana-prompt-quicker/main/prompts.json', cacheKey: 'banana_prompts_cache', cacheTsKey: 'banana_prompts_cache_time', defaultValue: [], } const CACHE_DURATION = 60 * 60 * 1000 // 60 min async function getJsonWithCache(url, key, tsKey, defaultValue) { const cache = await chrome.storage.local.get([key, tsKey]) const cachedData = cache[key] const cacheTimestamp = cache[tsKey] const now = Date.now() if (cachedData != null && cacheTimestamp && now - cacheTimestamp < CACHE_DURATION) { return cachedData } try { const data = await gmFetchJson(url) await chrome.storage.local.set({ [key]: data, [tsKey]: now }) return data } catch (e) { GM_log(`[Banana] Failed to fetch JSON from ${url}:`, e) return cachedData ?? defaultValue } } return { async get() { return getJsonWithCache(configDetails.url, configDetails.cacheKey, configDetails.cacheTsKey, configDetails.defaultValue) }, async getSelectors(platform, type) { const cfg = await this.get() const selectors = cfg && (cfg.selectors || cfg.selector) return selectors?.[platform]?.[type] }, async getPrompts() { return getJsonWithCache(promptsDetails.url, promptsDetails.cacheKey, promptsDetails.cacheTsKey, promptsDetails.defaultValue) }, } })() // 默认主题颜色配置 function getDefaultThemeColors(theme = 'light') { if (theme === 'dark') { return { background: '#141414', surface: '#1c1c1e', border: '#38383a', text: '#f5f5f7', textSecondary: '#98989d', primary: '#0a84ff', hover: '#2c2c2e', inputBg: '#1c1c1e', inputBorder: '#38383a', shadow: 'rgba(0,0,0,0.5)', surfaceHover: '#2c2c2e', } } return { background: '#ffffff', surface: '#f5f5f7', border: '#d2d2d7', text: '#1d1d1f', textSecondary: '#6e6e73', primary: '#007aff', hover: '#e8e8ed', inputBg: '#ffffff', inputBorder: '#d2d2d7', shadow: 'rgba(0,0,0,0.1)', surfaceHover: '#e8e8ed', } } // 20251127: switch to ConfigManager (config.json) only — remove selectors.json legacy usage async function getRemoteSelector(platform, type) { return ConfigManager.getSelectors(platform, type) } const FLASH_MODE_PROMPT = { title: '灵光模式', preview: 'https://cdn.jsdelivr.net/gh/glidea/banana-prompt-quicker@main/images/flash_mode.png', prompt: `你现在进入【灵光模式: 有灵感就够了】。请按照以下步骤辅助我完成创作: 1. 需求理解:分析我输入的粗略的想法描述(可能会包含图片) 2. 需求澄清:要求我做出细节澄清,提出 3 个你认为最重要的选择题(A/B/C/D),以明确我的生图或修图需求(例如风格、构图、光影、具体相关细节等)。请一次性列出这三个问题 3. 最终执行:等待我回答选择题后,根据我的原始描述和选择结果调用绘图工具生成图片(如果有附图,请务必作为参数传递给绘图工具,以保证一致性) --- OK,我想要:`, link: 'https://www.xiaohongshu.com/user/profile/5f7dc54d0000000001004afb', author: 'Official@glidea', isFlash: true, } // --- modal.js Logic --- class BananaModal { constructor(adapter) { this.adapter = adapter this.modal = null this.activeFilters = new Set() this.prompts = [] this.customPrompts = [] this.categories = new Set(['全部']) this.selectedCategory = 'all' this.sortMode = 'recommend' // 'recommend' | 'random' this.loadPrompts() this.loadSortMode() this.currentPage = 1 this.pageSize = this.isMobile() ? 8 : 12 this.filteredPrompts = [] this.favorites = [] this.keyboardHandler = this.handleKeyboard.bind(this) this._isInitialized = false // 用于区分首次显示和重新显示 this.randomMap = new Map() } async loadPrompts() { const staticPrompts = await ConfigManager.getPrompts() this.customPrompts = await this.getCustomPrompts() this.prompts = [...this.customPrompts, ...staticPrompts] // Aggregate categories this.categories = new Set(['全部']) this.prompts.forEach((p) => { if (p.category) { this.categories.add(p.category) } }) this.ensureRandomValues() this.updateCategoryDropdown() // 只在首次加载或有必要时重置页码 await this.applyFilters(!this._isInitialized) } ensureRandomValues() { this.prompts.forEach((p) => { const key = `${p.title}-${p.author}` if (!this.randomMap.has(key)) { this.randomMap.set(key, Math.random()) } p._randomVal = this.randomMap.get(key) }) } updateCategoryDropdown() { const optionsContainer = document.getElementById('category-options-container') const triggerText = document.getElementById('category-trigger-text') if (!optionsContainer || !triggerText) return this.populateCategoryDropdown(optionsContainer, triggerText) } populateCategoryDropdown(optionsContainer, triggerText) { // Clear existing options optionsContainer.innerHTML = '' // Populate categories const sortedCategories = Array.from(this.categories).sort((a, b) => { if (a === '全部') return -1 if (b === '全部') return 1 return a.localeCompare(b) }) if (sortedCategories.length === 0) { const empty = document.createElement('div') empty.textContent = '无分类' empty.style.cssText = `padding: 10px 16px; font-size: 14px; color: ${this.adapter.getThemeColors().textSecondary};` optionsContainer.appendChild(empty) } sortedCategories.forEach((cat) => { const option = document.createElement('div') option.textContent = cat const currentLabel = this.selectedCategory === 'all' ? '全部' : this.selectedCategory const isSelected = cat === currentLabel const colors = this.adapter.getThemeColors() const baseStyle = `padding: 10px 16px; cursor: pointer; transition: all 0.2s; font-size: 14px;` const selectedStyle = isSelected ? `background: ${colors.primary}15; color: ${colors.primary}; font-weight: 600;` : `background: transparent; color: ${colors.text};` option.style.cssText = baseStyle + selectedStyle option.onmouseenter = () => { if (!isSelected) { option.style.background = colors.surfaceHover } option.style.boxShadow = `0 2px 8px ${colors.shadow}` } option.onmouseleave = () => { if (!isSelected) { option.style.background = 'transparent' } else { option.style.background = `${colors.primary}15` } option.style.boxShadow = 'none' } option.onclick = (e) => { e.stopPropagation() this.selectedCategory = cat === '全部' ? 'all' : cat triggerText.textContent = cat // Hide dropdown optionsContainer.style.display = 'none' optionsContainer.setAttribute('data-visible', 'false') this.populateCategoryDropdown(optionsContainer, triggerText) this.applyFilters(true) } optionsContainer.appendChild(option) }) // Reset trigger text if needed const currentLabel = this.selectedCategory === 'all' ? '全部' : this.selectedCategory triggerText.textContent = currentLabel } async loadSortMode() { const result = await chrome.storage.local.get(['banana-sort-mode']) this.sortMode = result['banana-sort-mode'] || 'recommend' } async setSortMode(mode) { this.sortMode = mode await chrome.storage.local.set({ 'banana-sort-mode': mode }) } async getCustomPrompts() { const result = await chrome.storage.local.get(['banana-custom-prompts']) return result['banana-custom-prompts'] || [] } async compressImage(file) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.readAsDataURL(file) reader.onload = (event) => { const img = new Image() img.src = event.target.result img.onload = () => { const canvas = document.createElement('canvas') const MAX_WIDTH = 300 const MAX_HEIGHT = 300 let width = img.width let height = img.height if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width width = MAX_WIDTH } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height height = MAX_HEIGHT } } canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0, width, height) // 压缩为 JPEG, 质量 0.7 const dataUrl = canvas.toDataURL('image/jpeg', 0.7) resolve(dataUrl) } img.onerror = reject } reader.onerror = reject }) } show() { if (!this.modal) { this.modal = this.createModal() document.body.appendChild(this.modal) } this.modal.style.display = 'flex' if (!this._isInitialized) { // 首次显示:完整初始化 this.updateCategoryDropdown() this.applyFilters(true).then(() => (this._isInitialized = true)) } else { // 重新显示:只刷新数据,保留状态 this.loadPrompts() } // 添加键盘事件监听器 document.addEventListener('keydown', this.keyboardHandler) } hide() { if (this.modal) { this.modal.style.display = 'none' } // 移除键盘事件监听器 document.removeEventListener('keydown', this.keyboardHandler) } isMobile() { return window.innerWidth <= 768 } createModal() { const colors = this.adapter.getThemeColors() const mobile = this.isMobile() const modalElement = document.createElement('div') modalElement.id = 'prompts-modal' modalElement.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; z-index: 10000;' const container = document.createElement('div') container.style.cssText = `background: ${colors.background}; border-radius: ${mobile ? '24px 24px 0 0' : '20px'}; box-shadow: 0 20px 60px ${colors.shadow}; max-width: ${mobile ? '100%' : '900px'}; width: ${mobile ? '100%' : '90%'}; max-height: ${mobile ? '90vh' : '85vh'}; display: flex; flex-direction: column; ${mobile ? 'margin-top: auto;' : ''}; overflow: visible;` const searchSection = this.createSearchSection(colors, mobile) const content = this.createContent(colors, mobile) container.appendChild(searchSection) container.appendChild(content) modalElement.appendChild(container) modalElement.addEventListener('click', (e) => { if (e.target === modalElement) { this.hide() } }) if (mobile) { modalElement.addEventListener('touchstart', (e) => { if (e.target === modalElement) { this.hide() } }) } return modalElement } createSearchSection(colors, mobile) { const searchSection = document.createElement('div') searchSection.id = 'prompts-search-section' searchSection.style.cssText = `padding: ${mobile ? '16px' : '20px 24px'}; border-bottom: 1px solid ${colors.border}; display: flex; ${mobile ? 'flex-direction: column; gap: 12px;' : 'align-items: center; gap: 16px;'}; overflow: visible; z-index: 100; position: relative;` // 搜索框容器 const searchContainer = document.createElement('div') searchContainer.style.cssText = `${mobile ? 'width: 100%;' : 'flex: 1;'} display: flex; align-items: center; gap: 8px; position: relative;` const searchInput = document.createElement('input') searchInput.type = 'text' searchInput.id = 'prompt-search' searchInput.placeholder = '搜索...' searchInput.style.cssText = `flex: 1; padding: ${mobile ? '14px 20px' : '12px 18px'}; border: 1px solid ${colors.inputBorder}; border-radius: 16px; outline: none; font-size: ${mobile ? '16px' : '14px'}; background: ${colors.inputBg}; color: ${colors.text}; box-sizing: border-box; transition: all 0.2s;` searchInput.addEventListener('input', () => this.applyFilters(true)) searchInput.addEventListener('focus', () => { searchInput.style.borderColor = colors.primary }) searchInput.addEventListener('blur', () => { const currentColors = this.adapter.getThemeColors() searchInput.style.borderColor = currentColors.inputBorder }) // Sort Mode Button const sortBtnContainer = document.createElement('div') sortBtnContainer.style.cssText = 'position: relative; display: flex; align-items: center;' const sortBtn = document.createElement('button') sortBtn.id = 'sort-mode-btn' const currentModeText = this.sortMode === 'recommend' ? '随机焕新' : '推荐排序' sortBtn.innerHTML = this.sortMode === 'recommend' ? '' : '' sortBtn.style.cssText = `padding: ${mobile ? '10px' : '8px'}; border: none; background: transparent; color: ${colors.textSecondary}; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; border-radius: 8px;` sortBtn.onclick = () => this.toggleSortMode() // Tooltip const tooltip = document.createElement('div') tooltip.id = 'sort-tooltip' tooltip.textContent = `切换${currentModeText}` tooltip.style.cssText = `position: absolute; bottom: -40px; left: 50%; transform: translateX(-50%); background: ${colors.surface}; color: ${colors.text}; padding: 6px 12px; border-radius: 8px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s; box-shadow: 0 4px 12px ${colors.shadow}; border: 1px solid ${colors.border}; z-index: 1000;` if (!mobile) { sortBtn.onmouseenter = () => { sortBtn.style.color = colors.primary sortBtn.style.transform = 'scale(1.1)' sortBtn.style.background = `${colors.primary}10` tooltip.style.opacity = '1' } sortBtn.onmouseleave = () => { sortBtn.style.color = colors.textSecondary sortBtn.style.transform = 'scale(1)' sortBtn.style.background = 'transparent' tooltip.style.opacity = '0' } } sortBtnContainer.appendChild(sortBtn) sortBtnContainer.appendChild(tooltip) searchContainer.appendChild(searchInput) searchContainer.appendChild(sortBtnContainer) const filterContainer = document.createElement('div') filterContainer.style.cssText = `display: flex; gap: 8px; align-items: center; ${mobile ? 'justify-content: space-between; flex-wrap: wrap;' : ''}; position: relative; z-index: 101;` // Category Dropdown const dropdownContainer = document.createElement('div') dropdownContainer.style.cssText = `position: relative; z-index: 1000;` const dropdownTrigger = document.createElement('div') dropdownTrigger.id = 'category-dropdown-trigger' dropdownTrigger.style.cssText = `padding: ${mobile ? '10px 14px' : '8px 12px'}; border: 1px solid ${colors.border}; border-radius: 16px; background: ${colors.surface}; color: ${colors.text}; font-size: ${mobile ? '14px' : '13px'}; cursor: pointer; display: flex; align-items: center; gap: 4px; transition: all 0.2s; min-width: 70px; justify-content: space-between; user-select: none;` const triggerText = document.createElement('span') triggerText.id = 'category-trigger-text' triggerText.textContent = this.selectedCategory === 'all' ? '全部' : this.selectedCategory triggerText.style.cssText = 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: center;' const arrowIcon = document.createElement('span') arrowIcon.innerHTML = `` arrowIcon.style.cssText = `display: flex; align-items: center; transition: transform 0.2s; opacity: 0.6;` dropdownTrigger.appendChild(triggerText) dropdownTrigger.appendChild(arrowIcon) const optionsContainer = document.createElement('div') optionsContainer.id = 'category-options-container' optionsContainer.style.cssText = `position: absolute; top: 100%; left: 0; margin-top: 8px; width: 100%; background: ${colors.surface}; border: 1px solid ${colors.border}; border-radius: 16px; box-shadow: 0 10px 40px ${colors.shadow}; display: none; flex-direction: column; overflow: hidden; backdrop-filter: blur(20px); max-height: 300px; overflow-y: auto; z-index: 9999;` optionsContainer.setAttribute('data-visible', 'false') // Toggle Logic dropdownTrigger.onclick = (e) => { e.stopPropagation() const isVisible = optionsContainer.getAttribute('data-visible') === 'true' if (isVisible) { optionsContainer.style.display = 'none' optionsContainer.setAttribute('data-visible', 'false') arrowIcon.style.transform = 'rotate(0deg)' } else { optionsContainer.style.display = 'flex' optionsContainer.setAttribute('data-visible', 'true') arrowIcon.style.transform = 'rotate(180deg)' } } // Click outside to close document.addEventListener('click', (e) => { if (optionsContainer.getAttribute('data-visible') === 'true' && !dropdownContainer.contains(e.target)) { optionsContainer.style.display = 'none' optionsContainer.setAttribute('data-visible', 'false') arrowIcon.style.transform = 'rotate(0deg)' } }) if (!mobile) { dropdownTrigger.onmouseenter = () => { dropdownTrigger.style.borderColor = colors.primary dropdownTrigger.style.boxShadow = `0 2px 8px ${colors.shadow}` } dropdownTrigger.onmouseleave = () => { dropdownTrigger.style.borderColor = colors.border dropdownTrigger.style.boxShadow = 'none' } } dropdownContainer.appendChild(dropdownTrigger) dropdownContainer.appendChild(optionsContainer) // Populate immediately this.populateCategoryDropdown(optionsContainer, triggerText) const buttonsContainer = document.createElement('div') buttonsContainer.style.cssText = `display: flex; gap: 8px; ${mobile ? 'flex: 1; justify-content: space-between;' : ''}` const filters = [ { key: 'favorite', label: '收藏' }, { key: 'custom', label: '自定义' }, { key: 'generate', label: '生图' }, { key: 'edit', label: '编辑' }, ] filters.forEach((filter) => { const btn = document.createElement('button') btn.id = `filter-${filter.key}` btn.textContent = filter.label btn.style.cssText = `padding: ${mobile ? '10px 18px' : '8px 18px'}; border: 1px solid ${colors.border}; border-radius: 20px; background: ${colors.surface}; color: ${colors.text}; font-size: ${mobile ? '14px' : '13px'}; cursor: pointer; transition: all 0.25s ease; white-space: nowrap; touch-action: manipulation;` btn.onclick = () => this.toggleFilter(filter.key) buttonsContainer.appendChild(btn) }) const addBtn = document.createElement('button') addBtn.textContent = '+' addBtn.title = '添加自定义 Prompt' addBtn.style.cssText = `padding: ${mobile ? '10px 18px' : '8px 18px'}; border: 1px solid ${colors.primary}; border-radius: 20px; background: ${colors.primary}; color: white; font-size: ${mobile ? '18px' : '16px'}; font-weight: 600; cursor: pointer; transition: all 0.25s ease; display: flex; align-items: center; justify-content: center; line-height: 1; box-shadow: 0 2px 8px ${colors.shadow};` addBtn.onclick = () => this.showAddPromptModal() buttonsContainer.appendChild(addBtn) filterContainer.appendChild(dropdownContainer) filterContainer.appendChild(buttonsContainer) searchSection.appendChild(searchContainer) searchSection.appendChild(filterContainer) return searchSection } createContent(colors, mobile) { const container = document.createElement('div') container.style.cssText = 'flex: 1; display: flex; flex-direction: column; overflow: hidden;' const scrollArea = document.createElement('div') scrollArea.id = 'prompts-scroll-area' scrollArea.style.cssText = `flex: 1; overflow-y: auto; padding: ${mobile ? '16px' : '20px 24px'}; -webkit-overflow-scrolling: touch;` const grid = document.createElement('div') grid.id = 'prompts-grid' grid.style.cssText = `display: grid; grid-template-columns: ${mobile ? 'repeat(2, 1fr)' : 'repeat(4, 1fr)'}; gap: ${mobile ? '12px' : '16px'};` scrollArea.appendChild(grid) const pagination = document.createElement('div') pagination.id = 'prompts-pagination' pagination.style.cssText = `padding: ${mobile ? '12px' : '16px'}; border-top: 1px solid ${colors.border}; display: flex; justify-content: center; align-items: center; gap: 16px; background: ${colors.surface}; border-radius: ${mobile ? '0' : '0 0 20px 20px'};` container.appendChild(scrollArea) container.appendChild(pagination) return container } toggleFilter(filterKey) { const btn = document.getElementById(`filter-${filterKey}`) if (!btn) return const colors = this.adapter.getThemeColors() const mobile = this.isMobile() const setInactiveStyle = (targetBtn) => { targetBtn.style.cssText = `padding: ${mobile ? '10px 18px' : '8px 18px'}; border: 1px solid ${colors.border}; border-radius: 20px; background: ${colors.surface}; color: ${colors.text}; font-size: ${mobile ? '14px' : '13px'}; cursor: pointer; transition: all 0.25s ease; white-space: nowrap; touch-action: manipulation;` if (!mobile) { targetBtn.onmouseenter = () => { targetBtn.style.transform = 'scale(1.05)' targetBtn.style.boxShadow = `0 2px 8px ${colors.shadow}` } targetBtn.onmouseleave = () => { targetBtn.style.transform = 'scale(1)' targetBtn.style.boxShadow = 'none' } } } if (this.activeFilters.has(filterKey)) { this.activeFilters.delete(filterKey) setInactiveStyle(btn) } else { // Mutually exclusive logic for generate/edit if (filterKey === 'generate' && this.activeFilters.has('edit')) { this.activeFilters.delete('edit') const editBtn = document.getElementById('filter-edit') if (editBtn) setInactiveStyle(editBtn) } if (filterKey === 'edit' && this.activeFilters.has('generate')) { this.activeFilters.delete('generate') const generateBtn = document.getElementById('filter-generate') if (generateBtn) setInactiveStyle(generateBtn) } this.activeFilters.add(filterKey) btn.style.cssText = `padding: ${mobile ? '10px 18px' : '8px 18px'}; border: 1px solid ${colors.primary}; border-radius: 20px; background: ${colors.primary}; color: white; font-size: ${mobile ? '14px' : '13px'}; cursor: pointer; transition: all 0.25s ease; white-space: nowrap; touch-action: manipulation; box-shadow: 0 2px 8px ${colors.shadow};` if (!mobile) { btn.onmouseenter = () => { btn.style.transform = 'scale(1.05)' btn.style.boxShadow = `0 4px 12px ${colors.shadow}` } btn.onmouseleave = () => { btn.style.transform = 'scale(1)' btn.style.boxShadow = `0 2px 8px ${colors.shadow}` } } } this.applyFilters(true) } async toggleSortMode() { const newMode = this.sortMode === 'recommend' ? 'random' : 'recommend' await this.setSortMode(newMode) if (newMode === 'random') { this.randomMap.clear() this.ensureRandomValues() } // 更新按钮图标和 tooltip const sortBtn = document.getElementById('sort-mode-btn') const tooltip = document.getElementById('sort-tooltip') if (sortBtn) { const currentModeText = newMode === 'recommend' ? '随机焕新' : '推荐排序' sortBtn.innerHTML = newMode === 'recommend' ? '' : '' if (tooltip) { tooltip.textContent = `切换${currentModeText}` } } // 重新应用过滤和排序 this.applyFilters(true) } async applyFilters(resetPage = true) { const searchInput = document.getElementById('prompt-search') const keyword = searchInput ? searchInput.value.toLowerCase() : '' this.favorites = await this.getFavorites() let filtered = this.prompts.filter((prompt) => { const matchesSearch = !keyword || prompt.title.toLowerCase().includes(keyword) || prompt.prompt.toLowerCase().includes(keyword) || prompt.author.toLowerCase().includes(keyword) if (!matchesSearch) return false // Category Filter if (this.selectedCategory !== 'all' && prompt.category !== this.selectedCategory) { return false } if (this.activeFilters.size === 0) return true const promptId = `${prompt.title}-${prompt.author}` const isFavorite = this.favorites.includes(promptId) return Array.from(this.activeFilters).every((filter) => { if (filter === 'favorite') return isFavorite if (filter === 'custom') return prompt.isCustom if (filter === 'generate') return prompt.mode === 'generate' if (filter === 'edit') return prompt.mode === 'edit' return false }) }) // Sort: Favorites > Custom > Others (根据 sortMode) // 先分组 const favoriteItems = [] const customItems = [] const normalItems = [] filtered.forEach((item) => { const itemId = `${item.title}-${item.author}` const isFavorite = this.favorites.includes(itemId) if (isFavorite) { favoriteItems.push(item) } else if (item.isCustom) { customItems.push(item) } else { normalItems.push(item) } }) // 普通项根据 sortMode 排序 if (this.sortMode === 'random') { normalItems.sort((a, b) => a._randomVal - b._randomVal) } // recommend 模式下保持原顺序 // 合并:Flash Mode > 收藏 > 自定义 > 普通 filtered = [...favoriteItems, ...customItems, ...normalItems] // Always prepend Flash Mode filtered.unshift(FLASH_MODE_PROMPT) this.filteredPrompts = filtered // 智能处理页码:只在需要时重置,或者当前页超出范围时调整 if (resetPage) { this.currentPage = 1 } else { // 确保当前页在有效范围内 const totalPages = Math.ceil(this.filteredPrompts.length / this.pageSize) if (this.currentPage > totalPages && totalPages > 0) { this.currentPage = totalPages } } this.renderCurrentPage() } renderCurrentPage() { const grid = document.getElementById('prompts-grid') if (!grid) return const start = (this.currentPage - 1) * this.pageSize const end = start + this.pageSize const pageItems = this.filteredPrompts.slice(start, end) grid.innerHTML = '' if (pageItems.length === 0) { const placeholder = document.createElement('div') const colors = this.adapter.getThemeColors() const mobile = this.isMobile() const columns = mobile ? 2 : 4 const rows = Math.ceil(this.pageSize / columns) const cardMinHeight = mobile ? 240 : 260 const gap = mobile ? 12 : 16 const minHeight = rows * cardMinHeight + (rows - 1) * gap placeholder.style.cssText = ` grid-column: 1 / -1; display: flex; align-items: center; justify-content: center; min-height: ${minHeight}px; color: ${colors.textSecondary}; font-size: ${mobile ? '14px' : '16px'}; ` placeholder.textContent = '没有找到相关提示词' grid.appendChild(placeholder) } else { pageItems.forEach((prompt) => { const card = this.createPromptCard(prompt, this.favorites) grid.appendChild(card) }) if (pageItems.length < this.pageSize) { const remaining = this.pageSize - pageItems.length const mobile = this.isMobile() const cardMinHeight = mobile ? 240 : 260 for (let i = 0; i < remaining; i++) { const placeholder = document.createElement('div') placeholder.style.cssText = `min-height: ${cardMinHeight}px; opacity: 0; pointer-events: none;` grid.appendChild(placeholder) } } } const scrollArea = document.getElementById('prompts-scroll-area') if (scrollArea) scrollArea.scrollTop = 0 this.renderPagination() } renderPagination() { const pagination = document.getElementById('prompts-pagination') if (!pagination) return const totalPages = Math.ceil(this.filteredPrompts.length / this.pageSize) const colors = this.adapter.getThemeColors() const mobile = this.isMobile() pagination.innerHTML = '' if (totalPages <= 1) { pagination.style.display = 'none' return } if (mobile) { pagination.style.cssText = `padding: 12px; border-top: 1px solid ${colors.border}; display: flex; flex-direction: column; align-items: center; gap: 12px; background: ${colors.surface}; border-radius: 0;` } else { pagination.style.cssText = `padding: 16px 24px; border-top: 1px solid ${colors.border}; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; background: ${colors.surface}; border-radius: 0 0 20px 20px;` } const createBtn = (text, disabled, onClick) => { const btn = document.createElement('button') btn.textContent = text btn.disabled = disabled btn.style.cssText = `padding: ${mobile ? '10px 20px' : '8px 18px'}; border: 1px solid ${colors.border}; border-radius: 12px; background: ${disabled ? colors.surface : colors.primary}; color: ${disabled ? colors.textSecondary : '#fff'}; cursor: ${disabled ? 'not-allowed' : 'pointer'}; font-size: ${mobile ? '14px' : '13px'}; transition: all 0.25s ease; opacity: ${disabled ? 0.5 : 1}; font-weight: 500;` if (!disabled) { btn.onclick = onClick if (!mobile) { btn.onmouseenter = () => { btn.style.transform = 'scale(1.05)' btn.style.boxShadow = `0 4px 12px ${colors.shadow}` } btn.onmouseleave = () => { btn.style.transform = 'scale(1)' btn.style.boxShadow = 'none' } } } return btn } const prevBtn = createBtn('上一页', this.currentPage === 1, () => this.changePage(-1)) const pageInfo = document.createElement('div') const editablePageBtn = document.createElement('input') editablePageBtn.id = 'current-page-input' editablePageBtn.type = 'number' editablePageBtn.value = this.currentPage editablePageBtn.min = 1 editablePageBtn.max = totalPages editablePageBtn.style.cssText = `width: fit-content; max-width: 100px; padding: ${mobile ? '8px' : '6px'}; border: 1px solid ${colors.border}; border-radius: 12px; background: ${colors.surface}; text-align: center; outline: none; box-sizing: border-box; margin: 0 8px;color: inherit; font-size: inherit; font-weight: inherit;` editablePageBtn.onchange = () => { let val = parseInt(editablePageBtn.value) if (isNaN(val) || val < 1) val = 1 if (val > totalPages) val = totalPages this.currentPage = val this.renderCurrentPage() } const otherPageInfo = document.createElement('span') otherPageInfo.textContent = `/ ${totalPages}` otherPageInfo.style.width = '1.8em' pageInfo.style.cssText = `color: ${colors.text}; font-size: ${mobile ? '14px' : '13px'}; font-weight: 500; display: flex; align-items: center; justify-content: center;` pageInfo.appendChild(editablePageBtn) pageInfo.appendChild(otherPageInfo) const nextBtn = createBtn('下一页', this.currentPage === totalPages, () => this.changePage(1)) const controlsWrapper = document.createElement('div') controlsWrapper.style.cssText = 'display: flex; align-items: center; gap: 16px;' controlsWrapper.appendChild(prevBtn) controlsWrapper.appendChild(pageInfo) controlsWrapper.appendChild(nextBtn) const socialContainer = document.createElement('div') socialContainer.style.cssText = `display: flex; align-items: center; gap: ${mobile ? '12px' : '16px'}; justify-content: ${mobile ? 'center' : 'flex-end'};` const greasyfork = document.createElement('a') greasyfork.href = 'https://greasyfork.org/zh-CN/scripts/556866-banana-prompt-quicker' greasyfork.target = '_blank' greasyfork.innerHTML = `` greasyfork.style.cssText = `color: ${colors.textSecondary}; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; padding: 8px; border-radius: 50%; cursor: pointer;` socialContainer.appendChild(greasyfork) if (mobile) { pagination.appendChild(controlsWrapper) const spacer = document.createElement('div') pagination.appendChild(spacer) } else { const spacer = document.createElement('div') pagination.appendChild(spacer) pagination.appendChild(controlsWrapper) pagination.appendChild(socialContainer) } } changePage(delta) { this.currentPage += delta this.renderCurrentPage() } handleKeyboard(event) { if (!this.modal || this.modal.style.display === 'none') return const activeElement = document.activeElement if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable)) return const totalPages = Math.ceil(this.filteredPrompts.length / this.pageSize) if (totalPages <= 1) return if (event.key === 'ArrowLeft') { event.preventDefault() if (this.currentPage > 1) this.changePage(-1) } else if (event.key === 'ArrowRight') { event.preventDefault() if (this.currentPage < totalPages) this.changePage(1) } } createPromptCard(prompt, favorites) { const promptId = `${prompt.title}-${prompt.author}` const isFavorite = favorites.includes(promptId) const colors = this.adapter.getThemeColors() const theme = this.adapter.getCurrentTheme() const mobile = this.isMobile() const card = document.createElement('div') card.className = 'prompt-card' card.style.cssText = `background: ${colors.surface}; border-radius: 16px; border: 1px solid ${colors.border}; cursor: pointer; overflow: hidden; transition: all 0.3s ease; min-height: ${mobile ? '240px' : '260px'}; position: relative; touch-action: manipulation; display: flex; flex-direction: column;` if (!mobile) { card.addEventListener('mouseenter', () => { card.style.boxShadow = `0 8px 24px ${colors.shadow}` card.style.transform = 'translateY(-4px)' }) card.addEventListener('mouseleave', () => { card.style.boxShadow = 'none' card.style.transform = 'translateY(0)' }) } const img = GM_addElement('img', { src: prompt.preview, alt: prompt.title, style: `width: 100%; height: ${mobile ? '180px' : '200px'}; object-fit: cover; flex-shrink: 0;`, }) img.onclick = () => this.adapter.insertPrompt(prompt.prompt) const favoriteBtn = document.createElement('button') const favBtnBg = isFavorite ? 'rgba(255,193,7,0.9)' : theme === 'dark' ? 'rgba(48,49,52,0.9)' : 'rgba(255,255,255,0.9)' const favBtnColor = isFavorite ? '#000' : theme === 'dark' ? '#e8eaed' : '#5f6368' favoriteBtn.style.cssText = `position: absolute; top: 12px; right: 12px; width: ${mobile ? '36px' : '32px'}; height: ${mobile ? '36px' : '32px'}; border-radius: 50%; border: none; background: ${favBtnBg}; color: ${favBtnColor}; font-size: ${mobile ? '16px' : '14px'}; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.25s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.15); backdrop-filter: blur(10px); touch-action: manipulation;` favoriteBtn.textContent = isFavorite ? '⭐' : '☆' favoriteBtn.onclick = (e) => { e.stopPropagation() this.toggleFavorite(promptId) } const content = document.createElement('div') content.style.cssText = 'padding: 12px; flex: 1; display: flex; flex-direction: column; gap: 8px; justify-content: flex-start; min-height: 0; overflow: hidden;' const title = document.createElement('h3') title.style.cssText = `font-size: ${mobile ? '15px' : '14px'}; font-weight: 500; color: ${colors.text}; margin: 0; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;` title.textContent = prompt.title title.onclick = () => this.adapter.insertPrompt(prompt.prompt) const bottomRow = document.createElement('div') bottomRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-top: 4px;' const author = document.createElement('span') author.style.cssText = `font-size: ${mobile ? '13px' : '12px'}; color: ${colors.textSecondary}; font-weight: 400; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; margin-right: 8px;` author.textContent = prompt.author if (prompt.link) { author.style.textDecoration = 'underline' author.onclick = (e) => { e.stopPropagation() GM_openInTab(prompt.link, { active: true }) } } else { author.onclick = () => this.adapter.insertPrompt(prompt.prompt) } const modeTag = document.createElement('span') let tagText = '生图' let tagBg = '' let tagColor = '' if (prompt.isFlash) { tagText = '万能' // Special Flash Mode styling (e.g., purple/gradient) tagBg = theme === 'dark' ? 'rgba(168, 85, 247, 0.15)' : 'rgba(147, 51, 234, 0.12)' tagColor = theme === 'dark' ? '#a855f7' : '#9333ea' } else { const isEdit = prompt.mode === 'edit' tagText = isEdit ? '编辑' : '生图' tagBg = theme === 'dark' ? (isEdit ? 'rgba(10, 132, 255, 0.15)' : 'rgba(48, 209, 88, 0.15)') : isEdit ? 'rgba(0, 122, 255, 0.12)' : 'rgba(52, 199, 89, 0.12)' tagColor = theme === 'dark' ? (isEdit ? '#0a84ff' : '#30d158') : isEdit ? '#007aff' : '#34c759' } modeTag.style.cssText = `background: ${tagBg}; color: ${tagColor}; padding: 4px 10px; border-radius: 12px; font-size: ${mobile ? '12px' : '11px'}; font-weight: 600; backdrop-filter: blur(10px); flex-shrink: 0;` modeTag.textContent = tagText bottomRow.appendChild(author) bottomRow.appendChild(modeTag) content.appendChild(title) content.appendChild(bottomRow) if (prompt.isCustom) { const btnBg = theme === 'dark' ? 'rgba(48,49,52,0.9)' : 'rgba(255,255,255,0.9)' const btnColor = theme === 'dark' ? '#e8eaed' : '#5f6368' // 编辑按钮 const editBtn = document.createElement('button') editBtn.innerHTML = `` editBtn.title = '编辑' editBtn.style.cssText = `position: absolute; top: 12px; left: 12px; width: ${mobile ? '36px' : '32px'}; height: ${mobile ? '36px' : '32px'}; border-radius: 50%; border: none; background: ${btnBg}; color: ${btnColor}; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.25s ease; z-index: 2; backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0,0,0,0.15);` editBtn.onclick = (e) => { e.stopPropagation() this.showAddPromptModal(prompt) } if (!mobile) { editBtn.addEventListener('mouseenter', () => { editBtn.style.transform = 'scale(1.15)' editBtn.style.boxShadow = '0 6px 16px rgba(0,122,255,0.4)' }) editBtn.addEventListener('mouseleave', () => { editBtn.style.transform = 'scale(1)' editBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)' }) } // 删除按钮 const deleteBtn = document.createElement('button') deleteBtn.innerHTML = `` deleteBtn.title = '删除' deleteBtn.style.cssText = `position: absolute; top: 12px; left: ${mobile ? '56px' : '48px'}; width: ${mobile ? '36px' : '32px'}; height: ${mobile ? '36px' : '32px'}; border-radius: 50%; border: none; background: ${btnBg}; color: ${btnColor}; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.25s ease; z-index: 2; backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0,0,0,0.15);` deleteBtn.onclick = (e) => { e.stopPropagation() if (confirm('确定要删除这个 Prompt 吗?')) { this.deleteCustomPrompt(prompt.id) } } if (!mobile) { deleteBtn.addEventListener('mouseenter', () => { deleteBtn.style.transform = 'scale(1.15)' deleteBtn.style.boxShadow = '0 6px 16px rgba(0,0,0,0.25)' }) deleteBtn.addEventListener('mouseleave', () => { deleteBtn.style.transform = 'scale(1)' deleteBtn.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)' }) } card.appendChild(editBtn) card.appendChild(deleteBtn) } card.appendChild(img) card.appendChild(favoriteBtn) card.appendChild(content) return card } async getFavorites() { const result = await chrome.storage.sync.get(['banana-favorites']) return result['banana-favorites'] || [] } async toggleFavorite(promptId) { const favorites = await this.getFavorites() const index = favorites.indexOf(promptId) if (index > -1) { favorites.splice(index, 1) } else { favorites.push(promptId) } await chrome.storage.sync.set({ 'banana-favorites': favorites }) this.applyFilters(false) } showAddPromptModal(existingPrompt = null) { const colors = this.adapter.getThemeColors() const mobile = this.isMobile() const overlay = document.createElement('div') overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 11000;' overlay.onclick = (e) => { if (e.target === overlay) document.body.removeChild(overlay) } const dialog = document.createElement('div') dialog.style.cssText = `background: ${colors.surface}; padding: ${mobile ? '24px' : '32px'}; border-radius: 20px; width: ${mobile ? '90%' : '480px'}; max-width: 90%; box-shadow: 0 20px 60px ${colors.shadow}; display: flex; flex-direction: column; gap: 16px; color: ${colors.text};` const title = document.createElement('h3') title.textContent = existingPrompt ? '编辑自定义 Prompt' : '添加自定义 Prompt' title.style.cssText = 'margin: 0 0 4px 0; font-size: 20px; font-weight: 600;' const createInput = (placeholder, isTextarea = false) => { const input = document.createElement(isTextarea ? 'textarea' : 'input') input.placeholder = placeholder input.style.cssText = `width: 100%; padding: ${mobile ? '14px 16px' : '12px 16px'}; border: 1px solid ${colors.inputBorder}; border-radius: 12px; background: ${colors.inputBg}; color: ${colors.text}; font-size: 14px; outline: none; box-sizing: border-box; transition: all 0.2s; ${isTextarea ? 'min-height: 120px; resize: vertical; font-family: inherit;' : ''}` input.onfocus = () => { input.style.borderColor = colors.primary input.style.boxShadow = `0 0 0 3px ${colors.primary}15` } input.onblur = () => { input.style.borderColor = colors.inputBorder input.style.boxShadow = 'none' } return input } const titleInput = createInput('标题') if (existingPrompt) titleInput.value = existingPrompt.title // Mode Selection (Moved up) let selectedMode = existingPrompt?.mode || 'generate' const createModeSelection = () => { const modeContainer = document.createElement('div') modeContainer.style.cssText = `display: flex; background: ${colors.inputBg}; padding: 4px; border-radius: 10px; border: 1px solid ${colors.inputBorder};` const createOption = (value, label, iconSvg) => { const isSelected = selectedMode === value const option = document.createElement('div') option.style.cssText = `flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px; border-radius: 8px; cursor: pointer; font-size: 13px; transition: all 0.2s; font-weight: ${isSelected ? '600' : '400'}; color: ${isSelected ? colors.text : colors.textSecondary}; background: ${isSelected ? colors.surface : 'transparent'}; box-shadow: ${isSelected ? `0 2px 8px ${colors.shadow}` : 'none'};` option.onclick = () => { selectedMode = value modeContainer.parentNode.replaceChild(createModeSelection(), modeContainer) } const icon = document.createElement('span') icon.innerHTML = iconSvg icon.style.cssText = 'display: flex; align-items: center;' option.appendChild(icon) option.appendChild(document.createTextNode(label)) return option } const generateIcon = `` const editIcon = `` modeContainer.appendChild(createOption('generate', '文生图', generateIcon)) modeContainer.appendChild(createOption('edit', '编辑', editIcon)) return modeContainer } const modeContainer = createModeSelection() // Image Upload UI const imageContainer = document.createElement('div') imageContainer.style.cssText = `width: 100%; height: 140px; border: 1px dashed ${colors.border}; border-radius: 12px; background: ${colors.inputBg}; cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; overflow: hidden; transition: all 0.2s;` const fileInput = document.createElement('input') fileInput.type = 'file' fileInput.accept = 'image/*' fileInput.style.display = 'none' const placeholderIcon = document.createElement('span') placeholderIcon.innerHTML = `` placeholderIcon.style.cssText = 'margin-bottom: 8px' const placeholderText = document.createElement('span') placeholderText.style.cssText = `font-size: 13px; color: ${colors.textSecondary}; font-weight: 500;` placeholderText.textContent = '点击上传封面图' const placeholderContainer = document.createElement('div') placeholderContainer.style.cssText = 'display: flex; flex-direction: column; align-items: center; pointer-events: none;' placeholderContainer.appendChild(placeholderIcon) placeholderContainer.appendChild(placeholderText) const previewImg = document.createElement('img') previewImg.style.cssText = `width: 100%; height: 100%; object-fit: cover; display: none; position: absolute; top: 0; left: 0;` const clearBtn = document.createElement('button') clearBtn.innerHTML = '' clearBtn.style.cssText = `position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; border-radius: 50%; background: rgba(0,0,0,0.5); color: white; border: none; cursor: pointer; display: none; align-items: center; justify-content: center; backdrop-filter: blur(4px); transition: all 0.2s; z-index: 10;` clearBtn.onclick = () => { fileInput.value = '' selectedFile = null previewImg.removeAttribute('src') previewImg.style.display = 'none' placeholderContainer.style.display = 'flex' clearBtn.style.display = 'none' imageContainer.style.borderStyle = 'dashed' } clearBtn.onmouseenter = (e) => (e.target.style.background = 'rgba(0,0,0,0.7)') clearBtn.onmouseleave = (e) => (e.target.style.background = 'rgba(0,0,0,0.5)') // Click handler for container imageContainer.onclick = (e) => { if (e.target !== clearBtn && !clearBtn.contains(e.target)) { fileInput.click() } } let selectedFile = null // 如果是编辑模式且有预览图,显示预览图 if (existingPrompt?.preview && !existingPrompt.preview.includes('gstatic.com')) { previewImg.src = existingPrompt.preview previewImg.style.display = 'block' placeholderContainer.style.display = 'none' imageContainer.style.borderStyle = 'solid' clearBtn.style.display = 'flex' } fileInput.onchange = (e) => { if (e.target.files && e.target.files[0]) { const file = e.target.files[0] selectedFile = file const reader = new FileReader() reader.onload = (evt) => { previewImg.src = evt.target.result previewImg.style.display = 'block' placeholderContainer.style.display = 'none' imageContainer.style.borderStyle = 'solid' clearBtn.style.display = 'flex' } reader.readAsDataURL(file) } } imageContainer.onmouseenter = (e) => { if (!selectedFile && !previewImg.src) { e.target.style.borderColor = colors.primary e.target.style.background = colors.surfaceHover } } imageContainer.onmouseleave = (e) => { if (!selectedFile && !previewImg.src) { e.target.style.borderColor = colors.border e.target.style.background = colors.inputBg } } imageContainer.appendChild(fileInput) imageContainer.appendChild(placeholderContainer) imageContainer.appendChild(previewImg) imageContainer.appendChild(clearBtn) const promptInput = createInput('Prompt 内容', true) if (existingPrompt) promptInput.value = existingPrompt.prompt // Category Dropdown for Add Prompt const categoryContainer = document.createElement('div') categoryContainer.style.cssText = 'position: relative; width: 100%; z-index: 10;' const categoryTrigger = document.createElement('div') categoryTrigger.style.cssText = `width: 100%; padding: ${mobile ? '14px 16px' : '12px 16px'}; border: 1px solid ${colors.inputBorder}; border-radius: 12px; background: ${colors.inputBg}; color: ${colors.text}; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box;` const addCategories = Array.from(this.categories) .filter((c) => c !== '全部') .sort((a, b) => a.localeCompare(b)) let selectedAddCategory = existingPrompt?.category || addCategories[0] const categoryTriggerText = document.createElement('span') categoryTriggerText.textContent = selectedAddCategory const categoryArrow = document.createElement('span') categoryArrow.innerHTML = `` categoryArrow.style.cssText = `display: flex; align-items: center; transition: transform 0.2s; opacity: 0.6;` categoryTrigger.appendChild(categoryTriggerText) categoryTrigger.appendChild(categoryArrow) const categoryOptions = document.createElement('div') categoryOptions.style.cssText = `position: absolute; top: 100%; left: 0; margin-top: 8px; width: 100%; background: ${colors.surface}; border: 1px solid ${colors.border}; border-radius: 12px; box-shadow: 0 10px 40px ${colors.shadow}; display: none; flex-direction: column; overflow: hidden; backdrop-filter: blur(20px); max-height: 200px; overflow-y: auto; z-index: 100;` addCategories.forEach((cat) => { const option = document.createElement('div') option.textContent = cat const baseStyle = `padding: 10px 16px; cursor: pointer; transition: all 0.2s; font-size: 14px; background: transparent; color: ${colors.text};` option.style.cssText = baseStyle option.onmouseenter = () => { option.style.background = colors.surfaceHover } option.onmouseleave = () => { option.style.background = 'transparent' } option.onclick = (e) => { e.stopPropagation() selectedAddCategory = cat categoryTriggerText.textContent = cat categoryOptions.style.display = 'none' categoryArrow.style.transform = 'rotate(0deg)' } categoryOptions.appendChild(option) }) categoryTrigger.onclick = (e) => { e.stopPropagation() const isVisible = categoryOptions.style.display === 'flex' categoryOptions.style.display = isVisible ? 'none' : 'flex' categoryArrow.style.transform = isVisible ? 'rotate(0deg)' : 'rotate(180deg)' } const closeDropdown = (e) => { if (!categoryContainer.contains(e.target)) { categoryOptions.style.display = 'none' categoryArrow.style.transform = 'rotate(0deg)' } } document.addEventListener('click', closeDropdown) const cleanup = () => document.removeEventListener('click', closeDropdown) categoryContainer.appendChild(categoryTrigger) categoryContainer.appendChild(categoryOptions) const btnContainer = document.createElement('div') btnContainer.style.cssText = 'display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px;' const cancelBtn = document.createElement('button') cancelBtn.textContent = '取消' cancelBtn.style.cssText = `padding: ${mobile ? '12px 24px' : '10px 20px'}; border: 1px solid ${colors.border}; border-radius: 12px; background: transparent; color: ${colors.text}; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.25s ease;` cancelBtn.onclick = () => { cleanup() document.body.removeChild(overlay) } const saveBtn = document.createElement('button') saveBtn.textContent = '保存' saveBtn.style.cssText = `padding: ${mobile ? '12px 24px' : '10px 20px'}; border: none; border-radius: 12px; background: ${colors.primary}; color: white; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.25s ease; box-shadow: 0 2px 8px ${colors.shadow};` saveBtn.onclick = async () => { const titleVal = titleInput.value.trim() const promptVal = promptInput.value.trim() if (!titleVal || !promptVal) { alert('请填写标题和内容') return } let previewDataUrl = existingPrompt?.preview || 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg' if (selectedFile) { try { saveBtn.textContent = '处理中...' saveBtn.disabled = true previewDataUrl = await this.compressImage(selectedFile) } catch (err) { console.error('图片压缩失败', err) alert('图片处理失败,将使用默认图标') } finally { saveBtn.textContent = '保存' saveBtn.disabled = false } } const promptData = { title: titleVal, prompt: promptVal, mode: selectedMode, category: selectedAddCategory, preview: previewDataUrl, } if (existingPrompt) { await this.updateCustomPrompt(existingPrompt.id, promptData) } else { await this.saveCustomPrompt(promptData) } document.body.removeChild(overlay) cleanup() } btnContainer.appendChild(cancelBtn) btnContainer.appendChild(saveBtn) dialog.appendChild(title) dialog.appendChild(titleInput) dialog.appendChild(imageContainer) dialog.appendChild(categoryContainer) dialog.appendChild(promptInput) dialog.appendChild(modeContainer) dialog.appendChild(btnContainer) overlay.appendChild(dialog) document.body.appendChild(overlay) } async deleteCustomPrompt(promptId) { const customPrompts = await this.getCustomPrompts() const newPrompts = customPrompts.filter((p) => p.id !== promptId) await chrome.storage.local.set({ 'banana-custom-prompts': newPrompts }) await this.loadPrompts() } async updateCustomPrompt(promptId, data) { const customPrompts = await this.getCustomPrompts() const index = customPrompts.findIndex((p) => p.id === promptId) if (index !== -1) { customPrompts[index] = { ...customPrompts[index], ...data, id: promptId, author: 'Me', isCustom: true, } await chrome.storage.local.set({ 'banana-custom-prompts': customPrompts }) await this.loadPrompts() } } async saveCustomPrompt(data) { const newPrompt = { ...data, author: 'Me', isCustom: true, id: Date.now(), preview: data.preview || 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg', } const customPrompts = await this.getCustomPrompts() customPrompts.unshift(newPrompt) await chrome.storage.local.set({ 'banana-custom-prompts': customPrompts }) await this.loadPrompts() } } // --- content.js Logic --- class AIStudioAdapter { constructor() { this.modal = null this._initializingButton = false } async findPromptInput() { let el = document.querySelector('ms-prompt-input-wrapper textarea') if (el) { return el } el = document.querySelector('textarea') if (el) { return el } // Fallback. const s = await getRemoteSelector('aistudio', 'promptInput') return document.querySelector(s) } async findClosestInsertButton() { let el = document.querySelector('ms-run-button button') if (el) { return el } // Fallback. const s = await getRemoteSelector('aistudio', 'insertButton') return document.querySelector(s) } getCurrentTheme() { return document.body.classList.contains('dark-theme') ? 'dark' : 'light' } getThemeColors() { return getDefaultThemeColors(this.getCurrentTheme()) } createButton() { const wrapper = document.createElement('div') wrapper.className = 'button-wrapper' const btn = document.createElement('button') btn.id = 'banana-btn' btn.className = 'mat-mdc-tooltip-trigger ms-button-borderless ms-button-icon' const updateButtonTheme = () => { const colors = this.getThemeColors() btn.style.cssText = `width: 40px; height: 40px; border-radius: 50%; border: none; background: ${colors.hover}; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; margin-right: 8px; transition: background-color 0.2s;` } updateButtonTheme() btn.title = '快捷提示' btn.textContent = '🍌' btn.addEventListener('mouseenter', () => { const colors = this.getThemeColors() btn.style.background = colors.border }) btn.addEventListener('mouseleave', () => { const colors = this.getThemeColors() btn.style.background = colors.hover }) btn.addEventListener('click', () => { if (this.modal) this.modal.show() }) wrapper.appendChild(btn) return wrapper } async initButton() { if (document.getElementById('banana-btn')) return true if (this._initializingButton) { return false } this._initializingButton = true try { const runButton = await this.findClosestInsertButton() if (!runButton) { return false } const bananaBtn = this.createButton() const buttonWrapper = runButton.parentElement try { buttonWrapper.parentElement.insertBefore(bananaBtn, buttonWrapper) } catch (error) { console.error('插入香蕉按钮失败:', error) buttonWrapper.insertAdjacentElement('beforebegin', bananaBtn) } return true } finally { this._initializingButton = false } } async insertPrompt(promptText) { const textarea = await this.findPromptInput() if (textarea) { textarea.value = promptText textarea.dispatchEvent(new Event('input', { bubbles: true })) textarea.focus() const length = promptText.length textarea.setSelectionRange(length, length) if (this.modal) this.modal.hide() } } waitForElements() { const checkInterval = setInterval(async () => { const input = await this.findPromptInput() if (input) { const success = await this.initButton() if (success) clearInterval(checkInterval) } }, 1000) } startObserver() { const observer = new MutationObserver(() => { const existingBtn = document.getElementById('banana-btn') if (!existingBtn) this.initButton() }) observer.observe(document.body, { childList: true, subtree: true }) } } class GeminiAdapter { constructor() { this.modal = null this._initializingButton = false } async findPromptInput() { let el = document.querySelector('div.ql-editor[contenteditable="true"]') if (el) { return el } // Fallback. const selector = await getRemoteSelector('gemini', 'promptInput') return document.querySelector(selector) } async findClosestInsertButton() { let el = document.querySelector('button.toolbox-drawer-item-deselect-button:has(img.img-icon)') if (el) { return el } // Fallback. const s = await getRemoteSelector('gemini', 'insertButton') return document.querySelector(s) } getCurrentTheme() { return document.body.classList.contains('dark-theme') || document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light' } getThemeColors() { return getDefaultThemeColors(this.getCurrentTheme()) } createButton() { const isMobile = window.innerWidth <= 768 const btn = document.createElement('button') btn.id = 'banana-btn' btn.className = 'mat-mdc-button mat-mdc-button-base mat-unthemed' const updateButtonTheme = () => { const colors = this.getThemeColors() const mobile = window.innerWidth <= 768 btn.style.cssText = ` height: 40px; ${mobile ? 'width: 40px;' : ''} border-radius: ${mobile ? '50%' : '20px'}; border: none; background: transparent; color: ${colors.text}; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; font-size: 14px; font-family: 'Google Sans', Roboto, Arial, sans-serif; margin-left: 4px; transition: background-color 0.2s; padding: ${mobile ? '0' : '0 16px'}; gap: ${mobile ? '0' : '8px'}; ` } updateButtonTheme() btn.title = '快捷提示' btn.innerHTML = isMobile ? '🍌' : '🍌Prompts' btn.addEventListener('mouseenter', () => { const colors = this.getThemeColors() btn.style.background = colors.hover }) btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent' }) btn.addEventListener('click', (e) => { e.preventDefault() e.stopPropagation() if (this.modal) this.modal.show() }) return btn } async initButton() { if (document.getElementById('banana-btn')) return true if (this._initializingButton) { return false } this._initializingButton = true try { const imageBtn = await this.findClosestInsertButton() if (!imageBtn) { return false } const bananaBtn = this.createButton() try { imageBtn.insertAdjacentElement('afterend', bananaBtn) } catch (error) { console.error('插入香蕉按钮失败:', error) return false } return true } finally { this._initializingButton = false } } async insertPrompt(promptText) { const textarea = await this.findPromptInput() if (textarea) { textarea.focus() const lines = promptText.split('\n') const htmlContent = lines .map((line) => { const escaped = line.replace(/&/g, '&').replace(//g, '>') return `
${escaped || '
'}
${escaped || '
'}