Adult woman typing on a laptop at a desk with a smartphone and documents.

[Teilen] PDF Zusammenführen & Verschlüsseln Tool V1.0 (Offline-Version)

Vollständig offline nutzbares PDF-Zusammenführungs- und Verschlüsselungstool für Windows. Mehrere PDFs zusammenführen, mit AES-256 verschlüsseln und Ihre Dokumente sicher lokal verwalten – ohne Hochladen auf Online-Dienste.

Autor: Pecezen
Plattform: Windows
Programmiersprache: Go

✅ Tool-Übersicht

Dies ist ein vollständig offline nutzbares PDF-Zusammenführungs- und Verschlüsselungstool.

  • Keine Registrierung erforderlich
  • Kein Hochladen von Dateien
  • Keine technischen Kenntnisse nötig

Mit nur wenigen Schritten können mehrere PDFs zu einer einzigen Datei zusammengeführt und mit AES-256 Verschlüsselung geschützt werden.
So bleiben Ihre Dokumente privat und sicher auf Ihrem Computer.

⭐ Warum brauchen Sie dieses Tool?

Haben Sie schon einmal folgende Situationen erlebt?

📎 Zu viele PDF-Dateien, das Versenden per E-Mail oder Hochladen ist umständlich
🔐 Verträge, Ausweise oder sensible Dokumente möchte man nicht online speichern
🧾 Nach dem Scannen ist ein ganzer Ordner voller PDFs vorhanden
😵 Online-Tools haben Dateigrößenbeschränkungen, erfordern Logins oder sind langsam

Dieses Tool wurde speziell entwickelt, um solche Probleme zu lösen.

🔧 Hauptfunktionen

📂 PDF Zusammenführen
  • Mehrere PDF-Dateien hinzufügen
  • Ganze Ordner auf einmal hinzufügen
  • Reihenfolge der Zusammenführung frei anpassen
  • Optional Trennseiten zwischen Dokumenten einfügen
🔐 PDF-Verschlüsselung (AES-256)
  • Ein-Klick-Verschlüsselung nach dem Zusammenführen
  • AES-256 Verschlüsselungsstärke
  • Echtzeit-Passwortstärkeanzeige:
    • ❌ Schwach
    • ⚠ Mittel
    • ✅ Sicher

📊 Transparenter Verarbeitungsprozess
  • Fortschritt wird in Echtzeit angezeigt, keine Sorge über Blockierungen
  • Nach Abschluss werden angezeigt:
    • Speicherort der Datei
    • Größe der Ausgabedatei

🛡 Warum „offline“ so wichtig ist

🔒 Ihre Dateien werden nicht auf einen Server hochgeladen
🔒 Keine Drittanbieter-Plattform involviert
🔒 Keine Protokolle, keine Backups, keine Analyse

👉 Ihre Dateien verbleiben nur auf Ihrem eigenen Computer

Dies ist besonders wichtig für:

  • Verträge
  • Ausweise
  • Interne Dokumente

🖥 Verwendung (wirklich einfach)

1️⃣ Klicken Sie auf „PDF hinzufügen“ oder „Ordner hinzufügen“
2️⃣ Reihenfolge nach Bedarf anpassen
3️⃣ Speicherort und Dateiname auswählen
4️⃣ (Optional) Verschlüsselungspasswort festlegen
5️⃣ Auf „Zusammenführen & Verarbeiten starten“ klicken → Fertig

🖥️ Benutzeroberfläche

💾 Windows-Programm herunterladen

Download-Speicherort

Die heruntergeladene Datei ist ein komprimiertes Archiv.
Entpack-Passwort: pecezen.org

🔗Quellcode(Source Code)
package main

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"unicode"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/layout"
	"fyne.io/fyne/v2/storage"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"

	"github.com/pdfcpu/pdfcpu/pkg/api"
	"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
)

// 偏好設定 Key
const prefLangKey = "user_language"

