Initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.idea
|
276
extension.js
Normal file
276
extension.js
Normal file
@ -0,0 +1,276 @@
|
||||
'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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
enable() {
|
||||
this._initializeSettings();
|
||||
this._initializeDataDir();
|
||||
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') || '[]';
|
||||
|
||||
// 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_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._loadQuotes();
|
||||
if (this._label) {
|
||||
this._label.set_text(this._getCurrentQuote());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_loadQuotes() {
|
||||
this._quotes = [];
|
||||
|
||||
// Use custom quotes if enabled
|
||||
if (this._useCustomQuotes) {
|
||||
try {
|
||||
this._quotes = JSON.parse(this._customQuotes);
|
||||
if (!Array.isArray(this._quotes)) {
|
||||
this._quotes = [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse custom quotes:', e);
|
||||
this._quotes = [];
|
||||
}
|
||||
} else {
|
||||
// Load from file
|
||||
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();
|
||||
this._quotes = JSON.parse(decoder.decode(contents));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load quotes:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._quotes.length === 0) {
|
||||
this._quotes = [
|
||||
{ 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" }
|
||||
];
|
||||
this._saveQuotes();
|
||||
}
|
||||
}
|
||||
|
||||
_saveQuotes() {
|
||||
try {
|
||||
const contents = JSON.stringify(this._quotes, null, 2);
|
||||
GLib.file_set_contents(this._quotesFile, contents);
|
||||
} catch (e) {
|
||||
console.error('Failed to save quotes:', e);
|
||||
}
|
||||
}
|
||||
|
||||
_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._indicator.add_child(this._label);
|
||||
Main.panel.addToStatusArea('quotes', this._indicator, 1, 'left');
|
||||
|
||||
this._indicator.connect('button-press-event', () => {
|
||||
this._nextQuote();
|
||||
});
|
||||
}
|
||||
|
||||
_getCurrentQuote() {
|
||||
if (this._quotes.length === 0) return 'No quotes available';
|
||||
|
||||
const quote = this._quotes[this._currentQuoteIndex];
|
||||
let text = `"${quote.text}" - ${quote.author}`;
|
||||
|
||||
if (text.length > this._maxWidth) {
|
||||
text = text.substring(0, this._maxWidth - 3) + '...';
|
||||
}
|
||||
|
||||
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._rotationTimer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT,
|
||||
this._rotationInterval, () => {
|
||||
this._nextQuote();
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
});
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
const exists = this._quotes.some(q =>
|
||||
q.text === newQuote.text && q.author === newQuote.author);
|
||||
|
||||
if (!exists) {
|
||||
this._quotes.push(newQuote);
|
||||
this._saveQuotes();
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
14
metadata.json
Normal file
14
metadata.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Quotes",
|
||||
"description": "Display quotes in the top bar with periodic rotation and remote synchronization",
|
||||
"uuid": "quotes@thinh.dev",
|
||||
"shell-version": [
|
||||
"45",
|
||||
"46",
|
||||
"47",
|
||||
"48"
|
||||
],
|
||||
"version": 1,
|
||||
"url": "https://git.thinhha.pro/public/quotes-extension",
|
||||
"settings-schema": "org.gnome.shell.extensions.quotes"
|
||||
}
|
272
prefs.js
Normal file
272
prefs.js
Normal file
@ -0,0 +1,272 @@
|
||||
'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',
|
||||
subtitle: 'URL for fetching quotes from remote server',
|
||||
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',
|
||||
subtitle: 'Color for quote text in hex format',
|
||||
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',
|
||||
subtitle: 'JSON file name for storing quotes',
|
||||
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 Source',
|
||||
description: 'Choose between built-in/synced quotes or custom quotes',
|
||||
});
|
||||
quotesPage.add(quotesSourceGroup);
|
||||
|
||||
// Use custom quotes switch
|
||||
const useCustomQuotesRow = new Adw.SwitchRow({
|
||||
title: 'Use Custom Quotes',
|
||||
subtitle: 'Use custom quotes instead of built-in/synced quotes',
|
||||
active: settings.get_boolean('use-custom-quotes'),
|
||||
});
|
||||
quotesSourceGroup.add(useCustomQuotesRow);
|
||||
|
||||
const customQuotesGroup = new Adw.PreferencesGroup({
|
||||
title: 'Custom Quotes',
|
||||
description: 'Edit your custom quotes in JSON format',
|
||||
});
|
||||
quotesPage.add(customQuotesGroup);
|
||||
|
||||
// Custom quotes text area
|
||||
const quotesTextView = new Gtk.TextView({
|
||||
editable: true,
|
||||
wrap_mode: Gtk.WrapMode.WORD,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
});
|
||||
|
||||
const quotesBuffer = quotesTextView.get_buffer();
|
||||
const customQuotesText = settings.get_string('custom-quotes');
|
||||
|
||||
try {
|
||||
const quotesArray = JSON.parse(customQuotesText);
|
||||
quotesBuffer.set_text(JSON.stringify(quotesArray, null, 2), -1);
|
||||
} catch (e) {
|
||||
quotesBuffer.set_text('[\n {\n "text": "Your custom quote here",\n "author": "Author Name"\n }\n]', -1);
|
||||
}
|
||||
|
||||
const scrolledWindow = new Gtk.ScrolledWindow({
|
||||
child: quotesTextView,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
min_content_height: 200,
|
||||
});
|
||||
|
||||
const quotesExpander = new Adw.ExpanderRow({
|
||||
title: 'Custom Quotes Editor',
|
||||
subtitle: 'JSON format: [{"text": "quote", "author": "author"}]',
|
||||
});
|
||||
|
||||
quotesExpander.add_row(new Adw.PreferencesRow({
|
||||
child: scrolledWindow,
|
||||
}));
|
||||
|
||||
customQuotesGroup.add(quotesExpander);
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// 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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
BIN
schemas/gschemas.compiled
Normal file
BIN
schemas/gschemas.compiled
Normal file
Binary file not shown.
50
schemas/org.gnome.shell.extensions.quotes.gschema.xml
Normal file
50
schemas/org.gnome.shell.extensions.quotes.gschema.xml
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<schemalist>
|
||||
<schema id="org.gnome.shell.extensions.quotes" path="/org/gnome/shell/extensions/quotes/">
|
||||
<key name="rotation-interval" type="i">
|
||||
<default>30</default>
|
||||
<summary>Quote rotation interval</summary>
|
||||
<description>Time in seconds between quote rotations</description>
|
||||
</key>
|
||||
<key name="sync-interval" type="i">
|
||||
<default>3600</default>
|
||||
<summary>Sync interval</summary>
|
||||
<description>Time in seconds between API synchronizations</description>
|
||||
</key>
|
||||
<key name="api-url" type="s">
|
||||
<default>'https://api.quotable.io/random'</default>
|
||||
<summary>API URL</summary>
|
||||
<description>URL for fetching quotes from remote server</description>
|
||||
</key>
|
||||
<key name="font-size" type="i">
|
||||
<default>14</default>
|
||||
<summary>Font size</summary>
|
||||
<description>Font size for quote text in pixels</description>
|
||||
</key>
|
||||
<key name="text-color" type="s">
|
||||
<default>'#ffffff'</default>
|
||||
<summary>Text color</summary>
|
||||
<description>Color for quote text in hex format</description>
|
||||
</key>
|
||||
<key name="max-width" type="i">
|
||||
<default>60</default>
|
||||
<summary>Maximum character width</summary>
|
||||
<description>Maximum number of characters to display before truncating</description>
|
||||
</key>
|
||||
<key name="quotes-file" type="s">
|
||||
<default>'quotes.json'</default>
|
||||
<summary>Quotes file name</summary>
|
||||
<description>Name of the JSON file to store quotes</description>
|
||||
</key>
|
||||
<key name="custom-quotes" type="s">
|
||||
<default>'[]'</default>
|
||||
<summary>Custom quotes</summary>
|
||||
<description>JSON array of custom quotes objects with text and author properties</description>
|
||||
</key>
|
||||
<key name="use-custom-quotes" type="b">
|
||||
<default>false</default>
|
||||
<summary>Use custom quotes</summary>
|
||||
<description>Whether to use custom quotes instead of built-in/synced quotes</description>
|
||||
</key>
|
||||
</schema>
|
||||
</schemalist>
|
13
stylesheet.css
Normal file
13
stylesheet.css
Normal file
@ -0,0 +1,13 @@
|
||||
.quote-label {
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
padding: 0 8px;
|
||||
max-width: 400px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.quote-label:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
Reference in New Issue
Block a user