doomsday.git

doomsday

~
git clone https://github.com/debuman/doomsday.git

prefs.js (15952B)

      1 import Gtk from 'gi://Gtk';
      2 import Adw from 'gi://Adw';
      3 import Gio from 'gi://Gio';
      4 import GLib from 'gi://GLib';
      5 import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
      6 
      7 export default class DoomsdayPreferences extends ExtensionPreferences {
      8     fillPreferencesWindow(window) {
      9         const settings = this.getSettings();
     10 
     11         // Create a preferences page
     12         const page = new Adw.PreferencesPage({
     13             title: 'General',
     14             icon_name: 'dialog-information-symbolic',
     15         });
     16         window.add(page);
     17 
     18         // Create a preferences group for panel settings
     19         const panelGroup = new Adw.PreferencesGroup({
     20             title: 'Panel Settings',
     21             description: 'Customize the extension appearance',
     22         });
     23         page.add(panelGroup);
     24 
     25         // Panel position selector
     26         const positionRow = new Adw.ComboRow({
     27             title: 'Panel Position',
     28             subtitle: 'Choose where the extension appears in the top panel',
     29         });
     30 
     31         const positionModel = new Gtk.StringList();
     32         positionModel.append('Left');
     33         positionModel.append('Center');
     34         positionModel.append('Right');
     35         positionRow.set_model(positionModel);
     36 
     37         // Set initial value
     38         const currentPosition = settings.get_string('panel-position');
     39         const positions = ['left', 'center', 'right'];
     40         positionRow.set_selected(positions.indexOf(currentPosition));
     41 
     42         // Connect to changes
     43         positionRow.connect('notify::selected', () => {
     44             const newPosition = positions[positionRow.get_selected()];
     45             settings.set_string('panel-position', newPosition);
     46         });
     47 
     48         panelGroup.add(positionRow);
     49 
     50         // Panel index selector
     51         const indexRow = new Adw.SpinRow({
     52             title: 'Panel Index',
     53             subtitle: 'Fine-tune position within the panel (0=leftmost, -1=rightmost)',
     54         });
     55 
     56         indexRow.set_adjustment(new Gtk.Adjustment({
     57             lower: -1,
     58             upper: 99,
     59             step_increment: 1,
     60             page_increment: 10,
     61         }));
     62 
     63         // Set initial value
     64         const currentIndex = settings.get_int('panel-index');
     65         indexRow.set_value(currentIndex);
     66 
     67         // Connect to changes
     68         indexRow.connect('changed', () => {
     69             settings.set_int('panel-index', indexRow.get_value());
     70         });
     71 
     72         panelGroup.add(indexRow);
     73 
     74         // Create a preferences group for D-Day list
     75         const group = new Adw.PreferencesGroup({
     76             title: 'D-Day Events',
     77             description: 'Manage your countdown events',
     78         });
     79         page.add(group);
     80 
     81         // Create ListBox for events
     82         const listBox = new Gtk.ListBox({
     83             selection_mode: Gtk.SelectionMode.NONE,
     84             css_classes: ['boxed-list'],
     85         });
     86         group.add(listBox);
     87 
     88         let radioGroup = null;
     89 
     90         // Local function: create event row
     91         const createEventRow = (event, isSelected) => {
     92             const row = new Adw.ActionRow({
     93                 title: event.name,
     94                 subtitle: event.date,
     95             });
     96 
     97             // Add radio button for selection
     98             const radioButton = new Gtk.CheckButton({
     99                 active: isSelected,
    100                 valign: Gtk.Align.CENTER,
    101             });
    102             radioButton.connect('toggled', () => {
    103                 if (radioButton.get_active()) {
    104                     settings.set_string('selected-event-id', event.id);
    105                 }
    106             });
    107             row.add_prefix(radioButton);
    108 
    109             // Make all radio buttons part of same group
    110             if (!radioGroup) {
    111                 radioGroup = radioButton;
    112             } else {
    113                 radioButton.set_group(radioGroup);
    114             }
    115 
    116             // Add edit button
    117             const editButton = new Gtk.Button({
    118                 icon_name: 'document-edit-symbolic',
    119                 label: 'Edit',
    120                 valign: Gtk.Align.CENTER,
    121                 css_classes: ['flat'],
    122                 tooltip_text: 'Edit this event',
    123             });
    124             editButton.connect('clicked', () => showEventDialog(window, event));
    125             row.add_suffix(editButton);
    126 
    127             // Add delete button
    128             const deleteButton = new Gtk.Button({
    129                 icon_name: 'user-trash-symbolic',
    130                 label: 'Delete',
    131                 valign: Gtk.Align.CENTER,
    132                 css_classes: ['flat', 'destructive-action'],
    133                 tooltip_text: 'Delete this event',
    134             });
    135             deleteButton.connect('clicked', () => deleteEvent(event.id));
    136             row.add_suffix(deleteButton);
    137 
    138             return row;
    139         };
    140 
    141         // Local function: load events
    142         const loadEvents = () => {
    143             // Clear existing rows
    144             let child = listBox.get_first_child();
    145             while (child) {
    146                 const next = child.get_next_sibling();
    147                 listBox.remove(child);
    148                 child = next;
    149             }
    150 
    151             // Reset radio group
    152             radioGroup = null;
    153 
    154             // Load events from settings
    155             const eventsJson = settings.get_string('dday-events');
    156             const selectedId = settings.get_string('selected-event-id');
    157 
    158             try {
    159                 const events = JSON.parse(eventsJson);
    160 
    161                 events.forEach(event => {
    162                     const row = createEventRow(event, event.id === selectedId);
    163                     listBox.append(row);
    164                 });
    165 
    166                 // Show placeholder if no events
    167                 if (events.length === 0) {
    168                     const row = new Adw.ActionRow({
    169                         title: 'No D-Day events yet',
    170                         subtitle: 'Click "Add D-Day Event" to create your first countdown',
    171                     });
    172                     listBox.append(row);
    173                 }
    174 
    175             } catch (e) {
    176                 console.error('Doomsday: Failed to load events:', e);
    177             }
    178         };
    179 
    180         // Local function: save event
    181         const saveEvent = (eventId, name, date) => {
    182             const eventsJson = settings.get_string('dday-events');
    183             let events = [];
    184 
    185             try {
    186                 events = JSON.parse(eventsJson);
    187             } catch (e) {
    188                 console.error('Doomsday: Failed to parse events:', e);
    189             }
    190 
    191             if (eventId) {
    192                 // Update existing event
    193                 const index = events.findIndex(e => e.id === eventId);
    194                 if (index !== -1) {
    195                     events[index] = { id: eventId, name, date };
    196                 }
    197             } else {
    198                 // Create new event with UUID
    199                 const newId = GLib.uuid_string_random();
    200                 events.push({ id: newId, name, date });
    201 
    202                 // Auto-select if it's the first event
    203                 if (events.length === 1) {
    204                     settings.set_string('selected-event-id', newId);
    205                 }
    206             }
    207 
    208             // Save back to settings
    209             settings.set_string('dday-events', JSON.stringify(events));
    210         };
    211 
    212         // Local function: delete event
    213         const deleteEvent = (eventId) => {
    214             const eventsJson = settings.get_string('dday-events');
    215             let events = [];
    216 
    217             try {
    218                 events = JSON.parse(eventsJson);
    219             } catch (e) {
    220                 console.error('Doomsday: Failed to parse events:', e);
    221                 return;
    222             }
    223 
    224             // Remove event
    225             events = events.filter(e => e.id !== eventId);
    226 
    227             // If deleted event was selected, clear selection or select first
    228             const selectedId = settings.get_string('selected-event-id');
    229             if (selectedId === eventId) {
    230                 const newSelectedId = events.length > 0 ? events[0].id : '';
    231                 settings.set_string('selected-event-id', newSelectedId);
    232             }
    233 
    234             // Save back to settings
    235             settings.set_string('dday-events', JSON.stringify(events));
    236         };
    237 
    238         // Local function: show event dialog
    239         const showEventDialog = (parentWindow, existingEvent) => {
    240             const dialog = new Adw.MessageDialog({
    241                 transient_for: parentWindow,
    242                 modal: true,
    243                 heading: existingEvent ? 'Edit D-Day Event' : 'New D-Day Event',
    244             });
    245 
    246             // Create form content
    247             const box = new Gtk.Box({
    248                 orientation: Gtk.Orientation.VERTICAL,
    249                 spacing: 12,
    250                 margin_top: 12,
    251                 margin_bottom: 12,
    252                 margin_start: 12,
    253                 margin_end: 12,
    254             });
    255 
    256             // Event name entry
    257             const nameEntry = new Adw.EntryRow({
    258                 title: 'Event Name',
    259             });
    260             if (existingEvent) {
    261                 nameEntry.set_text(existingEvent.name);
    262             }
    263             box.append(nameEntry);
    264 
    265             // Date picker (using entry for simplicity)
    266             const dateEntry = new Adw.EntryRow({
    267                 title: 'Date (YYYY-MM-DD)',
    268             });
    269             if (existingEvent) {
    270                 dateEntry.set_text(existingEvent.date);
    271             } else {
    272                 // Default to today
    273                 const today = new Date();
    274                 const dateStr = today.toISOString().split('T')[0];
    275                 dateEntry.set_text(dateStr);
    276             }
    277             box.append(dateEntry);
    278 
    279             dialog.set_extra_child(box);
    280 
    281             // Add buttons
    282             dialog.add_response('cancel', 'Cancel');
    283             dialog.add_response('save', 'Save');
    284             dialog.set_response_appearance('save', Adw.ResponseAppearance.SUGGESTED);
    285 
    286             dialog.connect('response', (dlg, response) => {
    287                 if (response === 'save') {
    288                     const name = nameEntry.get_text().trim();
    289                     const date = dateEntry.get_text().trim();
    290 
    291                     // Validate inputs
    292                     if (!name || !date) {
    293                         console.error('Doomsday: Name and date are required');
    294                         return;
    295                     }
    296 
    297                     // Validate date format
    298                     if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
    299                         console.error('Doomsday: Invalid date format');
    300                         return;
    301                     }
    302 
    303                     // Save event
    304                     saveEvent(existingEvent?.id, name, date);
    305                 }
    306                 dlg.close();
    307             });
    308 
    309             dialog.present();
    310         };
    311 
    312         // Load and display events
    313         loadEvents();
    314 
    315         // Add button to create new event
    316         const addButton = new Gtk.Button({
    317             label: 'Add D-Day Event',
    318             css_classes: ['suggested-action'],
    319             margin_top: 12,
    320         });
    321         addButton.connect('clicked', () => showEventDialog(window, null));
    322         group.add(addButton);
    323 
    324         // Listen for settings changes
    325         const settingsChangedId = settings.connect('changed::dday-events',
    326             () => loadEvents());
    327 
    328         // Clean up settings signal on window close
    329         window.connect('close-request', () => {
    330             settings.disconnect(settingsChangedId);
    331         });
    332 
    333         // Create About page
    334         this._addAboutPage(window);
    335     }
    336 
    337     _addAboutPage(window) {
    338         // Create About page
    339         const aboutPage = new Adw.PreferencesPage({
    340             title: 'About',
    341             icon_name: 'help-about-symbolic',
    342         });
    343         window.add(aboutPage);
    344 
    345         // Extension info group
    346         const infoGroup = new Adw.PreferencesGroup({
    347             title: 'Doomsday Extension',
    348             description: 'D-Day countdown for GNOME Shell',
    349         });
    350         aboutPage.add(infoGroup);
    351 
    352         // Version row
    353         const versionRow = new Adw.ActionRow({
    354             title: 'Version',
    355             subtitle: '1.0',
    356         });
    357         infoGroup.add(versionRow);
    358 
    359         // Links group
    360         const linksGroup = new Adw.PreferencesGroup({
    361             title: 'Links',
    362         });
    363         aboutPage.add(linksGroup);
    364 
    365         // GitHub button
    366         const githubRow = new Adw.ActionRow({
    367             title: 'Doomsday on GitHub',
    368             subtitle: 'Visit the project repository',
    369             activatable: true,
    370         });
    371         const githubIcon = new Gtk.Image({
    372             icon_name: 'web-browser-symbolic',
    373         });
    374         githubRow.add_prefix(githubIcon);
    375         githubRow.connect('activated', () => {
    376             Gtk.show_uri(window, 'https://github.com/sqzer-x/doomsday', 0);
    377         });
    378         linksGroup.add(githubRow);
    379 
    380         // Report Issue button
    381         const issueRow = new Adw.ActionRow({
    382             title: 'Report an Issue',
    383             subtitle: 'Help improve this extension',
    384             activatable: true,
    385         });
    386         const issueIcon = new Gtk.Image({
    387             icon_name: 'dialog-warning-symbolic',
    388         });
    389         issueRow.add_prefix(issueIcon);
    390         issueRow.connect('activated', () => {
    391             Gtk.show_uri(window, 'https://github.com/sqzer-x/doomsday/issues', 0);
    392         });
    393         linksGroup.add(issueRow);
    394 
    395         // Donate button
    396         const donateRow = new Adw.ActionRow({
    397             title: 'Donate',
    398             subtitle: 'Support this project via GitHub Sponsors',
    399             activatable: true,
    400         });
    401         const donateIcon = new Gtk.Image({
    402             icon_name: 'emblem-favorite-symbolic',
    403         });
    404         donateRow.add_prefix(donateIcon);
    405         donateRow.connect('activated', () => {
    406             Gtk.show_uri(window, 'https://github.com/sponsors/sqzer-x', 0);
    407         });
    408         linksGroup.add(donateRow);
    409 
    410         // Legal group
    411         const legalGroup = new Adw.PreferencesGroup({
    412             title: 'Legal',
    413         });
    414         aboutPage.add(legalGroup);
    415 
    416         // License button
    417         const licenseRow = new Adw.ActionRow({
    418             title: 'License',
    419             subtitle: 'GPL-3.0',
    420             activatable: true,
    421         });
    422         const licenseIcon = new Gtk.Image({
    423             icon_name: 'text-x-generic-symbolic',
    424         });
    425         licenseRow.add_prefix(licenseIcon);
    426         licenseRow.connect('activated', () => {
    427             this._showLicenseDialog(window);
    428         });
    429         legalGroup.add(licenseRow);
    430 
    431         // Author row
    432         const authorRow = new Adw.ActionRow({
    433             title: 'Author',
    434             subtitle: 'sqzer-x',
    435         });
    436         const authorIcon = new Gtk.Image({
    437             icon_name: 'avatar-default-symbolic',
    438         });
    439         authorRow.add_prefix(authorIcon);
    440         legalGroup.add(authorRow);
    441     }
    442 
    443     _showLicenseDialog(window) {
    444         const dialog = new Adw.MessageDialog({
    445             transient_for: window,
    446             modal: true,
    447             heading: 'License Information',
    448             body: 'This extension is licensed under GPL-3.0.\n\n' +
    449                   'This program is free software: you can redistribute it and/or modify ' +
    450                   'it under the terms of the GNU General Public License as published by ' +
    451                   'the Free Software Foundation, either version 3 of the License, or ' +
    452                   '(at your option) any later version.\n\n' +
    453                   'This program is distributed in the hope that it will be useful, ' +
    454                   'but WITHOUT ANY WARRANTY; without even the implied warranty of ' +
    455                   'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ' +
    456                   'GNU General Public License for more details.',
    457         });
    458 
    459         dialog.add_response('close', 'Close');
    460         dialog.set_response_appearance('close', Adw.ResponseAppearance.SUGGESTED);
    461         dialog.connect('response', () => dialog.close());
    462         dialog.present();
    463     }
    464 }