Sidebar — uklanjanje treptanja pri učitavanju skupljenog stanja

Inline skript u <head> odmah postavlja klasu na <html> pre iscrtavanja,
CSS je primenjuje bez tranzicije, JS je uklanja nakon što doda .skupljen.

 Changes to be committed:
	modified:   build.sh
	modified:   go.mod
	modified:   go.sum
	modified:   internal/config/setup.go
	modified:   web/static/css/main.css
	modified:   web/templates/teme/podrazumevana/base.html
This commit is contained in:
2026-06-04 02:54:06 +02:00
parent 08b9359a76
commit d48d088efa
6 changed files with 190 additions and 30 deletions
+104 -8
View File
@@ -1,12 +1,108 @@
#!/bin/bash #!/bin/bash
VERSION=${1:-"dev"} set -e
echo "Buildovanje NTech v$VERSION..."
GOARCH=amd64 GOOS=linux go build -ldflags "-X main.Verzija=$VERSION -s -w" -o ntech ./cmd/ntech # ──────────────────────────────────────────────
if [ $? -eq 0 ]; then # Verzija
echo "Build završen: ntech v$VERSION" # ──────────────────────────────────────────────
ls -lh ntech read -p "Verzija (npr. 0.1.1): " VERZIJA
VERZIJA=${VERZIJA:-"dev"}
# ──────────────────────────────────────────────
# Okruženje
# ──────────────────────────────────────────────
echo ""
echo "Okruženje:"
echo " 1) production"
echo " 2) development"
read -p "Izbor [1/2, podrazumevano 1]: " OKR_IZBOR
OKR_IZBOR=${OKR_IZBOR:-1}
if [ "$OKR_IZBOR" = "2" ]; then
OKRUZENJE="development"
LDFLAGS="-X main.Verzija=dev-${VERZIJA}"
NAZIV="ntech-dev-${VERZIJA}"
else else
echo "Build neuspešan" OKRUZENJE="production"
exit 1 LDFLAGS="-X main.Verzija=${VERZIJA} -s -w"
NAZIV="ntech-${VERZIJA}"
fi fi
# ──────────────────────────────────────────────
# Ciljni OS
# ──────────────────────────────────────────────
echo ""
echo "Ciljni OS:"
echo " 1) Linux (amd64)"
echo " 2) Windows (amd64)"
read -p "Izbor [1/2, podrazumevano 1]: " OS_IZBOR
OS_IZBOR=${OS_IZBOR:-1}
if [ "$OS_IZBOR" = "2" ]; then
GOOS_VAL="windows"
NAZIV="${NAZIV}.exe"
else
GOOS_VAL="linux"
fi
# ──────────────────────────────────────────────
# UPX kompresija
# ──────────────────────────────────────────────
echo ""
UPX_DOSTUPAN=false
if command -v upx &>/dev/null; then
UPX_DOSTUPAN=true
read -p "Kompresovati UPX-om? [d/N]: " UPX_IZBOR
else
echo "UPX nije instaliran — kompresija preskočena."
UPX_IZBOR="n"
fi
# ──────────────────────────────────────────────
# Sažetak pre builda
# ──────────────────────────────────────────────
echo ""
echo "──────────────────────────────────────────"
echo " Okruženje : ${OKRUZENJE}"
echo " Verzija : ${VERZIJA}"
echo " OS : ${GOOS_VAL}/amd64"
echo " Izlaz : ${NAZIV}"
if [ "$UPX_DOSTUPAN" = true ] && [[ "$UPX_IZBOR" =~ ^[dDyY] ]]; then
echo " UPX : da"
else
echo " UPX : ne"
fi
echo "──────────────────────────────────────────"
echo ""
read -p "Pokrenuti build? [D/n]: " POTVRDA
POTVRDA=${POTVRDA:-"d"}
if [[ ! "$POTVRDA" =~ ^[dDyY] ]]; then
echo "Build otkazan."
exit 0
fi
# ──────────────────────────────────────────────
# Build
# ──────────────────────────────────────────────
echo ""
echo "Buildovanje..."
CGO_ENABLED=0 GOARCH=amd64 GOOS=${GOOS_VAL} go build \
-ldflags "${LDFLAGS}" \
-o "${NAZIV}" \
./cmd/ntech
echo "Build završen: ${NAZIV}"
ls -lh "${NAZIV}"
# ──────────────────────────────────────────────
# UPX
# ──────────────────────────────────────────────
if [ "$UPX_DOSTUPAN" = true ] && [[ "$UPX_IZBOR" =~ ^[dDyY] ]]; then
echo ""
echo "Kompresovanje sa UPX..."
upx --best "${NAZIV}"
echo "Nakon kompresije:"
ls -lh "${NAZIV}"
fi
echo ""
echo "Gotovo."
-1
View File
@@ -7,7 +7,6 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
golang.org/x/crypto v0.52.0 golang.org/x/crypto v0.52.0
modernc.org/sqlite v1.51.0 modernc.org/sqlite v1.51.0
) )
-2
View File
@@ -27,8 +27,6 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
+60 -19
View File
@@ -1,24 +1,57 @@
package config package config
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs" "io/fs"
"log" "log"
"net"
"net/http" "net/http"
"strings" "strings"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/webview/webview_go"
) )
// PokreniSetup pokreće WebView prozor za prvo podešavanje // nadjiLokalneAdrese vraća sve ne-loopback IPv4 adrese mrežnih kartica
func nadjiLokalneAdrese() []string {
var adrese []string
ifaces, err := net.Interfaces()
if err != nil {
return adrese
}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
if ip4 := ip.To4(); ip4 != nil {
adrese = append(adrese, ip4.String())
}
}
}
return adrese
}
// PokreniSetup pokreće HTTP server za prvo podešavanje i čeka da korisnik završi
func PokreniSetup(fsys fs.FS) { func PokreniSetup(fsys fs.FS) {
port := NadjiSlobodanPort() port := NadjiSlobodanPort()
if port == 0 { if port == 0 {
log.Fatal("Greška: nije pronađen nijedan slobodan port") log.Fatal("ntech: setup: nije pronađen nijedan slobodan port")
} }
gotov := make(chan struct{})
adresa := fmt.Sprintf(":%d", port) adresa := fmt.Sprintf(":%d", port)
r := chi.NewRouter() r := chi.NewRouter()
@@ -43,39 +76,47 @@ func PokreniSetup(fsys fs.FS) {
// proverava da li je određeni port slobodan // proverava da li je određeni port slobodan
r.Get("/setup/proveriPort", func(w http.ResponseWriter, req *http.Request) { r.Get("/setup/proveriPort", func(w http.ResponseWriter, req *http.Request) {
portStr := req.URL.Query().Get("port") portStr := req.URL.Query().Get("port")
var port int var p int
fmt.Sscanf(portStr, "%d", &port) fmt.Sscanf(portStr, "%d", &p)
slobodan := JelPortSlobodan(port) slobodan := JelPortSlobodan(p)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"slobodan": slobodan}) json.NewEncoder(w).Encode(map[string]bool{"slobodan": slobodan})
}) })
// prima izabrani port i upisuje .env // prima izabrani port, upisuje ntech.env i signalizira završetak
r.Post("/setup/potvrdi", func(w http.ResponseWriter, req *http.Request) { r.Post("/setup/potvrdi", func(w http.ResponseWriter, req *http.Request) {
var telo struct { var telo struct {
Port int `json:"port"` Port int `json:"port"`
} }
json.NewDecoder(req.Body).Decode(&telo) json.NewDecoder(req.Body).Decode(&telo)
err := SacuvajEnv(telo.Port) if err := SacuvajEnv(telo.Port); err != nil {
if err != nil {
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError) http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{"ok": true}) json.NewEncoder(w).Encode(map[string]bool{"ok": true})
close(gotov)
}) })
// pokreni server u pozadini srv := &http.Server{Addr: adresa, Handler: r}
go func() { go func() {
log.Printf("Setup server pokrenut na portu %d", port) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
http.ListenAndServe(adresa, r) log.Fatalf("ntech: setup server: %v", err)
}
}() }()
// otvori WebView prozor fmt.Printf("\n╔══════════════════════════════════════════════╗\n")
w := webview.New(false) fmt.Printf("║ NTech — prvo pokretanje ║\n")
defer w.Destroy() fmt.Printf("║ ║\n")
w.SetTitle("NTech — Konfiguracija") fmt.Printf("║ Otvorite u browseru: ║\n")
w.SetSize(520, 580, 0) fmt.Printf("║ http://localhost:%d/setup ║\n", port)
w.Navigate(fmt.Sprintf("http://localhost:%d/setup", port)) for _, ip := range nadjiLokalneAdrese() {
w.Run() fmt.Printf("║ http://%s:%d/setup ║\n", ip, port)
}
fmt.Printf("║ ║\n")
fmt.Printf("╚══════════════════════════════════════════════╝\n\n")
<-gotov
srv.Shutdown(context.Background())
} }
+19
View File
@@ -18,6 +18,25 @@ body {
overflow: hidden; overflow: hidden;
} }
/* bez tranzicije pri inicijalnom učitavanju skupljenog sidebara */
.sidebar-init-skupljen .sidebar {
width: 60px;
overflow: hidden;
transition: none;
}
.sidebar-init-skupljen .sidebar .logo-zona {
opacity: 0;
width: 0;
pointer-events: none;
transition: none;
}
.sidebar-init-skupljen .sidebar .nav-oznaka,
.sidebar-init-skupljen .sidebar .nav-stavka span {
opacity: 0;
pointer-events: none;
transition: none;
}
/* sidebar */ /* sidebar */
.sidebar { .sidebar {
width: 220px; width: 220px;
@@ -7,6 +7,12 @@
<title>{{block "naslov" .}}NTech{{end}}</title> <title>{{block "naslov" .}}NTech{{end}}</title>
<meta name="csrf-token" content="{{.CsrfToken}}"> <meta name="csrf-token" content="{{.CsrfToken}}">
<script>
if (window.innerWidth > 768 && localStorage.getItem('sidebar-skupljen') === 'true') {
document.documentElement.classList.add('sidebar-init-skupljen');
}
</script>
<!-- tema — učitava se prva --> <!-- tema — učitava se prva -->
<link rel="stylesheet" href="/static/css/teme/{{.Tema}}.css" /> <link rel="stylesheet" href="/static/css/teme/{{.Tema}}.css" />
@@ -51,6 +57,7 @@
if (!mobilni() && localStorage.getItem("sidebar-skupljen") === "true") { if (!mobilni() && localStorage.getItem("sidebar-skupljen") === "true") {
sidebar.classList.add("skupljen"); sidebar.classList.add("skupljen");
} }
document.documentElement.classList.remove('sidebar-init-skupljen');
hamburger.addEventListener("click", () => { hamburger.addEventListener("click", () => {
if (mobilni()) { if (mobilni()) {