""" Inkycal Calendar Module Copyright by aceinnolab """ # pylint: disable=logging-fstring-interpolation import calendar as cal from inkycal.custom import * from inkycal.modules.template import inkycal_module logger = logging.getLogger(__name__) class TaskEntry: """Class representing a task entry.""" def __init__(self, title, project=None, parent_project=None, subtasks=None): self.title = title self.project = project self.consumed_time = 0 # in minutes self.subtasks = [] def add_subtask(self, subtask): """Add a subtask to the task entry.""" self.subtasks.append(subtask) def mock_task_list(): """Generate a mock task list for testing purposes.""" finetune = TaskEntry("3 new models finetune work", project="AISentry") generate_data = TaskEntry("Generate training data", project="AISentry") function_development = TaskEntry("Function development", project="AISentry") finetune.add_subtask(generate_data) finetune.add_subtask(function_development) check_llama = TaskEntry("Check Llama model performance", project="llama.cpp") transform = TaskEntry("Transformers library exploration", project="llama.cpp") check_llama.add_subtask(transform) research_work = TaskEntry("Research new AI techniques", project="AISentry") meeting = TaskEntry("Team meeting", project="General") return [finetune, check_llama, research_work, meeting] def get_ip_address(): """Get public IP address from external service.""" try: # 方法1: 使用 ipify.org response = requests.get('https://api.ipify.org?format=json', timeout=5) return response.json()['ip'] except Exception: try: # 方法2: 使用 icanhazip.com (备用) response = requests.get('https://icanhazip.com', timeout=5) return response.text.strip() except Exception: try: # 方法3: 使用 ifconfig.me (备用) response = requests.get('https://ifconfig.me/ip', timeout=5) return response.text.strip() except Exception: return "N/A" class Today(inkycal_module): """today class Show today's date and events from given iCalendars """ name = "Today - Show today's date and events from iCalendars" optional = { "date_format": { "label": "Use an arrow-supported token for custom date formatting " + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. D MMM", "default": "D MMM", }, "time_format": { "label": "Use an arrow-supported token for custom time formatting " + "see https://arrow.readthedocs.io/en/stable/#supported-tokens, e.g. HH:mm", "default": "HH:mm", }, "webdav_hostname": { "label": "WebDAV Hostname (e.g. https://webdav.server.com)", "default": "", }, "webdav_login": { "label": "WebDAV Login Username", "default": "", }, "webdav_password": { "label": "WebDAV Login Password", "default": "", }, "webdav_file_path": { "label": "WebDAV File Path to Super Productivity JSON file", "default": "", }, } def __init__(self, config): """Initialize inkycal_calendar module""" super().__init__(config) config = config['config'] self.ical = None self.month_events = None self._upcoming_events = None self._days_with_events = None # optional parameters self.week_start = config['week_starts_on'] self.show_events = config['show_events'] self.date_format = config["date_format"] self.time_format = config['time_format'] self.language = config['language'] # webdav configuration self.webdav_options = { 'webdav_hostname': config.get('webdav_hostname', ''), 'webdav_login': config.get('webdav_login', ''), 'webdav_password': config.get('webdav_password', ''), 'webdav_file_path': config.get('webdav_file_path', ''), } # additional configuration self.timezone = get_system_tz() # 选择字体:优先使用支持中文的 NotoSansCJK,否则使用 NotoSans self._font_family = self._select_font_family() # give an OK message logger.debug(f'{__name__} loaded') def _select_font_family(self) -> str: preferred_fonts = [ 'NotoSansCJKsc-Regular', 'NotoSans-SemiCondensed' ] for font_name in preferred_fonts: if font_name in fonts: logger.debug(f'Selected font: {font_name}') return font_name return list(fonts.keys())[0] def _get_font(self, size: int) -> ImageFont.FreeTypeFont: return ImageFont.truetype(fonts[self._font_family], size=size) @staticmethod def flatten(values): """Flatten the values.""" return [x for y in values for x in y] def generate_image(self): """Generate the image for today's date and events. """ # **************************************************************************************************************** # Create base image # Define new image size with respect to padding im_width = self.width - 2 * self.padding_left im_height = self.height - 2 * self.padding_top im_size = (im_width, im_height) event_height = 0 logger.debug(f'Generating Today module image of size {im_size}') # Create an iamge for black and colour Inky displays im_black = Image.new('RGB', im_size, color='white') im_colour = Image.new('RGB', im_size, color='white') # Split the image into two sections: date section and events section left_section_width = int(im_width * 0.2) right_section_width = im_width - left_section_width # 5% bottom space will be reserved for show the day progress bar left_section = (0, 0, left_section_width, im_height - int(im_height * 0.05)) right_section = (left_section_width, 0, im_width, im_height - int(im_height * 0.05)) section_height = left_section[3] # **************************************************************************************************************** # Edit left section - show today's date now = arrow.now(tz=self.timezone) month_height = int(im_height * 0.15) month_font = self._get_font(int(self.fontsize * 1.5)) write( im_black, (0, 0), (left_section_width, month_height), now.format('MMMM', locale=self.language), font=month_font, autofit=False ) date_height = int(im_height * 0.5) date_y = month_height large_font = self._get_font(int(self.fontsize * 4)) date_time = arrow.now() day = date_time.day print(str(day)) write( im_colour, (0, date_y), (left_section_width, date_height), str(day), font=large_font, autofit=False ) weekday_y = month_height + date_height weekday_height = int(im_height * 0.15) weekday_font = self._get_font(int(self.fontsize * 2)) write( im_black, (0, weekday_y), (left_section_width, weekday_height), now.format('dddd', locale=self.language), font=weekday_font, autofit=False ) # show IP address at the bottom left ip_y = weekday_y + weekday_height ip_height = im_height - ip_y - 5 ip_address = get_ip_address() write( im_black, (0, ip_y), (left_section_width, ip_height), ip_address, font=self.font, alignment='center', autofit=True ) # **************************************************************************************************************** # Draw a dash line to separate left and right sections for _y in range(0, section_height, 8): ImageDraw.Draw(im_black).line( [(left_section_width, _y), (left_section_width, _y + 4)], fill='black', width=2, ) # **************************************************************************************************************** # Edit right section - show today's events if self.show_events: # 导入日历解析器 upcoming_events = True # 计算右侧可用空间 right_x = left_section_width + 5 # 留5px边距 right_usable_width = right_section_width - 10 # 左右各留5px # 计算行高 line_spacing = 2 text_bbox = self.font.getbbox("hg") line_height = text_bbox[3] + line_spacing max_lines = im_height // line_height from inkycal.modules.super_productivity_utils import get_today_tasks import requests url = self.webdav_options['webdav_hostname'] + self.webdav_options['webdav_file_path'] response = requests.get(url, auth=( self.webdav_options['webdav_login'], self.webdav_options['webdav_password'] )) content = response.content content = content[8:] with open('/workspaces/Inkycal/inkycal/modules/super_productivity.json', 'wb') as f: f.write(content) json_file_path = '/workspaces/Inkycal/inkycal/modules/super_productivity.json' task_list = get_today_tasks(json_file_path) if upcoming_events: # Split to 2 parts # Left part: title # Right part: Project name current_line = 0 for idx, event in enumerate(task_list): if current_line >= max_lines: break # 超出显示范围,停止绘制 # 写任务标题 write( im_black, (right_x, current_line * line_height), (int(right_usable_width * 0.7), line_height), event.title, font=self.font, alignment='left' ) # 写项目名称 project_name = event.project_name if event.project_name else "Inbox" write( im_colour, (right_x + int(right_usable_width * 0.7), current_line * line_height), (int(right_usable_width * 0.3), line_height), project_name, font=self.font, alignment='right' ) current_line += 1 if event.subtasks: for sub_idx, subtask in enumerate(event.subtasks): if subtask.is_done: continue if current_line + sub_idx + 1 >= max_lines: break # 超出显示范围,停止绘制 # 写子任务标题,缩进显示 write( im_black, (right_x + 10, current_line * line_height), (int(right_usable_width * 0.7) - 10, line_height), f"- {subtask.title}", font=self.font, alignment='left' ) current_line += 1 # 更新主循环的索引 pass else: # 没有事件时显示提示 write( im_colour, (right_x, int(im_height / 2)), (right_usable_width, line_height), "No events today", font=self.font, alignment='center' ) # **************************************************************************************************************** # Draw progress bar at the bottom (24 segments for 24 hours) progress_bar_height = int(im_height * 0.05) progress_bar_y = im_height - progress_bar_height # 计算当前小时进度 current_hour = now.hour current_minute = now.minute current_progress = current_hour + (current_minute / 60.0) # 0-24 的浮点数 # 绘制24个格子 num_segments = 24 segment_spacing = 2 # 格子之间的间距 total_spacing = segment_spacing * (num_segments - 1) segment_width = (im_width - total_spacing) / num_segments draw = ImageDraw.Draw(im_black) for i in range(num_segments): # 计算每个格子的位置 x_start = int(i * (segment_width + segment_spacing)) x_end = int(x_start + segment_width) # 判断该格子是否已完成 if i < current_progress: # 已完成的格子填充黑色(在 im_colour 上会显示为红色) draw.rectangle( [(x_start, progress_bar_y), (x_end, im_height)], fill='black', outline='black' ) else: # 未完成的格子只画边框(在 im_black 上画) ImageDraw.Draw(im_black).rectangle( [(x_start, progress_bar_y), (x_end, im_height)], fill='white', outline='black', width=1 ) return im_black, im_colour