Icon updates
@@ -27,7 +27,9 @@ When available, SVG icons are used as the starting point for all icon formats. O
|
||||
| PNG | All |
|
||||
| WebP | All |
|
||||
| AVIF | All |
|
||||
| ICO | All |
|
||||
| ICO<sup>1</sup> | All |
|
||||
|
||||
<sup>1</sup>Includes the following sizes: 16x16, 32x32, 48x48, 64x64, 128x128
|
||||
|
||||
## Custom Colors
|
||||
|
||||
|
||||
BIN
avif/crow-ci-dark.avif
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
avif/crow-ci-light.avif
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
avif/crow-ci.avif
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
avif/minecraft-creeper-dark.avif
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
avif/minecraft-creeper-light.avif
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
avif/minecraft-creeper.avif
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
avif/minecraft-detailed.avif
Normal file
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 13 KiB |
BIN
avif/playerr.avif
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
avif/questarr.avif
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
avif/spotify-to-plex.avif
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
avif/swiparr-dark.avif
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
avif/swiparr-light.avif
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
avif/swiparr.avif
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
avif/thinkread.avif
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
@@ -1,18 +0,0 @@
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod main.go ./
|
||||
|
||||
RUN CGO_ENABLED=0 go build -o server .
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /app/server /server
|
||||
|
||||
USER 65534:65534
|
||||
|
||||
EXPOSE 4050
|
||||
|
||||
ENTRYPOINT ["/server"]
|
||||
@@ -1,3 +0,0 @@
|
||||
module selfhst-icons
|
||||
|
||||
go 1.25
|
||||
437
build/main.go
@@ -1,437 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
IconSource string
|
||||
JSDelivrURL string
|
||||
LocalPath string
|
||||
StandardIconFormat string
|
||||
CacheTTL time.Duration
|
||||
CacheSize int
|
||||
}
|
||||
|
||||
type CacheItem struct {
|
||||
Content string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
items map[string]CacheItem
|
||||
mutex sync.RWMutex
|
||||
ttl time.Duration
|
||||
max int
|
||||
}
|
||||
|
||||
func NewCache(ttl time.Duration, maxSize int) *Cache {
|
||||
return &Cache{
|
||||
items: make(map[string]CacheItem),
|
||||
ttl: ttl,
|
||||
max: maxSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) (string, bool) {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
item, exists := c.items[key]
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if time.Since(item.Timestamp) > c.ttl {
|
||||
delete(c.items, key)
|
||||
return "", false
|
||||
}
|
||||
|
||||
return item.Content, true
|
||||
}
|
||||
|
||||
func (c *Cache) Set(key, value string) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
if len(c.items) >= c.max {
|
||||
var oldestKey string
|
||||
var oldestTime time.Time
|
||||
first := true
|
||||
|
||||
for k, v := range c.items {
|
||||
if first || v.Timestamp.Before(oldestTime) {
|
||||
oldestKey = k
|
||||
oldestTime = v.Timestamp
|
||||
first = false
|
||||
}
|
||||
}
|
||||
delete(c.items, oldestKey)
|
||||
}
|
||||
|
||||
c.items[key] = CacheItem{
|
||||
Content: value,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
config *Config
|
||||
cache *Cache
|
||||
)
|
||||
|
||||
func loadConfig() *Config {
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "4050"
|
||||
}
|
||||
|
||||
iconSource := os.Getenv("ICON_SOURCE")
|
||||
if iconSource == "" {
|
||||
iconSource = "remote"
|
||||
}
|
||||
|
||||
standardFormat := os.Getenv("STANDARD_ICON_FORMAT")
|
||||
if standardFormat == "" {
|
||||
standardFormat = "svg"
|
||||
}
|
||||
|
||||
if standardFormat != "svg" && standardFormat != "png" && standardFormat != "webp" && standardFormat != "avif" && standardFormat != "ico" {
|
||||
standardFormat = "svg"
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Port: port,
|
||||
IconSource: iconSource,
|
||||
JSDelivrURL: "https://cdn.jsdelivr.net/gh/selfhst/icons@main",
|
||||
LocalPath: "/app/icons",
|
||||
StandardIconFormat: standardFormat,
|
||||
CacheTTL: time.Hour,
|
||||
CacheSize: 500,
|
||||
}
|
||||
}
|
||||
|
||||
func isValidHexColor(color string) bool {
|
||||
matched, _ := regexp.MatchString("^[0-9A-Fa-f]{6}$", color)
|
||||
return matched
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func urlExists(url string) bool {
|
||||
resp, err := http.Head(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
|
||||
func readLocalFile(path string) (string, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func fetchRemoteFile(url string) (string, error) {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func applySVGColor(svgContent, colorCode string) string {
|
||||
color := "#" + colorCode
|
||||
|
||||
re1 := regexp.MustCompile(`style="[^"]*fill:\s*#fff[^"]*"`)
|
||||
svgContent = re1.ReplaceAllStringFunc(svgContent, func(match string) string {
|
||||
re2 := regexp.MustCompile(`fill:\s*#fff`)
|
||||
return re2.ReplaceAllString(match, "fill:"+color)
|
||||
})
|
||||
|
||||
re3 := regexp.MustCompile(`fill="#fff"`)
|
||||
svgContent = re3.ReplaceAllString(svgContent, `fill="`+color+`"`)
|
||||
|
||||
return svgContent
|
||||
}
|
||||
|
||||
func getContentType(format string) string {
|
||||
switch format {
|
||||
case "png":
|
||||
return "image/png"
|
||||
case "webp":
|
||||
return "image/webp"
|
||||
case "avif":
|
||||
return "image/avif"
|
||||
case "ico":
|
||||
return "image/x-icon"
|
||||
case "svg":
|
||||
return "image/svg+xml"
|
||||
default:
|
||||
return "image/svg+xml"
|
||||
}
|
||||
}
|
||||
|
||||
func getCacheKey(iconName, colorCode string) string {
|
||||
if colorCode == "" {
|
||||
return iconName + ":default"
|
||||
}
|
||||
return iconName + ":" + colorCode
|
||||
}
|
||||
|
||||
func handleIcon(w http.ResponseWriter, r *http.Request) {
|
||||
iconName := r.PathValue("iconname")
|
||||
colorCode := r.PathValue("colorcode")
|
||||
|
||||
if iconName == "" {
|
||||
http.Error(w, "Icon name is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if colorCode != "" && !isValidHexColor(colorCode) {
|
||||
log.Printf("[ERROR] Invalid color code for icon \"%s\": %s", iconName, colorCode)
|
||||
http.Error(w, "Invalid color code. Use 6-digit hex without #", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := getCacheKey(iconName, colorCode)
|
||||
|
||||
var contentType string
|
||||
var formatToServe string
|
||||
|
||||
if colorCode != "" {
|
||||
contentType = "image/svg+xml"
|
||||
formatToServe = "svg"
|
||||
} else {
|
||||
contentType = getContentType(config.StandardIconFormat)
|
||||
formatToServe = config.StandardIconFormat
|
||||
}
|
||||
|
||||
if cached, found := cache.Get(cacheKey); found {
|
||||
log.Printf("[CACHE] Serving cached icon: \"%s\"%s (%s)", iconName,
|
||||
func() string { if colorCode != "" { return " with color " + colorCode } else { return "" } }(),
|
||||
formatToServe)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Write([]byte(cached))
|
||||
return
|
||||
}
|
||||
|
||||
var iconContent string
|
||||
var err error
|
||||
|
||||
if config.IconSource == "local" {
|
||||
if colorCode != "" {
|
||||
lightPath := filepath.Join(config.LocalPath, "svg", iconName+"-light.svg")
|
||||
if fileExists(lightPath) {
|
||||
iconContent, err = readLocalFile(lightPath)
|
||||
if err == nil {
|
||||
iconContent = applySVGColor(iconContent, colorCode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var standardPath string
|
||||
if formatToServe == "svg" {
|
||||
standardPath = filepath.Join(config.LocalPath, "svg", iconName+".svg")
|
||||
} else {
|
||||
standardPath = filepath.Join(config.LocalPath, formatToServe, iconName+"."+formatToServe)
|
||||
}
|
||||
|
||||
if fileExists(standardPath) {
|
||||
iconContent, err = readLocalFile(standardPath)
|
||||
}
|
||||
}
|
||||
|
||||
if iconContent == "" {
|
||||
svgPath := filepath.Join(config.LocalPath, "svg", iconName+".svg")
|
||||
if fileExists(svgPath) {
|
||||
iconContent, err = readLocalFile(svgPath)
|
||||
contentType = "image/svg+xml"
|
||||
formatToServe = "svg"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if colorCode != "" {
|
||||
lightURL := config.JSDelivrURL + "/svg/" + iconName + "-light.svg"
|
||||
if urlExists(lightURL) {
|
||||
iconContent, err = fetchRemoteFile(lightURL)
|
||||
if err == nil {
|
||||
iconContent = applySVGColor(iconContent, colorCode)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var standardURL string
|
||||
if formatToServe == "svg" {
|
||||
standardURL = config.JSDelivrURL + "/svg/" + iconName + ".svg"
|
||||
} else {
|
||||
standardURL = config.JSDelivrURL + "/" + formatToServe + "/" + iconName + "." + formatToServe
|
||||
}
|
||||
|
||||
if urlExists(standardURL) {
|
||||
iconContent, err = fetchRemoteFile(standardURL)
|
||||
}
|
||||
}
|
||||
|
||||
if iconContent == "" {
|
||||
svgURL := config.JSDelivrURL + "/svg/" + iconName + ".svg"
|
||||
iconContent, err = fetchRemoteFile(svgURL)
|
||||
contentType = "image/svg+xml"
|
||||
formatToServe = "svg"
|
||||
}
|
||||
}
|
||||
|
||||
if iconContent == "" || err != nil {
|
||||
log.Printf("[ERROR] Icon not found: \"%s\"%s (source: %s)", iconName,
|
||||
func() string { if colorCode != "" { return " with color " + colorCode } else { return "" } }(),
|
||||
config.IconSource)
|
||||
http.Error(w, "Icon not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cache.Set(cacheKey, iconContent)
|
||||
|
||||
log.Printf("[SUCCESS] Serving icon: \"%s\"%s (%s, source: %s)", iconName,
|
||||
func() string { if colorCode != "" { return " with color " + colorCode } else { return "" } }(),
|
||||
formatToServe, config.IconSource)
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Write([]byte(iconContent))
|
||||
}
|
||||
|
||||
func handleCustomIcon(w http.ResponseWriter, r *http.Request) {
|
||||
filename := r.PathValue("filename")
|
||||
|
||||
if filename == "" {
|
||||
http.Error(w, "Filename is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
customPath := filepath.Join("/app/icons/custom", filename)
|
||||
|
||||
log.Printf("[DEBUG] Looking for custom icon at: %s", customPath)
|
||||
|
||||
if !fileExists(customPath) {
|
||||
if files, err := os.ReadDir("/app/icons/custom"); err == nil {
|
||||
var fileList []string
|
||||
for _, file := range files {
|
||||
fileList = append(fileList, file.Name())
|
||||
}
|
||||
log.Printf("[DEBUG] Files in /app/icons/custom: %v", fileList)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Failed to read /app/icons/custom directory: %v", err)
|
||||
}
|
||||
log.Printf("[ERROR] Custom icon not found: \"%s\" at path: %s", filename, customPath)
|
||||
http.Error(w, "Custom icon not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(customPath)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to read custom icon \"%s\": %v", filename, err)
|
||||
http.Error(w, "Failed to read custom icon", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
var contentType string
|
||||
switch ext {
|
||||
case ".png":
|
||||
contentType = "image/png"
|
||||
case ".jpg", ".jpeg":
|
||||
contentType = "image/jpeg"
|
||||
case ".gif":
|
||||
contentType = "image/gif"
|
||||
case ".svg":
|
||||
contentType = "image/svg+xml"
|
||||
case ".webp":
|
||||
contentType = "image/webp"
|
||||
case ".avif":
|
||||
contentType = "image/avif"
|
||||
case ".ico":
|
||||
contentType = "image/x-icon"
|
||||
default:
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
|
||||
log.Printf("[SUCCESS] Serving custom icon: \"%s\" (%s)", filename, contentType)
|
||||
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
configInfo := map[string]interface{}{
|
||||
"server": "Self-hosted icon server",
|
||||
"urlFormat": "https://subdomain.example.com/iconname/colorcode",
|
||||
"features": map[string]interface{}{
|
||||
"iconSource": func() string {
|
||||
if config.IconSource == "local" {
|
||||
return "Local volume"
|
||||
}
|
||||
return "Remote CDN"
|
||||
}(),
|
||||
"standardFormat": config.StandardIconFormat,
|
||||
"caching": fmt.Sprintf("TTL: %ds, Max items: %d", int(config.CacheTTL.Seconds()), config.CacheSize),
|
||||
"baseUrl": func() string {
|
||||
if config.IconSource == "local" {
|
||||
return config.LocalPath
|
||||
}
|
||||
return config.JSDelivrURL
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(configInfo)
|
||||
}
|
||||
|
||||
func main() {
|
||||
config = loadConfig()
|
||||
cache = NewCache(config.CacheTTL, config.CacheSize)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("GET /custom/{filename}", handleCustomIcon)
|
||||
|
||||
mux.HandleFunc("GET /{iconname}/{colorcode}", handleIcon)
|
||||
mux.HandleFunc("GET /{iconname}", handleIcon)
|
||||
|
||||
mux.HandleFunc("GET /", handleRoot)
|
||||
|
||||
log.Printf("Icon server listening on port %s", config.Port)
|
||||
log.Printf("Icon source: %s", func() string {
|
||||
if config.IconSource == "local" {
|
||||
return "Local volume"
|
||||
}
|
||||
return "Remote CDN"
|
||||
}())
|
||||
log.Printf("Cache settings: TTL %ds, Max %d items", int(config.CacheTTL.Seconds()), config.CacheSize)
|
||||
|
||||
log.Fatal(http.ListenAndServe(":"+config.Port, mux))
|
||||
}
|
||||
BIN
ico/crow-ci-dark.ico
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
ico/crow-ci-light.ico
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
ico/crow-ci.ico
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
ico/minecraft-creeper-dark.ico
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
ico/minecraft-creeper-light.ico
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
ico/minecraft-creeper.ico
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
ico/minecraft-detailed.ico
Normal file
|
After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 32 KiB |
BIN
ico/playerr.ico
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
ico/questarr.ico
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ico/spotify-to-plex.ico
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
ico/swiparr-dark.ico
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
ico/swiparr-light.ico
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
ico/swiparr.ico
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
ico/thinkread.ico
Normal file
|
After Width: | Height: | Size: 30 KiB |
2
index-consolidated.json
Normal file → Executable file
96
index.json
@@ -28318,5 +28318,101 @@
|
||||
"Category": "Self-Hosted",
|
||||
"Tags": "",
|
||||
"CreatedAt": "2026-01-03 10:40:04+00:00"
|
||||
},
|
||||
{
|
||||
"Name": "Spotify to Plex",
|
||||
"Reference": "spotify-to-plex",
|
||||
"SVG": "No",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "No",
|
||||
"Dark": "No",
|
||||
"Category": "Self-Hosted",
|
||||
"Tags": "",
|
||||
"CreatedAt": "2026-01-05 11:11:04+00:00"
|
||||
},
|
||||
{
|
||||
"Name": "Playerr",
|
||||
"Reference": "playerr",
|
||||
"SVG": "No",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "No",
|
||||
"Dark": "No",
|
||||
"Category": "Self-Hosted",
|
||||
"Tags": "",
|
||||
"CreatedAt": "2026-01-05 11:13:28+00:00"
|
||||
},
|
||||
{
|
||||
"Name": "ThinkRead",
|
||||
"Reference": "thinkread",
|
||||
"SVG": "No",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "No",
|
||||
"Dark": "No",
|
||||
"Category": "Self-Hosted",
|
||||
"Tags": "",
|
||||
"CreatedAt": "2026-01-05 11:18:14+00:00"
|
||||
},
|
||||
{
|
||||
"Name": "Crow CI",
|
||||
"Reference": "crow-ci",
|
||||
"SVG": "Yes",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "Yes",
|
||||
"Dark": "Yes",
|
||||
"Category": "Self-Hosted",
|
||||
"Tags": "",
|
||||
"CreatedAt": "2026-01-05 12:52:06+00:00"
|
||||
},
|
||||
{
|
||||
"Name": "Swiparr",
|
||||
"Reference": "swiparr",
|
||||
"SVG": "Yes",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "Yes",
|
||||
"Dark": "Yes",
|
||||
"Category": "Self-Hosted",
|
||||
"Tags": "",
|
||||
"CreatedAt": "2026-01-05 12:56:33+00:00"
|
||||
},
|
||||
{
|
||||
"Name": "Questarr",
|
||||
"Reference": "questarr",
|
||||
"SVG": "No",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "No",
|
||||
"Dark": "No",
|
||||
"Category": "Self-Hosted",
|
||||
"Tags": "",
|
||||
"CreatedAt": "2026-01-05 12:58:01+00:00"
|
||||
},
|
||||
{
|
||||
"Name": "Minecraft Creeper",
|
||||
"Reference": "minecraft-creeper",
|
||||
"SVG": "Yes",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "Yes",
|
||||
"Dark": "Yes",
|
||||
"Category": "Other",
|
||||
"Tags": "Minecraft,Video Games",
|
||||
"CreatedAt": "2026-01-05 13:06:23+00:00"
|
||||
},
|
||||
{
|
||||
"Name": "Minecraft (Detailed)",
|
||||
"Reference": "minecraft-detailed",
|
||||
"SVG": "Yes",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "No",
|
||||
"Dark": "No",
|
||||
"Category": "Other",
|
||||
"Tags": "Minecraft,Video Games",
|
||||
"CreatedAt": "2026-01-05 13:11:02+00:00"
|
||||
}
|
||||
]
|
||||
BIN
png/crow-ci-dark.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
png/crow-ci-light.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
png/crow-ci.png
Executable file
|
After Width: | Height: | Size: 26 KiB |
BIN
png/minecraft-creeper-dark.png
Executable file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
png/minecraft-creeper-light.png
Executable file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
png/minecraft-creeper.png
Executable file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
png/minecraft-detailed.png
Executable file
|
After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 43 KiB |
BIN
png/playerr.png
Executable file
|
After Width: | Height: | Size: 47 KiB |
BIN
png/questarr.png
Executable file
|
After Width: | Height: | Size: 39 KiB |
BIN
png/spotify-to-plex.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
png/swiparr-dark.png
Executable file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
png/swiparr-light.png
Executable file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
png/swiparr.png
Executable file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
png/thinkread.png
Executable file
|
After Width: | Height: | Size: 138 KiB |
1
svg/crow-ci-dark.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M512 252.1c0-.1-23.6-136.4-144.8-193.6-.2-.1-.5-.2-.7-.3-.3-.1-.6-.3-.9-.4-32.3-15-63-20.5-90.5-20.5-5.2 0-10.2.2-15.1.6-35.3 2.6-86.8 19.9-99.9 31.2-13.1 11.2-25.7 47.3-28.1 58.5C26 164.6.1 223.5.1 223.6c0 0 59.4-37.6 160-25.3 96 11.8 96 107.7 96 107.7.3 3.7.4 7.3.4 10.9.1 94.6-93.9 146.1-110.7 154.5 30.9-9.7 218.1-77.2 162.5-278.2 37 16.8 74.1 60.6 74.1 89.2 0 0 1.1-2.2 1.7-45.5 20.2 26.9 33.1 79.1 23.6 117.8-.7 3-1.6 5.9-2.7 8.8 2.6-4.4 4.2-7.7 4.8-9.2-4.9 58.7-33.2 95.5-33.3 95.5C458 390.8 479 330.2 479 283.4c0-49.8-23.8-84-23.8-84 40.5 15.7 56.8 52.7 56.8 52.7M184.4 130.5c-4.9 0-8.8-4-8.8-8.8 0-4.9 4-8.8 8.8-8.8 4.9 0 8.8 4 8.8 8.8.1 4.9-3.9 8.8-8.8 8.8m24.5-14.9c97.9-27 144.4 5.2 153 10.1-3.1-9.2-8.4-16.1-18-24.8 90 37.2 113.6 98 113.6 98z" style="fill-rule:evenodd;clip-rule:evenodd"/><path d="M195.6 208c60.4 27.6 60.4 98 60.4 98 .3 3.7.4 7.3.4 10.9 0 22.5-5.3 42.6-13.6 60.2-20.1-12.1-47.9-30.9-32.3-92.6 10.3-41.2-1.8-64.2-14.9-76.5m-54.1 264.7c122.9 13.5 248.1-44.9 266.1-117.8 9.5-38.7-3.4-90.9-23.6-117.8-.6 43.2-1.7 45.5-1.7 45.5 0-28.6-37-72.4-74.1-89.2 60.6 218.7-166.7 279.3-166.7 279.3m220.4-347c-3.1-9.2-8.4-16.1-18-24.8 90 37.2 113.6 98 113.6 98l-248.7-83.4c98-26.9 144.5 5.3 153.1 10.2" style="opacity:.7"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
svg/crow-ci-light.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M512 252.1c0-.1-23.6-136.4-144.8-193.6-.2-.1-.5-.2-.7-.3-.3-.1-.6-.3-.9-.4-32.3-15-63-20.5-90.5-20.5-5.2 0-10.2.2-15.1.6-35.3 2.6-86.8 19.9-99.9 31.2-13.1 11.2-25.7 47.3-28.1 58.5C26 164.6.1 223.5.1 223.6c0 0 59.4-37.6 160-25.3 96 11.8 96 107.7 96 107.7.3 3.7.4 7.3.4 10.9.1 94.6-93.9 146.1-110.7 154.5 30.9-9.7 218.1-77.2 162.5-278.2 37 16.8 74.1 60.6 74.1 89.2 0 0 1.1-2.2 1.7-45.5 20.2 26.9 33.1 79.1 23.6 117.8-.7 3-1.6 5.9-2.7 8.8 2.6-4.4 4.2-7.7 4.8-9.2-4.9 58.7-33.2 95.5-33.3 95.5C458 390.8 479 330.2 479 283.4c0-49.8-23.8-84-23.8-84 40.5 15.7 56.8 52.7 56.8 52.7M184.4 130.5c-4.9 0-8.8-4-8.8-8.8 0-4.9 4-8.8 8.8-8.8 4.9 0 8.8 4 8.8 8.8.1 4.9-3.9 8.8-8.8 8.8m24.5-14.9c97.9-27 144.4 5.2 153 10.1-3.1-9.2-8.4-16.1-18-24.8 90 37.2 113.6 98 113.6 98z" style="fill-rule:evenodd;clip-rule:evenodd;fill:#fff"/><path d="M195.6 208c60.4 27.6 60.4 98 60.4 98 .3 3.7.4 7.3.4 10.9 0 22.5-5.3 42.6-13.6 60.2-20.1-12.1-47.9-30.9-32.3-92.6 10.3-41.2-1.8-64.2-14.9-76.5m-54.1 264.7c122.9 13.5 248.1-44.9 266.1-117.8 9.5-38.7-3.4-90.9-23.6-117.8-.6 43.2-1.7 45.5-1.7 45.5 0-28.6-37-72.4-74.1-89.2 60.6 218.7-166.7 279.3-166.7 279.3m220.4-347c-3.1-9.2-8.4-16.1-18-24.8 90 37.2 113.6 98 113.6 98l-248.7-83.4c98-26.9 144.5 5.3 153.1 10.2" style="opacity:.7;fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
svg/crow-ci.svg
Executable file
|
After Width: | Height: | Size: 7.8 KiB |
1
svg/minecraft-creeper-dark.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M481.7 0H30.3C13.6 0 0 13.6 0 30.3v451.3C0 498.4 13.6 512 30.3 512h451.3c16.8 0 30.3-13.6 30.3-30.3V30.3C512 13.6 498.4 0 481.7 0m-72.6 204.8H306.7V256h50.7v178.2h-50.7v-76.8H204.8v76.8h-50.7V256h50.7v-51.2H102.4V102.4h102.4v102.4h101.9V102.4h102.4z"/><path d="M154.1 256h50.7v25.1h-50.7zm50.7-26.1h101.9v-25.1H204.8zm101.9 51.2h50.7V256h-50.7zm0-178.7V128H383v76.3h-76.3v.5h102.4V102.4zm-101.9 0v102.4H102.4V102.4zm-.5 25.6H128v76.3h76.3z" style="opacity:.75"/><path d="M204.3 204.3H128V128h76.3zM306.7 128v76.3H383V128zm0 101.9H204.8v51.2h-50.7v153.1h50.7v-76.8h101.9v76.8h50.7V281.1h-50.7z" style="opacity:.25"/></svg>
|
||||
|
After Width: | Height: | Size: 713 B |
1
svg/minecraft-creeper-light.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M481.7 0H30.3C13.6 0 0 13.6 0 30.3v451.3C0 498.4 13.6 512 30.3 512h451.3c16.8 0 30.3-13.6 30.3-30.3V30.3C512 13.6 498.4 0 481.7 0m-72.6 204.8H306.7V256h50.7v178.2h-50.7v-76.8H204.8v76.8h-50.7V256h50.7v-51.2H102.4V102.4h102.4v102.4h101.9V102.4h102.4z" style="fill:#fff"/><path d="M154.1 256h50.7v25.1h-50.7zm50.7-26.1h101.9v-25.1H204.8zm101.9 51.2h50.7V256h-50.7zm0-178.7V128H383v76.3h-76.3v.5h102.4V102.4zm-101.9 0v102.4H102.4V102.4zm-.5 25.6H128v76.3h76.3z" style="opacity:.75;fill:#fff"/><path d="M204.3 204.3H128V128h76.3zM306.7 128v76.3H383V128zm0 101.9H204.8v51.2h-50.7v153.1h50.7v-76.8h101.9v76.8h50.7V281.1h-50.7z" style="opacity:.25;fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 751 B |
1
svg/minecraft-creeper.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M30.3 0h451.3C498.4 0 512 13.6 512 30.3v451.3c0 16.8-13.6 30.3-30.3 30.3H30.3C13.6 512 0 498.4 0 481.7V30.3C0 13.6 13.6 0 30.3 0" style="fill:#1db53c"/><path d="M102.4 102.4h102.4v102.4H102.4zm204.3 0h102.4v102.4H306.7zM154.1 256h50.7v25.1h-50.7zm50.7-51.2h101.9v25.1H204.8zM306.7 256h50.7v25.1h-50.7z" style="fill:#073e21"/><path d="M204.3 204.3H128V128h76.3zM306.7 128v76.3H383V128zm0 101.9H204.8v51.2h-50.7v153.1h50.7v-76.8h101.9v76.8h50.7V281.1h-50.7z"/></svg>
|
||||
|
After Width: | Height: | Size: 556 B |
1
svg/minecraft-detailed.svg
Executable file
|
After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 12 KiB |
1
svg/swiparr-dark.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M381.8 159.8v192.4c0 8.4-6.8 15.3-15.3 15.3H257c-8.4 0-15.3-6.8-15.3-15.3V159.8c0-8.4 6.8-15.3 15.3-15.3h109.6c8.4.1 15.2 6.9 15.2 15.3M512 84.3v343.3c0 46.6-37.8 84.3-84.3 84.3H84.3C37.8 512 0 474.2 0 427.7V84.3C0 37.8 37.8 0 84.3 0h343.3C474.2 0 512 37.8 512 84.3m-380.7 102c0-7.9-6.4-14.2-14.2-14.2-7.9 0-14.2 6.4-14.2 14.2v139.5c0 7.9 6.4 14.2 14.2 14.2 7.9 0 14.2-6.4 14.2-14.2zm55.9-27.5c0-7.9-6.4-14.2-14.2-14.2-7.9 0-14.2 6.4-14.2 14.2v194.5c0 7.9 6.4 14.2 14.2 14.2 7.9 0 14.2-6.4 14.2-14.2zm222.4-.2c0-23.1-18.8-41.9-41.9-41.9H255.8c-23.1 0-41.9 18.8-41.9 41.9v195c0 23.1 18.8 41.9 41.9 41.9h111.8c23.1 0 41.9-18.8 41.9-41.9v-195z"/></svg>
|
||||
|
After Width: | Height: | Size: 741 B |
1
svg/swiparr-light.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M381.8 159.8v192.4c0 8.4-6.8 15.3-15.3 15.3H257c-8.4 0-15.3-6.8-15.3-15.3V159.8c0-8.4 6.8-15.3 15.3-15.3h109.6c8.4.1 15.2 6.9 15.2 15.3M512 84.3v343.3c0 46.6-37.8 84.3-84.3 84.3H84.3C37.8 512 0 474.2 0 427.7V84.3C0 37.8 37.8 0 84.3 0h343.3C474.2 0 512 37.8 512 84.3m-380.7 102c0-7.9-6.4-14.2-14.2-14.2-7.9 0-14.2 6.4-14.2 14.2v139.5c0 7.9 6.4 14.2 14.2 14.2 7.9 0 14.2-6.4 14.2-14.2zm55.9-27.5c0-7.9-6.4-14.2-14.2-14.2-7.9 0-14.2 6.4-14.2 14.2v194.5c0 7.9 6.4 14.2 14.2 14.2 7.9 0 14.2-6.4 14.2-14.2zm222.4-.2c0-23.1-18.8-41.9-41.9-41.9H255.8c-23.1 0-41.9 18.8-41.9 41.9v195c0 23.1 18.8 41.9 41.9 41.9h111.8c23.1 0 41.9-18.8 41.9-41.9v-195z" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 759 B |
1
svg/swiparr.svg
Executable file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M427.7 512H84.3C37.8 512 0 474.2 0 427.7V84.3C0 37.8 37.8 0 84.3 0h343.3C474.2 0 512 37.8 512 84.3v343.3c0 46.6-37.8 84.4-84.3 84.4"/><path d="M367.7 116.7H255.8c-23.1 0-41.9 18.8-41.9 41.9v195c0 23.1 18.8 41.9 41.9 41.9h111.8c23.1 0 41.9-18.8 41.9-41.9v-195c.1-23.2-18.7-41.9-41.8-41.9m14.1 235.6c0 8.4-6.8 15.3-15.3 15.3H257c-8.4 0-15.3-6.8-15.3-15.3V159.8c0-8.4 6.8-15.3 15.3-15.3h109.6c8.4 0 15.3 6.8 15.3 15.3v192.5zM173 367.5c-7.9 0-14.2-6.4-14.2-14.2V158.8c0-7.9 6.4-14.2 14.2-14.2 7.9 0 14.2 6.4 14.2 14.2v194.5c0 7.9-6.4 14.2-14.2 14.2M117 340c-7.9 0-14.2-6.4-14.2-14.2V186.3c0-7.9 6.4-14.2 14.2-14.2 7.9 0 14.2 6.4 14.2 14.2v139.5c.1 7.9-6.3 14.2-14.2 14.2" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 785 B |
BIN
webp/crow-ci-dark.webp
Executable file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
webp/crow-ci-light.webp
Executable file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
webp/crow-ci.webp
Executable file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
webp/minecraft-creeper-dark.webp
Executable file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
webp/minecraft-creeper-light.webp
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
webp/minecraft-creeper.webp
Executable file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
webp/minecraft-detailed.webp
Executable file
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
BIN
webp/playerr.webp
Executable file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
webp/questarr.webp
Executable file
|
After Width: | Height: | Size: 22 KiB |
BIN
webp/spotify-to-plex.webp
Executable file
|
After Width: | Height: | Size: 18 KiB |
BIN
webp/swiparr-dark.webp
Executable file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
webp/swiparr-light.webp
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
webp/swiparr.webp
Executable file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
webp/thinkread.webp
Executable file
|
After Width: | Height: | Size: 7.4 KiB |