// I18n 翻譯資料結構
type i18n struct {
	windowTitle     string
	addBtn          string
	addFolderBtn    string
	removeBtn       string
	moveUp          string
	moveDown        string
	outPlaceholder  string
	chooseOut       string
	dividerCheck    string
	encryptCheck    string
	mergeBtn        string
	passwordPlace   string
	sectionMerge    string
	sectionEnc      string
	langLabel       string
	aboutBtn        string
	aboutTitle      string
	aboutContent    string
	msgSuccess      string
	msgErrMinFile   string
	msgErrNoOut     string
	msgErrNoPW      string
	outputSizeLabel string
	strengthWeak    string
	strengthMedium  string
	strengthStrong  string
	processing      string
	// 新增:加密/保護檔案提示
	msgEncryptedListHeader string
}

var translations = map[string]i18n{
	"繁體中文": {
		windowTitle:     "PDF 合併與加密工具 V1.0",
		addBtn:          "加入 PDF",
		addFolderBtn:    "加入資料夾",
		removeBtn:       "移除",
		moveUp:          "上移",
		moveDown:        "下移",
		outPlaceholder:  "輸出檔名 (如: merged.pdf)",
		chooseOut:       "儲存位置",
		dividerCheck:    "在文件間插入分隔頁",
		encryptCheck:    "合併後啟用 AES-256 加密",
		mergeBtn:        "開始合併與處理",
		passwordPlace:   "請輸入加密密碼",
		sectionMerge:    "1. 輸出與合併選項",
		sectionEnc:      "2. 加密設定",
		langLabel:       "語言:",
		aboutBtn:        "關於",
		aboutTitle:      "關於此程式",
		aboutContent:    "作者: pecezen.org\nCopyright © 2026 pecezen.org. All rights reserved.\n\n【第三方內容聲明】\n本程式核心處理引擎採用 pdfcpu 開源專案。\npdfcpu 依據 Apache License 2.0 授權使用。\nCopyright © 2026 pdfcpu authors.\n\n【免責聲明】\n作者不對因使用本程式所造成的任何資料遺失、損壞或其他後果負責。",
		msgSuccess:      "操作成功!",
		msgErrMinFile:   "請至少加入 2 個 PDF 檔案。",
		msgErrNoOut:     "請先指定輸出位置。",
		msgErrNoPW:      "啟用加密時,必須輸入密碼。",
		outputSizeLabel: "輸出檔案大小: ",
		strengthWeak:    "❌ 太弱",
		strengthMedium:  "⚠ 普通",
		strengthStrong:  "✅ 安全",
		processing:      "正在處理中,請稍候...",
		// 新增
		msgEncryptedListHeader: "以下檔案無法合併(可能已加密或受保護):",
	},
	"English": {
		windowTitle:     "PDF Merge & Encrypt Tool V1.0",
		addBtn:          "Add PDF",
		addFolderBtn:    "Add Folder",
		removeBtn:       "Remove",
		moveUp:          "Up",
		moveDown:        "Down",
		outPlaceholder:  "Output name (e.g. merged.pdf)",
		chooseOut:       "Save As...",
		dividerCheck:    "Insert divider pages",
		encryptCheck:    "Enable AES-256 Encryption",
		mergeBtn:        "Run Process",
		passwordPlace:   "Enter Password",
		sectionMerge:    "1. Merge Settings",
		sectionEnc:      "2. Security Settings",
		langLabel:       "Language:",
		aboutBtn:        "About",
		aboutTitle:      "About",
		aboutContent:    "Author: pecezen.org\nCopyright © 2026 pecezen.org. All rights reserved.\n\n[Third-party Attribution]\nThis tool uses the pdfcpu library.\npdfcpu is licensed under the Apache License 2.0.\nCopyright © 2026 pdfcpu authors.\n\n[Disclaimer]\nThe author is not responsible for any data loss, damage, or other consequences caused by the use of this program.",
		msgSuccess:      "Success!",
		msgErrMinFile:   "Add at least 2 PDF files.",
		msgErrNoOut:     "Please specify output location.",
		msgErrNoPW:      "Password required for encryption.",
		outputSizeLabel: "Output Size: ",
		strengthWeak:    "❌ Too Weak",
		strengthMedium:  "⚠ Medium",
		strengthStrong:  "✅ Strong",
		processing:      "Processing, please wait...",
		// 新增
		msgEncryptedListHeader: "The following file(s) cannot be merged (encrypted/protected):",
	},
	"日文": {
		windowTitle:     "PDF結合・暗号化ツール V1.0",
		addBtn:          "PDFを追加",
		addFolderBtn:    "フォルダを追加",
		removeBtn:       "削除",
		moveUp:          "上へ",
		moveDown:        "下へ",
		outPlaceholder:  "出力ファイル名",
		chooseOut:       "保存先",
		dividerCheck:    "仕切りページを掃入",
		encryptCheck:    "AES-256暗号化を有効にする",
		mergeBtn:        "結合と実行",
		passwordPlace:   "パスワード",
		sectionMerge:    "1. 結合設定",
		sectionEnc:      "2. セキュリティ設定",
		langLabel:       "言語:",
		aboutBtn:        "情報",
		aboutTitle:      "このアプリについて",
		aboutContent:    "著者: pecezen.org\nCopyright © 2026 pecezen.org. All rights reserved.\n\n【サードパーティ製ライブラリの声明】\n本プログラムのコア處理引擎は pdfcpu を採用しています。\npdfcpu は Apache License 2.0 に基づいて使用されています。\nCopyright © 2026 pdfcpu authors.\n\n【免責事項】\n作者は、本プログラムの使用によって生じたデータの紛失、破損、その他の結果について一切の責任を負いません。",
		msgSuccess:      "成功しました!",
		msgErrMinFile:   "2つ以上のPDFファイルを追加してください。",
		msgErrNoOut:     "保存先を指定してください。",
		msgErrNoPW:      "パスワードが必要です。",
		outputSizeLabel: "出力サイズ: ",
		strengthWeak:    "❌ 弱い",
		strengthMedium:  "⚠ 普通",
		strengthStrong:  "✅ 強固",
		processing:      "処理中、しばらくお待ちください...",
		// 新增
		msgEncryptedListHeader: "次のファイルは結合できません(暗号化/保護されています):",
	},
	"Deutsch": {
		windowTitle:     "PDF Zusammenführen & Verschlüsseln V1.0",
		addBtn:          "PDF hinzufügen",
		addFolderBtn:    "Ordner hinzufügen",
		removeBtn:       "Entfernen",
		moveUp:          "Nach oben",
		moveDown:        "Nach unten",
		outPlaceholder:  "Dateiname für Ausgabe",
		chooseOut:       "Speichern unter...",
		dividerCheck:    "Trennblätter einfügen",
		encryptCheck:    "AES-256-Verschlüsselung aktivieren",
		mergeBtn:        "Prozess starten",
		passwordPlace:   "Passwort eingeben",
		sectionMerge:    "1. Zusammenführen-Einstellungen",
		sectionEnc:      "2. Sicherheitseinstellungen",
		langLabel:       "Sprache:",
		aboutBtn:        "Über",
		aboutTitle:      "Über diese App",
		aboutContent:    "Autor: pecezen.org\nCopyright © 2026 pecezen.org. All rights reserved.\n\n[Dritthersteller-Attributierung]\nDieses Tool verwendet die pdfcpu-Bibliothek.\npdfcpu ist unter der Apache License 2.0 lizenziert.\nCopyright © 2026 pdfcpu authors.\n\n[Haftungsausschluss]\nDer Autor haftet nicht für Datenverlust, Schäden oder andere Folgen, die durch die Nutzung dieses Programms entstehen.",
		msgSuccess:      "Erfolg!",
		msgErrMinFile:   "Mindestens 2 PDF-Dateien hinzufügen.",
		msgErrNoOut:     "Bitte Ausgabeort angeben.",
		msgErrNoPW:      "Passwort für Verschlüsselung erforderlich.",
		outputSizeLabel: "Ausgabegröße: ",
		strengthWeak:    "❌ Zu schwach",
		strengthMedium:  "⚠ Mittel",
		strengthStrong:  "✅ Stark",
		processing:      "Verarbeitung, bitte warten...",
		// 新增
		msgEncryptedListHeader: "Folgende Datei(en) können nicht zusammengeführt werden (verschlüsselt/geschützt):",
	},
}

