persian-clock-widget 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README-LINUX.md +128 -0
- package/README.md +263 -2
- package/assets/fonts/BYekan+ Bold.ttf +0 -0
- package/assets/fonts/Iranian Sans.ttf +0 -0
- package/assets/fonts/Vazir-Medium.ttf +0 -0
- package/assets/icons/app_icon.ico +0 -0
- package/assets/icons/app_icon.png +0 -0
- package/assets/sounds/alarm.mp3 +0 -0
- package/assets/sounds/axel.mp3 +0 -0
- package/assets/sounds/heeeeyyyyy.mp3 +0 -0
- package/assets/sounds/liilililililililii.mp3 +0 -0
- package/assets/sounds/morning_glory.mp3 +0 -0
- package/assets/sounds/peter_axel_f.mp3 +0 -0
- package/bin/persian-clock.js +60 -0
- package/main.py +26 -0
- package/package.json +39 -4
- package/release/windows/PersianClockWidget.exe +0 -0
- package/requirements.txt +3 -0
- package/src/__init__.py +8 -0
- package/src/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/__pycache__/alarm_manager.cpython-314.pyc +0 -0
- package/src/__pycache__/calendar_data.cpython-314.pyc +0 -0
- package/src/__pycache__/taskbar_clock.cpython-314.pyc +0 -0
- package/src/__pycache__/widget.cpython-314.pyc +0 -0
- package/src/__pycache__/win_utils.cpython-314.pyc +0 -0
- package/src/alarm_manager.py +1314 -0
- package/src/calendar_data.py +519 -0
- package/src/icon_generator.py +106 -0
- package/src/taskbar_clock.py +913 -0
- package/src/widget.py +3570 -0
- package/src/win_utils.py +618 -0
|
@@ -0,0 +1,1314 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
سیستم مدیریت رویدادها، یادآورها و آلارمهای هوشمند
|
|
4
|
+
ویژگیها:
|
|
5
|
+
- انتخابگر تاریخ به صورت تقویم گرافیکی تعاملی با قابلیت انتخاب یک یا چند روز خاص (Multi-Select Calendar)
|
|
6
|
+
- حذف خودکار آلارمهای تاریخدار پس از گذشت زمان آنها (در صورتی که Snooze / به تاخیر نیفتاده باشند)
|
|
7
|
+
- ۶ صدای آلارم MP3 داخلی با پیشفرض heeeeyyyyy.mp3
|
|
8
|
+
- پاپآپ زنگوله طلایی متحرک در مرکز مانیتور
|
|
9
|
+
طراحی و توسعه: شایان بامیار (® Shayan Bamyar)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import tkinter as tk
|
|
16
|
+
from tkinter import ttk
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from src.calendar_data import (
|
|
21
|
+
PERSIAN_MONTHS,
|
|
22
|
+
PERSIAN_WEEK_SHORT,
|
|
23
|
+
get_month_calendar,
|
|
24
|
+
get_today_info,
|
|
25
|
+
)
|
|
26
|
+
from src.win_utils import (
|
|
27
|
+
DEFAULT_ALARM_SOUND,
|
|
28
|
+
apply_rounded_corners,
|
|
29
|
+
get_available_alarm_sounds,
|
|
30
|
+
get_resource_path,
|
|
31
|
+
hide_from_taskbar,
|
|
32
|
+
play_mp3_sound,
|
|
33
|
+
stop_mp3_sound,
|
|
34
|
+
)
|
|
35
|
+
except ImportError:
|
|
36
|
+
from calendar_data import (
|
|
37
|
+
PERSIAN_MONTHS,
|
|
38
|
+
PERSIAN_WEEK_SHORT,
|
|
39
|
+
get_month_calendar,
|
|
40
|
+
get_today_info,
|
|
41
|
+
)
|
|
42
|
+
from win_utils import (
|
|
43
|
+
DEFAULT_ALARM_SOUND,
|
|
44
|
+
apply_rounded_corners,
|
|
45
|
+
get_available_alarm_sounds,
|
|
46
|
+
get_resource_path,
|
|
47
|
+
hide_from_taskbar,
|
|
48
|
+
play_mp3_sound,
|
|
49
|
+
stop_mp3_sound,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
PERSIAN_WEEKDAY_NAMES = [
|
|
53
|
+
"شنبه",
|
|
54
|
+
"یکشنبه",
|
|
55
|
+
"دوشنبه",
|
|
56
|
+
"سهشنبه",
|
|
57
|
+
"چهارشنبه",
|
|
58
|
+
"پنجشنبه",
|
|
59
|
+
"جمعه",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def draw_outlined_canvas_text(
|
|
64
|
+
canvas: tk.Canvas,
|
|
65
|
+
x: int,
|
|
66
|
+
y: int,
|
|
67
|
+
text: str,
|
|
68
|
+
font_spec: tuple,
|
|
69
|
+
fill: str,
|
|
70
|
+
outline: str = "#000000",
|
|
71
|
+
anchor: str = "center",
|
|
72
|
+
justify: str = "center",
|
|
73
|
+
width: int = 0,
|
|
74
|
+
tags: str | tuple = (),
|
|
75
|
+
):
|
|
76
|
+
"""رسم متن با خط دور مشکی (Stroke) روی بوم Canvas برای خوانایی عالی روی هر پسزمینه"""
|
|
77
|
+
offsets = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]
|
|
78
|
+
for dx, dy in offsets:
|
|
79
|
+
canvas.create_text(
|
|
80
|
+
x + dx,
|
|
81
|
+
y + dy,
|
|
82
|
+
text=text,
|
|
83
|
+
font=font_spec,
|
|
84
|
+
fill=outline,
|
|
85
|
+
anchor=anchor,
|
|
86
|
+
justify=justify,
|
|
87
|
+
width=width,
|
|
88
|
+
tags=tags,
|
|
89
|
+
)
|
|
90
|
+
return canvas.create_text(
|
|
91
|
+
x,
|
|
92
|
+
y,
|
|
93
|
+
text=text,
|
|
94
|
+
font=font_spec,
|
|
95
|
+
fill=fill,
|
|
96
|
+
anchor=anchor,
|
|
97
|
+
justify=justify,
|
|
98
|
+
width=width,
|
|
99
|
+
tags=tags,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _normalize_alarm_dates(alarm: dict) -> list[str]:
|
|
104
|
+
"""استخراج لیست تاریخهای شمسی (YYYY/MM/DD) از یک آلارم"""
|
|
105
|
+
dates = alarm.get("dates")
|
|
106
|
+
if isinstance(dates, list) and dates:
|
|
107
|
+
return sorted(set(str(d) for d in dates if d))
|
|
108
|
+
jy = alarm.get("jy")
|
|
109
|
+
jm = alarm.get("jm")
|
|
110
|
+
jd = alarm.get("jd")
|
|
111
|
+
if jy and jm and jd:
|
|
112
|
+
return [f"{int(jy):04d}/{int(jm):02d}/{int(jd):02d}"]
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AlarmStore:
|
|
117
|
+
"""
|
|
118
|
+
ذخیره، بارگذاری و بررسی لحظهای آلارمها و رویدادها
|
|
119
|
+
همراه با حذف خودکار آلارمهای تاریخدار پس از گذشت زمان آنها (در صورت عدم Snooze)
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, alarms_path: str):
|
|
123
|
+
self.alarms_path = alarms_path
|
|
124
|
+
self.alarms: list[dict] = []
|
|
125
|
+
self.snoozed: list[dict] = [] # لیست آلارمهای به تاخیر افتاده (Snoozed)
|
|
126
|
+
self.active_ringing_ids: set[str] = set() # آلارمهایی که هماکنون پنجره زنگوله آنها باز است
|
|
127
|
+
self.load()
|
|
128
|
+
|
|
129
|
+
def load(self):
|
|
130
|
+
if os.path.exists(self.alarms_path):
|
|
131
|
+
try:
|
|
132
|
+
with open(self.alarms_path, "r", encoding="utf-8") as f:
|
|
133
|
+
data = json.load(f)
|
|
134
|
+
if isinstance(data, list):
|
|
135
|
+
self.alarms = data
|
|
136
|
+
except Exception:
|
|
137
|
+
self.alarms = []
|
|
138
|
+
else:
|
|
139
|
+
self.alarms = []
|
|
140
|
+
|
|
141
|
+
def save(self):
|
|
142
|
+
try:
|
|
143
|
+
os.makedirs(os.path.dirname(self.alarms_path), exist_ok=True)
|
|
144
|
+
with open(self.alarms_path, "w", encoding="utf-8") as f:
|
|
145
|
+
json.dump(self.alarms, f, ensure_ascii=False, indent=2)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
def add_or_update(self, alarm_data: dict):
|
|
150
|
+
if not alarm_data.get("sound_file"):
|
|
151
|
+
alarm_data["sound_file"] = DEFAULT_ALARM_SOUND
|
|
152
|
+
aid = alarm_data.get("id")
|
|
153
|
+
if aid:
|
|
154
|
+
for idx, item in enumerate(self.alarms):
|
|
155
|
+
if item.get("id") == aid:
|
|
156
|
+
self.alarms[idx] = alarm_data
|
|
157
|
+
self.save()
|
|
158
|
+
return
|
|
159
|
+
else:
|
|
160
|
+
alarm_data["id"] = str(uuid.uuid4())[:8]
|
|
161
|
+
self.alarms.append(alarm_data)
|
|
162
|
+
self.save()
|
|
163
|
+
|
|
164
|
+
def delete(self, alarm_id: str):
|
|
165
|
+
self.alarms = [a for a in self.alarms if a.get("id") != alarm_id]
|
|
166
|
+
self.snoozed = [s for s in self.snoozed if s.get("alarm", {}).get("id") != alarm_id]
|
|
167
|
+
self.save()
|
|
168
|
+
|
|
169
|
+
def get_events_for_jalali_day(self, jy: int, jm: int, jd: int, persian_week_idx: int) -> list[dict]:
|
|
170
|
+
"""دریافت رویدادها و آلارمهای مربوط به یک روز خاص برای نمایش در تقویم"""
|
|
171
|
+
target_date_str = f"{jy:04d}/{jm:02d}/{jd:02d}"
|
|
172
|
+
matched = []
|
|
173
|
+
for a in self.alarms:
|
|
174
|
+
if not a.get("enabled", True):
|
|
175
|
+
continue
|
|
176
|
+
mode = a.get("repeat_mode", "once")
|
|
177
|
+
if mode in ("once", "dates"):
|
|
178
|
+
if target_date_str in _normalize_alarm_dates(a):
|
|
179
|
+
matched.append(a)
|
|
180
|
+
elif mode == "weekly":
|
|
181
|
+
if persian_week_idx in a.get("weekdays", []):
|
|
182
|
+
matched.append(a)
|
|
183
|
+
elif mode == "daily":
|
|
184
|
+
matched.append(a)
|
|
185
|
+
elif mode == "yearly":
|
|
186
|
+
if f"{jm:02d}-{jd:02d}" in a.get("yearly_days", []):
|
|
187
|
+
matched.append(a)
|
|
188
|
+
return matched
|
|
189
|
+
|
|
190
|
+
def cleanup_expired_alarms(self, today_info: dict) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
حذف خودکار آلارمهای تاریخدار (once / dates) که زمان آنها گذشته است
|
|
193
|
+
و در لیست Snooze یا در حال زنگ زدن نیستند.
|
|
194
|
+
"""
|
|
195
|
+
now_date_str = f"{today_info['jy']:04d}/{today_info['jm']:02d}/{today_info['jd']:02d}"
|
|
196
|
+
now_time_tuple = (today_info["hour"], today_info["minute"])
|
|
197
|
+
|
|
198
|
+
snoozed_ids = {s.get("alarm", {}).get("id") for s in self.snoozed}
|
|
199
|
+
new_alarms = []
|
|
200
|
+
changed = False
|
|
201
|
+
|
|
202
|
+
for a in self.alarms:
|
|
203
|
+
aid = a.get("id")
|
|
204
|
+
mode = a.get("repeat_mode", "once")
|
|
205
|
+
if mode in ("once", "dates"):
|
|
206
|
+
if aid in snoozed_ids or aid in self.active_ringing_ids:
|
|
207
|
+
new_alarms.append(a)
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
ah = int(a.get("hour", 0))
|
|
211
|
+
am = int(a.get("minute", 0))
|
|
212
|
+
remaining_dates = []
|
|
213
|
+
for d_str in _normalize_alarm_dates(a):
|
|
214
|
+
if d_str > now_date_str:
|
|
215
|
+
remaining_dates.append(d_str)
|
|
216
|
+
elif d_str == now_date_str and (ah, am) >= now_time_tuple:
|
|
217
|
+
# اگر در همین دقیقه فعلی است ولی هنوز اجرا نشده، نگه دار تا زنگ بزند
|
|
218
|
+
if a.get("last_triggered") != f"{now_date_str} {ah:02d}:{am:02d}":
|
|
219
|
+
remaining_dates.append(d_str)
|
|
220
|
+
|
|
221
|
+
if remaining_dates:
|
|
222
|
+
if remaining_dates != a.get("dates"):
|
|
223
|
+
a["dates"] = remaining_dates
|
|
224
|
+
parts = remaining_dates[0].split("/")
|
|
225
|
+
a["jy"], a["jm"], a["jd"] = int(parts[0]), int(parts[1]), int(parts[2])
|
|
226
|
+
changed = True
|
|
227
|
+
new_alarms.append(a)
|
|
228
|
+
else:
|
|
229
|
+
# تمام تاریخهای این آلارم گذشته و به تاخیر هم نیفتاده -> حذف کامل از لیست
|
|
230
|
+
changed = True
|
|
231
|
+
else:
|
|
232
|
+
new_alarms.append(a)
|
|
233
|
+
|
|
234
|
+
if changed:
|
|
235
|
+
self.alarms = new_alarms
|
|
236
|
+
self.save()
|
|
237
|
+
return changed
|
|
238
|
+
|
|
239
|
+
def check_due_alarms(self, today_info: dict) -> list[dict]:
|
|
240
|
+
"""بررسی آلارمهایی که در دقیقه فعلی به وقت تهران باید زنگ بزنند"""
|
|
241
|
+
now_h = today_info["hour"]
|
|
242
|
+
now_m = today_info["minute"]
|
|
243
|
+
jy, jm, jd = today_info["jy"], today_info["jm"], today_info["jd"]
|
|
244
|
+
p_week_idx = today_info["persian_week_idx"]
|
|
245
|
+
today_str = f"{jy:04d}/{jm:02d}/{jd:02d}"
|
|
246
|
+
stamp_key = f"{today_str} {now_h:02d}:{now_m:02d}"
|
|
247
|
+
|
|
248
|
+
triggered = []
|
|
249
|
+
|
|
250
|
+
# 1. بررسی Snoozeها
|
|
251
|
+
remaining_snoozed = []
|
|
252
|
+
for s in self.snoozed:
|
|
253
|
+
if s.get("target_h") == now_h and s.get("target_m") == now_m and s.get("stamp") != stamp_key:
|
|
254
|
+
s["stamp"] = stamp_key
|
|
255
|
+
triggered.append(s["alarm"])
|
|
256
|
+
else:
|
|
257
|
+
remaining_snoozed.append(s)
|
|
258
|
+
self.snoozed = remaining_snoozed
|
|
259
|
+
|
|
260
|
+
# 2. بررسی آلارمهای اصلی
|
|
261
|
+
changed = False
|
|
262
|
+
for a in self.alarms:
|
|
263
|
+
if not a.get("enabled", True):
|
|
264
|
+
continue
|
|
265
|
+
if int(a.get("hour", -1)) != now_h or int(a.get("minute", -1)) != now_m:
|
|
266
|
+
continue
|
|
267
|
+
if a.get("last_triggered") == stamp_key:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
mode = a.get("repeat_mode", "once")
|
|
271
|
+
is_match = False
|
|
272
|
+
if mode == "daily":
|
|
273
|
+
is_match = True
|
|
274
|
+
elif mode == "weekly":
|
|
275
|
+
if p_week_idx in a.get("weekdays", []):
|
|
276
|
+
is_match = True
|
|
277
|
+
elif mode == "yearly":
|
|
278
|
+
if f"{jm:02d}-{jd:02d}" in a.get("yearly_days", []):
|
|
279
|
+
is_match = True
|
|
280
|
+
elif mode in ("once", "dates"):
|
|
281
|
+
if today_str in _normalize_alarm_dates(a):
|
|
282
|
+
is_match = True
|
|
283
|
+
|
|
284
|
+
if is_match:
|
|
285
|
+
a["last_triggered"] = stamp_key
|
|
286
|
+
if a.get("id"):
|
|
287
|
+
self.active_ringing_ids.add(a["id"])
|
|
288
|
+
changed = True
|
|
289
|
+
triggered.append(a)
|
|
290
|
+
|
|
291
|
+
if changed:
|
|
292
|
+
self.save()
|
|
293
|
+
|
|
294
|
+
# پاکسازی آلارمهای تاریخگذشته که در حال زنگ زدن یا Snooze نیستند
|
|
295
|
+
self.cleanup_expired_alarms(today_info)
|
|
296
|
+
return triggered
|
|
297
|
+
|
|
298
|
+
def on_alarm_dismissed(self, alarm: dict):
|
|
299
|
+
"""وقتی کاربر آلارم را بدون Snooze میبندد، اگر تاریخدار است و تاریخی باقی نمانده از لیست حذف شود"""
|
|
300
|
+
aid = alarm.get("id")
|
|
301
|
+
if aid in self.active_ringing_ids:
|
|
302
|
+
self.active_ringing_ids.discard(aid)
|
|
303
|
+
self.cleanup_expired_alarms(get_today_info())
|
|
304
|
+
|
|
305
|
+
def add_snooze(self, alarm: dict, minutes_later: int = 5):
|
|
306
|
+
aid = alarm.get("id")
|
|
307
|
+
if aid in self.active_ringing_ids:
|
|
308
|
+
self.active_ringing_ids.discard(aid)
|
|
309
|
+
info = get_today_info()
|
|
310
|
+
total_m = info["hour"] * 60 + info["minute"] + minutes_later
|
|
311
|
+
th = (total_m // 60) % 24
|
|
312
|
+
tm = total_m % 60
|
|
313
|
+
self.snoozed.append({
|
|
314
|
+
"alarm": alarm,
|
|
315
|
+
"target_h": th,
|
|
316
|
+
"target_m": tm,
|
|
317
|
+
"stamp": "",
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class AnimatedBellPopup:
|
|
322
|
+
"""
|
|
323
|
+
پنجره هشدار دارک در مرکز مانیتور همراه با زنگوله طلایی متحرک (که تکان میخورد و زنگ میزند)
|
|
324
|
+
و پخش فایل صوتی MP3 انتخابشده (پیشفرض: heeeeyyyyy.mp3)
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
def __init__(self, root: tk.Tk, alarm: dict, font_family: str, on_snooze=None, on_dismiss=None):
|
|
328
|
+
self.root = root
|
|
329
|
+
self.alarm = alarm
|
|
330
|
+
self.font_family = font_family
|
|
331
|
+
self.on_snooze = on_snooze
|
|
332
|
+
self.on_dismiss = on_dismiss
|
|
333
|
+
self._running = True
|
|
334
|
+
self._sound_running = bool(alarm.get("play_sound", True))
|
|
335
|
+
self._sound_file = alarm.get("sound_file", DEFAULT_ALARM_SOUND) or DEFAULT_ALARM_SOUND
|
|
336
|
+
self._step = 0
|
|
337
|
+
|
|
338
|
+
self.win = tk.Toplevel(root)
|
|
339
|
+
self.win.title("هشدار یادآور و آلارم")
|
|
340
|
+
self.win.overrideredirect(True)
|
|
341
|
+
self.win.configure(bg="#0F172A")
|
|
342
|
+
self.win.attributes("-topmost", True)
|
|
343
|
+
|
|
344
|
+
w, h = 410, 320
|
|
345
|
+
sx = (self.win.winfo_screenwidth() - w) // 2
|
|
346
|
+
sy = (self.win.winfo_screenheight() - h) // 2
|
|
347
|
+
self.win.geometry(f"{w}x{h}+{sx}+{sy}")
|
|
348
|
+
|
|
349
|
+
try:
|
|
350
|
+
hide_from_taskbar(self.win)
|
|
351
|
+
apply_rounded_corners(self.win, w, h, radius=20, enabled=True)
|
|
352
|
+
except Exception:
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
outer = tk.Frame(
|
|
356
|
+
self.win,
|
|
357
|
+
bg="#0F172A",
|
|
358
|
+
highlightbackground="#F59E0B",
|
|
359
|
+
highlightthickness=2,
|
|
360
|
+
padx=18,
|
|
361
|
+
pady=14,
|
|
362
|
+
)
|
|
363
|
+
outer.pack(fill="both", expand=True)
|
|
364
|
+
|
|
365
|
+
self.canvas = tk.Canvas(
|
|
366
|
+
outer,
|
|
367
|
+
width=370,
|
|
368
|
+
height=225,
|
|
369
|
+
bg="#0F172A",
|
|
370
|
+
highlightthickness=0,
|
|
371
|
+
)
|
|
372
|
+
self.canvas.pack(fill="x")
|
|
373
|
+
|
|
374
|
+
time_str = f"{int(alarm.get('hour', 0)):02d}:{int(alarm.get('minute', 0)):02d}"
|
|
375
|
+
title_str = alarm.get("title", "").strip() or "زمان یادآور فرا رسیده است!"
|
|
376
|
+
|
|
377
|
+
draw_outlined_canvas_text(
|
|
378
|
+
self.canvas,
|
|
379
|
+
185,
|
|
380
|
+
122,
|
|
381
|
+
text=f"⏰ {time_str}",
|
|
382
|
+
font_spec=(self.font_family, 20, "bold"),
|
|
383
|
+
fill="#FBBF24",
|
|
384
|
+
outline="#000000",
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
draw_outlined_canvas_text(
|
|
388
|
+
self.canvas,
|
|
389
|
+
185,
|
|
390
|
+
168,
|
|
391
|
+
text=title_str,
|
|
392
|
+
font_spec=(self.font_family, 12, "bold"),
|
|
393
|
+
fill="#F8FAFC",
|
|
394
|
+
outline="#000000",
|
|
395
|
+
width=340,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
draw_outlined_canvas_text(
|
|
399
|
+
self.canvas,
|
|
400
|
+
185,
|
|
401
|
+
206,
|
|
402
|
+
text=f"🎵 {self._sound_file}",
|
|
403
|
+
font_spec=("Segoe UI", 8),
|
|
404
|
+
fill="#38BDF8",
|
|
405
|
+
outline="#000000",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
btn_row = tk.Frame(outer, bg="#0F172A")
|
|
409
|
+
btn_row.pack(fill="x", pady=(8, 0))
|
|
410
|
+
|
|
411
|
+
dismiss_btn = tk.Button(
|
|
412
|
+
btn_row,
|
|
413
|
+
text="✓ متوجه شدم (قطع آلارم)",
|
|
414
|
+
font=(self.font_family, 10, "bold"),
|
|
415
|
+
bg="#10B981",
|
|
416
|
+
fg="#FFFFFF",
|
|
417
|
+
activebackground="#059669",
|
|
418
|
+
activeforeground="#FFFFFF",
|
|
419
|
+
relief="flat",
|
|
420
|
+
padx=16,
|
|
421
|
+
pady=6,
|
|
422
|
+
cursor="hand2",
|
|
423
|
+
command=self._dismiss,
|
|
424
|
+
)
|
|
425
|
+
dismiss_btn.pack(side="right", expand=True, fill="x", padx=(4, 0))
|
|
426
|
+
|
|
427
|
+
snooze_btn = tk.Button(
|
|
428
|
+
btn_row,
|
|
429
|
+
text="💤 ۵ دقیقه دیگر (تاخیر)",
|
|
430
|
+
font=(self.font_family, 9, "bold"),
|
|
431
|
+
bg="#334155",
|
|
432
|
+
fg="#E2E8F0",
|
|
433
|
+
activebackground="#475569",
|
|
434
|
+
activeforeground="#FFFFFF",
|
|
435
|
+
relief="flat",
|
|
436
|
+
padx=12,
|
|
437
|
+
pady=6,
|
|
438
|
+
cursor="hand2",
|
|
439
|
+
command=self._snooze,
|
|
440
|
+
)
|
|
441
|
+
snooze_btn.pack(side="left", padx=(0, 4))
|
|
442
|
+
|
|
443
|
+
self._animate_bell()
|
|
444
|
+
if self._sound_running:
|
|
445
|
+
play_mp3_sound(self._sound_file, repeat=True)
|
|
446
|
+
self.win.after(60000, stop_mp3_sound)
|
|
447
|
+
|
|
448
|
+
def _animate_bell(self):
|
|
449
|
+
if not self._running or not self.win.winfo_exists():
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
self.canvas.delete("bell_anim")
|
|
453
|
+
cx, cy = 185, 52
|
|
454
|
+
angle = math.sin(self._step * 0.45) * 0.42
|
|
455
|
+
|
|
456
|
+
def rot(px, py):
|
|
457
|
+
dx, dy = px - cx, py - (cy - 22)
|
|
458
|
+
rx = dx * math.cos(angle) - dy * math.sin(angle)
|
|
459
|
+
ry = dx * math.sin(angle) + dy * math.cos(angle)
|
|
460
|
+
return cx + rx, (cy - 22) + ry
|
|
461
|
+
|
|
462
|
+
wave_alpha = (self._step % 10) < 6
|
|
463
|
+
if wave_alpha:
|
|
464
|
+
for offset_r in (38, 48):
|
|
465
|
+
self.canvas.create_arc(
|
|
466
|
+
cx - offset_r, cy - offset_r, cx + offset_r, cy + offset_r,
|
|
467
|
+
start=135, extent=90, style="arc",
|
|
468
|
+
outline="#38BDF8", width=2.5, tags="bell_anim"
|
|
469
|
+
)
|
|
470
|
+
self.canvas.create_arc(
|
|
471
|
+
cx - offset_r, cy - offset_r, cx + offset_r, cy + offset_r,
|
|
472
|
+
start=-45, extent=90, style="arc",
|
|
473
|
+
outline="#38BDF8", width=2.5, tags="bell_anim"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
bell_pts_local = [
|
|
477
|
+
(cx - 8, cy - 22),
|
|
478
|
+
(cx + 8, cy - 22),
|
|
479
|
+
(cx + 22, cy + 8),
|
|
480
|
+
(cx + 28, cy + 16),
|
|
481
|
+
(cx - 28, cy + 16),
|
|
482
|
+
(cx - 22, cy + 8),
|
|
483
|
+
]
|
|
484
|
+
rotated_pts = []
|
|
485
|
+
for px, py in bell_pts_local:
|
|
486
|
+
rx, ry = rot(px, py)
|
|
487
|
+
rotated_pts.extend([rx, ry])
|
|
488
|
+
|
|
489
|
+
hx, hy = rot(cx, cy - 26)
|
|
490
|
+
self.canvas.create_oval(
|
|
491
|
+
hx - 6, hy - 6, hx + 6, hy + 6,
|
|
492
|
+
fill="#F59E0B", outline="#000000", width=2, tags="bell_anim"
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
clapper_x, clapper_y = rot(cx + math.sin(self._step * 0.45) * 8, cy + 20)
|
|
496
|
+
self.canvas.create_oval(
|
|
497
|
+
clapper_x - 7, clapper_y - 7, clapper_x + 7, clapper_y + 7,
|
|
498
|
+
fill="#D97706", outline="#000000", width=2, tags="bell_anim"
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
self.canvas.create_polygon(
|
|
502
|
+
rotated_pts,
|
|
503
|
+
fill="#FBBF24",
|
|
504
|
+
outline="#000000",
|
|
505
|
+
width=2.5,
|
|
506
|
+
smooth=True,
|
|
507
|
+
tags="bell_anim",
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
self._step += 1
|
|
511
|
+
self.win.after(35, self._animate_bell)
|
|
512
|
+
|
|
513
|
+
def _dismiss(self):
|
|
514
|
+
if self.on_dismiss:
|
|
515
|
+
self.on_dismiss(self.alarm)
|
|
516
|
+
self.close()
|
|
517
|
+
|
|
518
|
+
def _snooze(self):
|
|
519
|
+
if self.on_snooze:
|
|
520
|
+
self.on_snooze(self.alarm)
|
|
521
|
+
self.close()
|
|
522
|
+
|
|
523
|
+
def close(self):
|
|
524
|
+
self._running = False
|
|
525
|
+
self._sound_running = False
|
|
526
|
+
stop_mp3_sound()
|
|
527
|
+
try:
|
|
528
|
+
if self.win and self.win.winfo_exists():
|
|
529
|
+
self.win.destroy()
|
|
530
|
+
except Exception:
|
|
531
|
+
pass
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def format_alarm_schedule_summary(alarm: dict) -> str:
|
|
535
|
+
mode = alarm.get("repeat_mode", "once")
|
|
536
|
+
if mode == "daily":
|
|
537
|
+
return "هر روز"
|
|
538
|
+
elif mode == "yearly":
|
|
539
|
+
ydays = alarm.get("yearly_days", [])
|
|
540
|
+
if not ydays:
|
|
541
|
+
return "سالانه"
|
|
542
|
+
parts_txt = []
|
|
543
|
+
for mmdd in ydays:
|
|
544
|
+
try:
|
|
545
|
+
m_s, d_s = mmdd.split("-")
|
|
546
|
+
m_i, d_i = int(m_s), int(d_s)
|
|
547
|
+
m_name = PERSIAN_MONTHS[m_i] if 1 <= m_i <= 12 else str(m_i)
|
|
548
|
+
parts_txt.append(f"{d_i} {m_name}")
|
|
549
|
+
except Exception:
|
|
550
|
+
parts_txt.append(str(mmdd))
|
|
551
|
+
return "هر سال (" + "، ".join(parts_txt) + ")"
|
|
552
|
+
elif mode == "weekly":
|
|
553
|
+
days = alarm.get("weekdays", [])
|
|
554
|
+
if not days:
|
|
555
|
+
return "هفتگی"
|
|
556
|
+
names = [f"هر {PERSIAN_WEEKDAY_NAMES[d]}" for d in sorted(days) if 0 <= d < 7]
|
|
557
|
+
return "، ".join(names)
|
|
558
|
+
else:
|
|
559
|
+
dates = _normalize_alarm_dates(alarm)
|
|
560
|
+
if not dates:
|
|
561
|
+
return "بدون تاریخ"
|
|
562
|
+
if len(dates) == 1:
|
|
563
|
+
parts = dates[0].split("/")
|
|
564
|
+
jy, jm, jd = int(parts[0]), int(parts[1]), int(parts[2])
|
|
565
|
+
m_name = PERSIAN_MONTHS[jm] if 1 <= jm <= 12 else str(jm)
|
|
566
|
+
return f"{jd} {m_name} {jy}"
|
|
567
|
+
return f"{len(dates)} تاریخ انتخابشده ({'، '.join(dates[:3])}{'...' if len(dates) > 3 else ''})"
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class AlarmManagerWindow:
|
|
571
|
+
"""
|
|
572
|
+
پنجره گرافیکی حرفهای برای افزودن، ویرایش، انتخاب صدای آلارم MP3 و حذف رویدادها
|
|
573
|
+
همراه با تقویم گرافیکی تعاملی جهت انتخاب یک یا چند روز خاص
|
|
574
|
+
"""
|
|
575
|
+
|
|
576
|
+
def __init__(
|
|
577
|
+
self,
|
|
578
|
+
root: tk.Tk,
|
|
579
|
+
store: AlarmStore,
|
|
580
|
+
font_family: str,
|
|
581
|
+
initial_jalali: tuple[int, int, int],
|
|
582
|
+
on_change_callback=None,
|
|
583
|
+
):
|
|
584
|
+
self.root = root
|
|
585
|
+
self.store = store
|
|
586
|
+
self.font_family = font_family
|
|
587
|
+
self.initial_jy, self.initial_jm, self.initial_jd = initial_jalali
|
|
588
|
+
self.on_change_callback = on_change_callback
|
|
589
|
+
self.editing_id: str | None = None
|
|
590
|
+
|
|
591
|
+
# وضعیت تقویم تعاملی داخل فرم رویدادها (قابلیت انتخاب چند روز خاص)
|
|
592
|
+
self.cal_jy = self.initial_jy
|
|
593
|
+
self.cal_jm = self.initial_jm
|
|
594
|
+
self.selected_dates: set[str] = {
|
|
595
|
+
f"{self.initial_jy:04d}/{self.initial_jm:02d}/{self.initial_jd:02d}"
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
self.win = tk.Toplevel(root)
|
|
599
|
+
self.win.title("مدیریت رویدادها و آلارمها")
|
|
600
|
+
self.win.configure(bg="#0F172A")
|
|
601
|
+
self.win.resizable(False, False)
|
|
602
|
+
self.win.attributes("-topmost", True)
|
|
603
|
+
|
|
604
|
+
ico_path = get_resource_path(os.path.join("assets", "icons", "app_icon.ico"))
|
|
605
|
+
if os.path.exists(ico_path):
|
|
606
|
+
try:
|
|
607
|
+
self.win.iconbitmap(ico_path)
|
|
608
|
+
except Exception:
|
|
609
|
+
pass
|
|
610
|
+
|
|
611
|
+
w, h = 560, 760
|
|
612
|
+
sx = (self.win.winfo_screenwidth() - w) // 2
|
|
613
|
+
sy = max(10, (self.win.winfo_screenheight() - h) // 2)
|
|
614
|
+
self.win.geometry(f"{w}x{h}+{sx}+{sy}")
|
|
615
|
+
|
|
616
|
+
self.win.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
617
|
+
self._build_ui()
|
|
618
|
+
self._refresh_list()
|
|
619
|
+
|
|
620
|
+
def _on_close(self):
|
|
621
|
+
stop_mp3_sound()
|
|
622
|
+
self.win.destroy()
|
|
623
|
+
|
|
624
|
+
def _build_ui(self):
|
|
625
|
+
main = tk.Frame(self.win, bg="#0F172A", padx=16, pady=12)
|
|
626
|
+
main.pack(fill="both", expand=True)
|
|
627
|
+
|
|
628
|
+
tk.Label(
|
|
629
|
+
main,
|
|
630
|
+
text="⏰ مدیریت رویدادها، یادآورها و آلارمها",
|
|
631
|
+
font=(self.font_family, 12, "bold"),
|
|
632
|
+
fg="#38BDF8",
|
|
633
|
+
bg="#0F172A",
|
|
634
|
+
anchor="e",
|
|
635
|
+
).pack(fill="x", pady=(0, 6))
|
|
636
|
+
|
|
637
|
+
self.form_frame = tk.LabelFrame(
|
|
638
|
+
main,
|
|
639
|
+
text=" ➕ ثبت رویداد / آلارم جدید ",
|
|
640
|
+
font=(self.font_family, 9, "bold"),
|
|
641
|
+
fg="#E2E8F0",
|
|
642
|
+
bg="#1E293B",
|
|
643
|
+
bd=1,
|
|
644
|
+
padx=12,
|
|
645
|
+
pady=8,
|
|
646
|
+
)
|
|
647
|
+
self.form_frame.pack(fill="x", pady=(0, 8))
|
|
648
|
+
|
|
649
|
+
# 1. عنوان و متن رویداد
|
|
650
|
+
row1 = tk.Frame(self.form_frame, bg="#1E293B")
|
|
651
|
+
row1.pack(fill="x", pady=2)
|
|
652
|
+
tk.Label(
|
|
653
|
+
row1,
|
|
654
|
+
text="متن اعلان / عنوان رویداد:",
|
|
655
|
+
font=(self.font_family, 9),
|
|
656
|
+
fg="#CBD5E1",
|
|
657
|
+
bg="#1E293B",
|
|
658
|
+
).pack(side="right", padx=(6, 0))
|
|
659
|
+
|
|
660
|
+
self.title_entry = tk.Entry(
|
|
661
|
+
row1,
|
|
662
|
+
font=(self.font_family, 9),
|
|
663
|
+
bg="#0F172A",
|
|
664
|
+
fg="#F8FAFC",
|
|
665
|
+
insertbackground="#38BDF8",
|
|
666
|
+
justify="right",
|
|
667
|
+
relief="flat",
|
|
668
|
+
)
|
|
669
|
+
self.title_entry.pack(side="left", fill="x", expand=True, ipady=4)
|
|
670
|
+
self.title_entry.insert(0, "یادآوری مهم")
|
|
671
|
+
|
|
672
|
+
# 2. ساعت و دقیقه به وقت تهران
|
|
673
|
+
row2 = tk.Frame(self.form_frame, bg="#1E293B")
|
|
674
|
+
row2.pack(fill="x", pady=4)
|
|
675
|
+
tk.Label(
|
|
676
|
+
row2,
|
|
677
|
+
text="ساعت هشدار (به وقت تهران):",
|
|
678
|
+
font=(self.font_family, 9),
|
|
679
|
+
fg="#CBD5E1",
|
|
680
|
+
bg="#1E293B",
|
|
681
|
+
).pack(side="right", padx=(6, 0))
|
|
682
|
+
|
|
683
|
+
now_info = get_today_info()
|
|
684
|
+
time_box = tk.Frame(row2, bg="#1E293B")
|
|
685
|
+
time_box.pack(side="left")
|
|
686
|
+
|
|
687
|
+
self.hour_spin = tk.Spinbox(
|
|
688
|
+
time_box,
|
|
689
|
+
from_=0,
|
|
690
|
+
to=23,
|
|
691
|
+
format="%02.0f",
|
|
692
|
+
width=4,
|
|
693
|
+
font=("Segoe UI", 10, "bold"),
|
|
694
|
+
bg="#0F172A",
|
|
695
|
+
fg="#38BDF8",
|
|
696
|
+
buttonbackground="#334155",
|
|
697
|
+
justify="center",
|
|
698
|
+
)
|
|
699
|
+
self.hour_spin.delete(0, "end")
|
|
700
|
+
self.hour_spin.insert(0, f"{now_info['hour']:02d}")
|
|
701
|
+
self.hour_spin.pack(side="left")
|
|
702
|
+
|
|
703
|
+
tk.Label(time_box, text=" : ", font=("Segoe UI", 10, "bold"), fg="#F8FAFC", bg="#1E293B").pack(side="left")
|
|
704
|
+
|
|
705
|
+
self.min_spin = tk.Spinbox(
|
|
706
|
+
time_box,
|
|
707
|
+
from_=0,
|
|
708
|
+
to=59,
|
|
709
|
+
format="%02.0f",
|
|
710
|
+
width=4,
|
|
711
|
+
font=("Segoe UI", 10, "bold"),
|
|
712
|
+
bg="#0F172A",
|
|
713
|
+
fg="#38BDF8",
|
|
714
|
+
buttonbackground="#334155",
|
|
715
|
+
justify="center",
|
|
716
|
+
)
|
|
717
|
+
self.min_spin.delete(0, "end")
|
|
718
|
+
self.min_spin.insert(0, f"{(now_info['minute'] + 1) % 60:02d}")
|
|
719
|
+
self.min_spin.pack(side="left")
|
|
720
|
+
|
|
721
|
+
# 3. نوع تکرار (انتخاب از روی تقویم / هر روز / روزهای هفته)
|
|
722
|
+
row3 = tk.Frame(self.form_frame, bg="#1E293B")
|
|
723
|
+
row3.pack(fill="x", pady=3)
|
|
724
|
+
tk.Label(
|
|
725
|
+
row3,
|
|
726
|
+
text="نوع زمانبندی:",
|
|
727
|
+
font=(self.font_family, 9),
|
|
728
|
+
fg="#CBD5E1",
|
|
729
|
+
bg="#1E293B",
|
|
730
|
+
).pack(side="right")
|
|
731
|
+
|
|
732
|
+
self.repeat_var = tk.StringVar(value="dates")
|
|
733
|
+
for val, lbl_t in [
|
|
734
|
+
("dates", "📅 انتخاب روز(ها) از تقویم"),
|
|
735
|
+
("daily", "🔄 هر روز"),
|
|
736
|
+
("weekly", "📆 روزهای خاص هفته (مثلاً هر دوشنبه)"),
|
|
737
|
+
]:
|
|
738
|
+
tk.Radiobutton(
|
|
739
|
+
row3,
|
|
740
|
+
text=lbl_t,
|
|
741
|
+
variable=self.repeat_var,
|
|
742
|
+
value=val,
|
|
743
|
+
command=self._update_schedule_subframe,
|
|
744
|
+
font=(self.font_family, 8, "bold"),
|
|
745
|
+
fg="#F8FAFC",
|
|
746
|
+
bg="#1E293B",
|
|
747
|
+
selectcolor="#0F172A",
|
|
748
|
+
activebackground="#1E293B",
|
|
749
|
+
).pack(side="right", padx=3)
|
|
750
|
+
|
|
751
|
+
self.schedule_subframe = tk.Frame(self.form_frame, bg="#1E293B")
|
|
752
|
+
self.schedule_subframe.pack(fill="x", pady=4)
|
|
753
|
+
|
|
754
|
+
# الف) تقویم گرافیکی تعاملی با قابلیت انتخاب چند روز (Multi-Select Mini Calendar)
|
|
755
|
+
self.calendar_picker_frame = tk.Frame(
|
|
756
|
+
self.schedule_subframe,
|
|
757
|
+
bg="#0F172A",
|
|
758
|
+
highlightbackground="#334155",
|
|
759
|
+
highlightthickness=1,
|
|
760
|
+
padx=10,
|
|
761
|
+
pady=6,
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
cal_nav = tk.Frame(self.calendar_picker_frame, bg="#0F172A")
|
|
765
|
+
cal_nav.pack(fill="x", pady=(0, 4))
|
|
766
|
+
|
|
767
|
+
next_btn = tk.Label(
|
|
768
|
+
cal_nav, text="‹ ماه بعد", font=(self.font_family, 8, "bold"),
|
|
769
|
+
fg="#F8FAFC", bg="#1E293B", padx=8, pady=2, cursor="hand2"
|
|
770
|
+
)
|
|
771
|
+
next_btn.pack(side="left")
|
|
772
|
+
next_btn.bind("<Button-1>", lambda e: self._change_picker_month(1))
|
|
773
|
+
|
|
774
|
+
clear_dates_btn = tk.Label(
|
|
775
|
+
cal_nav, text="پاک کردن انتخابها", font=(self.font_family, 7),
|
|
776
|
+
fg="#F87171", bg="#1E293B", padx=6, pady=2, cursor="hand2"
|
|
777
|
+
)
|
|
778
|
+
clear_dates_btn.pack(side="left", padx=4)
|
|
779
|
+
clear_dates_btn.bind("<Button-1>", lambda e: self._clear_selected_dates())
|
|
780
|
+
|
|
781
|
+
prev_btn = tk.Label(
|
|
782
|
+
cal_nav, text="ماه قبل ›", font=(self.font_family, 8, "bold"),
|
|
783
|
+
fg="#F8FAFC", bg="#1E293B", padx=8, pady=2, cursor="hand2"
|
|
784
|
+
)
|
|
785
|
+
prev_btn.pack(side="right")
|
|
786
|
+
prev_btn.bind("<Button-1>", lambda e: self._change_picker_month(-1))
|
|
787
|
+
|
|
788
|
+
self.picker_title_lbl = tk.Label(
|
|
789
|
+
cal_nav, text="", font=(self.font_family, 9, "bold"),
|
|
790
|
+
fg="#38BDF8", bg="#0F172A"
|
|
791
|
+
)
|
|
792
|
+
self.picker_title_lbl.pack(side="right", expand=True)
|
|
793
|
+
|
|
794
|
+
# سرستون روزهای هفته در تقویم انتخابگر
|
|
795
|
+
wk_row = tk.Frame(self.calendar_picker_frame, bg="#0F172A")
|
|
796
|
+
wk_row.pack(fill="x")
|
|
797
|
+
for col_idx in range(7):
|
|
798
|
+
p_idx = 6 - col_idx
|
|
799
|
+
tk.Label(
|
|
800
|
+
wk_row,
|
|
801
|
+
text=PERSIAN_WEEK_SHORT[p_idx],
|
|
802
|
+
font=(self.font_family, 8, "bold"),
|
|
803
|
+
fg="#F87171" if p_idx == 6 else "#94A3B8",
|
|
804
|
+
bg="#0F172A",
|
|
805
|
+
width=4,
|
|
806
|
+
).grid(row=0, column=col_idx, sticky="nsew", padx=1)
|
|
807
|
+
wk_row.grid_columnconfigure(col_idx, weight=1)
|
|
808
|
+
|
|
809
|
+
# شبکه روزهای تقویم انتخابگر
|
|
810
|
+
self.picker_grid = tk.Frame(self.calendar_picker_frame, bg="#0F172A")
|
|
811
|
+
self.picker_grid.pack(fill="x", pady=2)
|
|
812
|
+
self.picker_cells: list[tk.Label] = []
|
|
813
|
+
for r in range(6):
|
|
814
|
+
self.picker_grid.grid_rowconfigure(r, weight=1)
|
|
815
|
+
for c in range(7):
|
|
816
|
+
self.picker_grid.grid_columnconfigure(c, weight=1)
|
|
817
|
+
lbl = tk.Label(
|
|
818
|
+
self.picker_grid,
|
|
819
|
+
text="",
|
|
820
|
+
font=("Segoe UI", 8, "bold"),
|
|
821
|
+
fg="#F8FAFC",
|
|
822
|
+
bg="#1E293B",
|
|
823
|
+
width=4,
|
|
824
|
+
pady=2,
|
|
825
|
+
cursor="hand2",
|
|
826
|
+
)
|
|
827
|
+
lbl.grid(row=r, column=c, sticky="nsew", padx=1, pady=1)
|
|
828
|
+
self.picker_cells.append(lbl)
|
|
829
|
+
|
|
830
|
+
self.selected_dates_summary_lbl = tk.Label(
|
|
831
|
+
self.calendar_picker_frame,
|
|
832
|
+
text="",
|
|
833
|
+
font=(self.font_family, 8),
|
|
834
|
+
fg="#10B981",
|
|
835
|
+
bg="#0F172A",
|
|
836
|
+
anchor="e",
|
|
837
|
+
justify="right",
|
|
838
|
+
)
|
|
839
|
+
self.selected_dates_summary_lbl.pack(fill="x", pady=(3, 0))
|
|
840
|
+
|
|
841
|
+
# ب) فریم انتخاب روزهای هفته (شنبه تا جمعه)
|
|
842
|
+
self.weekly_frame = tk.Frame(self.schedule_subframe, bg="#1E293B")
|
|
843
|
+
self.weekday_vars: list[tk.BooleanVar] = []
|
|
844
|
+
for idx, w_name in enumerate(PERSIAN_WEEKDAY_NAMES):
|
|
845
|
+
var = tk.BooleanVar(value=(idx == now_info["persian_week_idx"]))
|
|
846
|
+
self.weekday_vars.append(var)
|
|
847
|
+
tk.Checkbutton(
|
|
848
|
+
self.weekly_frame,
|
|
849
|
+
text=w_name,
|
|
850
|
+
variable=var,
|
|
851
|
+
font=(self.font_family, 8),
|
|
852
|
+
fg="#F8FAFC",
|
|
853
|
+
bg="#1E293B",
|
|
854
|
+
selectcolor="#0F172A",
|
|
855
|
+
activebackground="#1E293B",
|
|
856
|
+
padx=2,
|
|
857
|
+
).pack(side="right")
|
|
858
|
+
|
|
859
|
+
self._render_picker_calendar()
|
|
860
|
+
self._update_schedule_subframe()
|
|
861
|
+
|
|
862
|
+
# 4. انتخاب صدای آلارم MP3 (پیشفرض: heeeeyyyyy.mp3)
|
|
863
|
+
sound_row = tk.Frame(self.form_frame, bg="#1E293B")
|
|
864
|
+
sound_row.pack(fill="x", pady=4)
|
|
865
|
+
tk.Label(
|
|
866
|
+
sound_row,
|
|
867
|
+
text="🎵 انتخاب صدای آلارم:",
|
|
868
|
+
font=(self.font_family, 9, "bold"),
|
|
869
|
+
fg="#38BDF8",
|
|
870
|
+
bg="#1E293B",
|
|
871
|
+
).pack(side="right", padx=(6, 0))
|
|
872
|
+
|
|
873
|
+
available_sounds = get_available_alarm_sounds()
|
|
874
|
+
self.sound_cb = ttk.Combobox(
|
|
875
|
+
sound_row,
|
|
876
|
+
values=available_sounds,
|
|
877
|
+
state="readonly",
|
|
878
|
+
width=22,
|
|
879
|
+
)
|
|
880
|
+
if DEFAULT_ALARM_SOUND in available_sounds:
|
|
881
|
+
self.sound_cb.set(DEFAULT_ALARM_SOUND)
|
|
882
|
+
elif available_sounds:
|
|
883
|
+
self.sound_cb.current(0)
|
|
884
|
+
self.sound_cb.pack(side="left", padx=(0, 6))
|
|
885
|
+
|
|
886
|
+
tk.Button(
|
|
887
|
+
sound_row,
|
|
888
|
+
text="▶ تست صدا",
|
|
889
|
+
font=(self.font_family, 8, "bold"),
|
|
890
|
+
bg="#065F46",
|
|
891
|
+
fg="#A7F3D0",
|
|
892
|
+
relief="flat",
|
|
893
|
+
padx=8,
|
|
894
|
+
cursor="hand2",
|
|
895
|
+
command=lambda: play_mp3_sound(self.sound_cb.get(), repeat=False),
|
|
896
|
+
).pack(side="left", padx=2)
|
|
897
|
+
|
|
898
|
+
tk.Button(
|
|
899
|
+
sound_row,
|
|
900
|
+
text="⏹ توقف",
|
|
901
|
+
font=(self.font_family, 8),
|
|
902
|
+
bg="#7F1D1D",
|
|
903
|
+
fg="#FECACA",
|
|
904
|
+
relief="flat",
|
|
905
|
+
padx=6,
|
|
906
|
+
cursor="hand2",
|
|
907
|
+
command=stop_mp3_sound,
|
|
908
|
+
).pack(side="left", padx=2)
|
|
909
|
+
|
|
910
|
+
# 5. گزینههای فعال بودن صدا و پاپآپ زنگوله
|
|
911
|
+
row4 = tk.Frame(self.form_frame, bg="#1E293B")
|
|
912
|
+
row4.pack(fill="x", pady=(2, 4))
|
|
913
|
+
|
|
914
|
+
self.sound_var = tk.BooleanVar(value=True)
|
|
915
|
+
tk.Checkbutton(
|
|
916
|
+
row4,
|
|
917
|
+
text="🔊 پخش صدای آلارم",
|
|
918
|
+
variable=self.sound_var,
|
|
919
|
+
font=(self.font_family, 8, "bold"),
|
|
920
|
+
fg="#FBBF24",
|
|
921
|
+
bg="#1E293B",
|
|
922
|
+
selectcolor="#0F172A",
|
|
923
|
+
activebackground="#1E293B",
|
|
924
|
+
).pack(side="right", padx=6)
|
|
925
|
+
|
|
926
|
+
self.popup_var = tk.BooleanVar(value=True)
|
|
927
|
+
tk.Checkbutton(
|
|
928
|
+
row4,
|
|
929
|
+
text="🔔 نمایش زنگوله متحرک و متن در مرکز مانیتور",
|
|
930
|
+
variable=self.popup_var,
|
|
931
|
+
font=(self.font_family, 8, "bold"),
|
|
932
|
+
fg="#38BDF8",
|
|
933
|
+
bg="#1E293B",
|
|
934
|
+
selectcolor="#0F172A",
|
|
935
|
+
activebackground="#1E293B",
|
|
936
|
+
).pack(side="right", padx=6)
|
|
937
|
+
|
|
938
|
+
btn_bar = tk.Frame(self.form_frame, bg="#1E293B")
|
|
939
|
+
btn_bar.pack(fill="x", pady=(4, 0))
|
|
940
|
+
|
|
941
|
+
self.save_btn = tk.Button(
|
|
942
|
+
btn_bar,
|
|
943
|
+
text="➕ افزودن به لیست آلارمها",
|
|
944
|
+
font=(self.font_family, 9, "bold"),
|
|
945
|
+
bg="#0284C7",
|
|
946
|
+
fg="#FFFFFF",
|
|
947
|
+
relief="flat",
|
|
948
|
+
padx=14,
|
|
949
|
+
pady=4,
|
|
950
|
+
cursor="hand2",
|
|
951
|
+
command=self._save_alarm_from_form,
|
|
952
|
+
)
|
|
953
|
+
self.save_btn.pack(side="right")
|
|
954
|
+
|
|
955
|
+
self.cancel_edit_btn = tk.Button(
|
|
956
|
+
btn_bar,
|
|
957
|
+
text="انصراف از ویرایش",
|
|
958
|
+
font=(self.font_family, 8),
|
|
959
|
+
bg="#475569",
|
|
960
|
+
fg="#FFFFFF",
|
|
961
|
+
relief="flat",
|
|
962
|
+
padx=8,
|
|
963
|
+
pady=4,
|
|
964
|
+
cursor="hand2",
|
|
965
|
+
command=self._reset_form,
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
tk.Button(
|
|
969
|
+
btn_bar,
|
|
970
|
+
text="🔔 پیشنمایش زنگوله + صدا",
|
|
971
|
+
font=(self.font_family, 8),
|
|
972
|
+
bg="#334155",
|
|
973
|
+
fg="#FBBF24",
|
|
974
|
+
relief="flat",
|
|
975
|
+
padx=10,
|
|
976
|
+
pady=4,
|
|
977
|
+
cursor="hand2",
|
|
978
|
+
command=self._test_current_form,
|
|
979
|
+
).pack(side="left")
|
|
980
|
+
|
|
981
|
+
list_label_frame = tk.LabelFrame(
|
|
982
|
+
main,
|
|
983
|
+
text=" 📋 لیست آلارمها و رویدادهای فعال ",
|
|
984
|
+
font=(self.font_family, 9, "bold"),
|
|
985
|
+
fg="#E2E8F0",
|
|
986
|
+
bg="#1E293B",
|
|
987
|
+
bd=1,
|
|
988
|
+
padx=8,
|
|
989
|
+
pady=6,
|
|
990
|
+
)
|
|
991
|
+
list_label_frame.pack(fill="both", expand=True)
|
|
992
|
+
|
|
993
|
+
self.list_canvas = tk.Canvas(list_label_frame, bg="#1E293B", highlightthickness=0)
|
|
994
|
+
scrollbar = ttk.Scrollbar(list_label_frame, orient="vertical", command=self.list_canvas.yview)
|
|
995
|
+
self.list_inner = tk.Frame(self.list_canvas, bg="#1E293B")
|
|
996
|
+
|
|
997
|
+
self.list_inner.bind(
|
|
998
|
+
"<Configure>",
|
|
999
|
+
lambda e: self.list_canvas.configure(scrollregion=self.list_canvas.bbox("all")),
|
|
1000
|
+
)
|
|
1001
|
+
self.list_canvas.create_window((0, 0), window=self.list_inner, anchor="nw", width=500)
|
|
1002
|
+
self.list_canvas.configure(yscrollcommand=scrollbar.set)
|
|
1003
|
+
|
|
1004
|
+
self.list_canvas.pack(side="left", fill="both", expand=True)
|
|
1005
|
+
scrollbar.pack(side="right", fill="y")
|
|
1006
|
+
|
|
1007
|
+
footer = tk.Frame(main, bg="#0F172A")
|
|
1008
|
+
footer.pack(fill="x", pady=(6, 0))
|
|
1009
|
+
tk.Label(
|
|
1010
|
+
footer,
|
|
1011
|
+
text="® Shayan Bamyar",
|
|
1012
|
+
font=("Segoe UI", 8, "bold"),
|
|
1013
|
+
fg="#64748B",
|
|
1014
|
+
bg="#0F172A",
|
|
1015
|
+
).pack(side="left")
|
|
1016
|
+
|
|
1017
|
+
tk.Button(
|
|
1018
|
+
footer,
|
|
1019
|
+
text="بستن پنجره",
|
|
1020
|
+
font=(self.font_family, 9),
|
|
1021
|
+
bg="#334155",
|
|
1022
|
+
fg="#F8FAFC",
|
|
1023
|
+
relief="flat",
|
|
1024
|
+
padx=14,
|
|
1025
|
+
pady=3,
|
|
1026
|
+
cursor="hand2",
|
|
1027
|
+
command=self._on_close,
|
|
1028
|
+
).pack(side="right")
|
|
1029
|
+
|
|
1030
|
+
# -----------------------------------------------------------------------
|
|
1031
|
+
# منطق تقویم گرافیکی تعاملی با قابلیت انتخاب چند روز خاص
|
|
1032
|
+
# -----------------------------------------------------------------------
|
|
1033
|
+
def _change_picker_month(self, delta: int):
|
|
1034
|
+
m = self.cal_jm + delta
|
|
1035
|
+
y = self.cal_jy
|
|
1036
|
+
if m > 12:
|
|
1037
|
+
m = 1
|
|
1038
|
+
y += 1
|
|
1039
|
+
elif m < 1:
|
|
1040
|
+
m = 12
|
|
1041
|
+
y -= 1
|
|
1042
|
+
self.cal_jy, self.cal_jm = y, m
|
|
1043
|
+
self._render_picker_calendar()
|
|
1044
|
+
|
|
1045
|
+
def _clear_selected_dates(self):
|
|
1046
|
+
today = get_today_info()
|
|
1047
|
+
self.selected_dates = {f"{today['jy']:04d}/{today['jm']:02d}/{today['jd']:02d}"}
|
|
1048
|
+
self._render_picker_calendar()
|
|
1049
|
+
|
|
1050
|
+
def _toggle_picker_date(self, date_str: str):
|
|
1051
|
+
if date_str in self.selected_dates:
|
|
1052
|
+
if len(self.selected_dates) > 1:
|
|
1053
|
+
self.selected_dates.remove(date_str)
|
|
1054
|
+
else:
|
|
1055
|
+
self.selected_dates.add(date_str)
|
|
1056
|
+
self._render_picker_calendar()
|
|
1057
|
+
|
|
1058
|
+
def _render_picker_calendar(self):
|
|
1059
|
+
m_data = get_month_calendar(self.cal_jy, self.cal_jm)
|
|
1060
|
+
self.picker_title_lbl.configure(
|
|
1061
|
+
text=f"{m_data['header_title']} (با کلیک یک یا چند روز را انتخاب کنید)"
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
for cell in self.picker_cells:
|
|
1065
|
+
cell.configure(text="", bg="#0F172A", cursor="arrow")
|
|
1066
|
+
cell.unbind("<Button-1>")
|
|
1067
|
+
|
|
1068
|
+
first_w = m_data["first_day_week_idx"]
|
|
1069
|
+
for d_info in m_data["days"]:
|
|
1070
|
+
d = d_info["jd"]
|
|
1071
|
+
offset = first_w + (d - 1)
|
|
1072
|
+
row = offset // 7
|
|
1073
|
+
p_col = offset % 7
|
|
1074
|
+
g_col = 6 - p_col
|
|
1075
|
+
idx = row * 7 + g_col
|
|
1076
|
+
if idx >= len(self.picker_cells):
|
|
1077
|
+
continue
|
|
1078
|
+
|
|
1079
|
+
cell = self.picker_cells[idx]
|
|
1080
|
+
d_str = f"{self.cal_jy:04d}/{self.cal_jm:02d}/{d:02d}"
|
|
1081
|
+
is_sel = d_str in self.selected_dates
|
|
1082
|
+
|
|
1083
|
+
if is_sel:
|
|
1084
|
+
bg_c = "#10B981" # سبز زمردی برای روزهای انتخابشده
|
|
1085
|
+
fg_c = "#FFFFFF"
|
|
1086
|
+
else:
|
|
1087
|
+
bg_c = "#1E293B"
|
|
1088
|
+
fg_c = "#F87171" if d_info["is_holiday"] else "#E2E8F0"
|
|
1089
|
+
|
|
1090
|
+
cell.configure(text=str(d), bg=bg_c, fg=fg_c, cursor="hand2")
|
|
1091
|
+
cell.bind("<Button-1>", lambda e, ds=d_str: self._toggle_picker_date(ds))
|
|
1092
|
+
|
|
1093
|
+
sorted_d = sorted(self.selected_dates)
|
|
1094
|
+
summary = f"✓ روزهای انتخابشده ({len(sorted_d)} روز): " + " ، ".join(sorted_d[:4])
|
|
1095
|
+
if len(sorted_d) > 4:
|
|
1096
|
+
summary += f" و {len(sorted_d) - 4} روز دیگر"
|
|
1097
|
+
self.selected_dates_summary_lbl.configure(text=summary)
|
|
1098
|
+
|
|
1099
|
+
def _update_schedule_subframe(self):
|
|
1100
|
+
self.calendar_picker_frame.pack_forget()
|
|
1101
|
+
self.weekly_frame.pack_forget()
|
|
1102
|
+
mode = self.repeat_var.get()
|
|
1103
|
+
if mode in ("once", "dates"):
|
|
1104
|
+
self.calendar_picker_frame.pack(fill="x")
|
|
1105
|
+
elif mode == "weekly":
|
|
1106
|
+
self.weekly_frame.pack(fill="x")
|
|
1107
|
+
|
|
1108
|
+
def _collect_form_data(self) -> dict:
|
|
1109
|
+
try:
|
|
1110
|
+
h = max(0, min(23, int(self.hour_spin.get())))
|
|
1111
|
+
except ValueError:
|
|
1112
|
+
h = 12
|
|
1113
|
+
try:
|
|
1114
|
+
m = max(0, min(59, int(self.min_spin.get())))
|
|
1115
|
+
except ValueError:
|
|
1116
|
+
m = 0
|
|
1117
|
+
|
|
1118
|
+
sorted_dates = sorted(self.selected_dates)
|
|
1119
|
+
if not sorted_dates:
|
|
1120
|
+
sorted_dates = [f"{self.initial_jy:04d}/{self.initial_jm:02d}/{self.initial_jd:02d}"]
|
|
1121
|
+
first_parts = sorted_dates[0].split("/")
|
|
1122
|
+
jy, jm, jd = int(first_parts[0]), int(first_parts[1]), int(first_parts[2])
|
|
1123
|
+
|
|
1124
|
+
weekdays = [i for i, v in enumerate(self.weekday_vars) if v.get()]
|
|
1125
|
+
title = self.title_entry.get().strip() or "یادآور"
|
|
1126
|
+
sound_file = self.sound_cb.get() or DEFAULT_ALARM_SOUND
|
|
1127
|
+
|
|
1128
|
+
return {
|
|
1129
|
+
"id": self.editing_id,
|
|
1130
|
+
"title": title,
|
|
1131
|
+
"hour": h,
|
|
1132
|
+
"minute": m,
|
|
1133
|
+
"repeat_mode": self.repeat_var.get(),
|
|
1134
|
+
"dates": sorted_dates,
|
|
1135
|
+
"jy": jy,
|
|
1136
|
+
"jm": jm,
|
|
1137
|
+
"jd": jd,
|
|
1138
|
+
"weekdays": weekdays,
|
|
1139
|
+
"sound_file": sound_file,
|
|
1140
|
+
"play_sound": bool(self.sound_var.get()),
|
|
1141
|
+
"show_popup": bool(self.popup_var.get()),
|
|
1142
|
+
"enabled": True,
|
|
1143
|
+
"last_triggered": "",
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
def _save_alarm_from_form(self):
|
|
1147
|
+
stop_mp3_sound()
|
|
1148
|
+
data = self._collect_form_data()
|
|
1149
|
+
self.store.add_or_update(data)
|
|
1150
|
+
self._reset_form()
|
|
1151
|
+
self._refresh_list()
|
|
1152
|
+
if self.on_change_callback:
|
|
1153
|
+
self.on_change_callback()
|
|
1154
|
+
|
|
1155
|
+
def _test_current_form(self):
|
|
1156
|
+
data = self._collect_form_data()
|
|
1157
|
+
AnimatedBellPopup(self.root, data, self.font_family)
|
|
1158
|
+
|
|
1159
|
+
def _reset_form(self):
|
|
1160
|
+
self.editing_id = None
|
|
1161
|
+
self.form_frame.configure(text=" ➕ ثبت رویداد / آلارم جدید ")
|
|
1162
|
+
self.save_btn.configure(text="➕ افزودن به لیست آلارمها", bg="#0284C7")
|
|
1163
|
+
self.cancel_edit_btn.pack_forget()
|
|
1164
|
+
self.title_entry.delete(0, "end")
|
|
1165
|
+
self.title_entry.insert(0, "یادآوری مهم")
|
|
1166
|
+
self.sound_cb.set(DEFAULT_ALARM_SOUND)
|
|
1167
|
+
self.selected_dates = {
|
|
1168
|
+
f"{self.initial_jy:04d}/{self.initial_jm:02d}/{self.initial_jd:02d}"
|
|
1169
|
+
}
|
|
1170
|
+
self._render_picker_calendar()
|
|
1171
|
+
|
|
1172
|
+
def _load_into_form(self, alarm: dict):
|
|
1173
|
+
self.editing_id = alarm.get("id")
|
|
1174
|
+
self.form_frame.configure(text=" ✏️ در حال ویرایش آلارم انتخابشده ")
|
|
1175
|
+
self.save_btn.configure(text="💾 ذخیره تغییرات", bg="#10B981")
|
|
1176
|
+
self.cancel_edit_btn.pack(side="right", padx=6)
|
|
1177
|
+
|
|
1178
|
+
self.title_entry.delete(0, "end")
|
|
1179
|
+
self.title_entry.insert(0, alarm.get("title", ""))
|
|
1180
|
+
|
|
1181
|
+
self.hour_spin.delete(0, "end")
|
|
1182
|
+
self.hour_spin.insert(0, f"{int(alarm.get('hour', 0)):02d}")
|
|
1183
|
+
self.min_spin.delete(0, "end")
|
|
1184
|
+
self.min_spin.insert(0, f"{int(alarm.get('minute', 0)):02d}")
|
|
1185
|
+
|
|
1186
|
+
mode = alarm.get("repeat_mode", "dates")
|
|
1187
|
+
if mode == "once":
|
|
1188
|
+
mode = "dates"
|
|
1189
|
+
self.repeat_var.set(mode)
|
|
1190
|
+
|
|
1191
|
+
norm_dates = _normalize_alarm_dates(alarm)
|
|
1192
|
+
if norm_dates:
|
|
1193
|
+
self.selected_dates = set(norm_dates)
|
|
1194
|
+
first_p = norm_dates[0].split("/")
|
|
1195
|
+
self.cal_jy, self.cal_jm = int(first_p[0]), int(first_p[1])
|
|
1196
|
+
self._render_picker_calendar()
|
|
1197
|
+
|
|
1198
|
+
w_days = set(alarm.get("weekdays", []))
|
|
1199
|
+
for idx, var in enumerate(self.weekday_vars):
|
|
1200
|
+
var.set(idx in w_days)
|
|
1201
|
+
|
|
1202
|
+
self.sound_cb.set(alarm.get("sound_file", DEFAULT_ALARM_SOUND) or DEFAULT_ALARM_SOUND)
|
|
1203
|
+
self.sound_var.set(bool(alarm.get("play_sound", True)))
|
|
1204
|
+
self.popup_var.set(bool(alarm.get("show_popup", True)))
|
|
1205
|
+
self._update_schedule_subframe()
|
|
1206
|
+
|
|
1207
|
+
def _toggle_item_enabled(self, alarm: dict):
|
|
1208
|
+
alarm["enabled"] = not alarm.get("enabled", True)
|
|
1209
|
+
self.store.save()
|
|
1210
|
+
self._refresh_list()
|
|
1211
|
+
if self.on_change_callback:
|
|
1212
|
+
self.on_change_callback()
|
|
1213
|
+
|
|
1214
|
+
def _delete_item(self, alarm_id: str):
|
|
1215
|
+
self.store.delete(alarm_id)
|
|
1216
|
+
if self.editing_id == alarm_id:
|
|
1217
|
+
self._reset_form()
|
|
1218
|
+
self._refresh_list()
|
|
1219
|
+
if self.on_change_callback:
|
|
1220
|
+
self.on_change_callback()
|
|
1221
|
+
|
|
1222
|
+
def _refresh_list(self):
|
|
1223
|
+
for child in self.list_inner.winfo_children():
|
|
1224
|
+
child.destroy()
|
|
1225
|
+
|
|
1226
|
+
if not self.store.alarms:
|
|
1227
|
+
tk.Label(
|
|
1228
|
+
self.list_inner,
|
|
1229
|
+
text="هیچ رویداد یا آلارم فعالی در لیست وجود ندارد.",
|
|
1230
|
+
font=(self.font_family, 9),
|
|
1231
|
+
fg="#64748B",
|
|
1232
|
+
bg="#1E293B",
|
|
1233
|
+
pady=18,
|
|
1234
|
+
).pack(fill="x")
|
|
1235
|
+
return
|
|
1236
|
+
|
|
1237
|
+
for alarm in self.store.alarms:
|
|
1238
|
+
card = tk.Frame(
|
|
1239
|
+
self.list_inner,
|
|
1240
|
+
bg="#0F172A",
|
|
1241
|
+
highlightbackground="#334155",
|
|
1242
|
+
highlightthickness=1,
|
|
1243
|
+
padx=8,
|
|
1244
|
+
pady=6,
|
|
1245
|
+
)
|
|
1246
|
+
card.pack(fill="x", pady=3, padx=2)
|
|
1247
|
+
|
|
1248
|
+
is_en = alarm.get("enabled", True)
|
|
1249
|
+
t_str = f"{int(alarm.get('hour', 0)):02d}:{int(alarm.get('minute', 0)):02d}"
|
|
1250
|
+
sched_str = format_alarm_schedule_summary(alarm)
|
|
1251
|
+
snd_name = alarm.get("sound_file", DEFAULT_ALARM_SOUND)
|
|
1252
|
+
icons = []
|
|
1253
|
+
if alarm.get("play_sound", True):
|
|
1254
|
+
icons.append(f"🔊({snd_name})")
|
|
1255
|
+
if alarm.get("show_popup", True):
|
|
1256
|
+
icons.append("🔔")
|
|
1257
|
+
icons_str = " ".join(icons)
|
|
1258
|
+
|
|
1259
|
+
tk.Button(
|
|
1260
|
+
card,
|
|
1261
|
+
text="🗑 حذف",
|
|
1262
|
+
font=(self.font_family, 8),
|
|
1263
|
+
bg="#7F1D1D",
|
|
1264
|
+
fg="#FECACA",
|
|
1265
|
+
relief="flat",
|
|
1266
|
+
padx=6,
|
|
1267
|
+
cursor="hand2",
|
|
1268
|
+
command=lambda aid=alarm["id"]: self._delete_item(aid),
|
|
1269
|
+
).pack(side="left", padx=2)
|
|
1270
|
+
|
|
1271
|
+
tk.Button(
|
|
1272
|
+
card,
|
|
1273
|
+
text="✏️ ویرایش",
|
|
1274
|
+
font=(self.font_family, 8),
|
|
1275
|
+
bg="#1E3A8A",
|
|
1276
|
+
fg="#BFDBFE",
|
|
1277
|
+
relief="flat",
|
|
1278
|
+
padx=6,
|
|
1279
|
+
cursor="hand2",
|
|
1280
|
+
command=lambda a=alarm: self._load_into_form(a),
|
|
1281
|
+
).pack(side="left", padx=2)
|
|
1282
|
+
|
|
1283
|
+
tk.Button(
|
|
1284
|
+
card,
|
|
1285
|
+
text="فعال" if is_en else "غیرفعال",
|
|
1286
|
+
font=(self.font_family, 8, "bold"),
|
|
1287
|
+
bg="#065F46" if is_en else "#334155",
|
|
1288
|
+
fg="#A7F3D0" if is_en else "#94A3B8",
|
|
1289
|
+
relief="flat",
|
|
1290
|
+
padx=6,
|
|
1291
|
+
cursor="hand2",
|
|
1292
|
+
command=lambda a=alarm: self._toggle_item_enabled(a),
|
|
1293
|
+
).pack(side="left", padx=2)
|
|
1294
|
+
|
|
1295
|
+
info_box = tk.Frame(card, bg="#0F172A")
|
|
1296
|
+
info_box.pack(side="right", fill="x", expand=True, padx=4)
|
|
1297
|
+
|
|
1298
|
+
tk.Label(
|
|
1299
|
+
info_box,
|
|
1300
|
+
text=f"{t_str} | {alarm.get('title', '')}",
|
|
1301
|
+
font=(self.font_family, 9, "bold"),
|
|
1302
|
+
fg="#F8FAFC" if is_en else "#64748B",
|
|
1303
|
+
bg="#0F172A",
|
|
1304
|
+
anchor="e",
|
|
1305
|
+
).pack(fill="x")
|
|
1306
|
+
|
|
1307
|
+
tk.Label(
|
|
1308
|
+
info_box,
|
|
1309
|
+
text=f"زمانبندی: {sched_str} | {icons_str}",
|
|
1310
|
+
font=(self.font_family, 8),
|
|
1311
|
+
fg="#38BDF8" if is_en else "#475569",
|
|
1312
|
+
bg="#0F172A",
|
|
1313
|
+
anchor="e",
|
|
1314
|
+
).pack(fill="x")
|