Fix provider sync, left mouse event handling

This commit is contained in:
2025-07-16 17:18:43 +00:00
parent c0d2cd4599
commit d3cd06cd7f
4 changed files with 277 additions and 43 deletions

View File

@ -36,6 +36,8 @@ class QuotesExtension extends Extension {
this._quotesFile = null; this._quotesFile = null;
this._settings = null; this._settings = null;
this._isPaused = false; this._isPaused = false;
this._preChangeTimer = null;
this._isShowingIndicator = false;
} }
enable() { enable() {
@ -57,6 +59,10 @@ class QuotesExtension extends Extension {
GLib.source_remove(this._syncTimer); GLib.source_remove(this._syncTimer);
this._syncTimer = null; this._syncTimer = null;
} }
if (this._preChangeTimer) {
GLib.source_remove(this._preChangeTimer);
this._preChangeTimer = null;
}
if (this._indicator) { if (this._indicator) {
this._indicator.destroy(); this._indicator.destroy();
this._indicator = null; this._indicator = null;
@ -77,13 +83,15 @@ class QuotesExtension extends Extension {
this._settings = this.getSettings(); this._settings = this.getSettings();
this._rotationInterval = this._settings.get_int('rotation-interval') || 30; this._rotationInterval = this._settings.get_int('rotation-interval') || 30;
this._syncInterval = this._settings.get_int('sync-interval') || 3600; 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._fontSize = this._settings.get_int('font-size') || 14;
this._textColor = this._settings.get_string('text-color') || '#ffffff'; this._textColor = this._settings.get_string('text-color') || '#ffffff';
this._maxWidth = this._settings.get_int('max-width') || 60; this._maxWidth = this._settings.get_int('max-width') || 60;
this._quotesFileName = this._settings.get_string('quotes-file') || 'quotes.json'; this._quotesFileName = this._settings.get_string('quotes-file') || 'quotes.json';
this._useCustomQuotes = this._settings.get_boolean('use-custom-quotes'); this._useCustomQuotes = this._settings.get_boolean('use-custom-quotes');
this._customQuotes = this._settings.get_string('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: [] }; this._quoteSources = { synced: [], custom: [] };
// Listen for settings changes // Listen for settings changes
@ -107,6 +115,12 @@ class QuotesExtension extends Extension {
if (this._label) { if (this._label) {
this._label.set_text(this._getCurrentQuote()); 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 // Create popup menu
this._createPopupMenu(); this._createPopupMenu();
this._indicator.connect('button-press-event', (actor, event) => { // 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 if (event.get_button() === 1) { // Left click
this._nextQuote(); this._nextQuote();
return Clutter.EVENT_STOP; // Prevent menu from opening return Clutter.EVENT_STOP;
} else if (event.get_button() === 3) { // Right click
this._indicator.menu.toggle();
return Clutter.EVENT_STOP;
}
} }
return Clutter.EVENT_PROPAGATE; return Clutter.EVENT_PROPAGATE;
}); });
@ -314,11 +339,16 @@ class QuotesExtension extends Extension {
GLib.source_remove(this._rotationTimer); GLib.source_remove(this._rotationTimer);
this._rotationTimer = null; 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, this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
seconds, () => { seconds, () => {
if (!this._isPaused) { if (!this._isPaused) {
this._nextQuote(); this._scheduleQuoteChange();
this._setupRotationTimer(); this._setupRotationTimer();
} }
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
@ -326,10 +356,10 @@ class QuotesExtension extends Extension {
} }
_getCurrentQuote() { _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]; const quote = this._quotes[this._currentQuoteIndex];
let text = `"${quote.text}" - ${quote.author}`; let text = ` "${quote.text}" - ${quote.author}`;
return text; return text;
} }
@ -362,12 +392,54 @@ class QuotesExtension extends Extension {
this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
this._rotationInterval, () => { this._rotationInterval, () => {
if (!this._isPaused) { if (!this._isPaused) {
this._nextQuote(); this._scheduleQuoteChange();
} }
return GLib.SOURCE_CONTINUE; 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() { _setupSyncTimer() {
this._syncTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this._syncTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
this._syncInterval, () => { 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() { _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 { try {
this._session = new Soup.Session(); 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) => { this._session.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null, (session, result) => {
try { try {
@ -388,29 +497,54 @@ class QuotesExtension extends Extension {
const response = decoder.decode(bytes.get_data()); const response = decoder.decode(bytes.get_data());
const data = JSON.parse(response); const data = JSON.parse(response);
if (data.content && data.author) { this._processProviderResponse(data, provider);
} catch (e) {
console.error(`Failed to sync from ${provider.name}:`, e);
}
});
} catch (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 = { const newQuote = {
text: data.content, text: text,
author: data.author, author: author,
hash: simpleHash(data.content + data.author), hash: simpleHash(text + author),
source: 'api' source: provider.name
}; };
const exists = this._quoteSources.synced.some(q => q.hash === newQuote.hash); const exists = this._quoteSources.synced.some(q => q.hash === newQuote.hash);
if (!exists) { if (!exists) {
this._quoteSources.synced.push(newQuote); this._quoteSources.synced.push(newQuote);
}
}
});
this._mergeAndSaveQuotes(); this._mergeAndSaveQuotes();
this._loadQuotes(); this._loadQuotes();
} }
}
} catch (e) { _getNestedProperty(obj, path) {
console.error('Failed to sync quotes:', e); return path.split('.').reduce((current, key) => current?.[key], obj);
}
});
} catch (e) {
console.error('Failed to create sync session:', e);
}
} }
} }

112
prefs.js
View File

@ -51,6 +51,28 @@ export default class QuotesPreferences extends ExtensionPreferences {
}); });
timingGroup.add(syncRow); 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 // API Group
const apiGroup = new Adw.PreferencesGroup({ const apiGroup = new Adw.PreferencesGroup({
title: 'API Settings', title: 'API Settings',
@ -58,12 +80,72 @@ export default class QuotesPreferences extends ExtensionPreferences {
}); });
behaviorPage.add(apiGroup); behaviorPage.add(apiGroup);
// API URL // API Providers
const apiRow = new Adw.EntryRow({ const apiProvidersRow = new Adw.ExpanderRow({
title: 'API URL', title: 'API Providers Configuration',
text: settings.get_string('api-url'), 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 // Appearance Settings Page
const appearancePage = new Adw.PreferencesPage({ const appearancePage = new Adw.PreferencesPage({
@ -400,12 +482,6 @@ export default class QuotesPreferences extends ExtensionPreferences {
Gio.SettingsBindFlags.DEFAULT Gio.SettingsBindFlags.DEFAULT
); );
settings.bind(
'api-url',
apiRow,
'text',
Gio.SettingsBindFlags.DEFAULT
);
settings.bind( settings.bind(
'font-size', 'font-size',
@ -442,5 +518,19 @@ export default class QuotesPreferences extends ExtensionPreferences {
Gio.SettingsBindFlags.DEFAULT Gio.SettingsBindFlags.DEFAULT
); );
settings.bind(
'show-change-indicator',
indicatorRow,
'active',
Gio.SettingsBindFlags.DEFAULT
);
settings.bind(
'indicator-duration',
indicatorDurationRow,
'value',
Gio.SettingsBindFlags.DEFAULT
);
} }
} }

Binary file not shown.

View File

@ -11,10 +11,10 @@
<summary>Sync interval</summary> <summary>Sync interval</summary>
<description>Time in seconds between API synchronizations</description> <description>Time in seconds between API synchronizations</description>
</key> </key>
<key name="api-url" type="s"> <key name="api-providers" type="s">
<default>'https://api.quotable.io/random'</default> <default>'[{"name":"quotable","url":"https://api.quotable.io/random","method":"GET","mapping":{"text":"content","author":"author"},"enabled":true}]'</default>
<summary>API URL</summary> <summary>API providers</summary>
<description>URL for fetching quotes from remote server</description> <description>JSON array of API provider configurations</description>
</key> </key>
<key name="font-size" type="i"> <key name="font-size" type="i">
<default>14</default> <default>14</default>
@ -46,5 +46,15 @@
<summary>Use custom quotes</summary> <summary>Use custom quotes</summary>
<description>Whether to use custom quotes instead of built-in/synced quotes</description> <description>Whether to use custom quotes instead of built-in/synced quotes</description>
</key> </key>
<key name="show-change-indicator" type="b">
<default>true</default>
<summary>Show change indicator</summary>
<description>Whether to show countdown before quote changes</description>
</key>
<key name="indicator-duration" type="i">
<default>5</default>
<summary>Indicator duration</summary>
<description>Duration in seconds for the change indicator countdown</description>
</key>
</schema> </schema>
</schemalist> </schemalist>