func formatFileSize(size int64) string {
	if size < 1024 {
		return fmt.Sprintf("%d B", size)
	} else if size < 1024*1024 {
		return fmt.Sprintf("%.2f KB", float64(size)/1024)
	}
	return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
}

func checkPasswordStrength(pw string, t i18n) string {
	if len(pw) == 0 {
		return ""
	}
	if len(pw) < 6 {
		return t.strengthWeak
	}
	var hasNumber, hasUpper, hasSpecial bool
	for _, char := range pw {
		switch {
		case unicode.IsNumber(char):
			hasNumber = true
		case unicode.IsUpper(char):
			hasUpper = true
		case unicode.IsPunct(char) || unicode.IsSymbol(char):
			hasSpecial = true
		}
	}
	if len(pw) >= 10 && hasNumber && hasUpper && hasSpecial {
		return t.strengthStrong
	}
	return t.strengthMedium
}

func cleanPath(p string) string {
	res := p
	if runtime.GOOS == "windows" {
		if len(res) > 3 && res[0] == '/' && res[2] == ':' {
			res = res[1:]
		}
	}
	res = filepath.FromSlash(res)
	if absPath, err := filepath.Abs(res); err == nil {
		return absPath
	}
	return res
}

// 安全地刪除零位元組檔案(避免誤刪已有內容的檔案)
func removeIfEmpty(p string) {
	if p == "" {
		return
	}
	if fi, err := os.Stat(p); err == nil && fi.Size() == 0 {
		_ = os.Remove(p)
	}
}

