diff --git a/APK.md b/APK.md index d9c7d0a..5fbb7d6 100644 --- a/APK.md +++ b/APK.md @@ -29,6 +29,11 @@ The repo tracks `gogio` as a Go tool, so the build runs through: ```sh go tool gogio -target android ... +``` + +The Android build uses the branded icon asset at: + +- `assets/keepassgo-icon.png` Note: @@ -41,4 +46,3 @@ Note: rendered correctly with `gioui.org v0.8.0` on the same emulator and SDK/JDK pipeline. KeePassGO is pinned to the working Gio line until that regression is understood upstream. -``` diff --git a/Makefile b/Makefile index 0aaa63c..0e133f1 100644 --- a/Makefile +++ b/Makefile @@ -28,4 +28,5 @@ apk: -version $(APK_VERSION) \ -minsdk $(ANDROID_MIN_SDK) \ -targetsdk $(ANDROID_TARGET_SDK) \ + -icon assets/keepassgo-icon.png \ . diff --git a/README.md b/README.md index e7a3a16..ebfce1f 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ go test ./... Install: ```bash -go install gioui.org/cmd/gogio@latest +go get -tool gioui.org/cmd/gogio@latest ``` Package: ```bash -gogio -target android . +go tool gogio -target android -icon assets/keepassgo-icon.png . ``` You will need the Android SDK and NDK installed and configured for real device or release packaging. diff --git a/assets/assets.go b/assets/assets.go new file mode 100644 index 0000000..30fcb3d --- /dev/null +++ b/assets/assets.go @@ -0,0 +1,23 @@ +package assets + +import ( + "bytes" + "embed" + "image" + "image/png" +) + +//go:embed *.png *.svg +var files embed.FS + +func MustPNG(name string) image.Image { + data, err := files.ReadFile(name) + if err != nil { + panic(err) + } + img, err := png.Decode(bytes.NewReader(data)) + if err != nil { + panic(err) + } + return img +} diff --git a/assets/keepassgo-icon.png b/assets/keepassgo-icon.png new file mode 100644 index 0000000..c8b3e98 Binary files /dev/null and b/assets/keepassgo-icon.png differ diff --git a/assets/keepassgo-icon.svg b/assets/keepassgo-icon.svg new file mode 100644 index 0000000..302102f --- /dev/null +++ b/assets/keepassgo-icon.svg @@ -0,0 +1,18 @@ + + KeePassGO icon + Vault-shaped mark with lock, layered record lines, and navigation accent. + + + + + + + + + + + + + + + diff --git a/assets/keepassgo-logo-horizontal.png b/assets/keepassgo-logo-horizontal.png new file mode 100644 index 0000000..ff22b6e Binary files /dev/null and b/assets/keepassgo-logo-horizontal.png differ diff --git a/assets/keepassgo-logo-horizontal.svg b/assets/keepassgo-logo-horizontal.svg new file mode 100644 index 0000000..25ec1be --- /dev/null +++ b/assets/keepassgo-logo-horizontal.svg @@ -0,0 +1,22 @@ + + KeePassGO horizontal logo + KeePassGO symbol with wordmark for light-theme desktop UI. + + + + + + + + + + + + + + + + + + KeePassGO + diff --git a/assets/keepassgo-splash-light.svg b/assets/keepassgo-splash-light.svg new file mode 100644 index 0000000..c56b8d2 --- /dev/null +++ b/assets/keepassgo-splash-light.svg @@ -0,0 +1,75 @@ + + KeePassGO splash screen + Light-theme desktop splash screen with structured panels and the KeePassGO logo. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KeePassGO + KeePass-compatible password manager + + + + + + diff --git a/assets/keepassgo-splash-square.png b/assets/keepassgo-splash-square.png new file mode 100644 index 0000000..4000819 Binary files /dev/null and b/assets/keepassgo-splash-square.png differ diff --git a/assets/keepassgo-splash-square.svg b/assets/keepassgo-splash-square.svg new file mode 100644 index 0000000..e855192 --- /dev/null +++ b/assets/keepassgo-splash-square.svg @@ -0,0 +1,41 @@ + + KeePassGO square splash preview + Square crop of the KeePassGO splash system. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KeePassGO + KeePass-compatible password manager + diff --git a/buildapk/config.go b/buildapk/config.go index 100d750..84cd13e 100644 --- a/buildapk/config.go +++ b/buildapk/config.go @@ -15,6 +15,7 @@ const ( DefaultVersion = "0.1.0.1" DefaultMinSDK = "28" DefaultTargetSDK = "35" + DefaultIconPath = "assets/keepassgo-icon.png" ) type Config struct { @@ -26,6 +27,7 @@ type Config struct { Version string MinSDK string TargetSDK string + IconPath string } func DefaultConfig() Config { @@ -38,6 +40,7 @@ func DefaultConfig() Config { Version: DefaultVersion, MinSDK: DefaultMinSDK, TargetSDK: DefaultTargetSDK, + IconPath: DefaultIconPath, } } @@ -50,6 +53,7 @@ func (c Config) GogioArgs() []string { "-version", c.Version, "-minsdk", c.MinSDK, "-targetsdk", c.TargetSDK, + "-icon", c.IconPath, ".", } } @@ -73,6 +77,9 @@ func (c Config) Validate() error { if !isDir(filepath.Join(c.SDKRoot, "build-tools")) { return fmt.Errorf("Android build-tools are missing") } + if !isFile(c.IconPath) { + return fmt.Errorf("Android icon asset is missing: %s", c.IconPath) + } return nil } @@ -88,3 +95,8 @@ func isExecutable(path string) bool { } return info.Mode()&0o111 != 0 } + +func isFile(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} diff --git a/buildapk/config_test.go b/buildapk/config_test.go index c6be37f..91c2fd4 100644 --- a/buildapk/config_test.go +++ b/buildapk/config_test.go @@ -19,6 +19,7 @@ func TestDefaultConfigGogioArgs(t *testing.T) { "-version", DefaultVersion, "-minsdk", DefaultMinSDK, "-targetsdk", DefaultTargetSDK, + "-icon", DefaultIconPath, ".", } @@ -53,6 +54,11 @@ func TestValidateAcceptsCompleteAndroidToolchainLayout(t *testing.T) { Version: DefaultVersion, MinSDK: DefaultMinSDK, TargetSDK: DefaultTargetSDK, + IconPath: filepath.Join(root, "icon.png"), + } + + if err := os.WriteFile(cfg.IconPath, []byte("png"), 0o644); err != nil { + t.Fatalf("WriteFile(%q) error = %v", cfg.IconPath, err) } if err := cfg.Validate(); err != nil { diff --git a/main.go b/main.go index 9741d21..466c47b 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ import ( "git.julianfamily.org/keepassgo/apiapproval" "git.julianfamily.org/keepassgo/apiaudit" "git.julianfamily.org/keepassgo/apitokens" + keepassassets "git.julianfamily.org/keepassgo/assets" "git.julianfamily.org/keepassgo/appstate" "git.julianfamily.org/keepassgo/clipboard" "git.julianfamily.org/keepassgo/passwords" @@ -130,6 +131,8 @@ const ( type ui struct { mode string theme *material.Theme + logoHorizontal paint.ImageOp + splashSquare paint.ImageOp search widget.Editor vaultPath widget.Editor saveAsPath widget.Editor @@ -342,8 +345,10 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) th.Palette.ContrastFg = color.NRGBA{R: 255, G: 252, B: 247, A: 255} u := &ui{ - mode: mode, - theme: th, + mode: mode, + theme: th, + logoHorizontal: paint.NewImageOp(keepassassets.MustPNG("keepassgo-logo-horizontal.png")), + splashSquare: paint.NewImageOp(keepassassets.MustPNG("keepassgo-splash-square.png")), search: widget.Editor{ SingleLine: true, Submit: false, @@ -2040,6 +2045,8 @@ func (u *ui) lifecycleScreen(gtx layout.Context) layout.Dimensions { } return panel(gtx, func(gtx layout.Context) layout.Dimensions { rows := []layout.Widget{ + u.lifecycleBranding, + layout.Spacer{Height: unit.Dp(8)}.Layout, u.lifecycleControls, } return material.List(u.theme, &u.lifecycleList).Layout(gtx, len(rows), func(gtx layout.Context, i int) layout.Dimensions { @@ -2367,9 +2374,7 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions { return compactCard(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(20), productName) - lbl.Color = accentColor - return lbl.Layout(gtx) + return u.brandMark(gtx, 132, 42) }), layout.Rigid(u.headerActions), ) @@ -2381,9 +2386,7 @@ func (u *ui) header(gtx layout.Context) layout.Dimensions { return card(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Alignment: layout.Middle}.Layout(gtx, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - lbl := material.Label(u.theme, unit.Sp(24), productName) - lbl.Color = accentColor - return lbl.Layout(gtx) + return u.brandMark(gtx, 196, 56) }), layout.Rigid(u.headerActions), ) diff --git a/ui_branding.go b/ui_branding.go new file mode 100644 index 0000000..90c9c13 --- /dev/null +++ b/ui_branding.go @@ -0,0 +1,46 @@ +package main + +import ( + "image" + + "gioui.org/layout" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" +) + +func (u *ui) lifecycleBranding(gtx layout.Context) layout.Dimensions { + if u.mode != "phone" { + return layout.Dimensions{} + } + return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return u.brandImage(gtx, u.splashSquare, 132, 132) + }) +} + +func (u *ui) brandMark(gtx layout.Context, widthDP, heightDP float32) layout.Dimensions { + if u.mode == "phone" { + return u.brandImage(gtx, u.splashSquare, widthDP, heightDP) + } + return u.brandImage(gtx, u.logoHorizontal, widthDP, heightDP) +} + +func (u *ui) brandImage(gtx layout.Context, src paint.ImageOp, widthDP, heightDP float32) layout.Dimensions { + width := gtx.Dp(unit.Dp(widthDP)) + height := gtx.Dp(unit.Dp(heightDP)) + if width > gtx.Constraints.Max.X { + width = gtx.Constraints.Max.X + } + if height > gtx.Constraints.Max.Y && gtx.Constraints.Max.Y > 0 { + height = gtx.Constraints.Max.Y + } + img := widget.Image{ + Src: src, + Fit: widget.Contain, + Position: layout.W, + Scale: 1.0 / gtx.Metric.PxPerDp, + } + gtx.Constraints.Min = image.Point{} + gtx.Constraints.Max = image.Pt(width, height) + return img.Layout(gtx) +}