add images from memos

This commit is contained in:
2026-02-12 11:24:09 +03:00
parent ed04c11eff
commit c083461d38
6 changed files with 117 additions and 22 deletions
+3 -2
View File
@@ -17,8 +17,9 @@ type Config struct {
}
type MemosConfig struct {
URL string `toml:"url"`
Token string `toml:"token"`
URL string `toml:"url"`
Token string `toml:"token"`
PublicURL string `toml:"public_url"`
}
type DatabaseConfig struct {
+39 -11
View File
@@ -5,17 +5,45 @@ import "time"
// Memo represents a memo from the Memos API.
// JSON field names follow protojson camelCase convention.
type Memo struct {
Name string `json:"name"`
State string `json:"state"`
Creator string `json:"creator"`
CreateTime time.Time `json:"createTime"`
UpdateTime time.Time `json:"updateTime"`
DisplayTime time.Time `json:"displayTime"`
Content string `json:"content"`
Visibility string `json:"visibility"`
Tags []string `json:"tags"`
Pinned bool `json:"pinned"`
Snippet string `json:"snippet"`
Name string `json:"name"`
State string `json:"state"`
Creator string `json:"creator"`
CreateTime time.Time `json:"createTime"`
UpdateTime time.Time `json:"updateTime"`
DisplayTime time.Time `json:"displayTime"`
Content string `json:"content"`
Visibility string `json:"visibility"`
Tags []string `json:"tags"`
Pinned bool `json:"pinned"`
Snippet string `json:"snippet"`
Attachments []Attachment `json:"attachments"`
}
// Attachment represents a file attached to a memo.
type Attachment struct {
Name string `json:"name"` // "attachments/{uid}"
Filename string `json:"filename"`
Type string `json:"type"` // MIME type
Size int64 `json:"size,string"`
ExternalLink string `json:"externalLink"`
}
// IsImage returns true if the attachment is an image.
func (a Attachment) IsImage() bool {
switch a.Type {
case "image/png", "image/jpeg", "image/gif", "image/webp", "image/heic", "image/heif", "image/svg+xml":
return true
}
return false
}
// UID extracts the uid part from "attachments/{uid}".
func (a Attachment) UID() string {
const prefix = "attachments/"
if len(a.Name) > len(prefix) {
return a.Name[len(prefix):]
}
return a.Name
}
// ListMemosResponse is the response from GET /api/v1/memos.
+42 -7
View File
@@ -6,6 +6,7 @@ import (
"html/template"
"log/slog"
"net/http"
"strings"
"time"
"git.vakhrushev.me/av/remembos/internal/memory"
@@ -16,11 +17,18 @@ var templateFS embed.FS
var templates = template.Must(template.ParseFS(templateFS, "templates/*.html"))
type imageData struct {
URL string
Alt string
}
type memoryData struct {
DateFormatted string
AgoText string
Content string
Tags []string
Images []imageData
MemoURL string
Tier int
ShowCount int
}
@@ -30,16 +38,24 @@ type errorData struct {
}
type Handler struct {
service *memory.Service
logger *slog.Logger
mux *http.ServeMux
service *memory.Service
logger *slog.Logger
mux *http.ServeMux
memosURL string // internal Memos URL (for attachment files)
publicURL string // public Memos URL (for memo links)
}
func NewHandler(service *memory.Service, logger *slog.Logger) *Handler {
func NewHandler(service *memory.Service, memosURL, publicURL string, logger *slog.Logger) *Handler {
pub := publicURL
if pub == "" {
pub = memosURL
}
h := &Handler{
service: service,
logger: logger,
mux: http.NewServeMux(),
service: service,
memosURL: strings.TrimRight(memosURL, "/"),
publicURL: strings.TrimRight(pub, "/"),
logger: logger,
mux: http.NewServeMux(),
}
h.mux.HandleFunc("GET /", h.handleMemory)
h.mux.HandleFunc("GET /health", h.handleHealth)
@@ -74,11 +90,30 @@ func (h *Handler) handleMemory(w http.ResponseWriter, r *http.Request) {
return
}
var images []imageData
for _, att := range mem.Memo.Attachments {
if !att.IsImage() {
continue
}
var imgURL string
if att.ExternalLink != "" {
imgURL = att.ExternalLink
} else {
imgURL = fmt.Sprintf("%s/file/%s/%s", h.memosURL, att.Name, att.Filename)
}
images = append(images, imageData{URL: imgURL, Alt: att.Filename})
}
// Link to original memo: {publicURL}/{memoName}
memoURL := fmt.Sprintf("%s/%s", h.publicURL, mem.Memo.Name)
data := memoryData{
DateFormatted: formatDate(mem.Date),
AgoText: agoText(mem.Date),
Content: mem.Memo.Content,
Tags: mem.Memo.Tags,
Images: images,
MemoURL: memoURL,
Tier: mem.Tier,
ShowCount: mem.ShowCount,
}
+26 -1
View File
@@ -42,6 +42,16 @@
font-size: 1rem;
line-height: 1.6;
}
.images {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.images img {
max-width: 100%;
border-radius: 6px;
}
.tags {
margin-top: 1rem;
display: flex;
@@ -60,6 +70,13 @@
font-size: 0.8rem;
color: #aaa;
}
.meta a {
color: #aaa;
text-decoration: none;
}
.meta a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
@@ -70,13 +87,21 @@
</div>
<div class="card">
<div class="content">{{.Content}}</div>
{{if .Images}}
<div class="images">
{{range .Images}}<img src="{{.URL}}" alt="{{.Alt}}" loading="lazy">{{end}}
</div>
{{end}}
{{if .Tags}}
<div class="tags">
{{range .Tags}}<span class="tag">#{{.}}</span>{{end}}
</div>
{{end}}
</div>
<div class="meta">Tier {{.Tier}} · показов: {{.ShowCount}}</div>
<div class="meta">
Tier {{.Tier}} · показов: {{.ShowCount}}
{{if .MemoURL}} · <a href="{{.MemoURL}}" target="_blank" rel="noopener">оригинал</a>{{end}}
</div>
</div>
</body>
</html>