Files
croccybot/src/modules/pending-rewrite/gui-cards/tasklist.py

390 lines
13 KiB
Python

from io import BytesIO
import pickle
from PIL import Image, ImageDraw
from ..base import Card, Layout, fielded, Skin, FieldDesc
from ..base.Avatars import avatar_manager
from ..base.Skin import (
AssetField, StringField, NumberField,
FontField, ColourField, PointField, ComputedField
)
from .mixins import MiniProfileLayout
@fielded
class TasklistSkin(Skin):
_env = {
'scale': 2 # General size scale to match background resolution
}
# First page
first_page_bg: AssetField = "tasklist/first_page_background.png"
first_page_frame: AssetField = "tasklist/first_page_frame.png"
title_pre_gap: NumberField = 40
title_text: StringField = "TO DO LIST"
title_font: FontField = ('ExtraBold', 76)
title_size: ComputedField = lambda skin: skin.title_font.getsize(skin.title_text)
title_colour: ColourField = '#DDB21D'
title_underline_gap: NumberField = 10
title_underline_width: NumberField = 5
title_gap: NumberField = 50
# Profile section
mini_profile_indent: NumberField = 125
mini_profile_size: ComputedField = lambda skin: (
skin.first_page_bg.width - 2 * skin.mini_profile_indent,
int(skin._env['scale'] * 200)
)
mini_profile_avatar_mask: AssetField = FieldDesc(AssetField, 'mini-profile/avatar_mask.png', convert=None)
mini_profile_avatar_frame: AssetField = FieldDesc(AssetField, 'mini-profile/avatar_frame.png', convert='RGBA')
mini_profile_avatar_sep: NumberField = 50
mini_profile_name_font: FontField = ('BoldItalic', 55)
mini_profile_name_colour: ColourField = '#DDB21D'
mini_profile_discrim_font: FontField = mini_profile_name_font
mini_profile_discrim_colour: ColourField = '#BABABA'
mini_profile_name_gap: NumberField = 20
mini_profile_badge_end: AssetField = "mini-profile/badge_end.png"
mini_profile_badge_font: FontField = ('Black', 30)
mini_profile_badge_colour: ColourField = '#FFFFFF'
mini_profile_badge_text_colour: ColourField = '#051822'
mini_profile_badge_gap: NumberField = 20
mini_profile_badge_min_sep: NumberField = 10
# Other pages
other_page_bg: AssetField = "tasklist/other_page_background.png"
other_page_frame: AssetField = "tasklist/other_page_frame.png"
# Help frame
help_frame: AssetField = "tasklist/help_frame.png"
# Tasks
task_start_position: PointField = (100, 75)
task_done_number_bg: AssetField = "tasklist/task_done_bg.png"
task_done_number_font: FontField = ('Regular', 45)
task_done_number_colour: ColourField = '#292828'
task_done_text_font: FontField = ('Regular', 55)
task_done_text_colour: ColourField = '#686868'
task_done_line_width: NumberField = 3.5
task_undone_number_bg: AssetField = "tasklist/task_undone_bg.png"
task_undone_number_font: FontField = ('Regular', 45)
task_undone_number_colour: ColourField = '#FFFFFF'
task_undone_text_font: FontField = ('Regular', 55)
task_undone_text_colour: ColourField = '#FFFFFF'
task_text_height: ComputedField = lambda skin: skin.task_done_text_font.getsize('TASK')[1]
task_num_sep: NumberField = 30
task_inter_gap: NumberField = 32
task_intra_gap: NumberField = 25
# Date text
footer_pre_gap: NumberField = 50
footer_font: FontField = ('Bold', 28)
footer_colour: ColourField = '#686868'
footer_gap: NumberField = 50
class TasklistLayout(Layout, MiniProfileLayout):
def __init__(self, skin, name, discrim, tasks, date, avatar, badges=()):
self.skin = skin
self.data_name = name
self.data_discrim = discrim
self.data_avatar = avatar
self.data_tasks = tasks
self.data_date = date
self.data_badges = badges
self.tasks_drawn = 0
self.images = []
def _execute_draw(self):
image_data = []
for image in self.draw():
with BytesIO() as data:
image.save(data, format='PNG')
data.seek(0)
image_data.append(data.getvalue())
return pickle.dumps(image_data)
def draw(self):
self.images = []
self.images.append(self._draw_first_page())
while self.tasks_drawn < len(self.data_tasks):
self.images.append(self._draw_another_page())
return self.images
def close(self):
if self.images:
for image in self.images:
image.close()
def _draw_first_page(self) -> Image:
image = self.skin.first_page_bg
draw = ImageDraw.Draw(image)
xpos, ypos = 0, 0
# Draw header text
xpos = (image.width - self.skin.title_size[0]) // 2
ypos += self.skin.title_pre_gap
draw.text(
(xpos, ypos),
self.skin.title_text,
fill=self.skin.title_colour,
font=self.skin.title_font
)
# Underline it
ypos += self.skin.title_size[1] + self.skin.title_underline_gap
# draw.line(
# (xpos, ypos, xpos + self.skin.title_size[0], ypos),
# fill=self.skin.title_colour,
# width=self.skin.title_underline_width
# )
ypos += self.skin.title_underline_width + self.skin.title_gap
# Draw the profile
xpos = self.skin.mini_profile_indent
profile = self._draw_profile()
image.alpha_composite(
profile,
(xpos, ypos)
)
# Start from the bottom
ypos = image.height
if self.data_tasks:
# Draw the date text
ypos -= self.skin.footer_gap
date_text = self.data_date.strftime("As of %d %b")
size = self.skin.footer_font.getsize(date_text)
ypos -= size[1]
draw.text(
((image.width - size[0]) // 2, ypos),
date_text,
font=self.skin.footer_font,
fill=self.skin.footer_colour
)
ypos -= self.skin.footer_pre_gap
# Draw the tasks
task_image = self._draw_tasks_into(self.skin.first_page_frame.copy())
ypos -= task_image.height
image.alpha_composite(
task_image,
((image.width - task_image.width) // 2, ypos)
)
else:
# Draw the help frame
ypos -= self.skin.footer_gap
image.alpha_composite(
self.skin.help_frame,
((image.width - self.skin.help_frame.width) // 2, ypos - self.skin.help_frame.height)
)
return image
def _draw_another_page(self) -> Image:
image = self.skin.other_page_bg.copy()
draw = ImageDraw.Draw(image)
# Start from the bottom
ypos = image.height
# Draw the date text
ypos -= self.skin.footer_gap
date_text = self.data_date.strftime("As of %d %b • {} {}".format(self.data_name, self.data_discrim))
size = self.skin.footer_font.getsize(date_text)
ypos -= size[1]
draw.text(
((image.width - size[0]) // 2, ypos),
date_text,
font=self.skin.footer_font,
fill=self.skin.footer_colour
)
ypos -= self.skin.footer_pre_gap
# Draw the tasks
task_image = self._draw_tasks_into(self.skin.other_page_frame.copy())
ypos -= task_image.height
image.alpha_composite(
task_image,
((image.width - task_image.width) // 2, ypos)
)
return image
def _draw_tasks_into(self, image) -> Image:
"""
Draw as many tasks as possible into the given image background.
"""
draw = ImageDraw.Draw(image)
xpos, ypos = self.skin.task_start_position
for n, task, done in self.data_tasks[self.tasks_drawn:]:
# Draw task first to check if it fits on the page
task_image = self._draw_text(
task,
image.width - xpos - self.skin.task_done_number_bg.width - self.skin.task_num_sep,
done
)
if task_image.height + ypos + self.skin.task_inter_gap > image.height:
break
# Draw number background
bg = self.skin.task_done_number_bg if done else self.skin.task_undone_number_bg
image.alpha_composite(
bg,
(xpos, ypos)
)
# Draw number
font = self.skin.task_done_number_font if done else self.skin.task_undone_number_font
colour = self.skin.task_done_number_colour if done else self.skin.task_undone_number_colour
draw.text(
(xpos + bg.width // 2, ypos + bg.height // 2),
str(n),
fill=colour,
font=font,
anchor='mm'
)
# Draw text
image.alpha_composite(
task_image,
(xpos + bg.width + self.skin.task_num_sep, ypos - (bg.height - self.skin.task_text_height) // 2)
)
ypos += task_image.height + self.skin.task_inter_gap
self.tasks_drawn += 1
return image
def _draw_text(self, task, maxwidth, done) -> Image:
"""
Draw the text of a given task.
"""
font = self.skin.task_done_text_font if done else self.skin.task_undone_text_font
colour = self.skin.task_done_text_colour if done else self.skin.task_undone_text_colour
# Handle empty tasks
if not task.strip():
task = '~'
# First breakup the text
lines = []
line = []
width = 0
for word in task.split():
length = font.getlength(word + ' ')
if width + length > maxwidth:
if line:
lines.append(' '.join(line))
line = []
width = 0
line.append(word)
width += length
if line:
lines.append(' '.join(line))
# Then draw it
bboxes = [font.getbbox(line) for line in lines]
heights = [font.getsize(line)[1] for line in lines]
height = sum(height for height in heights) + (len(lines) - 1) * self.skin.task_intra_gap
image = Image.new('RGBA', (maxwidth, height))
draw = ImageDraw.Draw(image)
x, y = 0, 0
for line, (x1, y1, x2, y2), height in zip(lines, bboxes, heights):
draw.text(
(x, y),
line,
fill=colour,
font=font
)
if done:
# Also strikethrough
draw.line(
(x1, y + y1 + (y2 - y1) // 2, x2, y + y1 + (y2 - y1) // 2),
fill=self.skin.task_done_text_colour,
width=self.skin.task_done_line_width
)
y += height + self.skin.task_intra_gap
return image
class TasklistCard(Card):
route = 'tasklist_card'
card_id = 'tasklist'
layout = TasklistLayout
skin = TasklistSkin
display_name = "Tasklist"
@classmethod
async def request(cls, *args, **kwargs):
data = await super().request(*args, **kwargs)
return pickle.loads(data)
@classmethod
async def card_route(cls, runner, args, kwargs):
kwargs['avatar'] = await avatar_manager().get_avatar(*kwargs['avatar'], 256)
return await super().card_route(runner, args, kwargs)
@classmethod
def _execute(cls, *args, **kwargs):
with BytesIO(kwargs['avatar']) as image_data:
with Image.open(image_data).convert('RGBA') as avatar_image:
kwargs['avatar'] = avatar_image
return super()._execute(*args, **kwargs)
@classmethod
async def generate_sample(cls, ctx=None, **kwargs):
from ..utils import image_as_file
sample_kwargs = await cls.sample_args(ctx)
cards = await cls.request(**{**sample_kwargs, **kwargs})
return image_as_file(cards[0], "sample.png")
@classmethod
async def sample_args(cls, ctx, **kwargs):
import datetime
from ..utils import get_avatar_key
return {
'name': ctx.author.name if ctx else "John Doe",
'discrim': '#' + ctx.author.discriminator if ctx else "#0000",
'tasks': [
(0, 'Run 50km', True),
(1, 'Read 5 books', False),
(2, 'Renovate bedroom', True),
(3, 'Learn a new language', False),
(4, 'Upload a vlog', False),
(5, 'Bibendum arcu vitae elementum curabitur vitae nunc sed velit', False),
(6, 'Dictum fusce ut placerat orci', True),
(7, 'Pharetra vel turpis nunc eget lorem dolor', True)
],
'date': datetime.datetime.now().replace(hour=0, minute=0, second=0),
'avatar': get_avatar_key(ctx.client, ctx.author.id) if ctx else (0, None),
'badges': (
'STUDYING: MEDICINE',
'HOBBY: MATHS',
'CAREER: STUDENT',
'FROM: EUROPE',
'LOVES CATS <3'
),
}