'use strict'; import St from 'gi://St'; import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import Gio from 'gi://Gio'; import Soup from 'gi://Soup'; import GObject from 'gi://GObject'; import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; // Simple hash function for quote IDs function simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16); } class QuotesExtension extends Extension { constructor(metadata) { super(metadata); this._quotes = []; this._currentQuoteIndex = 0; this._rotationTimer = null; this._syncTimer = null; this._indicator = null; this._session = null; this._dataDir = null; this._quotesFile = null; this._settings = null; this._isPaused = false; } enable() { this._initializeSettings(); this._initializeDataDir(); this._processCustomQuotes(); this._loadQuotes(); this._createIndicator(); this._setupTimers(); this._syncQuotes(); } disable() { if (this._rotationTimer) { GLib.source_remove(this._rotationTimer); this._rotationTimer = null; } if (this._syncTimer) { GLib.source_remove(this._syncTimer); this._syncTimer = null; } if (this._indicator) { this._indicator.destroy(); this._indicator = null; } if (this._session) { this._session = null; } } _initializeDataDir() { this._dataDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'quotes-extension']); GLib.mkdir_with_parents(this._dataDir, 0o755); const fileName = this._settings ? this._settings.get_string('quotes-file') || 'quotes.json' : 'quotes.json'; this._quotesFile = GLib.build_filenamev([this._dataDir, fileName]); } _initializeSettings() { 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._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._quoteSources = { synced: [], custom: [] }; // Listen for settings changes this._settings.connect('changed', (settings, key) => { if (key === 'font-size' || key === 'text-color') { this._updateLabelStyle(); } else if (key === 'max-width') { this._maxWidth = this._settings.get_int('max-width') || 60; this._label.set_width(this._maxWidth * 8); this._label.set_text(this._getCurrentQuote()); } else if (key === 'quotes-file') { this._quotesFileName = this._settings.get_string('quotes-file') || 'quotes.json'; this._initializeDataDir(); this._loadQuotes(); } else if (key === 'use-custom-quotes' || key === 'custom-quotes') { this._useCustomQuotes = this._settings.get_boolean('use-custom-quotes'); this._customQuotes = this._settings.get_string('custom-quotes') || '[]'; this._processCustomQuotes(); this._mergeAndSaveQuotes(); this._loadQuotes(); if (this._label) { this._label.set_text(this._getCurrentQuote()); } } }); } _loadQuotes() { this._quotes = []; // Always load from file (contains both synced and custom quotes) try { if (GLib.file_test(this._quotesFile, GLib.FileTest.EXISTS)) { const [success, contents] = GLib.file_get_contents(this._quotesFile); if (success) { const decoder = new TextDecoder(); const fileData = JSON.parse(decoder.decode(contents)); // Handle both old format (array) and new format (object with sources) if (Array.isArray(fileData)) { this._quotes = fileData; } else if (fileData.synced && fileData.custom) { this._quoteSources = fileData; // Combine quotes based on use-custom-quotes setting if (this._useCustomQuotes) { this._quotes = [...this._quoteSources.custom, ...this._quoteSources.synced]; } else { this._quotes = [...this._quoteSources.synced, ...this._quoteSources.custom]; } } } } } catch (e) { console.error('Failed to load quotes:', e); } if (this._quotes.length === 0) { const defaultQuotes = [ { text: "The only way to do great work is to love what you do.", author: "Steve Jobs" }, { text: "Life is what happens to you while you're busy making other plans.", author: "John Lennon" }, { text: "The future belongs to those who believe in the beauty of their dreams.", author: "Eleanor Roosevelt" }, { text: "It is during our darkest moments that we must focus to see the light.", author: "Aristotle" }, { text: "Success is not final, failure is not fatal: it is the courage to continue that counts.", author: "Winston Churchill" }, { text: "The way to get started is to quit talking and begin doing.", author: "Walt Disney" }, { text: "Don't be afraid to give up the good to go for the great.", author: "John D. Rockefeller" }, { text: "Innovation distinguishes between a leader and a follower.", author: "Steve Jobs" }, { text: "Your time is limited, don't waste it living someone else's life.", author: "Steve Jobs" }, { text: "If you look at what you have in life, you'll always have more.", author: "Oprah Winfrey" }, { text: "The only impossible journey is the one you never begin.", author: "Tony Robbins" }, { text: "In the midst of winter, I found there was, within me, an invincible summer.", author: "Albert Camus" }, { text: "Be yourself; everyone else is already taken.", author: "Oscar Wilde" }, { text: "Two things are infinite: the universe and human stupidity; and I'm not sure about the universe.", author: "Albert Einstein" }, { text: "You miss 100% of the shots you don't take.", author: "Wayne Gretzky" }, { text: "Whether you think you can or you think you can't, you're right.", author: "Henry Ford" }, { text: "I have learned throughout my life as a composer chiefly through my mistakes and pursuits of false assumptions.", author: "Igor Stravinsky" }, { text: "The greatest glory in living lies not in never falling, but in rising every time we fall.", author: "Nelson Mandela" }, { text: "The only person you are destined to become is the person you decide to be.", author: "Ralph Waldo Emerson" }, { text: "What lies behind us and what lies before us are tiny matters compared to what lies within us.", author: "Ralph Waldo Emerson" }, { text: "Believe you can and you're halfway there.", author: "Theodore Roosevelt" }, { text: "The best time to plant a tree was 20 years ago. The second best time is now.", author: "Chinese Proverb" }, { text: "Don't judge each day by the harvest you reap but by the seeds that you plant.", author: "Robert Louis Stevenson" }, { text: "The only way to make sense out of change is to plunge into it, move with it, and join the dance.", author: "Alan Watts" }, { text: "Yesterday is history, tomorrow is a mystery, today is a gift of God, which is why we call it the present.", author: "Bill Keane" } ]; // Add hashes to default quotes and mark as synced this._quoteSources.synced = defaultQuotes.map(quote => ({ ...quote, hash: simpleHash(quote.text + quote.author), source: 'default' })); this._quotes = this._quoteSources.synced; this._saveQuotes(); } } _saveQuotes() { try { const contents = JSON.stringify(this._quoteSources, null, 2); GLib.file_set_contents(this._quotesFile, contents); } catch (e) { console.error('Failed to save quotes:', e); } } _processCustomQuotes() { try { const customQuotes = JSON.parse(this._customQuotes); if (Array.isArray(customQuotes)) { this._quoteSources.custom = customQuotes.map(quote => ({ ...quote, hash: simpleHash(quote.text + quote.author), source: 'custom' })); } } catch (e) { console.error('Failed to process custom quotes:', e); this._quoteSources.custom = []; } } _mergeAndSaveQuotes() { // Remove duplicates based on hash const existingHashes = new Set(); this._quoteSources.synced = this._quoteSources.synced.filter(quote => { if (existingHashes.has(quote.hash)) { return false; } existingHashes.add(quote.hash); return true; }); this._quoteSources.custom = this._quoteSources.custom.filter(quote => { if (existingHashes.has(quote.hash)) { return false; } existingHashes.add(quote.hash); return true; }); this._saveQuotes(); } _createIndicator() { this._indicator = new PanelMenu.Button(0.0, 'Quotes', false); this._label = new St.Label({ text: this._getCurrentQuote(), y_align: Clutter.ActorAlign.CENTER, style_class: 'quote-label' }); this._updateLabelStyle(); this._label.set_width(this._maxWidth * 8); // Set viewport width based on maxWidth this._label.clutter_text.set_ellipsize(3); // PANGO_ELLIPSIZE_END this._indicator.add_child(this._label); Main.panel.addToStatusArea('quotes', this._indicator, 1, 'left'); // 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 } return Clutter.EVENT_PROPAGATE; }); } _createPopupMenu() { // Toggle pause/resume this._pauseMenuItem = new PopupMenu.PopupMenuItem('Pause Rotation'); this._pauseMenuItem.connect('activate', () => { this._togglePause(); }); this._indicator.menu.addMenuItem(this._pauseMenuItem); // Next quote const nextMenuItem = new PopupMenu.PopupMenuItem('Next Quote'); nextMenuItem.connect('activate', () => { this._nextQuote(); }); this._indicator.menu.addMenuItem(nextMenuItem); // Separator this._indicator.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); // Delay options const delaySubmenu = new PopupMenu.PopupSubMenuMenuItem('Delay Next Quote'); const delay30s = new PopupMenu.PopupMenuItem('30 seconds'); delay30s.connect('activate', () => this._delayRotation(30)); delaySubmenu.menu.addMenuItem(delay30s); const delay1m = new PopupMenu.PopupMenuItem('1 minute'); delay1m.connect('activate', () => this._delayRotation(60)); delaySubmenu.menu.addMenuItem(delay1m); const delay5m = new PopupMenu.PopupMenuItem('5 minutes'); delay5m.connect('activate', () => this._delayRotation(300)); delaySubmenu.menu.addMenuItem(delay5m); const delay15m = new PopupMenu.PopupMenuItem('15 minutes'); delay15m.connect('activate', () => this._delayRotation(900)); delaySubmenu.menu.addMenuItem(delay15m); this._indicator.menu.addMenuItem(delaySubmenu); } _togglePause() { this._isPaused = !this._isPaused; if (this._isPaused) { this._pauseMenuItem.label.text = 'Resume Rotation'; if (this._rotationTimer) { GLib.source_remove(this._rotationTimer); this._rotationTimer = null; } } else { this._pauseMenuItem.label.text = 'Pause Rotation'; this._setupRotationTimer(); } } _delayRotation(seconds) { if (this._rotationTimer) { GLib.source_remove(this._rotationTimer); this._rotationTimer = null; } this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, seconds, () => { if (!this._isPaused) { this._nextQuote(); this._setupRotationTimer(); } return GLib.SOURCE_REMOVE; }); } _getCurrentQuote() { if (this._quotes.length === 0) return 'No quotes available'; const quote = this._quotes[this._currentQuoteIndex]; let text = `"${quote.text}" - ${quote.author}`; return text; } _updateLabelStyle() { if (!this._label) return; this._fontSize = this._settings.get_int('font-size') || 14; this._textColor = this._settings.get_string('text-color') || '#ffffff'; const style = `font-size: ${this._fontSize}px; color: ${this._textColor};`; this._label.set_style(style); } _nextQuote() { if (this._quotes.length === 0) return; this._currentQuoteIndex = (this._currentQuoteIndex + 1) % this._quotes.length; this._label.set_text(this._getCurrentQuote()); } _setupTimers() { this._setupRotationTimer(); this._setupSyncTimer(); } _setupRotationTimer() { if (this._isPaused) return; this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this._rotationInterval, () => { if (!this._isPaused) { this._nextQuote(); } return GLib.SOURCE_CONTINUE; }); } _setupSyncTimer() { this._syncTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, this._syncInterval, () => { this._syncQuotes(); return GLib.SOURCE_CONTINUE; }); } _syncQuotes() { try { this._session = new Soup.Session(); const message = Soup.Message.new('GET', this._apiUrl); this._session.send_and_read_async(message, GLib.PRIORITY_DEFAULT, null, (session, result) => { try { const bytes = session.send_and_read_finish(result); const decoder = new TextDecoder(); 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(); } } } catch (e) { console.error('Failed to sync quotes:', e); } }); } catch (e) { console.error('Failed to create sync session:', e); } } } export default class QuotesGnomeExtension extends Extension { enable() { this._extension = new QuotesExtension(this.metadata); this._extension.enable(); } disable() { if (this._extension) { this._extension.disable(); this._extension = null; } } }