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.
@@ -0,0 +1,519 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ماژول کاملا آفلاین تبدیل تاریخ شمسی، قمری و میلادی به همراه دیتابیس مناسبت‌ها و تعطیلات رسمی ایران
4
+ شامل محاسبه تقویم ماهانه، کبیسه شمسی و استخراج مناسبت‌های هر روز از هر ماه
5
+ """
6
+
7
+ from datetime import datetime, date, timezone, timedelta
8
+ import json
9
+ import os
10
+
11
+ # منطقه زمانی تهران (UTC+03:30 ثابت)
12
+ TEHRAN_TZ = timezone(timedelta(hours=3, minutes=30), name="Asia/Tehran")
13
+
14
+ PERSIAN_WEEKDAYS = [
15
+ "دوشنبه", # Monday (0)
16
+ "سه‌شنبه", # Tuesday (1)
17
+ "چهارشنبه", # Wednesday (2)
18
+ "پنج‌شنبه", # Thursday (3)
19
+ "جمعه", # Friday (4)
20
+ "شنبه", # Saturday (5)
21
+ "یک‌شنبه", # Sunday (6)
22
+ ]
23
+
24
+ # ترتیب روزهای هفته در تقویم ایرانی (از شنبه تا جمعه)
25
+ PERSIAN_WEEK_SHORT = ["ش", "ی", "د", "س", "چ", "پ", "ج"]
26
+
27
+ PERSIAN_MONTHS = [
28
+ "",
29
+ "فروردین",
30
+ "اردیبهشت",
31
+ "خرداد",
32
+ "تیر",
33
+ "مرداد",
34
+ "شهریور",
35
+ "مهر",
36
+ "آبان",
37
+ "آذر",
38
+ "دی",
39
+ "بهمن",
40
+ "اسفند",
41
+ ]
42
+
43
+
44
+ def gregorian_to_jalali(gy: int, gm: int, gd: int) -> tuple[int, int, int]:
45
+ """تبدیل دقیق تاریخ میلادی به هجری شمسی (جلالی)"""
46
+ g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
47
+ if gy > 1600:
48
+ jy = 979
49
+ gy -= 1600
50
+ else:
51
+ jy = 0
52
+ gy -= 621
53
+
54
+ gy2 = gy + 1 if gm > 2 else gy
55
+ days = (
56
+ (365 * gy)
57
+ + int((gy2 + 3) / 4)
58
+ - int((gy2 + 99) / 100)
59
+ + int((gy2 + 399) / 400)
60
+ - 80
61
+ + gd
62
+ + g_d_m[gm - 1]
63
+ )
64
+ jy += 33 * int(days / 12053)
65
+ days %= 12053
66
+ jy += 4 * int(days / 1461)
67
+ days %= 1461
68
+ if days > 365:
69
+ jy += int((days - 1) / 365)
70
+ days = (days - 1) % 365
71
+ if days < 186:
72
+ jm = 1 + int(days / 31)
73
+ jd = 1 + (days % 31)
74
+ else:
75
+ jm = 7 + int((days - 186) / 30)
76
+ jd = 1 + ((days - 186) % 30)
77
+ return jy, jm, jd
78
+
79
+
80
+ def jalali_to_gregorian(jy: int, jm: int, jd: int) -> tuple[int, int, int]:
81
+ """تبدیل دقیق تاریخ هجری شمسی (جلالی) به میلادی"""
82
+ jy += 1595
83
+ days = -355668 + (365 * jy) + (int(jy / 33) * 8) + int(((jy % 33) + 3) / 4) + jd
84
+ if jm < 7:
85
+ days += (jm - 1) * 31
86
+ else:
87
+ days += ((jm - 7) * 30) + 186
88
+ gy = 400 * int(days / 146097)
89
+ days %= 146097
90
+ if days > 36524:
91
+ days -= 1
92
+ gy += 100 * int(days / 36524)
93
+ days %= 36524
94
+ if days >= 365:
95
+ days += 1
96
+ gy += 4 * int(days / 1461)
97
+ days %= 1461
98
+ if days > 365:
99
+ gy += int((days - 1) / 365)
100
+ days = (days - 1) % 365
101
+ gd = days + 1
102
+ sal_a = [
103
+ 0,
104
+ 31,
105
+ 29 if ((gy % 4 == 0 and gy % 100 != 0) or (gy % 400 == 0)) else 28,
106
+ 31,
107
+ 30,
108
+ 31,
109
+ 30,
110
+ 31,
111
+ 31,
112
+ 30,
113
+ 31,
114
+ 30,
115
+ 31,
116
+ ]
117
+ gm = 0
118
+ while gm < 13 and gd > sal_a[gm]:
119
+ gd -= sal_a[gm]
120
+ gm += 1
121
+ return gy, gm, gd
122
+
123
+
124
+ def is_jalali_leap(jy: int) -> bool:
125
+ """بررسی کبیسه بودن سال شمسی"""
126
+ # اگر روز 30 اسفند به تاریخ اول فروردین سال بعد تبدیل نشود، سال کبیسه است
127
+ gy, gm, gd = jalali_to_gregorian(jy, 12, 30)
128
+ jy2, jm2, jd2 = gregorian_to_jalali(gy, gm, gd)
129
+ return (jy2 == jy and jm2 == 12 and jd2 == 30)
130
+
131
+
132
+ def get_jalali_month_days(jy: int, jm: int) -> int:
133
+ """تعداد روزهای ماه شمسی"""
134
+ if 1 <= jm <= 6:
135
+ return 31
136
+ elif 7 <= jm <= 11:
137
+ return 30
138
+ elif jm == 12:
139
+ return 30 if is_jalali_leap(jy) else 29
140
+ return 30
141
+
142
+
143
+ def gregorian_to_hijri(gy: int, gm: int, gd: int) -> tuple[int, int, int]:
144
+ """تبدیل تاریخ میلادی به هجری قمری"""
145
+ if gm < 3:
146
+ gy -= 1
147
+ gm += 12
148
+ a = int(gy / 100)
149
+ b = 2 - a + int(a / 4)
150
+ jd = int(365.25 * (gy + 4716)) + int(30.6001 * (gm + 1)) + gd + b - 1524
151
+
152
+ i_year = 10631.0 / 30.0
153
+ epoch_astro = 1948084
154
+ shift1 = 8.01 / 60.0
155
+
156
+ z = jd - epoch_astro
157
+ cyc = int(z / 10631.0)
158
+ z = z - 10631 * cyc
159
+ j = int((z - shift1) / i_year)
160
+ iy = 30 * cyc + j
161
+ z = z - int(j * i_year + shift1)
162
+ im = int((z + 28.5001) / 29.5)
163
+ if im == 13:
164
+ im = 12
165
+ id_day = z - int(29.5001 * im - 29)
166
+ return int(iy), int(im), int(id_day)
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # دیتابیس جامع مناسبت‌های شمسی ایران
171
+ # فرمت: (ماه, روز): [(عنوان مناسبت, آیا_تعطیل_رسمی_است؟), ...]
172
+ # ---------------------------------------------------------------------------
173
+ SHAMSI_OCCASIONS: dict[tuple[int, int], list[tuple[str, bool]]] = {
174
+ # فروردین
175
+ (1, 1): [("جشن نوروز و آغاز سال نو", True)],
176
+ (1, 2): [("عید نوروز", True)],
177
+ (1, 3): [("عید نوروز", True)],
178
+ (1, 4): [("عید نوروز", True)],
179
+ (1, 6): [("روز امید و زادروز زرتشت", False)],
180
+ (1, 7): [("روز جهانی تئاتر", False)],
181
+ (1, 10): [("جشن آبان‌گاه", False)],
182
+ (1, 12): [("روز جمهوری اسلامی ایران", True)],
183
+ (1, 13): [("جشن سیزده‌به‌در (روز طبیعت)", True)],
184
+ (1, 18): [("روز جهانی بهداشت و سلامتی", False)],
185
+ (1, 19): [("فروردین‌گان", False)],
186
+ (1, 20): [("روز ملی فناوری هسته‌ای", False)],
187
+ (1, 25): [("روز بزرگداشت عطار نیشابوری", False)],
188
+ (1, 29): [("روز ارتش جمهوری اسلامی ایران", False)],
189
+
190
+ # اردیبهشت
191
+ (2, 1): [("روز بزرگداشت سعدی", False)],
192
+ (2, 2): [("روز زمین پاک", False)],
193
+ (2, 3): [("روز بزرگداشت شیخ بهایی / روز معماری", False)],
194
+ (2, 7): [("روز ایمنی حمل و نقل", False)],
195
+ (2, 9): [("روز شوراها / روز روان‌شناس و مشاور", False)],
196
+ (2, 10): [("روز ملی خلیج فارس", False)],
197
+ (2, 12): [("روز معلم", False)],
198
+ (2, 15): [("روز بزرگداشت شیخ صدوق / جشن میانه بهار", False)],
199
+ (2, 18): [("روز جهانی صلیب سرخ و هلال احمر", False)],
200
+ (2, 25): [("روز پاسداشت زبان فارسی و بزرگداشت فردوسی", False)],
201
+ (2, 27): [("روز ارتباطات و روابط عمومی", False)],
202
+ (2, 28): [("روز بزرگداشت حکیم عمر خیام", False)],
203
+
204
+ # خرداد
205
+ (3, 1): [("روز بهره‌وری و بهینه‌سازی مصرف / بزرگداشت ملاصدرا", False)],
206
+ (3, 3): [("سالروز آزادسازی خرمشهر", False)],
207
+ (3, 4): [("روز دزفول (مقاومت و پایداری)", False)],
208
+ (3, 6): [("جشن خردادگان", False)],
209
+ (3, 14): [("رحلت امام خمینی", True)],
210
+ (3, 15): [("قیام 15 خرداد", True)],
211
+ (3, 16): [("روز جهانی محیط زیست", False)],
212
+ (3, 20): [("روز جهانی صنایع دستی و فرش", False)],
213
+ (3, 25): [("روز ملی گل و گیاه", False)],
214
+ (3, 27): [("روز جهاد کشاورزی", False)],
215
+ (3, 31): [("سالروز شهادت دکتر مصطفی چمران", False)],
216
+
217
+ # تیر
218
+ (4, 1): [("جشن آب‌پاشونک / روز اصناف", False)],
219
+ (4, 5): [("روز جهانی مبارزه با مواد مخدر", False)],
220
+ (4, 7): [("سالروز فاجعه هفتم تیر / روز قوه قضاییه", False)],
221
+ (4, 8): [("روز مبارزه با سلاح‌های شیمیایی و میکروبی", False)],
222
+ (4, 10): [("روز صنعت و معدن", False)],
223
+ (4, 13): [("جشن تیرگان و بزرگداشت آرش کمانگیر", False)],
224
+ (4, 14): [("روز قلم", False)],
225
+ (4, 16): [("روز مالیات", False)],
226
+ (4, 18): [("روز ادبیات کودکان و نوجوانان", False)],
227
+ (4, 23): [("روز گفت‌وگو و تعامل سازنده با جهان", False)],
228
+ (4, 25): [("روز بهزیستی و تامین اجتماعی", False)],
229
+ (4, 27): [("اعلام پذیرش قطعنامه 598 شورای امنیت", False)],
230
+
231
+ # مرداد
232
+ (5, 6): [("روز ترویج آموزش‌های فنی و حرفه‌ای", False)],
233
+ (5, 7): [("جشن امردادگان", False)],
234
+ (5, 8): [("روز بزرگداشت شیخ شهاب‌الدین سهروردی", False)],
235
+ (5, 9): [("روز اهدای خون", False)],
236
+ (5, 10): [("جشن چله تابستان", False)],
237
+ (5, 14): [("سالروز صدور فرمان مشروطیت", False)],
238
+ (5, 17): [("روز خبرنگار", False)],
239
+ (5, 22): [("روز تشکل‌ها و مشارکت‌های اجتماعی", False)],
240
+ (5, 26): [("سالروز ورود آزادگان به میهن", False)],
241
+ (5, 28): [("سالروز کودتای 28 مرداد", False)],
242
+ (5, 30): [("روز بزرگداشت علامه مجلسی", False)],
243
+
244
+ # شهریور
245
+ (6, 1): [("روز بزرگداشت ابوعلی سینا و روز پزشک", False)],
246
+ (6, 2): [("آغاز هفته دولت", False)],
247
+ (6, 4): [("جشن شهریورگان / روز کارمند", False)],
248
+ (6, 5): [("روز بزرگداشت محمد بن زکریای رازی و روز داروساز", False)],
249
+ (6, 8): [("روز مبارزه با تروریسم", False)],
250
+ (6, 11): [("روز صنعت چاپ", False)],
251
+ (6, 12): [("روز مبارزه با استعمار انگلیس (شهادت رئیس‌علی دلواری)", False)],
252
+ (6, 13): [("روز بزرگداشت ابوریحان بیرونی / روز تعاون", False)],
253
+ (6, 17): [("قیام 17 شهریور", False)],
254
+ (6, 21): [("روز سینما", False)],
255
+ (6, 27): [("روز شعر و ادب فارسی و بزرگداشت استاد شهریار", False)],
256
+ (6, 30): [("روز گفت‌وگوی تمدن‌ها", False)],
257
+ (6, 31): [("آغاز هفته دفاع مقدس", False)],
258
+
259
+ # مهر
260
+ (7, 1): [("آغاز سال تحصیلی و فصل پاییز", False)],
261
+ (7, 5): [("روز جهانی جهانگردی", False)],
262
+ (7, 7): [("روز آتش‌نشانی و ایمنی / بزرگداشت شمس تبریزی", False)],
263
+ (7, 8): [("روز بزرگداشت مولوی", False)],
264
+ (7, 9): [("روز همبستگی با کودکان و نوجوانان فلسطینی", False)],
265
+ (7, 10): [("جشن مهرگان", False)],
266
+ (7, 13): [("روز نیروی انتظامی", False)],
267
+ (7, 14): [("روز دامپزشکی", False)],
268
+ (7, 15): [("روز روستا و عشایر", False)],
269
+ (7, 16): [("روز جهانی کودک", False)],
270
+ (7, 17): [("روز جهانی پست", False)],
271
+ (7, 20): [("روز بزرگداشت حافظ شیرازی", False)],
272
+ (7, 21): [("جشن رام‌روز", False)],
273
+ (7, 24): [("روز ملی پارالمپیک", False)],
274
+ (7, 26): [("روز تربیت بدنی و ورزش", False)],
275
+ (7, 29): [("روز ملی کوهنورد", False)],
276
+
277
+ # آبان
278
+ (8, 1): [("روز آمار و برنامه‌ریزی", False)],
279
+ (8, 7): [("روز بزرگداشت کوروش بزرگ", False)],
280
+ (8, 8): [("روز نوجوان", False)],
281
+ (8, 10): [("جشن آبانگان", False)],
282
+ (8, 13): [("روز دانش‌آموز", False)],
283
+ (8, 14): [("روز فرهنگ عمومی", False)],
284
+ (8, 18): [("روز ملی کیفیت", False)],
285
+ (8, 24): [("روز کتاب و کتاب‌خوانی", False)],
286
+
287
+ # آذر
288
+ (9, 1): [("آغاز ماه آذر", False)],
289
+ (9, 5): [("روز بسیج مستضعفان", False)],
290
+ (9, 7): [("روز نیروی دریایی", False)],
291
+ (9, 9): [("جشن آذرگان", False)],
292
+ (9, 10): [("روز مجلس", False)],
293
+ (9, 13): [("روز بیمه", False)],
294
+ (9, 15): [("روز حسابدار", False)],
295
+ (9, 16): [("روز دانشجو", False)],
296
+ (9, 25): [("روز پژوهش", False)],
297
+ (9, 26): [("روز حمل و نقل و رانندگان", False)],
298
+ (9, 30): [("جشن شب یلدا (شب چله)", False)],
299
+
300
+ # دی
301
+ (10, 1): [("روز میلاد خورشید و آغاز فصل زمستان", False)],
302
+ (10, 4): [("جشن کریسمس", False)],
303
+ (10, 5): [("روز ملی ایمنی در برابر زلزله", False)],
304
+ (10, 7): [("سالروز تشکیل نهضت سوادآموزی", False)],
305
+ (10, 8): [("روز صنعت پتروشیمی", False)],
306
+ (10, 11): [("آغاز سال نو میلادی", False)],
307
+ (10, 13): [("سالروز شهادت سردار قاسم سلیمانی", False)],
308
+ (10, 15): [("جشن دی‌گان", False)],
309
+ (10, 20): [("سالروز قتل امیرکبیر", False)],
310
+ (10, 29): [("روز غزه", False)],
311
+
312
+ # بهمن
313
+ (11, 1): [("زادروز فردوسی", False)],
314
+ (11, 2): [("جشن بهمنگان", False)],
315
+ (11, 10): [("جشن سده", False)],
316
+ (11, 12): [("بازگشت امام خمینی به ایران و آغاز دهه فجر", False)],
317
+ (11, 14): [("روز فناوری فضایی", False)],
318
+ (11, 19): [("روز نیروی هوایی", False)],
319
+ (11, 22): [("پیروزی انقلاب اسلامی ایران", True)],
320
+ (11, 29): [("جشن سپندارمذگان (روز عشق ایرانی)", False)],
321
+
322
+ # اسفند
323
+ (12, 5): [("روز بزرگداشت خواجه نصیرالدین طوسی و روز مهندس", False)],
324
+ (12, 7): [("سالروز استقلال کانون وکلا و روز وکیل مدافع", False)],
325
+ (12, 8): [("روز امور تربیتی و تربیت اسلامی", False)],
326
+ (12, 14): [("روز احسان و نیکوکاری", False)],
327
+ (12, 15): [("روز درختکاری", False)],
328
+ (12, 18): [("روز جهانی زنان", False)],
329
+ (12, 25): [("روز بزرگداشت پروین اعتصامی", False)],
330
+ (12, 29): [("روز ملی شدن صنعت نفت ایران", True)],
331
+ (12, 30): [("آخرین روز سال (کبیسه)", True)],
332
+ }
333
+
334
+ # ---------------------------------------------------------------------------
335
+ # مناسبت‌ها و تعطیلات رسمی قمری ایران
336
+ # ---------------------------------------------------------------------------
337
+ HIJRI_OCCASIONS: dict[tuple[int, int], list[tuple[str, bool]]] = {
338
+ (1, 1): [("آغاز سال نو هجری قمری", False)],
339
+ (1, 9): [("تاسوعای حسینی", True)],
340
+ (1, 10): [("عاشورای حسینی", True)],
341
+ (1, 12): [("شهادت امام زین‌العابدین (ع)", False)],
342
+ (2, 20): [("اربعین حسینی", True)],
343
+ (2, 28): [("رحلت پیامبر اکرم (ص) و شهادت امام حسن مجتبی (ع)", True)],
344
+ (2, 30): [("شهادت امام رضا (ع)", True)],
345
+ (3, 8): [("شهادت امام حسن عسکری (ع) و آغاز امامت حضرت ولی‌عصر (عج)", True)],
346
+ (3, 12): [("آغاز هفته وحدت", False)],
347
+ (3, 17): [("میلاد پیامبر اکرم (ص) و امام جعفر صادق (ع)", True)],
348
+ (4, 8): [("ولادت امام حسن عسکری (ع)", False)],
349
+ (5, 5): [("ولادت حضرت زینب (س) و روز پرستار", False)],
350
+ (6, 3): [("شهادت حضرت فاطمه زهرا (س)", True)],
351
+ (6, 20): [("ولادت حضرت فاطمه زهرا (س) و روز مادر", False)],
352
+ (7, 1): [("ولادت امام محمد باقر (ع)", False)],
353
+ (7, 3): [("شهادت امام علی النقی الهادی (ع)", False)],
354
+ (7, 10): [("ولادت امام محمد تقی (ع)", False)],
355
+ (7, 13): [("ولادت امام علی (ع) و روز پدر", True)],
356
+ (7, 15): [("وفات حضرت زینب (س)", False)],
357
+ (7, 25): [("شهادت امام موسی کاظم (ع)", False)],
358
+ (7, 27): [("مبعث پیامبر اکرم (ص)", True)],
359
+ (8, 3): [("ولادت امام حسین (ع) و روز پاسدار", False)],
360
+ (8, 4): [("ولادت حضرت ابوالفضل العباس (ع) و روز جانباز", False)],
361
+ (8, 5): [("ولادت امام زین‌العابدین (ع)", False)],
362
+ (8, 11): [("ولادت حضرت علی‌اکبر (ع) و روز جوان", False)],
363
+ (8, 15): [("نیمه شعبان؛ ولادت حضرت قائم (عج)", True)],
364
+ (9, 15): [("ولادت امام حسن مجتبی (ع)", False)],
365
+ (9, 19): [("شب قدر (ضربت خوردن امام علی ع)", False)],
366
+ (9, 21): [("شهادت حضرت علی (ع)", True)],
367
+ (9, 23): [("شب قدر", False)],
368
+ (10, 1): [("عید سعید فطر", True)],
369
+ (10, 2): [("تعطیل به مناسبت عید سعید فطر", True)],
370
+ (10, 25): [("شهادت امام جعفر صادق (ع)", True)],
371
+ (11, 1): [("ولادت حضرت معصومه (س) و روز دختر", False)],
372
+ (11, 11): [("ولادت امام رضا (ع)", False)],
373
+ (11, 30): [("شهادت امام محمد تقی (ع)", False)],
374
+ (12, 1): [("سالروز ازدواج امام علی (ع) و حضرت فاطمه (س)", False)],
375
+ (12, 7): [("شهادت امام محمد باقر (ع)", False)],
376
+ (12, 9): [("روز عرفه", False)],
377
+ (12, 10): [("عید سعید قربان", True)],
378
+ (12, 15): [("ولادت امام علی النقی الهادی (ع)", False)],
379
+ (12, 18): [("عید سعید غدیر خم", True)],
380
+ (12, 20): [("ولادت امام موسی کاظم (ع)", False)],
381
+ }
382
+
383
+
384
+ def get_tehran_now() -> datetime:
385
+ """دریافت زمان فعلی به وقت تهران (UTC+03:30)"""
386
+ return datetime.now(TEHRAN_TZ)
387
+
388
+
389
+ def _load_custom_occasions(custom_occasions_path: str | None) -> dict:
390
+ if custom_occasions_path and os.path.exists(custom_occasions_path):
391
+ try:
392
+ with open(custom_occasions_path, "r", encoding="utf-8") as f:
393
+ return json.load(f)
394
+ except Exception:
395
+ pass
396
+ return {}
397
+
398
+
399
+ def get_day_details(
400
+ jy: int,
401
+ jm: int,
402
+ jd: int,
403
+ custom_data: dict | None = None,
404
+ ) -> dict:
405
+ """دریافت اطلاعات و مناسبت‌های یک روز مشخص شمسی"""
406
+ gy, gm, gd = jalali_to_gregorian(jy, jm, jd)
407
+ hy, hm, hd = gregorian_to_hijri(gy, gm, gd)
408
+
409
+ g_date = date(gy, gm, gd)
410
+ weekday_idx = g_date.weekday() # Monday=0 ... Sunday=6
411
+ # تبدیل اندیس هفته میلادی به اندیس هفته ایرانی (شنبه=0 تا جمعه=6)
412
+ persian_week_idx = (weekday_idx + 2) % 7
413
+ weekday_name = PERSIAN_WEEKDAYS[weekday_idx]
414
+ month_name = PERSIAN_MONTHS[jm]
415
+
416
+ is_friday = (weekday_idx == 4)
417
+ is_holiday = is_friday
418
+
419
+ events: list[tuple[str, bool]] = []
420
+
421
+ # 1. مناسبت‌های شمسی
422
+ if (jm, jd) in SHAMSI_OCCASIONS:
423
+ for title, hol in SHAMSI_OCCASIONS[(jm, jd)]:
424
+ events.append((title, hol))
425
+ if hol:
426
+ is_holiday = True
427
+
428
+ # 2. مناسبت‌های قمری
429
+ if (hm, hd) in HIJRI_OCCASIONS:
430
+ for title, hol in HIJRI_OCCASIONS[(hm, hd)]:
431
+ events.append((title, hol))
432
+ if hol:
433
+ is_holiday = True
434
+
435
+ # 3. مناسبت‌های سفارشی
436
+ if custom_data:
437
+ key = f"{jm:02d}-{jd:02d}"
438
+ if key in custom_data:
439
+ for item in custom_data[key]:
440
+ if isinstance(item, dict):
441
+ t = item.get("title", "")
442
+ h = bool(item.get("holiday", False))
443
+ else:
444
+ t = str(item)
445
+ h = False
446
+ if t:
447
+ events.append((t, h))
448
+ if h:
449
+ is_holiday = True
450
+
451
+ # مرتب‌سازی: ابتدا تعطیلات رسمی
452
+ events.sort(key=lambda x: (not x[1]))
453
+ titles = [e[0] for e in events]
454
+ primary_occasion = " | ".join(titles) if titles else ""
455
+
456
+ return {
457
+ "jy": jy,
458
+ "jm": jm,
459
+ "jd": jd,
460
+ "gy": gy,
461
+ "gm": gm,
462
+ "gd": gd,
463
+ "hy": hy,
464
+ "hm": hm,
465
+ "hd": hd,
466
+ "weekday_idx": weekday_idx,
467
+ "persian_week_idx": persian_week_idx, # 0=شنبه ... 6=جمعه
468
+ "weekday_name": weekday_name,
469
+ "month_name": month_name,
470
+ "date_text": f"{weekday_name} {jd} {month_name} {jy}",
471
+ "date_numeric": f"{jy:04d}/{jm:02d}/{jd:02d}",
472
+ "events": events,
473
+ "occasion": primary_occasion,
474
+ "has_occasion": len(events) > 0,
475
+ "is_holiday": is_holiday,
476
+ "is_friday": is_friday,
477
+ }
478
+
479
+
480
+ def get_month_calendar(
481
+ jy: int,
482
+ jm: int,
483
+ custom_occasions_path: str | None = None,
484
+ ) -> dict:
485
+ """
486
+ تولید اطلاعات کامل یک ماه شمسی برای نمایش در جدول تقویم
487
+ خروجی شامل لیست روزها، اندیس روز اول ماه در هفته (شنبه=0 تا جمعه=6) و مناسبت‌های هر روز
488
+ """
489
+ custom_data = _load_custom_occasions(custom_occasions_path)
490
+ total_days = get_jalali_month_days(jy, jm)
491
+
492
+ days_list = []
493
+ for d in range(1, total_days + 1):
494
+ days_list.append(get_day_details(jy, jm, d, custom_data))
495
+
496
+ first_day_week_idx = days_list[0]["persian_week_idx"] if days_list else 0
497
+
498
+ return {
499
+ "jy": jy,
500
+ "jm": jm,
501
+ "month_name": PERSIAN_MONTHS[jm],
502
+ "header_title": f"{PERSIAN_MONTHS[jm]} {jy}",
503
+ "total_days": total_days,
504
+ "first_day_week_idx": first_day_week_idx,
505
+ "days": days_list,
506
+ }
507
+
508
+
509
+ def get_today_info(custom_occasions_path: str | None = None) -> dict:
510
+ """دریافت اطلاعات لحظه‌ای ساعت تهران و تاریخ و مناسبت امروز"""
511
+ now = get_tehran_now()
512
+ jy, jm, jd = gregorian_to_jalali(now.year, now.month, now.day)
513
+ custom_data = _load_custom_occasions(custom_occasions_path)
514
+ info = get_day_details(jy, jm, jd, custom_data)
515
+ info["time_str"] = f"{now.hour:02d}:{now.minute:02d}"
516
+ info["hour"] = now.hour
517
+ info["minute"] = now.minute
518
+ info["second"] = now.second
519
+ return info
@@ -0,0 +1,106 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ تولیدکننده آیکون اختصاصی ساعت و تقویم فارسی (assets/icons/app_icon.ico و app_icon.png)
4
+ با کیفیت بالا و سایزهای استاندارد ویندوز (16 تا 256 پیکسل)
5
+ """
6
+
7
+ import os
8
+ from PIL import Image, ImageDraw, ImageFont
9
+
10
+
11
+ def generate_persian_calendar_clock_icon(output_dir: str) -> str:
12
+ os.makedirs(output_dir, exist_ok=True)
13
+ size = 256
14
+ img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
15
+ draw = ImageDraw.Draw(img)
16
+
17
+ # 1. پس‌زمینه اصلی کارت تقویم (گرد و مدرن)
18
+ draw.rounded_rectangle(
19
+ (16, 28, 224, 236),
20
+ radius=42,
21
+ fill=(15, 23, 42, 255),
22
+ outline=(56, 189, 248, 255),
23
+ width=6,
24
+ )
25
+
26
+ # 2. هدر بالای تقویم
27
+ draw.rounded_rectangle(
28
+ (22, 34, 218, 92),
29
+ radius=36,
30
+ fill=(225, 29, 72, 255),
31
+ )
32
+ draw.rectangle((22, 68, 218, 92), fill=(225, 29, 72, 255))
33
+
34
+ # 3. حلقه‌های بالای تقویم
35
+ for x in (68, 120, 172):
36
+ draw.rounded_rectangle(
37
+ (x - 8, 16, x + 8, 48),
38
+ radius=8,
39
+ fill=(248, 250, 252, 255),
40
+ outline=(15, 23, 42, 255),
41
+ width=3,
42
+ )
43
+
44
+ # 4. نوشتن عدد فارسی در مرکز تقویم
45
+ font_day = None
46
+ for font_name in ["Tahoma", "Segoe UI", "Arial"]:
47
+ try:
48
+ font_day = ImageFont.truetype(f"{font_name}bd.ttf", 84)
49
+ break
50
+ except Exception:
51
+ try:
52
+ font_day = ImageFont.truetype(f"{font_name}.ttf", 84)
53
+ break
54
+ except Exception:
55
+ continue
56
+
57
+ if font_day is None:
58
+ font_day = ImageFont.load_default()
59
+
60
+ draw.ellipse((52, 56, 64, 68), fill=(255, 255, 255, 220))
61
+ draw.ellipse((176, 56, 188, 68), fill=(255, 255, 255, 220))
62
+ draw.rounded_rectangle((84, 58, 156, 66), radius=4, fill=(255, 255, 255, 230))
63
+
64
+ persian_num = "۱۸"
65
+ bbox = draw.textbbox((0, 0), persian_num, font=font_day)
66
+ tw = bbox[2] - bbox[0]
67
+ th = bbox[3] - bbox[1]
68
+ draw.text(
69
+ (120 - tw // 2, 146 - th // 2 - 12),
70
+ persian_num,
71
+ fill=(248, 250, 252, 255),
72
+ font=font_day,
73
+ )
74
+
75
+ # 5. دایره ساعت در گوشه پایین-راست
76
+ cx, cy, r = 190, 190, 54
77
+ draw.ellipse((cx - r - 4, cy - r - 4, cx + r + 4, cy + r + 4), fill=(15, 23, 42, 255))
78
+ draw.ellipse(
79
+ (cx - r, cy - r, cx + r, cy + r),
80
+ fill=(14, 165, 233, 255),
81
+ outline=(248, 250, 252, 255),
82
+ width=6,
83
+ )
84
+ draw.ellipse((cx - r + 8, cy - r + 8, cx + r - 8, cy + r - 8), fill=(15, 23, 42, 255))
85
+
86
+ draw.line((cx, cy, cx, cy - 26), fill=(56, 189, 248, 255), width=6)
87
+ draw.line((cx, cy, cx + 24, cy + 10), fill=(251, 191, 36, 255), width=5)
88
+ draw.ellipse((cx - 5, cy - 5, cx + 5, cy + 5), fill=(248, 250, 252, 255))
89
+
90
+ ico_path = os.path.join(output_dir, "app_icon.ico")
91
+ png_path = os.path.join(output_dir, "app_icon.png")
92
+
93
+ img.save(png_path, format="PNG")
94
+ img.save(
95
+ ico_path,
96
+ format="ICO",
97
+ sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
98
+ )
99
+ return ico_path
100
+
101
+
102
+ if __name__ == "__main__":
103
+ root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
104
+ icons_dir = os.path.join(root_dir, "assets", "icons")
105
+ path = generate_persian_calendar_clock_icon(icons_dir)
106
+ print("Generated icon:", path)