2024-09-08 11:43:21 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2024-09-08 14:44:47 +00:00
|
|
|
import math
|
2024-09-08 15:50:38 +00:00
|
|
|
import argparse
|
|
|
|
import json
|
2024-09-08 15:17:55 +00:00
|
|
|
from time import sleep
|
|
|
|
from threading import Thread
|
|
|
|
from queue import Queue, Empty
|
2024-09-08 11:43:21 +00:00
|
|
|
from abc import abstractmethod
|
2024-09-08 14:44:47 +00:00
|
|
|
from typing import Tuple, Callable, TypedDict, List
|
2024-09-08 11:43:21 +00:00
|
|
|
|
|
|
|
import pygame
|
2024-09-08 14:44:47 +00:00
|
|
|
import pygame.freetype
|
|
|
|
|
|
|
|
|
|
|
|
class ButtonDef(TypedDict):
|
|
|
|
text: str
|
2024-09-08 14:50:18 +00:00
|
|
|
cbk: Callable[[], None]
|
2024-09-08 11:43:21 +00:00
|
|
|
|
|
|
|
|
2024-09-08 15:50:38 +00:00
|
|
|
class UrlDef(TypedDict):
|
|
|
|
label: str
|
|
|
|
url: str
|
|
|
|
|
|
|
|
|
2024-09-08 11:43:21 +00:00
|
|
|
class IBoard:
|
|
|
|
@abstractmethod
|
|
|
|
def draw(self, screen: pygame.Surface):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def handle_event(self, event: pygame.event.Event):
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
|
|
class Button:
|
2024-09-08 14:44:47 +00:00
|
|
|
def __init__(self, position: pygame.Rect, text: str, cbk: Callable[[], None]):
|
2024-09-08 11:43:21 +00:00
|
|
|
self.position: pygame.Rect
|
|
|
|
self.set_position(position)
|
2024-09-08 14:44:47 +00:00
|
|
|
self.text = text
|
2024-09-08 11:43:21 +00:00
|
|
|
self.cbk = cbk
|
2024-09-08 14:44:47 +00:00
|
|
|
self.font = pygame.freetype.SysFont(name="Sans", size=20)
|
|
|
|
text_rect = self.font.get_rect(self.text)
|
|
|
|
self.text_pos = pygame.Rect((self.position.left + (self.position.width - text_rect.width)/2),
|
|
|
|
(self.position.top + (self.position.height - text_rect.height)/2),
|
|
|
|
text_rect.width, text_rect.height)
|
2024-09-08 11:43:21 +00:00
|
|
|
|
|
|
|
def set_position(self, position: pygame.Rect):
|
|
|
|
self.position = position
|
|
|
|
|
|
|
|
def draw(self, screen: pygame.Surface):
|
|
|
|
pygame.draw.rect(screen, "black", self.position, 0)
|
2024-09-08 14:44:47 +00:00
|
|
|
self.font.render_to(screen, self.text_pos, self.text, fgcolor="pink")
|
2024-09-08 11:43:21 +00:00
|
|
|
|
|
|
|
def pos_is_inside(self, pos: Tuple) -> bool:
|
|
|
|
return (self.position.left < pos[0] and (self.position.left + self.position.width) > pos[0]) and \
|
|
|
|
(self.position.top < pos[1] and (self.position.top + self.position.height) > pos[1])
|
|
|
|
|
|
|
|
def handle_event(self, event: pygame.event.Event):
|
|
|
|
if event.type == pygame.MOUSEBUTTONUP:
|
|
|
|
if event.button == 1 and self.pos_is_inside(event.pos):
|
|
|
|
self.cbk()
|
|
|
|
|
|
|
|
|
2024-09-08 15:17:55 +00:00
|
|
|
class MessageBoard(IBoard):
|
2024-09-08 16:02:13 +00:00
|
|
|
def __init__(self, screen_rect: pygame.Rect, text: str, bg_color: str):
|
2024-09-08 15:17:55 +00:00
|
|
|
self.text = text
|
|
|
|
self.font = pygame.freetype.SysFont(name="Sans", size=30)
|
|
|
|
text_rect = self.font.get_rect(self.text)
|
|
|
|
self.text_pos = pygame.Rect((screen_rect.left + (screen_rect.width - text_rect.width)/2),
|
|
|
|
(screen_rect.top + (screen_rect.height - text_rect.height)/2),
|
|
|
|
text_rect.width, text_rect.height)
|
2024-09-08 16:02:13 +00:00
|
|
|
self.bg_color = bg_color
|
2024-09-08 15:17:55 +00:00
|
|
|
|
|
|
|
def handle_event(self, _):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def draw(self, screen: pygame.Surface):
|
2024-09-08 16:02:13 +00:00
|
|
|
screen.fill(self.bg_color)
|
2024-09-08 15:17:55 +00:00
|
|
|
self.font.render_to(screen, self.text_pos, self.text, fgcolor="black")
|
|
|
|
|
|
|
|
|
2024-09-08 11:43:21 +00:00
|
|
|
class MenuBoard(IBoard):
|
2024-09-08 14:44:47 +00:00
|
|
|
BUTTON_MARGINS = 10
|
|
|
|
|
2024-09-08 16:02:13 +00:00
|
|
|
def __init__(self, screen_rect: pygame.Rect, buttons: List[ButtonDef], bg_color: str):
|
2024-09-08 15:50:38 +00:00
|
|
|
self.rows = math.floor(math.sqrt(len(buttons)))
|
|
|
|
self.cols = math.ceil(math.sqrt(len(buttons)))
|
|
|
|
if (self.rows * self.cols) < len(buttons):
|
|
|
|
# extra row if buttons don't fit
|
|
|
|
self.rows += 1
|
2024-09-08 14:44:47 +00:00
|
|
|
button_positions = self.generate_button_positions(screen_rect)
|
2024-09-08 16:02:13 +00:00
|
|
|
self.buttons = list(map(lambda d: Button(next(button_positions), d['text'], d['cbk']), buttons))
|
|
|
|
self.bg_color = bg_color
|
2024-09-08 14:44:47 +00:00
|
|
|
|
|
|
|
def generate_button_positions(self, screen_rect: pygame.Rect):
|
|
|
|
current_button_row = 0
|
|
|
|
current_button_col = 0
|
|
|
|
button_width = math.floor((screen_rect.width - (self.cols + 1) * self.BUTTON_MARGINS) / self.cols)
|
|
|
|
button_height = math.floor((screen_rect.height - (self.rows + 1) * self.BUTTON_MARGINS) / self.rows)
|
|
|
|
while (current_button_row * current_button_col) < (self.cols * self.rows):
|
|
|
|
top = self.BUTTON_MARGINS + (current_button_row * (button_height + self.BUTTON_MARGINS))
|
|
|
|
left = self.BUTTON_MARGINS + (current_button_col * (button_width + self.BUTTON_MARGINS))
|
|
|
|
current_button_col += 1
|
|
|
|
if current_button_col >= self.cols:
|
|
|
|
current_button_col = 0
|
|
|
|
current_button_row += 1
|
|
|
|
yield pygame.Rect(left, top, button_width, button_height)
|
|
|
|
|
2024-09-08 11:43:21 +00:00
|
|
|
def handle_event(self, event: pygame.event.Event):
|
|
|
|
for b in self.buttons:
|
|
|
|
b.handle_event(event)
|
|
|
|
|
|
|
|
def draw(self, screen: pygame.Surface):
|
2024-09-08 16:02:13 +00:00
|
|
|
screen.fill(self.bg_color)
|
2024-09-08 11:43:21 +00:00
|
|
|
for b in self.buttons:
|
|
|
|
b.draw(screen)
|
|
|
|
|
|
|
|
|
|
|
|
class App:
|
2024-09-08 16:02:13 +00:00
|
|
|
def __init__(self, urls: List[UrlDef], bg_color: str):
|
2024-09-08 11:43:21 +00:00
|
|
|
pygame.init()
|
|
|
|
info = pygame.display.Info()
|
|
|
|
self.screen = pygame.display.set_mode((info.current_w, info.current_h), flags=pygame.FULLSCREEN)
|
|
|
|
self.clock = pygame.time.Clock()
|
|
|
|
self.running = False
|
2024-09-08 16:02:13 +00:00
|
|
|
self.bg_color = bg_color
|
2024-09-08 15:50:38 +00:00
|
|
|
buttons = list(map(lambda u: ButtonDef(text=u['label'], cbk=self.button_press_handler(u['url'])), urls))
|
2024-09-08 14:50:18 +00:00
|
|
|
buttons.append(ButtonDef(text="Exit", cbk=self.quit))
|
2024-09-08 16:02:13 +00:00
|
|
|
self.board: IBoard = MenuBoard(self.get_screen_rect(), buttons, bg_color)
|
2024-09-08 15:17:55 +00:00
|
|
|
self.task_q = Queue()
|
|
|
|
|
|
|
|
def get_screen_rect(self) -> pygame.Rect:
|
|
|
|
return pygame.Rect(0, 0, self.screen.get_width(), self.screen.get_height())
|
|
|
|
|
2024-09-08 15:50:38 +00:00
|
|
|
def button_press_handler(self, url: str) -> Callable[[], None]:
|
2024-09-08 15:17:55 +00:00
|
|
|
def impl():
|
|
|
|
previous_board = self.board
|
2024-09-08 15:20:47 +00:00
|
|
|
|
2024-09-08 15:17:55 +00:00
|
|
|
def thr_fun():
|
|
|
|
def end_thr():
|
|
|
|
self.board = previous_board
|
2024-09-08 15:20:47 +00:00
|
|
|
|
2024-09-08 15:17:55 +00:00
|
|
|
sleep(5)
|
|
|
|
self.task_q.put(end_thr)
|
2024-09-08 15:20:47 +00:00
|
|
|
|
2024-09-08 15:17:55 +00:00
|
|
|
process_thr = Thread(target=thr_fun)
|
|
|
|
process_thr.start()
|
2024-09-08 16:02:13 +00:00
|
|
|
self.board = MessageBoard(self.get_screen_rect(), f"Fetching: {url}", bg_color=self.bg_color)
|
2024-09-08 15:17:55 +00:00
|
|
|
return impl
|
2024-09-08 11:43:21 +00:00
|
|
|
|
|
|
|
def loop(self):
|
|
|
|
self.running = True
|
|
|
|
while self.running:
|
2024-09-08 15:17:55 +00:00
|
|
|
try:
|
|
|
|
self.task_q.get_nowait()()
|
|
|
|
except Empty:
|
|
|
|
pass
|
2024-09-08 11:43:21 +00:00
|
|
|
for event in pygame.event.get():
|
|
|
|
if event.type == pygame.QUIT:
|
|
|
|
self.running = False
|
|
|
|
self.board.handle_event(event)
|
|
|
|
|
|
|
|
self.board.draw(self.screen)
|
|
|
|
|
|
|
|
pygame.display.flip()
|
|
|
|
|
|
|
|
self.clock.tick(30)
|
|
|
|
pygame.quit()
|
|
|
|
|
2024-09-08 14:50:18 +00:00
|
|
|
def quit(self):
|
2024-09-08 16:02:13 +00:00
|
|
|
self.board = MessageBoard(self.get_screen_rect(), "Exiting...", bg_color=self.bg_color)
|
2024-09-08 14:50:18 +00:00
|
|
|
self.running = False
|
|
|
|
|
2024-09-08 11:43:21 +00:00
|
|
|
|
2024-09-08 16:02:13 +00:00
|
|
|
def get_url_defs(config_data: dict) -> List[UrlDef]:
|
2024-09-08 15:50:38 +00:00
|
|
|
url_defs = []
|
2024-09-08 16:02:13 +00:00
|
|
|
for d in data['urls']:
|
|
|
|
url_defs.append(UrlDef(label=d['label'], url=d['url']))
|
2024-09-08 15:50:38 +00:00
|
|
|
return url_defs
|
|
|
|
|
|
|
|
|
2024-09-08 11:43:21 +00:00
|
|
|
if __name__ == '__main__':
|
2024-09-08 15:50:38 +00:00
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument("config_path", help="Path to the config file.", type=str)
|
|
|
|
args = parser.parse_args()
|
2024-09-08 16:02:13 +00:00
|
|
|
|
|
|
|
url_defs: List[UrlDef] = []
|
|
|
|
with open(args.config_path, "rb") as f:
|
|
|
|
data = json.load(f)
|
|
|
|
url_defs = get_url_defs(data)
|
|
|
|
|
|
|
|
app = App(url_defs, bg_color=data["bg_color"])
|
2024-09-08 11:43:21 +00:00
|
|
|
app.loop()
|