From b777bd33e45987ea2eb8dea9c679fdb6849fdd69 Mon Sep 17 00:00:00 2001 From: Conatum Date: Fri, 23 Dec 2022 20:34:58 +0200 Subject: [PATCH] rewrite: Integrate GUI plugin. --- requirements.txt | 3 +- scripts/start_gui.py | 9 +- src/gui | 2 +- .../pending-rewrite/gui-cards/__init__.py | 8 - .../pending-rewrite/gui-cards/goals.py | 752 ------------------ .../pending-rewrite/gui-cards/leaderboard.py | 504 ------------ .../pending-rewrite/gui-cards/mixins.py | 143 ---- .../pending-rewrite/gui-cards/monthly.py | 750 ----------------- .../pending-rewrite/gui-cards/profile.py | 623 --------------- .../pending-rewrite/gui-cards/stats.py | 508 ------------ .../pending-rewrite/gui-cards/tasklist.py | 389 --------- src/modules/pending-rewrite/gui-cards/test.py | 38 - .../pending-rewrite/gui-cards/timer.py | 449 ----------- .../pending-rewrite/gui-cards/weekly.py | 713 ----------------- src/modules/tasklist/ui.py | 2 + 15 files changed, 13 insertions(+), 4880 deletions(-) delete mode 100644 src/modules/pending-rewrite/gui-cards/__init__.py delete mode 100644 src/modules/pending-rewrite/gui-cards/goals.py delete mode 100644 src/modules/pending-rewrite/gui-cards/leaderboard.py delete mode 100644 src/modules/pending-rewrite/gui-cards/mixins.py delete mode 100644 src/modules/pending-rewrite/gui-cards/monthly.py delete mode 100644 src/modules/pending-rewrite/gui-cards/profile.py delete mode 100644 src/modules/pending-rewrite/gui-cards/stats.py delete mode 100644 src/modules/pending-rewrite/gui-cards/tasklist.py delete mode 100644 src/modules/pending-rewrite/gui-cards/test.py delete mode 100644 src/modules/pending-rewrite/gui-cards/timer.py delete mode 100644 src/modules/pending-rewrite/gui-cards/weekly.py diff --git a/requirements.txt b/requirements.txt index d858c57f..f40f2604 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,8 @@ cachetools==4.2.2 configparser==5.0.2 discord.py=2.0.1 iso8601==0.1.16 -psycopg2==2.9.1 +psycopg pytz==2021.1 topggpy psutil +pillow diff --git a/scripts/start_gui.py b/scripts/start_gui.py index 0e615398..685fefa2 100755 --- a/scripts/start_gui.py +++ b/scripts/start_gui.py @@ -1,6 +1,13 @@ +# !/bin/python3 + +import sys +import os import asyncio -from src.server import main + +sys.path.insert(0, os.path.join(os.getcwd())) +sys.path.insert(0, os.path.join(os.getcwd(), "src")) if __name__ == '__main__': + from gui.server import main asyncio.run(main()) diff --git a/src/gui b/src/gui index fd34ea70..4f3d5740 160000 --- a/src/gui +++ b/src/gui @@ -1 +1 @@ -Subproject commit fd34ea70d62d83ce90b49e17ab74d8f73154ca93 +Subproject commit 4f3d5740a3bdddec37a7f7f2e3b55e8a8e23b674 diff --git a/src/modules/pending-rewrite/gui-cards/__init__.py b/src/modules/pending-rewrite/gui-cards/__init__.py deleted file mode 100644 index 27a258cb..00000000 --- a/src/modules/pending-rewrite/gui-cards/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .stats import StatsCard -from .profile import ProfileCard -from .goals import WeeklyGoalCard, MonthlyGoalCard -from .monthly import MonthlyStatsCard -from .weekly import WeeklyStatsCard -from .tasklist import TasklistCard -from .leaderboard import LeaderboardCard -from .timer import BreakTimerCard, FocusTimerCard diff --git a/src/modules/pending-rewrite/gui-cards/goals.py b/src/modules/pending-rewrite/gui-cards/goals.py deleted file mode 100644 index c353c83e..00000000 --- a/src/modules/pending-rewrite/gui-cards/goals.py +++ /dev/null @@ -1,752 +0,0 @@ -import math -import datetime -from io import BytesIO -from PIL import Image, ImageDraw, ImageOps, ImageColor - -from ..base import Card, Layout, fielded, Skin, FieldDesc -from ..base.Avatars import avatar_manager -from ..base.Skin import ( - AssetField, RGBAAssetField, BlobField, AssetPathField, StringField, NumberField, - FontField, ColourField, PointField, ComputedField -) - -from ..utils import get_avatar_key - -from .mixins import MiniProfileLayout - - -@fielded -class _GoalSkin(Skin): - _env = { - 'scale': 2 # General size scale to match background resolution - } - - background: AssetField = "goals/background.png" - - help_frame: AssetField = None - - # Title section - title_pre_gap: NumberField = 40 - title_text: StringField = "" - 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 = 0 - title_gap: NumberField = 50 - - # Profile section - mini_profile_indent: NumberField = 125 - mini_profile_size: ComputedField = lambda skin: ( - skin.background.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 - - # Progress bars - progress_mask: RGBAAssetField = 'goals/progressbar_mask.png' - progress_bg_colour: ColourField = '#273341' - progress_bg: BlobField = FieldDesc( - BlobField, - mask_field='progress_mask', - colour_field='progress_bg_colour', - colour_override_field=None - ) - progress_end: RGBAAssetField = 'goals/progressbar_end_mask.png' - progress_colour: ColourField = '#6CB7D0' - progress_colour_override: ColourField = None - progress_full: BlobField = FieldDesc( - BlobField, - mask_field='progress_mask', - colour_field='progress_colour', - colour_override_field='progress_colour_override' - ) - - line_gap: NumberField = 5 - progress_text_at: ComputedField = lambda skin: 7 * (skin.progress_bg.height // 10) - - task_count_font: FontField = ('Bold', 76) - task_count_colour: ColourField = '#DDB21D' - task_done_font: FontField = ('Bold', 37) - task_done_colour: ColourField = '#FFFFFF' - task_goal_font: FontField = ('Bold', 27) - task_goal_colour: ColourField = '#FFFFFF' - task_goal_number_font: FontField = ('Light', 27) - task_goal_number_colour: ColourField = '#FFFFFF' - task_text_size: ComputedField = lambda skin: ( - skin.task_count_font.getsize("00")[0] - + skin.task_done_font.getsize("TASKS DONE")[0] - + skin.task_goal_font.getsize("GOAL")[0] - + 3 * skin.line_gap, - skin.task_done_font.getsize("TASKS DONE")[1] - ) - task_progress_text_height: ComputedField = lambda skin: ( - skin.task_count_font.getsize('100')[1] + - skin.task_done_font.getsize('TASKS DONE')[1] + - skin.task_goal_font.getsize('GOAL')[1] + - 2 * skin.line_gap - ) - - attendance_rate_font: FontField = ('Bold', 76) - attendance_rate_colour: ColourField = '#DDB21D' - attendance_font: FontField = ('Bold', 37) - attendance_colour: ColourField = '#FFFFFF' - attendance_text_height: ComputedField = lambda skin: ( - skin.attendance_rate_font.getsize('100%')[1] + - skin.attendance_font.getsize('ATTENDANCE')[1] * 2 + - 2 * skin.line_gap - ) - - studied_text_font: FontField = ('Bold', 37) - studied_text_colour: ColourField = '#FFFFFF' - studied_hour_font: FontField = ('Bold', 60) - studied_hour_colour: ColourField = '#DDB21D' - studied_text_height: ComputedField = lambda skin: ( - skin.studied_text_font.getsize('STUDIED')[1] * 2 - + skin.studied_hour_font.getsize('400')[1] - + 2 * skin.line_gap - ) - - progress_gap: NumberField = 50 - - # Tasks - task_frame: AssetField = "goals/task_frame.png" - task_margin: PointField = (100, 50) - task_column_sep: NumberField = 100 - - task_header: StringField = "" - task_header_font: FontField = ('Black', 50) - task_header_colour: ColourField = '#DDB21D' - task_header_gap: NumberField = 25 - task_underline_gap: NumberField = 10 - task_underline_width: NumberField = 5 - - task_done_number_bg: AssetField = "goals/task_done.png" - task_done_number_font: FontField = ('Regular', 28) - task_done_number_colour: ColourField = '#292828' - - task_done_text_font: FontField = ('Regular', 35) - task_done_text_colour: ColourField = '#686868' - - task_done_line_width: NumberField = FieldDesc(NumberField, 7, scaled=False) - - task_undone_number_bg: AssetField = "goals/task_undone.png" - task_undone_number_font: FontField = ('Regular', 28) - task_undone_number_colour: ColourField = '#FFFFFF' - - task_undone_text_font: FontField = ('Regular', 35) - task_undone_text_colour: ColourField = '#FFFFFF' - - task_text_height: ComputedField = lambda skin: skin.task_done_text_font.getsize('TASK')[1] - task_num_sep: NumberField = 15 - task_inter_gap: NumberField = 25 - - # Date text - footer_pre_gap: NumberField = 25 - footer_font: FontField = ('Bold', 28) - footer_colour: ColourField = '#6f6e6f' - footer_gap: NumberField = 50 - - -@fielded -class WeeklyGoalSkin(_GoalSkin): - title_text: StringField = "WEEKLY STATISTICS" - task_header: StringField = "GOALS OF THE WEEK" - - help_frame: AssetField = "weekly/help_frame.png" - - -@fielded -class MonthlyGoalSkin(_GoalSkin): - title_text: StringField = "MONTHLY STATISTICS" - task_header: StringField = "GOALS OF THE MONTH" - - help_frame: AssetField = "monthly/help_frame.png" - - -class GoalPage(Layout, MiniProfileLayout): - def __init__(self, skin, - name, discrim, avatar, badges, - tasks_done, studied_hours, attendance, - tasks_goal, studied_goal, goals, - date): - self.skin = skin - - self.data_name = name - self.data_discrim = discrim - self.data_avatar = avatar - self.data_badges = badges - - self.data_tasks_done = tasks_done - self.data_studied_hours = studied_hours - self.data_attendance = attendance - self.data_tasks_goal = tasks_goal - - self.data_studied_goal = studied_goal - self.data_goals = goals - self.data_date = date - - self.image = None - - def draw(self) -> Image: - image = self.skin.background - 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 - title_size = self.skin.title_font.getsize(self.skin.title_text) - ypos += title_size[1] + self.skin.title_gap - # ypos += title_size[1] + self.skin.title_underline_gap - # draw.line( - # (xpos, ypos, xpos + 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 - - # 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 - - if self.data_goals or self.data_tasks_goal or self.data_studied_goal: - # Draw the tasks - task_image = self._draw_tasks() - - ypos -= task_image.height - image.alpha_composite( - task_image, - ((image.width - task_image.width) // 2, ypos) - ) - - # Draw the progress bars - progress_image = self._draw_progress() - ypos -= progress_image.height + self.skin.progress_gap - image.alpha_composite( - progress_image, - ((image.width - progress_image.width) // 2, ypos) - ) - else: - ypos -= self.skin.help_frame.height - image.alpha_composite( - self.skin.help_frame, - ((image.width - self.skin.help_frame.width) // 2, ypos) - ) - - self.image = image - - return image - - def _draw_tasks(self): - image = self.skin.task_frame - draw = ImageDraw.Draw(image) - - # Task container is smaller than frame - xpos, ypos = self.skin.task_margin - - # Draw header text - draw.text( - (xpos, ypos), - self.skin.task_header, - fill=self.skin.task_header_colour, - font=self.skin.task_header_font - ) - - # Underline it - title_size = self.skin.task_header_font.getsize(self.skin.task_header) - ypos += title_size[1] + self.skin.task_underline_gap - draw.line( - (xpos, ypos, xpos + title_size[0], ypos), - fill=self.skin.task_header_colour, - width=self.skin.task_underline_width - ) - ypos += self.skin.task_underline_width + self.skin.task_header_gap - - if len(self.data_goals) > 5: - # Split remaining space into two boxes - task_box_1 = Image.new( - 'RGBA', - (image.width // 2 - self.skin.task_margin[0] - self.skin.task_column_sep // 2, - image.height - ypos) - ) - task_box_2 = Image.new( - 'RGBA', - (image.width // 2 - self.skin.task_margin[0] - self.skin.task_column_sep // 2, - image.height - ypos) - ) - self._draw_tasks_into(self.data_goals[:5], task_box_1) - self._draw_tasks_into(self.data_goals[5:], task_box_2) - image.alpha_composite( - task_box_1, - (xpos, ypos) - ) - image.alpha_composite( - task_box_2, - (xpos + task_box_1.width + self.skin.task_column_sep, ypos) - ) - else: - task_box = Image.new( - 'RGBA', - (image.width - 2 * self.skin.task_margin[0], image.height) - ) - self._draw_tasks_into(self.data_goals, task_box) - image.alpha_composite( - task_box, - (xpos, ypos) - ) - return image - - def _draw_progress(self): - image = Image.new('RGBA', (self.skin.background.width, self.skin.progress_bg.height)) - - sep = (self.skin.background.width - 3 * self.skin.progress_bg.width) // 4 - - xpos = sep - image.alpha_composite( - self._draw_task_progress(), - (xpos, 0) - ) - - xpos += self.skin.progress_bg.width + sep - image.alpha_composite( - self._draw_study_progress(), - (xpos, 0) - ) - - xpos += self.skin.progress_bg.width + sep - image.alpha_composite( - self._draw_attendance(), - (xpos, 0) - ) - - return image - - def _draw_task_progress(self): - if not self.data_tasks_goal: - amount = 1 - else: - amount = self.data_tasks_done / self.data_tasks_goal - - progress_image = self._draw_progress_bar(amount) - draw = ImageDraw.Draw(progress_image) - - # Draw text into the bar - ypos = self.skin.progress_text_at - self.skin.task_progress_text_height - xpos = progress_image.width // 2 - - text = str(self.data_tasks_done) - draw.text( - (xpos, ypos), - text, - font=self.skin.task_count_font, - fill=self.skin.task_count_colour, - anchor='mt' - ) - ypos += self.skin.task_count_font.getsize(text)[1] + self.skin.line_gap - - text = "TASKS DONE" - draw.text( - (xpos, ypos), - text, - font=self.skin.task_done_font, - fill=self.skin.task_done_colour, - anchor='mt' - ) - ypos += self.skin.task_done_font.getsize(text)[1] + self.skin.line_gap - - text1 = "GOAL: " - length1 = self.skin.task_goal_font.getlength(text1) - text2 = str(self.data_tasks_goal) if self.data_tasks_goal else "N/A" - length2 = self.skin.task_goal_number_font.getlength(text2) - draw.text( - (xpos - length2 // 2, ypos), - text1, - font=self.skin.task_goal_font, - fill=self.skin.task_goal_colour, - anchor='mt' - ) - draw.text( - (xpos + length1 // 2, ypos), - text2, - font=self.skin.task_goal_number_font, - fill=self.skin.task_goal_number_colour, - anchor='mt' - ) - return progress_image - - def _draw_study_progress(self): - if not self.data_studied_goal: - amount = 1 - else: - amount = self.data_studied_hours / self.data_studied_goal - - progress_image = self._draw_progress_bar(amount) - draw = ImageDraw.Draw(progress_image) - - ypos = self.skin.progress_text_at - self.skin.studied_text_height - xpos = progress_image.width // 2 - - text = "STUDIED" - draw.text( - (xpos, ypos), - text, - font=self.skin.studied_text_font, - fill=self.skin.studied_text_colour, - anchor='mt' - ) - ypos += self.skin.studied_text_font.getsize(text)[1] + self.skin.line_gap - - if self.data_studied_goal: - text = f"{self.data_studied_hours}/{self.data_studied_goal}" - else: - text = str(self.data_studied_hours) - draw.text( - (xpos, ypos), - text, - font=self.skin.studied_hour_font, - fill=self.skin.studied_hour_colour, - anchor='mt' - ) - ypos += self.skin.studied_hour_font.getsize(text)[1] + self.skin.line_gap - - text = "HOURS" - draw.text( - (xpos, ypos), - text, - font=self.skin.studied_text_font, - fill=self.skin.studied_text_colour, - anchor='mt' - ) - return progress_image - - def _draw_attendance(self): - amount = self.data_attendance or 0 - - progress_image = self._draw_progress_bar(amount) - draw = ImageDraw.Draw(progress_image) - - ypos = self.skin.progress_text_at - self.skin.attendance_text_height - xpos = progress_image.width // 2 - - if self.data_attendance is not None: - text = f"{int(self.data_attendance * 100)}%" - else: - text = "N/A" - draw.text( - (xpos, ypos), - text, - font=self.skin.attendance_rate_font, - fill=self.skin.attendance_rate_colour, - anchor='mt' - ) - ypos += self.skin.attendance_rate_font.getsize(text)[1] + self.skin.line_gap - - text = "ATTENDANCE" - draw.text( - (xpos, ypos), - text, - font=self.skin.attendance_font, - fill=self.skin.attendance_colour, - anchor='mt' - ) - ypos += self.skin.attendance_font.getsize(text)[1] + self.skin.line_gap - - text = "RATE" - draw.text( - (xpos, ypos), - text, - font=self.skin.attendance_font, - fill=self.skin.attendance_colour, - anchor='mt' - ) - return progress_image - - def _draw_tasks_into(self, tasks, image) -> Image: - """ - Draw as many tasks as possible into the given image background. - """ - draw = ImageDraw.Draw(image) - xpos, ypos = 0, 0 - - for n, task, done in tasks: - # 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 > 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 - - 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 - - size = font.getsize(task) - image = Image.new('RGBA', (min(size[0], maxwidth), size[1])) - draw = ImageDraw.Draw(image) - - draw.text((0, 0), task, font=font, fill=colour) - - if done: - # Also strikethrough - y = 0 - x1, y1, x2, y2 = font.getbbox(task) - 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 - ) - - return image - - def _draw_progress_bar(self, amount): - amount = min(amount, 1) - amount = max(amount, 0) - - end = self.skin.progress_end - mask = self.skin.progress_mask - - center = ( - mask.width // 2 + 1, - mask.height // 2 - ) - radius = 2 * 158 - theta = amount * math.pi * 2 - math.pi / 2 - x = int(center[0] + radius * math.cos(theta)) - y = int(center[1] + radius * math.sin(theta)) - - canvas = Image.new('RGBA', size=(mask.width, mask.height)) - draw = ImageDraw.Draw(canvas) - - if amount >= 0.01: - canvas.alpha_composite( - end, - ( - center[0] - end.width // 2, - 30 - end.height // 2 - ) - ) - canvas.alpha_composite( - end, - ( - x - end.width // 2, - y - end.height // 2 - ) - ) - - sidelength = mask.width // 2 - line_ends = ( - int(center[0] + sidelength * math.cos(theta)), - int(center[1] + sidelength * math.sin(theta)) - ) - if amount <= 0.25: - path = [ - center, - (center[0], center[1] - sidelength), - (mask.width, 0), - line_ends - ] - elif amount <= 0.5: - path = [ - center, - (center[0], center[1] - sidelength), - (mask.width, 0), - (mask.width, mask.height), - line_ends - ] - elif amount <= 0.75: - path = [ - center, - (center[0], center[1] - sidelength), - (mask.width, 0), - (mask.width, mask.height), - (0, mask.height), - line_ends - ] - else: - path = [ - center, - (center[0], center[1] - sidelength), - (mask.width, 0), - (mask.width, mask.height), - (0, mask.height), - (0, 0), - line_ends - ] - - draw.polygon( - path, - fill='#FFFFFF' - ) - # canvas.paste((0, 0, 0, 0), mask=mask) - - image = Image.composite( - self.skin.progress_full, - self.skin.progress_bg.copy(), - canvas - ) - - else: - image = self.skin.progress_bg.copy() - return image - - -class _GoalCard(Card): - layout = GoalPage - - @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) - - -class WeeklyGoalCard(_GoalCard): - route = "weekly_goal_card" - card_id = "weekly_goals" - - skin = WeeklyGoalSkin - - display_name = "Weekly Goals" - - @classmethod - async def sample_args(cls, ctx, **kwargs): - return { - 'name': ctx.author.name if ctx else 'John Doe', - 'discrim': ('#' + ctx.author.discriminator) if ctx else '#0000', - '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' - ), - 'tasks_done': 100, - 'tasks_goal': 300, - 'studied_hours': 16, - 'studied_goal': 48, - 'attendance': 0.9, - 'goals': [(0, 'Write a 200 page thesis', False), - (1, 'Feed the kangaroo', True), - (2, 'Cure world hunger', False), - (3, 'Finish assignment 2', True)], - 'date': datetime.datetime.now() - } - - -class MonthlyGoalCard(_GoalCard): - route = "monthly_goal_card" - card_id = "monthly_goals" - - skin = MonthlyGoalSkin - - display_name = "Monthly Goals" - - @classmethod - async def sample_args(cls, ctx, **kwargs): - return { - 'name': ctx.author.name if ctx else 'John Doe', - 'discrim': ('#' + ctx.author.discriminator) if ctx else '#0000', - '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' - ), - 'tasks_done': 400, - 'tasks_goal': 1200, - 'studied_hours': 64, - 'studied_goal': 128, - 'attendance': 0.95, - 'goals': [(0, 'Meet 10 new people', False), - (1, 'Feed the lion', False), - (2, 'Realise I am awesome', False), - (3, 'Never going to give you up', True)], - 'date': datetime.datetime.now() - } diff --git a/src/modules/pending-rewrite/gui-cards/leaderboard.py b/src/modules/pending-rewrite/gui-cards/leaderboard.py deleted file mode 100644 index 2fd50363..00000000 --- a/src/modules/pending-rewrite/gui-cards/leaderboard.py +++ /dev/null @@ -1,504 +0,0 @@ -import asyncio -from io import BytesIO -from PIL import Image, ImageDraw - -from ..base import Card, Layout, fielded, Skin, FieldDesc -from ..base.Avatars import avatar_manager -from ..base.Skin import ( - AssetField, RGBAAssetField, AssetPathField, BlobField, StringField, NumberField, - FontField, ColourField, ComputedField -) - - -class LeaderboardEntry: - __slots__ = ( - 'userid', - 'position', - 'time', - 'name', - 'avatar_key', - 'image' - ) - - def __init__(self, userid, position, time, name, avatar_key): - self.userid = userid - - self.position = position - self.time = time - - self.name = name - self.name = ''.join(i if ord(i) < 128 or i == '∞' else '*' for i in self.name) - - self.avatar_key = avatar_key - - self.image = None - - async def get_avatar(self): - if not self.image: - self.image = await avatar_manager().get_avatar( - *self.avatar_key, - size=512 if self.position in (1, 2, 3) else 256 - ) - - def convert_avatar(self): - if self.image: - with BytesIO(self.image) as data: - self.image = Image.open(data).convert('RGBA') - - -@fielded -class LeaderboardSkin(Skin): - _env = { - 'scale': 2 # General size scale to match background resolution - } - - header_text_pre_gap: NumberField = 20 - header_text: StringField = "STUDY TIME LEADERBOARD" - header_text_font: FontField = ('ExtraBold', 80) - header_text_size: ComputedField = lambda skin: skin.header_text_font.getsize(skin.header_text) - header_text_colour: ColourField = '#DDB21D' - - header_text_gap: NumberField = 15 - header_text_line_width: NumberField = 0 - header_text_line_gap: NumberField = 20 - - subheader_name_font: FontField = ('SemiBold', 27) - subheader_name_colour: ColourField = '#FFFFFF' - subheader_value_font: FontField = ('Regular', 27) - subheader_value_colour: ColourField = '#FFFFFF' - - header_gap: NumberField = 20 - - # First page constants - first_bg_path: AssetPathField = "leaderboard/first_page_background.png" - header_bg_gap: NumberField = 20 - first_header_height: NumberField = 694 - - first_avatar_mask: RGBAAssetField = "leaderboard/medal_avatar_mask.png" - - first_avatar_bg: RGBAAssetField = "leaderboard/first_avatar_background.png" - second_avatar_bg: RGBAAssetField = "leaderboard/second_avatar_background.png" - third_avatar_bg: RGBAAssetField = "leaderboard/third_avatar_background.png" - - first_avatar_gap: NumberField = 20 - - first_top_gap: NumberField = 20 - - top_position_font: FontField = ('Bold', 30) - top_position_colour: ColourField = '#FFFFFF' - top_name_font: FontField = ('Bold', 30) - top_name_colour: ColourField = '#DDB21D' - top_hours_font: FontField = ('Medium', 30) - top_hours_colour: ColourField = '#FFFFFF' - top_text_sep: NumberField = 5 - - # Other page constants - other_bg_path: AssetPathField = "leaderboard/other_page_background.png" - other_header_height: NumberField = 276 - other_header_gap: NumberField = 20 - - # Entry constants - entry_position_font: FontField = ("SemiBold", 45) - entry_position_colour: ColourField = '#FFFFFF' - entry_position_highlight_colour: ColourField = '#FFFFFF' - entry_name_highlight_colour: ColourField = '#FFFFFF' - entry_hours_highlight_colour: ColourField = '#FFFFFF' - entry_name_font: FontField = ("SemiBold", 45) - entry_name_colour: ColourField = '#FFFFFF' - entry_hours_font: FontField = ("SemiBold", 45) - entry_hours_colour: ColourField = '#FFFFFF' - entry_position_at: NumberField = 200 - entry_name_at: NumberField = 300 - entry_time_at: NumberField = -150 - - entry_mask: AssetField = "leaderboard/entry_avatar_mask.png" - - entry_bg_mask: AssetField = "leaderboard/entry_bg_mask.png" - entry_bg_colour: ColourField = "#162D3C" - entry_bg_highlight_colour: ColourField = "#0D4865" - - entry_bg: BlobField = FieldDesc( - BlobField, - mask_field='entry_bg_mask', - colour_field='entry_bg_colour', - colour_override_field=None - ) - entry_highlight_bg: BlobField = FieldDesc( - BlobField, - mask_field='entry_bg_mask', - colour_field='entry_bg_highlight_colour', - colour_override_field=None - ) - - entry_gap: NumberField = 13 - - -class LeaderboardPage(Layout): - def __init__(self, skin, server_name, entries, highlight=None): - self.skin = skin - - self.server_name = server_name - self.entries = entries - self.highlight = highlight - self.first_page = any(entry.position in (1, 2, 3) for entry in entries) - - self.image = None - - def draw(self) -> Image: - if self.first_page: - self.image = self._draw_first_page() - else: - self.image = self._draw_other_page() - return self.image - - def _draw_first_page(self) -> Image: - # Collect background - image = Image.open(self.skin.first_bg_path) - draw = ImageDraw.Draw(image) - - xpos, ypos = 0, 0 - - # Draw the header text - ypos += self.skin.header_text_pre_gap - header = self._draw_header_text() - image.alpha_composite( - header, - (xpos + (image.width // 2 - header.width // 2), - ypos) - ) - ypos += header.height + self.skin.header_gap - - # Draw the top 3 - first_entry = self.entries[0] - first = self._draw_first(first_entry, level=1) - first_x = (image.width - first.width) // 2 - image.alpha_composite( - first, - (first_x, ypos) - ) - first_text_y = ypos + first.height + self.skin.first_top_gap - text_y = first_text_y - text_x = first_x + (first.width // 2) - draw.text( - (text_x, text_y), - '1ST', - font=self.skin.top_position_font, - fill=self.skin.top_position_colour, - anchor='mt' - ) - text_y += self.skin.top_name_font.getsize('1ST')[1] + self.skin.top_text_sep - draw.text( - (text_x, text_y), - first_entry.name, - font=self.skin.top_name_font, - fill=self.skin.top_name_colour, - anchor='mt' - ) - text_y += self.skin.top_name_font.getsize(first_entry.name)[1] + self.skin.top_text_sep - draw.text( - (text_x, text_y), - "{} hours".format(first_entry.time // 3600), - font=self.skin.top_hours_font, - fill=self.skin.top_hours_colour, - anchor='mt' - ) - - if len(self.entries) >= 2: - second_entry = self.entries[1] - second = self._draw_first(second_entry, level=2) - second_x = image.width // 4 - second.width // 2 - image.alpha_composite( - second, - ( - second_x, - ypos + (first.height - second.height) // 2 - ) - ) - text_y = first_text_y - text_x = second_x + (second.width // 2) - draw.text( - (text_x, text_y), - '2ND', - font=self.skin.top_position_font, - fill=self.skin.top_position_colour, - anchor='mt' - ) - text_y += self.skin.top_name_font.getsize('2ND')[1] + self.skin.top_text_sep - draw.text( - (text_x, text_y), - second_entry.name, - font=self.skin.top_name_font, - fill=self.skin.top_name_colour, - anchor='mt' - ) - text_y += self.skin.top_name_font.getsize(second_entry.name)[1] + self.skin.top_text_sep - draw.text( - (text_x, text_y), - "{} hours".format(second_entry.time // 3600), - font=self.skin.top_hours_font, - fill=self.skin.top_hours_colour, - anchor='mt' - ) - - if len(self.entries) >= 3: - third_entry = self.entries[2] - third = self._draw_first(third_entry, level=3) - third_x = 3 * image.width // 4 - third.width // 2 - image.alpha_composite( - third, - ( - third_x, - ypos + (first.height - third.height) // 2 - ) - ) - text_y = first_text_y - text_x = third_x + (third.width // 2) - draw.text( - (text_x, text_y), - '3RD', - font=self.skin.top_position_font, - fill=self.skin.top_position_colour, - anchor='mt' - ) - text_y += self.skin.top_name_font.getsize('3ND')[1] + self.skin.top_text_sep - draw.text( - (text_x, text_y), - third_entry.name, - font=self.skin.top_name_font, - fill=self.skin.top_name_colour, - anchor='mt' - ) - text_y += self.skin.top_name_font.getsize(third_entry.name)[1] + self.skin.top_text_sep - draw.text( - (text_x, text_y), - "{} hours".format(third_entry.time // 3600), - font=self.skin.top_hours_font, - fill=self.skin.top_hours_colour, - anchor='mt' - ) - - # Draw the entries - xpos = (image.width - self.skin.entry_bg_mask.width) // 2 - ypos = self.skin.first_header_height + self.skin.header_bg_gap - - for entry in self.entries[3:]: - entry_image = self._draw_entry( - entry, - highlight=self.highlight and (entry.position == self.highlight) - ) - image.alpha_composite( - entry_image, - (xpos, ypos) - ) - ypos += self.skin.entry_bg_mask.height + self.skin.entry_gap - - return image - - def _draw_other_page(self) -> Image: - # Collect background - image = Image.open(self.skin.other_bg_path).convert('RGBA') - - # Draw header onto background - header = self._draw_header_text() - image.alpha_composite( - header, - ( - (image.width - header.width) // 2, - (self.skin.other_header_height - header.height) // 2 - ) - ) - - # Draw the entries - xpos = (image.width - self.skin.entry_bg.width) // 2 - ypos = ( - image.height - 10 * self.skin.entry_bg.height - 9 * self.skin.entry_gap - + self.skin.other_header_height - self.skin.other_header_gap - ) // 2 - - for entry in self.entries: - entry_image = self._draw_entry( - entry, - highlight=self.highlight and (entry.position == self.highlight) - ) - image.alpha_composite( - entry_image, - (xpos, ypos) - ) - ypos += self.skin.entry_bg.height + self.skin.entry_gap - - return image - - def _draw_entry(self, entry, highlight=False) -> Image: - # Get the appropriate background - image = (self.skin.entry_bg if not highlight else self.skin.entry_highlight_bg).copy() - draw = ImageDraw.Draw(image) - ypos = image.height // 2 - - # Mask the avatar, if it exists - avatar = entry.image - avatar.thumbnail((187, 187)) - avatar.paste((0, 0, 0, 0), mask=self.skin.entry_mask) - - # Paste avatar onto image - image.alpha_composite(avatar, (0, 0)) - - # Write position - draw.text( - (self.skin.entry_position_at, ypos), - str(entry.position), - fill=self.skin.entry_position_highlight_colour if highlight else self.skin.entry_position_colour, - font=self.skin.entry_position_font, - anchor='mm' - ) - - # Write name - draw.text( - (self.skin.entry_name_at, ypos), - entry.name, - fill=self.skin.entry_name_highlight_colour if highlight else self.skin.entry_name_colour, - font=self.skin.entry_name_font, - anchor='lm' - ) - - # Write time - time_str = "{:02d}:{:02d}".format( - entry.time // 3600, - (entry.time % 3600) // 60 - ) - draw.text( - (image.width + self.skin.entry_time_at, ypos), - time_str, - fill=self.skin.entry_hours_highlight_colour if highlight else self.skin.entry_hours_colour, - font=self.skin.entry_hours_font, - anchor='mm' - ) - - return image - - def _draw_first(self, entry, level) -> Image: - if level == 1: - image = self.skin.first_avatar_bg - elif level == 2: - image = self.skin.second_avatar_bg - elif level == 3: - image = self.skin.third_avatar_bg - - # Retrieve and mask avatar - avatar = entry.image - avatar.paste((0, 0, 0, 0), mask=self.skin.first_avatar_mask) - - # Resize for background with gap - dest_width = image.width - 2 * self.skin.first_avatar_gap - avatar.thumbnail((dest_width, dest_width)) - - # Paste on the background - image.alpha_composite( - avatar.convert('RGBA'), - ( - (image.width - avatar.width) // 2, - image.height - self.skin.first_avatar_gap - avatar.height) - ) - - return image - - def _draw_header_text(self) -> Image: - image = Image.new( - 'RGBA', - (self.skin.header_text_size[0], - self.skin.header_text_size[1] + self.skin.header_text_gap + self.skin.header_text_line_width - + self.skin.header_text_line_gap - + self.skin.subheader_name_font.getsize("THIS MONTHghjyp")[1]), - ) - draw = ImageDraw.Draw(image) - xpos, ypos = 0, 0 - - # Draw the top text - draw.text( - (0, 0), - self.skin.header_text, - font=self.skin.header_text_font, - fill=self.skin.header_text_colour - ) - ypos += self.skin.header_text_size[1] + self.skin.header_text_gap - - # Draw the underline - # draw.line( - # (xpos, ypos, - # xpos + self.skin.header_text_size[0], ypos), - # fill=self.skin.header_text_colour, - # width=self.skin.header_text_line_width - # ) - # ypos += self.skin.header_text_line_gap - - # Draw the subheader - text_name = "SERVER: " - text_name_width = self.skin.subheader_name_font.getlength(text_name) - text_value = self.server_name - text_value_width = self.skin.subheader_value_font.getlength(text_value) - total_width = text_name_width + text_value_width - xpos += (image.width - total_width) // 2 - draw.text( - (xpos, ypos), - text_name, - fill=self.skin.subheader_name_colour, - font=self.skin.subheader_name_font - ) - xpos += text_name_width - draw.text( - (xpos, ypos), - text_value, - fill=self.skin.subheader_value_colour, - font=self.skin.subheader_value_font - ) - - return image - - -class LeaderboardCard(Card): - route = 'leaderboard_card' - card_id = 'leaderboard' - - layout = LeaderboardPage - skin = LeaderboardSkin - - display_name = "Leaderboard" - - @classmethod - async def card_route(cls, runner, args, kwargs): - entries = [LeaderboardEntry(*entry) for entry in kwargs['entries']] - await asyncio.gather( - *(entry.get_avatar() for entry in entries) - ) - kwargs['entries'] = entries - return await super().card_route(runner, args, kwargs) - - @classmethod - def _execute(cls, *args, **kwargs): - for entry in kwargs['entries']: - entry.convert_avatar() - return super()._execute(*args, **kwargs) - - @classmethod - async def sample_args(cls, ctx, **kwargs): - from ..utils import get_avatar_key - - return { - 'server_name': (ctx.guild.name if ctx.guild else f"{ctx.author.name}'s DMs") if ctx else "No Server", - 'entries': [ - ( - (ctx.author.id, 1, 1474481, ctx.author.name, get_avatar_key(ctx.client, ctx.author.id)) - if ctx else - (0, 1, 1474481, "John Doe", (0, None)) - ), - (1, 2, 1445975, 'Abioye', (0, None)), - (2, 3, 1127296, 'Lacey', (0, None)), - (3, 4, 1112495, 'Chesed', (0, None)), - (4, 5, 854514, 'Almas', (0, None)), - (5, 6, 824414, 'Uche', (0, None)), - (6, 7, 634560, 'Boitumelo', (0, None)), - (7, 8, 540633, 'Abimbola', (0, None)), - (8, 9, 417487, 'Keone', (0, None)), - (9, 10, 257274, 'Desta', (0, None)) - ], - 'highlight': 4 - } diff --git a/src/modules/pending-rewrite/gui-cards/mixins.py b/src/modules/pending-rewrite/gui-cards/mixins.py deleted file mode 100644 index 10f9d1e8..00000000 --- a/src/modules/pending-rewrite/gui-cards/mixins.py +++ /dev/null @@ -1,143 +0,0 @@ -from PIL import Image, ImageDraw, ImageOps, ImageColor - -from ..base import Card, Layout, fielded, Skin, FieldDesc -from ..base.Avatars import avatar_manager -from ..base.Skin import ( - AssetField, RGBAAssetField, BlobField, AssetPathField, StringField, NumberField, - FontField, ColourField, PointField, ComputedField -) - - -@fielded -class MiniProfileSkin(Skin): - - # Profile section - mini_profile_indent: NumberField = 125 - mini_profile_size: ComputedField = lambda skin: ( - skin.background.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=None) - 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 - - -class MiniProfileLayout: - def _draw_profile(self) -> Image: - image = Image.new('RGBA', self.skin.mini_profile_size) - draw = ImageDraw.Draw(image) - xpos, ypos = 0, 0 - - frame = self.skin.mini_profile_avatar_frame - if frame.height >= image.height: - frame.thumbnail((image.height, image.height)) - - # Draw avatar - avatar = self.data_avatar - avatar.paste((0, 0, 0, 0), mask=self.skin.mini_profile_avatar_mask) - avatar_image = Image.new('RGBA', frame.size) - avatar_image.paste( - avatar, - ( - (frame.width - avatar.width) // 2, - (frame.height - avatar.height) // 2 - ) - ) - avatar_image.alpha_composite(frame) - avatar_image = avatar_image.resize( - (self.skin.mini_profile_size[1], self.skin.mini_profile_size[1]) - ) - image.alpha_composite(avatar_image, (0, 0)) - - xpos += avatar_image.width + self.skin.mini_profile_avatar_sep - - # Draw name - name_text = self.data_name - name_length = self.skin.mini_profile_name_font.getlength(name_text + ' ') - name_height = self.skin.mini_profile_name_font.getsize(name_text)[1] - draw.text( - (xpos, ypos), - name_text, - fill=self.skin.mini_profile_name_colour, - font=self.skin.mini_profile_name_font - ) - draw.text( - (xpos + name_length, ypos), - self.data_discrim, - fill=self.skin.mini_profile_discrim_colour, - font=self.skin.mini_profile_discrim_font - ) - ypos += name_height + self.skin.mini_profile_name_gap - - # Draw badges - _x = 0 - max_x = self.skin.mini_profile_size[0] - xpos - - badges = [self._draw_badge(text) for text in self.data_badges] - for badge in badges: - if badge.width + _x > max_x: - _x = 0 - ypos += badge.height + self.skin.mini_profile_badge_gap - image.paste( - badge, - (xpos + _x, ypos) - ) - _x += badge.width + self.skin.mini_profile_badge_min_sep - return image - - def _draw_badge(self, text) -> Image: - """ - Draw a single profile badge, with the given text. - """ - text_length = self.skin.mini_profile_badge_font.getsize(text)[0] - - height = self.skin.mini_profile_badge_end.height - width = text_length + self.skin.mini_profile_badge_end.width - - badge = Image.new('RGBA', (width, height), color=(0, 0, 0, 0)) - - # Add blobs to ends - badge.paste( - self.skin.mini_profile_badge_end, - (0, 0) - ) - badge.paste( - self.skin.mini_profile_badge_end, - (width - self.skin.mini_profile_badge_end.width, 0) - ) - - # Add rectangle to middle - draw = ImageDraw.Draw(badge) - draw.rectangle( - ( - (self.skin.mini_profile_badge_end.width // 2, 0), - (width - self.skin.mini_profile_badge_end.width // 2, height), - ), - fill='#FFFFFF', - width=0 - ) - badge.paste(ImageColor.getrgb(self.skin.mini_profile_badge_colour), mask=badge) - - # Write badge text - draw.text( - (self.skin.mini_profile_badge_end.width // 2, height // 2), - text, - font=self.skin.mini_profile_badge_font, - fill=self.skin.mini_profile_badge_text_colour, - anchor='lm' - ) - - return badge diff --git a/src/modules/pending-rewrite/gui-cards/monthly.py b/src/modules/pending-rewrite/gui-cards/monthly.py deleted file mode 100644 index 4281b5d1..00000000 --- a/src/modules/pending-rewrite/gui-cards/monthly.py +++ /dev/null @@ -1,750 +0,0 @@ -import math -import calendar -from collections import defaultdict -from PIL import Image, ImageDraw -from datetime import timedelta -import datetime - -from ..base import Card, Layout, fielded, Skin -from ..base.Skin import ( - FieldDesc, - AssetField, RGBAAssetField, BlobField, StringField, NumberField, RawField, - FontField, ColourField, PointField, ComputedField -) - - -@fielded -class MonthlyStatsSkin(Skin): - _env = { - 'scale': 2 # General size scale to match background resolution - } - - background: AssetField = 'monthly/background.png' - - # Header - title_pre_gap: NumberField = 40 - title_text: StringField = "STUDY HOURS" - 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 = 0 - title_gap: NumberField = 10 - - # Top - top_grid_x: NumberField = 37 - top_grid_y: NumberField = 100 - - top_hours_font: FontField = ('Black', 36) - top_hours_colour: ColourField = '#FFFFFF' - - top_hours_bg_mask: AssetField = 'monthly/hours_bg_mask.png' - top_hours_bg_colour: ColourField = '#0B465E' - top_hours_bg_colour_override: ColourField = None - top_hours_bg: BlobField = FieldDesc( - BlobField, - mask_field='top_hours_bg_mask', - colour_field='top_hours_bg_colour', - colour_field_override='top_hours_bg_colour_override' - ) - - top_hours_sep: NumberField = 100 - - top_line_width: NumberField = 10 - top_line_colour: ColourField = '#042231' - - top_date_pre_gap: NumberField = 20 - top_date_font: FontField = ('Light', 25) - top_date_colour: ColourField = '#FFFFFF' - top_date_height: ComputedField = lambda skin: skin.top_date_font.getsize('31')[1] - - top_bar_mask: RGBAAssetField = 'monthly/bar_mask.png' - - top_this_colour: ColourField = '#DDB21D' - top_this_color_override: ColourField = None - - top_last_colour: ColourField = '#377689CC' - top_last_color_override: ColourField = None - - top_this_bar_full: BlobField = FieldDesc( - BlobField, - mask_field='top_bar_mask', - colour_field='top_this_colour', - colour_field_override='top_this_colour_override' - ) - - top_last_bar_full: BlobField = FieldDesc( - BlobField, - mask_field='top_bar_mask', - colour_field='top_last_colour', - colour_field_override='top_last_colour_override' - ) - - top_this_hours_font: FontField = ('Medium', 20) - top_this_hours_colour: ColourField = '#DDB21D' - - top_time_bar_sep: NumberField = 7 - top_time_sep: NumberField = 5 - - top_last_hours_font: FontField = ('Medium', 20) - top_last_hours_colour: ColourField = '#5F91A1' - - top_gap: NumberField = 40 - - weekdays: RawField = ('M', 'T', 'W', 'T', 'F', 'S', 'S') - - # Summary - summary_pre_gap: NumberField = 50 - - summary_mask: AssetField = 'monthly/summary_mask.png' - this_month_image: BlobField = FieldDesc( - BlobField, - mask_field='summary_mask', - colour_field='top_this_colour', - colour_field_override='top_this_colour_override' - ) - this_month_font: FontField = ('Light', 23) - this_month_colour: ColourField = '#BABABA' - - summary_sep: NumberField = 300 - - last_month_font: FontField = ('Light', 23) - last_month_colour: ColourField = '#BABABA' - last_month_image: BlobField = FieldDesc( - BlobField, - mask_field='summary_mask', - colour_field='top_last_colour', - colour_field_override='top_last_colour_override' - ) - - summary_gap: NumberField = 50 - - # Bottom - bottom_frame: AssetField = 'monthly/bottom_frame.png' - bottom_margins: PointField = (100, 100) - - heatmap_mask: AssetField = 'monthly/heatmap_blob_mask.png' - heatmap_empty_colour: ColourField = "#082534" - heatmap_empty_colour_override: ColourField = None - heatmap_empty: BlobField = FieldDesc( - BlobField, - mask_field='heatmap_mask', - colour_field='heatmap_empty_colour', - colour_field_override='heatmap_empty_colour_override' - ) - heatmap_colours: RawField = [ - '#0E2A77', - '#15357D', - '#1D3F82', - '#244A88', - '#2C548E', - '#335E93', - '#3B6998', - '#43729E', - '#4B7CA3', - '#5386A8', - '#5B8FAD', - '#6398B2', - '#6BA1B7', - '#73A9BC', - '#7CB1C1', - '#85B9C5', - ] - heatmap_colours.reverse() - - weekday_background_mask: AssetField = 'monthly/weekday_mask.png' - weekday_background_colour: ColourField = '#60606038' - weekday_background_colour_override: ColourField = None - weekday_background: BlobField = FieldDesc( - BlobField, - mask_field='weekday_background_mask', - colour_field='weekday_background_colour', - colour_field_override='weekday_background_colour_override' - ) - - weekday_font: FontField = ('Black', 26.85) - weekday_colour: ColourField = '#FFFFFF' - weekday_sep: NumberField = 20 - - month_background_mask: AssetField = 'monthly/month_mask.png' - month_background_colour: ColourField = '#60606038' - month_background_colour_override: ColourField = None - month_background: BlobField = FieldDesc( - BlobField, - mask_field='month_background_mask', - colour_field='month_background_colour', - colour_field_override='month_background_colour_override' - ) - month_font: FontField = ('Bold', 25.75) - month_colour: ColourField = '#FFFFFF' - month_sep: ComputedField = lambda skin: ( - skin.bottom_frame.width - 2 * skin.bottom_margins[0] - - skin.weekday_background.width - - skin.weekday_sep - - 4 * skin.month_background.width - ) // 3 - month_gap: NumberField = 25 - - btm_grid_x: ComputedField = lambda skin: (skin.month_background.width - skin.heatmap_mask.width) // 5 - btm_grid_y: ComputedField = lambda skin: skin.btm_grid_x - - # Stats - stats_key_font: FontField = ('Medium', 23.65) - stats_key_colour: ColourField = '#FFFFFF' - stats_value_font: FontField = ('Light', 23.65) - stats_value_colour: ColourField = '#808080' - stats_sep: ComputedField = lambda skin: ( - skin.month_background.width + skin.month_sep + (skin.weekday_background.width + skin.weekday_sep) // 3 - ) - - # Date text - footer_font: FontField = ('Bold', 28) - footer_colour: ColourField = '#6f6e6f' - footer_gap: NumberField = 50 - - -# TODO: Month hour bars.. Blobasset full bars and use them as masks, e.g. profile progress bar. - -class MonthlyStatsPage(Layout): - def __init__(self, skin, name, discrim, sessions, date, current_streak, longest_streak, first_session_start): - """ - `sessions` is a list of study sessions from the last two weeks. - """ - self.skin = skin - - self.data_sessions = sessions - self.data_date = date - - self.data_name = name - self.data_discrim = discrim - - self.current_streak = current_streak - self.longest_streak = longest_streak - - self.month_start = date.replace(day=1) - - self.data_time = defaultdict(int) - - for start, end in sessions: - day_start = start.replace(hour=0, minute=0, second=0, microsecond=0) - day_end = day_start + timedelta(hours=24) - - if end > day_end: - self.data_time[day_start.date()] += (day_end - start).total_seconds() - self.data_time[day_end.date()] += (end - day_end).total_seconds() - else: - self.data_time[day_start.date()] += (end - start).total_seconds() - - self.this_month_days = calendar.monthrange(self.month_start.year, self.month_start.month)[1] - self.hours_this_month = [ - self.data_time[self.month_start + timedelta(days=i)] / 3600 - for i in range(0, self.this_month_days) - ] - - self.months = [self.month_start] - for i in range(0, 3): - self.months.append((self.months[-1] - timedelta(days=1)).replace(day=1)) - - self.months.reverse() - - last_month_start = self.months[-2] - last_month_days = calendar.monthrange(last_month_start.year, last_month_start.month)[1] - self.hours_last_month = [ - self.data_time[last_month_start + timedelta(days=i)] / 3600 - for i in range(0, last_month_days) - ][:self.this_month_days] # Truncate to this month length - - max_hours = max(*self.hours_this_month, *self.hours_last_month) - - self.max_hour_label = (4 * math.ceil(max_hours / 4)) or 4 - - self.days_learned = sum(val != 0 for val in self.data_time.values()) - self.total_days = sum( - calendar.monthrange(month.year, month.month)[1] - for month in self.months - ) - self.days_since_start = min( - (date - first_session_start.date()).days, - (date - self.months[0]).days - ) + 1 - self.average_time = (sum(self.data_time.values()) / self.days_learned) if self.days_learned else 0 - - self.image = None - - def draw(self) -> Image: - image = self.image = self.skin.background - 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 - title_size = self.skin.title_font.getsize(self.skin.title_text) - ypos += title_size[1] + self.skin.title_underline_gap - # draw.line( - # (xpos, ypos, xpos + 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 top box - top = self.draw_top() - image.alpha_composite( - top, - ((image.width - top.width) // 2, ypos) - ) - - ypos += top.height + self.skin.top_gap - - # Draw the summaries - summary_image = self.draw_summaries() - image.alpha_composite( - summary_image, - ((image.width - summary_image.width) // 2, ypos) - ) - ypos += summary_image.height + self.skin.summary_gap - - # Draw the bottom box - bottom = self.draw_bottom() - image.alpha_composite( - bottom, - ((image.width - bottom.width) // 2, ypos) - ) - - # Draw the footer - ypos = image.height - ypos -= self.skin.footer_gap - date_text = self.data_date.strftime( - "Monthly Statistics • 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 - ) - return image - - def draw_summaries(self) -> Image: - this_month_text = " THIS MONTH: {} Hours".format(int(sum(self.hours_this_month))) - this_month_length = int(self.skin.this_month_font.getlength(this_month_text)) - last_month_text = " LAST MONTH: {} Hours".format(int(sum(self.hours_last_month))) - last_month_length = int(self.skin.last_month_font.getlength(last_month_text)) - - image = Image.new( - 'RGBA', - ( - self.skin.this_month_image.width + this_month_length - + self.skin.summary_sep - + self.skin.last_month_image.width + last_month_length, - self.skin.this_month_image.height - ) - ) - draw = ImageDraw.Draw(image) - - xpos = 0 - ypos = image.height // 2 - image.alpha_composite( - self.skin.this_month_image, - (0, 0) - ) - xpos += self.skin.this_month_image.width - draw.text( - (xpos, ypos), - this_month_text, - fill=self.skin.this_month_colour, - font=self.skin.this_month_font, - anchor='lm' - ) - - xpos += self.skin.summary_sep + this_month_length - - image.alpha_composite( - self.skin.last_month_image, - (xpos, 0) - ) - xpos += self.skin.last_month_image.width - draw.text( - (xpos, ypos), - last_month_text, - fill=self.skin.last_month_colour, - font=self.skin.last_month_font, - anchor='lm' - ) - return image - - def draw_top(self) -> Image: - size_x = ( - self.skin.top_hours_bg.width // 2 + self.skin.top_hours_sep - + (self.this_month_days - 1) * self.skin.top_grid_x + self.skin.top_bar_mask.width // 2 - + self.skin.top_hours_bg.width // 2 - ) - size_y = ( - self.skin.top_hours_bg.height // 2 + 4 * self.skin.top_grid_y + self.skin.top_date_pre_gap - + self.skin.top_date_height - + self.skin.top_time_bar_sep + int(self.skin.top_this_hours_font.getlength('24 H 24 H')) - ) - image = Image.new('RGBA', (size_x, size_y)) - draw = ImageDraw.Draw(image) - - x0 = self.skin.top_hours_bg.width // 2 + self.skin.top_hours_sep - y0 = self.skin.top_hours_bg.height // 2 + 4 * self.skin.top_grid_y - y0 += self.skin.top_time_bar_sep + int(self.skin.top_this_hours_font.getlength('24 H 24 H')) - - # Draw lines and numbers - labels = list(int(i * self.max_hour_label // 4) for i in range(0, 5)) - - xpos = x0 - self.skin.top_hours_sep - ypos = y0 - for label in labels: - draw.line( - ((xpos, ypos), (image.width, ypos)), - width=self.skin.top_line_width, - fill=self.skin.top_line_colour - ) - - image.alpha_composite( - self.skin.top_hours_bg, - (xpos - self.skin.top_hours_bg.width // 2, ypos - self.skin.top_hours_bg.height // 2) - ) - text = str(label) - draw.text( - (xpos, ypos), - text, - fill=self.skin.top_hours_colour, - font=self.skin.top_hours_font, - anchor='mm' - ) - ypos -= self.skin.top_grid_y - - # Draw dates - xpos = x0 - ypos = y0 + self.skin.top_date_pre_gap - for i in range(1, self.this_month_days + 1): - draw.text( - (xpos, ypos), - str(i), - fill=self.skin.top_date_colour, - font=self.skin.top_date_font, - anchor='mt' - ) - xpos += self.skin.top_grid_x - - # Draw bars - for i, (last_hours, this_hours) in enumerate(zip(self.hours_last_month, self.hours_this_month)): - xpos = x0 + i * self.skin.top_grid_x - - if not (last_hours or this_hours): - continue - - bar_height = 0 - for draw_last in (last_hours > this_hours, not last_hours > this_hours): - hours = last_hours if draw_last else this_hours - height = (4 * self.skin.top_grid_y) * (hours / self.max_hour_label) - height = int(height) - - if height >= self.skin.top_bar_mask.width: - bar = self.draw_vertical_bar( - height, - self.skin.top_last_bar_full if draw_last else self.skin.top_this_bar_full, - self.skin.top_bar_mask - ) - bar_height = max(height, bar_height) - image.alpha_composite( - bar, - (xpos - bar.width // 2, y0 - bar.height) - ) - - # Draw text - if bar_height: - text = ['{} H'.format(hours) for hours in (last_hours, this_hours) if hours] - text_size = self.skin.top_this_hours_font.getsize(' '.join(text)) - text_image = Image.new( - 'RGBA', - text_size - ) - text_draw = ImageDraw.Draw(text_image) - txpos = 0 - if last_hours: - last_text = "{} H ".format(int(last_hours)) - text_draw.text( - (txpos, 0), last_text, - fill=self.skin.top_last_hours_colour, - font=self.skin.top_last_hours_font - ) - txpos += self.skin.top_last_hours_font.getlength(last_text) - if this_hours: - this_text = "{} H ".format(int(this_hours)) - text_draw.text( - (txpos, 0), this_text, - fill=self.skin.top_this_hours_colour, - font=self.skin.top_this_hours_font - ) - - text_image = text_image.rotate(90, expand=True) - text_image = text_image.crop(text_image.getbbox()) - - image.alpha_composite( - text_image, - (xpos - text_image.width // 2, - y0 - bar_height - self.skin.top_time_bar_sep - text_image.height) - ) - - return image - - def draw_vertical_bar(self, height, full_bar, mask_bar, crop=False): - y_2 = mask_bar.height - y_1 = height - - image = Image.new('RGBA', full_bar.size) - image.paste(mask_bar, (0, y_2 - y_1), mask=mask_bar) - image.paste(full_bar, mask=image) - - if crop: - image = image.crop( - (0, y_2 - y_1), (image.width, y_2 - y_1), - (image.height, 0), (image.height, image.width) - ) - - return image - - def draw_bottom(self) -> Image: - image = self.skin.bottom_frame - draw = ImageDraw.Draw(image) - - xpos, ypos = self.skin.bottom_margins - - # Draw the weekdays - y0 = self.skin.month_background.height + self.skin.month_gap - for i, weekday in enumerate(self.skin.weekdays): - y = y0 + i * self.skin.btm_grid_y - image.alpha_composite( - self.skin.weekday_background, - (xpos, ypos + y) - ) - draw.text( - (xpos + self.skin.weekday_background.width // 2, ypos + y + self.skin.weekday_background.height // 2), - weekday, - fill=self.skin.weekday_colour, - font=self.skin.weekday_font, - anchor='mm' - ) - - # Draw the months - x0 = self.skin.weekday_background.width + self.skin.weekday_sep - for i, date in enumerate(self.months): - name = date.strftime('%B').upper() - - x = x0 + i * (self.skin.month_background.width + self.skin.month_sep) - image.alpha_composite( - self.skin.month_background, - (xpos + x, ypos) - ) - draw.text( - (xpos + x + self.skin.month_background.width // 2, - ypos + self.skin.month_background.height // 2), - name, - fill=self.skin.month_colour, - font=self.skin.month_font, - anchor='mm' - ) - - heatmap = self.draw_month_heatmap(date) - image.alpha_composite( - heatmap, - (xpos + x + self.skin.month_background.width // 2 - heatmap.width // 2, ypos + y0) - ) - - # Draw the streak and stats information - x = xpos + self.skin.weekday_background.width // 2 - y = image.height - self.skin.bottom_margins[1] - - key_text = "Current streak: " - key_len = self.skin.stats_key_font.getlength(key_text) - value_text = "{} day{}".format( - self.current_streak, - 's' if self.current_streak != 1 else '' - ) - draw.text( - (x, y), - key_text, - font=self.skin.stats_key_font, - fill=self.skin.stats_key_colour - ) - draw.text( - (x + key_len, y), - value_text, - font=self.skin.stats_value_font, - fill=self.skin.stats_value_colour - ) - x += self.skin.stats_sep - - key_text = "Daily average: " - key_len = self.skin.stats_key_font.getlength(key_text) - value_text = "{} hour{}".format( - (hours := int(self.average_time // 3600)), - 's' if hours != 1 else '' - ) - draw.text( - (x, y), - key_text, - font=self.skin.stats_key_font, - fill=self.skin.stats_key_colour - ) - draw.text( - (x + key_len, y), - value_text, - font=self.skin.stats_value_font, - fill=self.skin.stats_value_colour - ) - x += self.skin.stats_sep - - key_text = "Longest streak: " - key_len = self.skin.stats_key_font.getlength(key_text) - value_text = "{} day{}".format( - self.longest_streak, - 's' if self.current_streak != 1 else '' - ) - draw.text( - (x, y), - key_text, - font=self.skin.stats_key_font, - fill=self.skin.stats_key_colour - ) - draw.text( - (x + key_len, y), - value_text, - font=self.skin.stats_value_font, - fill=self.skin.stats_value_colour - ) - x += self.skin.stats_sep - - key_text = "Days learned: " - key_len = self.skin.stats_key_font.getlength(key_text) - value_text = "{}%".format( - int((100 * self.days_learned) // self.days_since_start) - ) - draw.text( - (x, y), - key_text, - font=self.skin.stats_key_font, - fill=self.skin.stats_key_colour - ) - draw.text( - (x + key_len, y), - value_text, - font=self.skin.stats_value_font, - fill=self.skin.stats_value_colour - ) - x += self.skin.stats_sep - - return image - - def draw_month_heatmap(self, month_start) -> Image: - cal = calendar.monthcalendar(month_start.year, month_start.month) - columns = len(cal) - - size_x = ( - (columns - 1) * self.skin.btm_grid_x - + self.skin.heatmap_mask.width - ) - size_y = ( - 6 * self.skin.btm_grid_y + self.skin.heatmap_mask.height - ) - - image = Image.new('RGBA', (size_x, size_y)) - - x0 = self.skin.heatmap_mask.width // 2 - y0 = self.skin.heatmap_mask.height // 2 - - for (i, week) in enumerate(cal): - xpos = x0 + i * self.skin.btm_grid_x - for (j, day) in enumerate(week): - if day: - ypos = y0 + j * self.skin.btm_grid_y - date = datetime.date(month_start.year, month_start.month, day) - time = self.data_time[date] - bubble = self.draw_bubble(time) - image.alpha_composite( - bubble, - (xpos - bubble.width // 2, ypos - bubble.width // 2) - ) - - return image - - def draw_bubble(self, time): - # Calculate colour level - if time == 0: - image = self.skin.heatmap_empty - colour = self.skin.heatmap_empty_colour - else: - amount = min((time / self.average_time) if self.average_time else 0, 2) / 2 - index = math.ceil(amount * len(self.skin.heatmap_colours)) - 1 - colour = self.skin.heatmap_colours[index] - - image = Image.new('RGBA', self.skin.heatmap_mask.size) - image.paste(colour, mask=self.skin.heatmap_mask) - return image - - -class MonthlyStatsCard(Card): - route = "monthly_stats_card" - card_id = "monthly_stats" - - layout = MonthlyStatsPage - skin = MonthlyStatsSkin - - display_name = "Monthly Stats" - - @classmethod - async def sample_args(cls, ctx, **kwargs): - import random - from datetime import timezone, datetime, timedelta - - sessions = [] - streak = 0 - longest_streak = 0 - day_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) - day_start -= timedelta(hours=24) * 120 - for day in range(0, 120): - day_start += timedelta(hours=24) - - roll = random.randint(0, 30) - if roll == 0: - longest_streak = max(streak, longest_streak) - streak = 0 - continue - else: - streak += 1 - - # start of day - pointer = 6 * 60 - session_duration = int(abs(random.normalvariate(8 * 60, 2 * 60))) - sessions.append(( - day_start + timedelta(minutes=pointer), - day_start + timedelta(minutes=(pointer + session_duration)), - ) - ) - longest_streak = max(streak, longest_streak) - - return { - 'name': ctx.author.name if ctx else "John Doe", - 'discrim': ('#' + ctx.author.discriminator) if ctx else "#0000", - 'sessions': sessions, - 'date': datetime.now(timezone.utc).date(), - 'current_streak': streak, - 'longest_streak': longest_streak, - 'first_session_start': day_start - timedelta(days=200) - } diff --git a/src/modules/pending-rewrite/gui-cards/profile.py b/src/modules/pending-rewrite/gui-cards/profile.py deleted file mode 100644 index a53fa51e..00000000 --- a/src/modules/pending-rewrite/gui-cards/profile.py +++ /dev/null @@ -1,623 +0,0 @@ -from io import BytesIO -from PIL import Image, ImageDraw - -from ..utils import get_avatar_key -from ..base import Card, Layout, fielded, Skin, FieldDesc -from ..base.Avatars import avatar_manager -from ..base.Skin import ( - AssetField, RGBAAssetField, AssetPathField, NumberField, BlobField, - FontField, ColourField, PointField, ComputedField -) - - -@fielded -class ProfileSkin(Skin): - _env = { - 'scale': 2 # General size scale to match background resolution - } - - # Background images - bg_path: AssetField = "profile/background.png" - - # Inner container - container_position: PointField = (70, 65) # Position of top left corner - container_size: PointField = (1400, 600) # Size of the inner container - - # Header - header_font: FontField = ('BlackItalic', 28) - header_colour_1: ColourField = '#DDB21D' - header_colour_2: ColourField = '#BABABA' - header_gap: NumberField = 35 - header_height: ComputedField = lambda skin: skin.header_font.getsize("USERNAME #0000")[1] - - # Column 1 - avatar_mask: AssetField = FieldDesc(AssetField, 'profile/avatar_mask.png', convert=None) - avatar_outline: AssetField = FieldDesc(AssetField, 'profile/avatar_outline.png', convert=None) - avatar_size: ComputedField = lambda skin: skin.avatar_outline.size - avatar_gap: NumberField = 10 - - counter_bg_mask: AssetField = "profile/counter_bg_mask.png" - counter_bg_colour: ColourField = "#515B8D60" - counter_background: BlobField = FieldDesc( - BlobField, - mask_field='counter_bg_mask', - colour_field='counter_bg_colour' - ) - - coin_icon: AssetField = "icons/coin.png" - gem_icon: AssetField = "icons/gem.png" - gift_icon: AssetField = "icons/gift.png" - - counter_font: FontField = ('Black', 14) - counter_colour: ColourField = '#FFFFFF' - counter_icon_align: NumberField = 20 - counter_text_align: NumberField = 40 - - counter_gap: NumberField = 5 - - col1_size: ComputedField = lambda skin: ( - skin.avatar_size[0], - skin.avatar_size[1] + skin.avatar_gap - + 3 * skin.counter_background.height + 2 * skin.counter_gap - ) - - column_sep: NumberField = 20 - - # Column 2 - subheader_font: FontField = ('Black', 27) - subheader_colour: ColourField = '#DDB21D' - subheader_height: ComputedField = lambda skin: skin.subheader_font.getsize('PROFILE')[1] - subheader_gap: NumberField = 15 - - col2_size: ComputedField = lambda skin: ( - skin.container_size[0] - skin.col1_size[0] - skin.column_sep, - skin.container_size[1] - skin.header_gap - skin.header_height - ) - - col2_sep: NumberField = 40 # Minimum separation between profile and achievements - - # Achievement section - achievement_active_path: AssetPathField = 'profile/achievements_active/' - achievement_inactive_path: AssetPathField = 'profile/achievements_inactive/' - achievement_icon_size: PointField = (115, 96) # Individual achievement box size - achievement_gap: NumberField = 10 - achievement_sep: NumberField = 0 - achievement_size: ComputedField = lambda skin: ( - 4 * skin.achievement_icon_size[0] + 3 * skin.achievement_sep, - skin.subheader_height + skin.subheader_gap - + 2 * skin.achievement_icon_size[1] + 1 * skin.achievement_gap - ) - - # Profile section - badge_font: FontField = ('Black', 13) - badge_text_colour: ColourField = '#FFFFFF' - badge_blob_colour: ColourField = '#1473A2' - badge_blob_colour_override: ColourField = None - badge_blob_mask: AssetField = 'profile/badge_end_mask.png' - - badge_end_blob: BlobField = FieldDesc( - BlobField, - mask_field='badge_blob_mask', - colour_field='badge_blob_colour', - colour_override_field='badge_blob_colour_override' - ) - - badge_gap: NumberField = 5 - badge_min_sep: NumberField = 5 - profile_size: ComputedField = lambda skin: ( - skin.col2_size[0] - skin.achievement_size[0] - skin.col2_sep, - skin.subheader_height + skin.subheader_gap - + 4 * skin.badge_end_blob.height + 3 * skin.badge_gap - ) - - # Rank section - rank_name_font: FontField = ('Black', 23) - rank_name_colour: ColourField = '#DDB21D' - rank_name_height: ComputedField = lambda skin: skin.rank_name_font.getsize('VAMPIRE')[1] - rank_hours_font: FontField = ('Light', 18) - rank_hours_colour: ColourField = '#FFFFFF' - - bar_gap: NumberField = 5 - bar_mask: RGBAAssetField = 'profile/progress_mask.png' - bar_full_colour: ColourField = '#DDB21D' - bar_full_colour_override: ColourField = None - bar_full: BlobField = FieldDesc( - BlobField, - mask_field='bar_mask', - colour_field='bar_full_colour', - colour_override_field='bar_full_colour_override' - ) - bar_empty_colour: ColourField = '#2F4858' - bar_empty_colour_override: ColourField = None - bar_empty: BlobField = FieldDesc( - BlobField, - mask_field='bar_mask', - colour_field='bar_empty_colour', - colour_override_field='bar_empty_colour_override' - ) - - next_rank_font: FontField = ('Italic', 15) - next_rank_colour: ColourField = '#FFFFFF' - next_rank_height: ComputedField = lambda skin: skin.next_rank_font.getsize('NEXT RANK:')[1] - - rank_size: ComputedField = lambda skin: ( - skin.col2_size[0], - skin.rank_name_height + skin.bar_gap - + skin.bar_full.height + skin.bar_gap - + skin.next_rank_height + skin.next_rank_height // 2 # Adding skin.height skin.for skin.taller skin.glyphs - ) - - -class ProfileLayout(Layout): - def __init__(self, skin, name, discrim, - coins, time, gems, gifts, - avatar, - badges=(), - achievements=(), - current_rank=None, - next_rank=None, - draft=False, **kwargs): - - self.skin = skin - - self.draft = draft - - self.data_name = name - self.data_discrim = discrim - - self.data_avatar = avatar - - self.data_coins = coins - self.data_time = time - self.data_hours = self.data_time / 3600 - self.data_gems = gems - self.data_gifts = gifts - - self.data_badges = badges - self.data_achievements = achievements - - self.data_current_rank = current_rank - self.data_next_rank = next_rank - - self.image: Image = None # Final Image - - def draw(self): - # Load/copy background - image = self.skin.bg_path - - # Draw inner container - inner_container = self.draw_inner_container() - - # Paste inner container on background - image.alpha_composite(inner_container, self.skin.container_position) - - self.image = image - return image - - def draw_inner_container(self) -> Image: - container = Image.new('RGBA', self.skin.container_size) - draw = ImageDraw.Draw(container) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.container_size[0]-1, self.skin.container_size[1]-1))) - - position = 0 - - # Draw header - xposition = 0 - draw.text( - (xposition, position), - self.data_name, - font=self.skin.header_font, - fill=self.skin.header_colour_1 - ) - xposition += self.skin.header_font.getlength(self.data_name + ' ') - draw.text( - (xposition, position), - self.data_discrim, - font=self.skin.header_font, - fill=self.skin.header_colour_2 - ) - position += self.skin.header_height + self.skin.header_gap - - # Draw column 1 - col1 = self.draw_column_1() - container.alpha_composite(col1, (0, position)) - - # Draw column 2 - col2 = self.draw_column_2() - container.alpha_composite(col2, (container.width - col2.width, position)) - - return container - - def draw_column_1(self) -> Image: - # Create new image for column 1 - col1 = Image.new('RGBA', self.skin.col1_size) - draw = ImageDraw.Draw(col1) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.col1_size[0]-1, self.skin.col1_size[1]-1))) - - # Tracking current drawing height - position = 0 - - # Draw avatar - _avatar = self.data_avatar - - # Mask the avatar image to the desired shape - _avatar.paste((0, 0, 0, 0), mask=self.skin.avatar_mask) - - # Place the image on a larger canvas - avatar_image = Image.new('RGBA', self.skin.avatar_outline.size) - avatar_image.paste( - _avatar, - ( - (self.skin.avatar_outline.width - _avatar.width) // 2, - (self.skin.avatar_outline.height - _avatar.height) // 2, - ) - ) - - # Add the outline over the masked avatar - avatar_image.alpha_composite(self.skin.avatar_outline) - - # Paste onto column - col1.alpha_composite( - avatar_image, - (0, position) - ) - position += self.skin.avatar_size[1] + self.skin.avatar_gap - - # Draw counters - counters = ( - (self.skin.coin_icon, self.data_coins), - (self.skin.gem_icon, self.data_gems), - (self.skin.gift_icon, self.data_gifts), - ) - for icon, amount in counters: - col1.alpha_composite( - self.draw_counter(icon, amount), - ((self.skin.avatar_outline.width - self.skin.counter_background.width) // 2, position) - ) - position += self.skin.counter_background.height + self.skin.counter_gap - - return col1 - - def draw_counter(self, icon, amount): - image = self.skin.counter_background.copy() - draw = ImageDraw.Draw(image) - - image.alpha_composite( - icon, - ( - self.skin.counter_icon_align - icon.width // 2, - (self.skin.counter_background.height - icon.height) // 2 - ) - ) - - draw.text( - (self.skin.counter_text_align, self.skin.counter_background.height // 2), - f"{amount:,}", - font=self.skin.counter_font, - fill=self.skin.counter_colour, - anchor='lm' - ) - return image - - def draw_column_2(self) -> Image: - # Create new image for column 1 - col2 = Image.new('RGBA', self.skin.col2_size) - draw = ImageDraw.Draw(col2) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.col2_size[0]-1, self.skin.col2_size[1]-1))) - - # Tracking current drawing position - position = 0 - xposition = 0 - - # Draw Profile box - profile = self.draw_profile() - col2.paste( - profile, - (xposition, position) - ) - xposition += profile.width + self.skin.col2_sep - - # Draw Achievements box - achievements = self.draw_achievements() - col2.paste( - achievements, - (xposition, position) - ) - - # Draw ranking box - position = self.skin.col2_size[1] - self.skin.rank_size[1] - - ranking = self.draw_rank() - col2.alpha_composite( - ranking, - (0, position) - ) - - return col2 - - def draw_profile(self) -> Image: - profile = Image.new('RGBA', self.skin.profile_size) - draw = ImageDraw.Draw(profile) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.profile_size[0]-1, self.skin.profile_size[1]-1))) - - position = 0 - - # Draw subheader - draw.text( - (0, position), - 'PROFILE', - font=self.skin.subheader_font, - fill=self.skin.subheader_colour - ) - position += self.skin.subheader_height + self.skin.subheader_gap - - # Draw badges - # TODO: Nicer/Smarter layout - xposition = 0 - max_x = self.skin.profile_size[0] - - badges = [self.draw_badge(text) for text in self.data_badges] - for badge in badges: - if badge.width + xposition > max_x: - xposition = 0 - position += badge.height + self.skin.badge_gap - profile.paste( - badge, - (xposition, position) - ) - xposition += badge.width + self.skin.badge_min_sep - - return profile - - def draw_badge(self, text) -> Image: - """ - Draw a single profile badge, with the given text. - """ - text_length = self.skin.badge_font.getsize(text)[0] - - height = self.skin.badge_end_blob.height - width = text_length + self.skin.badge_end_blob.width - - badge = Image.new('RGBA', (width, height), color=(0, 0, 0, 0)) - - # Add blobs to ends - badge.paste( - self.skin.badge_end_blob, - (0, 0) - ) - badge.paste( - self.skin.badge_end_blob, - (width - self.skin.badge_end_blob.width, 0) - ) - - # Add rectangle to middle - draw = ImageDraw.Draw(badge) - draw.rectangle( - ( - (self.skin.badge_end_blob.width // 2, 0), - (width - self.skin.badge_end_blob.width // 2, height), - ), - fill=self.skin.badge_blob_colour, - width=0 - ) - - # Write badge text - draw.text( - (self.skin.badge_end_blob.width // 2, height // 2), - text, - font=self.skin.badge_font, - fill=self.skin.badge_text_colour, - anchor='lm' - ) - - return badge - - def draw_achievements(self) -> Image: - achievements = Image.new('RGBA', self.skin.achievement_size) - draw = ImageDraw.Draw(achievements) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.achievement_size[0]-1, self.skin.achievement_size[1]-1))) - - position = 0 - - # Draw subheader - draw.text( - (0, position), - 'ACHIEVEMENTS', - font=self.skin.subheader_font, - fill=self.skin.subheader_colour - ) - position += self.skin.subheader_height + self.skin.subheader_gap - xposition = 0 - - for i in range(0, 8): - # Top left corner of grid box - nxpos = (i % 4) * (self.skin.achievement_icon_size[0] + self.skin.achievement_sep) - nypos = (i // 4) * (self.skin.achievement_icon_size[1] + self.skin.achievement_gap) - - # Choose the active or inactive icon as given by data - icon_path = "{}{}.png".format( - self.skin.achievement_active_path if (i in self.data_achievements) else self.skin.achievement_inactive_path, - i + 1 - ) - icon = Image.open(icon_path).convert('RGBA') - - # Offset to top left corner of pasted icon - xoffset = (self.skin.achievement_icon_size[0] - icon.width) // 2 - # xoffset = 0 - yoffset = self.skin.achievement_icon_size[1] - icon.height - - # Paste the icon - achievements.alpha_composite( - icon, - (xposition + nxpos + xoffset, position + nypos + yoffset) - ) - - return achievements - - def draw_rank(self) -> Image: - rank = Image.new('RGBA', self.skin.rank_size) - draw = ImageDraw.Draw(rank) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.rank_size[0]-1, self.skin.rank_size[1]-1))) - - position = 0 - - # Draw the current rank - if self.data_current_rank: - rank_name, hour_1, hour_2 = self.data_current_rank - - xposition = 0 - draw.text( - (xposition, position), - rank_name, - font=self.skin.rank_name_font, - fill=self.skin.rank_name_colour, - ) - name_size = self.skin.rank_name_font.getsize(rank_name + ' ') - position += name_size[1] - xposition += name_size[0] - - if hour_2: - progress = (self.data_hours - hour_1) / (hour_2 - hour_1) - if hour_1: - hour_str = '{} - {}h'.format(hour_1, hour_2) - else: - hour_str = '≤{}h'.format(hour_2) - else: - progress = 1 - hour_str = '≥{}h'.format(hour_1) - - draw.text( - (xposition, position), - hour_str, - font=self.skin.rank_hours_font, - fill=self.skin.rank_hours_colour, - anchor='lb' - ) - position += self.skin.bar_gap - else: - draw.text( - (0, position), - 'UNRANKED', - font=self.skin.rank_name_font, - fill=self.skin.rank_name_colour, - ) - position += self.skin.rank_name_height + self.skin.bar_gap - progress = 0 - - # Draw rankbar - rankbar = self.draw_rankbar(progress) - rank.alpha_composite( - rankbar, - (0, position) - ) - position += rankbar.height + self.skin.bar_gap - - # Draw next rank text - if self.data_next_rank: - rank_name, hour_1, hour_2 = self.data_next_rank - if hour_2: - if hour_1: - hour_str = '{} - {}h'.format(hour_1, hour_2) - else: - hour_str = '≤{}h'.format(hour_2) - else: - hour_str = '≥{}h'.format(hour_1) - rank_str = "NEXT RANK: {} {}".format(rank_name, hour_str) - else: - if self.data_current_rank: - rank_str = "YOU HAVE REACHED THE MAXIMUM RANK!" - else: - rank_str = "NO RANKS AVAILABLE!" - - draw.text( - (0, position), - rank_str, - font=self.skin.next_rank_font, - fill=self.skin.next_rank_colour, - ) - - return rank - - def draw_rankbar(self, progress: float) -> Image: - """ - Draw the rank progress bar with the given progress filled. - `progress` should be given as a proportion between `0` and `1`. - """ - # Ensure sane values - progress = min(progress, 1) - progress = max(progress, 0) - - if progress == 0: - return self.skin.bar_empty - elif progress == 1: - return self.skin.bar_full - else: - _bar = self.skin.bar_empty - x = -1 * int((1 - progress) * self.skin.bar_full.width) - _bar.paste( - self.skin.bar_full, - (x, 0), - mask=self.skin.bar_mask - ) - bar = Image.new('RGBA', _bar.size) - bar.paste( - _bar, - mask=self.skin.bar_mask - ) - return bar - - -class ProfileCard(Card): - route = 'profile_card' - card_id = 'profile' - - layout = ProfileLayout - skin = ProfileSkin - - display_name = "User Profile" - - @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 sample_args(cls, ctx, **kwargs): - return { - 'name': ctx.author.name if ctx else 'John Doe', - 'discrim': ('#' + ctx.author.discriminator) if ctx else '#0000', - 'avatar': get_avatar_key(ctx.client, ctx.author.id) if ctx else (0, None), - 'coins': 58596, - 'time': 3750 * 3600, - 'gems': 10000, - 'gifts': 100, - 'badges': ( - 'STUDYING: MEDICINE', - 'HOBBY: MATHS', - 'CAREER: STUDENT', - 'FROM: EUROPE', - 'LOVES CATS <3' - ), - 'achievements': (0, 2, 5, 7), - 'current_rank': ('VAMPIRE', 3000, 4000), - 'next_rank': ('WIZARD', 4000, 8000), - } diff --git a/src/modules/pending-rewrite/gui-cards/stats.py b/src/modules/pending-rewrite/gui-cards/stats.py deleted file mode 100644 index 4492cf1c..00000000 --- a/src/modules/pending-rewrite/gui-cards/stats.py +++ /dev/null @@ -1,508 +0,0 @@ -import itertools -from datetime import datetime, timedelta -from PIL import Image, ImageDraw - -from ..base import Card, Layout, fielded, Skin -from ..base.Skin import ( - AssetField, BlobField, StringField, NumberField, RawField, - FontField, ColourField, PointField, ComputedField, FieldDesc -) - - -def format_lb(pos): - """ - Format a leaderboard position into a string. - """ - if pos is None: - return 'Unranked' - - if pos % 10 == 1 and pos % 100 != 11: - return f"{pos}ST" - - if pos % 10 == 2 and pos % 100 != 12: - return f"{pos}ND" - - if pos % 10 == 3 and pos % 100 != 13: - return f"{pos}RD" - - return f"{pos}TH" - - -def format_time(seconds): - return "{:02}:{:02}".format( - int(seconds // 3600), - int(seconds % 3600 // 60) - ) - - -@fielded -class StatsSkin(Skin): - _env = { - 'scale': 2 # General size scale to match background resolution - } - - # Background images - background: AssetField = "stats/background.png" - - # Inner container - container_position: PointField = (60, 50) # Position of top left corner - container_size: PointField = (1410, 800) # Size of the inner container - - # Major (topmost) header - header_font: FontField = ('Black', 27) - header_colour: ColourField = '#DDB21D' - header_gap: NumberField = 35 # Gap between header and column contents - header_height: ComputedField = lambda skin: skin.header_font.getsize('STATISTICS')[1] - - # First column - col1_header: StringField = 'STATISTICS' - stats_subheader_pregap: NumberField = 8 - stats_subheader_font: FontField = ('Black', 21) - stats_subheader_colour: ColourField = '#FFFFFF' - stats_subheader_size: ComputedField = lambda skin: skin.stats_subheader_font.getsize('LEADERBOARD POSITION') - stats_text_gap: NumberField = 13 # Gap between stat lines - stats_text_font: FontField = ('SemiBold', 19) - stats_text_height: ComputedField = lambda skin: skin.stats_text_font.getsize('DAILY')[1] - stats_text_colour: ColourField = '#BABABA' - - col1_size: ComputedField = lambda skin: ( - skin.stats_subheader_size[0], - skin.header_height + skin.header_gap - + 3 * skin.stats_subheader_size[1] - + 2 * skin.stats_subheader_pregap - + 6 * skin.stats_text_height - + 8 * skin.stats_text_gap - ) - - # Second column - col2_header: StringField = 'STUDY STREAK' - col2_date_font: FontField = ('Black', 21) - col2_date_colour: ColourField = '#FFFFFF' - col2_hours_colour: ColourField = '#1473A2' - col2_date_gap: NumberField = 25 # Gap between date line and calender - col2_subheader_height: ComputedField = lambda skin: skin.col2_date_font.getsize('JANUARY')[1] - cal_column_sep: NumberField = 35 - cal_weekday_text: RawField = ('S', 'M', 'T', 'W', 'T', 'F', 'S') - cal_weekday_font: FontField = ('ExtraBold', 21) - cal_weekday_colour: ColourField = '#FFFFFF' - cal_weekday_height: ComputedField = lambda skin: skin.cal_weekday_font.getsize('S')[1] - cal_weekday_gap: NumberField = 23 - cal_number_font: FontField = ('Medium', 20) - cal_number_end_colour: ColourField = '#BABABA' - cal_number_colour: ColourField = '#BABABA' - cal_number_gap: NumberField = 28 - alt_cal_number_gap: NumberField = 20 - cal_number_size: ComputedField = lambda skin: skin.cal_number_font.getsize('88') - - cal_streak_mask: AssetField = 'stats/streak_mask.png' - - cal_streak_end_colour: ColourField = '#1473A2' - cal_streak_end_colour_override: ColourField = None - cal_streak_end: BlobField = FieldDesc( - BlobField, - mask_field='cal_streak_mask', - colour_field='cal_streak_end_colour', - colour_override_field='cal_streak_end_colour_override' - ) - - cal_streak_middle_colour: ColourField = '#1B3343' - cal_streak_middle_colour_override: ColourField = None - cal_streak_middle: BlobField = FieldDesc( - BlobField, - mask_field='cal_streak_mask', - colour_field='cal_streak_middle_colour', - colour_override_field='cal_streak_middle_colour_override' - ) - - cal_size: ComputedField = lambda skin: ( - 7 * skin.cal_number_size[0] + 6 * skin.cal_column_sep + skin.cal_streak_end.width // 2, - 5 * skin.cal_number_size[1] + 4 * skin.cal_number_gap - + skin.cal_weekday_height + skin.cal_weekday_gap - + skin.cal_streak_end.height // 2 - ) - - alt_cal_size: ComputedField = lambda skin: ( - 7 * skin.cal_number_size[0] + 6 * skin.cal_column_sep + skin.cal_streak_end.width // 2, - 6 * skin.cal_number_size[1] + 5 * skin.alt_cal_number_gap - + skin.cal_weekday_height + skin.cal_weekday_gap - + skin.cal_streak_end.height // 2 - ) - - col2_size: ComputedField = lambda skin: ( - skin.cal_size[0], - skin.header_height + skin.header_gap - + skin.col2_subheader_height + skin.col2_date_gap - + skin.cal_size[1] - ) - - alt_col2_size: ComputedField = lambda skin: ( - skin.alt_cal_size[0], - skin.header_height + skin.header_gap - + skin.col2_subheader_height + skin.col2_date_gap - + skin.alt_cal_size[1] - ) - - -class StatsLayout(Layout): - def __init__(self, skin, lb_data, time_data, workouts, streak_data, date=None, draft=False, **kwargs): - self.draft = draft - - self.skin = skin - - self.data_lb_time = lb_data[0] # Position on time leaderboard, or None - self.data_lb_lc = lb_data[1] # Position on coin leaderboard, or None - - self.data_time_daily = int(time_data[0]) # Daily time in seconds - self.data_time_weekly = int(time_data[1]) # Weekly time in seconds - self.data_time_monthly = int(time_data[2]) # Monthly time in seconds - self.data_time_all = int(time_data[3]) # All time in seconds - - self.data_workouts = workouts # Number of workout sessions - self.data_streaks = streak_data # List of streak day ranges to show - - # Extract date info - date = date if date else datetime.today() # Date to show for month/year - month_first_day = date.replace(day=1) - month_days = (month_first_day.replace(month=(month_first_day.month % 12) + 1) - timedelta(days=1)).day - - self.date = date - self.month = date.strftime('%B').upper() - self.first_weekday = month_first_day.weekday() # Which weekday the month starts on - self.month_days = month_days - self.alt_layout = (month_days + self.first_weekday + 1) > 35 # Whether to use the alternate layout - - if self.alt_layout: - self.skin.fields['cal_number_gap'].value = self.skin.alt_cal_number_gap - self.skin.fields['cal_size'].value = self.skin.alt_cal_size - self.skin.fields['col2_size'].value = self.skin.alt_col2_size - - self.image: Image = None # Final Image - - def draw(self): - # Load/copy background - image = self.skin.background - - # Draw inner container - inner_container = self.draw_inner_container() - - # Paste inner container on background - image.alpha_composite(inner_container, self.skin.container_position) - - self.image = image - return image - - def draw_inner_container(self): - container = Image.new('RGBA', self.skin.container_size) - - col1 = self.draw_column_1() - col2 = self.draw_column_2() - - container.alpha_composite(col1) - container.alpha_composite(col2, (container.width - col2.width, 0)) - - if self.draft: - draw = ImageDraw.Draw(container) - draw.rectangle(((0, 0), (self.skin.container_size[0]-1, self.skin.container_size[1]-1))) - - return container - - def draw_column_1(self) -> Image: - # Create new image for column 1 - col1 = Image.new('RGBA', self.skin.col1_size) - draw = ImageDraw.Draw(col1) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.col1_size[0]-1, self.skin.col1_size[1]-1))) - - # Tracking current drawing height - position = 0 - - # Write header - draw.text( - (0, position), - self.skin.col1_header, - font=self.skin.header_font, - fill=self.skin.header_colour - ) - position += self.skin.header_height + self.skin.header_gap - - # Leaderboard - draw.text( - (0, position), - 'LEADERBOARD POSITION', - font=self.skin.stats_subheader_font, - fill=self.skin.stats_subheader_colour - ) - position += self.skin.col2_subheader_height + self.skin.stats_text_gap - - draw.text( - (0, position), - f"TIME: {format_lb(self.data_lb_time)}", - font=self.skin.stats_text_font, - fill=self.skin.stats_text_colour - ) - position += self.skin.stats_text_height + self.skin.stats_text_gap - - draw.text( - (0, position), - "ANKI: COMING SOON", - font=self.skin.stats_text_font, - fill=self.skin.stats_text_colour - ) - position += self.skin.stats_text_height + self.skin.stats_text_gap - - position += self.skin.stats_subheader_pregap - # Study time - draw.text( - (0, position), - 'STUDY TIME', - font=self.skin.stats_subheader_font, - fill=self.skin.stats_subheader_colour - ) - position += self.skin.col2_subheader_height + self.skin.stats_text_gap - - draw.text( - (0, position), - f'DAILY: {format_time(self.data_time_daily)}', - font=self.skin.stats_text_font, - fill=self.skin.stats_text_colour - ) - position += self.skin.stats_text_height + self.skin.stats_text_gap - - draw.text( - (0, position), - f'MONTHLY: {format_time(self.data_time_monthly)}', - font=self.skin.stats_text_font, - fill=self.skin.stats_text_colour - ) - position += self.skin.stats_text_height + self.skin.stats_text_gap - - draw.text( - (0, position), - f'WEEKLY: {format_time(self.data_time_weekly)}', - font=self.skin.stats_text_font, - fill=self.skin.stats_text_colour - ) - position += self.skin.stats_text_height + self.skin.stats_text_gap - - draw.text( - (0, position), - f'ALL TIME: {format_time(self.data_time_all)}', - font=self.skin.stats_text_font, - fill=self.skin.stats_text_colour - ) - position += self.skin.stats_text_height + self.skin.stats_text_gap - - position += self.skin.stats_subheader_size[1] // 2 - - position += self.skin.stats_subheader_pregap - # Workouts - workout_text = "WORKOUTS: " - draw.text( - (0, position), - workout_text, - font=self.skin.stats_subheader_font, - fill=self.skin.stats_subheader_colour, - anchor='lm' - ) - xposition = self.skin.stats_subheader_font.getlength(workout_text) - draw.text( - (xposition, position), - str(self.data_workouts), - font=self.skin.stats_text_font, - fill=self.skin.stats_subheader_colour, - anchor='lm' - ) - - return col1 - - def draw_column_2(self) -> Image: - # Create new image for column 1 - col2 = Image.new('RGBA', self.skin.col2_size) - draw = ImageDraw.Draw(col2) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.col2_size[0]-1, self.skin.col2_size[1]-1))) - - # Tracking current drawing height - position = 0 - - # Write header - draw.text( - (0, position), - self.skin.col2_header, - font=self.skin.header_font, - fill=self.skin.header_colour - ) - position += self.skin.header_height + self.skin.header_gap - - # Draw date line - month_text = "{}: ".format(self.month) - draw.text( - (0, position), - month_text, - font=self.skin.col2_date_font, - fill=self.skin.col2_date_colour - ) - xposition = self.skin.col2_date_font.getlength(month_text) - draw.text( - (xposition, position), - f"{self.data_time_monthly // 3600} HRS", - font=self.skin.col2_date_font, - fill=self.skin.col2_hours_colour - ) - year_text = str(self.date.year) - xposition = col2.width - self.skin.col2_date_font.getlength(year_text) - draw.text( - (xposition, position), - year_text, - font=self.skin.col2_date_font, - fill=self.skin.col2_date_colour - ) - position += self.skin.col2_subheader_height + self.skin.col2_date_gap - - # Draw calendar - cal = self.draw_calendar() - - col2.alpha_composite(cal, (0, position)) - - return col2 - - def draw_calendar(self) -> Image: - cal = Image.new('RGBA', self.skin.cal_size) - draw = ImageDraw.Draw(cal) - - if self.draft: - draw.rectangle(((0, 0), (self.skin.cal_size[0]-1, self.skin.cal_size[1]-1))) - - xpos, ypos = (0, 0) # Approximate position of top left corner to draw on - - # Constant offset to mid basepoint of text - xoffset = self.skin.cal_streak_end.width // 2 - yoffset = self.skin.cal_number_size[1] // 2 - - # Draw the weekdays - for i, l in enumerate(self.skin.cal_weekday_text): - draw.text( - (xpos + xoffset, ypos + yoffset), - l, - font=self.skin.cal_weekday_font, - fill=self.skin.cal_weekday_colour, - anchor='mm' - ) - xpos += self.skin.cal_number_size[0] + self.skin.cal_column_sep - ypos += self.skin.cal_weekday_height + self.skin.cal_weekday_gap - xpos = 0 - - streak_starts = list(itertools.chain(*self.data_streaks)) - streak_middles = list(itertools.chain(*(range(i+1, j) for i, j in self.data_streaks))) - streak_pairs = set(i for i, j in self.data_streaks if j-i == 1) - - # Draw the days of the month - num_diff_x = self.skin.cal_number_size[0] + self.skin.cal_column_sep - num_diff_y = self.skin.cal_number_size[1] + self.skin.cal_number_gap - offset = (self.first_weekday + 1) % 7 - - centres = [ - (xpos + xoffset + (i + offset) % 7 * num_diff_x, - ypos + yoffset + (i + offset) // 7 * num_diff_y) - for i in range(0, self.month_days) - ] - - for day in streak_middles: - if day < 1: - continue - i = day - 1 - if i >= len(centres): - # Shouldn't happen, but ignore - continue - x, y = centres[i] - week_day = (i + offset) % 7 - - top = y - self.skin.cal_streak_end.height // 2 - bottom = y + self.skin.cal_streak_end.height // 2 - 1 - - # Middle of streak on edges - if week_day == 0 or i == 0: - # Draw end bobble - cal.paste( - self.skin.cal_streak_middle, - (x - self.skin.cal_streak_end.width // 2, top) - ) - if week_day != 6: - # Draw rectangle forwards - draw.rectangle( - ((x, top), (x + num_diff_x, bottom)), - fill=self.skin.cal_streak_middle_colour, - width=0 - ) - elif week_day == 6 or i == self.month_days - 1: - # Draw end bobble - cal.paste( - self.skin.cal_streak_middle, - (x - self.skin.cal_streak_end.width // 2, top) - ) - if week_day != 0: - # Draw rectangle backwards - draw.rectangle( - ((x - num_diff_x, top), (x, bottom)), - fill=self.skin.cal_streak_middle_colour, - width=0 - ) - else: - # Draw rectangle on either side - draw.rectangle( - ((x - num_diff_x, top), (x + num_diff_x, bottom)), - fill=self.skin.cal_streak_middle_colour, - width=0 - ) - - for i, (x, y) in enumerate(centres): - # Streak endpoint - if i + 1 in streak_starts: - if i + 1 in streak_pairs and (i + offset) % 7 != 6: - # Draw rectangle forwards - top = y - self.skin.cal_streak_end.height // 2 - bottom = y + self.skin.cal_streak_end.height // 2 - 1 - draw.rectangle( - ((x, top), (x + num_diff_x, bottom)), - fill=self.skin.cal_streak_middle_colour, - width=0 - ) - cal.alpha_composite( - self.skin.cal_streak_end, - (x - self.skin.cal_streak_end.width // 2, y - self.skin.cal_streak_end.height // 2) - ) - - for i, (x, y) in enumerate(centres): - numstr = str(i + 1) - - draw.text( - (x, y), - numstr, - font=self.skin.cal_number_font, - fill=self.skin.cal_number_end_colour if (i+1 in streak_starts) else self.skin.cal_number_colour, - anchor='mm' - ) - - return cal - - -class StatsCard(Card): - route = 'stats_card' - card_id = 'stats' - - layout = StatsLayout - skin = StatsSkin - - display_name = "User Stats" - - @classmethod - async def sample_args(cls, ctx, **kwargs): - return { - 'lb_data': (21, 123), - 'time_data': (3600, 5 * 24 * 3600, 1.5 * 24 * 3600, 100 * 24 * 3600), - 'workouts': 50, - 'streak_data': [(1, 3), (7, 8), (10, 10), (12, 16), (18, 25), (27, 31)], - 'date': datetime(2022, 2, 1) - } diff --git a/src/modules/pending-rewrite/gui-cards/tasklist.py b/src/modules/pending-rewrite/gui-cards/tasklist.py deleted file mode 100644 index a8bff4cf..00000000 --- a/src/modules/pending-rewrite/gui-cards/tasklist.py +++ /dev/null @@ -1,389 +0,0 @@ -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' - ), - } diff --git a/src/modules/pending-rewrite/gui-cards/test.py b/src/modules/pending-rewrite/gui-cards/test.py deleted file mode 100644 index 4a255183..00000000 --- a/src/modules/pending-rewrite/gui-cards/test.py +++ /dev/null @@ -1,38 +0,0 @@ -from io import BytesIO -from PIL import Image, ImageDraw -from .Card import Card -from .Avatars import avatar_manager - - -class TestCard(Card): - server_route = "testing" - - def __init__(self, text, avatar): - self.text = text - self.avatar = avatar - - self.image = None - - def draw(self): - bg = Image.new('RGBA', (100, 100)) - draw = ImageDraw.Draw(bg) - draw.text( - (0, 0), - self.text, - fill='#FF0000' - ) - bg.alpha_composite(self.avatar, (0, 30)) - - return bg - - @classmethod - async def card_route(cls, executor, args, kwargs): - kwargs['avatar'] = (await avatar_manager().get_avatars(kwargs['avatar']))[0] - return await super().card_route(executor, 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) diff --git a/src/modules/pending-rewrite/gui-cards/timer.py b/src/modules/pending-rewrite/gui-cards/timer.py deleted file mode 100644 index 2d0e9332..00000000 --- a/src/modules/pending-rewrite/gui-cards/timer.py +++ /dev/null @@ -1,449 +0,0 @@ -import math -from io import BytesIO -from PIL import Image, ImageDraw, ImageOps - -from ..base import Card, Layout, fielded, Skin -from ..base.Avatars import avatar_manager -from ..base.Skin import ( - AssetField, StringField, NumberField, - FontField, ColourField, PointField, ComputedField -) - - -@fielded -class _TimerSkin(Skin): - _env = { - 'scale': 2 # General size scale to match background resolution - } - - background: AssetField = "timer/background.png" - main_colour: ColourField - - header_field_height: NumberField = 171.5 - header_font: FontField = ('ExtraBold', 76) - - inner_margin: NumberField = 40 - inner_sep: NumberField = 7.5 - - # Timer section - # Outer progress bar - progress_end: AssetField - progress_start: AssetField - progress_bg: AssetField = "timer/break_timer.png" - progress_mask: ComputedField = lambda skin: ImageOps.invert(skin.progress_bg.split()[-1].convert('L')) - - timer_bg: AssetField = "timer/timer_bg.png" - - # Inner timer text - countdown_font: FontField = ('Light', 112) - countdown_gap: NumberField = 10 - stage_font: FontField = ('Light', 43.65) - stage_colour: ColourField = '#FFFFFF' - - mic_icon: AssetField - stage_text: StringField - - # Members - user_bg: AssetField = "timer/break_user.png" - user_mask: AssetField = "timer/avatar_mask.png" - - time_font: FontField = ('Black', 26) - time_colour: ColourField = '#FFFFFF' - - tag_gap: NumberField = 5.5 - tag: AssetField - tag_font: FontField = ('SemiBold', 25) - - # grid_x = (background.width - progress_mask.width - 2 * progress_end.width - grid_start[0] - user_bg.width) // 4 - grid: PointField = (344, 246) - - # Date text - date_font: FontField = ('Bold', 28) - date_colour: ColourField = '#6f6e6f' - date_gap: NumberField = 50 - - -@fielded -class FocusTimerSkin(_TimerSkin): - main_colour: ColourField = '#DDB21D' - user_bg: AssetField = "timer/focus_user.png" - mic_icon: AssetField = "timer/mute.png" - progress_end: AssetField = "timer/progress_end_focus.png" - progress_start: AssetField = "timer/progress_start_focus.png" - stage_text: StringField = "FOCUS" - tag: AssetField = "timer/focus_tag.png" - - -@fielded -class BreakTimerSkin(_TimerSkin): - main_colour: ColourField = '#78B7EF' - user_bg: AssetField = "timer/break_user.png" - mic_icon: AssetField = "timer/unmute.png" - progress_end: AssetField = "timer/progress_end_break.png" - progress_start: AssetField = "timer/progress_start_break.png" - stage_text: StringField = "BREAK" - tag: AssetField = "timer/break_tag.png" - - -class TimerLayout(Layout): - def __init__(self, skin, name, remaining, duration, users): - self.skin = skin - - self.data_name = name - self.data_remaining = 5 * math.ceil(remaining / 5) - self.data_duration = duration - self.data_amount = 1 - remaining / duration - self.data_users = sorted(users, key=lambda user: user[1], reverse=True) # (avatar, time) - - @staticmethod - def format_time(time, hours=True): - if hours: - return "{:02}:{:02}".format(int(time // 3600), int((time // 60) % 60)) - else: - return "{:02}:{:02}".format(int(time // 60), int(time % 60)) - - def draw(self): - image = self.skin.background - draw = ImageDraw.Draw(image) - - # Draw header - text = self.data_name - length = self.skin.header_font.getlength(text) - draw.text( - (image.width // 2, self.skin.header_field_height // 2), - text, - fill=self.skin.main_colour, - font=self.skin.header_font, - anchor='mm' - ) - - # Draw timer - timer_image = self._draw_progress_bar(self.data_amount) - ypos = timer_y = ( - self.skin.header_field_height - + (image.height - self.skin.header_field_height - timer_image.height) // 2 - - self.skin.progress_end.height // 2 - ) - xpos = timer_x = image.width - self.skin.inner_margin - timer_image.width - - image.alpha_composite( - timer_image, - (xpos, ypos) - ) - - # Draw timer text - stage_size = self.skin.stage_font.getsize(' ' + self.skin.stage_text) - - ypos += timer_image.height // 2 - stage_size[1] // 2 - xpos += timer_image.width // 2 - draw.text( - (xpos, ypos), - (text := self.format_time(self.data_remaining)), - fill=self.skin.main_colour, - font=self.skin.countdown_font, - anchor='mm' - ) - - size = int(self.skin.countdown_font.getsize(text)[1]) - ypos += size - - self.skin.mic_icon.thumbnail((stage_size[1], stage_size[1])) - length = int(self.skin.mic_icon.width + self.skin.stage_font.getlength(' ' + self.skin.stage_text)) - xpos -= length // 2 - - image.alpha_composite( - self.skin.mic_icon, - (xpos, ypos - self.skin.mic_icon.height) - ) - draw.text( - (xpos + self.skin.mic_icon.width, ypos), - ' ' + self.skin.stage_text, - fill=self.skin.stage_colour, - font=self.skin.stage_font, - anchor='ls' - ) - - # Draw user grid - if self.data_users: - grid_image = self.draw_user_grid() - - # ypos = self.skin.header_field_height + (image.height - self.skin.header_field_height - grid_image.height) // 2 - ypos = timer_y + (timer_image.height - grid_image.height) // 2 - stage_size[1] // 2 - xpos = ( - self.skin.inner_margin - + (timer_x - self.skin.inner_sep - self.skin.inner_margin) // 2 - - grid_image.width // 2 - ) - - image.alpha_composite( - grid_image, - (xpos, ypos) - ) - - # Draw the footer - ypos = image.height - ypos -= self.skin.date_gap - date_text = "Use !now [text] to show what you are working on!" - size = self.skin.date_font.getsize(date_text) - ypos -= size[1] - draw.text( - ((image.width - size[0]) // 2, ypos), - date_text, - font=self.skin.date_font, - fill=self.skin.date_colour - ) - return image - - def draw_user_grid(self) -> Image: - users = list(self.data_users)[:25] - - # Set these to 5 and 5 to force top left corner - rows = math.ceil(len(users) / 5) - columns = 5 - # columns = min(len(users), 5) - - size = ( - (columns - 1) * self.skin.grid[0] + self.skin.user_bg.width, - (rows - 1) * self.skin.grid[1] + self.skin.user_bg.height + self.skin.tag_gap + self.skin.tag.height - ) - - image = Image.new( - 'RGBA', - size - ) - for i, user in enumerate(users): - x = (i % 5) * self.skin.grid[0] - y = (i // 5) * self.skin.grid[1] - - user_image = self.draw_user(user) - image.alpha_composite( - user_image, - (x, y) - ) - return image - - def draw_user(self, user): - width = self.skin.user_bg.width - height = self.skin.user_bg.height + self.skin.tag_gap + self.skin.tag.height - image = Image.new('RGBA', (width, height)) - draw = ImageDraw.Draw(image) - - image.alpha_composite(self.skin.user_bg) - - avatar, time, tag = user - avatar = avatar - timestr = self.format_time(time, hours=True) - - # Mask avatar - avatar.paste((0, 0, 0, 0), mask=self.skin.user_mask.convert('RGBA')) - - # Resize avatar - avatar.thumbnail((self.skin.user_bg.height - 10, self.skin.user_bg.height - 10)) - - image.alpha_composite( - avatar, - (5, 5) - ) - draw.text( - (120, self.skin.user_bg.height // 2), - timestr, - anchor='lm', - font=self.skin.time_font, - fill=self.skin.time_colour - ) - - if tag: - ypos = self.skin.user_bg.height + self.skin.tag_gap - image.alpha_composite( - self.skin.tag, - ((image.width - self.skin.tag.width) // 2, ypos) - ) - draw.text( - (image.width // 2, ypos + self.skin.tag.height // 2), - tag, - font=self.skin.tag_font, - fill='#FFFFFF', - anchor='mm' - ) - return image - - def _draw_progress_bar(self, amount): - amount = min(amount, 1) - amount = max(amount, 0) - bg = self.skin.timer_bg - end = self.skin.progress_start - mask = self.skin.progress_mask - - center = ( - bg.width // 2 + 1, - bg.height // 2 - ) - radius = 553 - theta = amount * math.pi * 2 - math.pi / 2 - x = int(center[0] + radius * math.cos(theta)) - y = int(center[1] + radius * math.sin(theta)) - - canvas = Image.new('RGBA', size=(bg.width, bg.height)) - draw = ImageDraw.Draw(canvas) - - if amount >= 0.01: - canvas.alpha_composite( - end, - ( - center[0] - end.width // 2, - 26 - end.height // 2 - ) - ) - - sidelength = bg.width // 2 - line_ends = ( - int(center[0] + sidelength * math.cos(theta)), - int(center[1] + sidelength * math.sin(theta)) - ) - if amount <= 0.25: - path = [ - center, - (center[0], center[1] - sidelength), - (bg.width, 0), - line_ends - ] - elif amount <= 0.5: - path = [ - center, - (center[0], center[1] - sidelength), - (bg.width, 0), - (bg.width, bg.height), - line_ends - ] - elif amount <= 0.75: - path = [ - center, - (center[0], center[1] - sidelength), - (bg.width, 0), - (bg.width, bg.height), - (0, bg.height), - line_ends - ] - else: - path = [ - center, - (center[0], center[1] - sidelength), - (bg.width, 0), - (bg.width, bg.height), - (0, bg.height), - (0, 0), - line_ends - ] - - draw.polygon( - path, - fill=self.skin.main_colour - ) - - canvas.paste((0, 0, 0, 0), mask=mask) - - image = Image.new( - 'RGBA', - size=(bg.width + self.skin.progress_end.width, - bg.height + self.skin.progress_end.height) - ) - image.alpha_composite( - bg, - (self.skin.progress_end.width // 2, - self.skin.progress_end.height // 2) - ) - image.alpha_composite( - canvas, - (self.skin.progress_end.width // 2, - self.skin.progress_end.height // 2) - ) - - image.alpha_composite( - self.skin.progress_end, - ( - x, - y - ) - ) - - return image - - -class _TimerCard(Card): - layout = TimerLayout - - @classmethod - async def card_route(cls, runner, args, kwargs): - if kwargs['users']: - avatar_keys, times, tags = zip(*kwargs['users']) - avatars = await avatar_manager().get_avatars(*((*key, 512) for key in avatar_keys)) - kwargs['users'] = tuple(zip(avatars, times, tags)) - - return await super().card_route(runner, args, kwargs) - - @classmethod - def _execute(cls, *args, **kwargs): - if kwargs['users']: - avatar_data, times, tags = zip(*kwargs['users']) - avatars = [] - for datum in avatar_data: - with BytesIO(datum) as buffer: - buffer.seek(0) - avatars.append(Image.open(buffer).convert('RGBA')) - kwargs['users'] = tuple(zip(avatars, times, tags)) - - return super()._execute(*args, **kwargs) - - -class FocusTimerCard(_TimerCard): - route = 'focus_timer_card' - card_id = 'focus_timer' - - skin = FocusTimerSkin - display_name = "Focus Timer" - - @classmethod - async def sample_args(cls, ctx, **kwargs): - from ..utils import get_avatar_key - - return { - 'name': 'Pomodoro Timer', - 'remaining': 1658, - 'duration': 3000, - 'users': [ - (get_avatar_key(ctx.client, ctx.author.id), 7055, "SkinShop"), - ((0, None), 6543, "Never"), - ((0, None), 5432, "Going"), - ((0, None), 4321, "To"), - ((0, None), 3210, "Give"), - ((0, None), 2109, "You"), - ((0, None), 1098, "Up"), - ] - } - - -class BreakTimerCard(_TimerCard): - route = 'break_timer_card' - card_id = 'break_timer' - - skin = BreakTimerSkin - display_name = "Break Timer" - - @classmethod - async def sample_args(cls, ctx, **kwargs): - from ..utils import get_avatar_key - - return { - 'name': 'Pomodoro Timer', - 'remaining': 1658, - 'duration': 3000, - 'users': [ - (get_avatar_key(ctx.client, ctx.author.id), 7055, "SkinShop"), - ((0, None), 6543, "Never"), - ((0, None), 5432, "Going"), - ((0, None), 4321, "To"), - ((0, None), 3210, "Let"), - ((0, None), 2109, "You"), - ((0, None), 1098, "Down"), - ] - } diff --git a/src/modules/pending-rewrite/gui-cards/weekly.py b/src/modules/pending-rewrite/gui-cards/weekly.py deleted file mode 100644 index c6ae61ee..00000000 --- a/src/modules/pending-rewrite/gui-cards/weekly.py +++ /dev/null @@ -1,713 +0,0 @@ -import os -import math -from PIL import Image, ImageDraw, ImageColor -from datetime import timedelta, datetime, timezone - -from ..utils import resolve_asset_path -from ..base import Card, Layout, fielded, Skin -from ..base.Skin import ( - AssetField, RGBAAssetField, AssetPathField, BlobField, StringField, NumberField, PointField, RawField, - FontField, ColourField, ComputedField, FieldDesc -) - - -@fielded -class WeeklyStatsSkin(Skin): - _env = { - 'scale': 1 # General size scale to match background resolution - } - - background: AssetField = 'weekly/background.png' - - # Header - title_pre_gap: NumberField = 40 - title_text: StringField = "STUDY HOURS" - 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 - - # Top - top_grid_x: NumberField = 150 - top_grid_y: NumberField = 100 - - top_hours_font: FontField = ('Bold', 36.35) - top_hours_colour: ColourField = '#FFFFFF' - - top_hours_bg_mask: AssetField = 'weekly/hours_bg_mask.png' - top_hours_bg_colour: ColourField = '#0B465E' # TODO: Check this - top_hours_bg_colour_override: ColourField = None - top_hours_bg: BlobField = FieldDesc( - BlobField, - mask_field='top_hours_bg_mask', - colour_field='top_hours_bg_colour', - colour_field_override='top_hours_bg_colour_override' - ) - - top_hours_sep: NumberField = 100 - - top_line_width: NumberField = 10 - top_line_colour: ColourField = '#042231' - - top_weekday_pre_gap: NumberField = 20 - top_weekday_font: FontField = ('Bold', 36.35) - top_weekday_colour: ColourField = '#FFFFFF' - top_weekday_height: ComputedField = lambda skin: skin.top_weekday_font.getsize('MTWTFSS')[1] - top_weekday_gap: NumberField = 5 - top_date_font: FontField = ('SemiBold', 30) - top_date_colour: ColourField = '#808080' - top_date_height: ComputedField = lambda skin: skin.top_date_font.getsize('8/8')[1] - - top_bar_mask: RGBAAssetField = 'weekly/top_bar_mask.png' - - top_this_colour: ColourField = '#DDB21D' - top_this_color_override: ColourField = None - - top_last_colour: ColourField = '#377689CC' - top_last_color_override: ColourField = None - - top_this_bar_full: BlobField = FieldDesc( - BlobField, - mask_field='top_bar_mask', - colour_field='top_this_colour', - colour_field_override='top_this_colour_override' - ) - - top_last_bar_full: BlobField = FieldDesc( - BlobField, - mask_field='top_bar_mask', - colour_field='top_last_colour', - colour_field_override='top_last_colour_override' - ) - - top_gap: NumberField = 80 - - weekdays: RawField = ('M', 'T', 'W', 'T', 'F', 'S', 'S') - - # Bottom - btm_bar_horiz_colour: ColourField = "#052B3B93" - btm_bar_vert_colour: ColourField = "#042231B2" - btm_weekly_background_size: PointField = (66, 400) - btm_weekly_background_colour: ColourField = '#06324880' - btm_weekly_background: ComputedField = lambda skin: ( - Image.new( - 'RGBA', - skin.btm_weekly_background_size, - color=ImageColor.getrgb(skin.btm_weekly_background_colour) - ) - ) - - btm_timeline_end_mask: RGBAAssetField = 'weekly/timeline_end_mask.png' - - btm_this_colour: ColourField = '#DDB21D' - btm_this_colour_override: ColourField = None - btm_this_end: BlobField = FieldDesc( - BlobField, - mask_field='btm_timeline_end_mask', - colour_field='btm_this_colour', - colour_override_field='btm_this_colour_override' - ) - - btm_last_colour: ColourField = '#5E6C747F' - btm_last_colour_override: ColourField = None - btm_last_end: BlobField = FieldDesc( - BlobField, - mask_field='btm_timeline_end_mask', - colour_field='btm_last_colour', - colour_override_field='btm_last_colour_override' - ) - - btm_horiz_width: ComputedField = lambda skin: skin.btm_this_end.height - btm_sep: ComputedField = lambda skin: (skin.btm_weekly_background_size[1] - 7 * skin.btm_horiz_width) // 6 - - btm_vert_width: NumberField = 10 - - btm_grid_x: NumberField = 48 - btm_grid_y: ComputedField = lambda skin: skin.btm_horiz_width + skin.btm_sep - - btm_weekday_font: FontField = ('Bold', 36.35) - btm_weekday_colour: ColourField = '#FFFFFF' - - btm_day_font: FontField = ('SemiBold', 31) - btm_day_colour: ColourField = '#FFFFFF' - btm_day_height: ComputedField = lambda skin: skin.btm_day_font.getsize('88')[1] - btm_day_gap: NumberField = 15 - - btm_emoji_path: StringField = "weekly/emojis" - btm_emojis: ComputedField = lambda skin: { - state: Image.open( - resolve_asset_path( - skin._env['PATH'], - os.path.join(skin.btm_emoji_path, f"{state}.png") - ) - ).convert('RGBA') - for state in ('very_happy', 'happy', 'neutral', 'sad', 'shocked') - } - - # Summary - summary_pre_gap: NumberField = 50 - - summary_mask: AssetField = 'weekly/summary_mask.png' - - this_week_font: FontField = ('Light', 23) - this_week_colour: ColourField = '#BABABA' - this_week_image: BlobField = FieldDesc( - BlobField, - mask_field='summary_mask', - colour_field='top_this_colour', - colour_field_override='top_this_colour_override' - ) - - summary_sep: NumberField = 300 - - last_week_font: FontField = ('Light', 23) - last_week_colour: ColourField = '#BABABA' - last_week_image: BlobField = FieldDesc( - BlobField, - mask_field='summary_mask', - colour_field='top_last_colour', - colour_field_override='top_last_colour_override' - ) - - # Date text - footer_font: FontField = ('Bold', 28) - footer_colour: ColourField = '#6f6e6f' - footer_gap: NumberField = 50 - - -class WeeklyStatsPage(Layout): - def __init__(self, skin, name, discrim, sessions, date): - """ - `sessions` is a list of study sessions from the last two weeks. - """ - self.skin = skin - - self.data_sessions = sessions - self.data_date = date - - self.data_name = name - self.data_discrim = discrim - - self.week_start = date - timedelta(days=date.weekday()) - self.last_week_start = self.week_start - timedelta(days=7) - - periods = [] - times = [] - - day_start = self.last_week_start - day_time = 0 - day_periods = [] - current_period = [] - i = 0 - while i < len(sessions): - start, end = sessions[i] - i += 1 - - day_end = day_start + timedelta(hours=24) - - if end < day_start: - continue - - if start < day_start: - start = day_start - elif start >= day_end: - if current_period: - day_periods.append(current_period) - periods.append(day_periods) - times.append(day_time) - current_period = [] - day_periods = [] - day_time = 0 - day_start = day_end - i -= 1 - continue - - if (ended_after := (end - day_end).total_seconds()) > 0: - if ended_after > 60 * 20: - end = day_end - else: - end = day_end - timedelta(minutes=1) - - day_time += (end - start).total_seconds() - if not current_period: - current_period = [start, end] - elif (start - current_period[1]).total_seconds() < 60 * 60: - current_period[1] = end - else: - day_periods.append(current_period) - current_period = [start, end] - - if ended_after > 0: - if current_period: - day_periods.append(current_period) - periods.append(day_periods) - times.append(day_time) - current_period = [] - day_periods = [] - day_time = 0 - day_start = day_end - - if ended_after > 60 * 20: - i -= 1 - - if current_period: - day_periods.append(current_period) - periods.append(day_periods) - times.append(day_time) - - self.data_periods = periods - for i in range(len(periods), 14): - periods.append([]) - self.data_hours = [time / 3600 for time in times] - for i in range(len(self.data_hours), 14): - self.data_hours.append(0) - - self.date_labels = [ - (self.week_start + timedelta(days=i)).strftime('%d/%m') - for i in range(0, 7) - ] - - self.max_hour_label = (4 * math.ceil(max(self.data_hours) / 4)) or 4 - - self.image = None - - def draw(self) -> Image: - image = self.skin.background - self.image = image - - 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 - title_size = self.skin.title_font.getsize(self.skin.title_text) - ypos += title_size[1] + self.skin.title_gap - # ypos += title_size[1] + self.skin.title_underline_gap - # draw.line( - # (xpos, ypos, xpos + 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 top box - top = self.draw_top() - image.alpha_composite( - top, - ((image.width - top.width) // 2, ypos) - ) - - ypos += top.height + self.skin.top_gap - - # Draw the bottom box - bottom = self.draw_bottom() - image.alpha_composite( - bottom, - ((image.width - bottom.width) // 2, ypos) - ) - ypos += bottom.height + self.skin.summary_pre_gap - - # Draw the summaries - summary_image = self.draw_summaries() - image.alpha_composite( - summary_image, - ((image.width - summary_image.width) // 2, ypos) - ) - - # Draw the footer - ypos = image.height - ypos -= self.skin.footer_gap - date_text = self.data_date.strftime( - "Weekly Statistics • 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 - ) - return image - - def draw_summaries(self) -> Image: - this_week_text = " THIS WEEK: {} Hours".format(int(sum(self.data_hours[7:]))) - this_week_length = int(self.skin.this_week_font.getlength(this_week_text)) - last_week_text = " LAST WEEK: {} Hours".format(int(sum(self.data_hours[:7]))) - last_week_length = int(self.skin.last_week_font.getlength(last_week_text)) - - image = Image.new( - 'RGBA', - ( - self.skin.this_week_image.width + this_week_length - + self.skin.summary_sep - + self.skin.last_week_image.width + last_week_length, - self.skin.this_week_image.height - ) - ) - draw = ImageDraw.Draw(image) - - xpos = 0 - ypos = image.height // 2 - image.alpha_composite( - self.skin.this_week_image, - (0, 0) - ) - xpos += self.skin.this_week_image.width - draw.text( - (xpos, ypos), - this_week_text, - fill=self.skin.this_week_colour, - font=self.skin.this_week_font, - anchor='lm' - ) - - xpos += self.skin.summary_sep + this_week_length - - image.alpha_composite( - self.skin.last_week_image, - (xpos, 0) - ) - xpos += self.skin.last_week_image.width - draw.text( - (xpos, ypos), - last_week_text, - fill=self.skin.last_week_colour, - font=self.skin.last_week_font, - anchor='lm' - ) - return image - - def draw_top(self) -> Image: - size_x = ( - self.skin.top_hours_bg.width // 2 + self.skin.top_hours_sep - + 6 * self.skin.top_grid_x + self.skin.top_bar_mask.width // 2 - + self.skin.top_hours_bg.width // 2 - ) - size_y = ( - self.skin.top_hours_bg.height // 2 + 4 * self.skin.top_grid_y + self.skin.top_weekday_pre_gap - + self.skin.top_weekday_height + self.skin.top_weekday_gap + self.skin.top_date_height - ) - image = Image.new('RGBA', (size_x, size_y)) - draw = ImageDraw.Draw(image) - - x0 = self.skin.top_hours_bg.width // 2 + self.skin.top_hours_sep - y0 = self.skin.top_hours_bg.height // 2 + 4 * self.skin.top_grid_y - - # Draw lines and numbers - labels = list(int(i * self.max_hour_label // 4) for i in range(0, 5)) - - xpos = x0 - self.skin.top_hours_sep - ypos = y0 - for label in labels: - draw.line( - ((xpos, ypos), (image.width, ypos)), - width=self.skin.top_line_width, - fill=self.skin.top_line_colour - ) - - image.alpha_composite( - self.skin.top_hours_bg, - (xpos - self.skin.top_hours_bg.width // 2, ypos - self.skin.top_hours_bg.height // 2) - ) - text = str(label) - draw.text( - (xpos, ypos), - text, - fill=self.skin.top_hours_colour, - font=self.skin.top_hours_font, - anchor='mm' - ) - ypos -= self.skin.top_grid_y - - # Draw dates - xpos = x0 - ypos = y0 + self.skin.top_weekday_pre_gap - for letter, datestr in zip(self.skin.weekdays, self.date_labels): - draw.text( - (xpos, ypos), - letter, - fill=self.skin.top_weekday_colour, - font=self.skin.top_weekday_font, - anchor='mt' - ) - draw.text( - (xpos, ypos + self.skin.top_weekday_height + self.skin.top_weekday_gap), - datestr, - fill=self.skin.top_date_colour, - font=self.skin.top_date_font, - anchor='mt' - ) - xpos += self.skin.top_grid_x - - # Draw bars - for i, (last_hours, this_hours) in enumerate(zip(self.data_hours[:7], self.data_hours[7:])): - day = i % 7 - xpos = x0 + day * self.skin.top_grid_x - - for draw_last in (last_hours > this_hours, not last_hours > this_hours): - hours = last_hours if draw_last else this_hours - height = (4 * self.skin.top_grid_y) * (hours / self.max_hour_label) - height = int(height) - - if height >= 2 * self.skin.top_bar_mask.width: - bar = self.draw_vertical_bar( - height, - self.skin.top_last_bar_full if draw_last else self.skin.top_this_bar_full, - self.skin.top_bar_mask - ) - image.alpha_composite( - bar, - (xpos - bar.width // 2, y0 - bar.height) - ) - - return image - - def draw_vertical_bar(self, height, full_bar, mask_bar, crop=False): - y_2 = mask_bar.height - y_1 = height - - image = Image.new('RGBA', full_bar.size) - image.paste(mask_bar, (0, y_2 - y_1), mask=mask_bar) - image.paste(full_bar, mask=image) - - if crop: - image = image.crop( - (0, y_2 - y_1), (image.width, y_2 - y_1), - (image.height, 0), (image.height, image.width) - ) - - return image - - def draw_horizontal_bar(self, length, full_bar, mask_bar, crop=False): - x_2 = mask_bar.length - x_1 = length - - image = Image.new('RGBA', full_bar.size) - image.paste(mask_bar, (x_2 - x_1, 0), mask=mask_bar) - image.paste(full_bar, mask=image) - - if crop: - image = image.crop( - (x_2 - x_1, 0), (image.width, 0), - (x_2 - x_1, image.height), (image.width, image.height) - ) - - return image - - def draw_bottom(self) -> Image: - size_x = int( - self.skin.btm_weekly_background_size[0] - + self.skin.btm_grid_x * 25 - + self.skin.btm_day_font.getlength('24') // 2 - + self.skin.btm_vert_width // 2 - ) - size_y = int( - 7 * self.skin.btm_horiz_width + 6 * self.skin.btm_sep - + self.skin.btm_day_gap - + self.skin.btm_day_height - ) - image = Image.new('RGBA', (size_x, size_y)) - draw = ImageDraw.Draw(image) - - # Grid origin - x0 = self.skin.btm_weekly_background_size[0] + self.skin.btm_vert_width // 2 + self.skin.btm_grid_x - y0 = self.skin.btm_day_gap + self.skin.btm_day_height + self.skin.btm_horiz_width // 2 - - # Draw the hours - ypos = y0 - self.skin.btm_horiz_width // 2 - self.skin.btm_day_gap - for i in range(-1, 25): - xpos = x0 + i * self.skin.btm_grid_x - if i >= 0: - draw.text( - (xpos, ypos), - str(i), - fill=self.skin.btm_day_colour, - font=self.skin.btm_day_font, - anchor='ms' - ) - - draw.line( - ( - (xpos, y0 - self.skin.btm_horiz_width // 2), - (xpos, image.height) - ), - fill=self.skin.btm_bar_vert_colour, - width=self.skin.btm_vert_width - ) - - # Draw the day bars - bar_image = Image.new( - 'RGBA', - (image.width, self.skin.btm_horiz_width), - self.skin.btm_bar_horiz_colour - ) - for i in range(0, 7): - ypos = y0 + i * self.skin.btm_grid_y - self.skin.btm_horiz_width // 2 - image.alpha_composite( - bar_image, - (0, ypos) - ) - - # Draw the weekday background - image.alpha_composite( - self.skin.btm_weekly_background, - (0, y0 - self.skin.btm_horiz_width // 2) - ) - - # Draw the weekdays - xpos = self.skin.btm_weekly_background_size[0] // 2 - for i, l in enumerate(self.skin.weekdays): - ypos = y0 + i * self.skin.btm_grid_y - draw.text( - (xpos, ypos), - l, - font=self.skin.btm_weekday_font, - fill=self.skin.btm_weekday_colour, - anchor='mm' - ) - - # Draw the sessions - seconds_in_day = 60 * 60 * 24 - day_width = 24 * self.skin.btm_grid_x - for i, day in enumerate(reversed(self.data_periods)): - last = (i // 7) - ypos = y0 + (6 - i % 7) * self.skin.btm_grid_y - - for start, end in day: - if end <= start: - continue - day_start = start.replace(hour=0, minute=0, second=0, microsecond=0) - - flat_start = (start == day_start) - duration = (end - start).total_seconds() - xpos = x0 + int((start - day_start).total_seconds() / seconds_in_day * day_width) - - flat_end = (end == day_start + timedelta(days=1)) - - if flat_end: - width = image.width - xpos - else: - width = int(duration / seconds_in_day * day_width) - - bar = self.draw_timeline_bar( - width, - last=last, - flat_start=flat_start, - flat_end=flat_end - ) - - image.alpha_composite( - bar, - (xpos, ypos - bar.height // 2) - ) - - # Draw the emojis - xpos = x0 - self.skin.btm_grid_x // 2 - average_study = sum(self.data_hours[7:]) / 7 - for i, hours in enumerate(self.data_hours[7:]): - if hours: - ypos = y0 + i * self.skin.btm_grid_y - relative = hours / average_study - if relative > 1: - state = 'very_happy' - elif relative > 0.75: - state = 'happy' - elif relative > 0.25: - state = 'neutral' - else: - state = 'sad' - emoji = self.skin.btm_emojis[state] - image.alpha_composite( - emoji, - (xpos - emoji.width // 2, ypos - emoji.height // 2) - ) - return image - - def draw_timeline_bar(self, width, last=False, flat_start=False, flat_end=False) -> Image: - if last: - end = self.skin.btm_last_end - colour = self.skin.btm_last_colour - else: - end = self.skin.btm_this_end - colour = self.skin.btm_this_colour - - image = Image.new( - 'RGBA', - (width, end.height) - ) - draw = ImageDraw.Draw(image) - - # Draw endpoints - if not flat_start: - image.alpha_composite( - end, - (0, 0) - ) - - if not flat_end: - image.alpha_composite( - end, - (width - end.width, 0) - ) - - # Draw the rectangle - rstart = (not flat_start) * (end.width // 2) - rend = width - (not flat_end) * (end.width // 2) - draw.rectangle( - ((rstart, 0), (rend, image.height)), - fill=colour, - width=0 - ) - - return image - - -class WeeklyStatsCard(Card): - route = 'weekly_stats_card' - card_id = 'weekly_stats' - - layout = WeeklyStatsPage - skin = WeeklyStatsSkin - - display_name = "Weekly Stats" - - @classmethod - async def sample_args(cls, ctx, **kwargs): - import random - sessions = [] - day_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) - day_start -= timedelta(hours=24) * 14 - for day in range(0, 14): - day_start += timedelta(hours=24) - - # start of day - pointer = int(abs(random.normalvariate(6 * 60, 1 * 60))) - while pointer < 20 * 60: - session_duration = int(abs(random.normalvariate(4 * 60, 1 * 60))) - sessions.append(( - day_start + timedelta(minutes=pointer), - day_start + timedelta(minutes=(pointer + session_duration)), - ) - ) - pointer += session_duration - pointer += int(abs(random.normalvariate(2.5 * 60, 1 * 60))) - - return { - 'name': ctx.author.name if ctx else "John Doe", - 'discrim': ('#' + ctx.author.discriminator) if ctx else "#0000", - 'sessions': sessions, - 'date': datetime.now(timezone.utc).replace(hour=0, minute=0, second=0) - } diff --git a/src/modules/tasklist/ui.py b/src/modules/tasklist/ui.py index a88b36a1..552b514d 100644 --- a/src/modules/tasklist/ui.py +++ b/src/modules/tasklist/ui.py @@ -581,8 +581,10 @@ class TasklistUI(BasePager): await self.redraw() async def delete_selector_refresh(self): + t = self.bot.translator.t self.delete_selector.placeholder = t(_p('ui:tasklist|menu:delete|placeholder', "Select to Delete")) self.delete_selector.options = self.toggle_selector.options + self.delete_selector.max_values = len(self.toggle_selector.options) @button(label="ClOSE_PLACEHOLDER", style=ButtonStyle.red) async def close_pressed(self, interaction: discord.Interaction, pressed: Button):