fix(plugin): Feed text encoding fix and panel UI improvements V1.0.2 (#271)
* fix(plugin): Feed text encoding fix and panel UI improvements V1.0.2 * fix(plugin): Feed text encoding fix and panel UI improvements V1.0.2 * Add xdg-open dependency to plugin.toml Added 'xdg-open' as a dependency for the plugin.
This commit is contained in:
+21
-7
@@ -3,6 +3,7 @@
|
|||||||
-- recentes, ou um estado vazio com icone quando nao ha nada novo.
|
-- recentes, ou um estado vazio com icone quando nao ha nada novo.
|
||||||
|
|
||||||
local items = {}
|
local items = {}
|
||||||
|
local hoveredKey = nil
|
||||||
|
|
||||||
-- declaradas antes para permitir referencia cruzada entre elas
|
-- declaradas antes para permitir referencia cruzada entre elas
|
||||||
local render
|
local render
|
||||||
@@ -38,13 +39,22 @@ dismissItem = function(item)
|
|||||||
end
|
end
|
||||||
|
|
||||||
itemRow = function(item)
|
itemRow = function(item)
|
||||||
|
local key = item.id or item.link or item.title
|
||||||
|
local hovered = hoveredKey == key
|
||||||
|
|
||||||
return ui.row({
|
return ui.row({
|
||||||
key = item.id or item.link or item.title,
|
key = key,
|
||||||
gap = 6,
|
gap = 6,
|
||||||
padding = 10,
|
padding = 10,
|
||||||
radius = 8,
|
radius = 8,
|
||||||
fill = "surface_variant/0.4",
|
fill = "surface_variant/0.4",
|
||||||
align = "center",
|
align = "center",
|
||||||
|
border = "outline",
|
||||||
|
borderWidth = hovered and 1 or 0,
|
||||||
|
onHover = function(isHovering)
|
||||||
|
hoveredKey = isHovering and key or nil
|
||||||
|
render()
|
||||||
|
end,
|
||||||
}, {
|
}, {
|
||||||
ui.column({
|
ui.column({
|
||||||
gap = 2,
|
gap = 2,
|
||||||
@@ -81,6 +91,8 @@ render = function()
|
|||||||
for _, item in ipairs(items) do
|
for _, item in ipairs(items) do
|
||||||
table.insert(rows, itemRow(item))
|
table.insert(rows, itemRow(item))
|
||||||
end
|
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)
|
body = ui.scroll({ gap = 6, flexGrow = 1 }, rows)
|
||||||
end
|
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 = "refresh", variant = "outline", controlSize = "sm", tooltip = "Atualizar agora", onClick = "onRefreshClicked" }))
|
||||||
table.insert(headerButtons, ui.button({ glyph = "close", variant = "outline", controlSize = "sm", onClick = "onCloseClicked" }))
|
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.row({ align = "center", justify = "space_between" }, {
|
||||||
ui.label({ text = "RSS/Atom Notifier", fontSize = 15, fontWeight = "bold" }),
|
ui.label({ text = "RSS/Atom Notifier", fontSize = 15, fontWeight = "bold" }),
|
||||||
ui.row({ gap = 4 }, headerButtons),
|
ui.row({ gap = 4 }, headerButtons),
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
id = "nilsonlinux/rss-notifier"
|
id = "nilsonlinux/rss-notifier"
|
||||||
name = "RSS/Atom Notifier"
|
name = "RSS/Atom Notifier"
|
||||||
version = "1.0.1"
|
version = "1.0.2"
|
||||||
plugin_api = 14
|
plugin_api = 14
|
||||||
author = "Nilsonlinux"
|
author = "Nilsonlinux"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
icon = "rss"
|
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"]
|
tags = ["utility", "indicator"]
|
||||||
dependencies = ["xdg-open"]
|
dependencies = ["xdg-open"]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Plugin level settings: shared by ALL entries
|
||||||
|
# (service + widget). Edited in Settings -> Plugins.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
[[setting]]
|
[[setting]]
|
||||||
key = "feed_urls"
|
key = "feed_urls"
|
||||||
type = "string_list"
|
type = "string_list"
|
||||||
@@ -42,14 +47,33 @@ min = 1
|
|||||||
max = 50
|
max = 50
|
||||||
advanced = true
|
advanced = true
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Service: runs in the background, without UI, searches and parses feeds
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
[[service]]
|
[[service]]
|
||||||
id = "fetcher"
|
id = "fetcher"
|
||||||
entry = "service.luau"
|
entry = "service.luau"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bar widget: shows the count of unread items
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
[[widget]]
|
[[widget]]
|
||||||
id = "badge"
|
id = "badge"
|
||||||
entry = "widget.luau"
|
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]]
|
[[panel]]
|
||||||
id = "list"
|
id = "list"
|
||||||
entry = "panel.luau"
|
entry = "panel.luau"
|
||||||
|
|||||||
@@ -98,10 +98,54 @@ local function isFirstRunForFeed(url)
|
|||||||
return not bucket or not bucket.order or #bucket.order == 0
|
return not bucket or not bucket.order or #bucket.order == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Remove/descarta qualquer sequencia de bytes que NAO seja UTF-8 valido.
|
-- Converte um codepoint Unicode para a sequencia de bytes UTF-8 equivalente.
|
||||||
-- Necessario porque alguns feeds vem com o charset errado no servidor (bytes
|
local function codepointToUtf8(cp)
|
||||||
-- Latin-1/CP-1252 marcados como UTF-8, por exemplo) - nesse caso nem um corte
|
if not cp or cp < 0 then
|
||||||
-- perfeito resolve, o defeito ja vem no texto original.
|
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)
|
local function sanitizeUtf8(s)
|
||||||
if not s then
|
if not s then
|
||||||
return s
|
return s
|
||||||
@@ -141,7 +185,8 @@ local function sanitizeUtf8(s)
|
|||||||
out[#out + 1] = s:sub(i, i + seqLen - 1)
|
out[#out + 1] = s:sub(i, i + seqLen - 1)
|
||||||
i = i + seqLen
|
i = i + seqLen
|
||||||
else
|
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
|
end
|
||||||
end
|
end
|
||||||
@@ -226,6 +271,22 @@ end
|
|||||||
-- que parece caro demais para o orcamento de CPU deste ambiente.
|
-- 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)
|
local function decodeEntities(s)
|
||||||
if not s then
|
if not s then
|
||||||
return s
|
return s
|
||||||
@@ -234,7 +295,20 @@ local function decodeEntities(s)
|
|||||||
s = s:gsub(">", ">")
|
s = s:gsub(">", ">")
|
||||||
s = s:gsub(""", '"')
|
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))
|
return sanitizeUtf8(noctalia.string.trim(s))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 75 KiB |
@@ -1,20 +1,24 @@
|
|||||||
{
|
{
|
||||||
"settings": {
|
"settings": {
|
||||||
"feed_urls": {
|
"feed_urls": {
|
||||||
"description": "List of RSS/Atom feed URLs to monitor (one per line)",
|
"label": "Feed URLs",
|
||||||
"label": "Feed URLs"
|
"description": "One RSS or Atom feed URL per entry (e.g. https://example.com/feed.xml)."
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"refresh_minutes": {
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
{
|
{
|
||||||
"settings": {
|
"settings": {
|
||||||
"feed_urls": {
|
"feed_urls": {
|
||||||
"description": "Lista de URLs de feeds RSS/Atom para monitorar (por linha).",
|
"label": "URLs dos feeds",
|
||||||
"label": "URLs dos Feeds"
|
"description": "Uma URL de feed RSS ou Atom por entrada (ex: https://exemplo.com/feed.xml)."
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"refresh_minutes": {
|
"refresh_minutes": {
|
||||||
"description": "Com que frequência verificar novos itens (em minutos).",
|
"label": "Intervalo de atualização (minutos)",
|
||||||
"label": "Intervalo de atualização"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ local function render()
|
|||||||
}))
|
}))
|
||||||
end
|
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
|
local container = barWidget.isVertical() and ui.column or ui.row
|
||||||
barWidget.render(container({ gap = 4, align = "center" }, children))
|
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
|
noctalia.setUpdateInterval(5000) -- so mantem o widget "vivo"; os dados reais vem do state
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function onConfigChanged()
|
||||||
|
render() -- pega o novo glyph quando o usuario troca o icone nas settings do widget
|
||||||
|
end
|
||||||
|
|
||||||
function onClick()
|
function onClick()
|
||||||
-- abre/fecha a janelinha com a lista de itens; o proprio painel marca
|
-- 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
|
-- como lido (zera o badge) quando e aberto, via IPC pro service
|
||||||
|
|||||||
Reference in New Issue
Block a user