summaryrefslogtreecommitdiff
path: root/Hero/Core.hs
diff options
context:
space:
mode:
Diffstat (limited to 'Hero/Core.hs')
-rw-r--r--Hero/Core.hs939
1 files changed, 939 insertions, 0 deletions
diff --git a/Hero/Core.hs b/Hero/Core.hs
new file mode 100644
index 0000000..c11456d
--- /dev/null
+++ b/Hero/Core.hs
@@ -0,0 +1,939 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+
+module Hero.Core where
+
+import Alpha
+import qualified Clay
+import Data.Aeson
+ ( FromJSON (..),
+ ToJSON (..),
+ defaultOptions,
+ genericParseJSON,
+ genericToJSON,
+ )
+import Data.Data (Data, Typeable)
+import qualified Data.List as List
+import qualified Data.List.Split as List
+import Data.Proxy (Proxy (..))
+import Data.String
+import Data.String.Quote
+import Data.Text (Text, replace, toLower)
+import GHC.Generics (Generic)
+import qualified GHC.Show as Legacy
+import qualified Hero.Assets as Assets
+import Hero.Look as Look
+import Hero.Look.Typography
+import Miso
+import qualified Miso (for_)
+import Miso.Extend
+import Miso.String
+import Network.RemoteData
+import Servant.API
+ ( (:<|>) (..),
+ (:>),
+ )
+import qualified Servant.API as Api
+import Servant.Links (linkURI)
+
+-- | The css id for controling music in the comic player.
+audioId :: MisoString
+audioId = "audioSource"
+
+-- TODO: make ComicId a hashid
+-- https://hackage.haskell.org/package/hashids-1.0.2.4/docs/Web-Hashids.html
+newtype ComicId
+ = ComicId String
+ deriving
+ ( Show,
+ Eq,
+ Ord,
+ Data,
+ Typeable,
+ Generic,
+ ToMisoString,
+ IsString,
+ Api.ToHttpApiData,
+ Api.FromHttpApiData
+ )
+
+instance ToJSON ComicId where
+ toJSON = genericToJSON Data.Aeson.defaultOptions
+
+instance FromJSON ComicId where
+ parseJSON = genericParseJSON Data.Aeson.defaultOptions
+
+-- | Used for looking up images on S3, mostly
+comicSlug :: Comic -> Text
+comicSlug Comic {..} = snake comicName <> "-" <> comicIssue
+
+-- * user
+
+data User
+ = User
+ { userEmail :: Text,
+ userName :: Text,
+ userLibrary :: [Comic]
+ }
+ deriving (Show, Eq, Generic, Data, Ord)
+
+instance Semigroup User where
+ a <> b =
+ User
+ (userEmail a <> userEmail b)
+ (userName a <> userName b)
+ (userLibrary a <> userLibrary b)
+
+instance Monoid User where
+ mempty = User mempty mempty mempty
+
+instance ToJSON User where
+ toJSON = genericToJSON Data.Aeson.defaultOptions
+
+instance FromJSON User where
+ parseJSON = genericParseJSON Data.Aeson.defaultOptions
+
+-- | Class for rendering media objects in different ways.
+class IsMediaObject o where
+ -- | Render a thumbnail for use in a shelf, or otherwise.
+ thumbnail :: o -> View Action
+
+ -- | Render a featured banner.
+ feature :: o -> User -> View Action
+
+ -- | Media info view
+ info :: o -> User -> View Action
+
+-- | How much to Zoom the comic image
+type Magnification = Int
+
+-- | All the buttons.
+data Button
+ = Watch Comic
+ | Read Comic
+ | Save Comic User
+ | SaveIcon Comic User
+ | ZoomIcon Magnification Comic Page
+ | PlayPause MisoString AudioState
+ | Arrow Action
+
+-- | Class for defining general, widely used elements in the heroverse.
+class Elemental v where el :: v -> View Action
+
+-- TODO: what if I just did this on all actions?
+-- then I could e.g. `el $ ToggleAudio audioId audioState`
+instance Elemental Button where
+ el (PlayPause id model) =
+ button_
+ [ class_ "button is-large icon",
+ onClick $ ToggleAudio id
+ ]
+ [i_ [class_ $ "fa " <> icon] []]
+ where
+ icon = case model of
+ Paused -> "fa-play-circle"
+ Playing -> "fa-pause-circle"
+ el (Arrow act) =
+ button_
+ [class_ "button is-large turn-page", onClick act]
+ [img_ [src_ $ ms $ Assets.demo <> image <> ".png"]]
+ where
+ image = case act of
+ PrevPage -> "prev-page"
+ NextPage -> "next-page"
+ _ -> "prev-page"
+ el (Save c u) =
+ if c `elem` (userLibrary u) -- in library
+ then
+ a_
+ [class_ "wrs-button saved", onClick $ ToggleInLibrary c]
+ [ img_ [src_ $ ms $ Assets.icon <> "save.svg"],
+ span_ [] [text "saved"]
+ ]
+ else-- not in library
+
+ a_
+ [class_ "wrs-button", onClick $ ToggleInLibrary c]
+ [ img_ [src_ $ ms $ Assets.icon <> "save.svg"],
+ span_ [] [text "save"]
+ ]
+ el (SaveIcon c u) =
+ if c `elem` (userLibrary u) -- in library
+ then
+ button_
+ [ class_ "button is-large has-background-black",
+ onClick $ ToggleInLibrary c
+ ]
+ [img_ [src_ $ ms $ Assets.demo <> "library-add.png"]]
+ else-- not in library
+
+ button_
+ [ class_ "button is-large has-background-black-bis",
+ onClick $ ToggleInLibrary c
+ ]
+ [img_ [src_ $ ms $ Assets.demo <> "library-add.png"]]
+ el (ZoomIcon zmodel comic page) =
+ button_
+ [ id_ "zoom-button",
+ class_ "button is-large",
+ onClick $ ToggleZoom comic page
+ ]
+ [ img_ [src_ $ ms $ Assets.demo <> "zoom.png"],
+ input_
+ [ type_ "range",
+ min_ "0",
+ max_ "100",
+ disabled_ True,
+ value_ $ ms (show zmodel :: String),
+ class_ "ctrl",
+ id_ "zoom"
+ ],
+ label_
+ [class_ "ctrl", Miso.for_ "zoom"]
+ [text $ ms $ (show zmodel :: String) ++ "%"]
+ ]
+ el (Read c) =
+ a_
+ [class_ "wrs-button", onClick $ SelectExperience c]
+ [ img_ [src_ $ ms $ Assets.icon <> "read.svg"],
+ span_ [] [text "read"]
+ ]
+ el (Watch c) =
+ a_
+ [class_ "wrs-button", onClick $ StartWatching c]
+ [ img_ [src_ $ ms $ Assets.icon <> "watch.svg"],
+ span_ [] [text "watch"]
+ ]
+
+data AudioState = Playing | Paused
+ deriving (Show, Eq)
+
+data ComicReaderState
+ = NotReading
+ | Cover ComicId
+ | ChooseExperience ComicId Page
+ | Reading ComicReaderView ComicId Page
+ | Watching ComicId
+ deriving (Show, Eq)
+
+findComic :: ComicId -> [Comic] -> Maybe Comic
+findComic id = List.find (\c -> comicId c == id)
+
+-- | Main model for the app.
+--
+-- Try to prefix component-specific state with the component initials: 'd' for
+-- discover, 'cp' for comic player.
+data Model
+ = Model
+ { uri :: Api.URI,
+ appComics :: RemoteData MisoString [Comic],
+ user :: User,
+ dMediaInfo :: Maybe Comic,
+ cpState :: ComicReaderState,
+ cpAudioState :: AudioState,
+ magnification :: Magnification
+ }
+ deriving (Show, Eq)
+
+initModel :: Api.URI -> Model
+initModel uri_ =
+ Model
+ { uri = uri_,
+ appComics = NotAsked,
+ dMediaInfo = Nothing,
+ user = mempty,
+ cpState = detectPlayerState uri_,
+ cpAudioState = Paused,
+ magnification = 100
+ }
+
+-- | Hacky way to initialize the 'ComicReaderState' from the Api.URI.
+detectPlayerState :: Api.URI -> ComicReaderState
+detectPlayerState u = case List.splitOn "/" $ Api.uriPath u of
+ ["", "comic", id, pg, "experience"] -> ChooseExperience (ComicId id) (toPage pg)
+ ["", "comic", id, _, "video"] -> Watching $ ComicId id
+ ["", "comic", id, pg, "full"] -> Reading Full (ComicId id) (toPage pg)
+ ["", "comic", id, pg] -> Reading Spread (ComicId id) (toPage pg)
+ ["", "comic", id] -> Cover $ ComicId id
+ _ -> NotReading
+ where
+ toPage pg = fromMaybe 1 (readMaybe pg :: Maybe Page)
+
+type Page = Int
+
+data Action
+ = NoOp
+ | -- comic player stuff
+ SelectExperience Comic
+ | StartReading Comic
+ | StartWatching Comic
+ | NextPage
+ | PrevPage
+ | ToggleZoom Comic Page
+ | ToggleAudio MisoString
+ | FetchComics
+ | SetComics (RemoteData MisoString [Comic])
+ | ToggleFullscreen
+ | -- discover stuff
+ SetMediaInfo (Maybe Comic)
+ | ToggleInLibrary Comic
+ | -- login
+ ValidateUserPassword
+ | -- app stuff
+ ScrollIntoView MisoString
+ | HandleURI Api.URI
+ | ChangeURI Api.URI
+ | DumpModel
+ deriving (Show, Eq)
+
+type AppRoutes =
+ ComicCover
+ :<|> ComicReaderSpread
+ :<|> ComicReaderFull
+ :<|> ComicVideo
+ :<|> Discover
+ :<|> ChooseExperience
+
+handlers =
+ comicCover
+ :<|> comicReader
+ :<|> comicReader
+ :<|> comicReader
+ :<|> discover
+ :<|> comicReader
+
+routes :: Proxy AppRoutes
+routes = Proxy
+
+type PubRoutes =
+ Home
+ :<|> Login
+
+pubRoutes :: Proxy PubRoutes
+pubRoutes = Proxy
+
+-- * pages
+--
+-- TODO: consider making a typeclass, something like:
+--
+-- class Page name where
+-- type Route name :: View Action
+-- proxy :: Proxy name
+-- proxy = Proxy name
+-- view :: Model -> View Action
+-- link :: Api.URI
+
+-- * home
+--
+-- this is the unauthenticated page that you see when you first visit
+
+type Home =
+ View Action
+
+homeProxy :: Proxy Home
+homeProxy = Proxy
+
+homeLink :: Api.URI
+homeLink = linkURI $ Api.safeLink front homeProxy
+ where
+ front = Proxy :: Proxy Home
+
+home :: Model -> View Action
+home = login
+
+-- * login
+
+data LoginForm = LoginForm {loginEmail :: String, loginPass :: String}
+ deriving (Eq, Show, Read, Generic)
+
+instance ToJSON LoginForm
+
+instance FromJSON LoginForm
+
+type Login =
+ "login" :> View Action
+
+loginProxy :: Proxy Login
+loginProxy = Proxy
+
+loginLink :: Api.URI
+loginLink = linkURI $ Api.safeLink pubRoutes loginProxy
+
+login :: Model -> View Action
+login _ =
+ template
+ "login"
+ [ div_
+ [id_ "login-inner"]
+ [ img_
+ [ class_ fadeIn,
+ src_ $ ms $ Assets.cdnEdge <> "/old-assets/images/icons/hero-large.png"
+ ],
+ hr_ [class_ fadeIn],
+ form_
+ [class_ fadeIn]
+ [ ctrl [id_ "user", class_ "input", type_ "email", placeholder_ "Email"],
+ ctrl [id_ "pass", class_ "input", type_ "password", placeholder_ "Password"],
+ div_
+ [class_ "action", css euro]
+ [ div_
+ [class_ "checkbox remember-me"]
+ [ input_ [type_ "checkbox"],
+ label_ [Miso.for_ "checkbox"] [text "Remember Me"]
+ ],
+ div_
+ [class_ "button is-black", onClick ValidateUserPassword]
+ [text "Login"]
+ ]
+ ],
+ hr_ [class_ fadeIn],
+ p_
+ [class_ $ "help " <> fadeIn]
+ [ a_ [href_ "#"] [text "Forgot your username or password?"],
+ a_ [href_ "#"] [text "Don't have an account? Sign Up"]
+ ],
+ img_
+ [ id_ "hero-logo",
+ class_ "blur-out",
+ src_ $ ms $ Assets.cdnEdge <> "/old-assets/images/icons/success-her-image.png"
+ ]
+ ]
+ ]
+ where
+ fadeIn = "animated fadeIn delay-2s"
+ ctrl x = div_ [class_ "control"] [input_ x]
+
+-- * discover
+
+type Discover = "discover" :> View Action
+
+discoverLink :: Api.URI
+discoverLink = linkURI $ Api.safeLink routes discoverProxy
+
+discoverProxy :: Proxy Discover
+discoverProxy = Proxy
+
+discover :: Model -> View Action
+discover model@Model {user = u} =
+ template
+ "discover"
+ [ topbar,
+ main_ [id_ "app-body"] $ case appComics model of
+ NotAsked -> [loading]
+ Loading -> [loading]
+ Failure _ -> [nocomics]
+ Success [] -> [nocomics]
+ Success (comic : rest) ->
+ [ feature comic u,
+ shelf "Recent Releases" (comic : rest),
+ maybeView (`info` u) $ dMediaInfo model
+ ],
+ appmenu,
+ discoverFooter
+ ]
+
+discoverFooter :: View Action
+discoverFooter =
+ footer_
+ [ id_ "app-foot",
+ class_ "is-black"
+ ]
+ [ div_
+ [id_ "app-foot-social", css euro]
+ [ div_
+ [class_ "row is-marginless"]
+ [ smallImg "facebook.png" $ Just "https://www.facebook.com/musicmeetscomics",
+ smallImg "twitter.png" $ Just "https://twitter.com/musicmeetscomic",
+ smallImg "instagram.png" $ Just "https://www.instagram.com/musicmeetscomics/",
+ smallImg "spotify.png" $ Just "https://open.spotify.com/user/i4ntfg6ganjgxdsylinigcjlq?si=ymWsSkwsT9iaLw2LeAJNNg",
+ smallImg "youtube.png" $ Just "https://www.youtube.com/channel/UCnNPLiuJ1ueo1KTPgHDE7lA/"
+ ],
+ div_ [class_ "row"] [text "Team | Contact Us | Privacy Policy"]
+ ],
+ div_
+ [id_ "app-foot-quote", css euro]
+ [ p_ [] [text "With great power comes great responsiblity."],
+ p_ [] [text "-Stan Lee"]
+ ],
+ div_
+ [css euro, id_ "app-foot-logo", onClick DumpModel]
+ [ a_ [class_ "social-icon", href_ "#"] [img_ [src_ $ ms $ Assets.icon <> "hero-logo.svg"]],
+ span_ [] [text "© Hero Records, Inc. All Rights Reserved"]
+ ]
+ ]
+ where
+ attrs Nothing = [class_ "social-icon"]
+ attrs (Just lnk) = [class_ "social-icon", href_ lnk, target_ "_blank"]
+ smallImg x lnk =
+ a_
+ (attrs lnk)
+ [img_ [src_ $ ms $ Assets.cdnEdge <> "/old-assets/images/icons/" <> x]]
+
+-- * comic
+
+data Comic
+ = Comic
+ { comicId :: ComicId,
+ comicPages :: Integer,
+ comicName :: Text,
+ -- | Ideally this would be a dynamic number-like type
+ comicIssue :: Text,
+ comicDescription :: Text
+ }
+ deriving (Show, Eq, Generic, Data, Ord)
+
+instance ToJSON Comic where
+ toJSON = genericToJSON Data.Aeson.defaultOptions
+
+instance FromJSON Comic where
+ parseJSON = genericParseJSON Data.Aeson.defaultOptions
+
+instance IsMediaObject Comic where
+ thumbnail c@Comic {..} =
+ li_
+ []
+ [ a_
+ [ class_ "comic grow clickable",
+ id_ $ "comic-" <> ms comicId,
+ onClick $ SetMediaInfo $ Just c
+ ]
+ [ img_ [src_ $ ms $ Assets.demo <> comicSlug c <> ".png"],
+ span_ [] [text $ "Issue #" <> ms comicIssue],
+ span_ [] [text $ ms comicName]
+ ]
+ ]
+ feature comic lib =
+ div_
+ [id_ "featured-comic"]
+ [ img_
+ [ id_ "featured-banner",
+ src_ $ ms $ Assets.demo <> "feature-banner.png"
+ ],
+ div_
+ [id_ "featured-content"]
+ [ div_
+ [class_ "hero-original", css wide]
+ [ span_ [css thicc] [text "Herø"],
+ span_ [css euro] [text " Original"]
+ ],
+ div_
+ [class_ "comic-logo"]
+ [ img_
+ [ src_
+ $ ms
+ $ Assets.demo <> comicSlug comic <> "-logo.png"
+ ]
+ ],
+ div_ [class_ "comic-action-menu"] $
+ el <$> [Watch comic, Read comic, Save comic lib],
+ p_
+ [class_ "description"]
+ [ text . ms $ comicDescription comic
+ ]
+ ]
+ ]
+ info c@Comic {..} lib =
+ div_
+ [class_ "media-info", css euro]
+ [ div_
+ [class_ "media-info-meta"]
+ [ column [img_ [src_ $ ms $ Assets.demo <> "dmc-widethumb.png"]],
+ column
+ [ span_ [style_ title] [text $ ms comicName],
+ span_ [style_ subtitle] [text $ "Issue #" <> ms comicIssue],
+ span_ [] [text "Released: "],
+ span_ [] [text $ "Pages: " <> ms (show comicPages :: String)]
+ ]
+ ],
+ div_
+ [class_ "media-info-summary"]
+ [ p_
+ [style_ $ uppercase <> bold <> Look.expanded <> "font-size" =: ".8rem"]
+ [text "Summary"],
+ p_ [] [text $ ms comicDescription]
+ ],
+ div_ [class_ "media-info-actions"] $ el <$> [Save c lib, Read c, Watch c]
+ -- , row [ text "credits" ]
+ ]
+ where
+ title =
+ "color" =: "red" <> "font-size" =: "1.6rem" <> uppercase
+ <> "line-height"
+ =: "100%"
+ <> Look.condensed
+ <> bold
+ subtitle = "color" =: "#fff" <> "font-size" =: "1.2rem" <> bold <> Look.condensed
+
+type ComicCover =
+ "comic"
+ :> Api.Capture "comicId" ComicId
+ :> View Action
+
+comicProxy :: Proxy ComicCover
+comicProxy = Proxy
+
+comicCover :: ComicId -> Model -> View Action
+comicCover comicId_ = comicReader comicId_ 1
+
+comicLink :: ComicId -> Api.URI
+comicLink comicId_ = linkURI $ Api.safeLink routes comicProxy comicId_
+
+-- * chooseExperience
+
+type ChooseExperience =
+ "comic"
+ :> Api.Capture "id" ComicId
+ :> Api.Capture "page" Page
+ :> "experience"
+ :> View Action
+
+chooseExperienceProxy :: Proxy ChooseExperience
+chooseExperienceProxy = Proxy
+
+chooseExperienceLink :: ComicId -> Page -> Api.URI
+chooseExperienceLink id page =
+ linkURI $ Api.safeLink routes chooseExperienceProxy id page
+
+chooseExperiencePage :: Comic -> Page -> Model -> View Action
+chooseExperiencePage comic page model =
+ template
+ "choose-experience"
+ [ topbar,
+ main_
+ [id_ "app-body"]
+ [ h2_ [] [text "Choose Your Musical Experience"],
+ p_ [] [text experienceBlurb],
+ ul_ [] $ li comic </ experiences
+ ],
+ appmenu,
+ comicControls comic page model
+ ]
+ where
+ li c (name, artist, track) =
+ li_
+ [onClick $ StartReading c]
+ [ div_
+ []
+ [ img_ [src_ $ ms $ Assets.demo <> name <> ".png"],
+ span_ [] [text $ ms name]
+ ],
+ span_ [css thicc] [text $ ms artist],
+ span_ [] [text $ ms track]
+ ]
+ experiences :: [(Text, Text, Text)]
+ experiences =
+ [ ("comedic", "RxGF", "Soft Reveal"),
+ ("dark", "Logan Henderson", "Speak of the Devil"),
+ ("original", "Mehcad Brooks", "Stars"),
+ ("energetic", "Skela", "What's wrong with me"),
+ ("dramatic", "Josh Jacobson", "Sideline")
+ ]
+
+experienceBlurb :: MisoString
+experienceBlurb =
+ [s|
+As you enter the world of Hero, you will find that music and visual art have a
+symbiotic relationship that can only be experienced, not described. Here, choose
+the tonality of the experience you wish to adventure on, whether it's a comedic,
+dark, energetic or dramatic. Feeling indecisive? Let us navigate your journey
+with the original curated music for this piece of visual art.
+|]
+
+-- * comicReader
+
+data ComicReaderView = Spread | Full
+ deriving (Show, Eq)
+
+comicReader :: ComicId -> Page -> Model -> View Action
+comicReader _ _ model = case appComics model of
+ NotAsked -> loading
+ Loading -> loading
+ Failure _ -> nocomics
+ Success comics -> case cpState model of
+ NotReading -> template "comic-player" [text "error: not reading"]
+ Cover id -> viewOr404 comics comicSpread id 1 model
+ ChooseExperience id pg ->
+ viewOr404 comics chooseExperiencePage id pg model
+ Reading Spread id pg -> viewOr404 comics comicSpread id pg model
+ Reading Full id pg -> viewOr404 comics zoomScreen id pg model
+ Watching id -> viewOr404 comics comicVideo id 0 model
+
+zoomScreen :: Comic -> Page -> Model -> View Action
+zoomScreen comic page model =
+ template
+ "comic-player"
+ [ topbar,
+ main_
+ [id_ "app-body"]
+ [ img_
+ [ src_ comicImg,
+ class_ "comic-page-full"
+ ]
+ ],
+ comicControls comic page model
+ ]
+ where
+ comicImg =
+ ms Assets.demo
+ <> ms (comicSlug comic)
+ <> "-"
+ <> padLeft page
+ <> ".png"
+
+-- * comicReaderSpread
+
+type ComicReaderSpread =
+ "comic"
+ :> Api.Capture "id" ComicId
+ :> Api.Capture "page" Page
+ :> View Action
+
+comicReaderSpreadProxy :: Proxy ComicReaderSpread
+comicReaderSpreadProxy = Proxy
+
+comicReaderSpreadLink :: ComicId -> Page -> Api.URI
+comicReaderSpreadLink id page =
+ linkURI $ Api.safeLink routes comicReaderSpreadProxy id page
+
+comicSpread :: Comic -> Page -> Model -> View Action
+comicSpread comic page model =
+ template
+ "comic-player"
+ [ topbar,
+ main_
+ [id_ "app-body"]
+ [ div_
+ [class_ "comic-player"]
+ [ img_ [src_ comicImgLeft, class_ "comic-page"],
+ img_ [src_ comicImgRight, class_ "comic-page"]
+ ],
+ closeButton
+ ],
+ appmenu,
+ comicControls comic page model
+ ]
+ where
+ comicImgLeft, comicImgRight :: MisoString
+ comicImgLeft =
+ ms Assets.demo
+ <> ms (comicSlug comic)
+ <> "-"
+ <> padLeft page
+ <> ".png"
+ comicImgRight =
+ ms Assets.demo
+ <> ms (comicSlug comic)
+ <> "-"
+ <> padLeft (1 + page)
+ <> ".png"
+
+closeButton :: View Action
+closeButton =
+ a_
+ [id_ "close-button", onClick $ ChangeURI discoverLink]
+ [text "x"]
+
+-- * comicReaderFull
+
+type ComicReaderFull =
+ "comic"
+ :> Api.Capture "id" ComicId
+ :> Api.Capture "page" Page
+ :> "full"
+ :> View Action
+
+comicReaderFullProxy :: Proxy ComicReaderFull
+comicReaderFullProxy = Proxy
+
+comicReaderFullLink :: ComicId -> Page -> Api.URI
+comicReaderFullLink id page =
+ linkURI $ Api.safeLink routes comicReaderFullProxy id page
+
+-- * comicVideo
+
+type ComicVideo =
+ "comic"
+ :> Api.Capture "id" ComicId
+ :> Api.Capture "page" Page
+ :> "video"
+ :> View Action
+
+comicVideoProxy :: Proxy ComicVideo
+comicVideoProxy = Proxy
+
+comicVideoLink :: ComicId -> Page -> Api.URI
+comicVideoLink id page =
+ linkURI $ Api.safeLink routes comicVideoProxy id page
+
+frameborder_ :: MisoString -> Attribute action
+frameborder_ = textProp "frameborder"
+
+allowfullscreen_ :: Bool -> Attribute action
+allowfullscreen_ = boolProp "allowfullscreen"
+
+comicVideo :: Comic -> Page -> Model -> View Action
+comicVideo _ _ _ =
+ template
+ "comic-player"
+ [ topbar,
+ main_
+ [id_ "app-body"]
+ [ div_
+ [class_ "comic-video"]
+ [ iframe_
+ [ src_ "//player.vimeo.com/video/325757560",
+ frameborder_ "0",
+ allowfullscreen_ True
+ ]
+ []
+ ]
+ ]
+ ]
+
+-- * general page components & utils
+
+-- | If 'View' had a 'Monoid' instance, then '(text "")' could just be 'mempty'
+maybeView :: (a -> View action) -> Maybe a -> View action
+maybeView = maybe (text "")
+
+mediaInfo :: Maybe Comic -> User -> View Action
+mediaInfo Nothing _ = text ""
+mediaInfo (Just comic) user =
+ div_ [class_ "media-info"] [info comic user]
+
+appmenu :: View Action
+appmenu = aside_ [id_ "appmenu"] $ btn </ links
+ where
+ links =
+ -- these extra 'discoverLink's are just dummies
+ [ (discoverLink, "discover.svg", "discover"),
+ (discoverLink, "save.svg", "library"),
+ (discoverLink, "watch.svg", "videos"),
+ (comicLink "1", "read.svg", "comics"),
+ (discoverLink, "listen.svg", "music")
+ ]
+ btn (lnk, img, label) =
+ a_
+ [ class_ "button",
+ onPreventClick $ ChangeURI lnk
+ ]
+ [ img_ [src_ $ ms $ Assets.icon <> img],
+ span_ [] [text label]
+ ]
+
+-- TODO: make this a loading gif of some sort... maybe the hero icon filling
+-- from white to red
+loading :: View Action
+loading = div_ [class_ "loading"] [text "Loading..."]
+
+nocomics :: View Action
+nocomics = div_ [class_ "loading"] [text "error: no comics found"]
+
+shelf :: IsMediaObject o => MisoString -> [o] -> View Action
+shelf title comics =
+ div_
+ [class_ "shelf"]
+ [ div_ [class_ "shelf-head"] [text title],
+ ul_ [class_ "shelf-body"] $ thumbnail </ comics
+ ]
+
+viewOr404 ::
+ [Comic] ->
+ (Comic -> Page -> Model -> View Action) ->
+ ComicId ->
+ Page ->
+ Model ->
+ View Action
+viewOr404 comics f id pg model =
+ case findComic id comics of
+ Just c -> f c pg model
+ Nothing -> the404 model
+
+template :: MisoString -> [View Action] -> View Action
+template id = div_ [id_ id, class_ "app is-black"]
+
+padLeft :: Int -> MisoString
+padLeft n
+ | n < 10 = ms ("0" <> Legacy.show n)
+ | otherwise = ms $ Legacy.show n
+
+comicControls :: Comic -> Page -> Model -> View Action
+comicControls comic page model =
+ footer_
+ [id_ "app-foot", class_ "comic-controls"]
+ [ div_
+ [ class_ "comic-nav-audio",
+ css flexCenter
+ ]
+ [ audio_
+ [id_ audioId, loop_ True, crossorigin_ "anonymous"]
+ [source_ [src_ $ ms $ Assets.demo <> "stars-instrumental.mp3"]],
+ el $ PlayPause audioId $ cpAudioState model,
+ span_
+ [css $ euro <> thicc <> smol <> wide]
+ [text "Experiencing: Original"]
+ ],
+ div_
+ [class_ "comic-controls-pages", css euro]
+ [ el $ Arrow PrevPage,
+ span_ [] [text $ leftPage <> "-" <> rightPage <> " of " <> totalpages],
+ el $ Arrow NextPage
+ ],
+ div_
+ [class_ "comic-controls-share"]
+ [ el $ SaveIcon comic $ user model,
+ el $ ZoomIcon (magnification model) comic page,
+ button_
+ [class_ "button icon is-large", onClick ToggleFullscreen]
+ [i_ [class_ "fa fa-expand"] []]
+ ]
+ ]
+ where
+ leftPage = ms . Legacy.show $ page
+ rightPage = ms . Legacy.show $ 1 + page
+ totalpages = ms . Legacy.show $ comicPages comic
+
+topbar :: View Action
+topbar =
+ header_
+ [id_ "app-head", class_ "is-black", css euro]
+ [ a_
+ [ class_ "button is-medium is-black",
+ onClick $ ChangeURI discoverLink
+ ]
+ [img_ [src_ $ ms $ Assets.icon <> "hero-logo.svg"]],
+ div_
+ [id_ "app-head-right"]
+ [ button_
+ [class_ "button icon is-medium is-black"]
+ [i_ [class_ "fas fa-search"] []],
+ button_
+ [ class_ "button is-medium is-black is-size-7",
+ css $ euro <> wide <> thicc
+ ]
+ [text "News"],
+ span_
+ [class_ "icon is-large"]
+ [ i_ [class_ "fas fa-user"] []
+ ]
+ ]
+ ]
+
+row :: [View Action] -> View Action
+row = div_ [css $ Clay.display Clay.flex <> Clay.flexDirection Clay.row]
+
+column :: [View Action] -> View Action
+column = div_ [css $ Clay.display Clay.flex <> Clay.flexDirection Clay.column]
+
+-- | Links
+the404 :: Model -> View Action
+the404 _ = template "404" [p_ [] [text "Not found"]]