diff --git a/gnome-extension/extension.js b/gnome-extension/extension.js index ad74f42..313842e 100644 --- a/gnome-extension/extension.js +++ b/gnome-extension/extension.js @@ -185,6 +185,12 @@ const CURRENCY_SYMBOLS = { ZWL: "$", }; +const PRICE_DIRECTION = { + UNCHANGED: 0, + UP: 1, + DOWN: -1, +}; + const RabbitForexIndicator = GObject.registerClass( class RabbitForexIndicator extends PanelMenu.Button { _init(extension) { @@ -195,7 +201,12 @@ const RabbitForexIndicator = GObject.registerClass( this._httpSession = new Soup.Session(); this._rates = {}; this._timestamps = {}; + this._previousRates = {}; + this._previousTimestamps = {}; + this._lastKnownTimestamps = {}; + this._referenceRates = {}; this._updateTimeout = null; + this._historyFetchTimeout = null; // Create the panel button layout this._box = new St.BoxLayout({ @@ -221,6 +232,7 @@ const RabbitForexIndicator = GObject.registerClass( this._fetchAllRates(); this._startUpdateTimer(); + this._fetchHistoricalRatesIfNeeded(); } _getEndpoints() { @@ -233,6 +245,29 @@ const RabbitForexIndicator = GObject.registerClass( }; } + _getHistoryEndpoint(category, symbol) { + const primaryCurrency = this._settings.get_string("primary-currency"); + const mode = this._settings.get_string("price-change-mode"); + + let resolution = ""; + if (mode === "day-start" || mode === "day-ago") { + resolution = "/hourly"; + } + + switch (category) { + case "fiat": + return `${API_BASE}/rates/history/${symbol}${resolution}`; + case "metals": + return `${API_BASE}/metals/history/${symbol}/currency/${primaryCurrency}${resolution}`; + case "crypto": + return `${API_BASE}/crypto/history/${symbol}/currency/${primaryCurrency}${resolution}`; + case "stocks": + return `${API_BASE}/stocks/history/${symbol}/currency/${primaryCurrency}${resolution}`; + default: + return null; + } + } + _getWatchedCategory(category) { if (!CATEGORIES.includes(category)) return []; @@ -262,6 +297,7 @@ const RabbitForexIndicator = GObject.registerClass( const refreshItem = new PopupMenu.PopupMenuItem("🔄 Refresh Now"); refreshItem.connect("activate", () => { this._fetchAllRates(); + this._fetchHistoricalRatesIfNeeded(); }); this.menu.addMenuItem(refreshItem); @@ -292,21 +328,104 @@ const RabbitForexIndicator = GObject.registerClass( }); } + _startHistoryFetchTimer() { + if (this._historyFetchTimeout) { + GLib.source_remove(this._historyFetchTimeout); + this._historyFetchTimeout = null; + } + + const mode = this._settings.get_string("price-change-mode"); + if (mode === "none" || mode === "previous-update") { + return; + } + + // Fetch historical data every 5 minutes for hour-ago mode + // and every 15 minutes for day modes + const interval = mode === "hour-ago" ? 300 : 900; + + this._historyFetchTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, interval, () => { + this._fetchHistoricalRatesIfNeeded(); + return GLib.SOURCE_CONTINUE; + }); + } + _onSettingsChanged() { this._startUpdateTimer(); + this._startHistoryFetchTimer(); this._fetchAllRates(); + this._fetchHistoricalRatesIfNeeded(); } async _fetchAllRates() { + const mode = this._settings.get_string("price-change-mode"); + + const ratesBeforeFetch = {}; + if (mode === "previous-update") { + for (const category of CATEGORIES) { + if (this._rates[category]) { + ratesBeforeFetch[category] = { ...this._rates[category] }; + } + } + } + for (const category of CATEGORIES) { if (this._hasWatchedItems(category)) { await this._fetchRates(category); } } + // For previous-update mode: only update previousRates when backend timestamp changes + if (mode === "previous-update") { + for (const category of CATEGORIES) { + if (this._rates[category] && this._timestamps[category]) { + const currentTimestamp = this._getRelevantTimestamp(category); + const lastKnownTimestamp = this._lastKnownTimestamps[category]; + + // Check if the backend has actually updated the prices + if (currentTimestamp && currentTimestamp !== lastKnownTimestamp) { + // Backend has new data - the rates we had before this fetch become "previous" + if (ratesBeforeFetch[category]) { + if (!this._previousRates[category]) { + this._previousRates[category] = {}; + } + if (!this._previousTimestamps[category]) { + this._previousTimestamps[category] = {}; + } + + for (const symbol of Object.keys(ratesBeforeFetch[category])) { + this._previousRates[category][symbol] = ratesBeforeFetch[category][symbol]; + this._previousTimestamps[category][symbol] = lastKnownTimestamp; + } + } + + // Update the last known timestamp + this._lastKnownTimestamps[category] = currentTimestamp; + } + } + } + } + this._updateDisplay(); } + _getRelevantTimestamp(category) { + const timestamps = this._timestamps[category]; + if (!timestamps) return null; + + switch (category) { + case "fiat": + return timestamps.currency; + case "metals": + return timestamps.metal || timestamps.currency; + case "crypto": + return timestamps.crypto || timestamps.currency; + case "stocks": + return timestamps.stock || timestamps.currency; + default: + return null; + } + } + _hasWatchedItems(category) { const watched = this._getWatchedCategory(category); return watched.length > 0; @@ -345,6 +464,188 @@ const RabbitForexIndicator = GObject.registerClass( } } + async _fetchHistoricalRatesIfNeeded() { + const mode = this._settings.get_string("price-change-mode"); + + if (mode === "none" || mode === "previous-update") { + return; + } + + for (const category of CATEGORIES) { + const watched = this._getWatchedCategory(category); + const panelSymbols = this._getPanelCategory(category); + const allSymbols = [...new Set([...watched, ...panelSymbols])]; + + for (const symbol of allSymbols) { + await this._fetchHistoricalRate(category, symbol); + } + } + + this._updateDisplay(); + } + + async _fetchHistoricalRate(category, symbol) { + const url = this._getHistoryEndpoint(category, symbol); + if (!url) return; + + try { + const message = Soup.Message.new("GET", url); + + const bytes = await new Promise((resolve, reject) => { + this._httpSession.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null, (session, result) => { + try { + const bytes = session.send_and_read_finish(result); + resolve(bytes); + } catch (e) { + reject(e); + } + }); + }); + + if (message.status_code !== 200) { + return; + } + + const decoder = new TextDecoder("utf-8"); + const text = decoder.decode(bytes.get_data()); + const data = JSON.parse(text); + + const referencePrice = this._extractReferencePrice(data, category); + if (referencePrice !== null) { + if (!this._referenceRates[category]) { + this._referenceRates[category] = {}; + } + this._referenceRates[category][symbol] = referencePrice; + } + } catch (error) {} + } + + _extractReferencePrice(historyData, category) { + const mode = this._settings.get_string("price-change-mode"); + const dataPoints = historyData.data; + + if (!dataPoints || dataPoints.length === 0) { + return null; + } + + const now = new Date(); + + if (mode === "hour-ago") { + // Find the data point closest to 1 hour ago + const targetTime = new Date(now.getTime() - 60 * 60 * 1000); + return this._findPriceAtOrBefore(dataPoints, targetTime, category); + } else if (mode === "day-start") { + // Find the data point for the start of today (00:00 UTC) + const startOfDay = new Date(now); + startOfDay.setUTCHours(0, 0, 0, 0); + return this._findPriceAtOrBefore(dataPoints, startOfDay, category); + } else if (mode === "day-ago") { + // Find the data point closest to 24 hours ago + const targetTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); + return this._findPriceAtOrBefore(dataPoints, targetTime, category); + } + + return null; + } + + _findPriceAtOrBefore(dataPoints, targetTime, category) { + const target = targetTime.getTime(); + + for (let i = dataPoints.length - 1; i >= 0; i--) { + const t = Date.parse(dataPoints[i].timestamp); + if (t <= target) { + const price = dataPoints[i].price ?? dataPoints[i].open ?? dataPoints[i].avg; + if (price === undefined) return null; + return category === "fiat" ? 1 / price : price; + } + } + + return null; + } + + _getPriceDirection(category, symbol, currentRate) { + const mode = this._settings.get_string("price-change-mode"); + + if (mode === "none") { + return PRICE_DIRECTION.UNCHANGED; + } + + let referenceRate; + + if (mode === "previous-update") { + referenceRate = this._previousRates[category]?.[symbol]; + } else { + const referencePrice = this._referenceRates[category]?.[symbol]; + if (referencePrice !== undefined) { + if (category === "fiat") { + referenceRate = referencePrice; + } else { + referenceRate = 1 / referencePrice; + } + } + } + + if (referenceRate === undefined) { + return PRICE_DIRECTION.UNCHANGED; + } + + const currentPrice = this._getRawPrice(currentRate, category); + const referencePrice = this._getRawPrice(referenceRate, category); + + const minPercentThreshold = 0.01; + const percentChange = referencePrice !== 0 ? Math.abs((currentPrice - referencePrice) / referencePrice) * 100 : 0; + + if (percentChange < minPercentThreshold) { + return PRICE_DIRECTION.UNCHANGED; + } else if (currentPrice > referencePrice) { + return PRICE_DIRECTION.UP; + } else { + return PRICE_DIRECTION.DOWN; + } + } + + _getPriceChange(category, symbol, currentRate) { + const mode = this._settings.get_string("price-change-mode"); + + if (mode === "none") { + return { change: 0, percent: 0 }; + } + + let referenceRate; + + if (mode === "previous-update") { + referenceRate = this._previousRates[category]?.[symbol]; + } else { + const referencePrice = this._referenceRates[category]?.[symbol]; + if (referencePrice !== undefined) { + if (category === "fiat") { + referenceRate = referencePrice; + } else { + referenceRate = 1 / referencePrice; + } + } + } + + if (referenceRate === undefined) { + return { change: 0, percent: 0 }; + } + + const currentPrice = this._getRawPrice(currentRate, category); + const referencePrice = this._getRawPrice(referenceRate, category); + + const change = currentPrice - referencePrice; + const percent = referencePrice !== 0 ? (change / referencePrice) * 100 : 0; + + return { change, percent }; + } + + _applyTemplate(template, symbol, formattedRate, change, percent) { + const changeStr = this._formatNumber(Math.abs(change)); + const percentStr = Math.abs(percent).toFixed(2); + + return template.replace("{symbol}", symbol).replace("{rate}", formattedRate).replace("{change}", changeStr).replace("{percent}", percentStr); + } + _updateDisplay() { this._updatePanelLabel(); this._updateMenuRates(); @@ -356,6 +657,8 @@ const RabbitForexIndicator = GObject.registerClass( const showCurrencyInPanel = this._settings.get_boolean("show-currency-in-panel"); const panelSeparator = this._settings.get_string("panel-separator"); const panelItemTemplate = this._settings.get_string("panel-item-template"); + const panelItemTemplateUp = this._settings.get_string("panel-item-template-up"); + const panelItemTemplateDown = this._settings.get_string("panel-item-template-down"); const sortOrder = this._settings.get_string("panel-sort-order"); const allPanelItems = []; @@ -370,8 +673,20 @@ const RabbitForexIndicator = GObject.registerClass( const rate = this._rates[category][symbol]; const price = this._getRawPrice(rate, category); const formattedRate = this._formatPanelRate(rate, category, symbol, showCurrencyInPanel); - const panelItem = panelItemTemplate.replace("{symbol}", symbol).replace("{rate}", formattedRate); - allPanelItems.push({ symbol, price, panelItem }); + const direction = this._getPriceDirection(category, symbol, rate); + const { change, percent } = this._getPriceChange(category, symbol, rate); + + let template; + if (direction === PRICE_DIRECTION.UP) { + template = panelItemTemplateUp; + } else if (direction === PRICE_DIRECTION.DOWN) { + template = panelItemTemplateDown; + } else { + template = panelItemTemplate; + } + + const panelItem = this._applyTemplate(template, symbol, formattedRate, change, percent); + allPanelItems.push({ symbol, price, panelItem, direction }); } } } @@ -408,6 +723,8 @@ const RabbitForexIndicator = GObject.registerClass( const primaryCurrency = this._settings.get_string("primary-currency"); const metalsUnit = this._settings.get_string("metals-unit"); const menuItemTemplate = this._settings.get_string("menu-item-template"); + const menuItemTemplateUp = this._settings.get_string("menu-item-template-up"); + const menuItemTemplateDown = this._settings.get_string("menu-item-template-down"); let hasAnyRates = false; @@ -441,9 +758,22 @@ const RabbitForexIndicator = GObject.registerClass( const rate = this._rates[category][symbol]; const rawPrice = this._getRawPrice(rate, category); const displayRate = this._formatDisplayRate(rate, category, symbol, primaryCurrency); - const menuItemText = menuItemTemplate.replace("{symbol}", symbol).replace("{rate}", displayRate); + const direction = this._getPriceDirection(category, symbol, rate); + const { change, percent } = this._getPriceChange(category, symbol, rate); + + let template; + if (direction === PRICE_DIRECTION.UP) { + template = menuItemTemplateUp; + } else if (direction === PRICE_DIRECTION.DOWN) { + template = menuItemTemplateDown; + } else { + template = menuItemTemplate; + } + + const menuItemText = this._applyTemplate(template, symbol, displayRate, change, percent); - const rateItem = new PopupMenu.PopupMenuItem(` ${menuItemText}`, { reactive: true }); + const rateItem = new PopupMenu.PopupMenuItem(` `, { reactive: true }); + rateItem.label.clutter_text.set_markup(` ${menuItemText}`); rateItem.connect("activate", () => { const clipboardText = this._getClipboardText(symbol, rawPrice, displayRate, primaryCurrency, category); @@ -456,7 +786,7 @@ const RabbitForexIndicator = GObject.registerClass( this._ratesSection.addMenuItem(rateItem); } else { - const menuItemText = menuItemTemplate.replace("{symbol}", symbol).replace("{rate}", "N/A"); + const menuItemText = this._applyTemplate(menuItemTemplate, symbol, "N/A", 0, 0); const rateItem = new PopupMenu.PopupMenuItem(` ${menuItemText}`, { reactive: false }); this._ratesSection.addMenuItem(rateItem); } @@ -653,6 +983,11 @@ const RabbitForexIndicator = GObject.registerClass( this._updateTimeout = null; } + if (this._historyFetchTimeout) { + GLib.source_remove(this._historyFetchTimeout); + this._historyFetchTimeout = null; + } + if (this._settingsChangedId) { this._settings.disconnect(this._settingsChangedId); this._settingsChangedId = null; @@ -690,10 +1025,18 @@ export default class RabbitForexExtension extends Extension { _addIndicator(preserveState = false) { let rates = {}; let timestamps = {}; + let previousRates = {}; + let previousTimestamps = {}; + let lastKnownTimestamps = {}; + let referenceRates = {}; if (preserveState && this._indicator) { rates = this._indicator._rates; timestamps = this._indicator._timestamps; + previousRates = this._indicator._previousRates; + previousTimestamps = this._indicator._previousTimestamps; + lastKnownTimestamps = this._indicator._lastKnownTimestamps; + referenceRates = this._indicator._referenceRates; this._indicator.destroy(); this._indicator = null; } @@ -703,6 +1046,10 @@ export default class RabbitForexExtension extends Extension { if (preserveState) { this._indicator._rates = rates; this._indicator._timestamps = timestamps; + this._indicator._previousRates = previousRates; + this._indicator._previousTimestamps = previousTimestamps; + this._indicator._lastKnownTimestamps = lastKnownTimestamps; + this._indicator._referenceRates = referenceRates; } const position = this._settings.get_string("panel-position"); diff --git a/gnome-extension/metadata.json b/gnome-extension/metadata.json index 5ffacc0..959edd3 100644 --- a/gnome-extension/metadata.json +++ b/gnome-extension/metadata.json @@ -3,7 +3,7 @@ "description": "Monitor exchange rates for fiat currencies, precious metals, cryptocurrencies and stocks.\n\nClick on any rate to copy it to clipboard.", "uuid": "rabbitforex@rabbit-company.com", "shell-version": ["47", "48", "49"], - "version-name": "1.6.0", + "version-name": "1.7.0", "settings-schema": "org.gnome.shell.extensions.rabbitforex", "url": "https://github.com/Rabbit-Company/RabbitForexAPI", "donations": { diff --git a/gnome-extension/prefs.js b/gnome-extension/prefs.js index 6e2faaf..43ee984 100644 --- a/gnome-extension/prefs.js +++ b/gnome-extension/prefs.js @@ -226,6 +226,14 @@ const PANEL_POSITION_OPTIONS = [ { id: "right", label: "Right" }, ]; +const PRICE_CHANGE_MODES = [ + { id: "none", label: "Disabled" }, + { id: "previous-update", label: "Previous update" }, + { id: "hour-ago", label: "1 hour ago" }, + { id: "day-start", label: "Start of day" }, + { id: "day-ago", label: "24 hours ago" }, +]; + export default class RabbitForexPreferences extends ExtensionPreferences { fillPreferencesWindow(window) { const settings = this.getSettings(); @@ -244,7 +252,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); generalPage.add(currencyGroup); - // Primary currency dropdown const currencyModel = new Gtk.StringList(); for (const currency of COMMON_FIATS) { currencyModel.append(currency); @@ -256,7 +263,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { model: currencyModel, }); - // Set current value const currentCurrency = settings.get_string("primary-currency"); const currencyIndex = COMMON_FIATS.indexOf(currentCurrency); if (currencyIndex >= 0) { @@ -276,7 +282,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); generalPage.add(panelGroup); - // Show currency in panel toggle const showCurrencyInPanelRow = new Adw.SwitchRow({ title: "Show Currency in Panel", subtitle: "Display currency symbol/code alongside rates in the panel", @@ -287,7 +292,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); panelGroup.add(showCurrencyInPanelRow); - // Max panel items const maxPanelRow = new Adw.SpinRow({ title: "Max Panel Items", subtitle: "Maximum number of rates to show in the panel", @@ -304,7 +308,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); panelGroup.add(maxPanelRow); - // Panel sort order dropdown const sortModel = new Gtk.StringList(); for (const option of PANEL_SORT_OPTIONS) { sortModel.append(option.label); @@ -326,7 +329,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); panelGroup.add(sortRow); - // Panel position dropdown const panelPositionModel = new Gtk.StringList(); for (const option of PANEL_POSITION_OPTIONS) { panelPositionModel.append(option.label); @@ -348,7 +350,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); panelGroup.add(panelPositionRow); - // Panel index (order within the box) const panelIndexRow = new Adw.SpinRow({ title: "Panel Index", subtitle: "Order within panel area", @@ -365,34 +366,76 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); panelGroup.add(panelIndexRow); - // Panel separator - const separatorRow = new Adw.EntryRow({ - title: "Panel Separator", - }); + const separatorRow = new Adw.EntryRow({ title: "Panel Separator" }); separatorRow.text = settings.get_string("panel-separator"); separatorRow.connect("changed", () => { settings.set_string("panel-separator", separatorRow.text); }); panelGroup.add(separatorRow); - // Panel item template - const templateRow = new Adw.EntryRow({ - title: "Panel Item Template", - }); + const templateRow = new Adw.EntryRow({ title: "Panel Item Template" }); templateRow.text = settings.get_string("panel-item-template"); templateRow.connect("changed", () => { settings.set_string("panel-item-template", templateRow.text); }); panelGroup.add(templateRow); - // Panel template help text const panelTemplateHelpRow = new Adw.ActionRow({ title: "Template Placeholders", - subtitle: "Use {symbol} and {rate}. Supports Pango markup for colors.", + subtitle: "Use {symbol}, {rate}, {change}, {percent}. Supports Pango markup.", }); panelTemplateHelpRow.sensitive = false; panelGroup.add(panelTemplateHelpRow); + // Price Change Indicator Group + const priceChangeGroup = new Adw.PreferencesGroup({ + title: "Price Change Indicator", + description: "Configure how price changes are displayed", + }); + generalPage.add(priceChangeGroup); + + const priceChangeModeModel = new Gtk.StringList(); + for (const mode of PRICE_CHANGE_MODES) { + priceChangeModeModel.append(mode.label); + } + + const priceChangeModeRow = new Adw.ComboRow({ + title: "Price Change Mode", + subtitle: "How to determine if price increased or decreased", + model: priceChangeModeModel, + }); + + const currentPriceChangeMode = settings.get_string("price-change-mode"); + const priceChangeModeIndex = PRICE_CHANGE_MODES.findIndex((m) => m.id === currentPriceChangeMode); + priceChangeModeRow.selected = priceChangeModeIndex >= 0 ? priceChangeModeIndex : 0; + + priceChangeModeRow.connect("notify::selected", () => { + const selected = PRICE_CHANGE_MODES[priceChangeModeRow.selected].id; + settings.set_string("price-change-mode", selected); + }); + priceChangeGroup.add(priceChangeModeRow); + + const templateUpRow = new Adw.EntryRow({ title: "Panel Template (Price Up)" }); + templateUpRow.text = settings.get_string("panel-item-template-up"); + templateUpRow.connect("changed", () => { + settings.set_string("panel-item-template-up", templateUpRow.text); + }); + priceChangeGroup.add(templateUpRow); + + const templateDownRow = new Adw.EntryRow({ title: "Panel Template (Price Down)" }); + templateDownRow.text = settings.get_string("panel-item-template-down"); + templateDownRow.connect("changed", () => { + settings.set_string("panel-item-template-down", templateDownRow.text); + }); + priceChangeGroup.add(templateDownRow); + + const priceChangeHelpRow = new Adw.ActionRow({ + title: "Template Placeholders", + subtitle: "Use {symbol}, {rate}, {change}, {percent}. Supports Pango markup.", + }); + priceChangeHelpRow.sensitive = false; + priceChangeGroup.add(priceChangeHelpRow); + // Menu Settings Group const menuGroup = new Adw.PreferencesGroup({ title: "Menu Settings", @@ -400,20 +443,30 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); generalPage.add(menuGroup); - // Menu item template - const menuTemplateRow = new Adw.EntryRow({ - title: "Menu Item Template", - }); + const menuTemplateRow = new Adw.EntryRow({ title: "Menu Item Template" }); menuTemplateRow.text = settings.get_string("menu-item-template"); menuTemplateRow.connect("changed", () => { settings.set_string("menu-item-template", menuTemplateRow.text); }); menuGroup.add(menuTemplateRow); - // Menu template help text + const menuTemplateUpRow = new Adw.EntryRow({ title: "Menu Template (Price Up)" }); + menuTemplateUpRow.text = settings.get_string("menu-item-template-up"); + menuTemplateUpRow.connect("changed", () => { + settings.set_string("menu-item-template-up", menuTemplateUpRow.text); + }); + menuGroup.add(menuTemplateUpRow); + + const menuTemplateDownRow = new Adw.EntryRow({ title: "Menu Template (Price Down)" }); + menuTemplateDownRow.text = settings.get_string("menu-item-template-down"); + menuTemplateDownRow.connect("changed", () => { + settings.set_string("menu-item-template-down", menuTemplateDownRow.text); + }); + menuGroup.add(menuTemplateDownRow); + const menuTemplateHelpRow = new Adw.ActionRow({ title: "Template Placeholders", - subtitle: "Use {symbol} for the symbol name and {rate} for the formatted rate", + subtitle: "Use {symbol}, {rate}, {change}, {percent}. Supports Pango markup.", }); menuTemplateHelpRow.sensitive = false; menuGroup.add(menuTemplateHelpRow); @@ -425,7 +478,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); generalPage.add(formatGroup); - // Number format dropdown const formatModel = new Gtk.StringList(); for (const format of NUMBER_FORMATS) { formatModel.append(format.label); @@ -447,7 +499,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); formatGroup.add(formatRow); - // Decimal places const decimalRow = new Adw.SpinRow({ title: "Decimal Places", subtitle: "Number of decimal places to display", @@ -471,7 +522,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); generalPage.add(symbolsGroup); - // Use currency symbols toggle const useSymbolsRow = new Adw.SwitchRow({ title: "Use Currency Symbols", subtitle: "Display € instead of EUR, $ instead of USD, etc.", @@ -482,7 +532,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); symbolsGroup.add(useSymbolsRow); - // Symbol position dropdown const positionModel = new Gtk.StringList(); for (const pos of SYMBOL_POSITIONS) { positionModel.append(pos.label); @@ -511,7 +560,16 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); generalPage.add(clipboardGroup); - // Clipboard format dropdown + const clipboardNotificationRow = new Adw.SwitchRow({ + title: "Show Notification", + subtitle: "Display a notification when a rate is copied to clipboard", + }); + clipboardNotificationRow.active = settings.get_boolean("clipboard-notification"); + clipboardNotificationRow.connect("notify::active", () => { + settings.set_boolean("clipboard-notification", clipboardNotificationRow.active); + }); + clipboardGroup.add(clipboardNotificationRow); + const clipboardModel = new Gtk.StringList(); for (const format of CLIPBOARD_FORMATS) { clipboardModel.append(format.label); @@ -523,17 +581,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { model: clipboardModel, }); - // Clipboard notification toggle - const clipboardNotificationRow = new Adw.SwitchRow({ - title: "Show Notification", - subtitle: "Display a notification when a rate is copied to clipboard", - }); - clipboardNotificationRow.active = settings.get_boolean("clipboard-notification"); - clipboardNotificationRow.connect("notify::active", () => { - settings.set_boolean("clipboard-notification", clipboardNotificationRow.active); - }); - clipboardGroup.add(clipboardNotificationRow); - const currentClipboard = settings.get_string("clipboard-format"); const clipboardIndex = CLIPBOARD_FORMATS.findIndex((f) => f.id === currentClipboard); clipboardRow.selected = clipboardIndex >= 0 ? clipboardIndex : 0; @@ -544,17 +591,13 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); clipboardGroup.add(clipboardRow); - // Clipboard template - const clipboardTemplateRow = new Adw.EntryRow({ - title: "Clipboard Template", - }); + const clipboardTemplateRow = new Adw.EntryRow({ title: "Clipboard Template" }); clipboardTemplateRow.text = settings.get_string("clipboard-template"); clipboardTemplateRow.connect("changed", () => { settings.set_string("clipboard-template", clipboardTemplateRow.text); }); clipboardGroup.add(clipboardTemplateRow); - // Clipboard template help text const clipboardTemplateHelpRow = new Adw.ActionRow({ title: "Template Placeholders", subtitle: "Used when format is 'As displayed'. Use {symbol} and {rate}.", @@ -569,7 +612,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); generalPage.add(metalsGroup); - // Metals unit dropdown const unitModel = new Gtk.StringList(); unitModel.append("Gram"); unitModel.append("Troy Ounce"); @@ -580,7 +622,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { model: unitModel, }); - // Set current value const currentUnit = settings.get_string("metals-unit"); unitRow.selected = currentUnit === "troy-ounce" ? 1 : 0; @@ -597,7 +638,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); generalPage.add(updateGroup); - // Update interval const intervalRow = new Adw.SpinRow({ title: "Update Interval", subtitle: "How often to fetch new rates (in seconds)", @@ -643,11 +683,7 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); page.add(watchedGroup); - // Current watched symbols display - const watchedEntry = new Adw.EntryRow({ - title: "Symbols (comma-separated)", - }); - + const watchedEntry = new Adw.EntryRow({ title: "Symbols (comma-separated)" }); const watched = settings.get_strv(`watched-${category}`); watchedEntry.text = watched.join(", "); @@ -668,10 +704,7 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); page.add(panelSymbolsGroup); - const panelEntry = new Adw.EntryRow({ - title: "Panel Symbols (comma-separated)", - }); - + const panelEntry = new Adw.EntryRow({ title: "Panel Symbols (comma-separated)" }); const panelSymbols = settings.get_strv(`panel-${category}`); panelEntry.text = panelSymbols.join(", "); @@ -692,7 +725,6 @@ export default class RabbitForexPreferences extends ExtensionPreferences { }); page.add(popularGroup); - // Create a flow box for popular symbol buttons const flowBox = new Gtk.FlowBox({ selection_mode: Gtk.SelectionMode.NONE, homogeneous: true, @@ -754,14 +786,12 @@ export default class RabbitForexPreferences extends ExtensionPreferences { fetchRow.add_suffix(fetchButton); fetchGroup.add(fetchRow); - // Available symbols list (expandable) const availableExpander = new Adw.ExpanderRow({ title: "Available Symbols", subtitle: 'Click "Fetch" to load symbols', }); fetchGroup.add(availableExpander); - // Store reference to clear rows later let symbolRows = []; fetchButton.connect("clicked", async () => { @@ -772,21 +802,15 @@ export default class RabbitForexPreferences extends ExtensionPreferences { try { const symbols = await this._fetchAvailableSymbols(category); - // Clear ALL existing rows first for (const row of symbolRows) { try { availableExpander.remove(row); - } catch (e) { - // Row might already be removed - } + } catch (e) {} } symbolRows = []; - const displaySymbols = symbols; - for (const symbol of displaySymbols) { - const symbolRow = new Adw.ActionRow({ - title: symbol, - }); + for (const symbol of symbols) { + const symbolRow = new Adw.ActionRow({ title: symbol }); const addButton = new Gtk.Button({ icon_name: "list-add-symbolic", diff --git a/gnome-extension/schemas/org.gnome.shell.extensions.rabbitforex.gschema.xml b/gnome-extension/schemas/org.gnome.shell.extensions.rabbitforex.gschema.xml index c790121..dc05a01 100644 --- a/gnome-extension/schemas/org.gnome.shell.extensions.rabbitforex.gschema.xml +++ b/gnome-extension/schemas/org.gnome.shell.extensions.rabbitforex.gschema.xml @@ -35,10 +35,9 @@ - 'none' + 'price-desc' Panel sort order - How to sort panel items: none (as configured), symbol-asc, symbol-desc, - price-asc, price-desc + How to sort panel items: none, symbol-asc, symbol-desc, price-asc, price-desc @@ -62,14 +61,45 @@ ' <span color="#BFBFBF">{rate}</span> <span color="#7F7F7F">{symbol}</span> ' Panel item template - Template for displaying items in the panel. Use {symbol} and {rate} as placeholders. + Template for displaying items in the panel. Use {symbol}, {rate}, {change}, {percent} as placeholders. + + + + + 'previous-update' + Price change indicator mode + How to determine price change: none, previous-update, hour-ago, day-start, day-ago + + + + ' <span color="#69F0AE">{rate}</span> <span color="#7F7F7F">{symbol}</span> ' + Panel item template for price increase + Template for displaying items when price has increased. Use {symbol}, {rate}, {change}, {percent} as placeholders. + + + + ' <span color="#FF6B6B">{rate}</span> <span color="#7F7F7F">{symbol}</span> ' + Panel item template for price decrease + Template for displaying items when price has decreased. Use {symbol}, {rate}, {change}, {percent} as placeholders. - '{symbol}: {rate}' + '<span color="#FFFFFF">{symbol}: {rate}</span>' Menu item template - Template for displaying items in the dropdown menu. Use {symbol} and {rate} as placeholders. + Template for displaying items in the dropdown menu. Use {symbol}, {rate}, {change}, {percent} as placeholders. + + + + '<span color="#69F0AE">{symbol}: {rate} (+{percent}%)</span>' + Menu item template for price increase + Template for displaying items in menu when price has increased. + + + + '<span color="#FF6B6B">{symbol}: {rate} (-{percent}%)</span>' + Menu item template for price decrease + Template for displaying items in menu when price has decreased. @@ -82,9 +112,7 @@ '{symbol}: {rate}' Clipboard template - Template for copying to clipboard when using display format. Use {symbol} - and - {rate} as placeholders. + Template for copying to clipboard when using display format. Use {symbol} and {rate} as placeholders.