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 f5637f5..95071ff 100644
Binary files a/schemas/gschemas.compiled and b/schemas/gschemas.compiled differ
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