diff --git a/rss-notifier/panel.luau b/rss-notifier/panel.luau index 2b5d9f8..471da1e 100644 --- a/rss-notifier/panel.luau +++ b/rss-notifier/panel.luau @@ -3,6 +3,7 @@ -- recentes, ou um estado vazio com icone quando nao ha nada novo. local items = {} +local hoveredKey = nil -- declaradas antes para permitir referencia cruzada entre elas local render @@ -38,13 +39,22 @@ dismissItem = function(item) end itemRow = function(item) + local key = item.id or item.link or item.title + local hovered = hoveredKey == key + return ui.row({ - key = item.id or item.link or item.title, - gap = 6, - padding = 10, - radius = 8, - fill = "surface_variant/0.4", - align = "center", + key = key, + gap = 6, + padding = 10, + radius = 8, + fill = "surface_variant/0.4", + align = "center", + border = "outline", + borderWidth = hovered and 1 or 0, + onHover = function(isHovering) + hoveredKey = isHovering and key or nil + render() + end, }, { ui.column({ gap = 2, @@ -81,6 +91,8 @@ render = function() for _, item in ipairs(items) do table.insert(rows, itemRow(item)) end + -- flexGrow preenche o espaco que sobrar dentro da coluna raiz - so + -- funciona se essa coluna tiver uma altura definida (ver abaixo). body = ui.scroll({ gap = 6, flexGrow = 1 }, rows) end @@ -91,7 +103,9 @@ render = function() table.insert(headerButtons, ui.button({ glyph = "refresh", variant = "outline", controlSize = "sm", tooltip = "Atualizar agora", onClick = "onRefreshClicked" })) table.insert(headerButtons, ui.button({ glyph = "close", variant = "outline", controlSize = "sm", onClick = "onCloseClicked" })) - panel.render(ui.column({ gap = 10, padding = 12, fill = true }, { + -- um pouco menor que os 415 do painel (ver plugin.toml) para sobrar uma + -- margem de seguranca antes da borda arredondada, em vez de encostar nela + panel.render(ui.column({ gap = 10, padding = 12, height = 415 }, { ui.row({ align = "center", justify = "space_between" }, { ui.label({ text = "RSS/Atom Notifier", fontSize = 15, fontWeight = "bold" }), ui.row({ gap = 4 }, headerButtons), diff --git a/rss-notifier/plugin.toml b/rss-notifier/plugin.toml index 29fa93a..4cf1ae7 100644 --- a/rss-notifier/plugin.toml +++ b/rss-notifier/plugin.toml @@ -1,14 +1,19 @@ id = "nilsonlinux/rss-notifier" name = "RSS/Atom Notifier" -version = "1.0.1" +version = "1.0.2" plugin_api = 14 author = "Nilsonlinux" license = "MIT" icon = "rss" -description = "Acompanha feeds RSS/Atom e notifica quando surgem novos itens." +description = "Monitors RSS/Atom feeds and notifies you when new items appear." tags = ["utility", "indicator"] dependencies = ["xdg-open"] +# --------------------------------------------------------------------------- +# Plugin level settings: shared by ALL entries +# (service + widget). Edited in Settings -> Plugins. +# --------------------------------------------------------------------------- + [[setting]] key = "feed_urls" type = "string_list" @@ -42,14 +47,33 @@ min = 1 max = 50 advanced = true +# --------------------------------------------------------------------------- +# Service: runs in the background, without UI, searches and parses feeds +# --------------------------------------------------------------------------- + [[service]] id = "fetcher" entry = "service.luau" +# --------------------------------------------------------------------------- +# Bar widget: shows the count of unread items +# --------------------------------------------------------------------------- + [[widget]] id = "badge" entry = "widget.luau" +[[widget.setting]] +key = "glyph" +type = "glyph" +label_key = "settings.glyph.label" +description_key = "settings.glyph.description" +default = "rss" + +# --------------------------------------------------------------------------- +# Panel: small window with the list of items, opened by clicking on the widget +# --------------------------------------------------------------------------- + [[panel]] id = "list" entry = "panel.luau" diff --git a/rss-notifier/service.luau b/rss-notifier/service.luau index cd3369f..a31563e 100644 --- a/rss-notifier/service.luau +++ b/rss-notifier/service.luau @@ -98,10 +98,54 @@ local function isFirstRunForFeed(url) return not bucket or not bucket.order or #bucket.order == 0 end --- Remove/descarta qualquer sequencia de bytes que NAO seja UTF-8 valido. --- Necessario porque alguns feeds vem com o charset errado no servidor (bytes --- Latin-1/CP-1252 marcados como UTF-8, por exemplo) - nesse caso nem um corte --- perfeito resolve, o defeito ja vem no texto original. +-- Converte um codepoint Unicode para a sequencia de bytes UTF-8 equivalente. +local function codepointToUtf8(cp) + if not cp or cp < 0 then + return "" + elseif cp < 0x80 then + return string.char(cp) + elseif cp < 0x800 then + return string.char(0xC0 + math.floor(cp / 0x40), 0x80 + (cp % 0x40)) + elseif cp < 0x10000 then + return string.char( + 0xE0 + math.floor(cp / 0x1000), + 0x80 + (math.floor(cp / 0x40) % 0x40), + 0x80 + (cp % 0x40) + ) + else + return string.char( + 0xF0 + math.floor(cp / 0x40000), + 0x80 + (math.floor(cp / 0x1000) % 0x40), + 0x80 + (math.floor(cp / 0x40) % 0x40), + 0x80 + (cp % 0x40) + ) + end +end + +-- CP-1252 diverge de Latin-1 exatamente nos bytes 0x80-0x9F: em Latin-1 sao +-- codigos de controle inuteis, mas em CP-1252 (o padrao de fato mais comum +-- em feeds mal-marcados) sao aspas curvas, travessao, reticencias etc. +local CP1252_HIGH = { + [0x80] = 0x20AC, [0x82] = 0x201A, [0x83] = 0x0192, [0x84] = 0x201E, + [0x85] = 0x2026, [0x86] = 0x2020, [0x87] = 0x2021, [0x88] = 0x02C6, + [0x89] = 0x2030, [0x8A] = 0x0160, [0x8B] = 0x2039, [0x8C] = 0x0152, + [0x8E] = 0x017D, [0x91] = 0x2018, [0x92] = 0x2019, [0x93] = 0x201C, + [0x94] = 0x201D, [0x95] = 0x2022, [0x96] = 0x2013, [0x97] = 0x2014, + [0x98] = 0x02DC, [0x99] = 0x2122, [0x9A] = 0x0161, [0x9B] = 0x203A, + [0x9C] = 0x0153, [0x9E] = 0x017E, [0x9F] = 0x0178, +} + +-- Converte um unico byte Latin-1/CP-1252 (0x80-0xFF) para a sequencia UTF-8 +-- equivalente - usado como fallback quando um byte nao forma UTF-8 valido. +local function latin1ByteToUtf8(b) + local cp = CP1252_HIGH[b] or b -- 0xA0-0xFF: Latin-1 e CP-1252 coincidem + return codepointToUtf8(cp) +end + +-- Corrige/normaliza texto para UTF-8 valido. Quando um byte nao forma uma +-- sequencia UTF-8 valida, assume Latin-1/CP-1252 (charset errado e comum +-- de feeds mais antigos, como sites de noticia brasileiros) e CONVERTE em +-- vez de descartar - assim o acento aparece certo em vez de sumir. local function sanitizeUtf8(s) if not s then return s @@ -141,7 +185,8 @@ local function sanitizeUtf8(s) out[#out + 1] = s:sub(i, i + seqLen - 1) i = i + seqLen else - i = i + 1 -- byte invalido: descarta so ele e continua + out[#out + 1] = latin1ByteToUtf8(b) -- converte em vez de descartar + i = i + 1 end end end @@ -226,6 +271,22 @@ end -- que parece caro demais para o orcamento de CPU deste ambiente. -- --------------------------------------------------------------------------- +-- (codepointToUtf8 ja foi definida mais acima, perto de sanitizeUtf8 - reusada aqui) + +-- Entidades nomeadas de acento mais comuns em portugues (nao sao XML puro, +-- mas alguns feeds mal-formados usam mesmo assim). +local NAMED_ENTITIES = { + aacute = 0xE1, eacute = 0xE9, iacute = 0xED, oacute = 0xF3, uacute = 0xFA, + Aacute = 0xC1, Eacute = 0xC9, Iacute = 0xCD, Oacute = 0xD3, Uacute = 0xDA, + atilde = 0xE3, otilde = 0xF5, Atilde = 0xC3, Otilde = 0xD5, + acirc = 0xE2, ecirc = 0xEA, ocirc = 0xF4, Acirc = 0xC2, Ecirc = 0xCA, Ocirc = 0xD4, + ccedil = 0xE7, Ccedil = 0xC7, + agrave = 0xE0, egrave = 0xE8, Agrave = 0xC0, Egrave = 0xC8, + uuml = 0xFC, Uuml = 0xDC, nbsp = 0x20, ndash = 0x2013, mdash = 0x2014, + lsquo = 0x2018, rsquo = 0x2019, ldquo = 0x201C, rdquo = 0x201D, + apos = 0x0027, hellip = 0x2026, bull = 0x2022, +} + local function decodeEntities(s) if not s then return s @@ -234,7 +295,20 @@ local function decodeEntities(s) s = s:gsub(">", ">") s = s:gsub(""", '"') s = s:gsub("'", "'") - s = s:gsub("&", "&") + -- numericas decimais: ô + s = s:gsub("&#(%d+);", function(digits) + return codepointToUtf8(tonumber(digits)) + end) + -- numericas hexadecimais: ô ou ô + s = s:gsub("&#[xX](%x+);", function(hex) + return codepointToUtf8(tonumber(hex, 16)) + end) + -- nomeadas comuns de acento (feeds nao-XML-estritos) + s = s:gsub("&(%a+);", function(name) + local cp = NAMED_ENTITIES[name] + return cp and codepointToUtf8(cp) or ("&" .. name .. ";") + end) + s = s:gsub("&", "&") -- por ultimo, senao "&lt;" viraria "<" errado return sanitizeUtf8(noctalia.string.trim(s)) end diff --git a/rss-notifier/thumbnail.webp b/rss-notifier/thumbnail.webp index ff9b0a8..da2ff6a 100644 Binary files a/rss-notifier/thumbnail.webp and b/rss-notifier/thumbnail.webp differ diff --git a/rss-notifier/translations/en.json b/rss-notifier/translations/en.json index 664f7e1..a1c711d 100644 --- a/rss-notifier/translations/en.json +++ b/rss-notifier/translations/en.json @@ -1,20 +1,24 @@ { "settings": { "feed_urls": { - "description": "List of RSS/Atom feed URLs to monitor (one per line)", - "label": "Feed URLs" - }, - "max_notifications_per_cycle": { - "description": "Maximum notifications to show in one check", - "label": "Max Notifications per Cycle" - }, - "notify_new": { - "description": "Display notifications when new items arrive", - "label": "Show Notifications" + "label": "Feed URLs", + "description": "One RSS or Atom feed URL per entry (e.g. https://example.com/feed.xml)." }, "refresh_minutes": { - "description": "How often to check for new items", - "label": "Refresh Interval (minutes)" + "label": "Refresh interval (minutes)", + "description": "How often feeds are checked for new items." + }, + "notify_new": { + "label": "Notify on new items", + "description": "Show a notification for each new item found in the feeds." + }, + "max_notifications_per_cycle": { + "label": "Max notifications per cycle", + "description": "Avoids a flood of notifications when many new items appear at once." + }, + "glyph": { + "label": "Icon", + "description": "Icon shown in the bar for this widget." } } } diff --git a/rss-notifier/translations/pt-BR.json b/rss-notifier/translations/pt-BR.json index e9d7c3d..dfbc789 100644 --- a/rss-notifier/translations/pt-BR.json +++ b/rss-notifier/translations/pt-BR.json @@ -1,20 +1,24 @@ { "settings": { "feed_urls": { - "description": "Lista de URLs de feeds RSS/Atom para monitorar (por linha).", - "label": "URLs dos Feeds" - }, - "max_notifications_per_cycle": { - "description": "Limitar o número de notificações enviadas em um único ciclo.", - "label": "Máximo de notificações" - }, - "notify_new": { - "description": "Mostrar notificação para cada novo item encontrado.", - "label": "Notificar novos itens" + "label": "URLs dos feeds", + "description": "Uma URL de feed RSS ou Atom por entrada (ex: https://exemplo.com/feed.xml)." }, "refresh_minutes": { - "description": "Com que frequência verificar novos itens (em minutos).", - "label": "Intervalo de atualização" + "label": "Intervalo de atualização (minutos)", + "description": "Frequência com que os feeds são checados em busca de novidades." + }, + "notify_new": { + "label": "Notificar novos itens", + "description": "Mostra uma notificação para cada item novo encontrado nos feeds." + }, + "max_notifications_per_cycle": { + "label": "Máximo de notificações por ciclo", + "description": "Evita uma enxurrada de notificações quando muitos itens novos aparecem de uma vez." + }, + "glyph": { + "label": "Ícone", + "description": "Ícone mostrado na barra para este widget." } } } diff --git a/rss-notifier/widget.luau b/rss-notifier/widget.luau index ab8af22..37ffe2b 100644 --- a/rss-notifier/widget.luau +++ b/rss-notifier/widget.luau @@ -22,7 +22,7 @@ local function render() })) end - table.insert(children, ui.glyph({ name = "rss", size = 14 })) + table.insert(children, ui.glyph({ name = noctalia.getConfig("glyph") or "rss", size = 14 })) local container = barWidget.isVertical() and ui.column or ui.row barWidget.render(container({ gap = 4, align = "center" }, children)) @@ -41,6 +41,10 @@ function update() noctalia.setUpdateInterval(5000) -- so mantem o widget "vivo"; os dados reais vem do state end +function onConfigChanged() + render() -- pega o novo glyph quando o usuario troca o icone nas settings do widget +end + function onClick() -- abre/fecha a janelinha com a lista de itens; o proprio painel marca -- como lido (zera o badge) quando e aberto, via IPC pro service