A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add GroupedList scene

+572 -13
+29 -10
src/Applications/UI/Tracks.elm
··· 37 37 import UI.Queue.Page 38 38 import UI.Reply exposing (Reply(..)) 39 39 import UI.Tracks.Core exposing (..) 40 + import UI.Tracks.Scene.GroupedList 40 41 import UI.Tracks.Scene.List 41 42 42 43 ··· 96 97 model.collection.harvested 97 98 ) 98 99 in 99 - case it of 100 - Just identifiedTrack -> 101 - case model.scene of 102 - List -> 103 - identifiedTrack 104 - |> UI.Tracks.Scene.List.scrollToNowPlaying 105 - |> Return.commandWithModel model 106 - |> Return.addReply (GoToPage UI.Page.Index) 100 + model.nowPlaying 101 + |> Maybe.map (Tuple.second >> .id) 102 + |> Maybe.andThen 103 + (\id -> 104 + List.find 105 + (Tuple.second >> .id >> (==) id) 106 + model.collection.harvested 107 + ) 108 + |> Maybe.map 109 + (case model.scene of 110 + GroupedList -> 111 + UI.Tracks.Scene.GroupedList.scrollToNowPlaying 107 112 108 - Nothing -> 109 - return model 113 + List -> 114 + UI.Tracks.Scene.List.scrollToNowPlaying 115 + ) 116 + |> Maybe.map 117 + (\cmd -> 118 + cmd 119 + |> Return.commandWithModel model 120 + |> Return.addReply (GoToPage UI.Page.Index) 121 + ) 122 + |> Maybe.withDefault (return model) 110 123 111 124 SetEnabledSourceIds sourceIds -> 112 125 reviseCollection identify ··· 288 301 ---------- 289 302 , if oldHarvest /= newHarvest then 290 303 case model.scene of 304 + GroupedList -> 305 + UI.Tracks.Scene.GroupedList.scrollToTop 306 + 291 307 List -> 292 308 UI.Tracks.Scene.List.scrollToTop 293 309 ··· 371 387 372 388 else 373 389 case core.tracks.scene of 390 + GroupedList -> 391 + UI.Tracks.Scene.GroupedList.view { height = core.viewport.height } core.tracks 392 + 374 393 List -> 375 394 UI.Tracks.Scene.List.view { height = core.viewport.height } core.tracks 376 395 ]
+2 -1
src/Applications/UI/Tracks/Core.elm
··· 66 66 67 67 68 68 type Scene 69 - = List 69 + = GroupedList 70 + | List
+459
src/Applications/UI/Tracks/Scene/GroupedList.elm
··· 1 + module UI.Tracks.Scene.GroupedList exposing (scrollToNowPlaying, scrollToTop, view) 2 + 3 + import Browser.Dom as Dom 4 + import Chunky exposing (..) 5 + import Classes as C 6 + import Color 7 + import Color.Ext as Color 8 + import Conditional exposing (ifThenElse) 9 + import Css 10 + import Dict 11 + import Html as UnstyledHtml 12 + import Html.Attributes as UnstyledHtmlAttributes 13 + import Html.Events.Extra.Mouse exposing (onWithOptions) 14 + import Html.Styled as Html exposing (Html, text) 15 + import Html.Styled.Attributes exposing (css, fromUnstyled, id) 16 + import Html.Styled.Events exposing (onClick, onDoubleClick) 17 + import Html.Styled.Lazy 18 + import InfiniteList 19 + import List.Extra as List 20 + import Material.Icons exposing (Coloring(..)) 21 + import Material.Icons.Maps as Icons 22 + import Material.Icons.Navigation as Icons 23 + import Tachyons.Classes as T 24 + import Task 25 + import Time 26 + import Time.Ext as Time 27 + import Tracks exposing (..) 28 + import UI.Kit 29 + import UI.Reply 30 + import UI.Tracks.Core exposing (..) 31 + 32 + 33 + 34 + -- 🗺 35 + 36 + 37 + type alias Necessities = 38 + { height : Float 39 + } 40 + 41 + 42 + type alias GroupedIdentifiedTrack = 43 + { date : ( Int, Time.Month ) 44 + , identifiedTrack : IdentifiedTrack 45 + , isFirst : Bool 46 + } 47 + 48 + 49 + view : Necessities -> Model -> Html Msg 50 + view necessities model = 51 + let 52 + { infiniteList } = 53 + model 54 + 55 + groupedTracksDictionary = 56 + List.foldl 57 + (\( i, t ) -> 58 + let 59 + ( year, month ) = 60 + ( Time.toYear Time.utc t.insertedAt 61 + , Time.toMonth Time.utc t.insertedAt 62 + ) 63 + 64 + item = 65 + { date = ( year, month ) 66 + , identifiedTrack = ( i, t ) 67 + , isFirst = False 68 + } 69 + in 70 + Dict.update 71 + (year * 1000 + Time.monthNumber month) 72 + (\maybeList -> 73 + case maybeList of 74 + Just list -> 75 + Just (item :: list) 76 + 77 + Nothing -> 78 + Just [ { item | isFirst = True } ] 79 + ) 80 + ) 81 + Dict.empty 82 + model.collection.harvested 83 + 84 + groupedTracks = 85 + groupedTracksDictionary 86 + |> Dict.values 87 + |> List.reverse 88 + |> List.map List.reverse 89 + |> List.concat 90 + in 91 + brick 92 + [ fromUnstyled (InfiniteList.onScroll InfiniteListMsg) 93 + , id containerId 94 + ] 95 + [ T.flex_grow_1 96 + , T.vh_25 97 + , T.overflow_x_hidden 98 + , T.overflow_y_scroll 99 + ] 100 + [ Html.Styled.Lazy.lazy2 header model.sortBy model.sortDirection 101 + , Html.fromUnstyled 102 + (InfiniteList.view 103 + (infiniteListConfig necessities model) 104 + infiniteList 105 + groupedTracks 106 + ) 107 + ] 108 + 109 + 110 + containerId : String 111 + containerId = 112 + "diffuse__track-list" 113 + 114 + 115 + scrollToNowPlaying : IdentifiedTrack -> Cmd Msg 116 + scrollToNowPlaying ( identifiers, track ) = 117 + (22 - rowHeight / 2 + 5 + toFloat identifiers.indexInList * rowHeight) 118 + |> Dom.setViewportOf containerId 0 119 + |> Task.attempt (always Bypass) 120 + 121 + 122 + scrollToTop : Cmd Msg 123 + scrollToTop = 124 + Task.attempt (always Bypass) (Dom.setViewportOf containerId 0 0) 125 + 126 + 127 + 128 + -- HEADER 129 + 130 + 131 + header : SortBy -> SortDirection -> Html Msg 132 + header sortBy sortDirection = 133 + let 134 + sortIcon = 135 + (if sortDirection == Desc then 136 + Icons.expand_less 137 + 138 + else 139 + Icons.expand_more 140 + ) 141 + 15 142 + Inherit 143 + 144 + sortIconHtml = 145 + Html.fromUnstyled sortIcon 146 + 147 + maybeSortIcon s = 148 + ifThenElse (sortBy == s) (Just sortIconHtml) Nothing 149 + in 150 + brick 151 + [ css headerStyles ] 152 + [ T.bg_white, T.flex, T.fw6, T.relative, T.z_5 ] 153 + [ headerColumn "" 4.5 First Nothing Bypass 154 + , headerColumn "Title" 37.5 Between (maybeSortIcon Title) (SortBy Title) 155 + , headerColumn "Artist" 29.0 Between (maybeSortIcon Artist) (SortBy Artist) 156 + , headerColumn "Album" 29.0 Last (maybeSortIcon Album) (SortBy Album) 157 + ] 158 + 159 + 160 + headerStyles : List Css.Style 161 + headerStyles = 162 + [ Css.borderBottom3 (Css.px 1) Css.solid (Color.toElmCssColor UI.Kit.colors.subtleBorder) 163 + , Css.color (Color.toElmCssColor headerTextColor) 164 + , Css.fontSize (Css.px 11) 165 + ] 166 + 167 + 168 + headerTextColor : Color.Color 169 + headerTextColor = 170 + Color.rgb255 207 207 207 171 + 172 + 173 + 174 + -- HEADER COLUMN 175 + 176 + 177 + type Pos 178 + = First 179 + | Between 180 + | Last 181 + 182 + 183 + headerColumn : 184 + String 185 + -> Float 186 + -> Pos 187 + -> Maybe (Html msg) 188 + -> msg 189 + -> Html msg 190 + headerColumn text_ width pos maybeSortIcon msg = 191 + brick 192 + [ onClick msg 193 + , css 194 + [ Css.borderLeft3 195 + (Css.px <| ifThenElse (pos /= First) 1 0) 196 + Css.solid 197 + (Color.toElmCssColor UI.Kit.colors.subtleBorder) 198 + , Css.width (Css.pct width) 199 + ] 200 + ] 201 + [ T.lh_title 202 + , T.pv1 203 + , T.relative 204 + 205 + -- 206 + , ifThenElse (pos == First) T.pl3 T.pl2 207 + , ifThenElse (pos == Last) T.pr3 T.pr2 208 + , ifThenElse (pos == First) "" T.pointer 209 + ] 210 + [ brick 211 + [ css [ Css.top (Css.px 1) ] ] 212 + [ T.relative ] 213 + [ text text_ ] 214 + , case maybeSortIcon of 215 + Just sortIcon -> 216 + brick 217 + [ css sortIconStyles ] 218 + [ T.absolute, T.mr1, T.right_0 ] 219 + [ sortIcon ] 220 + 221 + Nothing -> 222 + nothing 223 + ] 224 + 225 + 226 + sortIconStyles : List Css.Style 227 + sortIconStyles = 228 + [ Css.fontSize (Css.px 0) 229 + , Css.lineHeight (Css.px 0) 230 + , Css.top (Css.pct 50) 231 + , Css.transform (Css.translateY <| Css.pct -50) 232 + ] 233 + 234 + 235 + 236 + -- INFINITE LIST 237 + 238 + 239 + infiniteListConfig : Necessities -> Model -> InfiniteList.Config GroupedIdentifiedTrack Msg 240 + infiniteListConfig necessities model = 241 + InfiniteList.withCustomContainer 242 + infiniteListContainer 243 + (InfiniteList.config 244 + { itemView = itemView model 245 + , itemHeight = InfiniteList.withConstantHeight (round rowHeight) 246 + , containerHeight = round necessities.height 247 + } 248 + ) 249 + 250 + 251 + infiniteListContainer : 252 + List ( String, String ) 253 + -> List (UnstyledHtml.Html msg) 254 + -> UnstyledHtml.Html msg 255 + infiniteListContainer styles children = 256 + UnstyledHtml.div 257 + (List.map (\( k, v ) -> UnstyledHtmlAttributes.style k v) styles) 258 + [ (Html.toUnstyled << rawy) <| 259 + brick 260 + [ css listStyles ] 261 + [ T.f6 262 + , T.list 263 + , T.ma0 264 + , T.ph0 265 + , T.pv1 266 + ] 267 + (List.map Html.fromUnstyled children) 268 + ] 269 + 270 + 271 + listStyles : List Css.Style 272 + listStyles = 273 + [ Css.fontSize (Css.px 12.5) ] 274 + 275 + 276 + itemView : Model -> Int -> Int -> GroupedIdentifiedTrack -> UnstyledHtml.Html Msg 277 + itemView { favouritesOnly } _ idx { date, identifiedTrack, isFirst } = 278 + let 279 + ( year, month ) = 280 + date 281 + 282 + ( identifiers, track ) = 283 + identifiedTrack 284 + in 285 + Html.toUnstyled <| 286 + chunk 287 + [] 288 + [ if isFirst then 289 + brick 290 + [ css groupStyles ] 291 + [ T.f7 292 + , T.fw6 293 + , T.lh_copy 294 + , T.mb3 295 + , T.mh3 296 + , T.mt4 297 + , T.tracked 298 + ] 299 + [ inline 300 + [ T.dib, T.v_mid, C.lh_0 ] 301 + [ Html.fromUnstyled (Icons.terrain 16 Inherit) ] 302 + , inline 303 + [ T.dib, T.pl2, T.v_mid ] 304 + (if year == 1970 then 305 + [ text "AGES AGO" ] 306 + 307 + else 308 + [ text (Time.monthNumber month |> String.fromInt |> String.padLeft 2 '0') 309 + , text " / " 310 + , text (String.fromInt year) 311 + ] 312 + ) 313 + ] 314 + 315 + else 316 + nothing 317 + 318 + -- 319 + , brick 320 + [ css (rowStyles idx identifiers) 321 + 322 + -- Play 323 + ------- 324 + , [ UI.Reply.PlayTrack ( identifiers, track ) ] 325 + |> Reply 326 + |> onDoubleClick 327 + 328 + -- Context Menu 329 + --------------- 330 + , ( identifiers, track ) 331 + |> ShowContextMenu 332 + |> onWithOptions 333 + "contextmenu" 334 + { stopPropagation = True 335 + , preventDefault = True 336 + } 337 + |> Html.Styled.Attributes.fromUnstyled 338 + ] 339 + [ T.flex 340 + , T.items_center 341 + 342 + -- 343 + , ifThenElse identifiers.isMissing "" T.pointer 344 + , ifThenElse identifiers.isSelected T.fw6 "" 345 + ] 346 + [ favouriteColumn favouritesOnly identifiers 347 + , otherColumn 37.5 False track.tags.title 348 + , otherColumn 29.0 False track.tags.artist 349 + , otherColumn 29.0 True track.tags.album 350 + ] 351 + ] 352 + 353 + 354 + groupStyles : List Css.Style 355 + groupStyles = 356 + [ Css.color (Color.toElmCssColor UI.Kit.colorKit.base04) 357 + , Css.fontFamilies UI.Kit.headerFontFamilies 358 + , Css.fontSize (Css.px 10.5) 359 + ] 360 + 361 + 362 + 363 + -- ROWS 364 + 365 + 366 + rowHeight : Float 367 + rowHeight = 368 + 35 369 + 370 + 371 + rowStyles : Int -> Identifiers -> List Css.Style 372 + rowStyles idx { isMissing, isNowPlaying } = 373 + let 374 + bgColor = 375 + if isNowPlaying then 376 + Color.toElmCssColor UI.Kit.colorKit.base0D 377 + 378 + else if modBy 2 idx == 1 then 379 + Css.rgb 252 252 252 380 + 381 + else 382 + Css.rgb 255 255 255 383 + 384 + color = 385 + if isNowPlaying then 386 + Css.rgb 255 255 255 387 + 388 + else if isMissing then 389 + Color.toElmCssColor UI.Kit.colorKit.base04 390 + 391 + else 392 + Color.toElmCssColor UI.Kit.colors.text 393 + in 394 + [ Css.backgroundColor bgColor 395 + , Css.color color 396 + , Css.height (Css.px rowHeight) 397 + ] 398 + 399 + 400 + 401 + -- COLUMNS 402 + 403 + 404 + favouriteColumn : Bool -> Identifiers -> Html Msg 405 + favouriteColumn favouritesOnly identifiers = 406 + brick 407 + [ css (favouriteColumnStyles favouritesOnly identifiers) 408 + , onClick (ToggleFavourite identifiers.indexInList) 409 + ] 410 + [ T.pl3 ] 411 + [ if identifiers.isFavourite then 412 + text "t" 413 + 414 + else 415 + text "f" 416 + ] 417 + 418 + 419 + favouriteColumnStyles : Bool -> Identifiers -> List Css.Style 420 + favouriteColumnStyles favouritesOnly { isFavourite, isNowPlaying, isSelected } = 421 + let 422 + color = 423 + if isSelected then 424 + Color.toElmCssColor UI.Kit.colors.selection 425 + 426 + else if isNowPlaying && isFavourite then 427 + Css.rgb 255 255 255 428 + 429 + else if isNowPlaying then 430 + Css.rgba 255 255 255 0.4 431 + 432 + else if favouritesOnly || not isFavourite then 433 + Css.rgb 222 222 222 434 + 435 + else 436 + Color.toElmCssColor UI.Kit.colorKit.base08 437 + in 438 + [ Css.color color 439 + , Css.fontFamilies [ "or-favourites" ] 440 + , Css.width (Css.pct 4.5) 441 + ] 442 + 443 + 444 + otherColumn : Float -> Bool -> String -> Html msg 445 + otherColumn width isLast text_ = 446 + brick 447 + [ css (otherColumnStyles width) ] 448 + [ T.pl2 449 + , T.truncate 450 + 451 + -- 452 + , ifThenElse isLast T.pr3 T.pr2 453 + ] 454 + [ text text_ ] 455 + 456 + 457 + otherColumnStyles : Float -> List Css.Style 458 + otherColumnStyles columnWidth = 459 + [ Css.width (Css.pct columnWidth) ]
+82 -2
src/Library/Time/Ext.elm
··· 1 - module Time.Ext exposing (decoder, default, encode) 1 + module Time.Ext exposing (decoder, default, encode, monthName, monthNumber) 2 2 3 3 import Json.Decode as Decode exposing (Decoder) 4 4 import Json.Encode as Json 5 - import Time 5 + import Time exposing (Month(..)) 6 6 7 7 8 8 ··· 22 22 encode : Time.Posix -> Json.Value 23 23 encode time = 24 24 Json.int (Time.posixToMillis time) 25 + 26 + 27 + monthName : Time.Month -> String 28 + monthName month = 29 + case month of 30 + Jan -> 31 + "January" 32 + 33 + Feb -> 34 + "February" 35 + 36 + Mar -> 37 + "March" 38 + 39 + Apr -> 40 + "April" 41 + 42 + May -> 43 + "May" 44 + 45 + Jun -> 46 + "June" 47 + 48 + Jul -> 49 + "July" 50 + 51 + Aug -> 52 + "August" 53 + 54 + Sep -> 55 + "September" 56 + 57 + Oct -> 58 + "October" 59 + 60 + Nov -> 61 + "November" 62 + 63 + Dec -> 64 + "December" 65 + 66 + 67 + monthNumber : Time.Month -> Int 68 + monthNumber month = 69 + case month of 70 + Jan -> 71 + 1 72 + 73 + Feb -> 74 + 2 75 + 76 + Mar -> 77 + 3 78 + 79 + Apr -> 80 + 4 81 + 82 + May -> 83 + 5 84 + 85 + Jun -> 86 + 6 87 + 88 + Jul -> 89 + 7 90 + 91 + Aug -> 92 + 8 93 + 94 + Sep -> 95 + 9 96 + 97 + Oct -> 98 + 10 99 + 100 + Nov -> 101 + 11 102 + 103 + Dec -> 104 + 12