379 lines
14 KiB
Python
379 lines
14 KiB
Python
"""
|
||
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 = {
|
||
"week_starts_on": {
|
||
"label": "When does your week start? (default=Monday)",
|
||
"options": ["Monday", "Sunday"],
|
||
"default": "Monday",
|
||
},
|
||
"show_events": {
|
||
"label": "Show parsed events? (default = True)",
|
||
"options": [True, False],
|
||
"default": True,
|
||
},
|
||
"ical_urls": {
|
||
"label": "iCalendar URL/s, separate multiple ones with a comma",
|
||
},
|
||
"ical_files": {
|
||
"label": "iCalendar filepaths, separated with a comma",
|
||
},
|
||
"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",
|
||
},
|
||
}
|
||
|
||
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']
|
||
|
||
if config['ical_urls'] and isinstance(config['ical_urls'], str):
|
||
self.ical_urls = config['ical_urls'].split(',')
|
||
else:
|
||
self.ical_urls = config['ical_urls']
|
||
|
||
if config['ical_files'] and isinstance(config['ical_files'], str):
|
||
self.ical_files = config['ical_files'].split(',')
|
||
else:
|
||
self.ical_files = config['ical_files']
|
||
|
||
# additional configuration
|
||
self.timezone = get_system_tz()
|
||
|
||
# 选择字体:优先使用支持中文的 NotoSansCJK,否则使用 NotoSans
|
||
self._font_family = self._select_font_family()
|
||
self.num_font = ImageFont.truetype(
|
||
fonts[self._font_family], size=self.fontsize
|
||
)
|
||
|
||
# give an OK message
|
||
logger.debug(f'{__name__} loaded')
|
||
|
||
def _select_font_family(self) -> str:
|
||
"""选择合适的字体族(支持中文优先)"""
|
||
# 优先级:NotoSansCJKsc (支持中文) > NotoSans
|
||
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
|
||
|
||
|
||
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_black,
|
||
(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
|