using Inversion; using Spry; using Spry.Authentication; using Statum; using ClankerSaddle.Services; namespace ClankerSaddle { /** * The shipped Spry change-own-password action with this app's * post-change destination (the profile page) - the form fields and * verification (current/new/confirm) are Spry's. */ public class ChangeMyPasswordAction : Spry.Actions.ChangePasswordAction { protected override string login_uri { get { return "/login"; } } } /** * The public shape of the DEVICE-scope `ui` slot (goal 41 task 2): * the theme + colour mode the client applies to * `document.documentElement` (`data-theme`/`data-mode`). Plain * strings on purpose — the slot is cached client-side across * sessions, so it must stay readable by old frames. */ public class UiSlotPublic : Object { /** The Spry theme slug ("" = the core default). */ public string theme { get; set; default = ""; } /** auto | light | dark. */ public string mode { get; set; default = MODE_DEFAULT; } } /** * Saves the account's appearance choice (goal 41 task 2): the * form on /profile posts `theme` (the Spry core default "" or one * of the seven shipped theme slugs) and `mode` * (auto|light|dark). The choice is persisted per user in * `user_preferences` AND written to the DEVICE-scope `ui` slot — * the client's theme hook reads that slot on init and after every * statum render, so the new look applies immediately and survives * sessions on this device. */ public class SetThemeAction : StatumAction { /** The seven optional themes shipped with Spry 0.3. */ public const string[] THEME_SLUGS = { "airy", "dwarf", "formal", "glassy", "h2o", "living-sky", "modern" }; /** The three colour modes. */ public const string[] MODES = { "auto", "light", "dark" }; /** The DEVICE-scope slot type the client theme hook reads. */ public const string UI_SLOT_TYPE = "ui"; private UserService users = inject(); private UserPreferenceService preferences = inject(); /** True when `slug` names a theme the client may apply. */ public static bool is_valid_theme(string slug) { if (slug == "" || slug == "core") { return true; } foreach (string candidate in THEME_SLUGS) { if (candidate == slug) { return true; } } return false; } /** True when `mode` is a colour mode the client may apply. */ public static bool is_valid_mode(string mode) { foreach (string candidate in MODES) { if (candidate == mode) { return true; } } return false; } public override async DirectiveBuilder handle() throws GLib.Error { var guard = yield SpryAuth.require_login(directives(), request, users); if (guard != null) { return (!)guard; } string theme = Forms.field(request, "theme"); string mode = Forms.field(request, "mode"); if (mode.length == 0) { mode = MODE_DEFAULT; } if (!is_valid_theme(theme)) { return directives().notify("error", "Unknown theme '" + theme + "' - pick one of the themes offered below"); } if (!is_valid_mode(mode)) { return directives().notify("error", "Unknown colour mode '" + mode + "' - use auto, light or dark"); } var me = SpryAuth.current(request); try { yield preferences.set_theme(me.user_id, theme); yield preferences.set_mode(me.user_id, mode); var slot = new UiSlotPublic() { theme = theme, mode = mode }; return directives() .set_typed(UI_SLOT_TYPE, Statum.Scope.DEVICE, slot) .notify("success", "Appearance saved — the new theme applies immediately"); } catch (Error e) { return directives().notify("error", @"Could not save the appearance choice: $(e.message)"); } } } }