diff --git a/daily-quote/DesktopWidget.qml b/daily-quote/DesktopWidget.qml
new file mode 100644
index 000000000..182cd67f2
--- /dev/null
+++ b/daily-quote/DesktopWidget.qml
@@ -0,0 +1,405 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Io
+import qs.Commons
+import qs.Modules.DesktopWidgets
+import qs.Services.System
+import qs.Services.UI
+import qs.Widgets
+
+DraggableDesktopWidget {
+ id: root
+ property var pluginApi: null
+
+ // --- Read from widgetData (injected by DesktopWidgets.qml) ---
+ readonly property string storedQuote: widgetData?.currentQuote ?? ""
+ readonly property string storedAuthor: widgetData?.currentAuthor ?? ""
+ readonly property string storedDate: widgetData?.lastDate ?? ""
+ readonly property string scrambleSpeed: widgetData?.scrambleSpeed ?? "medium"
+ readonly property string scrambleChars: widgetData?.scrambleChars ?? "mix"
+ readonly property bool showAuthor: widgetData?.showAuthor ?? true
+ readonly property bool autoChangeDaily: widgetData?.autoChangeDaily ?? true
+ readonly property string quoteFont: widgetData?.quoteFont ?? ""
+ readonly property string authorFont: widgetData?.authorFont ?? ""
+ readonly property string quoteColor: widgetData?.quoteColor ?? "primary"
+ readonly property string textAlign: widgetData?.textAlign ?? "left"
+ readonly property bool showGradientOverlay: widgetData?.showGradientOverlay ?? true
+ readonly property string gradientDirection: widgetData?.gradientDirection ?? "vertical"
+
+ // --- Adaptive gradient color: dark mode = black, light mode = white ---
+ readonly property real _gradR: Settings.data.colorSchemes.darkMode ? 0 : 1
+ readonly property real _gradG: Settings.data.colorSchemes.darkMode ? 0 : 1
+ readonly property real _gradB: Settings.data.colorSchemes.darkMode ? 0 : 1
+
+ // --- Resolved alignment ---
+ readonly property int _effectiveAlign: textAlign === "center" ? Qt.AlignHCenter : textAlign === "right" ? Qt.AlignRight : Qt.AlignLeft
+
+ // --- Resolved colors from color key ---
+ readonly property color resolvedTextColor: Color.resolveColorKey(quoteColor)
+ readonly property color resolvedOnTextColor: Color.resolveOnColorKey(quoteColor)
+
+ // --- Internal display state ---
+ property string displayedQuote: ""
+ property string displayedAuthor: ""
+
+ // --- Scramble engine ---
+ property var _charStates: []
+ property bool _isAnimating: false
+ property string _targetText: ""
+ property int _revealIndex: 0 // next char to start scrambling
+ property int _activeCount: 0 // chars with iteration >= 1 (being scrambled)
+ property bool _pendingPersist: false
+ property bool _selfUpdate: false
+
+ readonly property var _charSets: ({
+ "ascii": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
+ "symbols": "!@#$%^&*()_+-=[]{}|;:<>?/~\u2591\u2592\u2593\u2588\u2584\u2580\u25A0\u25A1\u2557\u2551\u255A\u255D\u2310\u00AC\u2569\u2566\u2560\u2550\u256C",
+ "mix": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*\u2591\u2592\u2593\u2588\u2584\u2580\u25A0\u25A1\u2557\u255A\u255D\u2551\u2569\u2566\u2560\u2550\u256C"
+ })
+ readonly property string _charSet: _charSets[scrambleChars] || _charSets["mix"]
+
+ readonly property var speedPresets: ({
+ "fast": { "tick": 30, "iterations": 3, "revealPerTick": 3 },
+ "medium": { "tick": 35, "iterations": 5, "revealPerTick": 2 },
+ "slow": { "tick": 50, "iterations": 7, "revealPerTick": 1 }
+ })
+ readonly property var speedConfig: speedPresets[scrambleSpeed] || speedPresets["medium"]
+
+ // --- Sizing ---
+ implicitWidth: Math.round(320 * widgetScale)
+ implicitHeight: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * widgetScale)
+ width: implicitWidth
+ height: implicitHeight
+
+ // --- Built-in quote pool (loaded from quotes.json) ---
+ property var builtinQuotes: []
+ readonly property string _quotesPath: pluginApi ? pluginApi.pluginDir + "/quotes.json" : ""
+
+ FileView {
+ id: quotesFileView
+ path: root._quotesPath || undefined
+ watchChanges: true
+ onLoaded: {
+ try {
+ var data = JSON.parse(quotesFileView.text());
+ if (Array.isArray(data) && data.length > 0) {
+ root.builtinQuotes = data;
+ // Trigger init if widget data was already injected but quotes weren't loaded yet
+ if (root.widgetData && !root.initialized) {
+ root._tryInit();
+ }
+ }
+ } catch (e) {
+ Logger.w("DailyQuote", "Failed to parse quotes.json:", e.message);
+ }
+ }
+ onFileChanged: reload()
+ }
+
+ // --- Deferred init: wait for both widgetData and quotes ---
+ function _tryInit() {
+ if (initialized || !widgetData || builtinQuotes.length === 0) return;
+ initialized = true;
+ var today = Qt.formatDate(new Date(), "yyyy-MM-dd");
+ if (storedQuote && storedDate === today) {
+ displayedQuote = storedQuote;
+ displayedAuthor = storedAuthor;
+ _startScramble(storedQuote, false);
+ } else {
+ refreshQuote();
+ }
+ }
+
+ // --- Scramble engine ---
+ function _startScramble(text, animate) {
+ _tickTimer.stop();
+ _targetText = text;
+ _revealIndex = 0;
+ _activeCount = 0;
+
+ if (!animate) {
+ _isAnimating = false;
+ var states = [];
+ for (var i = 0; i < text.length; i++) {
+ states.push({ "settled": true, "iteration": speedConfig.iterations + 1, "current": text.charAt(i) });
+ }
+ _charStates = states;
+ _updateDisplay();
+ return;
+ }
+
+ _isAnimating = true;
+ var states = [];
+ for (var i = 0; i < text.length; i++) {
+ if (text.charAt(i) === " ") {
+ states.push({ "settled": true, "iteration": 0, "current": " " });
+ } else {
+ states.push({
+ "settled": false,
+ "iteration": 0,
+ "current": _charSet.charAt(Math.floor(Math.random() * _charSet.length))
+ });
+ }
+ }
+ _charStates = states;
+ _updateDisplay();
+ _tickTimer.start();
+ }
+
+ // --- Persist current quote to widgetData ---
+ function _persistQuote() {
+ _selfUpdate = true;
+ if (widgetIndex >= 0 && screen) {
+ DesktopWidgetRegistry.updateWidgetData(screen.name, widgetIndex, {
+ "currentQuote": displayedQuote,
+ "currentAuthor": displayedAuthor,
+ "lastDate": Qt.formatDate(new Date(), "yyyy-MM-dd")
+ });
+ }
+ Qt.callLater(function() { _selfUpdate = false; });
+ }
+
+ // --- Refresh: new quote + scramble animation ---
+ function refreshQuote() {
+ var pool = builtinQuotes.concat(widgetData?.userQuotes || []);
+ if (pool.length === 0) {
+ displayedQuote = "Agrega tu primera frase en la configuracion";
+ displayedAuthor = "";
+ _startScramble(displayedQuote, false);
+ _persistQuote();
+ return;
+ }
+ var attempts = 0;
+ var selected;
+ do {
+ selected = pool[Math.floor(Math.random() * pool.length)];
+ attempts++;
+ } while (pool.length > 1 && selected.text === displayedQuote && attempts < 10);
+
+ displayedQuote = selected.text;
+ displayedAuthor = selected.author || "";
+ _pendingPersist = true;
+ _startScramble(displayedQuote, true);
+ }
+
+ function _updateDisplay() {
+ var html = "";
+ var settledColor = root.resolvedTextColor.toString();
+ var accentR = root.resolvedTextColor.r;
+ var accentG = root.resolvedTextColor.g;
+ var accentB = root.resolvedTextColor.b;
+
+ for (var i = 0; i < _charStates.length; i++) {
+ var s = _charStates[i];
+ var ch = s.current;
+ if (ch === "<") ch = "<";
+ else if (ch === ">") ch = ">";
+ else if (ch === "&") ch = "&";
+ else if (ch === "\"") ch = """;
+
+ if (s.settled || _targetText.charAt(i) === " ") {
+ html += "" + ch + "";
+ } else if (s.iteration === 0) {
+ var dimColor = Qt.rgba(accentR, accentG, accentB, 0.3);
+ html += "" + ch + "";
+ } else {
+ var progress = s.iteration / (speedConfig.iterations + 1);
+ var alpha = 0.5 + (progress * 0.5);
+ var scrambleColor = Qt.rgba(accentR, accentG, accentB, alpha);
+ html += "" + ch + "";
+ }
+ }
+ scrambleDisplay.text = html;
+ }
+
+ // --- Single animation timer ---
+ Timer {
+ id: _tickTimer
+ interval: root.speedConfig.tick
+ repeat: true
+ running: false
+
+ onTriggered: {
+ var states = root._charStates.slice();
+ var changed = false;
+
+ // 1) Reveal: activate next characters
+ for (var r = 0; r < root.speedConfig.revealPerTick; r++) {
+ if (root._revealIndex >= states.length) break;
+ if (root._targetText.charAt(root._revealIndex) !== " ") {
+ states[root._revealIndex] = {
+ "settled": false,
+ "iteration": 1,
+ "current": root._charSet.charAt(Math.floor(Math.random() * root._charSet.length))
+ };
+ changed = true;
+ }
+ root._revealIndex++;
+ }
+
+ // 2) Scramble: advance all active chars
+ var allSettled = true;
+ for (var i = 0; i < states.length; i++) {
+ if (states[i].settled) continue;
+ if (states[i].iteration === 0) { allSettled = false; continue; }
+
+ allSettled = false;
+ var iter = states[i].iteration + 1;
+ if (iter > root.speedConfig.iterations) {
+ states[i] = { "settled": true, "iteration": iter, "current": root._targetText.charAt(i) };
+ } else {
+ states[i] = { "settled": false, "iteration": iter, "current": root._charSet.charAt(Math.floor(Math.random() * root._charSet.length)) };
+ }
+ changed = true;
+ }
+
+ if (changed) {
+ root._charStates = states;
+ root._updateDisplay();
+ }
+
+ if (allSettled) {
+ _tickTimer.stop();
+ root._isAnimating = false;
+ if (root._pendingPersist) {
+ root._pendingPersist = false;
+ root._persistQuote();
+ }
+ }
+ }
+ }
+
+ // Safety net: force-end animation if stuck (max 8s)
+ Timer {
+ id: _animationWatchdog
+ interval: 8000
+ repeat: false
+ running: root._isAnimating
+ onTriggered: {
+ if (root._isAnimating) {
+ _tickTimer.stop();
+ root._isAnimating = false;
+ // Force-settle all remaining chars
+ var states = root._charStates.slice();
+ for (var i = 0; i < states.length; i++) {
+ if (!states[i].settled) {
+ states[i] = { "settled": true, "iteration": root.speedConfig.iterations + 1, "current": root._targetText.charAt(i) };
+ }
+ }
+ root._charStates = states;
+ root._updateDisplay();
+ if (root._pendingPersist) {
+ root._pendingPersist = false;
+ root._persistQuote();
+ }
+ }
+ }
+ }
+
+ // --- Init ---
+ property bool initialized: false
+ onWidgetDataChanged: {
+ if (!widgetData) return;
+ if (_isAnimating) return;
+ if (_selfUpdate) return;
+ _tryInit();
+ }
+
+ // --- Midnight auto-change ---
+ Timer {
+ interval: 60000
+ repeat: true
+ running: root.initialized && root.autoChangeDaily
+ onTriggered: {
+ var today = Qt.formatDate(new Date(), "yyyy-MM-dd");
+ if (root.storedDate !== today) root.refreshQuote();
+ }
+ }
+
+ // --- Click anywhere to refresh ---
+ MouseArea {
+ id: clickArea
+ anchors.fill: parent
+ z: 2
+ acceptedButtons: Qt.LeftButton
+ hoverEnabled: true
+ cursorShape: root._isAnimating ? Qt.BusyCursor : Qt.PointingHandCursor
+ onClicked: {
+ if (!root._isAnimating) root.refreshQuote();
+ }
+ }
+
+ // --- Gradient overlay (when background is off) ---
+ Rectangle {
+ id: gradientOverlay
+ anchors.fill: parent
+ z: 0
+ visible: !root.showBackground && root.showGradientOverlay
+ radius: root.roundedCorners ? Math.min(Math.round(Style.radiusL * widgetScale), Style.radiusL, width / 2, height / 2) : 0
+ gradient: Gradient {
+ orientation: root.gradientDirection === "horizontal" ? Gradient.Horizontal : Gradient.Vertical
+ GradientStop {
+ position: 0.0
+ color: Qt.rgba(root._gradR, root._gradG, root._gradB, 0.35)
+ }
+ GradientStop {
+ position: 0.6
+ color: Qt.rgba(root._gradR, root._gradG, root._gradB, 0.15)
+ }
+ GradientStop {
+ position: 1.0
+ color: Qt.rgba(root._gradR, root._gradG, root._gradB, 0.05)
+ }
+ }
+ }
+
+ // --- Visual content ---
+ ColumnLayout {
+ id: contentLayout
+ anchors.fill: parent
+ z: 1
+ anchors.leftMargin: Math.round(Style.marginL * widgetScale)
+ anchors.rightMargin: Math.round(Style.marginL * widgetScale)
+ anchors.topMargin: Math.round(Style.marginM * widgetScale)
+ anchors.bottomMargin: Math.round(Style.marginM * widgetScale)
+ spacing: Math.round(Style.marginS * widgetScale)
+
+ Text {
+ id: scrambleDisplay
+ Layout.fillWidth: true
+ Layout.preferredHeight: implicitHeight
+ textFormat: Text.RichText
+ wrapMode: Text.WordWrap
+ horizontalAlignment: root._effectiveAlign
+ color: Color.mOnSurface
+ font.pointSize: Math.round(Style.fontSizeM * widgetScale)
+ font.family: root.quoteFont || "monospace"
+ }
+
+ NText {
+ id: authorText
+ Layout.fillWidth: true
+ text: root.displayedAuthor ? "\u2014 " + root.displayedAuthor : ""
+ color: root.resolvedTextColor
+ pointSize: Math.round(Style.fontSizeS * widgetScale)
+ family: root.authorFont || Settings.data.ui.fontDefault
+ font.italic: true
+ horizontalAlignment: root._effectiveAlign
+ visible: root.showAuthor && root.displayedAuthor !== ""
+ opacity: visible ? 1.0 : 0.0
+ Behavior on opacity { NumberAnimation { duration: 300 } }
+ }
+
+ // Subtle hint
+ NText {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter
+ text: root._isAnimating ? "" : "click anywhere to refresh"
+ color: root.resolvedTextColor
+ pointSize: Math.round(Style.fontSizeXS * widgetScale)
+ opacity: clickArea.containsMouse && !root._isAnimating ? 0.6 : 0.0
+ Behavior on opacity { NumberAnimation { duration: 200 } }
+ }
+ }
+}
diff --git a/daily-quote/DesktopWidgetSettings.qml b/daily-quote/DesktopWidgetSettings.qml
new file mode 100644
index 000000000..f4ea03fbe
--- /dev/null
+++ b/daily-quote/DesktopWidgetSettings.qml
@@ -0,0 +1,330 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Commons
+import qs.Services.System
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+ spacing: Style.marginM
+
+ property var pluginApi: null
+ property var widgetSettings: null
+
+ // --- Read from widgetSettings.data ---
+ property string scrambleSpeed: widgetSettings?.data?.scrambleSpeed ?? "medium"
+ property string scrambleChars: widgetSettings?.data?.scrambleChars ?? "mix"
+ property bool showAuthor: widgetSettings?.data?.showAuthor ?? true
+ property bool autoChangeDaily: widgetSettings?.data?.autoChangeDaily ?? true
+ property var userQuotes: widgetSettings?.data?.userQuotes ?? []
+ property string quoteFont: widgetSettings?.data?.quoteFont ?? ""
+ property string authorFont: widgetSettings?.data?.authorFont ?? ""
+ property bool showBackground: widgetSettings?.data?.showBackground ?? true
+ property bool showGradientOverlay: widgetSettings?.data?.showGradientOverlay ?? true
+ property string gradientDirection: widgetSettings?.data?.gradientDirection ?? "vertical"
+ property string quoteColor: widgetSettings?.data?.quoteColor ?? "primary"
+ property string textAlign: widgetSettings?.data?.textAlign ?? "left"
+
+ function saveSettings() {
+ if (!widgetSettings || !widgetSettings.data) return;
+ widgetSettings.data.scrambleSpeed = scrambleSpeed;
+ widgetSettings.data.scrambleChars = scrambleChars;
+ widgetSettings.data.showAuthor = showAuthor;
+ widgetSettings.data.autoChangeDaily = autoChangeDaily;
+ widgetSettings.data.userQuotes = userQuotes;
+ widgetSettings.data.quoteFont = quoteFont;
+ widgetSettings.data.authorFont = authorFont;
+ widgetSettings.data.showBackground = showBackground;
+ widgetSettings.data.showGradientOverlay = showGradientOverlay;
+ widgetSettings.data.gradientDirection = gradientDirection;
+ widgetSettings.data.quoteColor = quoteColor;
+ widgetSettings.data.textAlign = textAlign;
+ widgetSettings.save();
+ }
+
+ // --- Header ---
+ NLabel {
+ label: "Daily Quote"
+ description: "Widget de frases diarias con efecto scramble decode"
+ Layout.fillWidth: true
+ }
+
+ NDivider { Layout.fillWidth: true }
+
+ // --- User Quotes List ---
+ NLabel {
+ label: "Tus frases"
+ description: userQuotes.length + " frases personalizadas"
+ Layout.fillWidth: true
+ }
+
+ListView {
+ id: quotesList
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.min(quotesList.contentHeight, 220)
+ Layout.minimumHeight: root.userQuotes.length > 0 ? 60 : 0
+ Layout.maximumHeight: 220
+ clip: true
+ spacing: Style.marginS
+ model: root.userQuotes
+ visible: root.userQuotes.length > 0
+
+ delegate: Rectangle {
+ id: quoteCard
+ width: quotesList.width
+ height: quoteContent.height + Style.marginM * 2
+ color: Color.mSurfaceVariant
+ radius: Style.radiusM
+
+ RowLayout {
+ id: quoteRow
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Style.marginM
+ spacing: Style.marginS
+
+ ColumnLayout {
+ id: quoteContent
+ Layout.fillWidth: true
+ spacing: 2
+
+ NText {
+ text: modelData.text
+ color: Color.mOnSurface
+ pointSize: Style.fontSizeS
+ Layout.fillWidth: true
+ wrapMode: Text.WordWrap
+ }
+ NText {
+ text: modelData.author ? "\u2014 " + modelData.author : ""
+ color: Color.mOnSurfaceVariant
+ pointSize: Style.fontSizeXS
+ visible: modelData.author && modelData.author !== ""
+ Layout.fillWidth: true
+ }
+ }
+
+ NIconButton {
+ icon: "close"
+ color: hovering ? Color.mError : Color.mOnSurfaceVariant
+ Layout.alignment: Qt.AlignTop
+ onClicked: {
+ var quotes = root.userQuotes.slice();
+ quotes.splice(index, 1);
+ root.userQuotes = quotes;
+ root.saveSettings();
+ }
+ }
+ }
+ }
+}
+
+ NDivider { Layout.fillWidth: true }
+
+ // --- Add new quote ---
+ NLabel {
+ label: "Agregar frase"
+ Layout.fillWidth: true
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: Style.marginS
+
+ NTextInput {
+ id: newQuoteInput
+ Layout.fillWidth: true
+ placeholderText: "Escribe tu frase aqui..."
+ }
+
+ NTextInput {
+ id: newAuthorInput
+ Layout.fillWidth: true
+ placeholderText: "Autor (opcional)..."
+ }
+
+ NButton {
+ text: "+ Agregar"
+ Layout.fillWidth: true
+ onClicked: {
+ if (newQuoteInput.text.trim() !== "") {
+ var quotes = root.userQuotes.slice();
+ quotes.push({
+ "text": newQuoteInput.text.trim(),
+ "author": newAuthorInput.text.trim()
+ });
+ root.userQuotes = quotes;
+ root.saveSettings();
+ newQuoteInput.text = "";
+ newAuthorInput.text = "";
+ }
+ }
+ }
+ }
+
+ NDivider { Layout.fillWidth: true }
+
+ // --- Typography ---
+ NLabel {
+ label: "Tipografia"
+ Layout.fillWidth: true
+ }
+
+ NSearchableComboBox {
+ Layout.fillWidth: true
+ label: "Fuente de la frase"
+ model: FontService.availableFonts
+ currentKey: root.quoteFont
+ placeholder: "Predeterminada del sistema"
+ searchPlaceholder: "Buscar fuente..."
+ popupHeight: 320
+ minimumWidth: 260
+ onSelected: function(key) {
+ root.quoteFont = key;
+ root.saveSettings();
+ }
+ defaultValue: ""
+ }
+
+ NSearchableComboBox {
+ Layout.fillWidth: true
+ label: "Fuente del autor"
+ model: FontService.availableFonts
+ currentKey: root.authorFont
+ placeholder: "Predeterminada del sistema"
+ searchPlaceholder: "Buscar fuente..."
+ popupHeight: 320
+ minimumWidth: 260
+ onSelected: function(key) {
+ root.authorFont = key;
+ root.saveSettings();
+ }
+ defaultValue: ""
+ }
+
+ NColorChoice {
+ Layout.fillWidth: true
+ label: "Color del texto"
+ currentKey: root.quoteColor
+ onSelected: function(key) {
+ root.quoteColor = key;
+ root.saveSettings();
+ }
+ defaultValue: "primary"
+ }
+
+ NComboBox {
+ Layout.fillWidth: true
+ label: "Alineacion"
+ model: [
+ { "key": "left", "name": "Izquierda" },
+ { "key": "center", "name": "Centro" },
+ { "key": "right", "name": "Derecha" }
+ ]
+ currentKey: root.textAlign
+ onSelected: function(key) {
+ root.textAlign = key;
+ root.saveSettings();
+ }
+ }
+
+ NDivider { Layout.fillWidth: true }
+
+ // --- Options ---
+ NLabel {
+ label: "Opciones"
+ Layout.fillWidth: true
+ }
+
+ NComboBox {
+ Layout.fillWidth: true
+ label: "Velocidad decodificacion"
+ model: [
+ { "key": "fast", "name": "Rapido" },
+ { "key": "medium", "name": "Medio" },
+ { "key": "slow", "name": "Lento" }
+ ]
+ currentKey: root.scrambleSpeed
+ onSelected: function(key) {
+ root.scrambleSpeed = key;
+ root.saveSettings();
+ }
+ }
+
+ NComboBox {
+ Layout.fillWidth: true
+ label: "Estilo caracteres"
+ model: [
+ { "key": "mix", "name": "Mixto" },
+ { "key": "ascii", "name": "ASCII" },
+ { "key": "symbols", "name": "Simbolos" }
+ ]
+ currentKey: root.scrambleChars
+ onSelected: function(key) {
+ root.scrambleChars = key;
+ root.saveSettings();
+ }
+ }
+
+ NToggle {
+ Layout.fillWidth: true
+ label: "Mostrar fondo"
+ checked: root.showBackground
+ defaultValue: true
+ onToggled: function(val) {
+ root.showBackground = val;
+ root.saveSettings();
+ }
+ }
+
+ NToggle {
+ Layout.fillWidth: true
+ label: "Gradiente sobre wallpaper"
+ description: "Suave velo para mejorar legibilidad (se adapta al modo claro/oscuro)"
+ visible: !root.showBackground
+ checked: root.showGradientOverlay
+ defaultValue: true
+ onToggled: function(val) {
+ root.showGradientOverlay = val;
+ root.saveSettings();
+ }
+ }
+
+ NComboBox {
+ Layout.fillWidth: true
+ visible: !root.showBackground && root.showGradientOverlay
+ label: "Direccion del gradiente"
+ model: [
+ { "key": "vertical", "name": "Vertical" },
+ { "key": "horizontal", "name": "Horizontal" }
+ ]
+ currentKey: root.gradientDirection
+ onSelected: function(key) {
+ root.gradientDirection = key;
+ root.saveSettings();
+ }
+ }
+
+ NToggle {
+ Layout.fillWidth: true
+ label: "Mostrar autor"
+ checked: root.showAuthor
+ defaultValue: true
+ onToggled: function(val) {
+ root.showAuthor = val;
+ root.saveSettings();
+ }
+ }
+
+ NToggle {
+ Layout.fillWidth: true
+ label: "Cambio automatico diario"
+ checked: root.autoChangeDaily
+ defaultValue: true
+ onToggled: function(val) {
+ root.autoChangeDaily = val;
+ root.saveSettings();
+ }
+ }
+}
diff --git a/daily-quote/README.md b/daily-quote/README.md
new file mode 100644
index 000000000..304a8bf91
--- /dev/null
+++ b/daily-quote/README.md
@@ -0,0 +1,54 @@
+# Daily Quote
+
+A desktop widget that displays a daily quote with a hacker-style scramble decode animation. Click anywhere on the widget to refresh with a new quote.
+
+
+
+## Features
+
+- **Scramble decode animation** — characters randomize then settle left-to-right with color transitions, like a terminal decrypting text
+- **3 speed presets** — Fast, Medium, Slow (controls tick rate, iteration count, and reveal speed)
+- **3 character sets** — Mix (alphanumeric + box-drawing), ASCII, Symbols (Unicode box-drawing characters)
+- **Custom quotes** — add your own quotes with author, view and remove them from settings
+- **Custom fonts** — separate font selectors for the quote text and author line
+- **Theme-aware text colors** — choose from Primary, Secondary, Tertiary, Error, or None; colors automatically adapt to your color scheme and wallpaper
+- **Text alignment** — Left, Center, or Right
+- **Background options** — solid panel (default), gradient overlay, or fully transparent
+- **Adaptive gradient overlay** — when background is off, a subtle gradient veil improves readability; auto-switches between black (dark mode) and white (light mode)
+- **Daily auto-change** — automatically shows a new quote at midnight
+- **Click to refresh** — click the widget at any time for a new quote with animation
+
+## Settings
+
+| Setting | Description | Default |
+|---|---|---|
+| **Your Quotes** | Add, view, and remove custom quotes | — |
+| **Quote Font** | Font for the quote text (scramble + settled) | System monospace |
+| **Author Font** | Font for the author line | System default |
+| **Text Color** | Color key that adapts to your color scheme | Primary |
+| **Text Alignment** | Horizontal alignment for quote and author | Left |
+| **Show Background** | Solid panel with border and shadow | On |
+| **Gradient Overlay** | Subtle veil behind text when background is off | On |
+| **Gradient Direction** | Vertical or Horizontal | Vertical |
+| **Decoding Speed** | Fast, Medium, or Slow | Medium |
+| **Character Style** | Mix, ASCII, or Symbols | Mix |
+| **Show Author** | Toggle the author line | On |
+| **Daily Auto-Change** | New quote at midnight | On |
+
+## Custom Quotes
+
+You can add your own quotes through the settings panel. Quotes are stored in your widget data and persist across sessions.
+
+Additionally, the built-in quote pool is loaded from `quotes.json` in the plugin directory. You can edit this file directly to change the default quotes — the widget will hot-reload when the file changes.
+
+## Requirements
+
+- Noctalia Shell 4.7.0 or later
+
+## License
+
+MIT
+
+## Author
+
+Vikthor
diff --git a/daily-quote/manifest.json b/daily-quote/manifest.json
new file mode 100644
index 000000000..cb7416659
--- /dev/null
+++ b/daily-quote/manifest.json
@@ -0,0 +1,29 @@
+{
+ "id": "daily-quote",
+ "name": "Daily Quote",
+ "version": "1.0.0",
+ "minNoctaliaVersion": "4.7.0",
+ "author": "Vikthor",
+ "license": "MIT",
+ "repository": "https://github.com/noctalia-dev/noctalia-plugins",
+ "description": "Desktop widget that displays a daily quote with a scramble decode animation",
+ "tags": ["Desktop", "Fun"],
+ "entryPoints": {
+ "desktopWidget": "DesktopWidget.qml",
+ "desktopWidgetSettings": "DesktopWidgetSettings.qml"
+ },
+ "dependencies": {
+ "plugins": []
+ },
+ "metadata": {
+ "defaultSettings": {
+ "quoteFont": "",
+ "authorFont": "",
+ "showBackground": true,
+ "showGradientOverlay": true,
+ "gradientDirection": "vertical",
+ "quoteColor": "primary",
+ "textAlign": "left"
+ }
+ }
+}
\ No newline at end of file
diff --git a/daily-quote/preview.png b/daily-quote/preview.png
new file mode 100644
index 000000000..5c504ed46
Binary files /dev/null and b/daily-quote/preview.png differ
diff --git a/daily-quote/quotes.json b/daily-quote/quotes.json
new file mode 100644
index 000000000..57406dc83
--- /dev/null
+++ b/daily-quote/quotes.json
@@ -0,0 +1,63 @@
+[
+ { "text": "El unico modo de hacer un gran trabajo es amar lo que haces", "author": "Steve Jobs" },
+ { "text": "Talk is cheap, show me the code", "author": "Linus Torvalds" },
+ { "text": "La simplicidad es la maxima sofisticacion", "author": "Leonardo da Vinci" },
+ { "text": "Any fool can write code that a computer can understand. Good programmers write code that humans can understand", "author": "Martin Fowler" },
+ { "text": "La vida es lo que pasa mientras estas ocupado haciendo otros planes", "author": "John Lennon" },
+ { "text": "First, solve the problem. Then, write the code", "author": "John Johnson" },
+ { "text": "La creatividad es la inteligencia divirtiendose", "author": "Albert Einstein" },
+ { "text": "The best way to predict the future is to implement it", "author": "DHH" },
+ { "text": "Code is like humor. When you have to explain it, it's bad", "author": "Cory House" },
+ { "text": "Simplicity is prerequisite for reliability", "author": "Edsger Dijkstra" },
+ { "text": "There are only two hard things in CS: cache invalidation and naming things", "author": "Phil Karlton" },
+ { "text": "Premature optimization is the root of all evil", "author": "Donald Knuth" },
+ { "text": "Any sufficiently advanced technology is indistinguishable from magic", "author": "Arthur C. Clarke" },
+ { "text": "The only way to go fast is to go well", "author": "Robert C. Martin" },
+ { "text": "No llores porque se termino, sonrie porque sucedio", "author": "Dr. Seuss" },
+
+ { "text": "七転び八起き (Nanakorobi yaoki): Cae siete veces, levantate ocho", "author": "Proverbio Japones" },
+ { "text": "一期一会 (Ichigo ichie): Cada encuentro es unico", "author": "Proverbio Japones" },
+ { "text": "改善 (Kaizen): Mejora continua", "author": "Filosofia Japonesa" },
+ { "text": "武士道 (Bushido): El camino del guerrero", "author": "Codigo Samurai" },
+ { "text": "夢を諦めない (Yume wo akiramenai): Nunca abandones tus sueños", "author": "Frase Japonesa" },
+
+ { "text": "Memento mori: Recuerda que moriras", "author": "Tradicion Latina" },
+ { "text": "Per aspera ad astra: Hacia las estrellas a traves de las dificultades", "author": "Seneca" },
+ { "text": "Carpe diem: Aprovecha el dia", "author": "Horacio" },
+ { "text": "Acta non verba: Hechos, no palabras", "author": "Lema Latino" },
+ { "text": "Fortes fortuna adiuvat: La fortuna favorece a los valientes", "author": "Virgilio" },
+ { "text": "Ora et labora: Ora y trabaja", "author": "San Benito" },
+ { "text": "Fiat lux: Hagase la luz", "author": "Genesis 1:3" },
+ { "text": "Ad victoriam: Hacia la victoria", "author": "Lema Latino" },
+
+ { "text": "Chi va piano va sano e va lontano", "author": "Proverbio Italiano" },
+ { "text": "La notte e piu oscura prima dell'alba", "author": "Proverbio Italiano" },
+ { "text": "Coraggio sopra ogni cosa", "author": "Proverbio Italiano" },
+ { "text": "Dio e la mia luce", "author": "Lema Italiano" },
+
+ { "text": "La disciplina tarde o temprano vencera al talento", "author": "Anonimo" },
+ { "text": "Great things never came from comfort zones", "author": "Anonimo" },
+ { "text": "Discipline equals freedom", "author": "Jocko Willink" },
+ { "text": "Dreams demand sacrifice", "author": "Anonimo" },
+ { "text": "The obstacle is the way", "author": "Marco Aurelio" },
+ { "text": "Victory belongs to the most persevering", "author": "Napoleon Bonaparte" },
+ { "text": "Stay hungry, stay foolish", "author": "Steve Jobs" },
+ { "text": "He who has a why to live can bear almost any how", "author": "Friedrich Nietzsche" },
+
+ { "text": "No temas, porque yo estoy contigo", "author": "Isaias 41:10" },
+ { "text": "La verdad os hara libres", "author": "Juan 8:32" },
+ { "text": "Todo tiene su tiempo debajo del cielo", "author": "Eclesiastes 3:1" },
+ { "text": "Aunque ande en valle de sombra de muerte, no temere mal alguno", "author": "Salmos 23:4" },
+ { "text": "El hierro se afila con hierro", "author": "Proverbios 27:17" },
+
+ { "text": "The quieter you become, the more you are able to hear", "author": "Rumi" },
+ { "text": "Your future is created by what you do today, not tomorrow", "author": "Robert Kiyosaki" },
+ { "text": "Fall seven times, stand up eight", "author": "Proverbio Japones" },
+ { "text": "Never trust a computer you cant throw out a window", "author": "Steve Wozniak" },
+ { "text": "Programs must be written for people to read", "author": "Harold Abelson" },
+ { "text": "Reality is wrong, dreams are for real", "author": "Tupac Shakur" },
+ { "text": "The soul becomes dyed with the color of its thoughts", "author": "Marco Aurelio" },
+ { "text": "In the middle of difficulty lies opportunity", "author": "Albert Einstein" },
+ { "text": "Nothing worth having comes easy", "author": "Theodore Roosevelt" },
+ { "text": "Stars cant shine without darkness", "author": "Anonimo" }
+]
\ No newline at end of file