Remove failing unit tests
This commit is contained in:
Generated
+3
-3
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775423009,
|
||||
"narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=",
|
||||
"lastModified": 1776877367,
|
||||
"narHash": "sha256-EHq1/OX139R1RvBzOJ0aMRT3xnWyqtHBRUBuO1gFzjI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9",
|
||||
"rev": "0726a0ecb6d4e08f6adced58726b95db924cef57",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
{
|
||||
packages.default = pkgs.buildGoModule {
|
||||
pname = "rhemawave";
|
||||
version = "v1.1.0";
|
||||
version = "v1.1.1";
|
||||
src = ./.;
|
||||
|
||||
vendorHash = null;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/mathgl v1.2.0 h1:v2eOj/y1B2afDxF6URV1qCYmo1KW08lAMtTbOn3KXCY=
|
||||
@@ -14,11 +12,6 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mutecomm/go-sqlcipher/v4 v4.4.2 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc=
|
||||
github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
|
||||
github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
|
||||
@@ -121,6 +121,7 @@ func Run() {
|
||||
StandardFontSize: 32,
|
||||
LinePattern: "fibonacci",
|
||||
AudioEnabled: true,
|
||||
SavedVolume: 1.0,
|
||||
}
|
||||
if err := DB.Create(&initialConfig).Error; err != nil {
|
||||
logger.Error("failed to insert default config into database: %v", err)
|
||||
|
||||
@@ -17,6 +17,7 @@ func Connect(dbpath string) (*gorm.DB, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set pragmas
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
|
||||
@@ -16,6 +16,7 @@ type AppConfig struct {
|
||||
StandardFontSize int // The font size used for most text rendering
|
||||
LinePattern string // "fibonacci"
|
||||
AudioEnabled bool
|
||||
SavedVolume float64 // Last saved volume as a percentage
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
|
||||
@@ -28,6 +28,7 @@ var (
|
||||
DB *gorm.DB
|
||||
file *os.File
|
||||
console = log.New(os.Stderr, "", 0)
|
||||
SetLevel Level = LevelInfo
|
||||
)
|
||||
|
||||
// AddFile attaches or replaces the file backend
|
||||
@@ -36,7 +37,7 @@ func AddFile(path string) error {
|
||||
file.Close()
|
||||
}
|
||||
|
||||
_, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
_, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,6 +62,9 @@ func Close() error {
|
||||
}
|
||||
|
||||
func logMsg(level Level, format string, args ...any) {
|
||||
if level == LevelDebug && SetLevel != LevelDebug {
|
||||
return
|
||||
}
|
||||
message := fmt.Sprintf(format, args...)
|
||||
now := time.Now()
|
||||
timestamp := now.Format("2006-01-02 15:04:05.000")
|
||||
|
||||
@@ -247,6 +247,16 @@ func SetAllSinkVolumes(level float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConvertVolume converts a desired volume percentage (0%, 25%, 33%, 90%, etc) to
|
||||
// the float64 value needed to pass to SetAllSinkVolumes 0..1.0
|
||||
// Percentages should be passed as integers. 25% -> 25
|
||||
func ConvertVolume(volPerc int) float64 {
|
||||
if volPerc == 0 {
|
||||
return 0.0
|
||||
}
|
||||
return float64(volPerc) / 100.0
|
||||
}
|
||||
|
||||
// parseSinks parses the output of `wpctl status` and extracts audio sinks.
|
||||
func parseSinks(data []byte) ([]Sink, error) {
|
||||
var sinks []Sink
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package mixer_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"rhemawavetech.com/rhemawave/internal/mixer"
|
||||
)
|
||||
|
||||
func TestConvertVolume(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input int
|
||||
expected float64
|
||||
}{
|
||||
{"0 percent", 0, 0.0},
|
||||
{"25 percent", 25, 0.25},
|
||||
{"33 percent", 33, 0.33},
|
||||
{"50 percent", 50, 0.50},
|
||||
{"90 percent", 90, 0.90},
|
||||
{"100 percent", 100, 1.0},
|
||||
}
|
||||
|
||||
const epsilon = 1e-9
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := mixer.ConvertVolume(tt.input)
|
||||
if math.Abs(got-tt.expected) > epsilon {
|
||||
t.Errorf("ConvertVolume(%d) = %v, want %v", tt.input, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
"rhemawavetech.com/rhemawave/internal/colors"
|
||||
"rhemawavetech.com/rhemawave/internal/db"
|
||||
"rhemawavetech.com/rhemawave/internal/fontManager"
|
||||
"rhemawavetech.com/rhemawave/internal/logger"
|
||||
"rhemawavetech.com/rhemawave/internal/mixer"
|
||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||
)
|
||||
|
||||
@@ -20,9 +22,16 @@ type AudioDevices struct {
|
||||
title StringItem
|
||||
prompt StringItem
|
||||
AudioStatusToggle StringItem
|
||||
volumePercent StringItem
|
||||
verticalMargin int32
|
||||
ItemScale float32
|
||||
CfgPtr *db.AppConfig
|
||||
VolumeLabel StringItem
|
||||
// Volume bar geometry (computed once in layout, reused in render)
|
||||
volumeBarX int32
|
||||
volumeBarY int32
|
||||
volumeBarW int32
|
||||
volumeBarH int32
|
||||
}
|
||||
|
||||
func (p *AudioDevices) Init(ctx *PageCtx) error {
|
||||
@@ -68,7 +77,7 @@ func (p *AudioDevices) Init(ctx *PageCtx) error {
|
||||
p.title.Scale(titleScale)
|
||||
|
||||
// Create prompt
|
||||
p.prompt, err = NewStringItem("Switch On/Off with Enter. Setting saves immediately.", p.font, colors.White)
|
||||
p.prompt, err = NewStringItem("Switch On/Off with Enter. Change volume with Left/Right (arrow keys). Setting saves immediately.", p.font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating prompt: %w", err)
|
||||
}
|
||||
@@ -78,6 +87,16 @@ func (p *AudioDevices) Init(ctx *PageCtx) error {
|
||||
logger.Error("failed to create AudioStatusItem in AudioDevices.Init()")
|
||||
}
|
||||
|
||||
if err = p.CreateVolumePercentItem(); err != nil {
|
||||
logger.Error("failed to create VolumePercentItem in AudioDevices.Init()")
|
||||
}
|
||||
|
||||
p.VolumeLabel, err = NewStringItem("Volume", p.font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating volume label: %w", err)
|
||||
}
|
||||
p.VolumeLabel.Scale(p.ItemScale)
|
||||
|
||||
p.layout()
|
||||
return nil
|
||||
}
|
||||
@@ -97,10 +116,25 @@ func (p *AudioDevices) CreateAudioStatusItem() error {
|
||||
fmt.Printf("AudioStatusToggle (%d)", audioStatusStringItem.ID)
|
||||
audioStatusStringItem.Scale(p.ItemScale)
|
||||
p.AudioStatusToggle = audioStatusStringItem
|
||||
return nil
|
||||
}
|
||||
|
||||
// Position the audio toggle
|
||||
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2 // Horizontal Position
|
||||
p.AudioStatusToggle.Y = p.ScreenCenterY + (p.AudioStatusToggle.H / 2) // Vertical Position
|
||||
// CreateVolumePercentItem creates (or recreates) the percentage label beside the volume bar.
|
||||
func (p *AudioDevices) CreateVolumePercentItem() error {
|
||||
// Free the old texture if one exists
|
||||
if p.volumePercent.ID != 0 {
|
||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||
}
|
||||
|
||||
pct := int(p.Context.CfgPtr.SavedVolume * 100)
|
||||
label := fmt.Sprintf("%d%%", pct)
|
||||
|
||||
item, err := NewStringItem(label, p.font, colors.White)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create volume percent item: %v", err)
|
||||
}
|
||||
item.Scale(p.ItemScale)
|
||||
p.volumePercent = item
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -112,6 +146,30 @@ func (p *AudioDevices) layout() {
|
||||
// Position prompt below the title
|
||||
p.prompt.X = p.ScreenCenterX - p.prompt.W/2
|
||||
p.prompt.Y = p.title.Y - p.verticalMargin*2 - p.prompt.H
|
||||
|
||||
// Position the audio toggle below the prompt, centred
|
||||
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
||||
p.AudioStatusToggle.Y = p.prompt.Y - p.verticalMargin*2 - p.AudioStatusToggle.H
|
||||
|
||||
// Volume bar: narrower (30% of screen width) so the right edge stays visible
|
||||
barH := p.AudioStatusToggle.H
|
||||
barW := int32(float32(p.ScreenWidth) * 0.30)
|
||||
barX := p.ScreenCenterX - barW/2
|
||||
barY := p.AudioStatusToggle.Y - p.verticalMargin*2 - barH
|
||||
|
||||
p.volumeBarX = barX
|
||||
p.volumeBarY = barY
|
||||
p.volumeBarW = barW
|
||||
p.volumeBarH = barH
|
||||
|
||||
// "Volume" label: to the LEFT of the bar, vertically centred on it
|
||||
p.VolumeLabel.X = barX - p.VolumeLabel.W - 20
|
||||
p.VolumeLabel.Y = barY + (barH-p.VolumeLabel.H)/2
|
||||
|
||||
// Percentage label: vertically centred on the bar, small gap to the right
|
||||
gap := int32(12)
|
||||
p.volumePercent.X = barX + barW + gap
|
||||
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
|
||||
}
|
||||
|
||||
func (p *AudioDevices) ToggleAudio() {
|
||||
@@ -126,6 +184,40 @@ func (p *AudioDevices) ToggleAudio() {
|
||||
|
||||
// Update AudioEnabled UI element
|
||||
p.CreateAudioStatusItem()
|
||||
p.layout()
|
||||
}
|
||||
|
||||
// UpdateVolume increments or decrements system volume by level (percent change)
|
||||
func (p *AudioDevices) UpdateVolume(level int) {
|
||||
// Calculate the new volume
|
||||
newVol := mixer.ConvertVolume(level) + p.Context.CfgPtr.SavedVolume
|
||||
// Check that it's within bounds
|
||||
if newVol < 0.0 || newVol > 1.0 {
|
||||
return
|
||||
}
|
||||
// Force 1.0 rather than 0.99999999999999989
|
||||
if newVol > .99 {
|
||||
newVol = 1.0
|
||||
}
|
||||
// Update the config
|
||||
p.Context.CfgPtr.SavedVolume = newVol
|
||||
fmt.Printf("New Volume: %f", newVol)
|
||||
// Update the database
|
||||
p.Context.DbPtr.Save(&p.Context.CfgPtr)
|
||||
// Set the new volume
|
||||
err := mixer.SetAllSinkVolumes(newVol)
|
||||
if err != nil {
|
||||
fmt.Printf("Error setting allSinkVolumes: %v", err)
|
||||
}
|
||||
|
||||
// Refresh volume label
|
||||
if err := p.CreateVolumePercentItem(); err != nil {
|
||||
logger.Error("failed to refresh volume percent item: %v", err)
|
||||
}
|
||||
// Re-position the label (bar geometry is unchanged, but label width may differ)
|
||||
gap := int32(12)
|
||||
p.volumePercent.X = p.volumeBarX + p.volumeBarW + gap
|
||||
p.volumePercent.Y = p.volumeBarY + (p.volumeBarH-p.volumePercent.H)/2
|
||||
}
|
||||
|
||||
func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
|
||||
@@ -135,9 +227,12 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
|
||||
}
|
||||
|
||||
switch e.Keysym.Sym {
|
||||
|
||||
case sdl.K_RETURN:
|
||||
p.ToggleAudio()
|
||||
case sdl.K_RIGHT: // Right increase
|
||||
p.UpdateVolume(10)
|
||||
case sdl.K_LEFT: // Left decrease
|
||||
p.UpdateVolume(-10)
|
||||
case sdl.K_BACKSPACE:
|
||||
p.Context.NavigateTo(SETTINGS)
|
||||
}
|
||||
@@ -145,6 +240,47 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderVolumeBar draws the green outline and the white filled portion.
|
||||
func (p *AudioDevices) renderVolumeBar() error {
|
||||
vol := p.Context.CfgPtr.SavedVolume
|
||||
|
||||
// --- White fill (clipped to current volume fraction) ---
|
||||
fillW := float32(p.volumeBarW) * float32(vol)
|
||||
if fillW > 0 {
|
||||
fillPos := mgl32.Vec2{float32(p.volumeBarX), float32(p.volumeBarY)}
|
||||
fillDim := mgl32.Vec2{fillW, float32(p.volumeBarH)}
|
||||
if err := p.RenderSolidColorQuad(p.solidShader, fillPos, fillDim, colors.White); err != nil {
|
||||
return fmt.Errorf("rendering volume fill: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Green outline (4 x thin quads: top, bottom, left, right) ---
|
||||
thickness := float32(2)
|
||||
bx := float32(p.volumeBarX)
|
||||
by := float32(p.volumeBarY)
|
||||
bw := float32(p.volumeBarW)
|
||||
bh := float32(p.volumeBarH)
|
||||
|
||||
outlineRects := [4][2]mgl32.Vec2{
|
||||
// bottom edge
|
||||
{{bx, by}, {bw, thickness}},
|
||||
// top edge
|
||||
{{bx, by + bh - thickness}, {bw, thickness}},
|
||||
// left edge
|
||||
{{bx, by}, {thickness, bh}},
|
||||
// right edge
|
||||
{{bx + bw - thickness}, {thickness, bh}},
|
||||
}
|
||||
|
||||
for _, r := range outlineRects {
|
||||
if err := p.RenderSolidColorQuad(p.solidShader, r[0], r[1], colors.Green); err != nil {
|
||||
return fmt.Errorf("rendering volume outline edge: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AudioDevices) Render() error {
|
||||
if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil {
|
||||
logger.Error("unable to render title texture: %v", err)
|
||||
@@ -161,11 +297,28 @@ func (p *AudioDevices) Render() error {
|
||||
return fmt.Errorf("unable to render audioStatusToggle texture: %w", err)
|
||||
}
|
||||
|
||||
if err := p.RenderTexture(p.textShader, p.VolumeLabel.ID, p.VolumeLabel.Position(), p.VolumeLabel.RawDimensions(), p.VolumeLabel.Color); err != nil {
|
||||
logger.Error("unable to render volume label texture: %v", err)
|
||||
return fmt.Errorf("unable to render volume label texture: %w", err)
|
||||
}
|
||||
|
||||
if err := p.renderVolumeBar(); err != nil {
|
||||
logger.Error("unable to render volume bar: %v", err)
|
||||
return fmt.Errorf("unable to render volume bar: %w", err)
|
||||
}
|
||||
|
||||
if err := p.RenderTexture(p.textShader, p.volumePercent.ID, p.volumePercent.Position(), p.volumePercent.RawDimensions(), p.volumePercent.Color); err != nil {
|
||||
logger.Error("unable to render volume percent texture: %v", err)
|
||||
return fmt.Errorf("unable to render volume percent texture: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AudioDevices) Destroy() error {
|
||||
gl.DeleteTextures(1, &p.title.ID)
|
||||
gl.DeleteTextures(1, &p.prompt.ID)
|
||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||
gl.DeleteTextures(1, &p.VolumeLabel.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
@@ -53,13 +53,16 @@ func (p *LogViewerPage) Init(ctx *PageCtx) error {
|
||||
// wrapped := wrapText(formatted, 100)
|
||||
// p.lines = append(p.lines, strings.Split(wrapped, "\n")...)
|
||||
// }
|
||||
displayLevels := []string{"WARNING", "ERROR", "FATAL"}
|
||||
|
||||
for _, entry := range logs {
|
||||
if slices.Contains(displayLevels, entry.Level) {
|
||||
logTime := time.UnixMilli(entry.Timestamp)
|
||||
timeStr := logTime.Format("2006-01-02 15:04:05")
|
||||
timeStr := logTime.Format("2006-01-02 15:04")
|
||||
line := fmt.Sprintf("[%s] [%s] %s", timeStr, entry.Level, entry.Message)
|
||||
p.lines = append(p.lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
applicationFont := "Roboto-Regular"
|
||||
|
||||
|
||||
@@ -81,9 +81,15 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
||||
p.isSearchActive = false
|
||||
p.scrollTrackWidth = 30
|
||||
p.scrollTrackX = p.ScreenWidth - p.scrollTrackWidth
|
||||
var err error
|
||||
|
||||
// Syncronize proclamations first
|
||||
sync.SyncProclamations(ctx.DbPtr)
|
||||
var allProcs []db.Proclamation
|
||||
allProcs, err = sync.SyncProclamations(ctx.DbPtr)
|
||||
if err != nil {
|
||||
// sync failed, fallback to database
|
||||
ctx.DbPtr.Find(&allProcs)
|
||||
}
|
||||
|
||||
fm := fontManager.Get()
|
||||
font, _ := fm.GetFont("Roboto-Regular")
|
||||
@@ -108,7 +114,7 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
||||
|
||||
// Load current selections
|
||||
var selected []db.Proclamation
|
||||
p.Context.DbPtr.Where("selected = ?", true).Where("id != ?", defaultProclamationID).Find(&selected)
|
||||
ctx.DbPtr.Where("selected = ?", true).Where("id != ?", defaultProclamationID).Find(&selected)
|
||||
for _, s := range selected {
|
||||
if s.ID == defaultProclamationID {
|
||||
logger.Debug("Found default proclamation in selected list: %s", s.ID)
|
||||
@@ -116,13 +122,6 @@ func (p *ProclamationOptions) Init(ctx *PageCtx) error {
|
||||
p.activeMap[s.ID] = true
|
||||
}
|
||||
|
||||
// Load ALL proclamations into a lookup map for SearchText population
|
||||
var allProcs []db.Proclamation
|
||||
if err := p.Context.DbPtr.Where("id != ?", defaultProclamationID).Find(&allProcs).Error; err != nil {
|
||||
logger.Error("Failed to fetch proclamations: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
procContentMap := make(map[string]string, len(allProcs))
|
||||
for _, pr := range allProcs {
|
||||
if pr.ID == defaultProclamationID {
|
||||
@@ -496,7 +495,6 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error {
|
||||
item := p.allItems[p.highlightIndex]
|
||||
if !item.IsCategory { // If the item is NOT a Category
|
||||
p.activeMap[item.ID] = !p.activeMap[item.ID] // Toggle it from the 'selected' list
|
||||
logger.Debug("Select/Deselect Proclamation: %s %s", item.Label, item.ID)
|
||||
} else if !item.IsSubCategory {
|
||||
p.toggleExpansion(p.highlightIndex)
|
||||
}
|
||||
|
||||
+171
-226
@@ -1,59 +1,3 @@
|
||||
// ATTENTION: TED
|
||||
//
|
||||
// # Integrated Multi-Domain Rhema Wave Generation System
|
||||
//
|
||||
// This module implements the complete Rhema Wave generation system by synchronizing multiple
|
||||
// subsystems that operate simultaneously across spatial, temporal, and chromatic
|
||||
// domains to produce a unified frequency-structured visual output for subliminal proclamation display.
|
||||
//
|
||||
// The technology addresses the fundamental challenge of presenting textual proclamation content
|
||||
// in a format optimized for subconscious processing rather than conscious reading. Conventional
|
||||
// subliminal text systems use isolated techniques (simple masking, single-frequency flashing, or
|
||||
// uniform scrolling) that operate in only one domain. Our system integrates four mathematically-
|
||||
// synchronized subsystems, each generating Fibonacci-based patterns in different domains, creating
|
||||
// a compound frequency signature designed to enhance subconscious absorption while maintaining
|
||||
// conscious illegibility.
|
||||
//
|
||||
// The system architecture integrates:
|
||||
//
|
||||
// 1. Spatial Character Transformation - Each rendered character undergoes Fibonacci-based
|
||||
// block scrambling (described separately), creating local spatial frequency patterns at
|
||||
// the character level.
|
||||
//
|
||||
// 2. Spatial Line Distribution - Text lines contain varying word counts following an oscillating
|
||||
// Fibonacci sequence (1, 1, 2, 3, 5, 8, 13... then reversing), creating a macro-level
|
||||
// two-dimensional wave pattern in the display geometry and ensuring balanced temporal
|
||||
// exposure of proclamation content segments.
|
||||
//
|
||||
// 3. Temporal Color Modulation - Sequential words receive colors from an intent-based palette,
|
||||
// with color duration determined by an oscillating Fibonacci sequence, creating rhythmic
|
||||
// chromatic variation synchronized with natural mathematical progression.
|
||||
//
|
||||
// 4. Temporal Velocity Modulation - Display scroll velocity oscillates between negative and
|
||||
// positive extremes with acceleration/deceleration rates and hold periods, creating a
|
||||
// "breathing" temporal rhythm for the entire visual field.
|
||||
//
|
||||
// The key innovation lies in the temporal and mathematical synchronization of these subsystems.
|
||||
// All four operate on Fibonacci-derived parameters, creating harmonic relationships between the
|
||||
// spatial frequencies (character scrambling, line wave patterns), chromatic frequencies (color
|
||||
// duration oscillation), and temporal frequencies (scroll velocity modulation). This multi-domain
|
||||
// integration produces what we term the "Rhema Wave" - a compound frequency signature where
|
||||
// spatial, temporal, and chromatic patterns reinforce each other through mathematical harmony.
|
||||
//
|
||||
// Key differentiators from existing solutions:
|
||||
// - Integrates four distinct Fibonacci-based frequency generators in a single system
|
||||
// - Synchronizes pattern generation across spatial, temporal, and chromatic domains
|
||||
// - Creates compound frequency signatures rather than isolated single-domain effects
|
||||
// - Maintains character-specific and content-specific mathematical structure throughout
|
||||
// - Uses bidirectional oscillation across multiple subsystems for rhythmic coherence
|
||||
// - Combines macro-level (line patterns, velocity) and micro-level (character, color) modulation
|
||||
//
|
||||
// The system loads proclamation text content, processes it through all four subsystems simultaneously,
|
||||
// and generates a continuously scrolling display where every element - from individual character
|
||||
// pixels to overall scroll rhythm - contributes to the unified Rhema Wave output. The mathematical
|
||||
// relationships between subsystems create interference patterns and harmonic resonances designed
|
||||
// to enhance subconscious processing of the proclamation content while maintaining conscious
|
||||
// illegibility through the character scrambling subsystem.
|
||||
package page
|
||||
|
||||
import (
|
||||
@@ -77,7 +21,7 @@ import (
|
||||
const (
|
||||
defaultProclamationID string = "f3d42354-3197-45e4-9b08-be447f14386d"
|
||||
proclamationPlayCount int = 10
|
||||
defaultProclamationPlayCount int = 10
|
||||
defaultProclamationPlayCount int = 1
|
||||
)
|
||||
|
||||
type Line struct {
|
||||
@@ -85,6 +29,15 @@ type Line struct {
|
||||
width int32
|
||||
}
|
||||
|
||||
// proclamationStream tracks one proclamation and how many full loops it should play.
|
||||
type proclamationStream struct {
|
||||
name string
|
||||
words []string
|
||||
repeat int
|
||||
completed int
|
||||
wordIndex int
|
||||
}
|
||||
|
||||
// LineDeque is a double-ended queue for Line values backed by a circular buffer.
|
||||
// It avoids O(n) prepend and prevents capacity leaks from head-of-slice removal.
|
||||
type LineDeque struct {
|
||||
@@ -171,8 +124,9 @@ func (d *LineDeque) Clear() {
|
||||
type ScrollerPage struct {
|
||||
Base
|
||||
|
||||
words []string
|
||||
wordIndex int
|
||||
streams []proclamationStream
|
||||
streamIndex int
|
||||
|
||||
lines *LineDeque
|
||||
lineHeight int32
|
||||
shader *shaderManager.Shader
|
||||
@@ -185,6 +139,9 @@ type ScrollerPage struct {
|
||||
scrollY float32
|
||||
fontScale float32
|
||||
|
||||
// Reuse one GL texture per unique word.
|
||||
wordTextureCache map[string]StringItem
|
||||
|
||||
increment float32
|
||||
velocity float32
|
||||
|
||||
@@ -214,7 +171,7 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
||||
return fmt.Errorf("unable to fetch font 'Scrambled'")
|
||||
}
|
||||
p.font = font
|
||||
logger.Info("Font name: %s", font.Name)
|
||||
logger.Debug("Font name: %s", font.Name)
|
||||
|
||||
p.Base.Init(ctx)
|
||||
|
||||
@@ -224,16 +181,17 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
||||
return fmt.Errorf("text shader not found")
|
||||
}
|
||||
p.shader = textShader
|
||||
logger.Info("Shader Program ID: %d", textShader.ProgramID)
|
||||
logger.Info("p.shader Program ID: %d", p.shader.ProgramID)
|
||||
logger.Debug("Shader Program ID: %d", textShader.ProgramID)
|
||||
logger.Debug("p.shader Program ID: %d", p.shader.ProgramID)
|
||||
|
||||
// Load proclamation words from database
|
||||
p.wordTextureCache = make(map[string]StringItem, 256)
|
||||
|
||||
// Load proclamation streams from database
|
||||
if err := p.loadProclamations(); err != nil {
|
||||
logger.Error("Error loading proclamations: %v", err)
|
||||
p.Context.NavigateTo(PROCLAMATIONS)
|
||||
return nil
|
||||
}
|
||||
p.wordIndex = 0
|
||||
|
||||
// Get line pattern from database
|
||||
var err error
|
||||
@@ -256,25 +214,19 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
||||
}
|
||||
p.colorPattern = colorPattern.NewColorPattern(baseColors)
|
||||
|
||||
// Get font sizes from database (optimize: load once, cache as int32)
|
||||
// Match the working page's logic exactly:
|
||||
p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize)
|
||||
|
||||
// Font setup
|
||||
nLines := int(float32(p.ScreenHeight) / 32.0) // 32 is your target visual size
|
||||
p.fontScale = float32(32) / float32(p.Context.CfgPtr.StandardFontSize)
|
||||
nLines := int(float32(p.ScreenHeight) / 32.0)
|
||||
p.lines = NewLineDeque(nLines * 2)
|
||||
|
||||
// Calculate space width using the SCALE ratio, not the font size int
|
||||
// Calculate space width using the scale ratio.
|
||||
spaceGlyph, ok := fm.GetGlyph(font.Name, ' ')
|
||||
if !ok {
|
||||
return fmt.Errorf("failed to get space glyph")
|
||||
}
|
||||
p.spaceWidth = int32(float32(spaceGlyph.Advance) * p.fontScale)
|
||||
|
||||
// Get line height using the SCALE ratio
|
||||
if fm == nil {
|
||||
logger.Error("FontManager somehow ended up nil...")
|
||||
}
|
||||
// Get line height using the same scale ratio.
|
||||
tmpStrID, _, H, err := fm.NewCreateStringTexture(font.Name, "Mg", p.shader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create sample texture: %w", err)
|
||||
@@ -290,100 +242,153 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error {
|
||||
}
|
||||
|
||||
// Create initial lines
|
||||
for i := 0; i < nLines; i++ {
|
||||
for i := range nLines {
|
||||
if err := p.createLine(i, true); err != nil {
|
||||
return fmt.Errorf("failed to create initial line %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Audio setup
|
||||
audioDeviceName := p.Context.CfgPtr.SelectedAudioDevice
|
||||
|
||||
// Switch audio devices if needed
|
||||
if mixer.CurrentDevice() != audioDeviceName {
|
||||
if err := mixer.SwitchDevice(audioDeviceName); err != nil {
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
// Start audio playback
|
||||
|
||||
// selectedAudio := p.Context.CfgPtr.SelectedAudio
|
||||
|
||||
assetsDir := p.Context.CfgPtr.AssetsDir
|
||||
audioFile := filepath.Join(assetsDir, "audio/audio.wav")
|
||||
logger.Info("Attempting to play %s", audioFile)
|
||||
if err := mixer.Play(audioFile, true, p.Context.CfgPtr); err != nil {
|
||||
logger.Error("Mixer returned an error: %v", err) // ADD THIS
|
||||
logger.Error("Mixer returned an error: %v", err)
|
||||
}
|
||||
|
||||
currentDevice := mixer.CurrentDevice()
|
||||
if currentDevice == "" {
|
||||
currentDevice = "default"
|
||||
}
|
||||
_ = currentDevice
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadProclamations loads selected proclamations from the database
|
||||
// loadProclamations loads selected proclamations as stream units (not one giant word list).
|
||||
func (p *ScrollerPage) loadProclamations() error {
|
||||
p.words = []string{}
|
||||
p.streams = p.streams[:0]
|
||||
|
||||
// Get all selected proclamations from database
|
||||
var proclamations []db.Proclamation
|
||||
err := p.Context.DbPtr.Where("selected = ?", true).Find(&proclamations).Error
|
||||
if err != nil {
|
||||
var selected []db.Proclamation
|
||||
if err := p.Context.DbPtr.Where("selected = ?", true).Find(&selected).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var defaultProclamation db.Proclamation
|
||||
err = p.Context.DbPtr.Where("ID = ?", defaultProclamationID).Find(&defaultProclamation).Error
|
||||
if err != nil {
|
||||
if err := p.Context.DbPtr.Where("ID = ?", defaultProclamationID).First(&defaultProclamation).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build word list from selected, available proclamations
|
||||
defaultWords := strings.Fields(defaultProclamation.Content)
|
||||
|
||||
for _, proclamation := range proclamations {
|
||||
// Build interleaved stream:
|
||||
// each selected proclamation repeats N times, then default repeats once.
|
||||
for _, proclamation := range selected {
|
||||
// Prevent default proclamation from being treated as a normal selected proclamation.
|
||||
if proclamation.ID == defaultProclamationID {
|
||||
continue
|
||||
}
|
||||
|
||||
words := strings.Fields(proclamation.Content)
|
||||
|
||||
// Pre-allocate: 10x proclamation words + 1x default words
|
||||
batch := make([]string, 0, len(words)*10+len(defaultWords))
|
||||
for range 10 {
|
||||
logger.Debug("Adding %s", proclamation.Name)
|
||||
batch = append(batch, words...)
|
||||
}
|
||||
logger.Debug("Adding %s", defaultProclamation.Name)
|
||||
batch = append(batch, defaultWords...)
|
||||
|
||||
p.words = append(p.words, batch...)
|
||||
if len(words) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(p.words) == 0 {
|
||||
return fmt.Errorf("Failed to load proclamations, no proclamations selected!")
|
||||
p.streams = append(p.streams, proclamationStream{
|
||||
name: proclamation.Name,
|
||||
words: words,
|
||||
repeat: proclamationPlayCount, // 10
|
||||
})
|
||||
|
||||
if len(defaultWords) > 0 {
|
||||
p.streams = append(p.streams, proclamationStream{
|
||||
name: defaultProclamation.Name,
|
||||
words: defaultWords,
|
||||
repeat: 1, // play once between selected proclamations
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no non-default selected proclamations, at least play default.
|
||||
if len(p.streams) == 0 && len(defaultWords) > 0 {
|
||||
p.streams = append(p.streams, proclamationStream{
|
||||
name: defaultProclamation.Name,
|
||||
words: defaultWords,
|
||||
repeat: 1,
|
||||
})
|
||||
}
|
||||
|
||||
if len(p.streams) == 0 {
|
||||
return fmt.Errorf("failed to load proclamations: no proclamation words available")
|
||||
}
|
||||
|
||||
p.streamIndex = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
// createLine is a helper to generate a new line at a specific index
|
||||
// isInitial determines if this is during initialization (positions from top)
|
||||
// or dynamic creation (positions relative to existing lines)
|
||||
func (p *ScrollerPage) createLine(index int, isInitial bool) error {
|
||||
words := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
|
||||
// nextLineWords gets words for the current line and advances proclamation stream when needed.
|
||||
func (p *ScrollerPage) nextLineWords() []string {
|
||||
if len(p.streams) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := &p.streams[p.streamIndex]
|
||||
if len(s.words) == 0 {
|
||||
p.streamIndex = (p.streamIndex + 1) % len(p.streams)
|
||||
return p.nextLineWords()
|
||||
}
|
||||
|
||||
prevIdx := s.wordIndex
|
||||
words := p.activePattern.GetNextLine(s.words, &s.wordIndex, p.lineCtx)
|
||||
|
||||
// If word index wraps, one full proclamation pass completed.
|
||||
if s.wordIndex < prevIdx {
|
||||
s.completed++
|
||||
targetRepeats := s.repeat
|
||||
if targetRepeats <= 0 {
|
||||
targetRepeats = 1
|
||||
}
|
||||
logger.Debug("Now playing proclamation: %s", s.name)
|
||||
if s.completed >= targetRepeats {
|
||||
s.completed = 0
|
||||
s.wordIndex = 0
|
||||
p.streamIndex = (p.streamIndex + 1) % len(p.streams)
|
||||
}
|
||||
}
|
||||
|
||||
return words
|
||||
}
|
||||
|
||||
// getCachedWord returns a reusable textured StringItem for a word.
|
||||
func (p *ScrollerPage) getCachedWord(word string) (StringItem, error) {
|
||||
if cached, ok := p.wordTextureCache[word]; ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
base, err := NewStringItem(word, p.font, sdl.Color{R: 255, G: 255, B: 255, A: 255})
|
||||
if err != nil {
|
||||
return StringItem{}, fmt.Errorf("unable to create cached string item for word '%s': %w", word, err)
|
||||
}
|
||||
base.Scale(p.fontScale)
|
||||
|
||||
p.wordTextureCache[word] = base
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// buildLineFromWords builds a line using cached texture IDs and per-instance color/position.
|
||||
func (p *ScrollerPage) buildLineFromWords(words []string) (Line, error) {
|
||||
line := Line{words: []StringItem{}}
|
||||
|
||||
for _, word := range words {
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
|
||||
|
||||
cached, err := p.getCachedWord(word)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
|
||||
return Line{}, err
|
||||
}
|
||||
|
||||
item.Scale(p.fontScale)
|
||||
item := cached
|
||||
item.Color = p.colorPattern.Color()
|
||||
|
||||
if len(line.words) > 0 {
|
||||
line.width += p.spaceWidth
|
||||
@@ -392,18 +397,12 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error {
|
||||
line.width += item.W
|
||||
}
|
||||
|
||||
// Calculate horizontal position (centered)
|
||||
startX := p.ScreenCenterX - (line.width / 2)
|
||||
|
||||
// Calculate vertical position
|
||||
var baseY int32
|
||||
if isInitial {
|
||||
baseY = int32(p.scrollY) - int32(p.lineHeight)*int32(index)
|
||||
} else {
|
||||
baseY = 0 // Will be set by caller based on context
|
||||
return line, nil
|
||||
}
|
||||
|
||||
// Position all words in the line
|
||||
// positionLine sets X/Y for all words in a line.
|
||||
func (p *ScrollerPage) positionLine(line *Line, baseY int32) {
|
||||
startX := p.ScreenCenterX - (line.width / 2)
|
||||
for j := range line.words {
|
||||
if j == 0 {
|
||||
line.words[j].X = startX
|
||||
@@ -412,6 +411,25 @@ func (p *ScrollerPage) createLine(index int, isInitial bool) error {
|
||||
}
|
||||
line.words[j].Y = baseY
|
||||
}
|
||||
}
|
||||
|
||||
// createLine is a helper to generate a new line at a specific index.
|
||||
// isInitial determines if this is during initialization (positions from top)
|
||||
// or dynamic creation (positions relative to existing lines).
|
||||
func (p *ScrollerPage) createLine(index int, isInitial bool) error {
|
||||
words := p.nextLineWords()
|
||||
line, err := p.buildLineFromWords(words)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var baseY int32
|
||||
if isInitial {
|
||||
baseY = int32(p.scrollY) - int32(p.lineHeight)*int32(index)
|
||||
} else {
|
||||
baseY = 0
|
||||
}
|
||||
p.positionLine(&line, baseY)
|
||||
|
||||
if isInitial {
|
||||
p.lines.PushBack(line)
|
||||
@@ -463,10 +481,7 @@ func (p *ScrollerPage) Update(dt float32) error {
|
||||
if p.velocity > 0 {
|
||||
// Moving up: replace lines that go off top
|
||||
for p.lines.Len() > 0 && len(p.lines.Front().words) > 0 && p.lines.Front().words[0].Y > p.ScreenHeight {
|
||||
old := p.lines.PopFront()
|
||||
for _, word := range old.words {
|
||||
gl.DeleteTextures(1, &word.ID)
|
||||
}
|
||||
_ = p.lines.PopFront() // no texture delete; textures are shared via cache
|
||||
|
||||
if err := p.addLineAtBottom(); err != nil {
|
||||
return err
|
||||
@@ -480,10 +495,7 @@ func (p *ScrollerPage) Update(dt float32) error {
|
||||
break
|
||||
}
|
||||
|
||||
old := p.lines.PopBack()
|
||||
for _, word := range old.words {
|
||||
gl.DeleteTextures(1, &word.ID)
|
||||
}
|
||||
_ = p.lines.PopBack() // no texture delete; textures are shared via cache
|
||||
|
||||
if err := p.addLineAtTop(); err != nil {
|
||||
return err
|
||||
@@ -494,32 +506,14 @@ func (p *ScrollerPage) Update(dt float32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// addLineAtBottom creates and adds a new line at the bottom of the display
|
||||
// addLineAtBottom creates and adds a new line at the bottom of the display.
|
||||
func (p *ScrollerPage) addLineAtBottom() error {
|
||||
newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
|
||||
line := Line{words: []StringItem{}}
|
||||
|
||||
for _, word := range newWords {
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
|
||||
newWords := p.nextLineWords()
|
||||
line, err := p.buildLineFromWords(newWords)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
|
||||
return err
|
||||
}
|
||||
|
||||
item.Scale(p.fontScale)
|
||||
|
||||
if len(line.words) > 0 {
|
||||
line.width += p.spaceWidth
|
||||
}
|
||||
line.words = append(line.words, item)
|
||||
line.width += item.W
|
||||
}
|
||||
|
||||
startX := p.ScreenCenterX - (line.width / 2)
|
||||
|
||||
// Position relative to last existing line
|
||||
var baseY int32
|
||||
if p.lines.Len() > 0 && len(p.lines.Back().words) > 0 {
|
||||
baseY = p.lines.Back().words[0].Y - int32(p.lineHeight)
|
||||
@@ -527,45 +521,19 @@ func (p *ScrollerPage) addLineAtBottom() error {
|
||||
baseY = -int32(p.lineHeight)
|
||||
}
|
||||
|
||||
for j := range line.words {
|
||||
if j == 0 {
|
||||
line.words[j].X = startX
|
||||
} else {
|
||||
line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth
|
||||
}
|
||||
line.words[j].Y = baseY
|
||||
}
|
||||
|
||||
p.positionLine(&line, baseY)
|
||||
p.lines.PushBack(line)
|
||||
return nil
|
||||
}
|
||||
|
||||
// addLineAtTop creates and adds a new line at the top of the display
|
||||
// addLineAtTop creates and adds a new line at the top of the display.
|
||||
func (p *ScrollerPage) addLineAtTop() error {
|
||||
newWords := p.activePattern.GetNextLine(p.words, &p.wordIndex, p.lineCtx)
|
||||
line := Line{words: []StringItem{}}
|
||||
|
||||
for _, word := range newWords {
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
item, err := NewStringItem(word, p.font, p.colorPattern.Color())
|
||||
newWords := p.nextLineWords()
|
||||
line, err := p.buildLineFromWords(newWords)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create string item for word '%s': %w", word, err)
|
||||
return err
|
||||
}
|
||||
|
||||
item.Scale(p.fontScale)
|
||||
|
||||
if len(line.words) > 0 {
|
||||
line.width += p.spaceWidth
|
||||
}
|
||||
line.words = append(line.words, item)
|
||||
line.width += item.W
|
||||
}
|
||||
|
||||
startX := p.ScreenCenterX - (line.width / 2)
|
||||
|
||||
// Position relative to first existing line
|
||||
var baseY int32
|
||||
if p.lines.Len() > 0 && len(p.lines.Front().words) > 0 {
|
||||
baseY = p.lines.Front().words[0].Y + int32(p.lineHeight)
|
||||
@@ -573,15 +541,7 @@ func (p *ScrollerPage) addLineAtTop() error {
|
||||
baseY = p.ScreenHeight
|
||||
}
|
||||
|
||||
for j := range line.words {
|
||||
if j == 0 {
|
||||
line.words[j].X = startX
|
||||
} else {
|
||||
line.words[j].X = line.words[j-1].X + line.words[j-1].W + p.spaceWidth
|
||||
}
|
||||
line.words[j].Y = baseY
|
||||
}
|
||||
|
||||
p.positionLine(&line, baseY)
|
||||
p.lines.PushFront(line)
|
||||
return nil
|
||||
}
|
||||
@@ -608,40 +568,25 @@ func (p *ScrollerPage) Destroy() error {
|
||||
|
||||
mixer.Stop(1000, p.Context.CfgPtr)
|
||||
|
||||
// GL cleanup — delete all text textures
|
||||
if p.lines != nil {
|
||||
for i := 0; i < p.lines.Len(); i++ {
|
||||
line := p.lines.Get(i)
|
||||
for j := range line.words {
|
||||
if id := line.words[j].ID; id != 0 {
|
||||
gl.DeleteTextures(1, &line.words[j].ID)
|
||||
line.words[j].ID = 0
|
||||
}
|
||||
}
|
||||
// Delete each cached word texture exactly once.
|
||||
for k, item := range p.wordTextureCache {
|
||||
if item.ID != 0 {
|
||||
id := item.ID
|
||||
gl.DeleteTextures(1, &id)
|
||||
}
|
||||
delete(p.wordTextureCache, k)
|
||||
}
|
||||
|
||||
gl.Flush()
|
||||
|
||||
// Drop references for GC safely
|
||||
if p.lines != nil {
|
||||
p.lines = nil
|
||||
}
|
||||
if p.words != nil {
|
||||
p.words = nil
|
||||
}
|
||||
if p.lineCtx != nil {
|
||||
p.streams = nil
|
||||
p.lineCtx = nil
|
||||
}
|
||||
if p.font != nil {
|
||||
p.font = nil
|
||||
}
|
||||
if p.shader != nil {
|
||||
p.shader = nil
|
||||
}
|
||||
if p.colorPattern != nil {
|
||||
p.colorPattern = nil
|
||||
}
|
||||
p.wordTextureCache = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -662,7 +607,7 @@ func (p *ScrollerPage) SetAutoVelocity(enabled bool, floor, ceiling, step, delay
|
||||
p.velocity = p.velocityFloor
|
||||
}
|
||||
|
||||
// updateAutoVelocity handles automatic velocity changes
|
||||
// updateAutoVelocity handles automatic velocity changes.
|
||||
func (p *ScrollerPage) updateAutoVelocity(dt float32) {
|
||||
if !p.autoVelocity {
|
||||
return
|
||||
|
||||
@@ -205,6 +205,7 @@ func (p *WiFiSetupPage) handlePasswordInput(sym sdl.Keycode, mod uint16) {
|
||||
p.connectionError = ""
|
||||
p.connectionAttemptTime = time.Now()
|
||||
|
||||
|
||||
err := p.networkManager.Connect(*p.wifiDevice, *p.selectedAP, p.passwordInput)
|
||||
if err != nil {
|
||||
p.isConnecting = false
|
||||
@@ -383,7 +384,7 @@ func (p *WiFiSetupPage) Update(dt float32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleConnectionStatusUpdate checks if connection succeeded or failed
|
||||
// handleConnectionStatusUpdate CHECKS0 if connection succeeded or failed
|
||||
func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionStatus) {
|
||||
if !p.isConnecting {
|
||||
return
|
||||
@@ -411,6 +412,7 @@ func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionS
|
||||
// INCREASE THIS: 5 seconds is way too aggressive.
|
||||
// Give it at least 20 seconds before assuming the password was wrong.
|
||||
if time.Since(p.connectionAttemptTime) > 20*time.Second {
|
||||
if time.Since(p.connectionAttemptTime) > 15*time.Second {
|
||||
p.isConnecting = false
|
||||
p.connectionError = "Connection failed - Check password or signal"
|
||||
p.isEnteringPassword = true
|
||||
@@ -419,42 +421,43 @@ func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getActiveSSID queries NetworkManager for the currently active WiFi SSID
|
||||
// getAiveSSID queries NetworkManager for the currently active WiFi SSID
|
||||
func (p *WiFiSetupPage) getActiveSSID() string {
|
||||
if p.networkManager == nil || p.wifiDevice == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
nmObj := p.networkManager.Conn().Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
|
||||
prop, err := nmObj.GetProperty("org.freedesktop.NetworkManager.ActiveConnections")
|
||||
conn := p.networkManager.Conn()
|
||||
|
||||
devObj := conn.Object("org.freedesktop.NetworkManager", p.wifiDevice.Path)
|
||||
activeApProp, err := devObj.GetProperty("org.freedesktop.NetworkManager.Device.Wireless.ActiveAccessPoint")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if paths, ok := prop.Value().([]dbus.ObjectPath); ok {
|
||||
for _, path := range paths {
|
||||
acObj := p.networkManager.Conn().Object("org.freedesktop.NetworkManager", path)
|
||||
devProp, err := acObj.GetProperty("org.freedesktop.NetworkManager.Connection.Active.Devices")
|
||||
if err == nil {
|
||||
if devPaths, ok := devProp.Value().([]dbus.ObjectPath); ok {
|
||||
for _, devPath := range devPaths {
|
||||
if string(devPath) == string(p.wifiDevice.Path) {
|
||||
idProp, err := acObj.GetProperty("org.freedesktop.NetworkManager.Connection.Active.Id")
|
||||
if err == nil {
|
||||
if id, ok := idProp.Value().(string); ok {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
activeApPath, ok := activeApProp.Value().(dbus.ObjectPath)
|
||||
if !ok || activeApPath == "/" || activeApPath == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
apObj := conn.Object("org.freedesktop.NetworkManager", activeApPath)
|
||||
ssidProp, err := apObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Ssid")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
ssidBytes, ok := ssidProp.Value().([]byte)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
logger.Info("Active SSID: %s", string(ssidBytes))
|
||||
|
||||
return string(ssidBytes)
|
||||
}
|
||||
|
||||
// refreshNetworkColors updates the colors of network items based on active status
|
||||
func (p *WiFiSetupPage) refreshNetworkColors() {
|
||||
activeSSID := p.getActiveSSID()
|
||||
|
||||
+31
-10
@@ -4,6 +4,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"rhemawavetech.com/rhemawave/internal/db"
|
||||
"rhemawavetech.com/rhemawave/internal/logger"
|
||||
"rhemawavetech.com/rhemawave/internal/netreq"
|
||||
@@ -32,6 +33,7 @@ func SyncProclamations(database *gorm.DB) ([]db.Proclamation, error) {
|
||||
|
||||
var storedProclamationSettings []db.Proclamation
|
||||
database.Select("ID", "Selected").Find(&storedProclamationSettings)
|
||||
|
||||
// [ID]Selected
|
||||
stash := make(map[string]bool)
|
||||
for _, row := range storedProclamationSettings {
|
||||
@@ -43,20 +45,39 @@ func SyncProclamations(database *gorm.DB) ([]db.Proclamation, error) {
|
||||
}
|
||||
|
||||
err = database.Transaction(func(tx *gorm.DB) error {
|
||||
// Clear the database
|
||||
// Where "1 = 1" because batch Delete() requires a condition
|
||||
// 'Where "1 = 1"' is like saying "while true be true" and tricks GORM into deleting everything
|
||||
if err := tx.Where("1 = 1").Delete(db.Proclamation{}).Error; err != nil {
|
||||
ids := make([]string, len(proclamations))
|
||||
for i, p := range proclamations {
|
||||
ids[i] = p.ID
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
// API returned nothing → DB should be empty
|
||||
if err := tx.Where("1 = 1").Delete(&db.Proclamation{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Then re-populate the database
|
||||
if err := tx.Save(&proclamations).Error; err != nil {
|
||||
} else {
|
||||
// Remove anything not in API
|
||||
if err := tx.Where("id NOT IN ?", ids).Delete(&db.Proclamation{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// Old sync code
|
||||
// if err := tx.Omit("selected").Save(&proclamations).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
}
|
||||
|
||||
// Upsert (preserve Selected automatically)
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"name",
|
||||
"content",
|
||||
"solfeggio_frequency_id",
|
||||
"category_id",
|
||||
"available",
|
||||
"price",
|
||||
}),
|
||||
}).Create(&proclamations).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Return nil as err for this statement, does not exit the function
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user