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 }