diff --git a/Flashcards/README.md b/Flashcards/README.md new file mode 100644 index 00000000..c96c4c10 --- /dev/null +++ b/Flashcards/README.md @@ -0,0 +1,73 @@ +# Python Interactive Flashcards + +A lightweight, desktop-based flashcard application built with Python and Tkinter. This tool utilizes a Spaced Repetition System (SRS) to help you memorize information efficiently by scheduling reviews based on how well you remember each card. + +### 🚀 Features + +* **Dual Modes:** Separate workflows for **Learning** (new cards) and **Reviewing** (existing cards). +* **Spaced Repetition Algorithm:** Automatically calculates the best time to review a card based on your performance (intervals of 1, 3, 5, 7, and 15 days). +* **Persistent Storage:** All data is saved locally in a JSON file, so you never lose your progress. +* **Card Management:** Add, Edit, and Delete cards directly within the app. +* **Progress Tracking:** The main menu displays exactly how many cards are pending for learning or review. +* **Clean Interface:** Simple, distraction-free GUI. + +### 🛠️ Running + +1. **Clone the Repository or Download the Script** +2. **Navigate to the Script Location** +3. **Run the Script** + +### 📖 Usage Guide + +#### 1. The Main Menu + +Upon launching, you will see the dashboard. + +* **New Learning:** Shows the count of cards you have created but haven't studied yet. +* **To Review:** Shows the count of cards due for review today (based on the SRS algorithm). + +#### 2. Adding Cards + +* Click **"➕ Add Card"** in the main menu. +* Enter the **Question** (Front) and **Answer** (Back). +* Click **Save**. You can add multiple cards in a row. + +#### 3. Learning Mode (New Cards) + +* Click **"🆕 Start Learning"**. +* This mode presents cards you have never seen before. +* Click the card to flip it and reveal the answer. +* Click **"⏭ Next"** to mark the card as "Learned." +* *Note: Once learned, a card enters the review cycle and will be scheduled for its first review tomorrow (Day +1).* + +#### 4. Review Mode (Memory Training) + +* Click **"🔄 Start Review"**. +* This mode only shows cards that are due for review today or are overdue. +* **Flip the card** to see the answer. You will see two options: +* **🟩 Remembered:** The card is upgraded. The interval until the next review increases (e.g., 3 days 5 days 7 days). +* **🟥 Forgot:** The card level is reset to 0. It will be scheduled for review again tomorrow to reinforce memory. + +#### 5. Managing Cards + +While inside the Learning or Review interface, you can manage the current card using the buttons at the bottom: + +* **🗑️ Delete:** Permanently removes the current card. +* **✏️ Edit:** Allows you to modify typos or update the question/answer text. + +### 🧠 How the Algorithm Works + +The app uses a simplified interval scheduling logic to maximize retention: + +1. **New Card:** Becomes due **1 day** after first learning. +2. **Review Level 0:** If remembered due in **3 days**. +3. **Review Level 1:** If remembered due in **5 days**. +4. **Review Level 2:** If remembered due in **7 days**. +5. **Review Level 3+:** If remembered due in **15 days** (Max interval). +6. **Forgot:** If you mark a card as "Forgot" at any stage, it resets to **Level 0** and becomes due tomorrow. + +### 📂 Data Storage + +* Your cards are stored in a file named `flashcards_data.json` located in the same directory as the script. +* **Do not delete this file**, or you will lose your flashcards and progress history. +* You can back up this file to save your data. diff --git a/Flashcards/flashcards.py b/Flashcards/flashcards.py new file mode 100644 index 00000000..f22cd211 --- /dev/null +++ b/Flashcards/flashcards.py @@ -0,0 +1,353 @@ +import tkinter as tk +from tkinter import messagebox, simpledialog +import json +import random +import os +from datetime import datetime, timedelta, timedelta + +DATA_FILE = os.path.join(os.path.dirname(__file__), "flashcards_data.json") + +class FlashcardApp: + def __init__(self, root): + self.root = root + self.root.title("Python Interactive Flashcards") + self.root.geometry("600x500") + self.root.configure(bg="#f0f0f0") + + self.cards = self.load_data() + self.current_card = {} + self.is_flipped = False + self.study_queue = [] + self.mode = None # "learn" or "review" + + self.menu_frame = tk.Frame(root, bg="#f0f0f0") + self.game_frame = tk.Frame(root, bg="#f0f0f0") + self.completion_frame = tk.Frame(root, bg="#f0f0f0") + + self.setup_menu_ui() + self.setup_game_ui() + self.setup_completion_ui() + + self.show_menu() + + def setup_menu_ui(self): + tk.Label(self.menu_frame, text="Flashcard", font=("Microsoft YaHei", 24, "bold"), bg="#f0f0f0", fg="#333").pack(pady=40) + + self.lbl_learn_count = tk.Label(self.menu_frame, text="", font=("Microsoft YaHei", 12), bg="#f0f0f0", fg="#666") + self.lbl_learn_count.pack() + tk.Button(self.menu_frame, text="🆕 Start Learning", command=self.start_learn, font=("Microsoft YaHei", 14), bg="#ddffdd", width=20).pack(pady=10) + + self.lbl_review_count = tk.Label(self.menu_frame, text="", font=("Microsoft YaHei", 12), bg="#f0f0f0", fg="#666") + self.lbl_review_count.pack() + tk.Button(self.menu_frame, text="🔄 Start Review", command=self.start_review, font=("Microsoft YaHei", 14), bg="#ddddff", width=20).pack(pady=10) + + tk.Button(self.menu_frame, text="➕ Add Card", command=self.add_card_window, font=("Microsoft YaHei", 12), bg="#fff").pack(pady=20) + tk.Button(self.menu_frame, text="🚪 Exit", command=self.root.destroy, font=("Microsoft YaHei", 12), bg="#eee").pack(pady=5) + + def setup_game_ui(self): + self.header_label = tk.Label(self.game_frame, text="Memory Training", font=("Microsoft YaHei", 16, "bold"), bg="#f0f0f0", fg="#333") + self.header_label.pack(pady=20) + + self.card_canvas = tk.Canvas(self.game_frame, width=500, height=280, bg="white", highlightthickness=0) + self.card_canvas.pack(pady=10) + self.card_text = self.card_canvas.create_text(250, 140, text="", font=("Microsoft YaHei", 20, "bold"), width=480, fill="black") + self.card_canvas.bind("", self.flip_card) + + tk.Label(self.game_frame, text="Click card to reveal answer", font=("Arial", 10), bg="#f0f0f0", fg="#777").pack(pady=5) + + self.button_frame = tk.Frame(self.game_frame, bg="#f0f0f0") + self.button_frame.pack(pady=20, fill="x") + + btn_config = {"font": ("Microsoft YaHei", 10), "width": 12, "pady": 5, "bd": 1} + + self.btn_next = tk.Button(self.button_frame, text="⏭ Next", command=self.next_card, bg="#ddddff", **btn_config) + self.btn_remember = tk.Button(self.button_frame, text="Remembered", command=self.mark_remembered, bg="#ddffdd", **btn_config) + self.btn_forgot = tk.Button(self.button_frame, text="Forgot", command=self.mark_forgot, bg="#ffdddd", **btn_config) + self.btn_back_menu = tk.Button(self.button_frame, text="🏠 Main Menu", command=self.show_menu, bg="#eeeeee", **btn_config) + self.btn_back_menu.pack(side="left", padx=20) + + self.btn_delete = tk.Button(self.button_frame, text="🗑️ Delete", command=self.delete_card, bg="#ffcccc", **btn_config) + self.btn_delete.pack(side="left", padx=5) + + self.btn_edit = tk.Button(self.button_frame, text="✏️ Edit", command=self.edit_card, bg="#fff", **btn_config) + self.btn_edit.pack(side="left", padx=5) + + def setup_completion_ui(self): + tk.Label(self.completion_frame, text="🎉 Congratulations!", font=("Microsoft YaHei", 24, "bold"), bg="#f0f0f0", fg="#27ae60").pack(pady=50) + tk.Label(self.completion_frame, text="All cards in current queue completed.", font=("Microsoft YaHei", 14), bg="#f0f0f0").pack(pady=10) + tk.Button(self.completion_frame, text="🏠 Main Menu", command=self.show_menu, font=("Microsoft YaHei", 14), bg="#ddddff").pack(pady=30) + + def show_menu(self): + self.game_frame.pack_forget() + self.completion_frame.pack_forget() + self.menu_frame.pack(fill="both", expand=True) + + learn_count = len([c for c in self.cards if c.get("status") is None]) + review_count = len([c for c in self.cards if self.is_due_for_review(c)]) + + self.lbl_learn_count.config(text=f"To Learn: {learn_count} cards") + self.lbl_review_count.config(text=f"To Review: {review_count} cards") + + def start_learn(self): + self.mode = "learn" + self.current_card = {} + self.init_study_queue() + if not self.study_queue: + messagebox.showinfo("Info", "No new cards to learn!", parent=self.root) + return + self.start_game() + + def start_review(self): + self.mode = "review" + self.current_card = {} + + today = datetime.now().strftime("%Y-%m-%d") + + self.study_queue = [] + for c in self.cards: + nxt = c.get("next_review") + if nxt and nxt <= today: + self.study_queue.append(c) + elif c.get("status") == "forgot" and not nxt: + self.study_queue.append(c) + + print(f"Mode: {self.mode}, To study: {len(self.study_queue)}") + + if not self.study_queue: + messagebox.showinfo("Info", "No cards to review today!\nPlease learn new cards or wait for next review.", parent=self.root) + return + self.start_game() + + def start_game(self): + self.menu_frame.pack_forget() + self.completion_frame.pack_forget() + self.game_frame.pack(fill="both", expand=True) + self.next_card() + + def init_study_queue(self): + if self.mode == "learn": + self.study_queue = [c for c in self.cards if c.get("status") is None] + elif self.mode == "review": + self.study_queue = [c for c in self.cards if self.is_due_for_review(c)] + else: + self.study_queue = [] + print(f"Mode: {self.mode}, To study: {len(self.study_queue)}") + + def load_data(self): + if not os.path.exists(DATA_FILE): + return [] + try: + with open(DATA_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except: + return [] + + def save_data(self): + with open(DATA_FILE, "w", encoding="utf-8") as f: + json.dump(self.cards, f, ensure_ascii=False, indent=4) + + def next_card(self): + if self.mode == "learn" and self.current_card: + self.current_card["status"] = "learning" + self.current_card["level"] = 0 + self.current_card["last_review"] = datetime.now().strftime("%Y-%m-%d") + self.current_card["next_review"] = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + + self.save_data() + if self.current_card in self.study_queue: + self.study_queue.remove(self.current_card) + + self.btn_remember.pack_forget() + self.btn_forgot.pack_forget() + self.btn_next.pack(side="right", padx=20) + self.btn_back_menu.pack(side="left", padx=20) + + if not self.study_queue: + self.show_completion() + return + + if len(self.study_queue) > 1 and self.current_card in self.study_queue: + candidates = [c for c in self.study_queue if c != self.current_card] + self.current_card = random.choice(candidates) + else: + self.current_card = random.choice(self.study_queue) + + self.is_flipped = False + self.update_card_ui() + + def show_completion(self): + self.game_frame.pack_forget() + self.completion_frame.pack(fill="both", expand=True) + + def mark_remembered(self): + if self.current_card: + self.current_card["status"] = "remembered" + self.current_card["last_review"] = datetime.now().strftime("%Y-%m-%d") + + level = self.current_card.get("level", 0) + + intervals = [3, 5, 7, 15] + + if level < len(intervals): + days = intervals[level] + else: + days = 15 + + self.current_card["next_review"] = (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d") + self.current_card["level"] = level + 1 + + self.save_data() + if self.current_card in self.study_queue: + self.study_queue.remove(self.current_card) + self.next_card() + + def mark_forgot(self): + if self.current_card: + self.current_card["status"] = "forgot" + self.current_card["last_review"] = datetime.now().strftime("%Y-%m-%d") + + self.current_card["level"] = 0 + self.current_card["next_review"] = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") + + self.save_data() + self.next_card() + + def flip_card(self, event=None): + if not self.current_card: + return + + self.is_flipped = not self.is_flipped + self.update_card_ui() + + if self.is_flipped: + if self.mode == "review": + self.btn_next.pack_forget() + self.btn_forgot.pack(side="right", padx=10) + self.btn_remember.pack(side="right", padx=10) + else: + self.btn_remember.pack_forget() + self.btn_forgot.pack_forget() + self.btn_next.pack(side="right", padx=20) + + def update_card_ui(self): + if self.is_flipped: + self.card_canvas.itemconfig(self.card_text, text=self.current_card["answer"], fill="#2c3e50") + self.card_canvas.configure(bg="#e8f6f3") + self.header_label.config(text="✨ Answer") + else: + self.card_canvas.itemconfig(self.card_text, text=self.current_card["question"], fill="black") + self.card_canvas.configure(bg="white") + self.header_label.config(text="🤔 Question (" + ("Learn" if self.mode == "learn" else "Review") + ")") + + def add_card_window(self): + add_win = tk.Toplevel(self.root) + add_win.title("Add New Card") + add_win.geometry("400x300") + + tk.Label(add_win, text="Question (Front):", font=("Microsoft YaHei", 10)).pack(pady=5) + q_entry = tk.Entry(add_win, width=40, font=("Microsoft YaHei", 10)) + q_entry.pack(pady=5) + q_entry.focus_set() + + tk.Label(add_win, text="Answer (Back):", font=("Microsoft YaHei", 10)).pack(pady=5) + a_entry = tk.Entry(add_win, width=40, font=("Microsoft YaHei", 10)) + a_entry.pack(pady=5) + + def save_new(): + q = q_entry.get().strip() + a = a_entry.get().strip() + if q and a: + new_card = {"question": q, "answer": a, "status": None} + self.cards.append(new_card) + self.save_data() + if self.mode is None: + self.show_menu() + + messagebox.showinfo("Success", "Card added!", parent=add_win) + add_win.destroy() + else: + messagebox.showwarning("Warning", "Question and Answer cannot be empty", parent=add_win) + + tk.Button(add_win, text="Save", command=save_new, bg="#ddffdd", width=15).pack(pady=20) + + def delete_card(self): + """Delete current card""" + if not self.current_card: + return + + confirm = messagebox.askyesno("Confirm Delete", "Are you sure you want to delete this card? This cannot be undone.", parent=self.root) + if confirm: + if self.current_card in self.cards: + self.cards.remove(self.current_card) + if self.current_card in self.study_queue: + self.study_queue.remove(self.current_card) + + self.save_data() + messagebox.showinfo("Success", "Card deleted", parent=self.root) + self.next_card() + + def edit_card(self): + if not self.current_card: + return + + edit_win = tk.Toplevel(self.root) + edit_win.title("Edit Card") + edit_win.geometry("400x300") + + tk.Label(edit_win, text="Question (Front):", font=("Microsoft YaHei", 10)).pack(pady=5) + q_entry = tk.Entry(edit_win, width=40, font=("Microsoft YaHei", 10)) + q_entry.pack(pady=5) + q_entry.insert(0, self.current_card.get("question", "")) + q_entry.focus_set() + + tk.Label(edit_win, text="Answer (Back):", font=("Microsoft YaHei", 10)).pack(pady=5) + a_entry = tk.Entry(edit_win, width=40, font=("Microsoft YaHei", 10)) + a_entry.pack(pady=5) + a_entry.insert(0, self.current_card.get("answer", "")) + + def save_edit(): + q = q_entry.get().strip() + a = a_entry.get().strip() + if q and a: + self.current_card["question"] = q + self.current_card["answer"] = a + self.save_data() + self.update_card_ui() + messagebox.showinfo("Success", "Card updated!", parent=edit_win) + edit_win.destroy() + else: + messagebox.showwarning("Warning", "Question and Answer cannot be empty", parent=edit_win) + + tk.Button(edit_win, text="Save Changes", command=save_edit, bg="#ddffdd", width=15).pack(pady=20) + + def is_due_for_review(self, card): + status = card.get("status") + if status == "forgot": + return True + + next_review_str = card.get("next_review") + if next_review_str: + try: + next_review_date = datetime.strptime(next_review_str, "%Y-%m-%d") + if datetime.now() >= next_review_date: + return True + else: + return False + except ValueError: + pass + + last_review_str = card.get("last_review") + if status == "remembered" and last_review_str: + try: + last_review_date = datetime.strptime(last_review_str, "%Y-%m-%d") + if (datetime.now() - last_review_date) > timedelta(days=3): + return True + except ValueError: + pass + + return False + +if __name__ == "__main__": + root = tk.Tk() + app = FlashcardApp(root) + root.mainloop() \ No newline at end of file diff --git a/Flashcards/flashcards_data.json b/Flashcards/flashcards_data.json new file mode 100644 index 00000000..c29b1ada --- /dev/null +++ b/Flashcards/flashcards_data.json @@ -0,0 +1,82 @@ +[ + { + "question": "What keyword is used to create an anonymous function in Python?", + "answer": "lambda", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "What is the primary difference between a list and a tuple?", + "answer": "Lists are mutable, while tuples are immutable.", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "Which function is used to output text to the console?", + "answer": "print()", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "What data type is returned by `type(3.14)`?", + "answer": "float", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "Which keyword is used to include an external module?", + "answer": "import", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "Which character is used to start a single-line comment?", + "answer": "#", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "What is the result of the expression `2 ** 3`?", + "answer": "8", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "Which list method adds an item to the end of the list?", + "answer": "append()", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "Which logical operator returns True only if both operands are True?", + "answer": "and", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + }, + { + "question": "Which built-in function returns the number of items in a sequence?", + "answer": "len()", + "status": "learning", + "level": 0, + "last_review": "2025-12-31", + "next_review": "2025-12-31" + } +] \ No newline at end of file