From d3cd06cd7f8a0ec29eefcd21d550a9732f375e41 Mon Sep 17 00:00:00 2001 From: Thinh Ha Date: Wed, 16 Jul 2025 17:18:43 +0000 Subject: [PATCH] Fix provider sync, left mouse event handling --- extension.js | 190 +++++++++++++++--- prefs.js | 112 ++++++++++- schemas/gschemas.compiled | Bin 764 -> 1016 bytes ....gnome.shell.extensions.quotes.gschema.xml | 18 +- 4 files changed, 277 insertions(+), 43 deletions(-) diff --git a/extension.js b/extension.js index 7e5b6fb..ad0569e 100644 --- a/extension.js +++ b/extension.js @@ -36,6 +36,8 @@ class QuotesExtension extends Extension { this._quotesFile = null; this._settings = null; this._isPaused = false; + this._preChangeTimer = null; + this._isShowingIndicator = false; } enable() { @@ -57,6 +59,10 @@ class QuotesExtension extends Extension { GLib.source_remove(this._syncTimer); this._syncTimer = null; } + if (this._preChangeTimer) { + GLib.source_remove(this._preChangeTimer); + this._preChangeTimer = null; + } if (this._indicator) { this._indicator.destroy(); this._indicator = null; @@ -77,13 +83,15 @@ class QuotesExtension extends Extension { this._settings = this.getSettings(); this._rotationInterval = this._settings.get_int('rotation-interval') || 30; this._syncInterval = this._settings.get_int('sync-interval') || 3600; - this._apiUrl = this._settings.get_string('api-url') || 'https://api.quotable.io/random'; + this._apiProviders = this._parseApiProviders(); this._fontSize = this._settings.get_int('font-size') || 14; this._textColor = this._settings.get_string('text-color') || '#ffffff'; this._maxWidth = this._settings.get_int('max-width') || 60; this._quotesFileName = this._settings.get_string('quotes-file') || 'quotes.json'; this._useCustomQuotes = this._settings.get_boolean('use-custom-quotes'); this._customQuotes = this._settings.get_string('custom-quotes') || '[]'; + this._showChangeIndicator = this._settings.get_boolean('show-change-indicator'); + this._indicatorDuration = this._settings.get_int('indicator-duration') || 5; this._quoteSources = { synced: [], custom: [] }; // Listen for settings changes @@ -107,6 +115,12 @@ class QuotesExtension extends Extension { if (this._label) { this._label.set_text(this._getCurrentQuote()); } + } else if (key === 'api-providers') { + this._apiProviders = this._parseApiProviders(); + } else if (key === 'show-change-indicator') { + this._showChangeIndicator = this._settings.get_boolean('show-change-indicator'); + } else if (key === 'indicator-duration') { + this._indicatorDuration = this._settings.get_int('indicator-duration') || 5; } }); } @@ -245,10 +259,21 @@ class QuotesExtension extends Extension { // Create popup menu this._createPopupMenu(); - this._indicator.connect('button-press-event', (actor, event) => { - if (event.get_button() === 1) { // Left click - this._nextQuote(); - return Clutter.EVENT_STOP; // Prevent menu from opening + // Override the reactive property to handle clicks properly + this._indicator.reactive = true; + this._indicator.can_focus = true; + this._indicator.track_hover = true; + + // Connect to event signals using a different approach + this._indicator.connect('event', (actor, event) => { + if (event.type() === Clutter.EventType.BUTTON_PRESS) { + if (event.get_button() === 1) { // Left click + this._nextQuote(); + return Clutter.EVENT_STOP; + } else if (event.get_button() === 3) { // Right click + this._indicator.menu.toggle(); + return Clutter.EVENT_STOP; + } } return Clutter.EVENT_PROPAGATE; }); @@ -314,11 +339,16 @@ class QuotesExtension extends Extension { GLib.source_remove(this._rotationTimer); this._rotationTimer = null; } + if (this._preChangeTimer) { + GLib.source_remove(this._preChangeTimer); + this._preChangeTimer = null; + } + this._hideIndicator(); this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, () => { if (!this._isPaused) { - this._nextQuote(); + this._scheduleQuoteChange(); this._setupRotationTimer(); } return GLib.SOURCE_REMOVE; @@ -326,10 +356,10 @@ class QuotesExtension extends Extension { } _getCurrentQuote() { - if (this._quotes.length === 0) return 'No quotes available'; + if (this._quotes.length === 0) return ' No quotes available'; const quote = this._quotes[this._currentQuoteIndex]; - let text = `"${quote.text}" - ${quote.author}`; + let text = ` "${quote.text}" - ${quote.author}`; return text; } @@ -362,12 +392,54 @@ class QuotesExtension extends Extension { this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this._rotationInterval, () => { if (!this._isPaused) { - this._nextQuote(); + this._scheduleQuoteChange(); } return GLib.SOURCE_CONTINUE; }); } + _scheduleQuoteChange() { + if (this._preChangeTimer) { + GLib.source_remove(this._preChangeTimer); + this._preChangeTimer = null; + } + + if (this._showChangeIndicator) { + this._showIndicator(); + + this._preChangeTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this._indicatorDuration, () => { + this._hideIndicator(); + this._nextQuote(); + this._preChangeTimer = null; + return GLib.SOURCE_REMOVE; + }); + } else { + this._nextQuote(); + } + } + + _showIndicator() { + if (!this._label || this._isShowingIndicator) return; + + this._isShowingIndicator = true; + const currentQuote = this._getCurrentQuote(); + let countdown = this._indicatorDuration; + this._label.set_text(`⏳${countdown}${currentQuote}`); + + const countdownTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + countdown--; + if (this._isShowingIndicator && countdown > 0) { + this._label.set_text(`⏳${countdown}${currentQuote}`); + return GLib.SOURCE_CONTINUE; + } + return GLib.SOURCE_REMOVE; + }); + } + + _hideIndicator() { + this._isShowingIndicator = false; + } + _setupSyncTimer() { this._syncTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this._syncInterval, () => { @@ -376,10 +448,47 @@ class QuotesExtension extends Extension { }); } + _parseApiProviders() { + try { + const providersString = this._settings.get_string('api-providers') || '[]'; + return JSON.parse(providersString); + } catch (e) { + console.error('Failed to parse API providers:', e); + return [{ + name: 'quotable', + url: 'https://api.quotable.io/random', + method: 'GET', + mapping: { text: 'content', author: 'author' }, + enabled: true + }]; + } + } + _syncQuotes() { + const enabledProviders = this._apiProviders.filter(p => p.enabled); + if (enabledProviders.length === 0) { + console.log('No enabled API providers'); + return; + } + + enabledProviders.forEach(provider => this._syncFromProvider(provider)); + } + + _syncFromProvider(provider) { try { this._session = new Soup.Session(); - const message = Soup.Message.new('GET', this._apiUrl); + const message = Soup.Message.new(provider.method || 'GET', provider.url); + + if (provider.headers) { + Object.entries(provider.headers).forEach(([key, value]) => { + message.request_headers.append(key, value); + }); + } + + if (provider.body && (provider.method === 'POST' || provider.method === 'PUT')) { + message.set_request_body_from_bytes('application/json', + new GLib.Bytes(JSON.stringify(provider.body))); + } this._session.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null, (session, result) => { try { @@ -388,30 +497,55 @@ class QuotesExtension extends Extension { const response = decoder.decode(bytes.get_data()); const data = JSON.parse(response); - if (data.content && data.author) { - const newQuote = { - text: data.content, - author: data.author, - hash: simpleHash(data.content + data.author), - source: 'api' - }; - - const exists = this._quoteSources.synced.some(q => q.hash === newQuote.hash); - - if (!exists) { - this._quoteSources.synced.push(newQuote); - this._mergeAndSaveQuotes(); - this._loadQuotes(); - } - } + this._processProviderResponse(data, provider); } catch (e) { - console.error('Failed to sync quotes:', e); + console.error(`Failed to sync from ${provider.name}:`, e); } }); } catch (e) { - console.error('Failed to create sync session:', e); + console.error(`Failed to create sync session for ${provider.name}:`, e); } } + + _processProviderResponse(data, provider) { + const mapping = provider.mapping || { text: 'text', author: 'author' }; + let quotes = []; + + if (Array.isArray(data)) { + quotes = data; + } else if (provider.responseArray) { + quotes = this._getNestedProperty(data, provider.responseArray) || []; + } else { + quotes = [data]; + } + + quotes.forEach(item => { + const text = this._getNestedProperty(item, mapping.text); + const author = this._getNestedProperty(item, mapping.author); + + if (text && author) { + const newQuote = { + text: text, + author: author, + hash: simpleHash(text + author), + source: provider.name + }; + + const exists = this._quoteSources.synced.some(q => q.hash === newQuote.hash); + + if (!exists) { + this._quoteSources.synced.push(newQuote); + } + } + }); + + this._mergeAndSaveQuotes(); + this._loadQuotes(); + } + + _getNestedProperty(obj, path) { + return path.split('.').reduce((current, key) => current?.[key], obj); + } } export default class QuotesGnomeExtension extends Extension { diff --git a/prefs.js b/prefs.js index 6200eaa..347e644 100644 --- a/prefs.js +++ b/prefs.js @@ -51,6 +51,28 @@ export default class QuotesPreferences extends ExtensionPreferences { }); timingGroup.add(syncRow); + // Show change indicator + const indicatorRow = new Adw.SwitchRow({ + title: 'Show Change Indicator', + subtitle: 'Display countdown before quote changes', + active: settings.get_boolean('show-change-indicator'), + }); + timingGroup.add(indicatorRow); + + // Indicator duration + const indicatorDurationRow = new Adw.SpinRow({ + title: 'Indicator Duration', + subtitle: 'Countdown duration in seconds', + adjustment: new Gtk.Adjustment({ + lower: 1, + upper: 30, + step_increment: 1, + page_increment: 5, + value: settings.get_int('indicator-duration'), + }), + }); + timingGroup.add(indicatorDurationRow); + // API Group const apiGroup = new Adw.PreferencesGroup({ title: 'API Settings', @@ -58,12 +80,72 @@ export default class QuotesPreferences extends ExtensionPreferences { }); behaviorPage.add(apiGroup); - // API URL - const apiRow = new Adw.EntryRow({ - title: 'API URL', - text: settings.get_string('api-url'), + // API Providers + const apiProvidersRow = new Adw.ExpanderRow({ + title: 'API Providers Configuration', + subtitle: 'Configure multiple quote providers', }); - apiGroup.add(apiRow); + + const providersTextView = new Gtk.TextView({ + buffer: new Gtk.TextBuffer(), + hexpand: true, + vexpand: true, + css_classes: ['card'], + monospace: true, + }); + + const scrolledProviders = new Gtk.ScrolledWindow({ + child: providersTextView, + hexpand: true, + min_content_height: 200, + max_content_height: 400, + }); + + const providersContainer = new Gtk.Box({ + orientation: Gtk.Orientation.VERTICAL, + spacing: 12, + margin_top: 12, + margin_bottom: 12, + margin_start: 12, + margin_end: 12, + }); + + const providersLabel = new Gtk.Label({ + label: 'Provider Configuration (JSON):', + xalign: 0, + }); + + providersContainer.append(providersLabel); + providersContainer.append(scrolledProviders); + + const providersPrefsRow = new Adw.PreferencesRow({ + child: providersContainer, + }); + + apiProvidersRow.add_row(providersPrefsRow); + apiGroup.add(apiProvidersRow); + + // Load current providers + const currentProviders = settings.get_string('api-providers'); + try { + const formatted = JSON.stringify(JSON.parse(currentProviders), null, 2); + providersTextView.buffer.text = formatted; + } catch (e) { + providersTextView.buffer.text = currentProviders; + } + + // Save on focus out + const focusController = new Gtk.EventControllerFocus(); + focusController.connect('leave', () => { + const text = providersTextView.buffer.text; + try { + JSON.parse(text); // Validate JSON + settings.set_string('api-providers', text); + } catch (e) { + console.error('Invalid JSON in providers configuration'); + } + }); + providersTextView.add_controller(focusController); // Appearance Settings Page const appearancePage = new Adw.PreferencesPage({ @@ -400,12 +482,6 @@ export default class QuotesPreferences extends ExtensionPreferences { Gio.SettingsBindFlags.DEFAULT ); - settings.bind( - 'api-url', - apiRow, - 'text', - Gio.SettingsBindFlags.DEFAULT - ); settings.bind( 'font-size', @@ -442,5 +518,19 @@ export default class QuotesPreferences extends ExtensionPreferences { Gio.SettingsBindFlags.DEFAULT ); + settings.bind( + 'show-change-indicator', + indicatorRow, + 'active', + Gio.SettingsBindFlags.DEFAULT + ); + + settings.bind( + 'indicator-duration', + indicatorDurationRow, + 'value', + Gio.SettingsBindFlags.DEFAULT + ); + } } \ No newline at end of file diff --git a/schemas/gschemas.compiled b/schemas/gschemas.compiled index f5637f57439f8c7c2b6c68fa8c86842fa3775115..95071ff5ee74645997ad97929bbb36b76ebaaa57 100644 GIT binary patch literal 1016 zcmZ8gziSjh7@eqROfCsg2m}Pt733n>-8LMeHj)$;77_x%VsbZkyRtiT?#x~iV5w{=*pbkB_xUGS4t7`o-ok6xbeoe@dTv1pYMmG>~6R zU(lyM0e=pB8Q9spTj%+yYw#Dq8fbib)?q&Narj%{`@rbqtzY!1$Kh{-&*9K7&#!-` zPfdRgdr*0}~=0^Yt~9MGq(!;ipgz`cjNU+Gix{13p7fW3hz@~K&W0Dca9 zdh+fA^Qouc?||O`f9FEYx$J^D8;3#oEt8mxz7S_S#li>MnN+U%t8v>!@Gmgfh-YHm zj$6{1%x~wJbQmB5>=hQ`K~EO>AqE~60}qRVhyMm1mIDuku(?%!E4R+1{{Ag!tjx$> z?DsO$kJ~D%F2?ePr)8?VrpH9Nk|=N1%d-SV6%-MiY-Y0~Z)eswvoR4B3yXBI z63cCDZEP$AQ&?=LAhu#-DflC#5cHgRn+RSw@0>aFx%2Ma%S*bl+C`bf9QazTvx5`w z0Ql|6nR8^$oslbYAN&eN$`uUVTHP62V z?g7mpujy0sK99hgz?=E&7wA(r;XeVt1b&SqolnjG+6Iq-?FSEAtS@x~{(CTM-5G}H z`}uw?Z`?!;LVFlY{@+x#-0pf8^I-28ks0Kc%(huu298R#X3d+x6u>uNSt1@S@A%SJ zBJ-k-~5_{OsxX2IkRX*y7=g+N+rdrb_eZ*;gFs}zT zA6BLGVjP48r7%v7qL)ikSg$JWR(wCD2pg`mf6Od@2>XkHPB|4CPo>6Fsqxgm##8lO JmB^&|{sO!Nr``Yn diff --git a/schemas/org.gnome.shell.extensions.quotes.gschema.xml b/schemas/org.gnome.shell.extensions.quotes.gschema.xml index 7918a00..ca90f7e 100644 --- a/schemas/org.gnome.shell.extensions.quotes.gschema.xml +++ b/schemas/org.gnome.shell.extensions.quotes.gschema.xml @@ -11,10 +11,10 @@ Sync interval Time in seconds between API synchronizations - - 'https://api.quotable.io/random' - API URL - URL for fetching quotes from remote server + + '[{"name":"quotable","url":"https://api.quotable.io/random","method":"GET","mapping":{"text":"content","author":"author"},"enabled":true}]' + API providers + JSON array of API provider configurations 14 @@ -46,5 +46,15 @@ Use custom quotes Whether to use custom quotes instead of built-in/synced quotes + + true + Show change indicator + Whether to show countdown before quote changes + + + 5 + Indicator duration + Duration in seconds for the change indicator countdown + \ No newline at end of file