feat(profile): keep_screen_awake user preference (default on)
New users column surfaced as an On/Off dropdown in the profile form. The form and API carry "true"/"false" strings with the theme-style ""-guard, so JSON clients that omit the field can't clobber it; anything else is rejected. GET/PUT /api/auth/profile expose keep_screen_awake alongside the other profile fields, and the reader route threads the preference into its template metadata. Integration tests pin the schema default, both values, omission semantics, and rejection — dedicated test user, never testuser.
This commit is contained in:
1 parent
bdb9132b85
commit
c645b8e207
12 files changed
+244
-67
No files matched your search
@@ -597,4 +597,5 @@ type Users struct {
|
||||
Timezone pgtype.Text `db:"timezone" json:"timezone"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
DefaultAudioSpeed float32 `db:"default_audio_speed" json:"default_audio_speed"`
|
||||
KeepScreenAwake bool `db:"keep_screen_awake" json:"keep_screen_awake"`
|
||||
}
|
||||
@@ -699,6 +699,7 @@ type Querier interface {
|
||||
// Update universal progress
|
||||
UpdateUniversalProgress(ctx context.Context, arg UpdateUniversalProgressParams) (ReadingProgress, error)
|
||||
UpdateUserDefaultAudioSpeed(ctx context.Context, arg UpdateUserDefaultAudioSpeedParams) error
|
||||
UpdateUserKeepScreenAwake(ctx context.Context, arg UpdateUserKeepScreenAwakeParams) error
|
||||
UpdateUserMaxDevices(ctx context.Context, arg UpdateUserMaxDevicesParams) (Users, error)
|
||||
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
|
||||
UpdateUserRole(ctx context.Context, arg UpdateUserRoleParams) (UpdateUserRoleRow, error)
|
||||
|
||||
@@ -9942,6 +9942,7 @@ SELECT
|
||||
u.updated_at,
|
||||
u.timezone,
|
||||
u.default_audio_speed,
|
||||
u.keep_screen_awake,
|
||||
(SELECT COUNT(*) FROM devices WHERE user_id = u.id) as device_count
|
||||
FROM users u
|
||||
WHERE u.id = $1
|
||||
@@ -9960,6 +9961,7 @@ type GetUserRow struct {
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
Timezone pgtype.Text `db:"timezone" json:"timezone"`
|
||||
DefaultAudioSpeed float32 `db:"default_audio_speed" json:"default_audio_speed"`
|
||||
KeepScreenAwake bool `db:"keep_screen_awake" json:"keep_screen_awake"`
|
||||
DeviceCount int64 `db:"device_count" json:"device_count"`
|
||||
}
|
||||
|
||||
@@ -9979,6 +9981,7 @@ func (q *Queries) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, erro
|
||||
&i.UpdatedAt,
|
||||
&i.Timezone,
|
||||
&i.DefaultAudioSpeed,
|
||||
&i.KeepScreenAwake,
|
||||
&i.DeviceCount,
|
||||
)
|
||||
return i, err
|
||||
@@ -17236,9 +17239,23 @@ func (q *Queries) UpdateUserDefaultAudioSpeed(ctx context.Context, arg UpdateUse
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateUserKeepScreenAwake = `-- name: UpdateUserKeepScreenAwake :exec
|
||||
UPDATE users SET keep_screen_awake = $2, updated_at = NOW() WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdateUserKeepScreenAwakeParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
KeepScreenAwake bool `db:"keep_screen_awake" json:"keep_screen_awake"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserKeepScreenAwake(ctx context.Context, arg UpdateUserKeepScreenAwakeParams) error {
|
||||
_, err := q.db.Exec(ctx, UpdateUserKeepScreenAwake, arg.ID, arg.KeepScreenAwake)
|
||||
return err
|
||||
}
|
||||
|
||||
const UpdateUserMaxDevices = `-- name: UpdateUserMaxDevices :one
|
||||
UPDATE users SET max_devices = $2, updated_at = NOW() WHERE id = $1
|
||||
RETURNING id, email, username, password_hash, first_name, last_name, role, theme, max_devices, created_at, timezone, updated_at, default_audio_speed
|
||||
RETURNING id, email, username, password_hash, first_name, last_name, role, theme, max_devices, created_at, timezone, updated_at, default_audio_speed, keep_screen_awake
|
||||
`
|
||||
|
||||
type UpdateUserMaxDevicesParams struct {
|
||||
@@ -17263,6 +17280,7 @@ func (q *Queries) UpdateUserMaxDevices(ctx context.Context, arg UpdateUserMaxDev
|
||||
&i.Timezone,
|
||||
&i.UpdatedAt,
|
||||
&i.DefaultAudioSpeed,
|
||||
&i.KeepScreenAwake,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ SELECT
|
||||
u.updated_at,
|
||||
u.timezone,
|
||||
u.default_audio_speed,
|
||||
u.keep_screen_awake,
|
||||
(SELECT COUNT(*) FROM devices WHERE user_id = u.id) as device_count
|
||||
FROM users u
|
||||
WHERE u.id = $1;
|
||||
@@ -515,6 +516,9 @@ UPDATE users SET timezone = $2, updated_at = NOW() WHERE id = $1;
|
||||
-- name: UpdateUserDefaultAudioSpeed :exec
|
||||
UPDATE users SET default_audio_speed = $2, updated_at = NOW() WHERE id = $1;
|
||||
|
||||
-- name: UpdateUserKeepScreenAwake :exec
|
||||
UPDATE users SET keep_screen_awake = $2, updated_at = NOW() WHERE id = $1;
|
||||
|
||||
-- Per-book playback speed preference (player plan §8).
|
||||
|
||||
-- name: GetAudioPlaybackSpeed :one
|
||||
|
||||
@@ -122,6 +122,8 @@ type UserProfile struct {
|
||||
Theme string `json:"theme,omitempty"`
|
||||
// User-level default audiobook playback speed (plan §8); 0 omits.
|
||||
DefaultAudioSpeed float64 `json:"default_audio_speed,omitempty"`
|
||||
// Whether the web reader/player should hold a Screen Wake Lock.
|
||||
KeepScreenAwake bool `json:"keep_screen_awake"`
|
||||
}
|
||||
|
||||
type ProfileUpdateRequest struct {
|
||||
@@ -132,6 +134,9 @@ type ProfileUpdateRequest struct {
|
||||
Theme string `json:"theme,omitempty" form:"theme" validate:"omitempty"`
|
||||
Timezone string `json:"timezone,omitempty" form:"timezone" validate:"omitempty"`
|
||||
Role string `json:"role,omitempty" form:"role" validate:"omitempty,oneof=user admin"`
|
||||
// "true"/"false" — the profile dropdown always sends it; JSON clients
|
||||
// that omit it decode as "" and leave the column untouched.
|
||||
KeepScreenAwake string `json:"keep_screen_awake,omitempty" form:"keep_screen_awake" validate:"omitempty,oneof=true false"`
|
||||
// Per the SPEEDS list bounds; the profile select always sends it,
|
||||
// JSON clients omit it (0 = untouched).
|
||||
DefaultAudioSpeed float64 `json:"default_audio_speed,omitempty" form:"default_audio_speed" validate:"omitempty,min=0.25,max=4"`
|
||||
@@ -542,6 +547,7 @@ func (h *AuthHandler) GetProfile(c *echo.Context) error {
|
||||
Role: user.Role,
|
||||
Theme: user.Theme.String,
|
||||
DefaultAudioSpeed: float64(user.DefaultAudioSpeed),
|
||||
KeepScreenAwake: user.KeepScreenAwake,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -706,6 +712,22 @@ func (h *AuthHandler) UpdateProfile(c *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep-screen-awake preference. The form dropdown always sends it
|
||||
// ("true"/"false"); JSON clients that omit it decode as "" and leave
|
||||
// the column untouched.
|
||||
if req.KeepScreenAwake != "" {
|
||||
if req.KeepScreenAwake != "true" && req.KeepScreenAwake != "false" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "keep_screen_awake must be 'true' or 'false'"})
|
||||
}
|
||||
err := h.db.UpdateUserKeepScreenAwake(c.Request().Context(), database.UpdateUserKeepScreenAwakeParams{
|
||||
ID: targetUserUUID,
|
||||
KeepScreenAwake: req.KeepScreenAwake == "true",
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
// Browser path (htmx modal + profile page): refresh so every rendered
|
||||
// value (timezone select, table rows, header) reflects the save and the
|
||||
// modal closes. A bare JSON body swapped into #profile-result read as
|
||||
|
||||
@@ -67,6 +67,7 @@ func getTemplateUserWithTheme(c *echo.Context, cfg *Config) (templates.User, err
|
||||
LastName: utils.PgTextString(userDB.LastName),
|
||||
CreatedAt: userDB.CreatedAt.Time,
|
||||
DefaultAudioSpeed: float64(userDB.DefaultAudioSpeed),
|
||||
KeepScreenAwake: userDB.KeepScreenAwake,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ func registerReaderRoutes(cfg *Config) {
|
||||
TotalCharacters: mediaItem.TotalCharacters.Int64,
|
||||
EstimatedPages: sync.EstimatedPages(mediaItem.TotalCharacters.Int64),
|
||||
TreatAsComic: handlers.ShouldTreatAsComic(libraryWithType.TypeName, mediaItem.FormatGroup),
|
||||
KeepScreenAwake: user.KeepScreenAwake,
|
||||
}
|
||||
// Render template
|
||||
var buf bytes.Buffer
|
||||
|
||||
Reference in new issue
Block a user