ceecbb6af4
General - 配置界面支持配置对象存储桶 - 快捷键进入截图 - 一键上传截图 & 复制截图链接 MacOS - 状态栏指示器和应用图标
148 lines
4.8 KiB
Go
148 lines
4.8 KiB
Go
// Package application contains use-case orchestrators that the presentation
|
|
// layer (Wails bindings, frontend) calls into.
|
|
//
|
|
// Design rationale:
|
|
// - Application code only depends on domain interfaces; concrete adapters
|
|
// are injected from main.go. This keeps the layer easy to unit test and
|
|
// future-proofs us against swapping providers.
|
|
package application
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/mmmy/snapgo/internal/domain"
|
|
"github.com/mmmy/snapgo/internal/infrastructure/clipboard"
|
|
"github.com/mmmy/snapgo/internal/infrastructure/screencapture"
|
|
)
|
|
|
|
// Notifier sends user-facing UI notifications. We define it as an interface
|
|
// so the application package does not depend on the Wails runtime.
|
|
type Notifier interface {
|
|
NotifySuccess(url string)
|
|
NotifyFailure(reason string)
|
|
}
|
|
|
|
// CaptureAndUploadService wires capture → upload → clipboard → notify.
|
|
type CaptureAndUploadService struct {
|
|
Capturer screencapture.Capturer
|
|
Provider domain.OSSProvider
|
|
Clipboard clipboard.Writer
|
|
Notifier Notifier
|
|
// FallbackDir is the directory where PNGs are saved when upload fails.
|
|
FallbackDir string
|
|
// PathPrefix is prepended to every generated object key.
|
|
PathPrefix string
|
|
}
|
|
|
|
// Execute runs the full pipeline for a single screenshot region.
|
|
//
|
|
// On any failure after capture, the PNG is written to FallbackDir and the
|
|
// local file path is copied to the clipboard so the user can still recover
|
|
// the image manually.
|
|
func (s *CaptureAndUploadService) Execute(ctx context.Context, rect image.Rectangle) (*domain.UploadResult, error) {
|
|
if s.Provider == nil {
|
|
s.Notifier.NotifyFailure("OSS not configured")
|
|
return nil, fmt.Errorf("oss provider is nil")
|
|
}
|
|
|
|
pngBytes, err := s.Capturer.CaptureRegion(rect)
|
|
if err != nil {
|
|
s.Notifier.NotifyFailure("capture failed: " + err.Error())
|
|
return nil, err
|
|
}
|
|
return s.uploadAndCopy(ctx, pngBytes)
|
|
}
|
|
|
|
// ExecuteWithBytes runs only the upload-clipboard-notify portion of the
|
|
// pipeline using PNG bytes that have already been produced by an upstream
|
|
// capture step (e.g. macOS's interactive picker). Splitting this out keeps
|
|
// the service free of any knowledge about how the pixels were obtained.
|
|
func (s *CaptureAndUploadService) ExecuteWithBytes(ctx context.Context, pngBytes []byte) error {
|
|
if s.Provider == nil {
|
|
s.Notifier.NotifyFailure("OSS not configured")
|
|
return fmt.Errorf("oss provider is nil")
|
|
}
|
|
if len(pngBytes) == 0 {
|
|
s.Notifier.NotifyFailure("empty screenshot")
|
|
return fmt.Errorf("empty screenshot")
|
|
}
|
|
_, err := s.uploadAndCopy(ctx, pngBytes)
|
|
return err
|
|
}
|
|
|
|
// uploadAndCopy is the shared tail of Execute / ExecuteWithBytes. Extracting
|
|
// this avoids duplicating the failure-handling and clipboard logic.
|
|
func (s *CaptureAndUploadService) uploadAndCopy(ctx context.Context, pngBytes []byte) (*domain.UploadResult, error) {
|
|
key := buildObjectKey(s.PathPrefix)
|
|
start := time.Now()
|
|
url, uploadErr := s.Provider.Upload(ctx, key, pngBytes, "image/png")
|
|
if uploadErr != nil {
|
|
path := s.fallback(pngBytes)
|
|
_ = s.Clipboard.WriteText(path)
|
|
s.Notifier.NotifyFailure("upload failed: " + uploadErr.Error())
|
|
return nil, uploadErr
|
|
}
|
|
|
|
if err := s.Clipboard.WriteText(url); err != nil {
|
|
s.Notifier.NotifyFailure("clipboard write failed: " + err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
s.Notifier.NotifySuccess(url)
|
|
|
|
return &domain.UploadResult{
|
|
URL: url,
|
|
Key: key,
|
|
Provider: s.Provider.Name(),
|
|
Elapsed: time.Since(start),
|
|
}, nil
|
|
}
|
|
|
|
// fallback persists the PNG locally and returns the absolute path.
|
|
// Errors are swallowed because the caller has already failed once and we
|
|
// must not mask the original failure with a secondary error.
|
|
func (s *CaptureAndUploadService) fallback(data []byte) string {
|
|
if s.FallbackDir == "" {
|
|
s.FallbackDir = filepath.Join(os.TempDir(), "SnapGo")
|
|
}
|
|
_ = os.MkdirAll(s.FallbackDir, 0o755)
|
|
name := fmt.Sprintf("%s.png", time.Now().Format("20060102-150405"))
|
|
path := filepath.Join(s.FallbackDir, name)
|
|
_ = os.WriteFile(path, data, 0o644)
|
|
return path
|
|
}
|
|
|
|
// buildObjectKey produces a date-grouped object key with a 6-char random
|
|
// suffix to avoid collisions when many screenshots happen in the same second.
|
|
//
|
|
// The template is intentionally hard-coded for the MVP; making it user-
|
|
// configurable is left to a follow-up spec.
|
|
func buildObjectKey(prefix string) string {
|
|
now := time.Now()
|
|
return fmt.Sprintf("%s%s/%s/%s-%s.png",
|
|
prefix,
|
|
now.Format("2006"),
|
|
now.Format("01"),
|
|
now.Format("20060102-150405"),
|
|
randomSuffix(6),
|
|
)
|
|
}
|
|
|
|
const suffixAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
|
|
// randomSuffix produces a short non-cryptographic identifier sufficient for
|
|
// avoiding key collisions among consecutive captures.
|
|
func randomSuffix(n int) string {
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = suffixAlphabet[rand.Intn(len(suffixAlphabet))]
|
|
}
|
|
return string(b)
|
|
}
|