mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
CLI: Add txt.JoinAnd() helper function to format lists of items #5233
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
@@ -22,6 +21,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -118,7 +118,7 @@ func StartImport(router *gin.RouterGroup) {
|
||||
// Add imported files to albums if allowed.
|
||||
if len(frm.Albums) > 0 &&
|
||||
acl.Rules.AllowAny(acl.ResourceAlbums, s.GetUserRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
log.Debugf("import: adding files to album %s", clean.Log(strings.Join(frm.Albums, " and ")))
|
||||
log.Debugf("import: adding files to album %s", clean.Log(txt.JoinAnd(frm.Albums)))
|
||||
opt.Albums = frm.Albums
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// UploadUserFiles adds files to the user's upload folder from where they can be processed and indexed.
|
||||
@@ -329,7 +330,7 @@ func ProcessUserUpload(router *gin.RouterGroup) {
|
||||
// Add imported files to albums if allowed.
|
||||
if len(frm.Albums) > 0 &&
|
||||
acl.Rules.AllowAny(acl.ResourceAlbums, s.GetUserRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
log.Debugf("upload: adding files to album %s", clean.Log(strings.Join(frm.Albums, " and ")))
|
||||
log.Debugf("upload: adding files to album %s", clean.Log(txt.JoinAnd(frm.Albums)))
|
||||
opt.Albums = frm.Albums
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/entity/search"
|
||||
"github.com/photoprism/photoprism/internal/workers"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// VisionResetCommand configures the command name, flags, and action.
|
||||
@@ -70,7 +71,7 @@ func visionResetAction(ctx *cli.Context) error {
|
||||
confirmed := RunNonInteractively(ctx.Bool("yes"))
|
||||
|
||||
if !confirmed && len(selectedModels) > 0 {
|
||||
label := fmt.Sprintf("Reset generated %s for matching pictures?", describeVisionModels(selectedModels))
|
||||
label := fmt.Sprintf("Reset generated %s for matching pictures?", txt.JoinAnd(selectedModels))
|
||||
prompt := promptui.Prompt{Label: label, IsConfirm: true}
|
||||
if _, err := prompt.Run(); err != nil {
|
||||
return nil
|
||||
@@ -87,30 +88,3 @@ func visionResetAction(ctx *cli.Context) error {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func describeVisionModels(models []string) string {
|
||||
descriptions := make([]string, 0, len(models))
|
||||
|
||||
for _, m := range models {
|
||||
switch m {
|
||||
case vision.ModelTypeCaption:
|
||||
descriptions = append(descriptions, "captions")
|
||||
case vision.ModelTypeLabels:
|
||||
descriptions = append(descriptions, "labels")
|
||||
default:
|
||||
descriptions = append(descriptions, m)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(descriptions) {
|
||||
case 0:
|
||||
return "metadata"
|
||||
case 1:
|
||||
return descriptions[0]
|
||||
case 2:
|
||||
return descriptions[0] + " and " + descriptions[1]
|
||||
default:
|
||||
head := strings.Join(descriptions[:len(descriptions)-1], ", ")
|
||||
return head + ", and " + descriptions[len(descriptions)-1]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,10 +63,8 @@ func (w *Vision) Start(filter string, count int, models []string, customSrc stri
|
||||
if n := len(models); n == 0 {
|
||||
log.Warnf("vision: no models were specified")
|
||||
return nil
|
||||
} else if n == 1 {
|
||||
log.Infof("vision: running %s model", models[0])
|
||||
} else {
|
||||
log.Infof("vision: running %s models", strings.Join(models, " and "))
|
||||
log.Infof("vision: running %s models", txt.JoinAnd(models))
|
||||
}
|
||||
|
||||
// Source type for AI generated data.
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"path"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
@@ -19,6 +18,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Reset removes data generated by the specified model types for photos matching the search filter.
|
||||
@@ -41,10 +41,8 @@ func (w *Vision) Reset(filter string, count int, models []string, provider strin
|
||||
if n := len(models); n == 0 {
|
||||
log.Warnf("vision: no models were specified")
|
||||
return nil
|
||||
} else if n == 1 {
|
||||
log.Infof("vision: resetting %s model data", models[0])
|
||||
} else {
|
||||
log.Infof("vision: resetting %s model data", strings.Join(models, " and "))
|
||||
log.Infof("vision: resetting %s model data", txt.JoinAnd(models))
|
||||
}
|
||||
|
||||
provider = clean.ShortTypeLower(provider)
|
||||
|
||||
36
pkg/txt/list.go
Normal file
36
pkg/txt/list.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package txt
|
||||
|
||||
// JoinAnd formats a slice of strings using commas and a localized "and" before the final element.
|
||||
// Examples:
|
||||
//
|
||||
// []string{} => ""
|
||||
// []string{"a"} => "a"
|
||||
// []string{"a","b"} => "a and b"
|
||||
// []string{"a","b","c"} => "a, b, and c"
|
||||
func JoinAnd(values []string) string {
|
||||
length := len(values)
|
||||
|
||||
switch length {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return values[0]
|
||||
case 2:
|
||||
return values[0] + " and " + values[1]
|
||||
}
|
||||
|
||||
// length >= 3
|
||||
result := ""
|
||||
for i := 0; i < length; i++ {
|
||||
switch {
|
||||
case i == 0:
|
||||
result = values[i]
|
||||
case i == length-1:
|
||||
result += ", and " + values[i]
|
||||
default:
|
||||
result += ", " + values[i]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
23
pkg/txt/list_test.go
Normal file
23
pkg/txt/list_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package txt
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoinAnd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string
|
||||
expect string
|
||||
}{
|
||||
{"empty", []string{}, ""},
|
||||
{"single", []string{"caption"}, "caption"},
|
||||
{"two", []string{"caption", "labels"}, "caption and labels"},
|
||||
{"three", []string{"captions", "labels", "faces"}, "captions, labels, and faces"},
|
||||
{"many", []string{"one", "two", "three", "four"}, "one, two, three, and four"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
if got := JoinAnd(tc.input); got != tc.expect {
|
||||
t.Fatalf("%s: expected %q, got %q", tc.name, tc.expect, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user