Alarms App – DarkCorners

Date: 17/12/2025

Category: Python

  • Phần mềm hỗ trợ báo thức hàng loạt.
  • Hỗ trợ nhiều khung giờ, nhãn ghi chú và âm thanh khác nhau.
  1. Khởi động phần mềm.
  2. Điền thời gian bạn muốn báo thức theo định dạng HÀI HƯỚC:MM
  3. Điền nhãn ghi chú tùy ý.
  4. Điền ngày tháng báo thức theo định dạng YYYY-MM-DD.
  5. Chọn tệp âm thanh (hỗ trợ wav và mp3).
  6. Tích [Kích Hoạt]
  7. Nhấn nút [Thêm].
  8. Sau khi thêm tất cả báo thức cần có. Nhấn nút [Bật].
  9. Có thể chạy thử âm thanh để xem có hoạt động ổn định hay không?
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()
pyinstaller --noconsole --onefile --windowed --add-data "6-Alarm-icon.ico;." --icon=6-Alarm-icon.ico 6-Alarm-Source.py

Để lại một bình luận