func moveFile(src, dst string) error {
	if _, err := os.Stat(dst); err == nil {
		_ = os.Remove(dst)
	}
	if err := os.Rename(src, dst); err == nil {
		return nil
	}
	// 跨磁碟或不同裝置時,改用 copy + 刪除
	sf, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sf.Close()
	df, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer df.Close()
	if _, err = io.Copy(df, sf); err != nil {
		return err
	}
	_ = sf.Close()
	return os.Remove(src)
}

// 只複製(不刪來源)
func copyFile(src, dst string) error {
	if _, err := os.Stat(dst); err == nil {
		_ = os.Remove(dst)
	}
	sf, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sf.Close()
	df, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer df.Close()
	_, err = io.Copy(df, sf)
	return err
}

// 嘗試判斷檔案是否為加密/受保護(或不可讀),以避免合併時直接噴錯。
// 這裡以 Validate 嘗試,若錯誤訊息包含 encrypt/password/protect 等關鍵字,就視為無法合併。
func isEncryptedOrProtected(path string) bool {
	// 傳入 nil 讓 pdfcpu 使用預設設定(無密碼)
	err := api.ValidateFile(path, nil)
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	if strings.Contains(msg, "encrypt") ||
		strings.Contains(msg, "password") ||
		strings.Contains(msg, "protected") ||
		strings.Contains(msg, "permission") {
		return true
	}
	// 其他 validation 錯誤也視為無法處理,避免後續直接噴錯
	return true
}

