103 lines
3.1 KiB
Go
103 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/job"
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/settings"
|
|
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
|
|
)
|
|
|
|
// saveNameFromTitle turns a print-job title into a file name for the Save As
|
|
// dialog: path and extension dropped, app prefixes ("Microsoft Word - ")
|
|
// removed, characters invalid in file names replaced; empty titles fall back
|
|
// to "Printed YYYY-MM-DD HHMMSS".
|
|
func saveNameFromTitle(title string, now time.Time) string {
|
|
t := strings.TrimSpace(title)
|
|
for _, p := range []string{"Microsoft Word - ", "Microsoft Excel - ", "Microsoft PowerPoint - "} {
|
|
t = strings.TrimPrefix(t, p)
|
|
}
|
|
t = path.Base(strings.ReplaceAll(t, `\`, "/")) // slash-only: "a:b" is not a drive here
|
|
if ext := filepath.Ext(t); ext != "" && len(ext) <= 6 && !strings.ContainsAny(ext, " ") {
|
|
t = strings.TrimSuffix(t, ext)
|
|
}
|
|
t = strings.Map(func(r rune) rune {
|
|
if r < 32 || strings.ContainsRune(`<>:"/\|?*`, r) {
|
|
return '-'
|
|
}
|
|
return r
|
|
}, t)
|
|
if rs := []rune(t); len(rs) > 120 {
|
|
t = string(rs[:120])
|
|
}
|
|
t = strings.TrimRight(strings.TrimSpace(t), ".")
|
|
if t == "" || t == "." || t == "-" {
|
|
t = "Printed " + now.Format("2006-01-02 150405")
|
|
}
|
|
return t + ".pdf"
|
|
}
|
|
|
|
// saveStartDir: last folder used in the dialog, else the configured output
|
|
// folder, else Documents/Verae Timestamps (created so the dialog opens there).
|
|
func saveStartDir(outDir string) string {
|
|
for _, d := range []string{settings.LastSaveDir(), outDir} {
|
|
if d == "" {
|
|
continue
|
|
}
|
|
if st, err := os.Stat(d); err == nil && st.IsDir() {
|
|
return d
|
|
}
|
|
}
|
|
d, err := job.DefaultOutDir()
|
|
if err != nil {
|
|
h, _ := os.UserHomeDir()
|
|
return h
|
|
}
|
|
_ = os.MkdirAll(d, 0o755)
|
|
return d
|
|
}
|
|
|
|
// askSaveTarget shows the native Save As dialog for a print job. It returns
|
|
// (path, true) when the user picked a file, ("", false) when they cancelled,
|
|
// and ("", true) when no dialog could be shown (the job is then stamped into
|
|
// the default folder as before, so a print is never lost to a dialog error).
|
|
func askSaveTarget(lj vlog.Job, title, outDir string) (string, bool) {
|
|
name := saveNameFromTitle(title, time.Now())
|
|
dir := saveStartDir(outDir)
|
|
lj.Printf("saveas: prompting (default name %q, folder %q)", name, dir)
|
|
p, ok, err := nativeSaveDialog(name, dir)
|
|
if err != nil {
|
|
lj.Errorf("saveas: dialog unavailable (%v); saving to default folder", err)
|
|
return "", true
|
|
}
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
p = ensurePDFExt(p)
|
|
_ = settings.SetLastSaveDir(filepath.Dir(p))
|
|
lj.Printf("saveas: user chose %q", p)
|
|
return p, true
|
|
}
|
|
|
|
// ensurePDFExt adds ".pdf" when the user typed a name without it. The dialog
|
|
// only confirmed replacing the name as typed, so if "<name>.pdf" exists we
|
|
// pick "<name>-2.pdf" etc. instead of overwriting it.
|
|
func ensurePDFExt(p string) string {
|
|
if strings.EqualFold(filepath.Ext(p), ".pdf") {
|
|
return p
|
|
}
|
|
base := p
|
|
p = base + ".pdf"
|
|
for i := 2; i < 1000; i++ {
|
|
if _, err := os.Stat(p); os.IsNotExist(err) {
|
|
break
|
|
}
|
|
p = fmt.Sprintf("%s-%d.pdf", base, i)
|
|
}
|
|
return p
|
|
}
|