Fix custom quotes saving issue, fix viewport width
This commit is contained in:
221
extension.js
221
extension.js
@ -10,6 +10,18 @@ import GObject from 'gi://GObject';
|
|||||||
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
|
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||||
import * as Main from 'resource:///org/gnome/shell/ui/main.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 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 {
|
class QuotesExtension extends Extension {
|
||||||
constructor(metadata) {
|
constructor(metadata) {
|
||||||
@ -23,11 +35,13 @@ class QuotesExtension extends Extension {
|
|||||||
this._dataDir = null;
|
this._dataDir = null;
|
||||||
this._quotesFile = null;
|
this._quotesFile = null;
|
||||||
this._settings = null;
|
this._settings = null;
|
||||||
|
this._isPaused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
this._initializeSettings();
|
this._initializeSettings();
|
||||||
this._initializeDataDir();
|
this._initializeDataDir();
|
||||||
|
this._processCustomQuotes();
|
||||||
this._loadQuotes();
|
this._loadQuotes();
|
||||||
this._createIndicator();
|
this._createIndicator();
|
||||||
this._setupTimers();
|
this._setupTimers();
|
||||||
@ -70,6 +84,7 @@ class QuotesExtension extends Extension {
|
|||||||
this._quotesFileName = this._settings.get_string('quotes-file') || 'quotes.json';
|
this._quotesFileName = this._settings.get_string('quotes-file') || 'quotes.json';
|
||||||
this._useCustomQuotes = this._settings.get_boolean('use-custom-quotes');
|
this._useCustomQuotes = this._settings.get_boolean('use-custom-quotes');
|
||||||
this._customQuotes = this._settings.get_string('custom-quotes') || '[]';
|
this._customQuotes = this._settings.get_string('custom-quotes') || '[]';
|
||||||
|
this._quoteSources = { synced: [], custom: [] };
|
||||||
|
|
||||||
// Listen for settings changes
|
// Listen for settings changes
|
||||||
this._settings.connect('changed', (settings, key) => {
|
this._settings.connect('changed', (settings, key) => {
|
||||||
@ -77,6 +92,7 @@ class QuotesExtension extends Extension {
|
|||||||
this._updateLabelStyle();
|
this._updateLabelStyle();
|
||||||
} else if (key === 'max-width') {
|
} else if (key === 'max-width') {
|
||||||
this._maxWidth = this._settings.get_int('max-width') || 60;
|
this._maxWidth = this._settings.get_int('max-width') || 60;
|
||||||
|
this._label.set_width(this._maxWidth * 8);
|
||||||
this._label.set_text(this._getCurrentQuote());
|
this._label.set_text(this._getCurrentQuote());
|
||||||
} else if (key === 'quotes-file') {
|
} else if (key === 'quotes-file') {
|
||||||
this._quotesFileName = this._settings.get_string('quotes-file') || 'quotes.json';
|
this._quotesFileName = this._settings.get_string('quotes-file') || 'quotes.json';
|
||||||
@ -85,6 +101,8 @@ class QuotesExtension extends Extension {
|
|||||||
} else if (key === 'use-custom-quotes' || key === 'custom-quotes') {
|
} else if (key === 'use-custom-quotes' || key === 'custom-quotes') {
|
||||||
this._useCustomQuotes = this._settings.get_boolean('use-custom-quotes');
|
this._useCustomQuotes = this._settings.get_boolean('use-custom-quotes');
|
||||||
this._customQuotes = this._settings.get_string('custom-quotes') || '[]';
|
this._customQuotes = this._settings.get_string('custom-quotes') || '[]';
|
||||||
|
this._processCustomQuotes();
|
||||||
|
this._mergeAndSaveQuotes();
|
||||||
this._loadQuotes();
|
this._loadQuotes();
|
||||||
if (this._label) {
|
if (this._label) {
|
||||||
this._label.set_text(this._getCurrentQuote());
|
this._label.set_text(this._getCurrentQuote());
|
||||||
@ -96,34 +114,34 @@ class QuotesExtension extends Extension {
|
|||||||
_loadQuotes() {
|
_loadQuotes() {
|
||||||
this._quotes = [];
|
this._quotes = [];
|
||||||
|
|
||||||
// Use custom quotes if enabled
|
// Always load from file (contains both synced and custom quotes)
|
||||||
if (this._useCustomQuotes) {
|
try {
|
||||||
try {
|
if (GLib.file_test(this._quotesFile, GLib.FileTest.EXISTS)) {
|
||||||
this._quotes = JSON.parse(this._customQuotes);
|
const [success, contents] = GLib.file_get_contents(this._quotesFile);
|
||||||
if (!Array.isArray(this._quotes)) {
|
if (success) {
|
||||||
this._quotes = [];
|
const decoder = new TextDecoder();
|
||||||
}
|
const fileData = JSON.parse(decoder.decode(contents));
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse custom quotes:', e);
|
// Handle both old format (array) and new format (object with sources)
|
||||||
this._quotes = [];
|
if (Array.isArray(fileData)) {
|
||||||
}
|
this._quotes = fileData;
|
||||||
} else {
|
} else if (fileData.synced && fileData.custom) {
|
||||||
// Load from file
|
this._quoteSources = fileData;
|
||||||
try {
|
// Combine quotes based on use-custom-quotes setting
|
||||||
if (GLib.file_test(this._quotesFile, GLib.FileTest.EXISTS)) {
|
if (this._useCustomQuotes) {
|
||||||
const [success, contents] = GLib.file_get_contents(this._quotesFile);
|
this._quotes = [...this._quoteSources.custom, ...this._quoteSources.synced];
|
||||||
if (success) {
|
} else {
|
||||||
const decoder = new TextDecoder();
|
this._quotes = [...this._quoteSources.synced, ...this._quoteSources.custom];
|
||||||
this._quotes = JSON.parse(decoder.decode(contents));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load quotes:', e);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load quotes:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._quotes.length === 0) {
|
if (this._quotes.length === 0) {
|
||||||
this._quotes = [
|
const defaultQuotes = [
|
||||||
{ text: "The only way to do great work is to love what you do.", author: "Steve Jobs" },
|
{ 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: "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: "The future belongs to those who believe in the beauty of their dreams.", author: "Eleanor Roosevelt" },
|
||||||
@ -150,19 +168,65 @@ class QuotesExtension extends Extension {
|
|||||||
{ 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: "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" }
|
{ 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();
|
this._saveQuotes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_saveQuotes() {
|
_saveQuotes() {
|
||||||
try {
|
try {
|
||||||
const contents = JSON.stringify(this._quotes, null, 2);
|
const contents = JSON.stringify(this._quoteSources, null, 2);
|
||||||
GLib.file_set_contents(this._quotesFile, contents);
|
GLib.file_set_contents(this._quotesFile, contents);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to save quotes:', 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() {
|
_createIndicator() {
|
||||||
this._indicator = new PanelMenu.Button(0.0, 'Quotes', false);
|
this._indicator = new PanelMenu.Button(0.0, 'Quotes', false);
|
||||||
|
|
||||||
@ -173,12 +237,92 @@ class QuotesExtension extends Extension {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._updateLabelStyle();
|
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);
|
this._indicator.add_child(this._label);
|
||||||
Main.panel.addToStatusArea('quotes', this._indicator, 1, 'left');
|
Main.panel.addToStatusArea('quotes', this._indicator, 1, 'left');
|
||||||
|
|
||||||
this._indicator.connect('button-press-event', () => {
|
// 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._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() {
|
_getCurrentQuote() {
|
||||||
@ -187,10 +331,6 @@ class QuotesExtension extends Extension {
|
|||||||
const quote = this._quotes[this._currentQuoteIndex];
|
const quote = this._quotes[this._currentQuoteIndex];
|
||||||
let text = `"${quote.text}" - ${quote.author}`;
|
let text = `"${quote.text}" - ${quote.author}`;
|
||||||
|
|
||||||
if (text.length > this._maxWidth) {
|
|
||||||
text = text.substring(0, this._maxWidth - 3) + '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,12 +352,23 @@ class QuotesExtension extends Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_setupTimers() {
|
_setupTimers() {
|
||||||
|
this._setupRotationTimer();
|
||||||
|
this._setupSyncTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupRotationTimer() {
|
||||||
|
if (this._isPaused) return;
|
||||||
|
|
||||||
this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
|
this._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
|
||||||
this._rotationInterval, () => {
|
this._rotationInterval, () => {
|
||||||
this._nextQuote();
|
if (!this._isPaused) {
|
||||||
|
this._nextQuote();
|
||||||
|
}
|
||||||
return GLib.SOURCE_CONTINUE;
|
return GLib.SOURCE_CONTINUE;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setupSyncTimer() {
|
||||||
this._syncTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
|
this._syncTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
|
||||||
this._syncInterval, () => {
|
this._syncInterval, () => {
|
||||||
this._syncQuotes();
|
this._syncQuotes();
|
||||||
@ -240,15 +391,17 @@ class QuotesExtension extends Extension {
|
|||||||
if (data.content && data.author) {
|
if (data.content && data.author) {
|
||||||
const newQuote = {
|
const newQuote = {
|
||||||
text: data.content,
|
text: data.content,
|
||||||
author: data.author
|
author: data.author,
|
||||||
|
hash: simpleHash(data.content + data.author),
|
||||||
|
source: 'api'
|
||||||
};
|
};
|
||||||
|
|
||||||
const exists = this._quotes.some(q =>
|
const exists = this._quoteSources.synced.some(q => q.hash === newQuote.hash);
|
||||||
q.text === newQuote.text && q.author === newQuote.author);
|
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
this._quotes.push(newQuote);
|
this._quoteSources.synced.push(newQuote);
|
||||||
this._saveQuotes();
|
this._mergeAndSaveQuotes();
|
||||||
|
this._loadQuotes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
250
prefs.js
250
prefs.js
@ -61,7 +61,6 @@ export default class QuotesPreferences extends ExtensionPreferences {
|
|||||||
// API URL
|
// API URL
|
||||||
const apiRow = new Adw.EntryRow({
|
const apiRow = new Adw.EntryRow({
|
||||||
title: 'API URL',
|
title: 'API URL',
|
||||||
subtitle: 'URL for fetching quotes from remote server',
|
|
||||||
text: settings.get_string('api-url'),
|
text: settings.get_string('api-url'),
|
||||||
});
|
});
|
||||||
apiGroup.add(apiRow);
|
apiGroup.add(apiRow);
|
||||||
@ -96,7 +95,6 @@ export default class QuotesPreferences extends ExtensionPreferences {
|
|||||||
// Text color
|
// Text color
|
||||||
const colorRow = new Adw.EntryRow({
|
const colorRow = new Adw.EntryRow({
|
||||||
title: 'Text Color',
|
title: 'Text Color',
|
||||||
subtitle: 'Color for quote text in hex format',
|
|
||||||
text: settings.get_string('text-color'),
|
text: settings.get_string('text-color'),
|
||||||
});
|
});
|
||||||
displayGroup.add(colorRow);
|
displayGroup.add(colorRow);
|
||||||
@ -131,7 +129,6 @@ export default class QuotesPreferences extends ExtensionPreferences {
|
|||||||
// Quotes file
|
// Quotes file
|
||||||
const quotesFileRow = new Adw.EntryRow({
|
const quotesFileRow = new Adw.EntryRow({
|
||||||
title: 'Quotes File Name',
|
title: 'Quotes File Name',
|
||||||
subtitle: 'JSON file name for storing quotes',
|
|
||||||
text: settings.get_string('quotes-file'),
|
text: settings.get_string('quotes-file'),
|
||||||
});
|
});
|
||||||
storageGroup.add(quotesFileRow);
|
storageGroup.add(quotesFileRow);
|
||||||
@ -144,61 +141,250 @@ export default class QuotesPreferences extends ExtensionPreferences {
|
|||||||
window.add(quotesPage);
|
window.add(quotesPage);
|
||||||
|
|
||||||
const quotesSourceGroup = new Adw.PreferencesGroup({
|
const quotesSourceGroup = new Adw.PreferencesGroup({
|
||||||
title: 'Quote Source',
|
title: 'Quote Priority',
|
||||||
description: 'Choose between built-in/synced quotes or custom quotes',
|
description: 'Custom quotes and synced quotes are both saved to the JSON file',
|
||||||
});
|
});
|
||||||
quotesPage.add(quotesSourceGroup);
|
quotesPage.add(quotesSourceGroup);
|
||||||
|
|
||||||
// Use custom quotes switch
|
// Use custom quotes switch
|
||||||
const useCustomQuotesRow = new Adw.SwitchRow({
|
const useCustomQuotesRow = new Adw.SwitchRow({
|
||||||
title: 'Use Custom Quotes',
|
title: 'Prioritize Custom Quotes',
|
||||||
subtitle: 'Use custom quotes instead of built-in/synced quotes',
|
|
||||||
active: settings.get_boolean('use-custom-quotes'),
|
active: settings.get_boolean('use-custom-quotes'),
|
||||||
});
|
});
|
||||||
quotesSourceGroup.add(useCustomQuotesRow);
|
quotesSourceGroup.add(useCustomQuotesRow);
|
||||||
|
|
||||||
const customQuotesGroup = new Adw.PreferencesGroup({
|
const customQuotesGroup = new Adw.PreferencesGroup({
|
||||||
title: 'Custom Quotes',
|
title: 'Custom Quotes',
|
||||||
description: 'Edit your custom quotes in JSON format',
|
description: 'Manage your custom quotes with an easy-to-use interface.',
|
||||||
});
|
});
|
||||||
quotesPage.add(customQuotesGroup);
|
quotesPage.add(customQuotesGroup);
|
||||||
|
|
||||||
// Custom quotes text area
|
// Parse existing custom quotes
|
||||||
const quotesTextView = new Gtk.TextView({
|
let customQuotesArray = [];
|
||||||
editable: true,
|
|
||||||
wrap_mode: Gtk.WrapMode.WORD,
|
|
||||||
hexpand: true,
|
|
||||||
vexpand: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const quotesBuffer = quotesTextView.get_buffer();
|
|
||||||
const customQuotesText = settings.get_string('custom-quotes');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const quotesArray = JSON.parse(customQuotesText);
|
customQuotesArray = JSON.parse(settings.get_string('custom-quotes'));
|
||||||
quotesBuffer.set_text(JSON.stringify(quotesArray, null, 2), -1);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
quotesBuffer.set_text('[\n {\n "text": "Your custom quote here",\n "author": "Author Name"\n }\n]', -1);
|
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({
|
const scrolledWindow = new Gtk.ScrolledWindow({
|
||||||
child: quotesTextView,
|
child: quotesListBox,
|
||||||
hexpand: true,
|
hexpand: true,
|
||||||
vexpand: true,
|
vexpand: true,
|
||||||
min_content_height: 200,
|
min_content_height: 200,
|
||||||
|
max_content_height: 300,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
quotesContainer.append(scrolledWindow);
|
||||||
|
quotesContainer.append(buttonsBox);
|
||||||
|
|
||||||
const quotesExpander = new Adw.ExpanderRow({
|
const quotesExpander = new Adw.ExpanderRow({
|
||||||
title: 'Custom Quotes Editor',
|
title: 'Custom Quotes Manager',
|
||||||
subtitle: 'JSON format: [{"text": "quote", "author": "author"}]',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
quotesExpander.add_row(new Adw.PreferencesRow({
|
quotesExpander.add_row(new Adw.PreferencesRow({
|
||||||
child: scrolledWindow,
|
child: quotesContainer,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
customQuotesGroup.add(quotesExpander);
|
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
|
// Bind settings
|
||||||
settings.bind(
|
settings.bind(
|
||||||
'rotation-interval',
|
'rotation-interval',
|
||||||
@ -256,17 +442,5 @@ export default class QuotesPreferences extends ExtensionPreferences {
|
|||||||
Gio.SettingsBindFlags.DEFAULT
|
Gio.SettingsBindFlags.DEFAULT
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save custom quotes when text changes
|
|
||||||
quotesBuffer.connect('changed', () => {
|
|
||||||
const [start, end] = quotesBuffer.get_bounds();
|
|
||||||
const text = quotesBuffer.get_text(start, end, false);
|
|
||||||
try {
|
|
||||||
// Validate JSON
|
|
||||||
JSON.parse(text);
|
|
||||||
settings.set_string('custom-quotes', text);
|
|
||||||
} catch (e) {
|
|
||||||
// Invalid JSON, don't save
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user