add images from memos
This commit is contained in:
@@ -68,7 +68,7 @@ func main() {
|
||||
memorySvc := memory.NewService(selector, store, loc, logger)
|
||||
|
||||
// Web handler
|
||||
handler := web.NewHandler(memorySvc, logger)
|
||||
handler := web.NewHandler(memorySvc, cfg.Memos.URL, cfg.Memos.PublicURL, logger)
|
||||
|
||||
// HTTP server
|
||||
srv := &http.Server{
|
||||
|
||||
@@ -16,6 +16,12 @@ url = ""
|
||||
# Создаётся в Memos: Settings → Access Tokens.
|
||||
token = ""
|
||||
|
||||
# Публичный адрес Memos (для ссылок на оригинальные заметки).
|
||||
# Если Memos и Remembos развёрнуты на одном сервере, внутренний адрес (url)
|
||||
# может отличаться от публичного. Если не указан — используется url.
|
||||
# Пример: "https://memos.example.com"
|
||||
public_url = ""
|
||||
|
||||
# =============================================================================
|
||||
# База данных (SQLite)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user