func runPdfProcess(files []string, finalOut string, divider, encrypt bool, pw string) error {
	// 單檔案例:若需加密,直接加密單檔;若不加密,外層邏輯不會允許執行(維持原規則)
	if len(files) == 1 {
		single := files[0]
		if encrypt {
			pw = strings.TrimSpace(pw)
			if pw == "" {
				return fmt.Errorf("Password required")
			}
			tempEnc, err := os.CreateTemp("", "pdf_enc_*.pdf")
			if err != nil {
				return err
			}
			encPath := tempEnc.Name()
			tempEnc.Close()
			defer os.Remove(encPath)

			encConf := model.NewAESConfiguration(pw, pw, 256)
			if err := api.EncryptFile(single, encPath, encConf); err != nil {
				return fmt.Errorf("Encrypt Error: %v", err)
			}
			return moveFile(encPath, finalOut)
		}
		// 非加密且單檔:理論上外層不會呼叫到這裡;若呼叫,保守作法採複製
		return copyFile(single, finalOut)
	}

	// 多檔案例:先合併,再視需要加密
	tempMerge, err := os.CreateTemp("", "pdf_merge_*.pdf")
	if err != nil {
		return err
	}
	mergePath := tempMerge.Name()
	tempMerge.Close()
	defer os.Remove(mergePath)

	if err := api.MergeCreateFile(files, mergePath, divider, nil); err != nil {
		return fmt.Errorf("Merge Error: %v", err)
	}

	sourceToMove := mergePath

	if encrypt {
		pw = strings.TrimSpace(pw)
		if pw == "" {
			return fmt.Errorf("Password required")
		}
		tempEnc, err := os.CreateTemp("", "pdf_enc_*.pdf")
		if err != nil {
			return err
		}
		encPath := tempEnc.Name()
		tempEnc.Close()
		defer os.Remove(encPath)

		encConf := model.NewAESConfiguration(pw, pw, 256)
		if err := api.EncryptFile(mergePath, encPath, encConf); err != nil {
			return fmt.Errorf("Encrypt Error: %v", err)
		}
		sourceToMove = encPath
	}

	return moveFile(sourceToMove, finalOut)
}

