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"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize/english"
|
"github.com/dustin/go-humanize/english"
|
||||||
@@ -22,6 +21,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/i18n"
|
"github.com/photoprism/photoprism/pkg/i18n"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -118,7 +118,7 @@ func StartImport(router *gin.RouterGroup) {
|
|||||||
// Add imported files to albums if allowed.
|
// Add imported files to albums if allowed.
|
||||||
if len(frm.Albums) > 0 &&
|
if len(frm.Albums) > 0 &&
|
||||||
acl.Rules.AllowAny(acl.ResourceAlbums, s.GetUserRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
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
|
opt.Albums = frm.Albums
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/i18n"
|
"github.com/photoprism/photoprism/pkg/i18n"
|
||||||
"github.com/photoprism/photoprism/pkg/media"
|
"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.
|
// 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.
|
// Add imported files to albums if allowed.
|
||||||
if len(frm.Albums) > 0 &&
|
if len(frm.Albums) > 0 &&
|
||||||
acl.Rules.AllowAny(acl.ResourceAlbums, s.GetUserRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
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
|
opt.Albums = frm.Albums
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/entity"
|
"github.com/photoprism/photoprism/internal/entity"
|
||||||
"github.com/photoprism/photoprism/internal/entity/search"
|
"github.com/photoprism/photoprism/internal/entity/search"
|
||||||
"github.com/photoprism/photoprism/internal/workers"
|
"github.com/photoprism/photoprism/internal/workers"
|
||||||
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// VisionResetCommand configures the command name, flags, and action.
|
// VisionResetCommand configures the command name, flags, and action.
|
||||||
@@ -70,7 +71,7 @@ func visionResetAction(ctx *cli.Context) error {
|
|||||||
confirmed := RunNonInteractively(ctx.Bool("yes"))
|
confirmed := RunNonInteractively(ctx.Bool("yes"))
|
||||||
|
|
||||||
if !confirmed && len(selectedModels) > 0 {
|
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}
|
prompt := promptui.Prompt{Label: label, IsConfirm: true}
|
||||||
if _, err := prompt.Run(); err != nil {
|
if _, err := prompt.Run(); err != nil {
|
||||||
return 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 {
|
if n := len(models); n == 0 {
|
||||||
log.Warnf("vision: no models were specified")
|
log.Warnf("vision: no models were specified")
|
||||||
return nil
|
return nil
|
||||||
} else if n == 1 {
|
|
||||||
log.Infof("vision: running %s model", models[0])
|
|
||||||
} else {
|
} 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.
|
// Source type for AI generated data.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize/english"
|
"github.com/dustin/go-humanize/english"
|
||||||
@@ -19,6 +18,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/mutex"
|
"github.com/photoprism/photoprism/internal/mutex"
|
||||||
"github.com/photoprism/photoprism/internal/photoprism"
|
"github.com/photoprism/photoprism/internal/photoprism"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"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.
|
// 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 {
|
if n := len(models); n == 0 {
|
||||||
log.Warnf("vision: no models were specified")
|
log.Warnf("vision: no models were specified")
|
||||||
return nil
|
return nil
|
||||||
} else if n == 1 {
|
|
||||||
log.Infof("vision: resetting %s model data", models[0])
|
|
||||||
} else {
|
} 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)
|
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