Files
Inkycal/inkycal/modules/inkycal_today.py
2025-11-30 21:32:21 +01:00

380 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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