Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

fruitful case study

  • Loading branch information...
commit cd96e18da31a47d945adf4c6e2fd3f918671d119 0 parents
@astro authored
Showing with 9,786 additions and 0 deletions.
  1. +99 −0 Application.hs
  2. +165 −0 Foundation.hs
  3. +96 −0 Handler/Browse.hs
  4. +39 −0 Handler/Home.hs
  5. +28 −0 Import.hs
  6. +25 −0 LICENSE
  7. +81 −0 Model.hs
  8. +63 −0 Settings.hs
  9. +14 −0 Settings/Development.hs
  10. +18 −0 Settings/StaticFiles.hs
  11. +2 −0  config/client_session_key.aes
  12. BIN  config/favicon.ico
  13. +11 −0 config/models
  14. +21 −0 config/postgresql.yml
  15. +1 −0  config/robots.txt
  16. +9 −0 config/routes
  17. +19 −0 config/settings.yml
  18. +97 −0 deploy/Procfile
  19. +26 −0 devel.hs
  20. +8 −0 main.hs
  21. +1 −0  messages/en.msg
  22. +118 −0 prittorrent-hui.cabal
  23. BIN  static/404.jpg
  24. BIN  static/500.jpg
  25. BIN  static/activate-account.png
  26. +52 −0 static/activate.js
  27. BIN  static/atom.png
  28. BIN  static/audio.png
  29. +82 −0 static/audio.svg
  30. BIN  static/bitlove-button.png
  31. BIN  static/d.png
  32. +109 −0 static/d.svg
  33. BIN  static/dl.gif
  34. BIN  static/download.png
  35. +109 −0 static/download.svg
  36. +162 −0 static/edit-feed.js
  37. +152 −0 static/edit-user.js
  38. BIN  static/edit.png
  39. +315 −0 static/error.svg
  40. BIN  static/favicon.png
  41. +205 −0 static/filter.js
  42. +245 −0 static/graphs.js
  43. BIN  static/help-podcaster-feed.png
  44. +295 −0 static/help-podcaster-feed.svg
  45. BIN  static/img/glyphicons-halflings-white.png
  46. BIN  static/img/glyphicons-halflings.png
  47. +4 −0 static/jquery-1.7.1.min.js
  48. +2,697 −0 static/jquery.flot.js
  49. +1,165 −0 static/jsSHA.js
  50. BIN  static/l.png
  51. +142 −0 static/l.svg
  52. +62 −0 static/login.js
  53. BIN  static/logo.png
  54. +260 −0 static/logo.svg
  55. BIN  static/mail-bird.png
  56. BIN  static/podpress-widget.png
  57. BIN  static/powerpress-widget.png
  58. BIN  static/rss.png
  59. BIN  static/s.png
  60. +102 −0 static/s.svg
  61. +19 −0 static/signup.js
  62. BIN  static/stub.png
  63. +264 −0 static/stub.svg
  64. +886 −0 static/style.css
  65. BIN  static/swarm.png
  66. +288 −0 static/swarm.svg
  67. +1 −0  static/tmp/1sifjTMs.js
  68. +160 −0 static/tmp/GKy_B1VG.css
  69. +3 −0  static/tmp/ONzb8a8l.css
  70. +3 −0  static/tmp/UEG9zQsl.css
  71. +1 −0  static/tmp/x0PsQ9Am.css
  72. BIN  static/torrent.png
  73. +253 −0 static/torrent.svg
  74. BIN  static/torrentify.png
  75. +103 −0 static/u.js
  76. BIN  static/video.png
  77. +153 −0 static/video.svg
  78. +66 −0 templates/default-layout-wrapper.hamlet
  79. +3 −0  templates/default-layout.hamlet
  80. +38 −0 templates/homepage.hamlet
  81. +1 −0  templates/homepage.julius
  82. +6 −0 templates/homepage.lucius
  83. +439 −0 templates/normalize.lucius
