aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-07-18 14:18:39 +0530
committerBobby <[email protected]>2025-07-18 14:18:39 +0530
commit821773b12c07a4bc23628e7d98ac4b34da1eb9e1 (patch)
treec37711d86bd893a9bc5a829653890ee75c29ee09
parent01e730c68a79862112798d4816625ddcd00350d9 (diff)
downloadimageboard-821773b12c07a4bc23628e7d98ac4b34da1eb9e1.tar.xz
imageboard-821773b12c07a4bc23628e7d98ac4b34da1eb9e1.zip
template filters and image resizer
-rw-r--r--filters/filters.go19
-rw-r--r--filters/naturaltime.go86
-rw-r--r--imageboard/main.go2
-rw-r--r--models/image.go20
-rw-r--r--static/css/main.css8
-rw-r--r--static/scripts/resize.js11
-rw-r--r--templates/layouts/main.django2
-rw-r--r--templates/login.django2
-rw-r--r--templates/partials/search.django2
-rw-r--r--templates/posts/list.django2
-rw-r--r--templates/posts/single.django116
-rw-r--r--templates/preferences.django10
-rw-r--r--templates/register.django2
13 files changed, 228 insertions, 54 deletions
diff --git a/filters/filters.go b/filters/filters.go
new file mode 100644
index 0000000..9c1704a
--- /dev/null
+++ b/filters/filters.go
@@ -0,0 +1,19 @@
+package filters
+
+import (
+ "log"
+
+ "github.com/flosch/pongo2/v6"
+)
+
+func Initialize() {
+ filters := map[string]func(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error){
+ "naturaltime": naturaltimeFilter,
+ }
+
+ for name, filterFunc := range filters {
+ if err := pongo2.RegisterFilter(name, filterFunc); err != nil {
+ log.Println("Failed to register filter:", name, "Error:", err)
+ }
+ }
+}
diff --git a/filters/naturaltime.go b/filters/naturaltime.go
new file mode 100644
index 0000000..4b7e773
--- /dev/null
+++ b/filters/naturaltime.go
@@ -0,0 +1,86 @@
+package filters
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/flosch/pongo2/v6"
+)
+
+func naturaltimeFilter(in *pongo2.Value, param *pongo2.Value) (*pongo2.Value, *pongo2.Error) {
+ timeValue, ok := in.Interface().(time.Time)
+ if !ok {
+ return nil, &pongo2.Error{
+ Sender: "filter:naturaltime",
+ OrigError: fmt.Errorf("input must be a time.Time, got %T", in.Interface()),
+ }
+ }
+
+ now := time.Now()
+ diff := now.Sub(timeValue)
+
+ if diff < 0 {
+ diff = -diff
+ if diff < time.Minute {
+ return pongo2.AsValue("in a few seconds"), nil
+ } else if diff < time.Hour {
+ minutes := int(diff.Minutes())
+ if minutes == 1 {
+ return pongo2.AsValue("in 1 minute"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("in %d minutes", minutes)), nil
+ } else if diff < 24*time.Hour {
+ hours := int(diff.Hours())
+ if hours == 1 {
+ return pongo2.AsValue("in 1 hour"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("in %d hours", hours)), nil
+ } else {
+ days := int(diff.Hours() / 24)
+ if days == 1 {
+ return pongo2.AsValue("in 1 day"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("in %d days", days)), nil
+ }
+ }
+
+ if diff < time.Minute {
+ return pongo2.AsValue("just now"), nil
+ } else if diff < time.Hour {
+ minutes := int(diff.Minutes())
+ if minutes == 1 {
+ return pongo2.AsValue("1 minute ago"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("%d minutes ago", minutes)), nil
+ } else if diff < 24*time.Hour {
+ hours := int(diff.Hours())
+ if hours == 1 {
+ return pongo2.AsValue("1 hour ago"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("%d hours ago", hours)), nil
+ } else if diff < 7*24*time.Hour {
+ days := int(diff.Hours() / 24)
+ if days == 1 {
+ return pongo2.AsValue("1 day ago"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("%d days ago", days)), nil
+ } else if diff < 30*24*time.Hour {
+ weeks := int(diff.Hours() / (24 * 7))
+ if weeks == 1 {
+ return pongo2.AsValue("1 week ago"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("%d weeks ago", weeks)), nil
+ } else if diff < 365*24*time.Hour {
+ months := int(diff.Hours() / (24 * 30))
+ if months == 1 {
+ return pongo2.AsValue("1 month ago"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("%d months ago", months)), nil
+ } else {
+ years := int(diff.Hours() / (24 * 365))
+ if years == 1 {
+ return pongo2.AsValue("1 year ago"), nil
+ }
+ return pongo2.AsValue(fmt.Sprintf("%d years ago", years)), nil
+ }
+}
diff --git a/imageboard/main.go b/imageboard/main.go
index 511269b..13327f2 100644
--- a/imageboard/main.go
+++ b/imageboard/main.go
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"imageboard/config"
+ "imageboard/filters"
"imageboard/middleware"
"imageboard/processors"
"imageboard/router"
@@ -24,6 +25,7 @@ func main() {
log.Println("Warning: AppSecret is set to a default value which is not secure. Please set a strong random secret in your APP_SECRET environment variable or .env file.")
}
+ filters.Initialize()
engine := django.New("./templates", ".django")
engine.Reload(config.Server.IsDevMode)
app := fiber.New(fiber.Config{
diff --git a/models/image.go b/models/image.go
index d902ec7..17f369d 100644
--- a/models/image.go
+++ b/models/image.go
@@ -137,6 +137,26 @@ func (i *Image) GetOriginalDimensions() string {
return "Unknown"
}
+func (i *Image) GetOriginalSize() *ImageSize {
+ return i.GetSize(config.ImageSizeTypeOriginal)
+}
+
+func (i *Image) GetSmallSize() *ImageSize {
+ return i.GetSize(config.ImageSizeTypeSmall)
+}
+
+func (i *Image) GetMediumSize() *ImageSize {
+ return i.GetSize(config.ImageSizeTypeMedium)
+}
+
+func (i *Image) GetLargeSize() *ImageSize {
+ return i.GetSize(config.ImageSizeTypeLarge)
+}
+
+func (i *Image) GetThumbnailSize() *ImageSize {
+ return i.GetSize(config.ImageSizeTypeThumbnail)
+}
+
func (i *Image) GetAspectRatio() string {
if fullSize := i.GetSize(config.ImageSizeTypeOriginal); fullSize != nil {
if fullSize.Height == 0 {
diff --git a/static/css/main.css b/static/css/main.css
index 018ca2a..c6cb393 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -180,7 +180,8 @@ main {
text-align: center;
}
-.search-container input[type="text"] {
+.search-container input[type="text"],
+.search-container .itext {
width: 100%;
flex: 1;
}
@@ -195,6 +196,7 @@ main {
align-items: center;
}
+.itext,
input[type="text"],
input[type="email"],
input[type="password"],
@@ -206,6 +208,7 @@ input[type="url"] {
padding: 3px 5px;
}
+.itext:focus,
input[type="text"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
@@ -359,7 +362,8 @@ footer::before {
}
-.fg-sub input {
+.fg-sub input,
+.fg-sub.itext {
border-style: double;
border-width: 3px;
border-color: #9999ff;
diff --git a/static/scripts/resize.js b/static/scripts/resize.js
index 95b75c1..10ea02e 100644
--- a/static/scripts/resize.js
+++ b/static/scripts/resize.js
@@ -27,7 +27,6 @@ function handleResize() {
function switchSize(size) {
currentSize = size
const img = document.getElementById('post-image')
- const container = document.querySelector('.post-image-container')
const sizeData = sizes[size]
img.className = ''
@@ -69,12 +68,10 @@ function switchSize(size) {
img.classList.add('fixed-size')
}
- img.src = sizeData.src
+ if (img.src !== sizeData.src) {
+ img.src = sizeData.src
+ }
updateSizeSelection(size)
}
-window.addEventListener('resize', handleResize)
-
-window.addEventListener('load', function () {
- switchSize(currentSize)
-}) \ No newline at end of file
+window.addEventListener('resize', handleResize) \ No newline at end of file
diff --git a/templates/layouts/main.django b/templates/layouts/main.django
index 8673b21..bb35e00 100644
--- a/templates/layouts/main.django
+++ b/templates/layouts/main.django
@@ -10,6 +10,8 @@
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/icons/favicon-16x16.png" />
<link rel="manifest" href="/static/extra/site.webmanifest" />
{{ PreferencesCSS|safe }}
+ {% block head %}
+ {% endblock %}
</head>
<body>
{% include 'partials/navbar.django' %}
diff --git a/templates/login.django b/templates/login.django
index f2136fc..4d23375 100644
--- a/templates/login.django
+++ b/templates/login.django
@@ -18,7 +18,7 @@
<label for="username">Username</label>
</div>
<div class="fg-sub">
- <input type="text" id="username" name="username" required value="{{ Username }}" minlength="3" maxlength="72" autocomplete="username" pattern="[a-zA-Z0-9_-]+" />
+ <input type="text" class="itext" id="username" name="username" required value="{{ Username }}" minlength="3" maxlength="72" autocomplete="username" pattern="[a-zA-Z0-9_-]+" />
<small>3-72 characters, letters, numbers, underscores, and hyphens only</small>
</div>
</div>
diff --git a/templates/partials/search.django b/templates/partials/search.django
index 86a85b8..fc1ace4 100644
--- a/templates/partials/search.django
+++ b/templates/partials/search.django
@@ -1,7 +1,7 @@
<div class="search-container">
<form action="/posts" method="GET">
<h3>« Search Posts »</h3>
- <input type="text" name="tags" class="search-tags-input" placeholder="enter some tags..." value="{{ QueryTags|default_if_none:'' }}" autocomplete="off" aria-label="Search tags" />
+ <input type="text" class="itext" name="tags" class="search-tags-input" placeholder="enter some tags..." value="{{ QueryTags|default_if_none:'' }}" autocomplete="off" aria-label="Search tags" />
<input type="submit" value="Search" />
<input type="button" value="Clear" onclick="this.form.tags.value='';" />
<div class="rating-toggles">
diff --git a/templates/posts/list.django b/templates/posts/list.django
index 3603cb7..3aca8d2 100644
--- a/templates/posts/list.django
+++ b/templates/posts/list.django
@@ -10,7 +10,7 @@
<div class="post-list">
{% for image in Posts %}
<a href="/posts/{{ image.ID }}" class="post-item">
- <img src="{{ CDNURL }}/thumbnail/{{ image.FileName }}" alt="{{ image.Title }}" width="{{ image.Sizes.1.Width }}" height="{{ image.Sizes.1.Height }}" />
+ <img src="{{ CDNURL }}/thumbnail/{{ image.FileName }}" alt="{{ image.Title }}" width="{{ image.GetThumbnailSize.Width }}" height="{{ image.GetThumbnailSize.Height }}" />
<div class="post-overlay">
<div class="post-overlay-top">
<div class="post-id">ID: {{ image.ID }}</div>
diff --git a/templates/posts/single.django b/templates/posts/single.django
index 8657ffe..ef6bc35 100644
--- a/templates/posts/single.django
+++ b/templates/posts/single.django
@@ -1,4 +1,66 @@
{% extends 'layouts/main.django' %}
+{% block head %}
+ <script type="text/javascript" src="/static/scripts/resize.js"></script>
+ <script type="text/javascript">
+ const sizes = {
+ fitBoth: {
+ src: '{{ CDNURL }}/medium/{{ Post.FileName }}',
+ width: '{{ Post.GetMediumSize.Width }}',
+ height: '{{ Post.GetMediumSize.Height }}'
+ },
+ fitWidth: {
+ src: '{{ CDNURL }}/medium/{{ Post.FileName }}',
+ width: '{{ Post.GetMediumSize.Width }}',
+ height: '{{ Post.GetMediumSize.Height }}'
+ },
+ fitHeight: {
+ src: '{{ CDNURL }}/medium/{{ Post.FileName }}',
+ width: '{{ Post.GetMediumSize.Width }}',
+ height: '{{ Post.GetMediumSize.Height }}'
+ },
+ small: {
+ src: '{{ CDNURL }}/small/{{ Post.FileName }}',
+ width: '{{ Post.GetSmallSize.Width }}',
+ height: '{{ Post.GetSmallSize.Height }}'
+ },
+ medium: {
+ src: '{{ CDNURL }}/medium/{{ Post.FileName }}',
+ width: '{{ Post.GetMediumSize.Width }}',
+ height: '{{ Post.GetMediumSize.Height }}'
+ },
+ large: {
+ src: '{{ CDNURL }}/large/{{ Post.FileName }}',
+ width: '{{ Post.GetLargeSize.Width }}',
+ height: '{{ Post.GetLargeSize.Height }}'
+ },
+ original: {
+ src: '{{ CDNURL }}/original/{{ Post.FileName }}',
+ width: '{{ Post.GetOriginalSize.Width }}',
+ height: '{{ Post.GetOriginalSize.Height }}'
+ }
+ }
+ document.addEventListener('DOMContentLoaded', function () {
+ const imageWidth = parseInt('{{ Post.GetMediumSize.Width }}')
+ const imageHeight = parseInt('{{ Post.GetMediumSize.Height }}')
+ const img = document.getElementById('post-image')
+
+ if (img) {
+ const availableHeight = getAvailableHeight()
+
+ if (imageHeight > imageWidth) {
+ const ratio = availableHeight / imageHeight
+ img.style.width = Math.floor(imageWidth * ratio) + 'px'
+ img.style.height = availableHeight + 'px'
+ } else {
+ const containerWidth = document.querySelector('.post-image-container').offsetWidth
+ const ratio = containerWidth / imageWidth
+ img.style.width = containerWidth + 'px'
+ img.style.height = Math.floor(imageHeight * ratio) + 'px'
+ }
+ }
+ })
+ </script>
+{% endblock %}
{% block content %}
{% if Error %}
<div class="centered-main">
@@ -25,42 +87,24 @@
<div class="post-image-container">
<img src="{{ CDNURL }}/medium/{{ Post.FileName }}" alt="{{ Post.Title }}" id="post-image" />
</div>
+ <div class="post-details">
+ <div class="post-detail-item">
+ <span class="post-detail-label">ID:</span>
+ <span class="post-detail-value">{{ Post.ID }}</span>
+ </div>
+ <div class="post-detail-item">
+ <span class="post-detail-label">Uploader:</span>
+ <span class="post-detail-value"><a href="/u/{{ Post.Uploader.Username }}">{{ Post.Uploader.Username }}</a></span>
+ </div>
+ <div class="post-detail-item">
+ <span class="post-detail-label">Created:</span>
+ <span class="post-detail-value">{{ Post.CreatedAt|naturaltime }}</span>
+ </div>
+ <div class="post-detail-item">
+ <span class="post-detail-label">Original Size:</span>
+ <span class="post-detail-value">{{ Post.GetOriginalSize.Width }}x{{ Post.GetOriginalSize.Height }} ({{ Post.GetOriginalSize.GetFileSizeFormatted }})</span>
+ </div>
+ </div>
</div>
{% endif %}
{% endblock %}
-{% block scripts %}
- <script type="text/javascript">
- const sizes = {
- fitBoth: {
- src: '{{ CDNURL }}/medium/{{ Post.FileName }}'
- },
- fitWidth: {
- src: '{{ CDNURL }}/medium/{{ Post.FileName }}'
- },
- fitHeight: {
- src: '{{ CDNURL }}/medium/{{ Post.FileName }}'
- },
- small: {
- src: '{{ CDNURL }}/small/{{ Post.FileName }}',
- width: '{{ Post.Sizes.2.Width }}',
- height: '{{ Post.Sizes.2.Height }}'
- },
- medium: {
- src: '{{ CDNURL }}/medium/{{ Post.FileName }}',
- width: '{{ Post.Sizes.3.Width }}',
- height: '{{ Post.Sizes.3.Height }}'
- },
- large: {
- src: '{{ CDNURL }}/large/{{ Post.FileName }}',
- width: '{{ Post.Sizes.4.Width }}',
- height: '{{ Post.Sizes.4.Height }}'
- },
- original: {
- src: '{{ CDNURL }}/original/{{ Post.FileName }}',
- width: '{{ Post.Sizes.5.Width }}',
- height: '{{ Post.Sizes.5.Height }}'
- }
- }
- </script>
- <script type="text/javascript" src="/static/scripts/resize.js"></script>
-{% endblock %}
diff --git a/templates/preferences.django b/templates/preferences.django
index 691cbdf..59245da 100644
--- a/templates/preferences.django
+++ b/templates/preferences.django
@@ -11,7 +11,7 @@
<label for="sidebar-width">Sidebar Width</label>
</div>
<div class="fg-sub">
- <input type="text" id="sidebar-width" name="sidebar-width" value="{{ Preferences.SidebarWidth }}" placeholder="e.g., 250px" />
+ <input type="text" class="itext" id="sidebar-width" name="sidebar-width" value="{{ Preferences.SidebarWidth }}" placeholder="e.g., 250px" />
<small>Set the width of the sidebar. Use CSS units like px, em, or %.</small>
</div>
</div>
@@ -20,7 +20,7 @@
<label for="main-width">Main Content Width</label>
</div>
<div class="fg-sub">
- <input type="text" id="main-width" name="main-width" value="{{ Preferences.MainContentWidth }}" placeholder="e.g., 800px" />
+ <input type="text" class="itext" id="main-width" name="main-width" value="{{ Preferences.MainContentWidth }}" placeholder="e.g., 800px" />
<small>Set the width of the main content area. Use CSS units like px, em, or %.</small>
</div>
</div>
@@ -29,7 +29,7 @@
<label for="h1-font-size">H1 Font Size</label>
</div>
<div class="fg-sub">
- <input type="text" id="h1-font-size" name="h1-font-size" value="{{ Preferences.H1FontSize }}" min="10" max="100" />
+ <input type="text" class="itext" id="h1-font-size" name="h1-font-size" value="{{ Preferences.H1FontSize }}" min="10" max="100" />
<small>Set the font size for H1 elements. This will affect the main headings across the site. Use a valid CSS size (e.g., 16px, 1.5em, etc.).</small>
</div>
</div>
@@ -38,7 +38,7 @@
<label for="body-font-size">Body Font Size</label>
</div>
<div class="fg-sub">
- <input type="text" id="body-font-size" name="body-font-size" value="{{ Preferences.BodyFontSize }}" min="10" max="100" />
+ <input type="text" class="itext" id="body-font-size" name="body-font-size" value="{{ Preferences.BodyFontSize }}" min="10" max="100" />
<small>Set the font size for body text. This will affect the readability of the content across the site. Use a valid CSS size (e.g., 13px, 1em, etc.).</small>
</div>
</div>
@@ -47,7 +47,7 @@
<label for="small-font-size">Small Font Size</label>
</div>
<div class="fg-sub">
- <input type="text" id="small-font-size" name="small-font-size" value="{{ Preferences.SmallFontSize }}" min="8" max="50" />
+ <input type="text" class="itext" id="small-font-size" name="small-font-size" value="{{ Preferences.SmallFontSize }}" min="8" max="50" />
<small>Set the font size for small text elements. This will affect the visibility of smaller text across the site. Use a valid CSS size (e.g., 11px, 0.8em, etc.).</small>
</div>
</div>
diff --git a/templates/register.django b/templates/register.django
index 2990ba5..808f15a 100644
--- a/templates/register.django
+++ b/templates/register.django
@@ -18,7 +18,7 @@
<label for="username">Username</label>
</div>
<div class="fg-sub">
- <input type="text" id="username" name="username" required value="{{ Username }}" minlength="3" maxlength="72" autocomplete="username" pattern="[a-zA-Z0-9_-]+" />
+ <input type="text" class="itext" id="username" name="username" required value="{{ Username }}" minlength="3" maxlength="72" autocomplete="username" pattern="[a-zA-Z0-9_-]+" />
<small>3-72 characters, letters, numbers, underscores, and hyphens only</small>
</div>
</div>