mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-27 07:31:43 +01:00
Compare commits
12 Commits
snyk-fix-5
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c92488e90a | ||
|
|
47f87cbe30 | ||
|
|
cc2acbdaac | ||
|
|
28c3e102a2 | ||
|
|
116e39531b | ||
|
|
8a90b9c133 | ||
|
|
ef52009f57 | ||
|
|
76154263e0 | ||
|
|
108194e7fd | ||
|
|
bf845ae0f7 | ||
|
|
9be6a8c888 | ||
|
|
38a987676e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -60,7 +60,7 @@ backend/app/api/static/public/*
|
||||
backend/api
|
||||
|
||||
docs/.vitepress/cache/
|
||||
/.data/
|
||||
.data/
|
||||
|
||||
# Playwright
|
||||
frontend/test-results/
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -22,6 +22,7 @@
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"eslint.format.enable": true,
|
||||
"eslint.validate": ["javascript", "typescript", "vue"],
|
||||
"eslint.useFlatConfig": true,
|
||||
"css.validate": false,
|
||||
"tailwindCSS.includeLanguages": {
|
||||
|
||||
@@ -138,6 +138,13 @@ func run(cfg *config.Config) error {
|
||||
)
|
||||
}
|
||||
|
||||
if strings.ToLower(cfg.Database.Driver) == "sqlite3" {
|
||||
db := c.Sql()
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
log.Info().Msg("SQLite connection pool configured: max_open=1, max_idle=1")
|
||||
}
|
||||
|
||||
migrationsFs, err := migrations.Migrations(strings.ToLower(cfg.Database.Driver))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get migrations for %s: %w", strings.ToLower(cfg.Database.Driver), err)
|
||||
|
||||
@@ -50,6 +50,7 @@ func (svc *ItemService) AttachmentAdd(ctx Context, itemID uuid.UUID, filename st
|
||||
_, err = svc.repo.Attachments.Create(ctx, itemID, repo.ItemCreateAttachment{Title: filename, Content: file}, attachmentType, primary)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("failed to create attachment")
|
||||
return repo.ItemOut{}, err
|
||||
}
|
||||
|
||||
return svc.repo.Items.GetOneByGroup(ctx, ctx.GID, itemID)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/data/repo"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
)
|
||||
|
||||
func TestItemService_AddAttachment(t *testing.T) {
|
||||
@@ -60,3 +61,53 @@ func TestItemService_AddAttachment(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, contents, string(bts))
|
||||
}
|
||||
|
||||
func TestItemService_AddAttachment_InvalidStorage(t *testing.T) {
|
||||
// Create a service with an invalid storage path to simulate the issue
|
||||
svc := &ItemService{
|
||||
repo: tRepos,
|
||||
filepath: "/nonexistent/path/that/should/not/exist",
|
||||
}
|
||||
|
||||
// Create a temporary repo with invalid storage config
|
||||
invalidRepos := repo.New(tClient, tbus, config.Storage{
|
||||
PrefixPath: "/",
|
||||
ConnString: "file:///nonexistent/directory/that/does/not/exist",
|
||||
}, "mem://{{ .Topic }}", config.Thumbnail{
|
||||
Enabled: false,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
})
|
||||
|
||||
svc.repo = invalidRepos
|
||||
|
||||
loc, err := invalidRepos.Locations.Create(context.Background(), tGroup.ID, repo.LocationCreate{
|
||||
Description: "test",
|
||||
Name: "test-invalid",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, loc)
|
||||
|
||||
itmC := repo.ItemCreate{
|
||||
Name: fk.Str(10),
|
||||
Description: fk.Str(10),
|
||||
LocationID: loc.ID,
|
||||
}
|
||||
|
||||
itm, err := invalidRepos.Items.Create(context.Background(), tGroup.ID, itmC)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, itm)
|
||||
t.Cleanup(func() {
|
||||
err := invalidRepos.Items.Delete(context.Background(), itm.ID)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
contents := fk.Str(1000)
|
||||
reader := strings.NewReader(contents)
|
||||
|
||||
// Attempt to add attachment with invalid storage - should return an error
|
||||
_, err = svc.AttachmentAdd(tCtx, itm.ID, "testfile.txt", "attachment", false, reader)
|
||||
|
||||
// This should return an error now (after the fix)
|
||||
assert.Error(t, err, "AttachmentAdd should return an error when storage is invalid")
|
||||
}
|
||||
|
||||
@@ -218,9 +218,8 @@ func (r *AttachmentRepo) Create(ctx context.Context, itemID uuid.UUID, doc ItemC
|
||||
// Upload the file to the storage bucket
|
||||
path, err := r.UploadFile(ctx, itemGroup, doc)
|
||||
if err != nil {
|
||||
err := tx.Rollback()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
||||
return nil, rollbackErr
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@ package repo
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/sysadminsmedia/homebox/backend/pkgs/textutils"
|
||||
)
|
||||
|
||||
func TestItemsRepository_AccentInsensitiveSearch(t *testing.T) {
|
||||
// Test cases for accent-insensitive search
|
||||
testCases := []struct {
|
||||
name string
|
||||
itemName string
|
||||
searchQuery string
|
||||
shouldMatch bool
|
||||
description string
|
||||
name string
|
||||
itemName string
|
||||
searchQuery string
|
||||
shouldMatch bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "Spanish accented item, search without accents",
|
||||
@@ -155,25 +155,25 @@ func TestItemsRepository_AccentInsensitiveSearch(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test the normalization logic used in the repository
|
||||
normalizedSearch := textutils.NormalizeSearchQuery(tc.searchQuery)
|
||||
|
||||
|
||||
// This simulates what happens in the repository
|
||||
// The original search would find exact matches (case-insensitive)
|
||||
// The normalized search would find accent-insensitive matches
|
||||
|
||||
|
||||
// Test that our normalization works as expected
|
||||
if tc.shouldMatch {
|
||||
// If it should match, then either the original query should match
|
||||
// or the normalized query should match when applied to the stored data
|
||||
assert.NotEqual(t, "", normalizedSearch, "Normalized search should not be empty")
|
||||
|
||||
|
||||
// The key insight is that we're searching with both the original and normalized queries
|
||||
// So "electrónica" will be found when searching for "electronica" because:
|
||||
// 1. Original search: "electronica" doesn't match "electrónica"
|
||||
// 2. Normalized search: "electronica" matches the normalized version
|
||||
t.Logf("✓ %s: Item '%s' should be found with search '%s' (normalized: '%s')",
|
||||
t.Logf("✓ %s: Item '%s' should be found with search '%s' (normalized: '%s')",
|
||||
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
|
||||
} else {
|
||||
t.Logf("✗ %s: Item '%s' should NOT be found with search '%s' (normalized: '%s')",
|
||||
t.Logf("✗ %s: Item '%s' should NOT be found with search '%s' (normalized: '%s')",
|
||||
tc.description, tc.itemName, tc.searchQuery, normalizedSearch)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -71,6 +71,8 @@ type LabelMakerConf struct {
|
||||
DynamicLength bool `yaml:"bool" conf:"default:true"`
|
||||
LabelServiceUrl *string `yaml:"label_service_url"`
|
||||
LabelServiceTimeout *time.Duration `yaml:"label_service_timeout"`
|
||||
RegularFontPath *string `yaml:"regular_font_path"`
|
||||
BoldFontPath *string `yaml:"bold_font_path"`
|
||||
}
|
||||
|
||||
type BarcodeAPIConf struct {
|
||||
|
||||
@@ -27,6 +27,13 @@ import (
|
||||
"golang.org/x/image/font/gofont/gomedium"
|
||||
)
|
||||
|
||||
type FontType int
|
||||
|
||||
const (
|
||||
FontTypeRegular FontType = iota
|
||||
FontTypeBold
|
||||
)
|
||||
|
||||
type GenerateParameters struct {
|
||||
Width int
|
||||
Height int
|
||||
@@ -140,6 +147,48 @@ func wrapText(text string, face font.Face, maxWidth int, maxHeight int, lineHeig
|
||||
return wrappedLines, ""
|
||||
}
|
||||
|
||||
func loadFont(cfg *config.Config, fontType FontType) (*truetype.Font, error) {
|
||||
var fontPath *string
|
||||
var fallbackData []byte
|
||||
|
||||
switch fontType {
|
||||
case FontTypeRegular:
|
||||
if cfg != nil && cfg.LabelMaker.RegularFontPath != nil {
|
||||
fontPath = cfg.LabelMaker.RegularFontPath
|
||||
}
|
||||
fallbackData = gomedium.TTF
|
||||
case FontTypeBold:
|
||||
if cfg != nil && cfg.LabelMaker.BoldFontPath != nil {
|
||||
fontPath = cfg.LabelMaker.BoldFontPath
|
||||
}
|
||||
fallbackData = gobold.TTF
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown font type: %d", fontType)
|
||||
}
|
||||
|
||||
if fontPath != nil && *fontPath != "" {
|
||||
data, err := os.ReadFile(*fontPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load font from %s: %v, using fallback font", *fontPath, err)
|
||||
} else {
|
||||
font, err := truetype.Parse(data)
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse font from %s: %v, using fallback font", *fontPath, err)
|
||||
} else {
|
||||
log.Printf("Successfully loaded font from %s", *fontPath)
|
||||
return font, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
font, err := truetype.Parse(fallbackData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return font, nil
|
||||
}
|
||||
|
||||
func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config) error {
|
||||
if err := params.Validate(); err != nil {
|
||||
return err
|
||||
@@ -165,12 +214,12 @@ func GenerateLabel(w io.Writer, params *GenerateParameters, cfg *config.Config)
|
||||
qr.DisableBorder = true
|
||||
qrImage := qr.Image(params.QrSize)
|
||||
|
||||
regularFont, err := truetype.Parse(gomedium.TTF)
|
||||
regularFont, err := loadFont(cfg, FontTypeRegular)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
boldFont, err := truetype.Parse(gobold.TTF)
|
||||
boldFont, err := loadFont(cfg, FontTypeBold)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
187
backend/pkgs/labelmaker/labelmaker_test.go
Normal file
187
backend/pkgs/labelmaker/labelmaker_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package labelmaker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/sysadminsmedia/homebox/backend/internal/sys/config"
|
||||
"golang.org/x/image/font/gofont/gobold"
|
||||
"golang.org/x/image/font/gofont/gomedium"
|
||||
)
|
||||
|
||||
func TestLoadFont_WithNilConfig(t *testing.T) {
|
||||
font, err := loadFont(nil, FontTypeRegular)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, font)
|
||||
|
||||
font, err = loadFont(nil, FontTypeBold)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, font)
|
||||
}
|
||||
|
||||
func TestLoadFont_WithEmptyConfig(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
|
||||
font, err := loadFont(cfg, FontTypeRegular)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, font)
|
||||
|
||||
font, err = loadFont(cfg, FontTypeBold)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, font)
|
||||
}
|
||||
|
||||
func TestLoadFont_WithCustomFontPath(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
fontPath := filepath.Join(tempDir, "test-font.ttf")
|
||||
|
||||
err := os.WriteFile(fontPath, gomedium.TTF, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &config.Config{
|
||||
LabelMaker: config.LabelMakerConf{
|
||||
RegularFontPath: &fontPath,
|
||||
},
|
||||
}
|
||||
|
||||
font, err := loadFont(cfg, FontTypeRegular)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, font)
|
||||
}
|
||||
|
||||
func TestLoadFont_WithNonExistentFontPath(t *testing.T) {
|
||||
nonExistentPath := "/non/existent/path/font.ttf"
|
||||
cfg := &config.Config{
|
||||
LabelMaker: config.LabelMakerConf{
|
||||
RegularFontPath: &nonExistentPath,
|
||||
},
|
||||
}
|
||||
|
||||
font, err := loadFont(cfg, FontTypeRegular)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, font)
|
||||
}
|
||||
|
||||
func TestLoadFont_UnknownFontType(t *testing.T) {
|
||||
cfg := &config.Config{}
|
||||
|
||||
_, err := loadFont(cfg, FontType(999))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unknown font type")
|
||||
}
|
||||
|
||||
func TestLoadFont_BoldFontWithCustomPath(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
fontPath := filepath.Join(tempDir, "test-bold-font.ttf")
|
||||
|
||||
err := os.WriteFile(fontPath, gobold.TTF, 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := &config.Config{
|
||||
LabelMaker: config.LabelMakerConf{
|
||||
BoldFontPath: &fontPath,
|
||||
},
|
||||
}
|
||||
|
||||
font, err := loadFont(cfg, FontTypeBold)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, font)
|
||||
}
|
||||
|
||||
func TestLoadFont_EmptyStringPath(t *testing.T) {
|
||||
emptyPath := ""
|
||||
cfg := &config.Config{
|
||||
LabelMaker: config.LabelMakerConf{
|
||||
RegularFontPath: &emptyPath,
|
||||
},
|
||||
}
|
||||
|
||||
font, err := loadFont(cfg, FontTypeRegular)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, font)
|
||||
}
|
||||
|
||||
func TestLoadFont_CJKRendering(t *testing.T) {
|
||||
cjkFontPath := filepath.Join("testdata", "NotoSansKR-VF.ttf")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
fontPath string
|
||||
shouldHaveGlyph bool
|
||||
}{
|
||||
{
|
||||
name: "Korean with default font",
|
||||
text: "한글",
|
||||
fontPath: "",
|
||||
shouldHaveGlyph: false,
|
||||
},
|
||||
{
|
||||
name: "Chinese with default font",
|
||||
text: "中文",
|
||||
fontPath: "",
|
||||
shouldHaveGlyph: false,
|
||||
},
|
||||
{
|
||||
name: "Japanese with default font",
|
||||
text: "ひらがなカタカナ",
|
||||
fontPath: "",
|
||||
shouldHaveGlyph: false,
|
||||
},
|
||||
{
|
||||
name: "Korean with Noto Sans CJK",
|
||||
text: "한글",
|
||||
fontPath: cjkFontPath,
|
||||
shouldHaveGlyph: true,
|
||||
},
|
||||
{
|
||||
name: "Chinese with Noto Sans CJK",
|
||||
text: "中文",
|
||||
fontPath: cjkFontPath,
|
||||
shouldHaveGlyph: true,
|
||||
},
|
||||
{
|
||||
name: "Japanese with Noto Sans CJK",
|
||||
text: "ひらがなカタカナ",
|
||||
fontPath: cjkFontPath,
|
||||
shouldHaveGlyph: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var cfg *config.Config
|
||||
if tt.fontPath != "" {
|
||||
if _, err := os.Stat(tt.fontPath); os.IsNotExist(err) {
|
||||
t.Skipf("Font file not found: %s", tt.fontPath)
|
||||
}
|
||||
cfg = &config.Config{
|
||||
LabelMaker: config.LabelMakerConf{
|
||||
RegularFontPath: &tt.fontPath,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
font, err := loadFont(cfg, FontTypeRegular)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, font)
|
||||
|
||||
hasAllGlyphs := true
|
||||
for _, r := range tt.text {
|
||||
if font.Index(r) == 0 {
|
||||
hasAllGlyphs = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if tt.shouldHaveGlyph {
|
||||
assert.True(t, hasAllGlyphs, "Font should render %s characters", tt.name)
|
||||
} else {
|
||||
assert.False(t, hasAllGlyphs, "Default font should not render %s characters", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
92
backend/pkgs/labelmaker/testdata/NotoSans-LICENSE
vendored
Normal file
92
backend/pkgs/labelmaker/testdata/NotoSans-LICENSE
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
backend/pkgs/labelmaker/testdata/NotoSansKR-VF.ttf
vendored
Normal file
BIN
backend/pkgs/labelmaker/testdata/NotoSansKR-VF.ttf
vendored
Normal file
Binary file not shown.
15
backend/pkgs/labelmaker/testdata/README.md
vendored
Normal file
15
backend/pkgs/labelmaker/testdata/README.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Test Data
|
||||
|
||||
This directory contains font files used only for testing purposes.
|
||||
|
||||
## Fonts
|
||||
|
||||
- **NotoSansKR-VF.ttf**: Noto Sans CJK Korean Variable Font
|
||||
- Used for testing CJK (Chinese, Japanese, Korean) character rendering
|
||||
- License: See `NotoSans-LICENSE` file
|
||||
|
||||
## Notes
|
||||
|
||||
- These fonts are **only used during testing** and are **not included in production builds**
|
||||
- Go's build system automatically excludes `testdata` directories from production builds
|
||||
- The fonts support rendering of Korean, Chinese, and Japanese characters
|
||||
@@ -48,6 +48,8 @@ aside: false
|
||||
| HBOX_LABEL_MAKER_PRINT_COMMAND | | the command to use for printing labels. if empty, label printing is disabled. <span v-pre>`{{.FileName}}`</span> in the command will be replaced with the png filename of the label |
|
||||
| HBOX_LABEL_MAKER_DYNAMIC_LENGTH | true | allow label generation with open length. `HBOX_LABEL_MAKER_HEIGHT` is still used for layout and minimal height. If not used, long text may be cut off, but all labels have the same size. |
|
||||
| HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION | | Additional information added to the label like name or phone number |
|
||||
| HBOX_LABEL_MAKER_REGULAR_FONT_PATH | | path to regular font file for label generation (e.g., `/fonts/NotoSansKR-Regular.ttf`). If not set, uses embedded font. Supports TTF format. |
|
||||
| HBOX_LABEL_MAKER_BOLD_FONT_PATH | | path to bold font file for label generation (e.g., `/fonts/NotoSansKR-Bold.ttf`). If not set, uses embedded font. Supports TTF format. |
|
||||
| HBOX_THUMBNAIL_ENABLED | true | enable thumbnail generation for images, supports PNG, JPEG, AVIF, WEBP, GIF file types |
|
||||
| HBOX_THUMBNAIL_WIDTH | 500 | width for generated thumbnails in pixels |
|
||||
| HBOX_THUMBNAIL_HEIGHT | 500 | height for generated thumbnails in pixels |
|
||||
@@ -192,6 +194,8 @@ OPTIONS
|
||||
--label-maker-print-command/$HBOX_LABEL_MAKER_PRINT_COMMAND <string>
|
||||
--label-maker-dynamic-length/$HBOX_LABEL_MAKER_DYNAMIC_LENGTH <bool> (default: true)
|
||||
--label-maker-additional-information/$HBOX_LABEL_MAKER_ADDITIONAL_INFORMATION <string>
|
||||
--label-maker-regular-font-path/$HBOX_LABEL_MAKER_REGULAR_FONT_PATH <string>
|
||||
--label-maker-bold-font-path/$HBOX_LABEL_MAKER_BOLD_FONT_PATH <string>
|
||||
--thumbnail-enabled/$HBOX_THUMBNAIL_ENABLED <bool> (default: true)
|
||||
--thumbnail-width/$HBOX_THUMBNAIL_WIDTH <int> (default: 500)
|
||||
--thumbnail-height/$HBOX_THUMBNAIL_HEIGHT <int> (default: 500)
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
# Bounty Program
|
||||
|
||||
## About
|
||||
As part of our commitment to open source, and building an active community around Homebox (and hopefully active pool of developers), we are enabling bounties on issues.
|
||||
::: danger Bounties Paused
|
||||
Do to issues with the bounty provider we were using, the bounty program is currently paused. We are working on a solution and will update this page when bounties are available again.
|
||||
|
||||
After digging through several platforms, we ended up settling on [boss.dev](https://www.boss.dev/) as it has some of the lowest fees we could possibly find for any of these platforms other than spinning one up ourselves (which we currently aren't in a position to do).
|
||||
|
||||
While it's not the perfect solution, we think it's about the best one we could find at the moment to lower the rates as much as possible to make sure everyone get's the highest payouts possible. (Some we found were as high as a combined 16%!!!)
|
||||
|
||||
We hope that by enabling bounties on issues, people who have the means and want certain features implemented quicker can now sponsor issues, and in turn everyone contributing code can potentially earn some money for their hard work.
|
||||
|
||||
## Contributor
|
||||
As a contributor wanting to accept money from bounties all you need to do is simply register for an account via GitHub, and attach a bank account (or debit card in the USA).
|
||||
|
||||
## Sponsor
|
||||
Sign in with a GitHub account, and then attach a credit card to your account.
|
||||
|
||||
## Commands to use boss.dev
|
||||
There is documentation on their website regarding commands that you can put in comments to use the bounty system. [boss.dev Documentation](https://www.boss.dev/doc)
|
||||
For more information, please check our [Blog post](https://sysadminsjournal.com/navigating-the-future-of-homeboxs-bounties/).
|
||||
:::
|
||||
|
||||
80
docs/en/custom-font-setup.md
Normal file
80
docs/en/custom-font-setup.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# External Font Support for Label Maker
|
||||
|
||||
Label maker supports external font files.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Docker/Podman Setup
|
||||
|
||||
1. **Download external fonts** (e.g., Noto Sans KR):
|
||||
- Download from [Google Fonts](https://fonts.google.com/noto/specimen/Noto+Sans+KR)
|
||||
- Or use the Variable Font from [GitHub](https://github.com/notofonts/noto-cjk)
|
||||
|
||||
2. **Create a fonts directory**:
|
||||
```bash
|
||||
mkdir -p ./fonts
|
||||
# Place your font files in this directory
|
||||
# e.g., NotoSansKR-VF.ttf
|
||||
```
|
||||
|
||||
3. **Mount the fonts directory and set environment variables**:
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
homebox:
|
||||
image: homebox:latest
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./fonts:/fonts:ro # Mount fonts directory as read-only
|
||||
environment:
|
||||
- HBOX_LABEL_MAKER_REGULAR_FONT_PATH=/fonts/NotoSansKR-VF.ttf
|
||||
- HBOX_LABEL_MAKER_BOLD_FONT_PATH=/fonts/NotoSansKR-VF.ttf
|
||||
ports:
|
||||
- 3100:7745
|
||||
```
|
||||
|
||||
Or with podman:
|
||||
```bash
|
||||
podman run -d \
|
||||
--name homebox \
|
||||
-p 3100:7745 \
|
||||
-v ./data:/data \
|
||||
-v ./fonts:/fonts:ro \
|
||||
-e HBOX_LABEL_MAKER_REGULAR_FONT_PATH=/fonts/NotoSansKR-VF.ttf \
|
||||
-e HBOX_LABEL_MAKER_BOLD_FONT_PATH=/fonts/NotoSansKR-VF.ttf \
|
||||
homebox:latest
|
||||
```
|
||||
|
||||
4. **Restart the container** and test label generation with Chinese, Japanese, Korean text!
|
||||
|
||||
## Supported Fonts
|
||||
|
||||
- **Format**: TTF (TrueType Font)
|
||||
- **Recommended Fonts**:
|
||||
- Noto Sans KR (Korean)
|
||||
- Noto Sans CJK (Chinese, Japanese, Korean)
|
||||
- Noto Sans SC (Simplified Chinese)
|
||||
- Noto Sans JP (Japanese)
|
||||
|
||||
## Fallback Behavior
|
||||
|
||||
1. **External font specified** → Tries to load from `HBOX_LABEL_MAKER_*_FONT_PATH`
|
||||
2. **External font fails or not specified** → Falls back to bundled Go fonts (Latin-only, **does not support CJK characters**)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Labels still show squares (□□□)
|
||||
- Check if the font file exists at the specified path
|
||||
- Verify the font file format (must be TTF, not OTF)
|
||||
- Check container logs: `podman logs homebox | grep -i font`
|
||||
|
||||
### Font file not found
|
||||
- Ensure the volume is correctly mounted
|
||||
- Check file permissions (font files should be readable)
|
||||
- Use absolute paths in environment variables
|
||||
|
||||
## Why External Fonts?
|
||||
|
||||
- **Smaller base image**: No need to embed large font files (~10MB per font)
|
||||
- **Flexibility**: Easily switch fonts without rebuilding the image
|
||||
- **Multi-language support**: Add support for any language by mounting appropriate fonts
|
||||
@@ -1005,12 +1005,12 @@
|
||||
|
||||
.markdown :where(ul) {
|
||||
list-style: disc;
|
||||
margin-left: 2rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.markdown :where(ol) {
|
||||
list-style: decimal;
|
||||
margin-left: 2rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
/* Heading Styles */
|
||||
.markdown :where(h1) {
|
||||
|
||||
83
frontend/components/Form/MarkdownEditor.vue
Normal file
83
frontend/components/Form/MarkdownEditor.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import Markdown from "@/components/global/Markdown.vue";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string | null;
|
||||
label?: string | null;
|
||||
maxLength?: number;
|
||||
minLength?: number;
|
||||
}>(),
|
||||
{
|
||||
modelValue: null,
|
||||
label: null,
|
||||
maxLength: -1,
|
||||
minLength: -1,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const local = ref(props.modelValue ?? "");
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
v => {
|
||||
if (v !== local.value) local.value = v ?? "";
|
||||
}
|
||||
);
|
||||
|
||||
watch(local, v => emit("update:modelValue", v === "" ? null : v));
|
||||
|
||||
const showPreview = ref(false);
|
||||
|
||||
const id = useId();
|
||||
|
||||
const isLengthInvalid = computed(() => {
|
||||
if (typeof local.value !== "string") return false;
|
||||
const len = local.value.length;
|
||||
const max = props.maxLength ?? -1;
|
||||
const min = props.minLength ?? -1;
|
||||
return (max !== -1 && len > max) || (min !== -1 && len < min);
|
||||
});
|
||||
|
||||
const lengthIndicator = computed(() => {
|
||||
if (typeof local.value !== "string") return "";
|
||||
const max = props.maxLength ?? -1;
|
||||
if (max !== -1) {
|
||||
return `${local.value.length}/${max}`;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="mb-2 grid grid-cols-1 items-center gap-2 md:grid-cols-4">
|
||||
<div class="min-w-0">
|
||||
<Label :for="id" class="flex min-w-0 items-center gap-2 px-1">
|
||||
<span class="truncate" :title="props.label ?? ''">{{ props.label }}</span>
|
||||
<span class="grow" />
|
||||
<span class="ml-2 text-sm" :class="{ 'text-destructive': isLengthInvalid }">{{ lengthIndicator }}</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 flex items-center justify-start gap-2 md:col-span-3 md:justify-end">
|
||||
<label class="text-xs text-slate-500">{{ $t("global.preview") }}</label>
|
||||
<Checkbox v-model="showPreview" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<Textarea :id="id" v-model="local" autosize class="resize-none" />
|
||||
|
||||
<div v-if="showPreview">
|
||||
<Markdown :source="local" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,14 +25,13 @@
|
||||
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="markdown text-wrap break-words" v-html="raw" />
|
||||
<div class="markdown prose text-wrap break-words" v-html="raw" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
word-wrap: break-word; /*Fix for long words going out of emelent bounds and issue #407 */
|
||||
overflow-wrap: break-word; /*Fix for long words going out of emelent bounds and issue #407 */
|
||||
white-space: pre-wrap; /*Fix for long words going out of emelent bounds and issue #407 */
|
||||
}
|
||||
.markdown {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { CurrenciesCurrency } from "~/lib/api/types/data-contracts";
|
||||
|
||||
export function validDate(dt: Date | string | null | undefined): boolean {
|
||||
if (!dt) {
|
||||
return false;
|
||||
@@ -42,7 +44,7 @@ function clampDecimals(currency: string, decimals: number): number {
|
||||
}
|
||||
|
||||
// Type guard to validate currency response shape with strict validation
|
||||
function isValidCurrencyItem(item: any): item is { code: string; decimals: number } {
|
||||
function isValidCurrencyItem(item: CurrenciesCurrency): boolean {
|
||||
if (
|
||||
typeof item !== "object" ||
|
||||
item === null ||
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { factorRange, format, parse, zeroTime } from "./datelib";
|
||||
|
||||
describe("format", () => {
|
||||
test("should format a date as a string", () => {
|
||||
const date = new Date(2020, 1, 1);
|
||||
expect(format(date)).toBe("2020-02-01");
|
||||
});
|
||||
|
||||
test("should return the string if a string is passed in", () => {
|
||||
expect(format("2020-02-01")).toBe("2020-02-01");
|
||||
});
|
||||
});
|
||||
import { factorRange, parse, zeroTime } from "./datelib";
|
||||
|
||||
describe("zeroTime", () => {
|
||||
test("should zero out the time", () => {
|
||||
const date = new Date(2020, 1, 1, 12, 30, 30);
|
||||
const date = new Date(Date.UTC(2020, 1, 1, 12, 30, 30));
|
||||
const zeroed = zeroTime(date);
|
||||
expect(zeroed.getHours()).toBe(0);
|
||||
expect(zeroed.getMinutes()).toBe(0);
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { addDays } from "date-fns";
|
||||
|
||||
/*
|
||||
* Formats a date as a string
|
||||
* */
|
||||
export function format(date: Date | string): string {
|
||||
if (typeof date === "string") {
|
||||
return date;
|
||||
}
|
||||
return date.toISOString().split("T")[0]!;
|
||||
}
|
||||
|
||||
export function zeroTime(date: Date): Date {
|
||||
return new Date(
|
||||
new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() - date.getTimezoneOffset() * 60000
|
||||
);
|
||||
const result = new Date(date.getTime());
|
||||
result.setHours(0, 0, 0, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function factorRange(offset: number = 7): [Date, Date] {
|
||||
|
||||
@@ -279,7 +279,8 @@
|
||||
"updating": "Updating",
|
||||
"value": "Value",
|
||||
"version": "Version: { version }",
|
||||
"welcome": "Welcome, { username }"
|
||||
"welcome": "Welcome, { username }",
|
||||
"preview": "Preview"
|
||||
},
|
||||
"home": {
|
||||
"labels": "Labels",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"lint:fix": "eslint --ext \".ts,.js,.vue\" --ignore-pattern \"components/ui\" . --fix",
|
||||
"lint:ci": "eslint --ext \".ts,.js,.vue\" --ignore-pattern \"components/ui\" . --max-warnings 1",
|
||||
"typecheck": "pnpm nuxi typecheck --noEmit",
|
||||
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts",
|
||||
"test:ci": "TEST_SHUTDOWN_API_SERVER=true vitest --run --config ./test/vitest.config.ts --no-file-parallelism",
|
||||
"test:local": "TEST_SHUTDOWN_API_SERVER=false && vitest --run --config ./test/vitest.config.ts",
|
||||
"test:watch": " TEST_SHUTDOWN_API_SERVER=false vitest --config ./test/vitest.config.ts"
|
||||
},
|
||||
@@ -32,7 +32,7 @@
|
||||
"h3": "^1.7.1",
|
||||
"intl-messageformat": "^10.7.16",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"nuxt": "4.0.3",
|
||||
"nuxt": "4.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"shadcn-nuxt": "2.2.0",
|
||||
"typescript": "5.9.2",
|
||||
@@ -74,7 +74,7 @@
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul-vue": "^0.4.1",
|
||||
"vite": "^7.1.3",
|
||||
"vite": "^7.1.5",
|
||||
"vue": "3.5.20",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-sonner": "^2.0.8"
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import { DialogID } from "~/components/ui/dialog-provider/utils";
|
||||
import FormTextField from "~/components/Form/TextField.vue";
|
||||
import FormTextArea from "~/components/Form/TextArea.vue";
|
||||
import MarkdownEditor from "~/components/Form/MarkdownEditor.vue";
|
||||
import FormDatePicker from "~/components/Form/DatePicker.vue";
|
||||
import FormCheckbox from "~/components/Form/Checkbox.vue";
|
||||
import LocationSelector from "~/components/Location/Selector.vue";
|
||||
@@ -147,7 +148,7 @@
|
||||
type DateKeys<T> = Extract<keyof T, keyof { [K in keyof T as T[K] extends Date | string ? K : never]: any }>;
|
||||
|
||||
type TextFormField = {
|
||||
type: "text" | "textarea";
|
||||
type: "text" | "textarea" | "markdown";
|
||||
label: string;
|
||||
ref: NonNullableStringKeys<ItemOut>;
|
||||
maxLength?: number;
|
||||
@@ -188,7 +189,7 @@
|
||||
ref: "quantity",
|
||||
},
|
||||
{
|
||||
type: "textarea",
|
||||
type: "markdown",
|
||||
label: "items.description",
|
||||
ref: "description",
|
||||
maxLength: 1000,
|
||||
@@ -212,7 +213,7 @@
|
||||
maxLength: 255,
|
||||
},
|
||||
{
|
||||
type: "textarea",
|
||||
type: "markdown",
|
||||
label: "items.notes",
|
||||
ref: "notes",
|
||||
maxLength: 1000,
|
||||
@@ -613,6 +614,13 @@
|
||||
:max-length="field.maxLength"
|
||||
:min-length="field.minLength"
|
||||
/>
|
||||
<MarkdownEditor
|
||||
v-else-if="field.type === 'markdown'"
|
||||
v-model="item[field.ref]"
|
||||
:label="$t(field.label)"
|
||||
:max-length="field.maxLength"
|
||||
:min-length="field.minLength"
|
||||
/>
|
||||
<FormTextField
|
||||
v-else-if="field.type === 'text'"
|
||||
v-model="item[field.ref]"
|
||||
|
||||
3917
frontend/pnpm-lock.yaml
generated
3917
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3463
pnpm-lock.yaml
generated
3463
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user