mirror of
https://github.com/ihabunek/toot.git
synced 2024-11-03 04:17:21 -05:00
commit
a633f757b5
@ -1,4 +1,5 @@
|
||||
import re
|
||||
from typing import List
|
||||
import uuid
|
||||
|
||||
from urllib.parse import urlparse, urlencode, quote
|
||||
@ -362,6 +363,12 @@ def whois(app, user, account):
|
||||
return http.get(app, user, f'/api/v1/accounts/{account}').json()
|
||||
|
||||
|
||||
def vote(app, user, poll_id, choices: List[int]):
|
||||
url = f"/api/v1/polls/{poll_id}/votes"
|
||||
json = {'choices': choices}
|
||||
return http.post(app, user, url, json=json).json()
|
||||
|
||||
|
||||
def mute(app, user, account):
|
||||
return _account_action(app, user, account, 'mute')
|
||||
|
||||
|
@ -12,6 +12,7 @@ from .constants import PALETTE
|
||||
from .entities import Status
|
||||
from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource, StatusLinks, StatusZoom
|
||||
from .overlays import StatusDeleteConfirmation, Account
|
||||
from .poll import Poll
|
||||
from .timeline import Timeline
|
||||
from .utils import parse_content_links, show_media
|
||||
|
||||
@ -155,7 +156,7 @@ class TUI(urwid.Frame):
|
||||
|
||||
def _default_error_callback(ex):
|
||||
self.exception = ex
|
||||
self.footer.set_error_message("An exception occurred, press E to view")
|
||||
self.footer.set_error_message("An exception occurred, press X to view")
|
||||
|
||||
_error_callback = error_callback or _default_error_callback
|
||||
|
||||
@ -200,6 +201,9 @@ class TUI(urwid.Frame):
|
||||
def _menu(timeline, status):
|
||||
self.show_context_menu(status)
|
||||
|
||||
def _poll(timeline, status):
|
||||
self.show_poll(status)
|
||||
|
||||
def _zoom(timeline, status_details):
|
||||
self.show_status_zoom(status_details)
|
||||
|
||||
@ -214,6 +218,7 @@ class TUI(urwid.Frame):
|
||||
urwid.connect_signal(timeline, "focus", self.refresh_footer)
|
||||
urwid.connect_signal(timeline, "media", _media)
|
||||
urwid.connect_signal(timeline, "menu", _menu)
|
||||
urwid.connect_signal(timeline, "poll", _poll)
|
||||
urwid.connect_signal(timeline, "reblog", self.async_toggle_reblog)
|
||||
urwid.connect_signal(timeline, "reply", _reply)
|
||||
urwid.connect_signal(timeline, "source", _source)
|
||||
@ -445,6 +450,12 @@ class TUI(urwid.Frame):
|
||||
def show_help(self):
|
||||
self.open_overlay(Help(), title="Help")
|
||||
|
||||
def show_poll(self, status):
|
||||
self.open_overlay(
|
||||
widget=Poll(self.app, self.user, status),
|
||||
title="Poll",
|
||||
)
|
||||
|
||||
def goto_home_timeline(self):
|
||||
self.timeline_generator = api.home_timeline_generator(
|
||||
self.app, self.user, limit=40)
|
||||
@ -651,12 +662,14 @@ class TUI(urwid.Frame):
|
||||
def close_overlay(self):
|
||||
self.body = self.overlay.bottom_w
|
||||
self.overlay = None
|
||||
if self.timeline:
|
||||
self.timeline.refresh_status_details()
|
||||
|
||||
# --- Keys -----------------------------------------------------------------
|
||||
|
||||
def unhandled_input(self, key):
|
||||
# TODO: this should not be in unhandled input
|
||||
if key in ('e', 'E'):
|
||||
if key in ('x', 'X'):
|
||||
if self.exception:
|
||||
self.show_exception(self.exception)
|
||||
|
||||
|
@ -211,7 +211,7 @@ class Account(urwid.ListBox):
|
||||
super().__init__(walker)
|
||||
|
||||
def generate_contents(self, account):
|
||||
yield urwid.Text([('green', f"@{account['acct']}"), (f" {account['display_name']}")])
|
||||
yield urwid.Text([('green', f"@{account['acct']}"), f" {account['display_name']}"])
|
||||
|
||||
if account["note"]:
|
||||
yield urwid.Divider()
|
||||
@ -219,8 +219,8 @@ class Account(urwid.ListBox):
|
||||
yield urwid.Text(highlight_hashtags(line, followed_tags=set()))
|
||||
|
||||
yield urwid.Divider()
|
||||
yield urwid.Text([("ID: "), ("green", f"{account['id']}")])
|
||||
yield urwid.Text([("Since: "), ("green", f"{account['created_at'][:10]}")])
|
||||
yield urwid.Text(["ID: ", ("green", f"{account['id']}")])
|
||||
yield urwid.Text(["Since: ", ("green", f"{account['created_at'][:10]}")])
|
||||
yield urwid.Divider()
|
||||
|
||||
if account["bot"]:
|
||||
@ -233,15 +233,15 @@ class Account(urwid.ListBox):
|
||||
yield urwid.Text([("warning", "Suspended \N{cross mark}")])
|
||||
yield urwid.Divider()
|
||||
|
||||
yield urwid.Text([("Followers: "), ("yellow", f"{account['followers_count']}")])
|
||||
yield urwid.Text([("Following: "), ("yellow", f"{account['following_count']}")])
|
||||
yield urwid.Text([("Statuses: "), ("yellow", f"{account['statuses_count']}")])
|
||||
yield urwid.Text(["Followers: ", ("yellow", f"{account['followers_count']}")])
|
||||
yield urwid.Text(["Following: ", ("yellow", f"{account['following_count']}")])
|
||||
yield urwid.Text(["Statuses: ", ("yellow", f"{account['statuses_count']}")])
|
||||
|
||||
if account["fields"]:
|
||||
for field in account["fields"]:
|
||||
name = field["name"].title()
|
||||
yield urwid.Divider()
|
||||
yield urwid.Text([("yellow", f"{name.rstrip(':')}"), (":")])
|
||||
yield urwid.Text([("yellow", f"{name.rstrip(':')}"), ":"])
|
||||
for line in format_content(field["value"]):
|
||||
yield urwid.Text(highlight_hashtags(line, followed_tags=set()))
|
||||
if field["verified_at"]:
|
||||
|
102
toot/tui/poll.py
Normal file
102
toot/tui/poll.py
Normal file
@ -0,0 +1,102 @@
|
||||
import urwid
|
||||
|
||||
from toot import api
|
||||
from toot.exceptions import ApiError
|
||||
from toot.utils import format_content
|
||||
from .utils import highlight_hashtags, parse_datetime
|
||||
from .widgets import Button, CheckBox, RadioButton
|
||||
|
||||
|
||||
class Poll(urwid.ListBox):
|
||||
"""View and vote on a poll"""
|
||||
|
||||
def __init__(self, app, user, status):
|
||||
self.status = status
|
||||
self.app = app
|
||||
self.user = user
|
||||
self.poll = status.original.data.get("poll")
|
||||
self.button_group = []
|
||||
self.api_exception = None
|
||||
self.setup_listbox()
|
||||
|
||||
def setup_listbox(self):
|
||||
actions = list(self.generate_contents(self.status))
|
||||
walker = urwid.SimpleListWalker(actions)
|
||||
super().__init__(walker)
|
||||
|
||||
def build_linebox(self, contents):
|
||||
contents = urwid.Pile(list(contents))
|
||||
contents = urwid.Padding(contents, left=1, right=1)
|
||||
return urwid.LineBox(contents)
|
||||
|
||||
def vote(self, button_widget):
|
||||
poll = self.status.original.data.get("poll")
|
||||
choices = []
|
||||
for idx, button in enumerate(self.button_group):
|
||||
if button.get_state():
|
||||
choices.append(idx)
|
||||
|
||||
if len(choices):
|
||||
try:
|
||||
response = api.vote(self.app, self.user, poll["id"], choices=choices)
|
||||
self.status.original.data["poll"] = response
|
||||
self.api_exception = None
|
||||
self.poll["voted"] = True
|
||||
self.poll["own_votes"] = choices
|
||||
except ApiError as exception:
|
||||
self.api_exception = exception
|
||||
finally:
|
||||
self.setup_listbox()
|
||||
|
||||
def generate_poll_detail(self):
|
||||
poll = self.poll
|
||||
|
||||
self.button_group = [] # button group
|
||||
for idx, option in enumerate(poll["options"]):
|
||||
voted_for = (
|
||||
poll["voted"] and poll["own_votes"] and idx in poll["own_votes"]
|
||||
)
|
||||
|
||||
if poll["voted"] or poll["expired"]:
|
||||
prefix = " ✓ " if voted_for else " "
|
||||
yield urwid.Text(("gray", prefix + f'{option["title"]}'))
|
||||
else:
|
||||
if poll["multiple"]:
|
||||
checkbox = CheckBox(f'{option["title"]}')
|
||||
self.button_group.append(checkbox)
|
||||
yield checkbox
|
||||
else:
|
||||
yield RadioButton(self.button_group, f'{option["title"]}')
|
||||
|
||||
yield urwid.Divider()
|
||||
|
||||
poll_detail = "Poll · {} votes".format(poll["votes_count"])
|
||||
|
||||
if poll["expired"]:
|
||||
poll_detail += " · Closed"
|
||||
|
||||
if poll["expires_at"]:
|
||||
expires_at = parse_datetime(poll["expires_at"]).strftime(
|
||||
"%Y-%m-%d %H:%M"
|
||||
)
|
||||
poll_detail += " · Closes on {}".format(expires_at)
|
||||
|
||||
yield urwid.Text(("gray", poll_detail))
|
||||
|
||||
def generate_contents(self, status):
|
||||
yield urwid.Divider()
|
||||
for line in format_content(status.data["content"]):
|
||||
yield urwid.Text(highlight_hashtags(line, set()))
|
||||
|
||||
yield urwid.Divider()
|
||||
yield self.build_linebox(self.generate_poll_detail())
|
||||
yield urwid.Divider()
|
||||
|
||||
if self.poll["voted"]:
|
||||
yield urwid.Text(("grey", "< Already Voted >"))
|
||||
elif not self.poll["expired"]:
|
||||
yield Button("Vote", on_press=self.vote)
|
||||
|
||||
if self.api_exception:
|
||||
yield urwid.Divider()
|
||||
yield urwid.Text("warning", str(self.api_exception))
|
@ -31,6 +31,7 @@ class Timeline(urwid.Columns):
|
||||
"media", # Display media attachments
|
||||
"menu", # Show a context menu
|
||||
"next", # Fetch more statuses
|
||||
"poll", # Vote in a poll
|
||||
"reblog", # Reblog status
|
||||
"reply", # Compose a reply to a status
|
||||
"source", # Show status source
|
||||
@ -65,11 +66,12 @@ class Timeline(urwid.Columns):
|
||||
])
|
||||
|
||||
def wrap_status_details(self, status_details: "StatusDetails") -> urwid.Widget:
|
||||
"""Wrap StatusDetails widget with a scollbar and footer."""
|
||||
"""Wrap StatusDetails widget with a scrollbar and footer."""
|
||||
self.status_detail_scrollable = Scrollable(urwid.Padding(status_details, right=1))
|
||||
return urwid.Padding(
|
||||
urwid.Frame(
|
||||
body=ScrollBar(
|
||||
Scrollable(urwid.Padding(status_details, right=1)),
|
||||
self.status_detail_scrollable,
|
||||
thumb_char="\u2588",
|
||||
trough_char="\u2591",
|
||||
),
|
||||
@ -102,6 +104,8 @@ class Timeline(urwid.Columns):
|
||||
if not status:
|
||||
return None
|
||||
|
||||
poll = status.original.data.get("poll")
|
||||
|
||||
options = [
|
||||
"[A]ccount" if not status.is_mine else "",
|
||||
"[B]oost",
|
||||
@ -112,6 +116,7 @@ class Timeline(urwid.Columns):
|
||||
"[T]hread" if not self.is_thread else "",
|
||||
"[L]inks",
|
||||
"[R]eply",
|
||||
"[P]oll" if poll and not poll["expired"] else "",
|
||||
"So[u]rce",
|
||||
"[Z]oom",
|
||||
"Tra[n]slate" if self.can_translate else "",
|
||||
@ -148,7 +153,9 @@ class Timeline(urwid.Columns):
|
||||
def refresh_status_details(self):
|
||||
"""Redraws the details of the focused status."""
|
||||
status = self.get_focused_status()
|
||||
pos = self.status_detail_scrollable.get_scrollpos()
|
||||
self.draw_status_details(status)
|
||||
self.status_detail_scrollable.set_scrollpos(pos)
|
||||
|
||||
def draw_status_details(self, status):
|
||||
self.status_details = StatusDetails(self, status)
|
||||
@ -240,7 +247,7 @@ class Timeline(urwid.Columns):
|
||||
self._emit("clear-screen")
|
||||
return
|
||||
|
||||
if key in ("p", "P"):
|
||||
if key in ("e", "E"):
|
||||
self._emit("save", status)
|
||||
return
|
||||
|
||||
@ -248,6 +255,12 @@ class Timeline(urwid.Columns):
|
||||
self._emit("zoom", self.status_details)
|
||||
return
|
||||
|
||||
if key in ("p", "P"):
|
||||
poll = status.original.data.get("poll")
|
||||
if poll and not poll["expired"]:
|
||||
self._emit("poll", status)
|
||||
return
|
||||
|
||||
return super().keypress(size, key)
|
||||
|
||||
def append_status(self, status):
|
||||
@ -340,7 +353,7 @@ class StatusDetails(urwid.Pile):
|
||||
yield ("pack", urwid.Text(m["description"]))
|
||||
yield ("pack", urwid.Text(("link", m["url"])))
|
||||
|
||||
poll = status.data.get("poll")
|
||||
poll = status.original.data.get("poll")
|
||||
if poll:
|
||||
yield ("pack", urwid.Divider())
|
||||
yield ("pack", self.build_linebox(self.poll_generator(poll)))
|
||||
|
@ -37,19 +37,19 @@ def time_ago(value: datetime) -> datetime:
|
||||
now = datetime.now().astimezone()
|
||||
delta = now.timestamp() - value.timestamp()
|
||||
|
||||
if (delta < 1):
|
||||
if delta < 1:
|
||||
return "now"
|
||||
|
||||
if (delta < 8 * DAY):
|
||||
if (delta < MINUTE):
|
||||
if delta < 8 * DAY:
|
||||
if delta < MINUTE:
|
||||
return f"{math.floor(delta / SECOND)}".rjust(2, " ") + "s"
|
||||
if (delta < HOUR):
|
||||
if delta < HOUR:
|
||||
return f"{math.floor(delta / MINUTE)}".rjust(2, " ") + "m"
|
||||
if (delta < DAY):
|
||||
if delta < DAY:
|
||||
return f"{math.floor(delta / HOUR)}".rjust(2, " ") + "h"
|
||||
return f"{math.floor(delta / DAY)}".rjust(2, " ") + "d"
|
||||
|
||||
if (delta < 53 * WEEK): # not exactly correct but good enough as a boundary
|
||||
if delta < 53 * WEEK: # not exactly correct but good enough as a boundary
|
||||
return f"{math.floor(delta / WEEK)}".rjust(2, " ") + "w"
|
||||
|
||||
return ">1y"
|
||||
|
@ -46,3 +46,23 @@ class Button(urwid.AttrWrap):
|
||||
def set_label(self, *args, **kwargs):
|
||||
self.original_widget.original_widget.set_label(*args, **kwargs)
|
||||
self.original_widget.width = len(args[0]) + 4
|
||||
|
||||
|
||||
class CheckBox(urwid.AttrWrap):
|
||||
"""Styled checkbox."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.button = urwid.CheckBox(*args, **kwargs)
|
||||
padding = urwid.Padding(self.button, width=len(args[0]) + 4)
|
||||
return super().__init__(padding, "button", "button_focused")
|
||||
|
||||
def get_state(self):
|
||||
"""Return the state of the checkbox."""
|
||||
return self.button._state
|
||||
|
||||
|
||||
class RadioButton(urwid.AttrWrap):
|
||||
"""Styled radiobutton."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
button = urwid.RadioButton(*args, **kwargs)
|
||||
padding = urwid.Padding(button, width=len(args[1]) + 4)
|
||||
return super().__init__(padding, "button", "button_focused")
|
||||
|
Loading…
Reference in New Issue
Block a user