99 Application.hs
@@ -0,0 +1,99 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Application
+ ( makeApplication
+ , getApplicationDev
+ , makeFoundation
+ ) where
+
+import Import
+import Settings
+import Yesod.Auth
+import Yesod.Default.Config
+import Yesod.Default.Main
+import Yesod.Default.Handlers
+import Yesod.Logger (Logger, logBS, toProduction)
+import Network.Wai.Middleware.RequestLogger (logCallback, logCallbackDev)
+import Network.HTTP.Conduit (newManager, def)
+import Data.Conduit.Pool
+import Database.HDBC as HDBC (disconnect)
+import Database.HDBC.PostgreSQL
+import qualified Data.Aeson as Aeson
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Text as Text
+import Debug.Trace
+
+
+-- Import all relevant handler modules here.
+-- Don't forget to add new modules to your cabal file!
+import Handler.Home
+import Handler.Browse
+
+-- This line actually creates our YesodSite instance. It is the second half
+-- of the call to mkYesodData which occurs in Foundation.hs. Please see
+-- the comments there for more details.
+mkYesodDispatch "App" resourcesApp
+
+-- This function allocates resources (such as a database connection pool),
+-- performs initialization and creates a WAI application. This is also the
+-- place to put your migrate statements to have automatic database
+-- migrations handled by Yesod.
+makeApplication :: AppConfig DefaultEnv Extra -> Logger -> IO Application
+makeApplication conf logger = do
+ foundation <- makeFoundation conf setLogger
+ app <- toWaiAppPlain foundation
+ return $ logWare app
+ where
+ setLogger = if development then logger else toProduction logger
+ logWare = if development then logCallbackDev (logBS setLogger)
+ else logCallback (logBS setLogger)
+
+makeFoundation :: AppConfig DefaultEnv Extra -> Logger -> IO App
+makeFoundation conf setLogger = do
+ manager <- newManager def
+ s <- staticSite
+ dbconf <- withYamlEnvironment
+ "config/postgresql.yml"
+ (appEnv conf)
+ parseDBConf
+ pool <- makeDBPool dbconf
+ return $ App conf setLogger s pool manager
+
+parseDBConf = return . parse
+ where parse (Aeson.Object o) = do
+ (k, v) <- HashMap.toList o
+ let k' = Text.unpack k
+ case v of
+ Aeson.String v' ->
+ return (k', Text.unpack v')
+ (Aeson.Object _) ->
+ parse v
+ (Aeson.Number n) ->
+ return (k', show n)
+ _ ->
+ trace ("Cannot parse: " ++ show v) []
+
+makeDBPool :: [(String, String)] -> IO DBPool
+makeDBPool dbconf =
+ let dbconf' :: [([Char], [Char])]
+ dbconf' = filter (\(k, v) ->
+ k `elem` ["host", "hostaddr",
+ "port", "dbname",
+ "user", "password"]
+ ) dbconf
+ dbconf'' = unwords $
+ map (\(k, v) ->
+ k ++ "=" ++ v
+ ) dbconf'
+ in createPool
+ (connectPostgreSQL dbconf'')
+ HDBC.disconnect
+ 4 5 4
+
+-- for yesod devel
+getApplicationDev :: IO (Int, Application)
+getApplicationDev =
+ defaultDevelApp loader makeApplication
+ where
+ loader = loadConfig (configSettings Development)
+ { csParseExtra = parseExtra
+ }
165 Foundation.hs
@@ -0,0 +1,165 @@
+{-# LANGUAGE RankNTypes #-}
+module Foundation
+ ( App (..)
+ , Route (..)
+ , AppMessage (..)
+ , resourcesApp
+ , Handler
+ , Widget
+ , Form
+ , withDB, DBPool
+ , Period (..)
+ --, maybeAuth
+ --, requireAuth
+ , module Settings
+ , module Model
+ ) where
+
+import Prelude
+import Yesod
+import Yesod.Static
+--import Yesod.Auth
+import Yesod.Default.Config
+import Yesod.Default.Util (addStaticContentExternal)
+import Yesod.Logger (Logger, logMsg, formatLogText)
+import Network.HTTP.Conduit (Manager)
+import qualified Settings
+import Settings.StaticFiles
+import Settings (widgetFile, Extra (..))
+import Model
+import Text.Jasmine (minifym)
+import Web.ClientSession (getKey)
+import Text.Hamlet (hamletFile)
+import Control.Applicative
+import Data.Conduit.Pool
+import Control.Monad.Trans.Resource
+import qualified Database.HDBC.PostgreSQL as PostgreSQL (Connection)
+import qualified Data.Text as T
+
+
+type DBPool = Pool PostgreSQL.Connection
+
+
+data Period = PeriodDays Int
+ | PeriodAll
+ deriving (Show, Eq, Read, Ord)
+
+instance PathPiece Period where
+ fromPathPiece text =
+ case T.unpack text of
+ "1" -> Just $ PeriodDays 1
+ "7" -> Just $ PeriodDays 7
+ "30" -> Just $ PeriodDays 30
+ "all" -> Just $ PeriodAll
+ _ -> Nothing
+
+-- | The site argument for your application. This can be a good place to
+-- keep settings and values requiring initialization before your application
+-- starts running, such as database connections. Every handler will have
+-- access to the data present here.
+data App = App
+ { settings :: AppConfig DefaultEnv Extra
+ , getLogger :: Logger
+ , getStatic :: Static -- ^ Settings for static file serving.
+ , dbPool :: DBPool -- ^ Database connection pool.
+ , httpManager :: Manager
+ }
+
+-- Set up i18n messages. See the message folder.
+mkMessage "App" "messages" "en"
+
+-- This is where we define all of the routes in our application. For a full
+-- explanation of the syntax, please see:
+-- http://www.yesodweb.com/book/handler
+--
+-- This function does three things:
+--
+-- * Creates the route datatype AppRoute. Every valid URL in your
+-- application can be represented as a value of this type.
+-- * Creates the associated type:
+-- type instance Route App = AppRoute
+-- * Creates the value resourcesApp which contains information on the
+-- resources declared below. This is used in Handler.hs by the call to
+-- mkYesodDispatch
+--
+-- What this function does *not* do is create a YesodSite instance for
+-- App. Creating that instance requires all of the handler functions
+-- for our application to be in scope. However, the handler functions
+-- usually require access to the AppRoute datatype. Therefore, we
+-- split these actions into two functions and place them in separate files.
+mkYesodData "App" $(parseRoutesFile "config/routes")
+
+type Form x = Html -> MForm App App (FormResult x, Widget)
+
+-- Please see the documentation for the Yesod typeclass. There are a number
+-- of settings which can be configured by overriding methods here.
+instance Yesod App where
+ approot = ApprootMaster $ appRoot . settings
+
+ {-
+ -- Store session data on the client in encrypted cookies,
+ -- default session idle timeout is 120 minutes
+ makeSessionBackend _ = do
+ key <- getKey "config/client_session_key.aes"
+ return . Just $ clientSessionBackend key 120
+ -}
+
+ defaultLayout widget = do
+ master <- getYesod
+ mmsg <- getMessage
+
+ -- We break up the default layout into two components:
+ -- default-layout is the contents of the body tag, and
+ -- default-layout-wrapper is the entire page. Since the final
+ -- value passed to hamletToRepHtml cannot be a widget, this allows
+ -- you to use normal widget features in default-layout.
+
+ pc <- widgetToPageContent $ do
+ $(widgetFile "default-layout")
+ hamletToRepHtml $(hamletFile "templates/default-layout-wrapper.hamlet")
+
+ -- This is done to provide an optimization for serving static files from
+ -- a separate domain. Please see the staticRoot setting in Settings.hs
+ urlRenderOverride y (StaticR s) =
+ Just $ uncurry (joinPath y (Settings.staticRoot $ settings y)) $ renderRoute s
+ urlRenderOverride _ _ = Nothing
+
+ -- The page to be redirected to when authentication is required.
+ --authRoute _ = Just $ AuthR LoginR
+
+ messageLogger y loc level msg =
+ formatLogText (getLogger y) loc level msg >>= logMsg (getLogger y)
+
+ -- This function creates static content files in the static folder
+ -- and names them based on a hash of their content. This allows
+ -- expiration dates to be set far in the future without worry of
+ -- users receiving stale content.
+ addStaticContent = addStaticContentExternal minifym base64md5 Settings.staticDir (StaticR . flip StaticRoute [])
+
+ -- Place Javascript at bottom of the body tag so the rest of the page loads first
+ jsLoader _ = BottomOfBody
+
+-- How to run database actions.
+withDB :: (PostgreSQL.Connection -> IO a) -> Handler a
+withDB f = do
+ pool <- dbPool <$> getYesod
+ -- TODO: use takeResourceCheck, catch f
+ db <- takeResource pool
+ a <- lift $ lift $
+ f $ mrValue db
+ mrReuse db True
+ mrRelease db
+ return a
+
+
+-- This instance is required to use forms. You can modify renderMessage to
+-- achieve customized and internationalized form validation messages.
+instance RenderMessage App FormMessage where
+ renderMessage _ _ = defaultFormMessage
+
+-- Note: previous versions of the scaffolding included a deliver function to
+-- send emails. Unfortunately, there are too many different options for us to
+-- give a reasonable default. Instead, the information is available on the
+-- wiki:
+--
+-- https://github.com/yesodweb/yesod/wiki/Sending-email
96 Handler/Browse.hs
@@ -0,0 +1,96 @@
+{-# LANGUAGE TupleSections, OverloadedStrings #-}
+module Handler.Browse where
+
+import qualified Data.Text as T
+import Data.Maybe
+import Data.Time.Format
+import System.Locale
+
+import qualified Model
+import Import
+
+
+getNewR :: Handler RepHtml
+getNewR = do
+ downloads <- withDB $
+ Model.recentDownloads 50
+ defaultLayout $ do
+ setTitle "Bitlove: New Torrents"
+ toWidget [hamlet|
+<h2>New Torrents</h2>
+^{renderDownloads downloads}
+|]
+
+getTopR :: Handler RepHtml
+getTopR = do
+ downloads <- withDB $
+ Model.popularDownloads 25
+ defaultLayout $ do
+ setTitle "Bitlove: Popular Torrents"
+ toWidget [hamlet|
+<h2>Popular Torrents</h2>
+^{renderDownloads downloads}
+|]
+
+getTopDownloadedR :: Period -> Handler RepHtml
+getTopDownloadedR period = do
+ let (p, period_days) =
+ case period of
+ PeriodDays 1 -> (1, "1 day")
+ PeriodDays days -> (days, show days ++ " days")
+ PeriodAll -> (10000, "all time")
+ downloads <- withDB $
+ Model.mostDownloaded 10 p
+ lift $ lift $ putStrLn $ "render " ++ (show $ length downloads) ++ " downloads"
+ defaultLayout $ do
+ setTitle "Bitlove: Top Downloaded"
+ toWidget [hamlet|
+^{renderDownloads downloads}
+|]
+
+renderDownloads downloads =
+ let formatDate = formatTime defaultTimeLocale (iso8601DateFormat Nothing ++ " %H:%M") .
+ downloadPublished
+ in [hamlet|
+$forall d <- downloads
+ <article class="item">
+ <div>
+ $if not (T.null $ downloadImage d)
+ <img src="" class="logo">
+ <div class="right">
+ <p class="published">#{formatDate d}
+ <div class="flattr">
+ <div class="title">
+ <h3>
+ <a href="">#{downloadTitle d}
+ <p class="feed">
+ \ in #
+ <a href="">#{fromMaybe T.empty $ downloadFeedTitle d}
+ \ by #
+ <a href="">#{downloadUser d}
+ <ul class="download">
+ <li class="torrent">
+ <a href="" rel="enclosure" data-type=#{downloadType d}>
+ #{downloadName d} #
+ <span class="size" title="Download size">
+ #{humanSize (downloadSize d)}
+ <li class="stats">
+ <dl class="seeders">
+ <dt>#{downloadSeeders d}
+ <dd>Seeders
+ <dl class="leechers">
+ <dt>#{downloadLeechers d}
+ <dd>Leechers
+ <dl class="downloads">
+ <dt>#{downloadDownloaded d}
+ <dd>Downloads
+|]
+
+humanSize = humanSize' "KMGT" ""
+ where humanSize' units unit n
+ | n < 1024 || null units =
+ show n ++ " " ++ unit ++ "B"
+ | otherwise =
+ let (unit':units') = units
+ in humanSize' units' [unit'] $ n `div` 1024
+
39 Handler/Home.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE TupleSections, OverloadedStrings #-}
+module Handler.Home where
+
+import Import
+
+-- This is a handler function for the GET request method on the HomeR
+-- resource pattern. All of your resource patterns are defined in
+-- config/routes
+--
+-- The majority of the code you will write in Yesod lives in these handler
+-- functions. You can spread them across multiple files if you are so
+-- inclined, or create a single monolithic file.
+getHomeR :: Handler RepHtml
+getHomeR = do
+ (formWidget, formEnctype) <- generateFormPost sampleForm
+ let submission = Nothing :: Maybe (FileInfo, Text)
+ handlerName = "getHomeR" :: Text
+ defaultLayout $ do
+ aDomId <- lift newIdent
+ setTitle "Welcome To Yesod!"
+ $(widgetFile "homepage")
+
+postHomeR :: Handler RepHtml
+postHomeR = do
+ ((result, formWidget), formEnctype) <- runFormPost sampleForm
+ let handlerName = "postHomeR" :: Text
+ submission = case result of
+ FormSuccess res -> Just res
+ _ -> Nothing
+
+ defaultLayout $ do
+ aDomId <- lift newIdent
+ setTitle "Welcome To Yesod!"
+ $(widgetFile "homepage")
+
+sampleForm :: Form (FileInfo, Text)
+sampleForm = renderDivs $ (,)
+ <$> fileAFormReq "Choose a file"
+ <*> areq textField "What's on the file?" Nothing
28 Import.hs
@@ -0,0 +1,28 @@
+module Import
+ ( module Prelude
+ , module Yesod
+ , module Foundation
+ , module Settings.StaticFiles
+ , module Settings.Development
+ , module Data.Monoid
+ , module Control.Applicative
+ , Text
+#if __GLASGOW_HASKELL__ < 704
+ , (<>)
+#endif
+ ) where
+
+import Prelude hiding (writeFile, readFile, head, tail, init, last)
+import Yesod hiding (Route(..))
+import Foundation
+import Data.Monoid (Monoid (mappend, mempty, mconcat))
+import Control.Applicative ((<$>), (<*>), pure)
+import Data.Text (Text)
+import Settings.StaticFiles
+import Settings.Development
+
+#if __GLASGOW_HASKELL__ < 704
+infixr 5 <>
+(<>) :: Monoid m => m -> m -> m
+(<>) = mappend
+#endif
25 LICENSE
@@ -0,0 +1,25 @@
+The following license covers this documentation, and the source code, except
+where otherwise indicated.
+
+Copyright 2012, Astro. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
81 Model.hs
@@ -0,0 +1,81 @@
+{-# LANGUAGE FlexibleInstances, RankNTypes #-}
+module Model where
+
+import Prelude
+import Yesod
+import Data.Text (Text)
+import Database.HDBC
+import Data.Convertible
+import Data.Time.LocalTime (LocalTime)
+import Data.ByteString (ByteString)
+
+
+class Structurable e where
+ structureResult :: [SqlValue] -> e
+
+
+data Download = Download {
+ downloadUser :: Text,
+ downloadSlug :: Text,
+ downloadFeed :: Text,
+ downloadItem :: Text,
+ downloadEnclosure :: Maybe Text,
+ downloadInfoHash :: ByteString,
+ downloadName :: Text,
+ downloadSize :: Integer,
+ downloadType :: Text,
+ downloadFeedTitle :: Maybe Text,
+ downloadTitle :: Text,
+ downloadLang :: Maybe Text,
+ downloadSummary :: Maybe Text,
+ downloadPublished :: LocalTime,
+ downloadHomepage :: Text,
+ downloadPayment :: Text,
+ downloadImage :: Text,
+ downloadSeeders :: Integer,
+ downloadLeechers :: Integer,
+ downloadUpspeed :: Integer,
+ downloadDownspeed :: Integer,
+ downloadDownloaded :: Integer
+} deriving (Show{-, Typeable-})
+
+instance Structurable Download where
+ structureResult (user:slug:feed:item:enclosure:
+ feed_title:feed_public:
+ info_hash:name:size:type_:
+ title:lang:summary:published:
+ homepage:payment:image:
+ seeders:leechers:upspeed:downspeed:downloaded:_) =
+ Download (fromSql user) (fromSql slug) (fromSql feed) (fromSql item) (fromSql enclosure)
+ (fromSql info_hash) (fromSql name) (fromSql size) (fromSql type_)
+ (fromSql feed_title) (fromSql title) (fromSql lang) (fromSql summary) (fromSql published)
+ (fromSql homepage) (fromSql payment) (fromSql image)
+ (fromSql seeders) (fromSql leechers) (fromSql upspeed) (fromSql downspeed) (fromSql downloaded)
+ structureResult vals = error $ "Cannot structure " ++ show vals
+
+
+query :: (IConnection conn,
+ Convertible arg SqlValue,
+ Structurable e
+ ) => String -> [arg] -> conn -> IO [e]
+query sql args conn =
+ (map structureResult) `fmap`
+ (quickQuery' conn sql (map toSql args))
+
+
+{-groupDownloads :: [Download] -> [[Download]]
+groupDownloads-}
+
+type DownloadsQuery = IConnection conn => conn -> IO [Download]
+
+recentDownloads :: Int -> DownloadsQuery
+recentDownloads limit =
+ query "SELECT * FROM get_recent_downloads(?)" [limit]
+
+popularDownloads :: Int -> DownloadsQuery
+popularDownloads limit =
+ query "SELECT * FROM get_popular_downloads(?)" [limit]
+
+mostDownloaded :: Int -> Int -> DownloadsQuery
+mostDownloaded limit period =
+ query "SELECT * FROM get_most_downloaded(?, ?)" [limit, period]
63 Settings.hs
@@ -0,0 +1,63 @@
+-- | Settings are centralized, as much as possible, into this file. This
+-- includes database connection settings, static file locations, etc.
+-- In addition, you can configure a number of different aspects of Yesod
+-- by overriding methods in the Yesod typeclass. That instance is
+-- declared in the Foundation.hs file.
+module Settings
+ ( widgetFile
+ , staticRoot
+ , staticDir
+ , Extra (..)
+ , parseExtra
+ ) where
+
+import Prelude
+import Text.Shakespeare.Text (st)
+import Language.Haskell.TH.Syntax
+import Yesod.Default.Config
+import qualified Yesod.Default.Util
+import Data.Text (Text)
+import Data.Yaml
+import Control.Applicative
+import Settings.Development
+
+-- Static setting below. Changing these requires a recompile
+
+-- | The location of static files on your system. This is a file system
+-- path. The default value works properly with your scaffolded site.
+staticDir :: FilePath
+staticDir = "static"
+
+-- | The base URL for your static files. As you can see by the default
+-- value, this can simply be "static" appended to your application root.
+-- A powerful optimization can be serving static files from a separate
+-- domain name. This allows you to use a web server optimized for static
+-- files, more easily set expires and cache values, and avoid possibly
+-- costly transference of cookies on static files. For more information,
+-- please see:
+-- http://code.google.com/speed/page-speed/docs/request.html#ServeFromCookielessDomain
+--
+-- If you change the resource pattern for StaticR in Foundation.hs, you will
+-- have to make a corresponding change here.
+--
+-- To see how this value is used, see urlRenderOverride in Foundation.hs
+staticRoot :: AppConfig DefaultEnv x -> Text
+staticRoot conf = [st|#{appRoot conf}/static|]
+
+
+-- The rest of this file contains settings which rarely need changing by a
+-- user.
+
+widgetFile :: String -> Q Exp
+widgetFile = if development then Yesod.Default.Util.widgetFileReload
+ else Yesod.Default.Util.widgetFileNoReload
+
+data Extra = Extra
+ { extraCopyright :: Text
+ , extraAnalytics :: Maybe Text -- ^ Google Analytics
+ } deriving Show
+
+parseExtra :: DefaultEnv -> Object -> Parser Extra
+parseExtra _ o = Extra
+ <$> o .: "copyright"
+ <*> o .:? "analytics"
14 Settings/Development.hs
@@ -0,0 +1,14 @@
+module Settings.Development where
+
+import Prelude
+
+development :: Bool
+development =
+#if DEVELOPMENT
+ True
+#else
+ False
+#endif
+
+production :: Bool
+production = not development
18 Settings/StaticFiles.hs
@@ -0,0 +1,18 @@
+module Settings.StaticFiles where
+
+import Prelude (IO)
+import Yesod.Static
+import qualified Yesod.Static as Static
+import Settings (staticDir)
+import Settings.Development
+
+-- | use this to create your static file serving site
+staticSite :: IO Static.Static
+staticSite = if development then Static.staticDevel staticDir
+ else Static.static staticDir
+
+-- | This generates easy references to files in the static directory at compile time,
+-- giving you compile-time verification that referenced files exist.
+-- Warning: any files added to your static directory during run-time can't be
+-- accessed this way. You'll have to use their FilePath or URL to access them.
+$(staticFiles Settings.staticDir)
2  config/client_session_key.aes
@@ -0,0 +1,2 @@
+Do�PVO$p��|���3�n�J� ���!y�wN8��8�_��:
+�dư�5cIR׌�]>����k��B�����`��T��D|��j!�Z��
BIN  config/favicon.ico
Binary file not shown
11 config/models
@@ -0,0 +1,11 @@
+User
+ ident Text
+ password Text Maybe
+ UniqueUser ident
+Email
+ email Text
+ user UserId Maybe
+ verkey Text Maybe
+ UniqueEmail email
+
+ -- By default this file is used in Model.hs (which is imported by Foundation.hs)
21 config/postgresql.yml
@@ -0,0 +1,21 @@
+Default: &defaults
+ user: prittorrent
+ password: 1234
+ host: localhost
+ port: 5432
+ dbname: prittorrent
+ poolsize: 10
+
+Development:
+ <<: *defaults
+
+Testing:
+ <<: *defaults
+
+Staging:
+ poolsize: 10
+ <<: *defaults
+
+Production:
+ poolsize: 10
+ <<: *defaults
1  config/robots.txt
@@ -0,0 +1 @@
+User-agent: *
9 config/routes
@@ -0,0 +1,9 @@
+/static StaticR Static getStatic
+
+/favicon.ico FaviconR GET
+/robots.txt RobotsR GET
+
+/ HomeR GET POST
+/new NewR GET
+/top TopR GET
+/top/#Period TopDownloadedR GET
19 config/settings.yml
@@ -0,0 +1,19 @@
+Default: &defaults
+ host: "*6"
+ port: 8081
+ approot: "/"
+ copyright: APL2
+
+Development:
+ <<: *defaults
+
+Testing:
+ <<: *defaults
+
+Staging:
+ <<: *defaults
+
+Production:
+ port: 80
+ approot: "http://bitlove.org"
+ <<: *defaults
97 deploy/Procfile
@@ -0,0 +1,97 @@
+# Free deployment to Heroku.
+#
+# !! Warning: You must use a 64 bit machine to compile !!
+#
+# This could mean using a virtual machine. Give your VM as much memory as you can to speed up linking.
+#
+# Basic Yesod setup:
+#
+# * Move this file out of the deploy directory and into your root directory
+#
+# mv deploy/Procfile ./
+#
+# * Create an empty package.json
+# echo '{ "name": "prittorrent-hui", "version": "0.0.1", "dependencies": {} }' >> package.json
+#
+# Postgresql Yesod setup:
+#
+# * add a dependency on the "heroku" package in your cabal file
+#
+# * add code in Application.hs to use the heroku package and load the connection parameters.
+# The below works for Postgresql.
+#
+# #ifndef DEVELOPMENT
+# import qualified Web.Heroku
+# #endif
+#
+#
+# makeApplication :: AppConfig DefaultEnv Extra -> Logger -> IO Application
+# makeApplication conf logger = do
+# manager <- newManager def
+# s <- staticSite
+# hconfig <- loadHerokuConfig
+# dbconf <- withYamlEnvironment "config/postgresql.yml" (appEnv conf)
+# (Database.Persist.Store.loadConfig . combineMappings hconfig) >>=
+# Database.Persist.Store.applyEnv
+# p <- Database.Persist.Store.createPoolConfig (dbconf :: Settings.PersistConfig)
+# Database.Persist.Store.runPool dbconf (runMigration migrateAll) p
+# let foundation = App conf setLogger s p manager dbconf
+# app <- toWaiAppPlain foundation
+# return $ logWare app
+# where
+##ifdef DEVELOPMENT
+# logWare = logCallbackDev (logBS setLogger)
+# setLogger = logger
+##else
+# setLogger = toProduction logger -- by default the logger is set for development
+# logWare = logCallback (logBS setLogger)
+##endif
+#
+# #ifndef DEVELOPMENT
+# canonicalizeKey :: (Text, val) -> (Text, val)
+# canonicalizeKey ("dbname", val) = ("database", val)
+# canonicalizeKey pair = pair
+#
+# toMapping :: [(Text, Text)] -> AT.Value
+# toMapping xs = AT.Object $ M.fromList $ map (\(key, val) -> (key, AT.String val)) xs
+# #endif
+#
+# combineMappings :: AT.Value -> AT.Value -> AT.Value
+# combineMappings (AT.Object m1) (AT.Object m2) = AT.Object $ m1 `M.union` m2
+# combineMappings _ _ = error "Data.Object is not a Mapping."
+#
+# loadHerokuConfig :: IO AT.Value
+# loadHerokuConfig = do
+# #ifdef DEVELOPMENT
+# return $ AT.Object M.empty
+# #else
+# Web.Heroku.dbConnParams >>= return . toMapping . map canonicalizeKey
+# #endif
+
+
+
+# Heroku setup:
+# Find the Heroku guide. Roughly:
+#
+# * sign up for a heroku account and register your ssh key
+# * create a new application on the *cedar* stack
+#
+# * make your Yesod project the git repository for that application
+# * create a deploy branch
+#
+# git checkout -b deploy
+#
+# Repeat these steps to deploy:
+# * add your web executable binary (referenced below) to the git repository
+#
+# git checkout deploy
+# git add ./dist/build/prittorrent-hui/prittorrent-hui
+# git commit -m deploy
+#
+# * push to Heroku
+#
+# git push heroku deploy:master
+
+
+# Heroku configuration that runs your app
+web: ./dist/build/prittorrent-hui/prittorrent-hui production -p $PORT
26 devel.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE PackageImports #-}
+import "prittorrent-hui" Application (getApplicationDev)
+import Network.Wai.Handler.Warp
+ (runSettings, defaultSettings, settingsPort)
+import Control.Concurrent (forkIO)
+import System.Directory (doesFileExist, removeFile)
+import System.Exit (exitSuccess)
+import Control.Concurrent (threadDelay)
+
+main :: IO ()
+main = do
+ putStrLn "Starting devel application"
+ (port, app) <- getApplicationDev
+ forkIO $ runSettings defaultSettings
+ { settingsPort = port
+ } app
+ loop
+
+loop :: IO ()
+loop = do
+ threadDelay 100000
+ e <- doesFileExist "dist/devel-terminate"
+ if e then terminateDevel else loop
+
+terminateDevel :: IO ()
+terminateDevel = exitSuccess
8 main.hs
@@ -0,0 +1,8 @@
+import Prelude (IO)
+import Yesod.Default.Config (fromArgs)
+import Yesod.Default.Main (defaultMain)
+import Settings (parseExtra)
+import Application (makeApplication)
+
+main :: IO ()
+main = defaultMain (fromArgs parseExtra) makeApplication
1  messages/en.msg
@@ -0,0 +1 @@
+Hello: Hello
118 prittorrent-hui.cabal
@@ -0,0 +1,118 @@
+name: prittorrent-hui
+version: 0.0.0
+license: BSD3
+license-file: LICENSE
+author: Astro
+maintainer: Astro
+synopsis: The greatest Yesod web application ever.
+description: I'm sure you can say something clever here if you try.
+category: Web
+stability: Experimental
+cabal-version: >= 1.8
+build-type: Simple
+homepage: http://prittorrent-hui.yesodweb.com/
+
+Flag dev
+ Description: Turn on development settings, like auto-reload templates.
+ Default: False
+
+Flag library-only
+ Description: Build for use with "yesod devel"
+ Default: False
+
+library
+ exposed-modules: Application
+ Foundation
+ Import
+ Model
+ Settings
+ Settings.StaticFiles
+ Settings.Development
+ Handler.Home
+ Handler.Browse
+
+ if flag(dev) || flag(library-only)
+ cpp-options: -DDEVELOPMENT
+ ghc-options: -Wall -threaded -O0 -with-rtsopts=-N
+ else
+ ghc-options: -Wall -threaded -O2 -with-rtsopts=-N
+
+ extensions: TemplateHaskell
+ QuasiQuotes
+ OverloadedStrings
+ NoImplicitPrelude
+ CPP
+ MultiParamTypeClasses
+ TypeFamilies
+ GADTs
+ GeneralizedNewtypeDeriving
+ FlexibleContexts
+ EmptyDataDecls
+ NoMonomorphismRestriction
+
+ build-depends: base >= 4 && < 5
+ , yesod-platform >= 1.0 && < 1.1
+ , yesod >= 1.0 && < 1.1
+ , yesod-core >= 1.0 && < 1.1
+ , yesod-auth >= 1.0 && < 1.1
+ , yesod-static >= 1.0 && < 1.1
+ , yesod-default >= 1.0 && < 1.1
+ , yesod-form >= 1.0 && < 1.1
+ , yesod-test >= 0.2 && < 0.3
+ , clientsession >= 0.7.3 && < 0.8
+ , bytestring >= 0.9 && < 0.10
+ , text >= 0.11 && < 0.12
+ , template-haskell
+ , hamlet >= 1.0 && < 1.1
+ , shakespeare-css >= 1.0 && < 1.1
+ , shakespeare-js >= 1.0 && < 1.1
+ , shakespeare-text >= 1.0 && < 1.1
+ , hjsmin >= 0.1 && < 0.2
+ , monad-control >= 0.3 && < 0.4
+ , wai-extra >= 1.2 && < 1.3
+ , yaml >= 0.7 && < 0.8
+ , http-conduit >= 1.4 && < 1.5
+ , directory >= 1.1 && < 1.2
+ , warp >= 1.2 && < 1.3
+ , pool-conduit >= 0.1.0.2 && < 0.2
+ , HDBC >= 2.3.1.1 && < 2.4
+ , HDBC-postgresql >= 2.3.2.1 && < 2.4
+ , resourcet >= 0.3.2.1 && < 0.4
+ , unordered-containers >= 0.2.1.0 && < 0.3
+ , aeson >= 0.6.0.2 && < 0.7
+ , convertible >= 1.0.11.0 && < 1.1
+ , time >= 1.4 && < 1.5
+ , old-locale >= 1.0.0.4 && < 1.1
+
+executable prittorrent-hui
+ if flag(library-only)
+ Buildable: False
+
+ main-is: ../main.hs
+ hs-source-dirs: dist
+ build-depends: base
+ , prittorrent-hui
+ , yesod-default
+
+test-suite test
+ type: exitcode-stdio-1.0
+ main-is: main.hs
+ hs-source-dirs: tests
+ ghc-options: -Wall
+ extensions: TemplateHaskell
+ QuasiQuotes
+ OverloadedStrings
+ NoImplicitPrelude
+ CPP
+ OverloadedStrings
+ MultiParamTypeClasses
+ TypeFamilies
+ GADTs
+ GeneralizedNewtypeDeriving
+ FlexibleContexts
+
+ build-depends: base
+ , prittorrent-hui
+ , yesod-test
+ , yesod-default
+ , yesod-core
BIN  static/404.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  static/500.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  static/activate-account.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 static/activate.js
@@ -0,0 +1,52 @@
+$('#activate').click(function(ev) {
+ var activate = $('#activate');
+ activate.hide();
+ ev.preventDefault();
+
+ var token = activate.attr('data-token');
+ var salt = activate.attr('data-salt');
+ var password1 = $('#password1').val();
+ var password2 = $('#password2').val();
+
+ function progress(s) {
+ $('#progress').text(s);
+ }
+ function fail(s) {
+ progress(s);
+ activate.show();
+ return;
+ }
+
+ /* Validate form data */
+ if (password1 != password2)
+ return fail("Passwords must match");
+
+ /* Calculate salted password */
+ progress("Salting password");
+ var salted = hmac(salt, password1);
+
+ /* Request salt+challenge for hashing */
+ progress("Requesting account activation...");
+ $.ajax({ type: 'POST',
+ data: {
+ salted: salted
+ },
+ url: '/activate/' + token,
+ success: function(response) {
+ if (response && response.welcome) {
+ progress("Welcome to Bitlove!");
+ document.location = response.welcome;
+ } else {
+ fail((response && response.error) || "Cannot activate");
+ }
+ },
+ error: function() {
+ fail("Error sending request");
+ }
+ });
+});
+
+function hmac(key, text) {
+ var hmac = new jsSHA(text, 'ASCII');
+ return hmac.getHMAC(key, 'HEX', 'SHA-1', 'HEX');
+}
BIN  static/atom.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  static/audio.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 static/audio.svg
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="16"
+ height="16"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="audio.svg"
+ inkscape:export-filename="/home/stephan/programming/erlang/prittorrent/apps/ui/priv/static/audio.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="11.490936"
+ inkscape:cy="6.9066784"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="1436"
+ inkscape:window-height="877"
+ inkscape:window-x="0"
+ inkscape:window-y="19"
+ inkscape:window-maximized="0"
+ showguides="true"
+ inkscape:guide-bbox="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2987"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1036.3622)">
+ <path
+ style="fill:#008000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 9,1041.3622 5,-4 0,14 -5,-4 z"
+ id="rect3137"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccc" />
+ <rect
+ style="fill:#008000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect3140"
+ width="5"
+ height="6.0000005"
+ x="3"
+ y="1041.3622" />
+ </g>
+</svg>
BIN  static/bitlove-button.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  static/d.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 static/d.svg
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="11"
+ height="9"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="d.svg"
+ inkscape:export-filename="/home/stephan/programming/erlang/prittorrent/apps/ui/priv/static/d.png"
+ inkscape:export-xdpi="98.690002"
+ inkscape:export-ydpi="98.690002">
+ <defs
+ id="defs4">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 5.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="11 : 5.5 : 1"
+ inkscape:persp3d-origin="5.5 : 3.6666667 : 1"
+ id="perspective2990" />
+ <linearGradient
+ id="linearGradient3762">
+ <stop
+ style="stop-color:#008000;stop-opacity:1;"
+ offset="0"
+ id="stop3764" />
+ <stop
+ style="stop-color:#008000;stop-opacity:0;"
+ offset="1"
+ id="stop3766" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3762"
+ id="linearGradient3768"
+ x1="5.2363634"
+ y1="14.545455"
+ x2="30.060606"
+ y2="-16.363636"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.64453125,0,0,0.55,0.625,1041.3622)" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="22.627417"
+ inkscape:cx="8.3573852"
+ inkscape:cy="9.9126933"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="956"
+ inkscape:window-height="1057"
+ inkscape:window-x="2400"
+ inkscape:window-y="19"
+ inkscape:window-maximized="0"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="0px"
+ originy="0px" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1043.3622)">
+ <path
+ style="fill:url(#linearGradient3768);fill-opacity:1;fill-rule:evenodd;stroke:none"
+ d="m 2,1046.3622 2,3 5,-6 2,2 -7,7 0,0 -4,-4 z"
+ id="rect2991"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ </g>
+</svg>
BIN  static/dl.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  static/download.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
109 static/download.svg
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="12"
+ height="16"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.1 r9760"
+ sodipodi:docname="download.svg"
+ inkscape:export-filename="/home/stephan/programming/erlang/prittorrent/apps/ui/priv/static/download.png"
+ inkscape:export-xdpi="98.690002"
+ inkscape:export-ydpi="98.690002">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3762">
+ <stop
+ style="stop-color:#008000;stop-opacity:1;"
+ offset="0"
+ id="stop3764" />
+ <stop
+ style="stop-color:#008000;stop-opacity:0;"
+ offset="1"
+ id="stop3766" />
+ </linearGradient>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="32"
+ inkscape:cx="5.3939298"
+ inkscape:cy="9.0288098"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="1035"
+ inkscape:window-height="1057"
+ inkscape:window-x="1440"
+ inkscape:window-y="19"
+ inkscape:window-maximized="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-1036.3622)">
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#008000;stroke-linejoin:round;stroke-opacity:1"
+ d="m 1,1037.3622 5,0 5,4 0,10 -10,0 z"
+ id="rect3233"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="rect2987-7-6-3"
+ d="m 2,1047.3622 8,0 -4,3 z"
+ style="fill:#009f00;fill-opacity:1;fill-rule:evenodd;stroke:none" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="rect2987-7-2"
+ d="m 2,1044.3622 8,0 -4,3 z"
+ style="fill:#00c000;fill-opacity:1;fill-rule:evenodd;stroke:none" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="rect2987"
+ d="m 2,1041.3622 8,0 -4,3 z"
+ style="fill:#3fff3f;fill-opacity:1;fill-rule:evenodd;stroke:none" />
+ <path
+ style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#008000;stroke-width:0.80000001;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0"
+ d="m 6,1037.3622 5,4 -5,0 z"
+ id="rect4004"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ </g>
+</svg>
162 static/edit-feed.js
@@ -0,0 +1,162 @@
+function LightBox() {
+ this.el_background = $("<div class='lightboxbackground'></div>");
+ this.el = $("<div class='lightbox'></div>");
+ this.el_background.append(this.el);
+ $('body').append(this.el_background);
+}
+LightBox.prototype = {
+ remove: function() {
+ this.el.remove();
+ this.el_background.remove();
+ },
+ find: function(key) {
+ return this.el.find(key);
+ },
+ content: function(contents) {
+ this.el.empty();
+ this.el.append(contents);
+ }
+};
+
+/**
+ * Edit feed details
+ */
+
+var editButton = $("<p class='edit button'>Edit</p>");
+$('.meta').before(editButton);
+var detailsPath = document.location.pathname + "/details.json";
+editButton.bind('click', function() {
+ var box = new LightBox();
+ box.content('<p>Retrieving...</p>');
+
+ $.ajax({ url: detailsPath,
+ success: function(response) {
+ box.content("<form class='feededit'>" +
+ "<h2>Edit feed</h2>" +
+ "<p><input type='checkbox' id='public'> <label for='public'>Public</label> " +
+ "<span class='hint'>Show this feed on your user page and in public listings? You should enable this once everything works.</span></p>" +
+ "<p><input type='checkbox' id='settitle'> <label for='settitle'>Overwrite title</label></p>" +
+ "<p class='titleline'><label for='title'>Title:</label> <input id='title'></p>" +
+ "<input type='reset' class='cancel button' value='Cancel'>" +
+ "<input type='submit' class='save button' value='Save'>" +
+ "</form>");
+ box.find('#public').prop('checked', response.public);
+ box.find('#settitle').prop('checked', response.title && response.title.length > 0);
+ box.find('#title').val(response.title);
+ function titleVisibility() {
+ var titleline = box.find('.titleline');
+ if (box.find('#settitle').prop('checked'))
+ titleline.show();
+ else
+ titleline.hide();
+ }
+ box.find('#settitle').bind('change', titleVisibility);
+ titleVisibility();
+ box.find('.cancel').click(box.remove.bind(box));
+ box.find('.save').click(function(ev) {
+ ev.preventDefault();
+
+ var data = {
+ 'public': box.find('#public').prop('checked')
+ };
+ if (box.find('#settitle').prop('checked'))
+ data.title = box.find('#title').val();
+ box.content("<p>Submitting...</p>");
+ $.ajax({ type: 'POST',
+ url: detailsPath,
+ data: data,
+ success: function() {
+ /* Force refresh: */
+ document.location.search = "?" + Math.ceil(Math.random() * 999);
+ },
+ error: function() {
+ box.content("<p>Cannot submit</p>" +
+ "<p class='button'>Close</p>");
+ box.find('.button').click(box.remove.bind(box));
+ }
+ });
+ });
+ },
+ error: function() {
+ box.content("<p>An error occured</p>" +
+ "<p class='button'>Close</p>");
+ box.find('.button').click(box.remove.bind(box));
+ }
+ });
+});
+
+/**
+ * Remove feed button
+ */
+var rmButton = $("<p class='rm button'>Delete</p>");
+editButton.before(rmButton);
+rmButton.bind('click', function() {
+ var box = new LightBox();
+ box.content("<form>" +
+ "<h2>Delete feed</h2>" +
+ "<p class='hint'>You should not permanently remove feeds that your audience has subscribed. It is very inconvenient for them.</p>" +
+ "<p>Are you sure?</p>" +
+ "<input type='reset' class='cancel button' value='Cancel'>" +
+ "<input type='submit' class='save button' value='Delete'>" +
+ "</form>");
+ box.find('.cancel').click(box.remove.bind(box));
+ box.find('.save').click(function(ev) {
+ ev.preventDefault();
+
+ var path = document.location.pathname;
+ $.ajax({ type: 'DELETE',
+ url: path,
+ success: function(response) {
+ box.content("<p>Rest in peace, little feed.</p>" +
+ "<p class='button'>Sorry</p>");
+ box.find('.button').click(function() {
+ box.remove();
+ if (response && response.link)
+ document.location = response.link;
+ });
+ },
+ error: function() {
+ box.content("<p>Cannot submit</p>" +
+ "<p class='button'>Close</p>");
+ box.find('.button').click(box.remove.bind(box));
+ }
+ });
+ });
+});
+
+/**
+ * Remove torrent buttons
+ */
+$('.download').each(function() {
+ var download = $(this);
+ var purgeButton = $("<p class='purge button'>Purge</p>");
+ download.before(purgeButton);
+ purgeButton.click(function() {
+ var box = new LightBox();
+ box.content("<form>" +
+ "<h2>Purge enclosure</h2>" +
+ "<p class='hint'>Use this to remove or regenerate a torrent, depending on whether the enclosure is still present in the feed.</p>" +
+ "<p>Are you sure?</p>" +
+ "<input type='reset' class='cancel button' value='Cancel'>" +
+ "<input type='submit' class='save button' value='Purge'>" +
+ "</form>");
+ box.find('.cancel').click(box.remove.bind(box));
+ box.find('.save').click(function(ev) {
+ ev.preventDefault();
+
+ var path = download.find('.torrent a').attr('href');
+ $.ajax({ type: 'DELETE',
+ url: path,
+ success: function(response) {
+ download.remove();
+ box.remove();
+ },
+ error: function() {
+ box.content("<p>Request failed</p>" +
+ "<p class='button'>Close</p>");
+ box.find('.button').click(box.remove.bind(box));
+ }
+ });
+ });
+ });
+});
152 static/edit-user.js
@@ -0,0 +1,152 @@
+function LightBox() {
+ this.el_background = $("<div class='lightboxbackground'></div>");
+ this.el = $("<div class='lightbox'></div>");
+ this.el_background.append(this.el);
+ $('body').append(this.el_background);
+}
+LightBox.prototype = {
+ remove: function() {
+ this.el.remove();
+ this.el_background.remove();
+ },
+ find: function(key) {
+ return this.el.find(key);
+ },
+ content: function(contents) {
+ this.el.empty();
+ this.el.append(contents);
+ }
+};
+
+/**
+ * Edit user details
+ */
+
+var editButton = $("<p class='edit button'>Edit</p>");
+$('.meta').after(editButton);
+var detailsPath = document.location.pathname + "/details.json";
+editButton.bind('click', function() {
+ var box = new LightBox();
+ box.content('<p>Retrieving...</p>');
+
+ $.ajax({ url: detailsPath,
+ success: function(response) {
+ box.content("<form class='useredit'>" +
+ "<h2>Edit user information</h2>" +
+ "<p><label for='title'>Title: <input id='title'></p>" +
+ "<p><label for='image'>Image link: <input id='image'></p>" +
+ "<p><label for='homepage'>Homepage: <input id='homepage'></p>" +
+ "<input type='reset' class='cancel button' value='Cancel'>" +
+ "<input type='submit' class='save button' value='Save'>" +
+ "</form>");
+ box.find('#title').val(response.title);
+ box.find('#image').val(response.image);
+ box.find('#homepage').val(response.homepage);
+ box.find('.cancel').click(box.remove.bind(box));
+ box.find('.save').click(function(ev) {
+ ev.preventDefault();
+
+ var data = {
+ title: box.find('#title').val(),
+ image: box.find('#image').val(),
+ homepage: box.find('#homepage').val()
+ };
+ box.content("<p>Submitting...</p>");
+ $.ajax({ type: 'POST',
+ url: detailsPath,
+ data: data,
+ success: function() {
+ /* Force refresh: */
+ document.location.search = "?" + Math.ceil(Math.random() * 999);
+ },
+ error: function() {
+ box.content("<p>Cannot submit</p>" +
+ "<p class='button'>Close</p>");
+ box.find('.button').click(box.remove.bind(box));
+ }
+ });
+ });
+ },
+ error: function() {
+ box.content("<p>An error occured</p>" +
+ "<p class='button'>Close</p>");
+ box.find('.button').click(box.remove.bind(box));
+ }
+ });
+});
+
+/**
+ * Add feed button
+ */
+var addButton = $("<p class='add button'>Add</a>");
+$('.col1').append(addButton);
+addButton.bind('click', function() {
+ var box = new LightBox();
+ box.content("<form class='addfeed'>" +
+ "<h2>Add a new podcast feed</h2>" +
+ "<p><label for='slug'>Slug: <input id='slug'></p>" +
+ "<p id='slughint' class='hint'></p>" +
+ "<p><label for='url'>URL: <input id='url'></p>" +
+ "<p class='hint'>All feeds are subject to manual confirmation.</p>" +
+ "<input type='reset' class='cancel button' value='Cancel'>" +
+ "<input type='submit' class='save button' value='Add'>" +
+ "</form>");
+
+ var slugEl = box.find('#slug');
+ function fixSlug() {
+ var s = slugEl.val().
+ toLowerCase().
+ replace(/[^0-9a-z\-_]/g, "");
+
+ if (s !== slugEl.val())
+ slugEl.val(s);
+
+ box.find('#slughint').
+ text((s.length > 0) ?
+ "http://bitlove.org" + document.location.pathname + "/" + s:
+ "");
+ }
+ slugEl.bind('change', fixSlug);
+ slugEl.bind('input', fixSlug);
+ slugEl.bind('keyup', fixSlug);
+
+ box.find('.cancel').click(box.remove.bind(box));
+ box.find('.save').click(function(ev) {
+ ev.preventDefault();
+
+ var slug = slugEl.val();
+ var path = document.location.pathname + "/" + slug;
+ var url = box.find('#url').val();
+ box.content("<p>Adding your feed...</p>");
+
+ $.ajax({ type: 'PUT',
+ url: path,
+ data: {
+ url: url
+ },
+ success: function(response) {
+ if (response && response.link) {
+ box.content("<p>Your feed has been created: <a class='link'></a></p>" +
+ "<p class='hint'>Your feeds are private by default. Don't forget to edit them if you are satisfied with what you're seeing. Contact us otherwise: <a href='mailto:mail@bitlove.org'>mail@bitlove.org</a>.</p>" +
+ "<p class='button'>Close</p>");
+ box.find('.link').attr('href', response.link);
+ box.find('.link').text(response.link);
+ box.find('.button').click(function() {
+ /* Force refresh: */
+ document.location.search = "?" + Math.ceil(Math.random() * 999);
+ });
+ } else {
+ box.content("<p class='message'></p>" +
+ "<p class='button'>Close</p>");
+ box.find('.message').text((response && response.error) || "An error occured");
+ box.find('.button').click(box.remove.bind(box));
+ }
+ },
+ error: function() {
+ box.content("<p>Cannot communicate</p>" +
+ "<p class='button'>Close</p>");
+ box.find('.button').click(box.remove.bind(box));
+ }
+ });
+ });
+});
BIN  static/edit.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
315 static/error.svg
@@ -0,0 +1,315 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="46"
+ height="41"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="logo.svg"
+ inkscape:export-filename="/home/stephan/programming/erlang/prittorrent/apps/ui/priv/static/logo.png"
+ inkscape:export-xdpi="90"
+ inkscape:export-ydpi="90">
+ <defs
+ id="defs4" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="8"
+ inkscape:cx="11.20834"
+ inkscape:cy="21.122768"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1916"
+ inkscape:window-height="1057"
+ inkscape:window-x="1440"
+ inkscape:window-y="19"
+ inkscape:window-maximized="0">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2985"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ originx="-0.30302787px"
+ originy="4.754524px" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-0.30302787,-451.1167)">
+ <rect
+ style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987"
+ width="5.0000029"
+ height="5.0000029"
+ x="-82.757278"
+ y="449.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7"
+ width="5.0000029"
+ height="5.0000029"
+ x="-76.757278"
+ y="449.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-78"
+ width="5.0000029"
+ height="5.0000029"
+ x="-88.757278"
+ y="455.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-8"
+ width="5.0000029"
+ height="5.0000029"
+ x="-82.757278"
+ y="455.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-1"
+ width="5.0000029"
+ height="5.0000029"
+ x="-76.757278"
+ y="455.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-0"
+ width="5.0000029"
+ height="5.0000029"
+ x="-70.757271"
+ y="455.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-6"
+ width="5.0000029"
+ height="5.0000029"
+ x="-64.757271"
+ y="455.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-4"
+ width="5.0000029"
+ height="5.0000029"
+ x="90.96698"
+ y="456.53479"
+ inkscape:transform-center-x="-22.098751"
+ inkscape:transform-center-y="-11.922506"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-5"
+ width="5.0000029"
+ height="5.0000029"
+ x="-64.757271"
+ y="449.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-9"
+ width="5.0000029"
+ height="5.0000029"
+ x="-58.757259"
+ y="449.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-54"
+ width="5.0000029"
+ height="5.0000029"
+ x="90.96698"
+ y="462.53479"
+ inkscape:transform-center-x="-21.375321"
+ inkscape:transform-center-y="-5.9662786"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#550000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-6"
+ width="5.0000029"
+ height="5.0000029"
+ x="96.96698"
+ y="462.53479"
+ inkscape:transform-center-x="-27.331548"
+ inkscape:transform-center-y="-5.2428481"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-6-5"
+ width="5.0000029"
+ height="5.0000029"
+ x="96.96698"
+ y="456.53479"
+ inkscape:transform-center-x="-28.054979"
+ inkscape:transform-center-y="-11.199076"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-78-4"
+ width="5.0000029"
+ height="5.0000029"
+ x="-88.757278"
+ y="461.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-8-1"
+ width="5.0000029"
+ height="5.0000029"
+ x="-82.757278"
+ y="461.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-1-0"
+ width="5.0000029"
+ height="5.0000029"
+ x="-76.757278"
+ y="461.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-0-1"
+ width="5.0000029"
+ height="5.0000029"
+ x="-70.757271"
+ y="461.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-6-5"
+ width="5.0000029"
+ height="5.0000029"
+ x="84.96698"
+ y="462.53479"
+ inkscape:transform-center-x="-15.419093"
+ inkscape:transform-center-y="-6.689709"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#550000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-54-0"
+ width="5.0000029"
+ height="5.0000029"
+ x="90.96698"
+ y="468.53479"
+ inkscape:transform-center-x="-20.65189"
+ inkscape:transform-center-y="-0.010050872"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-8-1-5"
+ width="5.0000029"
+ height="5.0000029"
+ x="-82.757278"
+ y="467.93518"
+ transform="matrix(0.98144886,-0.19172409,0.19172409,0.98144886,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-1-0-9"
+ width="5.0000029"
+ height="5.0000029"
+ x="72.966965"
+ y="468.53479"
+ inkscape:transform-center-x="-2.7831916"
+ inkscape:transform-center-y="-2.180344"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-0-1-4"
+ width="5.0000029"
+ height="5.0000029"
+ x="78.966965"
+ y="468.53479"
+ inkscape:transform-center-x="-8.7394194"
+ inkscape:transform-center-y="-1.4569135"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-6-5-8"
+ width="5.0000029"
+ height="5.0000029"
+ x="84.96698"
+ y="468.53479"
+ inkscape:transform-center-x="-14.695663"
+ inkscape:transform-center-y="-0.73348134"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-1-0-9-3"
+ width="5.0000029"
+ height="5.0000029"
+ x="72.966965"
+ y="474.53479"
+ inkscape:transform-center-x="-2.0597612"
+ inkscape:transform-center-y="3.7758837"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#800000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-7-0-1-4-7"
+ width="5.0000029"
+ height="5.0000029"
+ x="78.966965"
+ y="474.53479"
+ inkscape:transform-center-x="-8.015989"
+ inkscape:transform-center-y="4.4993141"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#550000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-6-5-8-8"
+ width="5.0000029"
+ height="5.0000029"
+ x="84.96698"
+ y="474.53479"
+ inkscape:transform-center-x="-13.972231"
+ inkscape:transform-center-y="5.2227465"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ <rect
+ style="fill:#550000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+ id="rect2987-54-0-8"
+ width="5.0000029"
+ height="5.0000029"
+ x="78.966965"
+ y="480.53482"
+ inkscape:transform-center-x="-7.2925549"
+ inkscape:transform-center-y="10.455573"
+ transform="matrix(0.99270462,0.12057174,-0.12057174,0.99270462,0,0)" />
+ </g>
+</svg>
BIN  static/favicon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
205 static/filter.js
@@ -0,0 +1,205 @@
+(function() {
+
+function FilterableItem(el) {
+ this.el = el;
+ this.lang = el.attr('xml:lang') || '';
+ var types = [];
+ el.find('.torrent a').each(function() {
+ var type = $(this).data('type');
+ /* Split MIME type */
+ types.push(type ? type.split('/')[0] : '');
+ });
+ this.types = types;
+}
+FilterableItem.prototype = {
+ applyMask: function(mask) {
+ var langMatch = !mask.lang[this.lang];
+ var typeMatch = this.types.some(function(type) {
+ return !mask.type[type];
+ });
+
+ if (typeMatch && langMatch)
+ this.show();
+ else
+ this.hide();
+ },
+ show: function() {
+ this.el.removeClass('filteredout');
+ },
+ hide: function() {
+ this.el.addClass('filteredout');
+ }
+};
+
+var filterItems = [];
+$('.item').each(function() {
+ var el = $(this);
+ filterItems.push(new FilterableItem($(this)));
+});
+var Filter = {
+ items: $('.item').map(function() {
+ return new FilterableItem($(this));
+ }),
+
+ getAllTypes: function() {
+ var types = {}, i, j;
+ for(i = 0; i < Filter.items.length; i++)
+ for(j = 0; j < Filter.items[i].types.length; j++) {
+ var type = Filter.items[i].types[j];
+ if (!types.hasOwnProperty(type))
+ types[type] = 0;
+ types[type]++;
+ }
+ return types;
+ },
+ getAllLangs: function() {
+ var langs = {}, i;
+ for(i = 0; i < Filter.items.length; i++) {
+ var lang = Filter.items[i].lang;
+ if (!langs.hasOwnProperty(lang))
+ langs[lang] = 0;
+ langs[lang]++;
+ }
+ return langs;
+ },
+
+ /* Inverted: */
+ mask: {
+ type: {},
+ lang: {}
+ },
+
+ applyMask: function() {
+ for(var i = 0; i < Filter.items.length; i++)
+ Filter.items[i].applyMask(Filter.mask);
+
+ if (window.localStorage && window.localStorage.setItem)
+ window.localStorage.setItem('Prittorrent.UI.Filter.Mask', JSON.stringify(Filter.mask));
+ }
+};
+
+try {
+ if (window.localStorage && window.localStorage.getItem) {
+ Filter.mask = JSON.parse(window.localStorage.getItem('Prittorrent.UI.Filter.Mask'));
+ /* Repair: */
+ if (!Filter.mask)
+ Filter.mask = {};
+ if (!Filter.mask.hasOwnProperty('type'))
+ Filter.mask.type = {};
+ if (!Filter.mask.hasOwnProperty('lang'))
+ Filter.mask.lang = {};
+
+ Filter.applyMask();
+ }
+
+} catch (x) {
+ if (window.console && window.console.error)
+ window.console.error(x.stack || x);
+}
+
+function sortKeysNullLast(o) {
+ return Object.keys(o).sort(function(a, b) {
+ if (!a && b)
+ return 1;
+ else if (a && !b)
+ return -1;
+ else if (a < b)
+ return -1;
+ else if (a > b)
+ return 1;
+ else
+ return 0;
+ });
+}
+
+function FilterDialog() {
+ this.el = $('<form class="filterdialog"><div class="type"><h2>By type</h2><ul></ul><p><a class="all">Select all</a></p></div><div class="lang"><h2>By language</h2><ul></ul><p><a class="all">Select all</a></p></div></form>');
+
+ /* Add checkboxes */
+ var allTypes = Filter.getAllTypes();
+ sortKeysNullLast(allTypes).forEach(function(type) {
+ var text = type ?
+ (type.substr(0, 1).toLocaleUpperCase() + type.substr(1)) :
+ "Other";
+ this.addOption('type', type, text, allTypes[type]);
+ }.bind(this));
+ var allLangs = Filter.getAllLangs();
+ sortKeysNullLast(allLangs).forEach(function(lang) {
+ var text = lang ?
+ (lang.substr(0, 1).toLocaleUpperCase() + lang.substr(1)) :
+ "Other";
+ this.addOption('lang', lang, text, allLangs[lang]);
+ }.bind(this));
+
+ /* Select all */
+ var update = this.update.bind(this);