Add moderation TUI
This ended up way fancier than I imagined.
This commit is contained in:
parent
dcea8bffe1
commit
eebd5d8c6d
9 changed files with 681 additions and 0 deletions
3
modui/__init__.py
Normal file
3
modui/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .filetable import FileTable
|
||||
from .notification import Notification
|
||||
from .mpvwidget import MpvWidget
|
72
modui/filetable.py
Normal file
72
modui/filetable.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from textual.widgets import DataTable, Static
|
||||
from textual.reactive import Reactive
|
||||
from textual.message import Message, MessageTarget
|
||||
from textual import events, log
|
||||
from jinja2.filters import do_filesizeformat
|
||||
|
||||
from fhost import File
|
||||
from modui import mime
|
||||
|
||||
class FileTable(DataTable):
|
||||
query = Reactive(None)
|
||||
order_col = Reactive(0)
|
||||
order_desc = Reactive(True)
|
||||
limit = 10000
|
||||
colmap = [File.id, File.removed, File.nsfw_score, None, File.ext, File.size, File.mime]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.add_columns("#", "☣️", "🔞", "📂", "name", "size", "mime")
|
||||
self.base_query = File.query.filter(File.size != None)
|
||||
self.query = self.base_query
|
||||
|
||||
class Selected(Message):
|
||||
def __init__(self, sender: MessageTarget, f: File) -> None:
|
||||
self.file = f
|
||||
super().__init__(sender)
|
||||
|
||||
def watch_order_col(self, old, value) -> None:
|
||||
self.watch_query(None, None)
|
||||
|
||||
def watch_order_desc(self, old, value) -> None:
|
||||
self.watch_query(None, None)
|
||||
|
||||
def watch_query(self, old, value) -> None:
|
||||
def fmt_file(f: File) -> tuple:
|
||||
return (
|
||||
str(f.id),
|
||||
"🔴" if f.removed else " ",
|
||||
"🚩" if f.is_nsfw else " ",
|
||||
"👻" if not f.getpath().is_file() else " ",
|
||||
f.getname(),
|
||||
do_filesizeformat(f.size, True),
|
||||
f"{mime.mimemoji.get(f.mime.split('/')[0], mime.mimemoji.get(f.mime)) or ' '} " + f.mime,
|
||||
)
|
||||
|
||||
if (self.query):
|
||||
self.clear()
|
||||
order = FileTable.colmap[self.order_col]
|
||||
q = self.query
|
||||
if order: q = q.order_by(order.desc() if self.order_desc else order, File.id)
|
||||
self.add_rows(map(fmt_file, q.limit(self.limit)))
|
||||
|
||||
def _scroll_cursor_in_to_view(self, animate: bool = False) -> None:
|
||||
region = self._get_cell_region(self.cursor_row, 0)
|
||||
spacing = self._get_cell_border()
|
||||
self.scroll_to_region(region, animate=animate, spacing=spacing)
|
||||
|
||||
async def watch_cursor_cell(self, old, value) -> None:
|
||||
super().watch_cursor_cell(old, value)
|
||||
if value[0] < len(self.data) and value[0] >= 0:
|
||||
f = File.query.get(int(self.data[value[0]][0]))
|
||||
await self.emit(self.Selected(self, f))
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
super().on_click(event)
|
||||
meta = self.get_style_at(event.x, event.y).meta
|
||||
if meta:
|
||||
if meta["row"] == -1:
|
||||
qi = FileTable.colmap[meta["column"]]
|
||||
if meta["column"] == self.order_col:
|
||||
self.order_desc = not self.order_desc
|
||||
self.order_col = meta["column"]
|
122
modui/mime.py
Normal file
122
modui/mime.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from enum import Enum
|
||||
from textual import log
|
||||
|
||||
mimemoji = {
|
||||
"audio" : "🔈",
|
||||
"video" : "🎞",
|
||||
"text" : "📄",
|
||||
"image" : "🖼",
|
||||
"application/zip" : "🗜️",
|
||||
"application/x-zip-compressed" : "🗜️",
|
||||
"application/x-tar" : "🗄",
|
||||
"application/x-cpio" : "🗄",
|
||||
"application/x-xz" : "🗜️",
|
||||
"application/x-7z-compressed" : "🗜️",
|
||||
"application/gzip" : "🗜️",
|
||||
"application/zstd" : "🗜️",
|
||||
"application/x-rar" : "🗜️",
|
||||
"application/x-rar-compressed" : "🗜️",
|
||||
"application/vnd.ms-cab-compressed" : "🗜️",
|
||||
"application/x-bzip2" : "🗜️",
|
||||
"application/x-lzip" : "🗜️",
|
||||
"application/x-iso9660-image" : "💿",
|
||||
"application/pdf" : "📕",
|
||||
"application/epub+zip" : "📕",
|
||||
"application/mxf" : "🎞",
|
||||
"application/vnd.android.package-archive" : "📦",
|
||||
"application/vnd.debian.binary-package" : "📦",
|
||||
"application/x-rpm" : "📦",
|
||||
"application/x-dosexec" : "⚙",
|
||||
"application/x-execuftable" : "⚙",
|
||||
"application/x-sharedlib" : "⚙",
|
||||
"application/java-archive" : "☕",
|
||||
"application/x-qemu-disk" : "🖴",
|
||||
"application/pgp-encrypted" : "🔏",
|
||||
}
|
||||
|
||||
MIMECategory = Enum("MIMECategory",
|
||||
["Archive", "Text", "AV", "Document", "Fallback"]
|
||||
)
|
||||
|
||||
class MIMEHandler:
|
||||
def __init__(self):
|
||||
self.handlers = {
|
||||
MIMECategory.Archive : [[
|
||||
"application/zip",
|
||||
"application/x-zip-compressed",
|
||||
"application/x-tar",
|
||||
"application/x-cpio",
|
||||
"application/x-xz",
|
||||
"application/x-7z-compressed",
|
||||
"application/gzip",
|
||||
"application/zstd",
|
||||
"application/x-rar",
|
||||
"application/x-rar-compressed",
|
||||
"application/vnd.ms-cab-compressed",
|
||||
"application/x-bzip2",
|
||||
"application/x-lzip",
|
||||
"application/x-iso9660-image",
|
||||
"application/vnd.android.package-archive",
|
||||
"application/vnd.debian.binary-package",
|
||||
"application/x-rpm",
|
||||
"application/java-archive",
|
||||
"application/vnd.openxmlformats"
|
||||
], []],
|
||||
MIMECategory.Text : [["text"], []],
|
||||
MIMECategory.AV : [[
|
||||
"audio", "video", "image",
|
||||
"application/mxf"
|
||||
], []],
|
||||
MIMECategory.Document : [[
|
||||
"application/pdf",
|
||||
"application/epub",
|
||||
"application/x-mobipocket-ebook",
|
||||
], []],
|
||||
MIMECategory.Fallback : [[], []]
|
||||
}
|
||||
|
||||
self.exceptions = {
|
||||
MIMECategory.Archive : {
|
||||
".cbz" : MIMECategory.Document,
|
||||
".xps" : MIMECategory.Document,
|
||||
".epub" : MIMECategory.Document,
|
||||
},
|
||||
MIMECategory.Text : {
|
||||
".fb2" : MIMECategory.Document,
|
||||
}
|
||||
}
|
||||
|
||||
def register(self, category, handler):
|
||||
self.handlers[category][1].append(handler)
|
||||
|
||||
def handle(self, mime, ext):
|
||||
def getcat(s):
|
||||
cat = MIMECategory.Fallback
|
||||
for k, v in self.handlers.items():
|
||||
s = s.split(";")[0]
|
||||
if s in v[0] or s.split("/")[0] in v[0]:
|
||||
cat = k
|
||||
break
|
||||
|
||||
for x in v[0]:
|
||||
if s.startswith(x):
|
||||
cat = k
|
||||
break
|
||||
|
||||
if cat in self.exceptions:
|
||||
cat = self.exceptions[cat].get(ext) or cat
|
||||
|
||||
return cat
|
||||
|
||||
cat = getcat(mime)
|
||||
for handler in self.handlers[cat][1]:
|
||||
try:
|
||||
if handler(cat): return
|
||||
except: pass
|
||||
|
||||
for handler in self.handlers[MIMECategory.Fallback][1]:
|
||||
try:
|
||||
if handler(None): return
|
||||
except: pass
|
||||
|
||||
raise RuntimeError(f"Unhandled MIME type category: {cat}")
|
88
modui/mpvwidget.py
Normal file
88
modui/mpvwidget.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
import time
|
||||
import fcntl, struct, termios
|
||||
from sys import stdout
|
||||
|
||||
from textual import events, log
|
||||
from textual.widgets import Static
|
||||
|
||||
from fhost import app as fhost_app
|
||||
|
||||
class MpvWidget(Static):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.mpv = None
|
||||
self.vo = fhost_app.config.get("MOD_PREVIEW_PROTO")
|
||||
|
||||
if not self.vo in ["sixel", "kitty"]:
|
||||
self.update("⚠ Previews not enabled. \n\nSet MOD_PREVIEW_PROTO to 'sixel' or 'kitty' in config.py,\nwhichever is supported by your terminal.")
|
||||
else:
|
||||
try:
|
||||
import mpv
|
||||
self.mpv = mpv.MPV()
|
||||
self.mpv.profile = "sw-fast"
|
||||
self.mpv["vo"] = self.vo
|
||||
self.mpv[f"vo-{self.vo}-config-clear"] = False
|
||||
self.mpv[f"vo-{self.vo}-alt-screen"] = False
|
||||
self.mpv[f"vo-sixel-buffered"] = True
|
||||
self.mpv["audio"] = False
|
||||
self.mpv["loop-file"] = "inf"
|
||||
self.mpv["image-display-duration"] = 0.5 if self.vo == "sixel" else "inf"
|
||||
except Exception as e:
|
||||
self.mpv = None
|
||||
self.update(f"⚠ Previews require python-mpv with libmpv 0.36.0 or later \n\nError was:\n{type(e).__name__}: {e}")
|
||||
|
||||
def start_mpv(self, f: str|None = None, pos: float|str|None = None) -> None:
|
||||
self.display = True
|
||||
self.screen._refresh_layout()
|
||||
|
||||
if self.mpv:
|
||||
if self.content_region.x:
|
||||
r, c, w, h = struct.unpack('hhhh', fcntl.ioctl(0, termios.TIOCGWINSZ, '12345678'))
|
||||
width = int((w / c) * self.content_region.width)
|
||||
height = int((h / r) * (self.content_region.height + (1 if self.vo == "sixel" else 0)))
|
||||
self.mpv[f"vo-{self.vo}-left"] = self.content_region.x + 1
|
||||
self.mpv[f"vo-{self.vo}-top"] = self.content_region.y + 1
|
||||
self.mpv[f"vo-{self.vo}-rows"] = self.content_region.height + (1 if self.vo == "sixel" else 0)
|
||||
self.mpv[f"vo-{self.vo}-cols"] = self.content_region.width
|
||||
self.mpv[f"vo-{self.vo}-width"] = width
|
||||
self.mpv[f"vo-{self.vo}-height"] = height
|
||||
|
||||
if pos != None:
|
||||
self.mpv["start"] = pos
|
||||
|
||||
if f:
|
||||
self.mpv.loadfile(f)
|
||||
else:
|
||||
self.mpv.playlist_play_index(0)
|
||||
|
||||
def stop_mpv(self, wait: bool = False) -> None:
|
||||
if self.mpv:
|
||||
if not self.mpv.idle_active:
|
||||
self.mpv.stop(True)
|
||||
if wait:
|
||||
time.sleep(0.1)
|
||||
self.clear_mpv()
|
||||
self.display = False
|
||||
|
||||
def on_resize(self, size) -> None:
|
||||
if self.mpv:
|
||||
if not self.mpv.idle_active:
|
||||
t = self.mpv.time_pos
|
||||
self.stop_mpv()
|
||||
if t:
|
||||
self.mpv["start"] = t
|
||||
self.start_mpv()
|
||||
|
||||
def clear_mpv(self) -> None:
|
||||
if self.vo == "kitty":
|
||||
stdout.write("\033_Ga=d;\033\\")
|
||||
stdout.flush()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if self.mpv:
|
||||
self.mpv.stop()
|
||||
del self.mpv
|
||||
if self.vo == "kitty":
|
||||
stdout.write("\033_Ga=d;\033\\\033[?25l")
|
||||
stdout.flush()
|
8
modui/notification.py
Normal file
8
modui/notification.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from textual.widgets import Static
|
||||
|
||||
class Notification(Static):
|
||||
def on_mount(self) -> None:
|
||||
self.set_timer(3, self.remove)
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.remove()
|
Loading…
Add table
Add a link
Reference in a new issue