added functionality to copy magnets, added todo, lightly documented code
This commit is contained in:
parent
f71cee3ffa
commit
56e1a7ce0b
7
TODO
Normal file
7
TODO
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
COMMENT YOUR CODE!!!!!!!
|
||||||
|
|
||||||
|
make it look nicer :)
|
||||||
|
|
||||||
|
fix import/exports to be conservative
|
||||||
|
|
||||||
|
write tests
|
11
app/Main.hs
11
app/Main.hs
|
@ -10,13 +10,4 @@ import UI
|
||||||
import System.Environment
|
import System.Environment
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = runApp >> pure ()
|
||||||
args <- getArgs
|
|
||||||
case args of
|
|
||||||
([]) -> runApp >> pure ()
|
|
||||||
(x:_) -> do
|
|
||||||
movies <- queryMovies (T.pack x)
|
|
||||||
|
|
||||||
case movies of
|
|
||||||
(Left m) -> print m
|
|
||||||
(Right d) -> mapM_ printMagnets (J.movies d)
|
|
||||||
|
|
|
@ -17,10 +17,10 @@ library
|
||||||
exposed-modules: Request
|
exposed-modules: Request
|
||||||
, JSONTypes
|
, JSONTypes
|
||||||
, UI
|
, UI
|
||||||
|
, UI.Widgets
|
||||||
, Torrent
|
, Torrent
|
||||||
, Misc
|
, Misc
|
||||||
, AppTypes
|
, AppTypes
|
||||||
, Widgets
|
|
||||||
other-modules:
|
other-modules:
|
||||||
-- other-extensions:
|
-- other-extensions:
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
|
@ -37,6 +37,7 @@ library
|
||||||
, transformers
|
, transformers
|
||||||
, microlens
|
, microlens
|
||||||
, microlens-th
|
, microlens-th
|
||||||
|
, Clipboard
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,39 @@
|
||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Module : AppTypes
|
||||||
|
Description : Contains the types related to our Brick application
|
||||||
|
-}
|
||||||
|
|
||||||
module AppTypes where
|
module AppTypes where
|
||||||
|
|
||||||
import JSONTypes
|
import JSONTypes
|
||||||
import Lens.Micro.TH
|
import Lens.Micro.TH
|
||||||
|
|
||||||
|
-- | Contains the different elements which we
|
||||||
|
-- might want brick to be able to identify
|
||||||
data Ident = Listing | Input | ListItem Int
|
data Ident = Listing | Input | ListItem Int
|
||||||
deriving (Eq, Ord, Show)
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
data Mode = Search | Browse | Error
|
-- | Used to distiguish what set of
|
||||||
|
-- widgets should currently be rendered.
|
||||||
|
data Mode = Search | Browse | Message
|
||||||
deriving (Eq, Ord, Show)
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
|
-- | Used for scrolling
|
||||||
data ScrollDirection = Up | Down
|
data ScrollDirection = Up | Down
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- | The state of our app
|
||||||
data AppS = AppS
|
data AppS = AppS
|
||||||
{ _appMode :: Mode
|
{ _appMode :: Mode -- ^ The current mode of the app
|
||||||
, _appCursor :: Int
|
, _appCursor :: Int -- ^ The selected into the listing
|
||||||
, _appExpanded :: Bool
|
, _appExpanded :: Bool -- ^ If the currently selected listing is expanded
|
||||||
, _appPage :: Int
|
, _appPage :: Int -- ^ The page currently being viewed
|
||||||
, _appListing :: Maybe JSONListMovies
|
, _appListing :: Maybe JSONListMovies -- ^ The movies being browsed
|
||||||
, _appDetails :: Maybe JSONMovie
|
, _appDetails :: Maybe JSONMovie -- ^ The movie being focused
|
||||||
, _appError :: Maybe String
|
, _appMessage :: Maybe String -- ^ The message to be shown in message mode
|
||||||
|
, _appContinue :: Bool -- ^ If to continue after showing message
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
makeLenses ''AppS
|
makeLenses ''AppS
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Module : JSONTypes
|
||||||
|
Description : Contains all the types used for data extracted from JSON responses
|
||||||
|
|
||||||
|
Self explanatory
|
||||||
|
-}
|
||||||
|
|
||||||
module JSONTypes where
|
module JSONTypes where
|
||||||
|
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
|
@ -7,10 +14,10 @@ import Data.Aeson.Types
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
|
||||||
data JSONResponse d = JSONResponse
|
data JSONResponse d = JSONResponse
|
||||||
{ resp_status :: T.Text
|
{ respStatus :: T.Text
|
||||||
, resp_message :: T.Text
|
, respMessage :: T.Text
|
||||||
, response_data :: Maybe d
|
, respData :: Maybe d
|
||||||
} deriving (Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
instance (FromJSON d) => FromJSON (JSONResponse d) where
|
instance (FromJSON d) => FromJSON (JSONResponse d) where
|
||||||
parseJSON (Object v) = JSONResponse
|
parseJSON (Object v) = JSONResponse
|
||||||
|
@ -22,11 +29,11 @@ instance (FromJSON d) => FromJSON (JSONResponse d) where
|
||||||
(typeMismatch "Object" invalid)
|
(typeMismatch "Object" invalid)
|
||||||
|
|
||||||
data JSONListMovies = JSONListMovies
|
data JSONListMovies = JSONListMovies
|
||||||
{ movies_count :: Int
|
{ moviesCount :: Int
|
||||||
, movies_limit :: Int
|
, moviesLimit :: Int
|
||||||
, page_number :: Int
|
, pagenumber :: Int
|
||||||
, movies :: [JSONMovie]
|
, movies :: [JSONMovie]
|
||||||
} deriving (Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
instance FromJSON JSONListMovies where
|
instance FromJSON JSONListMovies where
|
||||||
parseJSON (Object v) = JSONListMovies
|
parseJSON (Object v) = JSONListMovies
|
||||||
|
@ -39,20 +46,20 @@ instance FromJSON JSONListMovies where
|
||||||
(typeMismatch "Object" invalid)
|
(typeMismatch "Object" invalid)
|
||||||
|
|
||||||
data JSONMovie = JSONMovie
|
data JSONMovie = JSONMovie
|
||||||
{ movie_id :: Int
|
{ movieId :: Int
|
||||||
, movie_url :: T.Text
|
, movieUrl :: T.Text
|
||||||
, imdb_code :: T.Text
|
, imdbCode :: T.Text
|
||||||
, movie_title :: T.Text
|
, movieTitle :: T.Text
|
||||||
, movie_title_long :: T.Text
|
, movieTitleLong :: T.Text
|
||||||
, movie_year :: Int
|
, movieYear :: Int
|
||||||
, movie_rating :: Double
|
, movieRating :: Double
|
||||||
, movie_runtime :: Int
|
, movieRuntime :: Int
|
||||||
, movie_genres :: [T.Text]
|
, movieGenres :: [T.Text]
|
||||||
, movie_summary :: T.Text
|
, movieSummary :: T.Text
|
||||||
, movie_language :: T.Text
|
, movieLanguage :: T.Text
|
||||||
, movie_state :: T.Text
|
, movieState :: T.Text
|
||||||
, movie_torrents :: [JSONTorrent]
|
, movieTorrents :: [JSONTorrent]
|
||||||
} deriving (Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
instance FromJSON JSONMovie where
|
instance FromJSON JSONMovie where
|
||||||
parseJSON (Object v) = JSONMovie
|
parseJSON (Object v) = JSONMovie
|
||||||
|
@ -74,17 +81,17 @@ instance FromJSON JSONMovie where
|
||||||
(typeMismatch "Object" invalid)
|
(typeMismatch "Object" invalid)
|
||||||
|
|
||||||
data JSONTorrent = JSONTorrent
|
data JSONTorrent = JSONTorrent
|
||||||
{ torrent_url :: T.Text
|
{ torrentUrl :: T.Text
|
||||||
, torrent_hash :: T.Text
|
, torrentHash :: T.Text
|
||||||
, torrent_quality :: T.Text
|
, torrentQuality :: T.Text
|
||||||
, torrent_type :: T.Text
|
, torrentType :: T.Text
|
||||||
, torrent_seeds :: Int
|
, torrentSeeds :: Int
|
||||||
, torrent_peers :: Int
|
, torrentPeers :: Int
|
||||||
, torrent_size :: T.Text
|
, torrentSize :: T.Text
|
||||||
, torrent_bytes :: Int
|
, torrentBytes :: Int
|
||||||
, torrent_uploaded :: T.Text
|
, torrentUploaded :: T.Text
|
||||||
, torrent_uploaded_unix :: Int -- TODO: better date type?
|
, torrentUploadedUnix :: Int -- TODO: better date type?
|
||||||
} deriving (Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
instance FromJSON JSONTorrent where
|
instance FromJSON JSONTorrent where
|
||||||
parseJSON (Object v) = JSONTorrent
|
parseJSON (Object v) = JSONTorrent
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
{-|
|
||||||
|
Module : Misc
|
||||||
|
Description : Contains miscelaneous helper functions which do not fit elsewhere
|
||||||
|
-}
|
||||||
|
|
||||||
module Misc where
|
module Misc where
|
||||||
|
|
||||||
infixl 3 !?
|
infixl 3 !?
|
||||||
|
-- | Safe version of (!!)
|
||||||
(!?) :: [a] -> Int -> Maybe a
|
(!?) :: [a] -> Int -> Maybe a
|
||||||
[] !? i = Nothing
|
[] !? i = Nothing
|
||||||
(x:xs) !? 0 = Just x
|
(x:xs) !? 0 = Just x
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Module : Request
|
||||||
|
Description : Contains code for issuing http requests
|
||||||
|
-}
|
||||||
|
|
||||||
module Request where
|
module Request where
|
||||||
|
|
||||||
import JSONTypes
|
import JSONTypes
|
||||||
|
@ -10,6 +15,7 @@ import Control.Lens
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
-- | Sends a request and unwraps the top level respone data structure
|
||||||
makeRequest :: (FromJSON a) => String -> WR.Options -> IO (Either T.Text a)
|
makeRequest :: (FromJSON a) => String -> WR.Options -> IO (Either T.Text a)
|
||||||
makeRequest url opts = do
|
makeRequest url opts = do
|
||||||
r <- asJSON =<< getWith opts url
|
r <- asJSON =<< getWith opts url
|
||||||
|
@ -17,10 +23,12 @@ makeRequest url opts = do
|
||||||
(JSONResponse "ok" _ (Just d)) -> Right d
|
(JSONResponse "ok" _ (Just d)) -> Right d
|
||||||
(JSONResponse _ m _) -> Left m
|
(JSONResponse _ m _) -> Left m
|
||||||
|
|
||||||
|
-- | Requests a list of all movies
|
||||||
getMovies :: IO (Either T.Text JSONListMovies)
|
getMovies :: IO (Either T.Text JSONListMovies)
|
||||||
getMovies = makeRequest "https://yts.mx/api/v2/list_movies.json"
|
getMovies = makeRequest "https://yts.mx/api/v2/list_movies.json"
|
||||||
(defaults & param "limit" .~ ["50"])
|
(defaults & param "limit" .~ ["50"])
|
||||||
|
|
||||||
|
-- | Requests a list of all movies matching a query term
|
||||||
queryMovies :: T.Text -> IO (Either T.Text JSONListMovies)
|
queryMovies :: T.Text -> IO (Either T.Text JSONListMovies)
|
||||||
queryMovies q = makeRequest "https://yts.mx/api/v2/list_movies.json"
|
queryMovies q = makeRequest "https://yts.mx/api/v2/list_movies.json"
|
||||||
(defaults & param "query_term" .~ [q]
|
(defaults & param "query_term" .~ [q]
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
{-|
|
||||||
|
Module : Torrent
|
||||||
|
Description : Contains code for formatting torrent info and retrieving magnet links
|
||||||
|
-}
|
||||||
|
|
||||||
module Torrent where
|
module Torrent where
|
||||||
|
|
||||||
import JSONTypes
|
import JSONTypes
|
||||||
|
@ -5,38 +10,47 @@ import Network.HTTP.Base
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
|
|
||||||
|
-- | Makes type signature a bit clearer
|
||||||
type Quality = String
|
type Quality = String
|
||||||
|
|
||||||
|
-- | A list of recommended trackers
|
||||||
trackerList :: [String]
|
trackerList :: [String]
|
||||||
trackerList = [ "udp://open.demonii.com:1337/announce"
|
trackerList = [ "udp://open.demonii.com:1337/announce"
|
||||||
, "udp://tracker.openbittorrent.com:80"
|
, "udp://tracker.openbittorrent.com:80"
|
||||||
, "udp://tracker.coppersurfer.tk:6969"
|
, "udp://tracker.coppersurfer.tk:6969"
|
||||||
, "udp://glotorrents.pw:6969/announce"
|
, "udp://glotorrents.pw:6969/announce"
|
||||||
, "udp://tracker.opentrackr.org:1337/announce"
|
, "udp://tracker.opentrackr.org:1337/announce"
|
||||||
, "udp://torrent.gresille.org:80/announce"
|
, "udp://torrent.gresille.org:80/announce"
|
||||||
, "udp://p4p.arenabg.com:1337"
|
, "udp://p4p.arenabg.com:1337"
|
||||||
, "udp://tracker.leechers-paradise.org:6969"
|
, "udp://tracker.leechers-paradise.org:6969"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
-- | A string to be embedded into the magnet link
|
||||||
trackerString :: String
|
trackerString :: String
|
||||||
trackerString = "&tr=" <> intercalate "&tr=" trackerList
|
trackerString = "&tr=" <> intercalate "&tr=" trackerList
|
||||||
|
|
||||||
toMagnets :: JSONMovie -> [(Quality, String)]
|
-- | Creates a String enumerating and listing the different torrents
|
||||||
toMagnets m = map (\t -> (quality t, toMagnet name (hash t))) torrents
|
-- quality and amount of peers
|
||||||
|
listTorrents :: JSONMovie -> String
|
||||||
|
listTorrents m = intercalate ", " (zipWith3 (\x y z -> x ++ y ++ z) numbers qualities seeders)
|
||||||
where
|
where
|
||||||
name = T.unpack (movie_title_long m)
|
qualities = map quality (movieTorrents m)
|
||||||
quality = T.unpack . torrent_quality
|
seeders = map (\x -> ' ':'(':seeds x ++ ")") (movieTorrents m)
|
||||||
hash = T.unpack . torrent_hash
|
numbers = map (\x -> '[':show x ++ "] ") [1..]
|
||||||
torrents = movie_torrents m
|
seeds = show . torrentSeeds
|
||||||
|
quality = T.unpack . torrentQuality
|
||||||
|
|
||||||
|
-- | Creates a list of magnet names given a movie
|
||||||
|
toMagnets :: JSONMovie -> [String]
|
||||||
|
toMagnets m = map (toMagnet name . hash) torrents
|
||||||
|
where
|
||||||
|
name = T.unpack (movieTitleLong m)
|
||||||
|
hash = T.unpack . torrentHash
|
||||||
|
torrents = movieTorrents m
|
||||||
|
|
||||||
|
-- | Takes the longName of a movie and a torrent hash and returns
|
||||||
|
-- a valid magnet link
|
||||||
toMagnet :: String -> String -> String
|
toMagnet :: String -> String -> String
|
||||||
toMagnet long_name hash = "magnet:?xt=urn:btih:" <> hash <> "&dn"
|
toMagnet longName hash = "magnet:?xt=urn:btih:" <> hash <> "&dn"
|
||||||
<> (urlEncode long_name) <> trackerString
|
<> (urlEncode longName) <> trackerString
|
||||||
|
|
||||||
printMagnets :: JSONMovie -> IO ()
|
|
||||||
printMagnets movie = do
|
|
||||||
putStrLn $ '\n' : title ++ '\n' : replicate (length title) '='
|
|
||||||
mapM_ ( \(q, m) -> putStrLn (q ++ '\t' : m)) (toMagnets movie)
|
|
||||||
where
|
|
||||||
title = T.unpack (movie_title movie)
|
|
||||||
|
|
79
src/UI.hs
79
src/UI.hs
|
@ -1,3 +1,8 @@
|
||||||
|
{-|
|
||||||
|
Module : UI
|
||||||
|
Description : This is the code which interacts with Brick
|
||||||
|
-}
|
||||||
|
|
||||||
module UI where
|
module UI where
|
||||||
|
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
|
@ -9,13 +14,16 @@ import Graphics.Vty.Attributes (withStyle, reverseVideo)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Lens.Micro
|
import Lens.Micro
|
||||||
|
import System.Clipboard
|
||||||
|
|
||||||
import JSONTypes
|
import JSONTypes
|
||||||
import Request
|
import Request
|
||||||
import Misc
|
import Misc
|
||||||
import Widgets
|
import UI.Widgets
|
||||||
import AppTypes
|
import AppTypes
|
||||||
|
import Torrent
|
||||||
|
|
||||||
|
-- | The initial state of our application
|
||||||
initialState :: AppS
|
initialState :: AppS
|
||||||
initialState = AppS
|
initialState = AppS
|
||||||
{ _appMode = Browse
|
{ _appMode = Browse
|
||||||
|
@ -24,9 +32,11 @@ initialState = AppS
|
||||||
, _appPage = 1
|
, _appPage = 1
|
||||||
, _appListing = Nothing
|
, _appListing = Nothing
|
||||||
, _appDetails = Nothing
|
, _appDetails = Nothing
|
||||||
, _appError = Nothing
|
, _appMessage = Nothing
|
||||||
|
, _appContinue = True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- | Our brick app specification
|
||||||
app :: App AppS () Ident
|
app :: App AppS () Ident
|
||||||
app = App
|
app = App
|
||||||
{ appDraw = draw
|
{ appDraw = draw
|
||||||
|
@ -36,42 +46,74 @@ app = App
|
||||||
, appAttrMap = attributeMap
|
, appAttrMap = attributeMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- | A small wrapper to run the brick app
|
||||||
runApp :: IO AppS
|
runApp :: IO AppS
|
||||||
runApp = defaultMain app initialState
|
runApp = defaultMain app initialState
|
||||||
|
|
||||||
|
-- | The starting event which grabs the inital listing
|
||||||
startEvent :: AppS -> EventM Ident AppS
|
startEvent :: AppS -> EventM Ident AppS
|
||||||
startEvent s = do
|
startEvent s = do
|
||||||
-- todo move unwrapping our response structure into a function
|
-- todo move unwrapping our response structure into a function
|
||||||
response <- liftIO getMovies
|
response <- liftIO getMovies
|
||||||
pure $ case response of
|
pure $ case response of
|
||||||
(Left msg) -> s & appError .~ Just (T.unpack msg)
|
(Left msg) -> s & appMessage .~ Just (T.unpack msg)
|
||||||
|
& appContinue .~ False
|
||||||
(Right listing) -> s & appListing .~ Just listing
|
(Right listing) -> s & appListing .~ Just listing
|
||||||
& appDetails .~ (movies listing !? s ^. appCursor)
|
& appDetails .~ (movies listing !? s ^. appCursor)
|
||||||
|
|
||||||
|
-- | The drawing function which defers to the proper function
|
||||||
|
-- from UI.Widgets
|
||||||
draw :: AppS -> [Widget Ident]
|
draw :: AppS -> [Widget Ident]
|
||||||
draw s = pure $ case s ^. appMode of
|
draw s = pure $ case s ^. appMode of
|
||||||
Browse -> browseWidget s
|
Browse -> browseWidget s
|
||||||
Search -> searchWidget s
|
Search -> searchWidget s
|
||||||
Error -> errorWidget s
|
Message -> messageWidget s
|
||||||
|
|
||||||
|
|
||||||
|
-- | Currently just showFirstCursor
|
||||||
chooseCursor :: AppS -> [CursorLocation Ident] -> Maybe (CursorLocation Ident)
|
chooseCursor :: AppS -> [CursorLocation Ident] -> Maybe (CursorLocation Ident)
|
||||||
chooseCursor = showFirstCursor -- replace if needed
|
chooseCursor = showFirstCursor
|
||||||
|
|
||||||
-- todo: lenses? NO OVERFLOW
|
-- | Scrolls our cursor and updates the app details
|
||||||
scroll :: ScrollDirection -> AppS -> AppS
|
scroll :: ScrollDirection -> AppS -> AppS
|
||||||
scroll d s = s & appCursor %~ (\x -> max 0 (min upperLimit (new x)))
|
scroll d s = s & appCursor .~ newCursor
|
||||||
& appExpanded .~ False
|
& appExpanded .~ False
|
||||||
|
& appDetails .~ (s ^. appListing >>= ((!? newCursor) . movies))
|
||||||
where
|
where
|
||||||
upperLimit = fromMaybe 0 (subtract 1 . length . movies <$> s ^. appListing)
|
upperLimit = fromMaybe 0 (subtract 1 . length . movies <$> s ^. appListing)
|
||||||
new = case d of { Up -> (subtract 1); Down -> (+1) }
|
new = case d of { Up -> (subtract 1); Down -> (+1) }
|
||||||
|
oldCursor = s ^. appCursor
|
||||||
|
newCursor = max 0 (min upperLimit (new oldCursor))
|
||||||
|
|
||||||
|
|
||||||
|
-- | Given a String, modify our state so that string
|
||||||
|
-- is displayed as a message
|
||||||
|
displayMessage :: AppS -> String -> AppS
|
||||||
|
displayMessage s str = s & appMode .~ Message
|
||||||
|
& appMessage .~ Just str
|
||||||
|
|
||||||
|
-- | Copy the magnet link of the focused movie at
|
||||||
|
-- the given index
|
||||||
|
copyMagnet :: AppS -> Int -> EventM Ident (Next AppS)
|
||||||
|
copyMagnet s i = case (do
|
||||||
|
m <- s ^. appDetails
|
||||||
|
_ <- movieTorrents m !? i - 1
|
||||||
|
toMagnets m !? i - 1) of
|
||||||
|
Nothing -> continue s
|
||||||
|
(Just magnet) -> do
|
||||||
|
liftIO (setClipboardString magnet)
|
||||||
|
continue (displayMessage s "Copied magnet link to clipboard!")
|
||||||
|
|
||||||
|
-- The event handler, takes care of keyboard events.
|
||||||
eventHandler :: AppS -> BrickEvent Ident () -> EventM Ident (Next AppS)
|
eventHandler :: AppS -> BrickEvent Ident () -> EventM Ident (Next AppS)
|
||||||
eventHandler s (VtyEvent (EvKey k _)) = do
|
eventHandler s (VtyEvent (EvKey k _)) =
|
||||||
case s ^. appMode of
|
case s ^. appMode of
|
||||||
Error -> halt s
|
Message -> if s ^. appContinue
|
||||||
Search -> undefined
|
then continue (s & appMode .~ Browse
|
||||||
Browse -> case k of
|
& appMessage .~ Nothing)
|
||||||
|
else halt s
|
||||||
|
Search -> undefined
|
||||||
|
Browse -> case k of
|
||||||
(KChar 'q') -> halt s
|
(KChar 'q') -> halt s
|
||||||
(KEsc) -> halt s
|
(KEsc) -> halt s
|
||||||
|
|
||||||
|
@ -83,6 +125,16 @@ eventHandler s (VtyEvent (EvKey k _)) = do
|
||||||
(KChar ' ') -> continue (s & appExpanded %~ not)
|
(KChar ' ') -> continue (s & appExpanded %~ not)
|
||||||
(KEnter) -> continue (s & appExpanded %~ not)
|
(KEnter) -> continue (s & appExpanded %~ not)
|
||||||
|
|
||||||
|
(KChar '1') -> copyMagnet s 1
|
||||||
|
(KChar '2') -> copyMagnet s 2
|
||||||
|
(KChar '3') -> copyMagnet s 3
|
||||||
|
(KChar '4') -> copyMagnet s 4
|
||||||
|
(KChar '5') -> copyMagnet s 5
|
||||||
|
(KChar '6') -> copyMagnet s 6
|
||||||
|
(KChar '7') -> copyMagnet s 7
|
||||||
|
(KChar '8') -> copyMagnet s 8
|
||||||
|
(KChar '9') -> copyMagnet s 9
|
||||||
|
|
||||||
(KChar '/') -> continue (s & appMode .~ Search)
|
(KChar '/') -> continue (s & appMode .~ Search)
|
||||||
(KChar 's') -> continue (s & appMode .~ Search)
|
(KChar 's') -> continue (s & appMode .~ Search)
|
||||||
|
|
||||||
|
@ -90,5 +142,6 @@ eventHandler s (VtyEvent (EvKey k _)) = do
|
||||||
|
|
||||||
eventHandler s _ = continue s
|
eventHandler s _ = continue s
|
||||||
|
|
||||||
|
-- | The attribute map, currently not dependant on state
|
||||||
attributeMap :: AppS -> AttrMap
|
attributeMap :: AppS -> AttrMap
|
||||||
attributeMap = const $ attrMap defAttr [(attrName "selected", withStyle defAttr reverseVideo)]
|
attributeMap = const $ attrMap defAttr [(attrName "selected", withStyle defAttr reverseVideo)]
|
||||||
|
|
83
src/UI/Widgets.hs
Normal file
83
src/UI/Widgets.hs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Module : UI.Widgets
|
||||||
|
Description : This is the code which builds the Brick frontend
|
||||||
|
-}
|
||||||
|
|
||||||
|
module UI.Widgets where
|
||||||
|
|
||||||
|
import Brick
|
||||||
|
import Brick.Main
|
||||||
|
import Brick.Widgets.Center
|
||||||
|
import Brick.Widgets.Border
|
||||||
|
import Brick.AttrMap (attrMap)
|
||||||
|
import Graphics.Vty (defAttr)
|
||||||
|
import Graphics.Vty.Input.Events
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Lens.Micro
|
||||||
|
|
||||||
|
import Data.Maybe
|
||||||
|
|
||||||
|
import JSONTypes
|
||||||
|
import Request
|
||||||
|
import Misc
|
||||||
|
import AppTypes
|
||||||
|
import Torrent
|
||||||
|
|
||||||
|
-- | Wrap a widget in the selected attribute
|
||||||
|
select :: Widget Ident -> Widget Ident
|
||||||
|
select = withAttr (attrName "selected")
|
||||||
|
|
||||||
|
-- | Given a movie and a tuple of widgets and state, append the
|
||||||
|
-- movie to the widgets. Used by movieWidgets
|
||||||
|
widgetCons :: JSONMovie -> (Widget Ident, AppS) -> (Widget Ident, AppS)
|
||||||
|
widgetCons m (w, s) =
|
||||||
|
embed $ if Just m == (s ^. appDetails)
|
||||||
|
then select . visible $
|
||||||
|
if s ^. appExpanded
|
||||||
|
then expandedWidget s m
|
||||||
|
else movieWidget s m
|
||||||
|
else movieWidget s m
|
||||||
|
where
|
||||||
|
embed x = (x <=> w, s)
|
||||||
|
|
||||||
|
-- | Returns a big list of all movies
|
||||||
|
movieWidgets :: AppS -> JSONListMovies -> Widget Ident
|
||||||
|
movieWidgets s m = let (items, _) = foldr widgetCons (emptyWidget, s) (movies m)
|
||||||
|
in items
|
||||||
|
|
||||||
|
-- | Returns a single movie listing
|
||||||
|
movieWidget :: AppS -> JSONMovie -> Widget Ident
|
||||||
|
movieWidget s m = txt (movieTitle m) <+> padLeft Max (str (show (movieYear m)))
|
||||||
|
|
||||||
|
-- | Returns an expanded movie listing showing additional info
|
||||||
|
expandedWidget :: AppS -> JSONMovie -> Widget Ident
|
||||||
|
expandedWidget s m = movieWidget s m
|
||||||
|
<=> (padRight (Pad 3) (str "Rating") <+> str (show (movieRating m)))
|
||||||
|
<=> (padRight (Pad 1) (str "Language") <+> txt (movieLanguage m))
|
||||||
|
<=> (padRight (Pad 3) (str "Genres") <+> txt (T.intercalate ", " (movieGenres m)))
|
||||||
|
<=> (padRight (Pad 2) (str "Magnets") <+> str (listTorrents m))
|
||||||
|
<=> (padRight (Pad 2) (str "Summary") <+> txtWrap (movieSummary m))
|
||||||
|
|
||||||
|
-- | The search mode widget (unimplemented)
|
||||||
|
searchWidget :: AppS -> Widget Ident
|
||||||
|
searchWidget = undefined
|
||||||
|
|
||||||
|
-- | The browse mode widget which returns a full listing of movies
|
||||||
|
-- or reports that there are no movies to list
|
||||||
|
browseWidget :: AppS -> Widget Ident
|
||||||
|
browseWidget s =
|
||||||
|
headings <=> case (s ^. appListing) of
|
||||||
|
Nothing -> center $ str "No movies found matching query."
|
||||||
|
(Just m) -> (viewport Listing Vertical (movieWidgets s m))
|
||||||
|
where
|
||||||
|
headings = str "Title" <+> padLeft Max (str "Year")
|
||||||
|
|
||||||
|
-- | The message widget which simply shows a message and informs
|
||||||
|
-- the user of how to escape
|
||||||
|
messageWidget :: AppS -> Widget Ident
|
||||||
|
messageWidget s = center $ border $
|
||||||
|
padAll 1 (hCenter (str (fromMaybe "Unkown Error!" (s ^. appMessage))))
|
||||||
|
<=> padAll 1 (hCenter (str "(Press any key to continue)"))
|
|
@ -1,64 +0,0 @@
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module Widgets where
|
|
||||||
|
|
||||||
import Brick
|
|
||||||
import Brick.Main
|
|
||||||
import Brick.Widgets.Center
|
|
||||||
import Brick.Widgets.Table
|
|
||||||
import Brick.AttrMap (attrMap)
|
|
||||||
import Graphics.Vty (defAttr)
|
|
||||||
import Graphics.Vty.Input.Events
|
|
||||||
import Control.Monad.IO.Class (liftIO)
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import Lens.Micro
|
|
||||||
|
|
||||||
import JSONTypes
|
|
||||||
import Request
|
|
||||||
import Misc
|
|
||||||
import AppTypes
|
|
||||||
|
|
||||||
select :: Widget Ident -> Widget Ident
|
|
||||||
select = (withAttr (attrName "selected"))
|
|
||||||
|
|
||||||
widgetCons :: JSONMovie -> (Widget Ident, Int, AppS) -> (Widget Ident, Int, AppS)
|
|
||||||
widgetCons m (w, i, s) =
|
|
||||||
embed $ if s ^. appCursor == i
|
|
||||||
then select . visible $
|
|
||||||
if s ^. appExpanded
|
|
||||||
then expandedWidget s m
|
|
||||||
else movieWidget s m
|
|
||||||
else movieWidget s m
|
|
||||||
where
|
|
||||||
embed x = (w <=> x, i+1, s)
|
|
||||||
|
|
||||||
|
|
||||||
movieWidgets :: AppS -> JSONListMovies -> Widget Ident
|
|
||||||
movieWidgets s m = let (items, _, _) = foldr widgetCons (emptyWidget, 0, s) (movies m)
|
|
||||||
in items
|
|
||||||
|
|
||||||
movieWidget :: AppS -> JSONMovie -> Widget Ident
|
|
||||||
movieWidget s m = txt (movie_title m) <+> padLeft Max (str (show (movie_year m)))
|
|
||||||
|
|
||||||
expandedWidget :: AppS -> JSONMovie -> Widget Ident
|
|
||||||
expandedWidget s m = movieWidget s m
|
|
||||||
<=> (padRight (Pad 3) (str "Rating") <+> str (show (movie_rating m)))
|
|
||||||
<=> (padRight (Pad 1) (str "Language") <+> txt (movie_language m))
|
|
||||||
<=> (padRight (Pad 3) (str "Genres") <+> txt (T.intercalate ", " (movie_genres m)))
|
|
||||||
<=> (padRight (Pad 2) (str "Summary") <+> txtWrap (movie_summary m))
|
|
||||||
|
|
||||||
searchWidget :: AppS -> Widget Ident
|
|
||||||
searchWidget = undefined
|
|
||||||
|
|
||||||
browseWidget :: AppS -> Widget Ident
|
|
||||||
browseWidget s =
|
|
||||||
case (s ^. appListing) of
|
|
||||||
Nothing -> center $ str "No movies found matching query."
|
|
||||||
(Just m) -> headings <=> (viewport Listing Vertical (movieWidgets s m))
|
|
||||||
where headings = str "Title" <+> padLeft Max (str "Year")
|
|
||||||
|
|
||||||
errorWidget :: AppS -> Widget Ident
|
|
||||||
errorWidget s = center $
|
|
||||||
case (s ^. appError) of
|
|
||||||
Nothing -> str "Unknown Error."
|
|
||||||
(Just e) -> str e
|
|
Loading…
Reference in New Issue
Block a user