func main() {
	a := app.NewWithID("org.pecezen.pdftool.v1")
	w := a.NewWindow("PDF Tool V1.0")
	w.Resize(fyne.NewSize(850, 650))

	// 設定視窗圖示
	if iconRes, err := fyne.LoadResourceFromPath("icon.png"); err == nil {
		w.SetIcon(iconRes)
	}

	files := make([]string, 0, 16)
	selectedIndex := -1
	savedLang := a.Preferences().StringWithFallback(prefLangKey, "繁體中文")
	currentLang := savedLang

	outEntry := widget.NewEntry()
	passwordEntry := widget.NewEntry()
	passwordEntry.Password = true
	passwordEntry.Disable()

	strengthLabel := widget.NewLabel("")
	sizeResultLabel := widget.NewLabel("")
	progressIndicator := widget.NewProgressBarInfinite()
	progressIndicator.Hide()

	dividerCheck := widget.NewCheck("", nil)
	encryptCheck := widget.NewCheck("", func(checked bool) {
		if checked {
			passwordEntry.Enable()
		} else {
			passwordEntry.Disable()
			strengthLabel.SetText("")
		}
	})

	passwordEntry.OnChanged = func(s string) {
		if encryptCheck.Checked {
			strengthLabel.SetText(checkPasswordStrength(s, translations[currentLang]))
		}
	}

	list := widget.NewList(
		func() int { return len(files) },
		func() fyne.CanvasObject { return widget.NewLabel("template") },
		func(i widget.ListItemID, o fyne.CanvasObject) {
			o.(*widget.Label).SetText(fmt.Sprintf("%d. %s", i+1, filepath.Base(files[i])))
		},
	)

	addBtn := widget.NewButtonWithIcon("", theme.ContentAddIcon(), nil)
	addFolderBtn := widget.NewButtonWithIcon("", theme.FolderOpenIcon(), nil)
	removeBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), nil)
	upBtn := widget.NewButtonWithIcon("", theme.MoveUpIcon(), nil)
	downBtn := widget.NewButtonWithIcon("", theme.MoveDownIcon(), nil)
	chooseOutBtn := widget.NewButtonWithIcon("", theme.DocumentSaveIcon(), nil)
	mergeBtn := widget.NewButtonWithIcon("", theme.ConfirmIcon(), nil)
	aboutBtn := widget.NewButtonWithIcon("", theme.InfoIcon(), nil)

	mergeLabel := widget.NewLabelWithStyle("", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
	encryptLabel := widget.NewLabelWithStyle("", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
	langLabelPrefix := widget.NewLabel("")

	refreshUI := func(langKey string) {
		currentLang = langKey
		t := translations[langKey]
		w.SetTitle(t.windowTitle)
		addBtn.SetText(t.addBtn)
		addFolderBtn.SetText(t.addFolderBtn)
		removeBtn.SetText(t.removeBtn)
		upBtn.SetText(t.moveUp)
		downBtn.SetText(t.moveDown)
		outEntry.SetPlaceHolder(t.outPlaceholder)
		chooseOutBtn.SetText(t.chooseOut)
		dividerCheck.Text = t.dividerCheck
		dividerCheck.Refresh()
		encryptCheck.Text = t.encryptCheck
		encryptCheck.Refresh()
		mergeBtn.SetText(t.mergeBtn)
		aboutBtn.SetText(t.aboutBtn)
		passwordEntry.SetPlaceHolder(t.passwordPlace)
		mergeLabel.SetText(t.sectionMerge)
		encryptLabel.SetText(t.sectionEnc)
		langLabelPrefix.SetText(t.langLabel)
	}

	mergeBtn.OnTapped = func() {
		t := translations[currentLang]

		// 放行條件:
		// - 多檔:需 >= 2
		// - 單檔:若勾選加密,允許;若未勾選加密,仍需 >= 2
		if !((len(files) >= 2) || (encryptCheck.Checked && len(files) == 1)) {
			dialog.ShowInformation("Info", t.msgErrMinFile, w)
			return
		}

		finalOut := strings.TrimSpace(outEntry.Text)
		if finalOut == "" || finalOut == ".pdf" {
			dialog.ShowInformation("Info", t.msgErrNoOut, w)
			return
		}
		if encryptCheck.Checked && strings.TrimSpace(passwordEntry.Text) == "" {
			dialog.ShowInformation("Info", t.msgErrNoPW, w)
			return
		}

		// === 新增:事前掃描無法合併的檔案(加密/保護/不可讀) ===
		filesToCheck := files
		badFiles := make([]string, 0, len(filesToCheck))
		for _, f := range filesToCheck {
			if isEncryptedOrProtected(f) {
				badFiles = append(badFiles, filepath.Base(f))
			}
		}
		if len(badFiles) > 0 {
			// 顯示清單並中止,不進入處理
			msg := t.msgEncryptedListHeader + "\n"
			for _, name := range badFiles {
				msg += "- " + name + "\n"
			}
			dialog.ShowInformation("Info", msg, w)
			return
		}
		// === 事前掃描結束 ===

		mergeBtn.Disable()
		progressIndicator.Show()
		sizeResultLabel.SetText(t.processing)

		go func() {
			err := runPdfProcess(files, finalOut, dividerCheck.Checked, encryptCheck.Checked, passwordEntry.Text)
			var sizeStr string
			if err == nil {
				if fInfo, statErr := os.Stat(finalOut); statErr == nil {
					sizeStr = formatFileSize(fInfo.Size())
				}
			}

			fyne.Do(func() {
				if err != nil {
					dialog.ShowError(err, w)
				} else {
					sizeResultLabel.SetText(t.outputSizeLabel + sizeStr)
					dialog.ShowInformation("OK", t.msgSuccess+"\n"+finalOut, w)
				}
				mergeBtn.Enable()
				progressIndicator.Hide()
			})
		}()
	}

	addBtn.OnTapped = func() {
		fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
			if err != nil || r == nil {
				return
			}
			path := cleanPath(r.URI().Path())
			if strings.EqualFold(filepath.Ext(path), ".pdf") {
				files = append(files, path)
				list.Refresh()
			}
		}, w)
		fd.SetFilter(storage.NewExtensionFileFilter([]string{".pdf"}))
		fd.Show()
	}

	addFolderBtn.OnTapped = func() {
		dialog.ShowFolderOpen(func(lu fyne.ListableURI, err error) {
			if err != nil || lu == nil {
				return
			}
			entries, _ := lu.List()
			for _, entry := range entries {
				path := cleanPath(entry.Path())
				if strings.EqualFold(filepath.Ext(path), ".pdf") {
					files = append(files, path)
				}
			}
			list.Refresh()
		}, w)
	}

	removeBtn.OnTapped = func() {
		if selectedIndex < 0 || selectedIndex >= len(files) {
			return
		}
		files = append(files[:selectedIndex], files[selectedIndex+1:]...)
		selectedIndex = -1
		list.Refresh()
	}

	upBtn.OnTapped = func() {
		if selectedIndex <= 0 {
			return
		}
		files[selectedIndex-1], files[selectedIndex] = files[selectedIndex], files[selectedIndex-1]
		selectedIndex--
		list.Refresh()
		list.Select(selectedIndex)
	}

	downBtn.OnTapped = func() {
		if selectedIndex < 0 || selectedIndex >= len(files)-1 {
			return
		}
		files[selectedIndex+1], files[selectedIndex] = files[selectedIndex], files[selectedIndex+1]
		selectedIndex++
		list.Refresh()
		list.Select(selectedIndex)
	}

	chooseOutBtn.OnTapped = func() {
		fs := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
			if err != nil || wc == nil {
				return
			}
			// 使用者實際在 SaveDialog 選到的原始路徑(可能未含 .pdf)
			raw := cleanPath(wc.URI().Path())
			out := raw
			if !strings.HasSuffix(strings.ToLower(out), ".pdf") {
				out += ".pdf"
			}
			// 關閉 Writer(必要),但此步可能會在 raw 位置建立零位元組檔案
			_ = wc.Close()

			// UI 顯示最終輸出路徑(含 .pdf)
			outEntry.SetText(out)

			// 修正:刪除 SaveDialog 可能建立的空白占位檔(0 bytes)
			removeIfEmpty(raw)
			if out != raw {
				removeIfEmpty(out)
			}
		}, w)
		fs.SetFileName("merged.pdf")
		fs.Show()
	}

	aboutBtn.OnTapped = func() {
		t := translations[currentLang]
		dialog.ShowInformation(t.aboutTitle, t.aboutContent, w)
	}

	langOptions := []string{"繁體中文", "English", "日文", "Deutsch"}
	langSelect := widget.NewSelect(langOptions, func(s string) {
		a.Preferences().SetString(prefLangKey, s)
		refreshUI(s)
	})
	langSelect.Selected = savedLang
	langContainer := container.NewHBox(langLabelPrefix, langSelect)

	toolbar := container.NewHBox(addBtn, addFolderBtn, removeBtn, upBtn, downBtn, layout.NewSpacer(), langContainer, aboutBtn)

	settings := container.NewVBox(
		widget.NewSeparator(),
		mergeLabel,
		container.NewBorder(nil, nil, nil, chooseOutBtn, outEntry),
		dividerCheck,
		widget.NewSeparator(),
		encryptLabel,
		encryptCheck,
		container.NewVBox(passwordEntry, strengthLabel),
		widget.NewSeparator(),
		progressIndicator,
		sizeResultLabel,
		mergeBtn,
	)

	list.OnSelected = func(id widget.ListItemID) { selectedIndex = int(id) }

	refreshUI(savedLang)
	w.SetContent(container.NewBorder(
		toolbar, nil, nil, nil,
		container.NewHSplit(list, container.NewPadded(settings)),
	))

	w.ShowAndRun()
}
⚠️ Haftungsausschluss(Disclaimer)

Der Autor übernimmt keine Verantwortung für Datenverlust, Beschädigungen oder andere Folgen, die durch die Nutzung dieser Software entstehen.


Zum Schutz Ihrer Privatsphäre und Sicherheit folgen die Werkzeuge von PeceZen einem klaren Prinzip: keine Nachverfolgung, keine Werbung und vollständig offline.
Wenn Ihnen diese Werkzeuge wertvolle Zeit gespart haben, lade ich Sie herzlich ein, mir eine Tasse Kaffee auszugeben und damit den Betrieb der Server sowie die Weiterentwicklung der Tools zu unterstützen.
[ ☕ Spendiere mir eine Tasse Kaffee ]

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert