netease-fm-1.2.2: exe/UI/Login.hs
{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
module UI.Login (
login
) where
import qualified Brick.Main as UI
import qualified Brick.Types as UI
import qualified Brick.Widgets.Center as UI
import qualified Brick.Widgets.Core as UI
import Brick.Widgets.Core ((<+>))
import qualified Brick.Widgets.Edit as UI
import qualified Graphics.Vty as UI
import qualified UI.Extra as UI
import Control.Concurrent.Chan (newChan, writeChan)
import Control.Exception (SomeException, try)
import Control.Monad (void)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Cont (ContT (..))
import Data.Default.Class
import Data.IORef
import System.Directory (createDirectoryIfMissing, getHomeDirectory)
import qualified FM.Cache as Cache
import qualified FM.NetEase as NetEase
import Types
getConfig :: (MonadIO m) => m FilePath
getConfig = liftIO $ do
dir <- (++ "/.fm") <$> getHomeDirectory
createDirectoryIfMissing True dir
return $ dir ++ "/login.conf"
readLoginConfig :: (MonadIO m) => m (String, String)
readLoginConfig = liftIO $ do
conf <- getConfig
[userName, password] <- take 2 . lines <$> readFile conf
return (userName, password)
writeLoginConfig :: (MonadIO m) => (String, String) -> m ()
writeLoginConfig (userName, password) = liftIO $ do
conf <- getConfig
writeFile conf (unlines [userName, password])
data Event = Event UI.Event | Hi | Hello | Goodbye
data EditorType = UserNameEditor | PasswordEditor
deriving (Show)
type NetEaseSavedSession = IORef (Maybe SomeSession)
data State = State {
currentEditor :: EditorType
, userNameEditor :: UI.Editor
, passwordEditor :: UI.Editor
, musicSource :: MusicSource
, netEaseSession :: NetEaseSavedSession
, cache :: Cache
, onGreetings :: Bool
, postEvent :: Event -> IO ()
, continuation :: SomeSession -> IO ()
}
editorName :: EditorType -> UI.Name
editorName e = UI.Name (show e)
selectEditor :: State -> UI.Editor
selectEditor State {..} = case currentEditor of
UserNameEditor -> userNameEditor
PasswordEditor -> passwordEditor
switchEditor :: State -> State
switchEditor state@State {..} = state { currentEditor = newEditor }
where
newEditor = case currentEditor of
UserNameEditor -> PasswordEditor
PasswordEditor -> UserNameEditor
loginDraw :: State -> [UI.Widget]
loginDraw State {..} = [ui]
where
ui = UI.vCenter $ if onGreetings
then UI.str []
else UI.vBox [ UI.mkYellow $ UI.hCenter $ UI.str "网易通行证登陆"
, UI.separator
, UI.hCenter $ UI.str "账号: " <+> UI.hLimit 15 (UI.vLimit 1 $ UI.renderEditor userNameEditor)
, UI.hCenter $ UI.str "密码: " <+> UI.hLimit 15 (UI.vLimit 1 $ UI.renderEditor passwordEditor)
]
loginEvent :: State -> Event -> UI.EventM (UI.Next State)
loginEvent state@State {..} event = case event of
Hi -> do
session <- if isLocal musicSource
then Cache.initSession cache
else NetEase.initSession True
UI.suspendAndResume $ continuation session >> postEvent Goodbye >> return state
Hello -> do
session <- liftIO $ try $ do
mSession <- readIORef netEaseSession
case mSession of
Just session -> return session
Nothing -> do
(userName, password) <- readLoginConfig
session <- NetEase.initSession True
runSession session (NetEase.login userName password)
writeIORef netEaseSession (Just session)
return session
case session of
Left (_ :: SomeException) -> liftIO (writeIORef netEaseSession Nothing) >> UI.continue state { onGreetings = False }
Right session -> UI.suspendAndResume $ continuation session >> postEvent Goodbye >> return state
Goodbye -> UI.halt state
Event (UI.EvKey UI.KEsc []) -> UI.halt state
Event (UI.EvKey UI.KEnter []) -> case currentEditor of
PasswordEditor -> do
let [userName] = UI.getEditContents userNameEditor
let [password] = NetEase.encryptPassword <$> UI.getEditContents passwordEditor
session <- NetEase.initSession True
liftIO $ do
runSession session (NetEase.login userName password)
writeLoginConfig (userName, password)
writeIORef netEaseSession (Just session)
UI.suspendAndResume $ continuation session >> postEvent Goodbye >> return state
UserNameEditor -> UI.continue $ switchEditor state
Event (UI.EvKey (UI.KChar '\t') []) -> UI.continue $ switchEditor state
Event (UI.EvKey UI.KBackTab []) -> UI.continue $ switchEditor state
Event event -> do
editor <- UI.handleEvent event (selectEditor state)
UI.continue $ case currentEditor of
UserNameEditor -> state { userNameEditor = editor }
PasswordEditor -> state { passwordEditor = editor }
loginCursor :: State -> [UI.CursorLocation] -> Maybe UI.CursorLocation
loginCursor state = UI.showCursorNamed (editorName $ currentEditor state)
loginApp :: UI.App State Event
loginApp = UI.App { UI.appDraw = loginDraw
, UI.appChooseCursor = loginCursor
, UI.appHandleEvent = loginEvent
, UI.appStartEvent = return
, UI.appAttrMap = const UI.defaultAttributeMap
, UI.appLiftVtyEvent = Event
}
loginCont :: MusicSource -> NetEaseSavedSession -> Cache -> (SomeSession -> IO ()) -> IO ()
loginCont source netEaseSession cache continuation = void $ do
let editor1 = UI.editor (editorName UserNameEditor) (UI.str . unlines) Nothing []
let editor2 = UI.editor (editorName PasswordEditor) (\[s] -> UI.str $ replicate (length s) '*') Nothing []
chan <- newChan
let postEvent = writeChan chan
postEvent $ if requireLogin source then Hello else Hi
UI.customMain (UI.mkVty def) chan loginApp State { currentEditor = UserNameEditor
, userNameEditor = editor1
, passwordEditor = editor2
, musicSource = source
, netEaseSession = netEaseSession
, cache = cache
, onGreetings = True
, postEvent = postEvent
, continuation = continuation
}
login :: MusicSource -> NetEaseSavedSession -> Cache -> ContT () IO SomeSession
login source netEaseSession cache = ContT (loginCont source netEaseSession cache)