appview/ogcard: introduce highlevel helpers to draw icons and assets #757

merged
opened by oppi.li targeting master from push-yvtruzmyvxps
  • DrawLucideIcon & DrawDollySilhouette to simplify the svg drawing logic
  • DrawDollySilhouette now depends on the html template itself, instead of a bespoke svg

this changes lets us remove dolly.svg from the fragments.

Signed-off-by: oppiliappan me@oppi.li

Changed files
+75 -26
appview
issues
ogcard
pulls
repo
+5 -5
appview/issues/opengraph.go
··· 143 143 var statusBgColor color.RGBA 144 144 145 145 if issue.Open { 146 - statusIcon = "static/icons/circle-dot.svg" 146 + statusIcon = "circle-dot" 147 147 statusText = "open" 148 148 statusBgColor = color.RGBA{34, 139, 34, 255} // green 149 149 } else { 150 - statusIcon = "static/icons/circle-dot.svg" 150 + statusIcon = "ban" 151 151 statusText = "closed" 152 152 statusBgColor = color.RGBA{52, 58, 64, 255} // dark gray 153 153 } ··· 155 155 badgeIconSize := 36 156 156 157 157 // Draw icon with status color (no background) 158 - err = statusCommentsArea.DrawSVGIcon(statusIcon, statsX, statsY+iconBaselineOffset-badgeIconSize/2+5, badgeIconSize, statusBgColor) 158 + err = statusCommentsArea.DrawLucideIcon(statusIcon, statsX, statsY+iconBaselineOffset-badgeIconSize/2+5, badgeIconSize, statusBgColor) 159 159 if err != nil { 160 160 log.Printf("failed to draw status icon: %v", err) 161 161 } ··· 172 172 currentX := statsX + badgeIconSize + 12 + statusTextWidth + 50 173 173 174 174 // Draw comment count 175 - err = statusCommentsArea.DrawSVGIcon("static/icons/message-square.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 175 + err = statusCommentsArea.DrawLucideIcon("message-square", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 176 176 if err != nil { 177 177 log.Printf("failed to draw comment icon: %v", err) 178 178 } ··· 193 193 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 194 194 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 195 195 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 196 - err = dollyArea.DrawSVGIcon("templates/fragments/dolly/silhouette.svg", dollyX, dollyY, dollySize, dollyColor) 196 + err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 197 197 if err != nil { 198 198 log.Printf("dolly silhouette not available (this is ok): %v", err) 199 199 }
+59 -10
appview/ogcard/card.go
··· 7 7 import ( 8 8 "bytes" 9 9 "fmt" 10 + "html/template" 10 11 "image" 11 12 "image/color" 12 13 "io" ··· 279 280 return width, nil 280 281 } 281 282 282 - // DrawSVGIcon draws an SVG icon from the embedded files at the specified position 283 - func (c *Card) DrawSVGIcon(svgPath string, x, y, size int, iconColor color.Color) error { 284 - svgData, err := pages.Files.ReadFile(svgPath) 285 - if err != nil { 286 - return fmt.Errorf("failed to read SVG file %s: %w", svgPath, err) 287 - } 288 - 283 + func BuildSVGIconFromData(svgData []byte, iconColor color.Color) (*oksvg.SvgIcon, error) { 289 284 // Convert color to hex string for SVG 290 285 rgba, isRGBA := iconColor.(color.RGBA) 291 286 if !isRGBA { ··· 304 299 // Parse SVG 305 300 icon, err := oksvg.ReadIconStream(strings.NewReader(svgString)) 306 301 if err != nil { 307 - return fmt.Errorf("failed to parse SVG %s: %w", svgPath, err) 302 + return nil, fmt.Errorf("failed to parse SVG: %w", err) 303 + } 304 + 305 + return icon, nil 306 + } 307 + 308 + func BuildSVGIconFromPath(svgPath string, iconColor color.Color) (*oksvg.SvgIcon, error) { 309 + svgData, err := pages.Files.ReadFile(svgPath) 310 + if err != nil { 311 + return nil, fmt.Errorf("failed to read SVG file %s: %w", svgPath, err) 312 + } 313 + 314 + icon, err := BuildSVGIconFromData(svgData, iconColor) 315 + if err != nil { 316 + return nil, fmt.Errorf("failed to build SVG icon %s: %w", svgPath, err) 317 + } 318 + 319 + return icon, nil 320 + } 321 + 322 + func BuildLucideIcon(name string, iconColor color.Color) (*oksvg.SvgIcon, error) { 323 + return BuildSVGIconFromPath(fmt.Sprintf("static/icons/%s.svg", name), iconColor) 324 + } 325 + 326 + func (c *Card) DrawLucideIcon(name string, x, y, size int, iconColor color.Color) error { 327 + icon, err := BuildSVGIconFromPath(fmt.Sprintf("static/icons/%s.svg", name), iconColor) 328 + if err != nil { 329 + return err 330 + } 331 + 332 + c.DrawSVGIcon(icon, x, y, size) 333 + 334 + return nil 335 + } 336 + 337 + func (c *Card) DrawDollySilhouette(x, y, size int, iconColor color.Color) error { 338 + tpl, err := template.New("dolly"). 339 + ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html") 340 + if err != nil { 341 + return fmt.Errorf("failed to read dolly silhouette template: %w", err) 342 + } 343 + 344 + var svgData bytes.Buffer 345 + if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", nil); err != nil { 346 + return fmt.Errorf("failed to execute dolly silhouette template: %w", err) 347 + } 348 + 349 + icon, err := BuildSVGIconFromData(svgData.Bytes(), iconColor) 350 + if err != nil { 351 + return err 308 352 } 309 353 354 + c.DrawSVGIcon(icon, x, y, size) 355 + 356 + return nil 357 + } 358 + 359 + // DrawSVGIcon draws an SVG icon from the embedded files at the specified position 360 + func (c *Card) DrawSVGIcon(icon *oksvg.SvgIcon, x, y, size int) { 310 361 // Set the icon size 311 362 w, h := float64(size), float64(size) 312 363 icon.SetTarget(0, 0, w, h) ··· 334 385 } 335 386 336 387 draw.Draw(c.Img, destRect, iconImg, image.Point{}, draw.Over) 337 - 338 - return nil 339 388 } 340 389 341 390 // DrawImage fills the card with an image, scaled to maintain the original aspect ratio and centered with respect to the non-filled dimension
+7 -7
appview/pulls/opengraph.go
··· 146 146 var statusColor color.RGBA 147 147 148 148 if pull.State.IsOpen() { 149 - statusIcon = "static/icons/git-pull-request.svg" 149 + statusIcon = "git-pull-request" 150 150 statusText = "open" 151 151 statusColor = color.RGBA{34, 139, 34, 255} // green 152 152 } else if pull.State.IsMerged() { 153 - statusIcon = "static/icons/git-merge.svg" 153 + statusIcon = "git-merge" 154 154 statusText = "merged" 155 155 statusColor = color.RGBA{138, 43, 226, 255} // purple 156 156 } else { 157 - statusIcon = "static/icons/git-pull-request-closed.svg" 157 + statusIcon = "git-pull-request-closed" 158 158 statusText = "closed" 159 159 statusColor = color.RGBA{128, 128, 128, 255} // gray 160 160 } ··· 162 162 statusIconSize := 36 163 163 164 164 // Draw icon with status color 165 - err = statusStatsArea.DrawSVGIcon(statusIcon, statsX, statsY+iconBaselineOffset-statusIconSize/2+5, statusIconSize, statusColor) 165 + err = statusStatsArea.DrawLucideIcon(statusIcon, statsX, statsY+iconBaselineOffset-statusIconSize/2+5, statusIconSize, statusColor) 166 166 if err != nil { 167 167 log.Printf("failed to draw status icon: %v", err) 168 168 } ··· 179 179 currentX := statsX + statusIconSize + 12 + statusTextWidth + 40 180 180 181 181 // Draw comment count 182 - err = statusStatsArea.DrawSVGIcon("static/icons/message-square.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 182 + err = statusStatsArea.DrawLucideIcon("message-square", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 183 183 if err != nil { 184 184 log.Printf("failed to draw comment icon: %v", err) 185 185 } ··· 198 198 currentX += commentTextWidth + 40 199 199 200 200 // Draw files changed 201 - err = statusStatsArea.DrawSVGIcon("static/icons/file-diff.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 201 + err = statusStatsArea.DrawLucideIcon("static/icons/file-diff", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 202 202 if err != nil { 203 203 log.Printf("failed to draw file diff icon: %v", err) 204 204 } ··· 241 241 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 242 242 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 243 243 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 244 - err = dollyArea.DrawSVGIcon("templates/fragments/dolly/silhouette.svg", dollyX, dollyY, dollySize, dollyColor) 244 + err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 245 245 if err != nil { 246 246 log.Printf("dolly silhouette not available (this is ok): %v", err) 247 247 }
+4 -4
appview/repo/opengraph.go
··· 158 158 // Draw star icon, count, and label 159 159 // Align icon baseline with text baseline 160 160 iconBaselineOffset := int(textSize) / 2 161 - err = statsArea.DrawSVGIcon("static/icons/star.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 161 + err = statsArea.DrawLucideIcon("star", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 162 162 if err != nil { 163 163 log.Printf("failed to draw star icon: %v", err) 164 164 } ··· 185 185 186 186 // Draw issues icon, count, and label 187 187 issueStartX := currentX 188 - err = statsArea.DrawSVGIcon("static/icons/circle-dot.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 188 + err = statsArea.DrawLucideIcon("circle-dot", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 189 189 if err != nil { 190 190 log.Printf("failed to draw circle-dot icon: %v", err) 191 191 } ··· 210 210 211 211 // Draw pull request icon, count, and label 212 212 prStartX := currentX 213 - err = statsArea.DrawSVGIcon("static/icons/git-pull-request.svg", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 213 + err = statsArea.DrawLucideIcon("git-pull-request", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 214 214 if err != nil { 215 215 log.Printf("failed to draw git-pull-request icon: %v", err) 216 216 } ··· 236 236 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 237 237 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 238 238 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 239 - err = dollyArea.DrawSVGIcon("templates/fragments/dolly/silhouette.svg", dollyX, dollyY, dollySize, dollyColor) 239 + err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 240 240 if err != nil { 241 241 log.Printf("dolly silhouette not available (this is ok): %v", err) 242 242 }