diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5df195b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +test/ +dist/ +build/ +.venv/ +*.zip +main.spec \ No newline at end of file diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..e5c9f4a Binary files /dev/null and b/assets/favicon.ico differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..b60864f --- /dev/null +++ b/main.py @@ -0,0 +1,327 @@ +import customtkinter as ctk +from tkinter import filedialog, messagebox + +ctk.set_appearance_mode("System") +ctk.set_default_color_theme("dark-blue") + +class MainApp(ctk.CTk): + def __init__(self): + super().__init__() + + self.title("Liskod") + self.geometry("400x403") + self.resizable(False, False) + + # --- Data State --- + self.current_file_path = None + self.list_in_memory = [] + self.set_in_memory = set() + self.any_change = False + + # --- Grid Configuration --- + self.grid_rowconfigure(0, weight=0) + self.grid_rowconfigure(1, weight=0) + self.grid_rowconfigure(2, weight=0) + self.grid_columnconfigure(0, weight=1) + + # ============================================================ + # ROW 0: File Input & Mode Checkbox + # ============================================================ + self.row1_frame = ctk.CTkFrame(self) + self.row1_frame.grid(row=0, column=0, padx=20, pady=(15, 10), sticky="ew") + self.row1_frame.grid_columnconfigure(0, weight=1) + self.row1_frame.grid_columnconfigure(1, weight=0) + + self.var_edit_mode = ctk.BooleanVar(value=False) + self.chk_mode = ctk.CTkCheckBox( + self.row1_frame, + text="Edit Existing File", + font=("Arial", 14), + variable=self.var_edit_mode, + command=self.toggle_confirm + ) + self.chk_mode.grid(row=0, column=0, padx=10, pady=(15, 5), sticky="w") + + self.sort_toggle = ctk.BooleanVar(value=False) + self.chk_mode2 = ctk.CTkCheckBox( + self.row1_frame, + text="Sort Out", + font=("Arial", 14), + variable=self.sort_toggle, + command=None + ) + self.chk_mode2.grid(row=0, column=1, pady=(15, 5), sticky="e") + + self.entry_filepath = ctk.CTkEntry( + self.row1_frame, + text_color="gray", + font=("Arial", 14), + placeholder_text="New File (Save As)", + height=35 + ) + self.entry_filepath.grid(row=1, column=0, padx=(10, 0), pady=5, sticky="ew") + self.entry_filepath.configure(state="disabled") + + self.btn_browse = ctk.CTkButton( + self.row1_frame, + text="Browse", + command=self.browse_file, + width=80, + height=35, + font=("Arial", 14), + ) + self.btn_browse.grid(row=1, column=1, padx=(10, 10), pady=10) + + # ============================================================ + # ROW 1: Preview Window + # ============================================================ + self.preview_frame = ctk.CTkFrame(self) + self.preview_frame.grid(row=1, column=0, padx=20, pady=(5, 10), sticky="ew") + self.preview_frame.grid_columnconfigure(0, weight=1) + + self.lbl_preview = ctk.CTkLabel(self.preview_frame, text="Preview", font=("Arial", 14)) + self.lbl_preview.grid(row=0, column=0, padx=10, pady=(5, 0), sticky="w") + + self.txt_preview = ctk.CTkTextbox(self.preview_frame, height=110) + self.txt_preview.grid(row=1, column=0, padx=10, pady=(5, 10), sticky="ew") + self.txt_preview.configure(state="disabled") + + self.input_container = ctk.CTkFrame(self.preview_frame, fg_color="transparent") + self.input_container.grid(row=2, column=0, padx=10, pady=(5, 10), sticky="ew") + self.input_container.grid_columnconfigure(0, weight=1) + + self.entry_text = ctk.CTkEntry(self.input_container, placeholder_text="Type content here...", height=35, font=("Arial", 14)) + self.entry_text.grid(row=0, column=0, padx=(0, 10), sticky="ew") + self.entry_text.bind("", lambda event: self.add_line_to_memory()) + + self.btn_add = ctk.CTkButton( + self.input_container, + text="Add", + width=80, + font=("Arial", 14), + command=self.add_line_to_memory, + height=35 + ) + self.btn_add.grid(row=0, column=1, sticky="e") + + # ============================================================ + # ROW 2: Save Button + # ============================================================ + self.row3_frame = ctk.CTkFrame(self, fg_color="transparent") + self.row3_frame.grid(row=3, column=0, padx=20, pady=(5, 20), sticky="ew") + self.row3_frame.grid_columnconfigure(0, weight=0) + self.row3_frame.grid_columnconfigure(1, weight=0) + + self.last_status = ctk.CTkLabel( + self.row3_frame, + height=35, + width=290, + fg_color="#282828", + corner_radius=4, + text_color="gray", + text="Status", + font=("Arial", 14), + ) + self.last_status.grid(row=0, column=0, padx=(0, 10), sticky="ew") + + self.btn_save = ctk.CTkButton( + self.row3_frame, + text="Save", + height=35, + width=60, + font=("Arial", 14), + fg_color="green", + hover_color="darkgreen", + command=self.save_action + ) + self.btn_save.grid(row=0, column=1, sticky="e") + self.toggle_mode() + + self.protocol("WM_DELETE_WINDOW", self.on_closing) + + # --- Logic Methods --- + + def toggle_confirm(self): + if len(self.list_in_memory) > 0: + new_state = self.var_edit_mode.get() + action = "enable" if new_state else "disable" + + answer = messagebox.askyesno( + title="Confirmation", + message=f"Are you sure you want to {action} this setting?" + ) + if answer: + self.delete_memory() + self.update_preview() + self.toggle_mode() + else: + self.var_edit_mode.set(not new_state) + else: + self.toggle_mode() + self.update_preview() + + + def toggle_mode(self): + new_state = self.var_edit_mode.get() + + if new_state: + # Checked: Edit Existing Mode + self.btn_browse.configure(state="normal") + self.entry_filepath.configure(state="normal") + self.entry_filepath.delete(0, "end") + if self.current_file_path: + self.entry_filepath.insert(0, f"...{self.current_file_path[-30:]}") + else: + self.entry_filepath.insert(0, "No file loaded...") + self.btn_add.configure(state="disabled") + self.entry_filepath.configure(state="disabled") + else: + # Unchecked: New File Mode + self.current_file_path = None + self.btn_browse.configure(state="disabled") + self.btn_add.configure(state="normal") + self.entry_filepath.configure(state="normal") + self.entry_filepath.delete(0, "end") + self.entry_filepath.insert(0, "Create New File") + self.entry_filepath.configure(state="disabled") + + + def browse_file(self): + filename = filedialog.askopenfilename( + title="Select a Text File", + filetypes=[("Text files", "*.txt")] + ) + + if filename: + if not filename.lower().endswith('.txt'): + messagebox.showerror("Error", "Only .txt files are allowed!") + return + + self.current_file_path = filename + + self.entry_filepath.configure(state="normal") + self.entry_filepath.delete(0, "end") + self.entry_filepath.insert(0, f"...{filename[-30:]}") + self.entry_filepath.configure(state="disabled") + + try: + with open(filename, "r", encoding="utf-8") as f: + self.list_in_memory = [line.strip() for line in f] + + self.set_in_memory = set(self.list_in_memory) + duplicates = len(self.list_in_memory) - len(self.set_in_memory) + self.last_status.configure( + text=f"File loaded, {duplicates} duplicate(s) removed", + text_color="#9efc90" + ) + self.list_in_memory = list(dict.fromkeys(self.list_in_memory)) + + self.btn_add.configure(state="normal") + self.update_preview() + + except Exception as e: + messagebox.showerror("Error", f"Could not read file:\n{e}") + self.last_status.configure( + text=f"Could not read file:\n{e}", + text_color="#ff9d9d" + ) + + + def add_line_to_memory(self): + text = self.entry_text.get() + if not text: + return + + clean_text = text.replace('\n', '').replace('\r', '').strip() + if clean_text in self.set_in_memory: + self.entry_text.delete(0, "end") + self.last_status.configure( + text=f"Line \"{clean_text[:10]}\" already exists!", + text_color="#ff9d9d" + ) + else: + if not len(clean_text) == 0: + self.any_change = True + self.list_in_memory.append(clean_text) + self.set_in_memory.add(clean_text) + self.entry_text.delete(0, "end") + self.update_preview() + self.last_status.configure( + text=f"Line \"{clean_text[:10]}\" added!", + text_color="#9efc90" + ) + self.entry_text.delete(0, "end") + + + def delete_memory(self): + self.list_in_memory = [] + self.set_in_memory = set() + + + def update_preview(self): + self.txt_preview.configure(state="normal") + self.txt_preview.delete("1.0", "end") + + if not self.list_in_memory: + display_text = "" + elif len(self.list_in_memory) > 5: + last_five = ["..."] + self.list_in_memory[-5:] + display_text = "\n".join(last_five) + else: + last_five = self.list_in_memory[-5:] + display_text = "\n".join(last_five) + + self.txt_preview.insert("1.0", display_text) + self.txt_preview.configure(state="disabled") + + + def save_action(self): + if len(self.list_in_memory) <= 0: + return + + target_path = filedialog.asksaveasfilename( + title="Save As", + defaultextension=".txt", + filetypes=[("Text files", "*.txt")] + ) + if not target_path: + return + + try: + with open(target_path, "w", encoding="utf-8") as f: + to_be_saved = self.list_in_memory + if self.sort_toggle.get(): + to_be_saved = sorted(self.list_in_memory) + full_content = "\n".join(to_be_saved) + f.write(full_content) + + self.any_change = False + self.last_status.configure( + text="File saved successfully!", + text_color="#9efc90" + ) + self.var_edit_mode.set(True) + self.current_file_path = target_path + + self.entry_filepath.configure(state="normal") + self.entry_filepath.delete(0, "end") + self.entry_filepath.insert(0, f"...{self.current_file_path[-30:]}") + self.entry_filepath.configure(state="disabled") + + except Exception as e: + self.last_status.configure( + text=f"Could not save file:\n{e}", + text_color="#ff9d9d" + ) + + def on_closing(self): + if self.any_change: + if messagebox.askokcancel("Quit", "There are unsaved changes, are you sure you want to quit?"): + self.destroy() + return + else: + self.destroy() + +if __name__ == "__main__": + app = MainApp() + app.mainloop() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7753ca1 Binary files /dev/null and b/requirements.txt differ