import crate_web/browser import crate_web/effects.{load_shelf} import crate_web/model.{type Model, Crate, LoggedOut, Model, blank_form} import crate_web/msg.{type Msg, GotEntry, OnRouteChange} import crate_web/prefs import crate_web/route import crate_web/update.{update} import crate_web/view.{view} import gleam/dict import gleam/list import gleam/option.{type Option, None, Some} import gleam/result import gleam/string import gleam/uri.{type Uri} import lustre import lustre/effect.{type Effect} import modem pub fn main() -> Nil { let app = lustre.application(init, update, view) let assert Ok(_) = lustre.start(app, "#app", Nil) Nil } fn init(_flags) -> #(Model, Effect(Msg)) { let login_error = oauth_error() let active_route = case modem.initial_uri() { Ok(uri) -> route.parse(uri) Error(_) -> Crate } let model = Model( auth: LoggedOut, route: active_route, login_handle: "", login_suggestions: [], login_error:, view: "owned", display: initial_display(), theme: initial_theme(), crates: dict.new(), crate_window: model.crate_window_size, crate_cursor: None, shelf_loading_more: False, via_handles: dict.new(), form: blank_form(), form_error: None, notice: None, busy: True, pending_revert: None, selected: None, editing: False, confirm_remove: False, entry_detail: model.EntryDetailLoading, discogs: model.blank_discogs(), scan: model.blank_scan(), amend_activity: model.AmendIdle, amend_error: None, amend: model.blank_amend(), avatar: None, browse: [], browse_loading: active_route == model.Browse, browse_cursor: None, browse_loading_more: False, browse_adding: None, browse_add_error: None, browse_query: "", browse_genre: "", browse_filters_open: False, inbox: model.InboxLoading, confirm_logout: False, confirm_apply: None, ignored_proposals: initial_ignored_proposals(), overlap: None, pressing: model.PressingLoading, feed: model.FeedLoading, feed_generation: 0, connections: model.ConnectionsLoading, connections_generation: 0, connection_write_generation: 0, connection_pending: None, connections_loading_more: False, feed_entries: dict.new(), feed_loading_more: False, nav_depth: 0, ) // modem.init wires up URL routing; clear_query drops a lingering callback ?error=. let routing = modem.init(on_url_change) // Landing directly on /add or /browse (e.g. back from the Discogs callback, // or a shared link) still needs its per-route data, since no OnRouteChange // fires for the initial route. let route_effects = case active_route { model.Add -> [effects.discogs_status()] model.Browse -> [effects.load_browse()] model.Feed -> [ effects.load_feed(model.FeedRequest(model.Feed, 0, None)), ] model.ConnectionsFollowing | model.ConnectionsFollowers -> [ effects.load_connections(model.ConnectionsRequest(active_route, 0, None)), ] model.EditInbox -> [effects.load_edit_inbox()] model.PublicCrate(handle) -> [effects.load_shelf(Some(handle), "current")] model.PublicRecord(handle, entry_id) -> [ effects.load_entry(Some(handle), entry_id, GotEntry), ] // Nothing's in `model.browse` yet at boot, so a direct load always fetches. model.PressingDetail(did, rkey) -> [ effects.load_pressing(model.release_uri(did, rkey)), ] // A direct load of Record needs its own detail fetch, since OnRouteChange // never fires for the initial route. model.Record(entry_id) -> [effects.load_entry(None, entry_id, GotEntry)] _ -> [] } let startup = case login_error { Some(_) -> effect.batch([ routing, load_shelf(None, "owned"), clear_query(), ..route_effects ]) None -> effect.batch([routing, load_shelf(None, "owned"), ..route_effects]) } #(model, startup) } fn on_url_change(uri: Uri) -> Msg { OnRouteChange(route.parse(uri)) } /// The raw `error` code from a failed OAuth callback redirect, mapped to copy /// on the signed-out page. None when there's no such query param. fn oauth_error() -> Option(String) { case uri.parse_query(string.drop_start(browser.current_search(), 1)) { Ok(params) -> case list.key_find(params, "error") { Ok(err) -> Some(err) Error(Nil) -> None } Error(Nil) -> None } } fn clear_query() -> Effect(Msg) { effect.from(fn(_dispatch) { browser.clear_query() }) } /// The persisted crate display mode, read synchronously at init so it's /// already correct in the first render (no flash of the Grid default). fn initial_display() -> model.Display { prefs.get(prefs.display_key) |> result.map(model.display_from_string) |> result.unwrap(model.Grid) } fn initial_theme() -> model.Theme { prefs.get(prefs.theme_key) |> result.map(model.theme_from_string) |> result.unwrap(model.System) } /// The persisted locally-ignored edit-proposal uri+cid pairs, same /// synchronous-at-init reasoning as `initial_display`. fn initial_ignored_proposals() -> List(#(String, String)) { prefs.get(prefs.ignored_proposals_key) |> result.map(prefs.decode_ignored_proposals) |> result.unwrap([]) }