mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2026-03-06 09:27:33 +01:00
Funciona ver posts.
This commit is contained in:
@@ -3,6 +3,13 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import languageHandler
|
||||||
|
import output
|
||||||
|
import widgetUtils
|
||||||
|
from controller import messages as base_messages
|
||||||
|
from wxUI.dialogs.blueski import postDialogs
|
||||||
|
|
||||||
# Translation function is provided globally by TWBlue's language handler (_)
|
# Translation function is provided globally by TWBlue's language handler (_)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -89,3 +96,167 @@ def format_error_message(error_description: str, details: str | None = None) ->
|
|||||||
# }
|
# }
|
||||||
|
|
||||||
logger.info("Blueski messages module loaded (placeholders).")
|
logger.info("Blueski messages module loaded (placeholders).")
|
||||||
|
|
||||||
|
|
||||||
|
def _g(obj: Any, key: str, default: Any = None) -> Any:
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj.get(key, default)
|
||||||
|
return getattr(obj, key, default)
|
||||||
|
|
||||||
|
|
||||||
|
def has_post_data(item: Any) -> bool:
|
||||||
|
post = _g(item, "post")
|
||||||
|
record = _g(post, "record") if post is not None else None
|
||||||
|
if record is None:
|
||||||
|
record = _g(item, "record")
|
||||||
|
return record is not None or post is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_labels(obj: Any) -> list[dict[str, Any]]:
|
||||||
|
labels = _g(obj, "labels", None)
|
||||||
|
if labels is None:
|
||||||
|
return []
|
||||||
|
if isinstance(labels, dict):
|
||||||
|
labels = labels.get("values", [])
|
||||||
|
if isinstance(labels, list):
|
||||||
|
return labels
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_cw_text(post: Any, record: Any) -> str:
|
||||||
|
labels = _extract_labels(post) + _extract_labels(record)
|
||||||
|
for label in labels:
|
||||||
|
val = _g(label, "val", "")
|
||||||
|
if val == "warn":
|
||||||
|
return _("Sensitive Content")
|
||||||
|
if isinstance(val, str) and val.startswith("warn:"):
|
||||||
|
return val.split("warn:", 1)[-1].strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_image_descriptions(post: Any, record: Any) -> str:
|
||||||
|
def _collect_images(embed: Any) -> list[Any]:
|
||||||
|
if not embed:
|
||||||
|
return []
|
||||||
|
etype = _g(embed, "$type") or _g(embed, "py_type") or ""
|
||||||
|
if "recordWithMedia" in etype:
|
||||||
|
media = _g(embed, "media")
|
||||||
|
mtype = _g(media, "$type") or _g(media, "py_type") or ""
|
||||||
|
if "images" in mtype:
|
||||||
|
return list(_g(media, "images", []) or [])
|
||||||
|
return []
|
||||||
|
if "images" in etype:
|
||||||
|
return list(_g(embed, "images", []) or [])
|
||||||
|
return []
|
||||||
|
|
||||||
|
images = []
|
||||||
|
images.extend(_collect_images(_g(post, "embed")))
|
||||||
|
if not images:
|
||||||
|
images.extend(_collect_images(_g(record, "embed")))
|
||||||
|
|
||||||
|
descriptions = []
|
||||||
|
for idx, img in enumerate(images, start=1):
|
||||||
|
alt = _g(img, "alt", "") or ""
|
||||||
|
if alt:
|
||||||
|
descriptions.append(_("Image {index}: {alt}").format(index=idx, alt=alt))
|
||||||
|
return "\n".join(descriptions)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_date(raw_date: str | None, offset_hours: int = 0) -> str:
|
||||||
|
if not raw_date:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
ts = arrow.get(raw_date)
|
||||||
|
if offset_hours:
|
||||||
|
ts = ts.shift(hours=offset_hours)
|
||||||
|
return ts.format(_("dddd, MMMM D, YYYY H:m"), locale=languageHandler.curLang[:2])
|
||||||
|
except Exception:
|
||||||
|
return str(raw_date)[:16].replace("T", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_post_view_data(session: Any, item: Any) -> dict[str, Any] | None:
|
||||||
|
post = _g(item, "post", item)
|
||||||
|
record = _g(post, "record") or _g(item, "record")
|
||||||
|
if record is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
author = _g(post, "author") or _g(item, "author") or {}
|
||||||
|
handle = _g(author, "handle", "")
|
||||||
|
display_name = _g(author, "displayName") or _g(author, "display_name") or handle or _("Unknown")
|
||||||
|
if handle and display_name != handle:
|
||||||
|
author_label = f"{display_name} (@{handle})"
|
||||||
|
elif handle:
|
||||||
|
author_label = f"@{handle}"
|
||||||
|
else:
|
||||||
|
author_label = display_name
|
||||||
|
|
||||||
|
text = _g(record, "text", "") or ""
|
||||||
|
cw_text = _extract_cw_text(post, record)
|
||||||
|
if cw_text:
|
||||||
|
text = f"CW: {cw_text}\n\n{text}" if text else f"CW: {cw_text}"
|
||||||
|
|
||||||
|
created_at = _g(record, "createdAt") or _g(record, "created_at")
|
||||||
|
indexed_at = _g(post, "indexedAt") or _g(post, "indexed_at")
|
||||||
|
date = _format_date(created_at or indexed_at, offset_hours=_g(session.db, "utc_offset", 0))
|
||||||
|
|
||||||
|
reply_count = _g(post, "replyCount", 0) or 0
|
||||||
|
repost_count = _g(post, "repostCount", 0) or 0
|
||||||
|
like_count = _g(post, "likeCount", 0) or 0
|
||||||
|
|
||||||
|
uri = _g(post, "uri") or _g(item, "uri")
|
||||||
|
item_url = ""
|
||||||
|
if uri and handle:
|
||||||
|
rkey = uri.split("/")[-1]
|
||||||
|
item_url = f"https://bsky.app/profile/{handle}/post/{rkey}"
|
||||||
|
|
||||||
|
image_description = _extract_image_descriptions(post, record)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"author": author_label,
|
||||||
|
"text": text,
|
||||||
|
"date": date,
|
||||||
|
"replies": reply_count,
|
||||||
|
"reposts": repost_count,
|
||||||
|
"likes": like_count,
|
||||||
|
"source": _("Bluesky"),
|
||||||
|
"privacy": _("Public"),
|
||||||
|
"image_description": image_description,
|
||||||
|
"item_url": item_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class viewPost(base_messages.basicMessage):
|
||||||
|
def __init__(self, session: Any, item: Any):
|
||||||
|
self.session = session
|
||||||
|
data = _extract_post_view_data(session, item)
|
||||||
|
if not data:
|
||||||
|
output.speak(_("No post available to view."), True)
|
||||||
|
return
|
||||||
|
title = _("Post from {}").format(data["author"])
|
||||||
|
self.message = postDialogs.viewPost(
|
||||||
|
text=data["text"],
|
||||||
|
reposts_count=data["reposts"],
|
||||||
|
likes_count=data["likes"],
|
||||||
|
source=data["source"],
|
||||||
|
date=data["date"],
|
||||||
|
privacy=data["privacy"],
|
||||||
|
)
|
||||||
|
self.message.SetTitle(title)
|
||||||
|
if data["image_description"]:
|
||||||
|
self.message.image_description.Enable(True)
|
||||||
|
self.message.image_description.ChangeValue(data["image_description"])
|
||||||
|
widgetUtils.connect_event(self.message.spellcheck, widgetUtils.BUTTON_PRESSED, self.spellcheck)
|
||||||
|
widgetUtils.connect_event(self.message.translateButton, widgetUtils.BUTTON_PRESSED, self.translate)
|
||||||
|
if data["item_url"]:
|
||||||
|
self.message.enable_button("share")
|
||||||
|
self.item_url = data["item_url"]
|
||||||
|
widgetUtils.connect_event(self.message.share, widgetUtils.BUTTON_PRESSED, self.share)
|
||||||
|
self.message.ShowModal()
|
||||||
|
|
||||||
|
def text_processor(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def share(self, *args, **kwargs):
|
||||||
|
if hasattr(self, "item_url"):
|
||||||
|
output.copy(self.item_url)
|
||||||
|
output.speak(_("Link copied to clipboard."))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import config
|
|||||||
import widgetUtils
|
import widgetUtils
|
||||||
from pubsub import pub
|
from pubsub import pub
|
||||||
from controller.buffers.base import base
|
from controller.buffers.base import base
|
||||||
|
from controller.blueski import messages as blueski_messages
|
||||||
from sessions.blueski import compose
|
from sessions.blueski import compose
|
||||||
from wxUI.buffers.blueski import panels as BlueskiPanels
|
from wxUI.buffers.blueski import panels as BlueskiPanels
|
||||||
|
|
||||||
@@ -257,35 +258,15 @@ class BaseBuffer(base.Buffer):
|
|||||||
def view_item(self, item=None):
|
def view_item(self, item=None):
|
||||||
if item is None:
|
if item is None:
|
||||||
item = self.get_item()
|
item = self.get_item()
|
||||||
if not item: return
|
if not item:
|
||||||
|
return
|
||||||
import wx
|
if not blueski_messages.has_post_data(item):
|
||||||
|
pub.sendMessage("execute-action", action="user_details")
|
||||||
def g(obj, key, default=None):
|
return
|
||||||
if isinstance(obj, dict):
|
try:
|
||||||
return obj.get(key, default)
|
blueski_messages.viewPost(self.session, item)
|
||||||
return getattr(obj, key, default)
|
except Exception:
|
||||||
|
log.exception("Error opening Bluesky post viewer")
|
||||||
# Handle simplified objects vs full feed items
|
|
||||||
post = g(item, "post", item)
|
|
||||||
author = g(post, "author")
|
|
||||||
record = g(post, "record")
|
|
||||||
|
|
||||||
handle = g(author, "handle", "Unknown")
|
|
||||||
display_name = g(author, "displayName", handle)
|
|
||||||
text = g(record, "text", "")
|
|
||||||
created_at = g(record, "createdAt", "")
|
|
||||||
|
|
||||||
# Stats
|
|
||||||
reply_count = g(post, "replyCount", 0)
|
|
||||||
repost_count = g(post, "repostCount", 0)
|
|
||||||
like_count = g(post, "likeCount", 0)
|
|
||||||
|
|
||||||
content = f"{display_name} (@{handle})\n{created_at}\n\n{text}\n\n💬 {reply_count} 🔁 {repost_count} ♥ {like_count}"
|
|
||||||
|
|
||||||
dlg = wx.MessageDialog(self.buffer, content, _("View Post"), wx.OK | wx.ICON_INFORMATION)
|
|
||||||
dlg.ShowModal()
|
|
||||||
dlg.Destroy()
|
|
||||||
|
|
||||||
def url_(self, *args, **kwargs):
|
def url_(self, *args, **kwargs):
|
||||||
self.url()
|
self.url()
|
||||||
|
|||||||
@@ -112,3 +112,94 @@ class Post(wx.Dialog):
|
|||||||
})
|
})
|
||||||
return text, files, cw_text, (lang and [lang] or [])
|
return text, files, cw_text, (lang and [lang] or [])
|
||||||
|
|
||||||
|
|
||||||
|
class viewPost(wx.Dialog):
|
||||||
|
def set_title(self, length):
|
||||||
|
self.SetTitle(_("Post - %i characters ") % length)
|
||||||
|
|
||||||
|
def __init__(self, text="", reposts_count=0, likes_count=0, source="", date="", privacy="", *args, **kwargs):
|
||||||
|
super(viewPost, self).__init__(parent=None, id=wx.ID_ANY, size=(850, 850))
|
||||||
|
self.init_ui(text, reposts_count, likes_count, source, date, privacy)
|
||||||
|
|
||||||
|
def init_ui(self, text, reposts_count, likes_count, source, date, privacy):
|
||||||
|
panel = wx.Panel(self)
|
||||||
|
main_sizer = wx.BoxSizer(wx.VERTICAL)
|
||||||
|
main_sizer.Add(self.create_text_section(panel, text), 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
main_sizer.Add(self.create_image_description_section(panel), 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
main_sizer.Add(self.create_info_section(panel, privacy, reposts_count, likes_count, source, date), 0, wx.EXPAND | wx.ALL, 5)
|
||||||
|
main_sizer.Add(self.create_buttons_section(panel), 0, wx.ALIGN_RIGHT | wx.ALL, 5)
|
||||||
|
panel.SetSizer(main_sizer)
|
||||||
|
self.SetClientSize(main_sizer.CalcMin())
|
||||||
|
|
||||||
|
def create_text_section(self, panel, text):
|
||||||
|
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Post")), wx.VERTICAL)
|
||||||
|
self.text = wx.TextCtrl(panel, -1, text, style=wx.TE_READONLY | wx.TE_MULTILINE)
|
||||||
|
sizer.Add(self.text, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
return sizer
|
||||||
|
|
||||||
|
def create_image_description_section(self, panel):
|
||||||
|
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Image description")), wx.VERTICAL)
|
||||||
|
self.image_description = wx.TextCtrl(panel, -1, style=wx.TE_READONLY | wx.TE_MULTILINE)
|
||||||
|
self.image_description.Enable(False)
|
||||||
|
sizer.Add(self.image_description, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
return sizer
|
||||||
|
|
||||||
|
def create_info_section(self, panel, privacy, reposts_count, likes_count, source, date):
|
||||||
|
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Information")), wx.VERTICAL)
|
||||||
|
flex_sizer = wx.FlexGridSizer(cols=3, hgap=10, vgap=10)
|
||||||
|
flex_sizer.AddGrowableCol(1)
|
||||||
|
flex_sizer.Add(wx.StaticText(panel, -1, _("Privacy")), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
flex_sizer.Add(wx.TextCtrl(panel, -1, privacy, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
|
||||||
|
flex_sizer.Add(self.create_reposts_section(panel, reposts_count), 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
flex_sizer.Add(self.create_likes_section(panel, likes_count), 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
flex_sizer.Add(wx.StaticText(panel, -1, _("Source")), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
flex_sizer.Add(wx.TextCtrl(panel, -1, source, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
|
||||||
|
flex_sizer.Add(wx.StaticText(panel, -1, _("Date")), 0, wx.ALIGN_CENTER_VERTICAL)
|
||||||
|
flex_sizer.Add(wx.TextCtrl(panel, -1, date, style=wx.TE_READONLY | wx.TE_MULTILINE), 1, wx.EXPAND)
|
||||||
|
sizer.Add(flex_sizer, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
return sizer
|
||||||
|
|
||||||
|
def create_reposts_section(self, panel, reposts_count):
|
||||||
|
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Reposts")), wx.VERTICAL)
|
||||||
|
self.reposts_button = wx.Button(panel, -1, str(reposts_count))
|
||||||
|
self.reposts_button.Enable(False)
|
||||||
|
sizer.Add(self.reposts_button, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
return sizer
|
||||||
|
|
||||||
|
def create_likes_section(self, panel, likes_count):
|
||||||
|
sizer = wx.StaticBoxSizer(wx.StaticBox(panel, wx.ID_ANY, _("Likes")), wx.VERTICAL)
|
||||||
|
self.likes_button = wx.Button(panel, -1, str(likes_count))
|
||||||
|
self.likes_button.Enable(False)
|
||||||
|
sizer.Add(self.likes_button, 1, wx.EXPAND | wx.ALL, 5)
|
||||||
|
return sizer
|
||||||
|
|
||||||
|
def create_buttons_section(self, panel):
|
||||||
|
sizer = wx.BoxSizer(wx.HORIZONTAL)
|
||||||
|
self.share = wx.Button(panel, wx.ID_ANY, _("&Copy link to clipboard"))
|
||||||
|
self.share.Enable(False)
|
||||||
|
self.spellcheck = wx.Button(panel, wx.ID_ANY, _("Check &spelling..."))
|
||||||
|
self.translateButton = wx.Button(panel, wx.ID_ANY, _("&Translate..."))
|
||||||
|
cancelButton = wx.Button(panel, wx.ID_CANCEL, _("C&lose"))
|
||||||
|
cancelButton.SetDefault()
|
||||||
|
sizer.Add(self.share, 0, wx.ALL, 5)
|
||||||
|
sizer.Add(self.spellcheck, 0, wx.ALL, 5)
|
||||||
|
sizer.Add(self.translateButton, 0, wx.ALL, 5)
|
||||||
|
sizer.Add(cancelButton, 0, wx.ALL, 5)
|
||||||
|
return sizer
|
||||||
|
|
||||||
|
def set_text(self, text):
|
||||||
|
self.text.ChangeValue(text)
|
||||||
|
|
||||||
|
def get_text(self):
|
||||||
|
return self.text.GetValue()
|
||||||
|
|
||||||
|
def text_focus(self):
|
||||||
|
self.text.SetFocus()
|
||||||
|
|
||||||
|
def onSelect(self, ev):
|
||||||
|
self.text.SelectAll()
|
||||||
|
|
||||||
|
def enable_button(self, buttonName):
|
||||||
|
if hasattr(self, buttonName):
|
||||||
|
return getattr(self, buttonName).Enable()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user