import os
import sys
import json
import fnmatch
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, ttk
import threading
import platform
if platform.system() == "Windows":
import winreg
def register_makelove_association(script_path):
try:
pythonw_path = sys.executable
if not pythonw_path.lower().endswith("pythonw.exe"):
pythonw_path = pythonw_path[:-10] + "pythonw.exe" if pythonw_path.endswith("python.exe") else pythonw_path
cmd = f'"{pythonw_path}" "{script_path}" "%1"'
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\Classes\.makelove") as key:
winreg.SetValue(key, "", winreg.REG_SZ, "CodeCollector.makelove")
with winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\Classes\CodeCollector.makelove\shell\open\command") as key:
winreg.SetValue(key, "", winreg.REG_SZ, cmd)
return True
except Exception as e:
print(f"注册文件关联失败: {e}")
return False
def is_makelove_registered():
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Classes\.makelove") as key:
val = winreg.QueryValue(key, "")
if val != "CodeCollector.makelove":
return False
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Classes\CodeCollector.makelove\shell\open\command") as key:
cmd = winreg.QueryValue(key, "")
return True
except FileNotFoundError:
return False
class DirSelector(tk.Toplevel):
def __init__(self, master, app, all_dirs, exclude_var):
super().__init__(master)
self.app = app
self.title("选择排除目录")
self.geometry("500x450")
self.resizable(True, True)
self.all_dirs = all_dirs
self.exclude_var = exclude_var
self.selected_dirs = set()
search_frame = ttk.Frame(self)
search_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(search_frame, text="搜索目录:").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *args: self.update_list())
ttk.Entry(search_frame, textvariable=self.search_var, width=30).pack(side=tk.LEFT, padx=5)
ttk.Button(search_frame, text="清除", command=lambda: self.search_var.set("")).pack(side=tk.LEFT)
list_frame = ttk.Frame(self)
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
if self.all_dirs:
self.listbox = tk.Listbox(list_frame, selectmode='multiple', exportselection=False)
scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.listbox.yview)
self.listbox.configure(yscrollcommand=scrollbar.set)
self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.listbox.bind('<<ListboxSelect>>', self.on_select)
else:
ttk.Label(list_frame, text="未发现任何子目录", anchor="center").pack(expand=True)
option_frame = ttk.Frame(self)
option_frame.pack(fill=tk.X, padx=10, pady=2)
self.use_dirname_var = tk.BooleanVar(value=True)
ttk.Checkbutton(option_frame, text="仅用目录名(*目录名*)", variable=self.use_dirname_var).pack(side=tk.LEFT)
ttk.Checkbutton(option_frame, text="或用完整相对路径", variable=tk.BooleanVar(value=False),
command=lambda: self.use_dirname_var.set(not self.use_dirname_var.get())).pack(side=tk.LEFT)
btn_frame = ttk.Frame(self)
btn_frame.pack(fill=tk.X, padx=10, pady=5)
if self.all_dirs:
ttk.Button(btn_frame, text="全选", command=self.select_all).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="取消全选", command=self.deselect_all).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="添加到排除列表", command=self.add_to_exclude).pack(side=tk.RIGHT, padx=5)
ttk.Button(btn_frame, text="关闭", command=self.destroy).pack(side=tk.RIGHT, padx=2)
if self.all_dirs:
self.update_list()
def update_list(self):
search_text = self.search_var.get().lower()
self.listbox.delete(0, tk.END)
for d in self.all_dirs:
if search_text in d.lower():
self.listbox.insert(tk.END, d)
for i in range(self.listbox.size()):
if self.listbox.get(i) in self.selected_dirs:
self.listbox.selection_set(i)
def on_select(self, event=None):
self.selected_dirs = {self.listbox.get(i) for i in self.listbox.curselection()}
def select_all(self):
self.listbox.selection_set(0, tk.END)
self.on_select()
def deselect_all(self):
self.listbox.selection_clear(0, tk.END)
self.selected_dirs.clear()
def add_to_exclude(self):
if not self.selected_dirs:
messagebox.showinfo("提示", "请先选择目录")
return
patterns = []
for d in self.selected_dirs:
if self.use_dirname_var.get():
dirname = os.path.basename(d)
patterns.append(f"*{dirname}*")
else:
patterns.append(d)
current = self.exclude_var.get().strip()
existing = [p.strip() for p in current.split(',') if p.strip()] if current else []
for pat in patterns:
if pat not in existing:
existing.append(pat)
self.exclude_var.set(", ".join(existing))
self.app.log_message(f"已添加排除模式: {', '.join(patterns)},请点击「开始扫描文件」应用更改。")
self.destroy()
class SuffixSelector(tk.Toplevel):
def __init__(self, master, app):
super().__init__(master)
self.app = app
self.title("选择后缀")
self.geometry("500x450")
self.resizable(True, True)
search_frame = ttk.Frame(self)
search_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(search_frame, text="搜索后缀:").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *args: self.update_checkboxes())
ttk.Entry(search_frame, textvariable=self.search_var, width=30).pack(side=tk.LEFT, padx=5)
ttk.Button(search_frame, text="清除", command=lambda: self.search_var.set("")).pack(side=tk.LEFT)
self.canvas = tk.Canvas(self, borderwidth=0, highlightthickness=0)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=scrollbar.set)
self.inner = ttk.Frame(self.canvas)
self.canvas.create_window((0,0), window=self.inner, anchor="nw")
self.inner.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
self.canvas.pack(side="left", fill="both", expand=True, padx=10, pady=5)
scrollbar.pack(side="right", fill="y")
self.canvas.bind("<Enter>", lambda e: self.canvas.focus_set())
self.canvas.bind("<MouseWheel>", lambda e: self.canvas.yview_scroll(int(-1*(e.delta/120)), "units"))
btn_frame = ttk.Frame(self)
btn_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Button(btn_frame, text="全选", command=self.select_all).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="取消全选", command=self.deselect_all).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="应用并关闭", command=self.apply_and_close).pack(side=tk.RIGHT, padx=5)
ttk.Button(btn_frame, text="关闭", command=self.destroy).pack(side=tk.RIGHT, padx=2)
self.checkbuttons = []
self.update_checkboxes()
def update_checkboxes(self):
for cb, _ in self.checkbuttons:
cb.destroy()
self.checkbuttons.clear()
filter_text = self.search_var.get().lower()
if filter_text:
display = [s for s in self.app.all_suffixes if filter_text in s.lower()]
else:
display = self.app.all_suffixes.copy()
if not display:
return
self.inner.update_idletasks()
frame_width = self.inner.winfo_width()
if frame_width < 100:
frame_width = 400
max_len = max(len(s) for s in display) if display else 5
btn_width = max(100, max_len * 10 + 40)
cols = max(1, frame_width // btn_width)
for i, ext in enumerate(display):
if ext not in self.app.suffix_vars:
self.app.suffix_vars[ext] = tk.BooleanVar(value=True)
var = self.app.suffix_vars[ext]
cb = ttk.Checkbutton(self.inner, text=ext, variable=var,
command=self.app.on_suffix_changed)
row = i // cols
col = i % cols
cb.grid(row=row, column=col, padx=3, pady=2, sticky='w')
self.checkbuttons.append((cb, ext))
self.inner.update_idletasks()
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def select_all(self):
self.app._batch_updating = True
for var in self.app.suffix_vars.values():
var.set(True)
self.app._batch_updating = False
self.app.on_suffix_changed()
def deselect_all(self):
self.app._batch_updating = True
for var in self.app.suffix_vars.values():
var.set(False)
self.app._batch_updating = False
self.app.on_suffix_changed()
def apply_and_close(self):
self.app.update_filter_display()
self.destroy()
class HeaderFooterEditor(tk.Toplevel):
def __init__(self, master, app):
super().__init__(master)
self.app = app
self.title("编辑头部/尾部文本")
self.geometry("700x600")
self.resizable(True, True)
self.protocol("WM_DELETE_WINDOW", self.save_and_close)
header_frame = ttk.LabelFrame(self, text="头部文本(在文件内容之前)")
header_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
self.header_text = scrolledtext.ScrolledText(header_frame, wrap=tk.WORD)
self.header_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.header_text.insert("1.0", self.app.header_content)
footer_frame = ttk.LabelFrame(self, text="尾部文本(在文件内容之后)")
footer_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
self.footer_text = scrolledtext.ScrolledText(footer_frame, wrap=tk.WORD)
self.footer_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.footer_text.insert("1.0", self.app.footer_content)
btn_frame = ttk.Frame(self)
btn_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Button(btn_frame, text="保存并关闭", command=self.save_and_close).pack(side=tk.RIGHT, padx=5)
def save_and_close(self):
self.app.header_content = self.header_text.get("1.0", tk.END).strip()
self.app.footer_content = self.footer_text.get("1.0", tk.END).strip()
self.destroy()
class CodeCollector:
def __init__(self, root):
self.root = root
self.root.title("C++ 源码收集器 v13.1")
self.root.geometry("900x750")
self.root.resizable(True, True)
self.all_files = []
self.all_suffixes = sorted([])
self.selected_files = set()
self.root_dir = ""
self.all_dirs = []
self.suffix_vars = {}
self._batch_updating = False
self.pending_selected_files = None
self.pending_active_suffixes = None
self.header_content = ""
self.footer_content = ""
# ---------- 注册 .makelove 关联 ----------
if platform.system() == "Windows":
script_path = os.path.abspath(__file__)
if not is_makelove_registered():
if register_makelove_association(script_path):
self.root.after(100, lambda: self.log_message("已自动注册 .makelove 文件关联,现可双击 .makelove 文件打开程序。"))
else:
try:
pythonw_path = sys.executable
if not pythonw_path.lower().endswith("pythonw.exe"):
pythonw_path = pythonw_path[:-10] + "pythonw.exe" if pythonw_path.endswith("python.exe") else pythonw_path
cmd = f'"{pythonw_path}" "{script_path}" "%1"'
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Classes\CodeCollector.makelove\shell\open\command", 0, winreg.KEY_SET_VALUE) as key:
winreg.SetValue(key, "", winreg.REG_SZ, cmd)
except:
pass
# ---------- 解析命令行参数 / 自动加载 1.makelove ----------
config_to_load = None
if len(sys.argv) > 1:
file_arg = sys.argv[1]
if os.path.isfile(file_arg) and file_arg.lower().endswith(".makelove"):
config_to_load = file_arg
if config_to_load is None:
default_config = os.path.join(os.path.dirname(os.path.abspath(__file__)), "1.makelove")
if os.path.isfile(default_config):
config_to_load = default_config
self.setup_ui()
if config_to_load:
self._load_config_file(config_to_load)
self.log_message(f"已自动加载配置: {os.path.basename(config_to_load)}")
self.root.bind_all("<MouseWheel>", self._on_mousewheel_global)
def setup_ui(self):
# ========== 目录选择 ==========
frame_top = ttk.Frame(self.root)
frame_top.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(frame_top, text="根目录:").pack(side=tk.LEFT)
self.path_var = tk.StringVar()
ttk.Entry(frame_top, textvariable=self.path_var, width=50).pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
ttk.Button(frame_top, text="浏览...", command=self.browse_folder).pack(side=tk.LEFT)
# ========== 扫描选项 + 排除目录 ==========
frame_scan = ttk.Frame(self.root)
frame_scan.pack(fill=tk.X, padx=10, pady=5)
self.recursive_var = tk.BooleanVar(value=True)
ttk.Checkbutton(frame_scan, text="包含子文件夹", variable=self.recursive_var).pack(side=tk.LEFT, padx=5)
ttk.Label(frame_scan, text="排除目录:").pack(side=tk.LEFT, padx=(15,2))
self.exclude_var = tk.StringVar(value="")
self.exclude_entry = ttk.Entry(frame_scan, textvariable=self.exclude_var, width=30)
self.exclude_entry.pack(side=tk.LEFT, padx=5)
ttk.Button(frame_scan, text="选择目录", command=self.open_dir_selector).pack(side=tk.LEFT, padx=5)
self.scan_btn = ttk.Button(frame_scan, text="开始扫描文件", command=self.start_scan)
self.scan_btn.pack(side=tk.LEFT, padx=10)
self.scan_progress = ttk.Label(frame_scan, text="")
self.scan_progress.pack(side=tk.LEFT, padx=5)
ttk.Button(frame_scan, text="保存配置", command=self.save_config).pack(side=tk.RIGHT, padx=2)
ttk.Button(frame_scan, text="加载配置", command=self.load_config).pack(side=tk.RIGHT, padx=2)
# ========== 后缀过滤摘要 ==========
suffix_summary = ttk.Frame(self.root)
suffix_summary.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(suffix_summary, text="后缀过滤:").pack(side=tk.LEFT)
self.suffix_display_var = tk.StringVar(value="")
ttk.Entry(suffix_summary, textvariable=self.suffix_display_var, state="readonly", width=60).pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
ttk.Button(suffix_summary, text="选择后缀", command=self.open_suffix_selector).pack(side=tk.LEFT, padx=5)
ttk.Button(suffix_summary, text="编辑头部/尾部", command=self.open_header_footer_editor).pack(side=tk.LEFT, padx=5)
# ========== 文件列表 ==========
frame_list = ttk.LabelFrame(self.root, text="文件列表(勾选要合并的文件)")
frame_list.pack(fill=tk.BOTH, padx=10, pady=5, expand=True)
search_frame = ttk.Frame(frame_list)
search_frame.pack(fill=tk.X, padx=5, pady=2)
ttk.Label(search_frame, text="搜索:").pack(side=tk.LEFT)
self.search_var = tk.StringVar()
self.search_var.trace_add("write", lambda *args: self.update_listbox())
ttk.Entry(search_frame, textvariable=self.search_var, width=30).pack(side=tk.LEFT, padx=5)
ttk.Button(search_frame, text="清除", command=lambda: self.search_var.set("")).pack(side=tk.LEFT)
list_container = ttk.Frame(frame_list)
list_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.listbox = tk.Listbox(list_container, selectmode='multiple', exportselection=False)
scrollbar_list = ttk.Scrollbar(list_container, orient="vertical", command=self.listbox.yview)
self.listbox.configure(yscrollcommand=scrollbar_list.set)
self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar_list.pack(side=tk.RIGHT, fill=tk.Y)
self.listbox.bind('<<ListboxSelect>>', self.on_listbox_select)
frame_list_btns = ttk.Frame(frame_list)
frame_list_btns.pack(fill=tk.X, padx=5, pady=2)
ttk.Button(frame_list_btns, text="全选全部", command=self.select_all).pack(side=tk.LEFT, padx=2)
ttk.Button(frame_list_btns, text="取消全部", command=self.deselect_all).pack(side=tk.LEFT, padx=2)
ttk.Button(frame_list_btns, text="选中可见", command=self.select_visible).pack(side=tk.LEFT, padx=2)
ttk.Button(frame_list_btns, text="取消可见", command=self.deselect_visible).pack(side=tk.LEFT, padx=2)
# ========== 操作按钮 ==========
frame_actions = ttk.Frame(self.root)
frame_actions.pack(fill=tk.X, padx=10, pady=5)
self.save_btn = ttk.Button(frame_actions, text="保存到文件", command=self.save_to_file, state='disabled')
self.save_btn.pack(side=tk.LEFT, padx=5)
self.copy_btn = ttk.Button(frame_actions, text="复制到剪贴板", command=self.copy_to_clipboard, state='disabled')
self.copy_btn.pack(side=tk.LEFT, padx=5)
ttk.Button(frame_actions, text="退出", command=self.root.destroy).pack(side=tk.RIGHT, padx=5)
# ========== 日志 ==========
self.log = scrolledtext.ScrolledText(self.root, height=6, state='disabled')
self.log.pack(fill=tk.BOTH, padx=10, pady=5, expand=False)
def browse_folder(self):
folder = filedialog.askdirectory()
if folder:
self.path_var.set(folder)
def log_message(self, msg):
self.log.configure(state='normal')
self.log.insert(tk.END, msg + "\n")
self.log.see(tk.END)
self.log.configure(state='disabled')
def update_button_states(self):
if self.selected_files:
self.save_btn.config(state='normal')
self.copy_btn.config(state='normal')
else:
self.save_btn.config(state='disabled')
self.copy_btn.config(state='disabled')
def open_dir_selector(self):
DirSelector(self.root, self, self.all_dirs, self.exclude_var)
def open_suffix_selector(self):
SuffixSelector(self.root, self)
def open_header_footer_editor(self):
HeaderFooterEditor(self.root, self)
def _on_mousewheel_global(self, event):
widget = self.root.winfo_containing(event.x_root, event.y_root)
if widget is None:
return
if self._is_descendant(widget, self.listbox):
self._on_mousewheel_list(event)
elif self._is_descendant(widget, self.log):
self._on_mousewheel_log(event)
def _is_descendant(self, widget, parent):
while widget is not None:
if widget is parent:
return True
widget = widget.master
return False
def _on_mousewheel_list(self, event):
self.listbox.yview_scroll(int(-1*(event.delta/120)), "units")
def _on_mousewheel_log(self, event):
self.log.yview_scroll(int(-1*(event.delta/120)), "units")
def get_exclude_patterns(self):
raw = self.exclude_var.get().strip()
if not raw:
return []
return [p.strip() for p in raw.split(',') if p.strip()]
def update_filter_display(self):
active = self.get_active_suffixes()
self.suffix_display_var.set(", ".join(active))
def get_active_suffixes(self):
return [ext for ext, var in self.suffix_vars.items() if var.get()]
def on_suffix_changed(self):
if self._batch_updating:
return
self.update_filter_display()
self.apply_filter()
def start_scan(self):
root_dir = self.path_var.get().strip()
if not root_dir or not os.path.isdir(root_dir):
messagebox.showerror("错误", "请选择有效的根目录")
return
self.scan_btn.config(state='disabled')
self.save_btn.config(state='disabled')
self.copy_btn.config(state='disabled')
self.listbox.delete(0, tk.END)
self.all_files.clear()
self.selected_files.clear()
self.root_dir = root_dir
self.scan_progress.config(text="扫描中...")
self.log_message("开始扫描所有文件...")
recursive = self.recursive_var.get()
exclude_patterns = self.get_exclude_patterns()
thread = threading.Thread(target=self.scan_files, args=(root_dir, recursive, exclude_patterns))
thread.daemon = True
thread.start()
def scan_files(self, root_dir, recursive, exclude_patterns):
files = []
suffixes = set()
dirs = []
try:
for dirpath, dirnames, filenames in os.walk(root_dir):
for d in dirnames:
rel_dir = os.path.relpath(os.path.join(dirpath, d), root_dir)
dirs.append(rel_dir)
if exclude_patterns:
dirnames[:] = [d for d in dirnames if not any(
fnmatch.fnmatch(d, pat) for pat in exclude_patterns
)]
rel = os.path.relpath(dirpath, root_dir)
if rel != '.':
dirs.append(rel)
for fname in filenames:
full = os.path.join(dirpath, fname)
rel_file = os.path.relpath(full, root_dir)
files.append(rel_file)
ext = os.path.splitext(fname)[1].lower()
if ext:
suffixes.add(ext)
if not recursive:
break
except Exception as e:
self.root.after(0, self.on_scan_error, str(e))
return
dirs = sorted(set(dirs))
self.root.after(0, self.on_scan_complete, files, sorted(suffixes), dirs)
def on_scan_error(self, err_msg):
self.log_message(f"扫描错误: {err_msg}")
self.scan_btn.config(state='normal')
self.scan_progress.config(text="")
def on_scan_complete(self, files, suffixes, dirs):
self.all_files = files
self.all_suffixes = suffixes
self.all_dirs = dirs
for ext in self.all_suffixes:
if ext not in self.suffix_vars:
self.suffix_vars[ext] = tk.BooleanVar(value=True)
if self.pending_active_suffixes is not None:
for ext, var in self.suffix_vars.items():
var.set(ext in self.pending_active_suffixes)
self.pending_active_suffixes = None
if self.pending_selected_files is not None:
restored = set(self.pending_selected_files).intersection(self.all_files)
self.selected_files = restored
self.pending_selected_files = None
else:
self.selected_files = set(files)
self.update_filter_display()
self.apply_filter()
self.scan_btn.config(state='normal')
self.update_button_states()
self.scan_progress.config(text=f"找到 {len(files)} 个文件,{len(suffixes)} 种后缀,{len(dirs)} 个子目录")
self.log_message(f"扫描完成,共找到 {len(files)} 个文件,{len(dirs)} 个子目录。")
def get_filtered_files(self):
active_exts = self.get_active_suffixes()
if not active_exts:
return []
return [f for f in self.all_files if os.path.splitext(f)[1].lower() in active_exts]
def apply_filter(self):
filtered = self.get_filtered_files()
new_selected = {f for f in filtered if f in self.selected_files}
if not new_selected and filtered:
new_selected = set(filtered)
self.selected_files = new_selected
self.update_listbox()
self.update_button_states()
self.log_message(f"过滤后显示 {len(filtered)} 个文件。")
def update_listbox(self):
search_text = self.search_var.get().lower()
filtered_files = self.get_filtered_files()
self.listbox.delete(0, tk.END)
for rel in filtered_files:
if search_text in rel.lower():
self.listbox.insert(tk.END, rel)
for idx in range(self.listbox.size()):
rel = self.listbox.get(idx)
if rel in self.selected_files:
self.listbox.selection_set(idx)
def on_listbox_select(self, event=None):
visible = [self.listbox.get(i) for i in range(self.listbox.size())]
selected_indices = self.listbox.curselection()
visible_selected = {self.listbox.get(i) for i in selected_indices}
for rel in visible:
if rel in visible_selected:
self.selected_files.add(rel)
else:
self.selected_files.discard(rel)
self.update_button_states()
def select_all(self):
filtered = self.get_filtered_files()
self.selected_files = set(filtered)
self.update_listbox()
self.update_button_states()
def deselect_all(self):
self.selected_files.clear()
self.update_listbox()
self.update_button_states()
def select_visible(self):
for idx in range(self.listbox.size()):
self.selected_files.add(self.listbox.get(idx))
self.update_listbox()
self.update_button_states()
def deselect_visible(self):
for idx in range(self.listbox.size()):
self.selected_files.discard(self.listbox.get(idx))
self.update_listbox()
self.update_button_states()
def save_config(self):
if not self.root_dir:
messagebox.showerror("错误", "请先扫描一个目录")
return
config = {
"root_dir": self.root_dir,
"recursive": self.recursive_var.get(),
"exclude_patterns": self.get_exclude_patterns(),
"active_suffixes": self.get_active_suffixes(),
"selected_files": list(self.selected_files),
"header": self.header_content,
"footer": self.footer_content
}
filepath = filedialog.asksaveasfilename(
defaultextension=".makelove",
filetypes=[("配置文件", "*.makelove"), ("所有文件", "*.*")],
title="保存配置文件"
)
if filepath:
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
self.log_message(f"配置已保存到: {filepath}")
def _load_config_file(self, filepath):
try:
with open(filepath, 'r', encoding='utf-8') as f:
config = json.load(f)
except Exception as e:
self.log_message(f"加载配置失败: {e}")
return
root_dir = config.get("root_dir", "")
if not root_dir or not os.path.isdir(root_dir):
self.log_message("配置中的根目录无效,无法加载")
return
self.path_var.set(root_dir)
self.recursive_var.set(config.get("recursive", True))
self.exclude_var.set(", ".join(config.get("exclude_patterns", [])))
self.header_content = config.get("header", "")
self.footer_content = config.get("footer", "")
self.pending_active_suffixes = config.get("active_suffixes", None)
self.pending_selected_files = config.get("selected_files", [])
self.start_scan()
def load_config(self):
filepath = filedialog.askopenfilename(
filetypes=[("配置文件", "*.makelove"), ("所有文件", "*.*")],
title="加载配置文件"
)
if not filepath:
return
self._load_config_file(filepath)
self.log_message(f"已手动加载配置: {filepath}")
def read_selected_files(self):
merged = []
count = 0
for rel in sorted(self.selected_files):
full = os.path.join(self.root_dir, rel)
if not os.path.isfile(full):
continue
with open(full, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
merged.append(f"// ====== 文件: {rel} ======\n")
merged.append(content)
if not content.endswith('\n'):
merged.append('\n')
merged.append('\n')
count += 1
return ''.join(merged), count
def build_final_text(self):
core, count = self.read_selected_files()
final = ""
if self.header_content:
final += self.header_content + "\n\n"
final += core
if self.footer_content:
final += "\n\n" + self.footer_content
return final, count
def save_to_file(self):
if not self.selected_files:
messagebox.showerror("错误", "没有选中任何文件")
return
self.save_btn.config(state='disabled')
self.copy_btn.config(state='disabled')
thread = threading.Thread(target=self._save_thread)
thread.daemon = True
thread.start()
def _save_thread(self):
text, count = self.build_final_text()
self.root.after(0, self._save_finish, text, count)
def _save_finish(self, text, count):
if count == 0:
self.log_message("没有成功读取任何文件,取消保存。")
else:
path = filedialog.asksaveasfilename(defaultextension=".txt",
filetypes=[("文本文件", "*.txt")],
title="保存合并文档")
if path:
with open(path, 'w', encoding='utf-8') as f:
f.write(text)
self.log_message(f"已保存 {count} 个文件到: {path}")
self.update_button_states()
def copy_to_clipboard(self):
if not self.selected_files:
messagebox.showerror("错误", "没有选中任何文件")
return
self.save_btn.config(state='disabled')
self.copy_btn.config(state='disabled')
thread = threading.Thread(target=self._copy_thread)
thread.daemon = True
thread.start()
def _copy_thread(self):
text, count = self.build_final_text()
self.root.after(0, self._copy_finish, text, count)
def _copy_finish(self, text, count):
if count == 0:
self.log_message("没有成功读取任何文件,剪贴板未更新。")
else:
self.root.clipboard_clear()
self.root.clipboard_append(text)
self.log_message(f"已将 {count} 个文件的最新内容复制到剪贴板。")
self.update_button_states()
if __name__ == "__main__":
root = tk.Tk()
app = CodeCollector(root)
root.mainloop()