'use strict'; import Adw from 'gi://Adw'; import Gio from 'gi://Gio'; import Gtk from 'gi://Gtk'; import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; export default class QuotesPreferences extends ExtensionPreferences { fillPreferencesWindow(window) { const settings = this.getSettings(); // Behavior Settings Page const behaviorPage = new Adw.PreferencesPage({ title: 'Behavior', icon_name: 'preferences-system-symbolic', }); window.add(behaviorPage); const timingGroup = new Adw.PreferencesGroup({ title: 'Timing Settings', description: 'Configure how quotes are rotated and synced', }); behaviorPage.add(timingGroup); // Rotation interval const rotationRow = new Adw.SpinRow({ title: 'Rotation Interval', subtitle: 'Time in seconds between quote changes', adjustment: new Gtk.Adjustment({ lower: 10, upper: 3600, step_increment: 10, page_increment: 60, value: settings.get_int('rotation-interval'), }), }); timingGroup.add(rotationRow); // Sync interval const syncRow = new Adw.SpinRow({ title: 'Sync Interval', subtitle: 'Time in seconds between API syncs', adjustment: new Gtk.Adjustment({ lower: 300, upper: 86400, step_increment: 300, page_increment: 3600, value: settings.get_int('sync-interval'), }), }); timingGroup.add(syncRow); // API Group const apiGroup = new Adw.PreferencesGroup({ title: 'API Settings', description: 'Configure external quote sources', }); behaviorPage.add(apiGroup); // API URL const apiRow = new Adw.EntryRow({ title: 'API URL', text: settings.get_string('api-url'), }); apiGroup.add(apiRow); // Appearance Settings Page const appearancePage = new Adw.PreferencesPage({ title: 'Appearance', icon_name: 'preferences-desktop-theme-symbolic', }); window.add(appearancePage); const displayGroup = new Adw.PreferencesGroup({ title: 'Display Settings', description: 'Configure how quotes are displayed', }); appearancePage.add(displayGroup); // Font size const fontSizeRow = new Adw.SpinRow({ title: 'Font Size', subtitle: 'Size of quote text in pixels', adjustment: new Gtk.Adjustment({ lower: 8, upper: 32, step_increment: 1, page_increment: 2, value: settings.get_int('font-size'), }), }); displayGroup.add(fontSizeRow); // Text color const colorRow = new Adw.EntryRow({ title: 'Text Color', text: settings.get_string('text-color'), }); displayGroup.add(colorRow); // Maximum width const widthRow = new Adw.SpinRow({ title: 'Maximum Width', subtitle: 'Maximum characters before truncating', adjustment: new Gtk.Adjustment({ lower: 20, upper: 200, step_increment: 5, page_increment: 10, value: settings.get_int('max-width'), }), }); displayGroup.add(widthRow); // Storage Settings Page const storagePage = new Adw.PreferencesPage({ title: 'Storage', icon_name: 'folder-symbolic', }); window.add(storagePage); const storageGroup = new Adw.PreferencesGroup({ title: 'Storage Settings', description: 'Configure data storage options', }); storagePage.add(storageGroup); // Quotes file const quotesFileRow = new Adw.EntryRow({ title: 'Quotes File Name', text: settings.get_string('quotes-file'), }); storageGroup.add(quotesFileRow); // Quotes Settings Page const quotesPage = new Adw.PreferencesPage({ title: 'Quotes', icon_name: 'text-editor-symbolic', }); window.add(quotesPage); const quotesSourceGroup = new Adw.PreferencesGroup({ title: 'Quote Priority', description: 'Custom quotes and synced quotes are both saved to the JSON file', }); quotesPage.add(quotesSourceGroup); // Use custom quotes switch const useCustomQuotesRow = new Adw.SwitchRow({ title: 'Prioritize Custom Quotes', active: settings.get_boolean('use-custom-quotes'), }); quotesSourceGroup.add(useCustomQuotesRow); const customQuotesGroup = new Adw.PreferencesGroup({ title: 'Custom Quotes', description: 'Manage your custom quotes with an easy-to-use interface.', }); quotesPage.add(customQuotesGroup); // Parse existing custom quotes let customQuotesArray = []; try { customQuotesArray = JSON.parse(settings.get_string('custom-quotes')); } catch (e) { customQuotesArray = []; } // Create list box for quotes const quotesListBox = new Gtk.ListBox({ selection_mode: Gtk.SelectionMode.SINGLE, hexpand: true, margin_top: 12, margin_bottom: 12, margin_start: 12, margin_end: 12, css_classes: ['boxed-list'], }); // Function to create quote row const createQuoteRow = (quote, index) => { const row = new Gtk.ListBoxRow(); const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 6, margin_top: 12, margin_bottom: 12, margin_start: 12, margin_end: 12, }); const quoteLabel = new Gtk.Label({ label: `"${quote.text}"`, wrap: true, xalign: 0, css_classes: ['title-4'], }); const authorLabel = new Gtk.Label({ label: `— ${quote.author}`, xalign: 0, css_classes: ['dim-label'], }); box.append(quoteLabel); box.append(authorLabel); row.set_child(box); row._quoteIndex = index; return row; }; // Populate list with existing quotes const refreshQuotesList = () => { const child = quotesListBox.get_first_child(); while (child) { const next = child.get_next_sibling(); quotesListBox.remove(child); child = next; } customQuotesArray.forEach((quote, index) => { quotesListBox.append(createQuoteRow(quote, index)); }); }; refreshQuotesList(); // Buttons container const buttonsBox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, spacing: 12, margin_top: 12, halign: Gtk.Align.END, }); // Add button const addButton = new Gtk.Button({ label: 'Add Quote', css_classes: ['suggested-action'], }); // Edit button const editButton = new Gtk.Button({ label: 'Edit', sensitive: false, }); // Remove button const removeButton = new Gtk.Button({ label: 'Remove', css_classes: ['destructive-action'], sensitive: false, }); buttonsBox.append(addButton); buttonsBox.append(editButton); buttonsBox.append(removeButton); // Main container const quotesContainer = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 12, }); const scrolledWindow = new Gtk.ScrolledWindow({ child: quotesListBox, hexpand: true, vexpand: true, min_content_height: 200, max_content_height: 300, }); quotesContainer.append(scrolledWindow); quotesContainer.append(buttonsBox); const quotesExpander = new Adw.ExpanderRow({ title: 'Custom Quotes Manager', }); quotesExpander.add_row(new Adw.PreferencesRow({ child: quotesContainer, })); customQuotesGroup.add(quotesExpander); // Handle list selection quotesListBox.connect('row-selected', (listbox, row) => { const hasSelection = row !== null; editButton.sensitive = hasSelection; removeButton.sensitive = hasSelection; }); // Quote dialog function const showQuoteDialog = (quote = null, index = -1) => { const dialog = new Gtk.Dialog({ title: quote ? 'Edit Quote' : 'Add Quote', modal: true, transient_for: window, }); dialog.add_button('Cancel', Gtk.ResponseType.CANCEL); dialog.add_button(quote ? 'Save' : 'Add', Gtk.ResponseType.OK); const contentArea = dialog.get_content_area(); contentArea.set_spacing(12); contentArea.set_margin_top(12); contentArea.set_margin_bottom(12); contentArea.set_margin_start(12); contentArea.set_margin_end(12); const textEntry = new Gtk.Entry({ placeholder_text: 'Enter quote text...', text: quote ? quote.text : '', hexpand: true, }); const authorEntry = new Gtk.Entry({ placeholder_text: 'Enter author name...', text: quote ? quote.author : '', hexpand: true, }); const textLabel = new Gtk.Label({ label: 'Quote Text:', xalign: 0, }); const authorLabel = new Gtk.Label({ label: 'Author:', xalign: 0, }); contentArea.append(textLabel); contentArea.append(textEntry); contentArea.append(authorLabel); contentArea.append(authorEntry); dialog.connect('response', (dialog, response) => { if (response === Gtk.ResponseType.OK) { const text = textEntry.get_text().trim(); const author = authorEntry.get_text().trim(); if (text && author) { const newQuote = { text, author }; if (index >= 0) { customQuotesArray[index] = newQuote; } else { customQuotesArray.push(newQuote); } settings.set_string('custom-quotes', JSON.stringify(customQuotesArray)); refreshQuotesList(); } } dialog.destroy(); }); dialog.present(); }; // Button handlers addButton.connect('clicked', () => { showQuoteDialog(); }); editButton.connect('clicked', () => { const selectedRow = quotesListBox.get_selected_row(); if (selectedRow) { const index = selectedRow._quoteIndex; showQuoteDialog(customQuotesArray[index], index); } }); removeButton.connect('clicked', () => { const selectedRow = quotesListBox.get_selected_row(); if (selectedRow) { const index = selectedRow._quoteIndex; customQuotesArray.splice(index, 1); settings.set_string('custom-quotes', JSON.stringify(customQuotesArray)); refreshQuotesList(); editButton.sensitive = false; removeButton.sensitive = false; } }); // Bind settings settings.bind( 'rotation-interval', rotationRow, 'value', Gio.SettingsBindFlags.DEFAULT ); settings.bind( 'sync-interval', syncRow, 'value', Gio.SettingsBindFlags.DEFAULT ); settings.bind( 'api-url', apiRow, 'text', Gio.SettingsBindFlags.DEFAULT ); settings.bind( 'font-size', fontSizeRow, 'value', Gio.SettingsBindFlags.DEFAULT ); settings.bind( 'text-color', colorRow, 'text', Gio.SettingsBindFlags.DEFAULT ); settings.bind( 'max-width', widthRow, 'value', Gio.SettingsBindFlags.DEFAULT ); settings.bind( 'quotes-file', quotesFileRow, 'text', Gio.SettingsBindFlags.DEFAULT ); settings.bind( 'use-custom-quotes', useCustomQuotesRow, 'active', Gio.SettingsBindFlags.DEFAULT ); } }