import json
import os
import threading
import time
from datetime import datetime
from pathlib import Path
from tkinter import (BOTH, Button, Checkbutton, Entry, Label, LEFT, RIGHT, Listbox,
Scrollbar, StringVar, Tk, VERTICAL, W, filedialog, messagebox, BooleanVar,
Frame, LabelFrame, CENTER, Toplevel)
try:
import winsound
except Exception:
winsound = None
try:
from playsound import playsound
except Exception:
playsound = None
APP_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
APP_DIR.mkdir(exist_ok=True)
ALARM_FILE = APP_DIR / "alarms.json"
LOCK = threading.Lock()
class Alarm:
def __init__(self, time_str, label="Alarm", one_time=False, date_str=None, sound_path=None, enabled=True):
self.time_str = time_str
self.label = label
self.one_time = bool(one_time)
self.date_str = date_str
self.sound_path = sound_path
self.enabled = bool(enabled)
def to_dict(self):
return self.__dict__
@classmethod
def from_dict(cls, d):
return cls(**d)
def matches_now(self, now: datetime):
if not self.enabled:
return False
if now.strftime("%H:%M") != self.time_str:
return False
if self.one_time:
return now.strftime("%Y-%m-%d") == self.date_str
return True
def sort_key(self):
# Dùng date_str nếu one_time, nếu không thì dùng ngày hiện tại cùng time_str để sắp xếp
if self.one_time and self.date_str:
dt_str = self.date_str + " " + self.time_str
return datetime.strptime(dt_str, "%Y-%m-%d %H:%M")
else:
dt_str = datetime.now().strftime("%Y-%m-%d") + " " + self.time_str
return datetime.strptime(dt_str, "%Y-%m-%d %H:%M")
class AlarmManager:
def __init__(self):
self.alarms = []
self.load()
def add(self, alarm: Alarm):
with LOCK:
self.alarms.append(alarm)
self.save()
def remove(self, index: int):
with LOCK:
if 0 <= index < len(self.alarms):
del self.alarms[index]
self.save()
def save(self):
with open(ALARM_FILE, "w", encoding="utf-8") as f:
json.dump([a.to_dict() for a in self.alarms], f, ensure_ascii=False, indent=2)
def load(self):
if ALARM_FILE.exists():
with open(ALARM_FILE, "r", encoding="utf-8") as f:
self.alarms = [Alarm.from_dict(d) for d in json.load(f)]
def center_window(root, width=800, height=600):
# Lấy kích thước màn hình
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
# Tính vị trí bắt đầu trục ngang và trục đứng
aaaxxx = (screen_width // 2) - (width // 2)
bbbyyy = (screen_height // 2) - (height // 2) - (40)
# Đặt geometry
root.geometry(f"{width}x{height}+{aaaxxx}+{bbbyyy}")
class AlarmApp:
def __init__(self, root: Tk):
self.root = root
self.root.title("Alarms App - DarkCorners")
center_window(self.root, 800, 600)
self.root.configure(bg="#f7f7f7")
self.manager = AlarmManager()
title_lbl = Label(root, text="Alarms App - DarkCorners", font=("Arial", 18, "bold"), bg="#f7f7f7", fg="#222")
title_lbl.pack(pady=10)
row2_frame = Frame(root, bg="#f7f7f7")
row2_frame.pack(fill=BOTH, expand=True, padx=10, pady=10)
# Left panel - Thêm báo thức mới
left_frame = LabelFrame(row2_frame, text="THÊM BÁO THỨC MỚI", font=("Arial", 12, "bold"), padx=10, pady=10, bg="#f7f7f7", fg="#222")
left_frame.pack(side=LEFT, fill=BOTH, expand=True, padx=5, pady=5)
self.time_var = StringVar()
Label(left_frame, text="Thời gian (HH:MM):", anchor=W, bg="#f7f7f7", fg="#222").pack(fill="x", pady=(2,0))
Entry(left_frame, textvariable=self.time_var, font=("Arial", 12)).pack(fill="x", pady=(0,5))
self.label_var = StringVar()
Label(left_frame, text="Nhãn:", anchor=W, bg="#f7f7f7", fg="#222").pack(fill="x", pady=(2,0))
Entry(left_frame, textvariable=self.label_var, font=("Arial", 12)).pack(fill="x", pady=(0,5))
self.one_time_var = BooleanVar()
Checkbutton(left_frame, text="Một lần (chọn ngày)", variable=self.one_time_var, bg="#f7f7f7", fg="#222").pack(anchor=W, pady=2)
self.date_var = StringVar()
Label(left_frame, text="Ngày (YYYY-MM-DD):", anchor=W, bg="#f7f7f7", fg="#222").pack(fill="x", pady=(2,0))
Entry(left_frame, textvariable=self.date_var, font=("Arial", 12)).pack(fill="x", pady=(0,5))
self.sound_var = StringVar()
Label(left_frame, text="File âm thanh (wav/mp3):", anchor=W, bg="#f7f7f7", fg="#222").pack(fill="x", pady=(2,0))
Entry(left_frame, textvariable=self.sound_var, font=("Arial", 12)).pack(fill="x", pady=(0,3))
Button(left_frame, text="Browse", command=self.browse_sound, bg="#4a90e2", fg="white", font=("Arial", 11, "bold")).pack(anchor=W, pady=(0,10))
self.enabled_var = BooleanVar(value=True)
Checkbutton(left_frame, text="Kích hoạt", variable=self.enabled_var, bg="#f7f7f7", fg="#222").pack(anchor=W, pady=(0,10))
Button(left_frame, text="Thêm", command=self.add_alarm, bg="#28a745", fg="white", font=("Arial", 14, "bold"), relief="raised").pack(fill="x", pady=10)
# Right panel - Danh sách báo thức
right_frame = LabelFrame(row2_frame, text="DANH SÁCH BÁO THỨC", font=("Arial", 12, "bold"), padx=10, pady=10, bg="#f7f7f7", fg="#222")
right_frame.pack(side=RIGHT, fill=BOTH, expand=True, padx=5, pady=5)
self.listbox = Listbox(right_frame, font=("Consolas", 12), bg="white", fg="#222", selectbackground="#4a90e2", activestyle="none")
self.listbox.pack(side=LEFT, fill=BOTH, expand=True, padx=(0,5))
scrollbar = Scrollbar(right_frame, orient=VERTICAL, command=self.listbox.yview)
scrollbar.pack(side=RIGHT, fill="y")
self.listbox.config(yscrollcommand=scrollbar.set)
# Control buttons
control_frame = Frame(root, bg="#f7f7f7", pady=10)
control_frame.pack(fill=BOTH, expand=False, padx=10)
btn_frame = Frame(control_frame, bg="#f7f7f7")
btn_frame.pack(anchor=CENTER)
btn_style = {"bg": "#4a90e2", "fg": "white", "font": ("Arial", 12, "bold"), "relief": "raised", "padx": 10, "pady": 5}
Button(btn_frame, text="Sửa", command=self.edit_selected, **btn_style).pack(side=LEFT, padx=5)
Button(btn_frame, text="Xóa", command=self.delete_selected, **btn_style).pack(side=LEFT, padx=5)
Button(btn_frame, text="Bật/Tắt", command=self.toggle_selected, **btn_style).pack(side=LEFT, padx=5)
Button(btn_frame, text="Chạy thử âm thanh", command=self.test_sound, **btn_style).pack(side=LEFT, padx=5)
Button(btn_frame, text="Thoát", command=self.on_close, bg="#dc3545", fg="white", font=("Arial", 12, "bold"), relief="raised", padx=10, pady=5).pack(side=LEFT, padx=5)
self.refresh_list()
self._stop = False
threading.Thread(target=self._checker_loop, daemon=True).start()
self._update_clock()
root.protocol("WM_DELETE_WINDOW", self.on_close)
def browse_sound(self):
p = filedialog.askopenfilename(title="Chọn file âm thanh", filetypes=[("Audio files", "*.wav *.mp3"), ("All files", "*.*")])
if p:
self.sound_var.set(p)
def add_alarm(self):
t = self.time_var.get().strip()
lbl = self.label_var.get().strip() or "Alarm"
ot = self.one_time_var.get()
d = self.date_var.get().strip() or None
sp = self.sound_var.get().strip() or None
en = self.enabled_var.get()
try:
datetime.strptime(t, "%H:%M")
if ot and d:
datetime.strptime(d, "%Y-%m-%d")
except Exception:
messagebox.showerror("Lỗi", "Thông tin thời gian/ngày không hợp lệ")
return
self.manager.add(Alarm(time_str=t, label=lbl, one_time=ot, date_str=d, sound_path=sp, enabled=en))
self.refresh_list()
def refresh_list(self):
self.listbox.delete(0, "end")
# Sắp xếp báo thức theo ngày giờ
sorted_alarms = sorted(self.manager.alarms, key=lambda a: a.sort_key())
for i, a in enumerate(sorted_alarms):
line = f"{a.time_str} - {a.label}"
if a.one_time:
line += f" ({a.date_str})"
line += " [ON]" if a.enabled else " [OFF]"
self.listbox.insert("end", line)
# Lưu thứ tự sắp xếp để xử lý sửa/xóa đúng
self._sorted_alarms = sorted_alarms
def edit_selected(self):
sel = self.listbox.curselection()
if not sel:
messagebox.showinfo("Thông báo", "Vui lòng chọn báo thức để sửa")
return
idx = sel[0]
alarm = self._sorted_alarms[idx]
edit_win = Toplevel(self.root)
edit_win.title("Sửa báo thức")
edit_win.geometry("400x350")
edit_win.configure(bg="#f7f7f7")
Label(edit_win, text="Thời gian (HH:MM):", bg="#f7f7f7", fg="#222").pack(anchor=W, padx=10, pady=(10,0))
time_var = StringVar(value=alarm.time_str)
Entry(edit_win, textvariable=time_var, font=("Arial", 12)).pack(fill="x", padx=10, pady=5)
Label(edit_win, text="Nhãn:", bg="#f7f7f7", fg="#222").pack(anchor=W, padx=10, pady=(10,0))
label_var = StringVar(value=alarm.label)
Entry(edit_win, textvariable=label_var, font=("Arial", 12)).pack(fill="x", padx=10, pady=5)
one_time_var = BooleanVar(value=alarm.one_time)
Checkbutton(edit_win, text="Một lần (chọn ngày)", variable=one_time_var, bg="#f7f7f7", fg="#222").pack(anchor=W, padx=10, pady=5)
Label(edit_win, text="Ngày (YYYY-MM-DD):", bg="#f7f7f7", fg="#222").pack(anchor=W, padx=10, pady=(10,0))
date_var = StringVar(value=alarm.date_str if alarm.date_str else "")
Entry(edit_win, textvariable=date_var, font=("Arial", 12)).pack(fill="x", padx=10, pady=5)
Label(edit_win, text="File âm thanh (wav/mp3):", bg="#f7f7f7", fg="#222").pack(anchor=W, padx=10, pady=(10,0))
sound_var = StringVar(value=alarm.sound_path if alarm.sound_path else "")
Entry(edit_win, textvariable=sound_var, font=("Arial", 12)).pack(fill="x", padx=10, pady=5)
def browse_edit_sound():
p = filedialog.askopenfilename(title="Chọn file âm thanh", filetypes=[("Audio files", "*.wav *.mp3"), ("All files", "*.*")])
if p:
sound_var.set(p)
Button(edit_win, text="Browse", command=browse_edit_sound, bg="#4a90e2", fg="white", font=("Arial", 11, "bold")).pack(anchor=W, padx=10, pady=5)
enabled_var = BooleanVar(value=alarm.enabled)
Checkbutton(edit_win, text="Kích hoạt", variable=enabled_var, bg="#f7f7f7", fg="#222").pack(anchor=W, padx=10, pady=5)
def save_edit():
t = time_var.get().strip()
lbl = label_var.get().strip() or "Alarm"
ot = one_time_var.get()
d = date_var.get().strip() or None
sp = sound_var.get().strip() or None
en = enabled_var.get()
try:
datetime.strptime(t, "%H:%M")
if ot and d:
datetime.strptime(d, "%Y-%m-%d")
except Exception:
messagebox.showerror("Lỗi", "Thông tin thời gian/ngày không hợp lệ")
return
# Cập nhật alarm gốc trong manager
with LOCK:
idx_orig = self.manager.alarms.index(alarm)
self.manager.alarms[idx_orig] = Alarm(time_str=t, label=lbl, one_time=ot, date_str=d, sound_path=sp, enabled=en)
self.manager.save()
self.refresh_list()
edit_win.destroy()
Button(edit_win, text="Lưu", command=save_edit, bg="#28a745", fg="white", font=("Arial", 14, "bold")).pack(fill="x", padx=10, pady=15)
def delete_selected(self):
sel = self.listbox.curselection()
if sel:
alarm = self._sorted_alarms[sel[0]]
with LOCK:
idx_orig = self.manager.alarms.index(alarm)
self.manager.remove(idx_orig)
self.refresh_list()
def toggle_selected(self):
sel = self.listbox.curselection()
if sel:
alarm = self._sorted_alarms[sel[0]]
alarm.enabled = not alarm.enabled
self.manager.save()
self.refresh_list()
def test_sound(self):
sel = self.listbox.curselection()
path = self._sorted_alarms[sel[0]].sound_path if sel else None
if not path:
path = filedialog.askopenfilename(title="Chọn file âm thanh", filetypes=[("Audio files", "*.wav *.mp3"), ("All files", "*.*")])
if path:
self._play_sound_async(path)
def _play_sound_async(self, path):
def _play():
try:
if winsound and path.lower().endswith(".wav"):
winsound.PlaySound(path, winsound.SND_FILENAME)
elif playsound:
playsound(path)
except Exception as e:
messagebox.showerror("Lỗi phát âm thanh", str(e))
threading.Thread(target=_play, daemon=True).start()
def _checker_loop(self):
while not self._stop:
now = datetime.now()
with LOCK:
for alarm in self.manager.alarms:
if alarm.matches_now(now):
self.root.after(0, lambda a=alarm: self._alarm_triggered(a))
time.sleep(30)
def _alarm_triggered(self, alarm: Alarm):
popup = Toplevel(self.root)
popup.title("Báo thức")
popup.geometry("400x180")
popup.configure(bg="#f7f7f7")
Label(popup, text=f"Đến giờ: {alarm.label}", font=("Arial", 16, "bold"), bg="#f7f7f7", fg="#222").pack(pady=(20,10))
def stop_alarm():
popup.destroy()
btn_stop = Button(popup, text="TẮT", command=stop_alarm, bg="#dc3545", fg="white", font=("Arial", 20, "bold"), relief="raised", padx=20, pady=10)
btn_stop.pack(pady=10)
if alarm.sound_path:
self._play_sound_async(alarm.sound_path)
def _update_clock(self):
# Có thể thêm đồng hồ hiện tại trên giao diện nếu muốn
self.root.after(1000, self._update_clock)
def on_close(self):
self._stop = True
self.root.destroy()
if __name__ == "__main__":
root = Tk()
app = AlarmApp(root)
root.mainloop()