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.