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.

Fix rebase issues

-843
-47
elm-package.json
··· 1 - { 2 - "version": "1.0.2", 3 - "summary": "Diffuse", 4 - "repository": "https://github.com/icidasset/diffuse.git", 5 - "license": "MIT", 6 - "source-directories": [ 7 - "src/App", 8 - "src/Lib", 9 - "src/Slave", 10 - "src/Styles" 11 - ], 12 - "exposed-modules": [], 13 - "dependencies": { 14 - "FabienHenon/elm-infinite-list-view": "2.0.0 <= v < 3.0.0", 15 - "eeue56/elm-xml": "2.2.0 <= v < 3.0.0", 16 - "elm-community/html-extra": "2.2.0 <= v < 3.0.0", 17 - "elm-community/list-extra": "6.1.0 <= v < 7.0.0", 18 - "elm-community/material-icons": "1.1.0 <= v < 2.0.0", 19 - "elm-community/maybe-extra": "4.0.0 <= v < 5.0.0", 20 - "elm-community/string-extra": "1.4.0 <= v < 2.0.0", 21 - "elm-lang/core": "5.1.1 <= v < 6.0.0", 22 - "elm-lang/dom": "1.1.1 <= v < 2.0.0", 23 - "elm-lang/html": "2.0.0 <= v < 3.0.0", 24 - "elm-lang/http": "1.0.0 <= v < 2.0.0", 25 - "elm-lang/lazy": "2.0.0 <= v < 3.0.0", 26 - "elm-lang/mouse": "1.0.1 <= v < 2.0.0", 27 - "elm-lang/navigation": "2.1.0 <= v < 3.0.0", 28 - "elm-lang/svg": "2.0.0 <= v < 3.0.0", 29 - "elm-lang/window": "1.0.1 <= v < 2.0.0", 30 - "eskimoblood/elm-color-extra": "5.0.0 <= v < 6.0.0", 31 - "etaque/elm-response": "3.0.0 <= v < 4.0.0", 32 - "evancz/url-parser": "2.0.1 <= v < 3.0.0", 33 - "jinjor/elm-debounce": "2.1.0 <= v < 2.2.0", 34 - "justinmimbs/elm-date-extra": "2.0.3 <= v < 3.0.0", 35 - "ktonon/elm-crypto": "1.1.1 <= v < 2.0.0", 36 - "lukewestby/elm-string-interpolate": "1.0.1 <= v < 2.0.0", 37 - "mdgriffith/style-elements": "4.3.0 <= v < 5.0.0", 38 - "newlandsvalley/elm-binary-base64": "1.0.1 <= v < 2.0.0", 39 - "ohanhi/keyboard-extra": "3.0.4 <= v < 4.0.0", 40 - "pablen/toasty": "1.0.4 <= v < 2.0.0", 41 - "pablohirafuji/elm-markdown": "2.0.4 <= v < 3.0.0", 42 - "rtfeldman/hex": "1.0.0 <= v < 2.0.0", 43 - "sporto/erl": "13.0.2 <= v < 14.0.0", 44 - "truqu/elm-base64": "2.0.1 <= v < 3.0.0" 45 - }, 46 - "elm-version": "0.18.0 <= v < 0.19.0" 47 - }
-719
src/App/Sources/View.elm
··· 1 - module Sources.View exposing (entry, pageEdit, pageEditForm, pageIndex, pageNew, pageNewForm, pageNewStep1, pageNewStep2, pageNewStep3, propertyRenderer, renderSource, renderSourceProperties, sourcePropertiesFilterer, sourcePropertiesNotToValidate, sourcePropertiesToIgnore, validateProperties) 2 - 3 - -- Elements 4 - -- Styles 5 - 6 - import Color 7 - import Color.Convert 8 - import ContextMenu.Types exposing (Msg(ShowSourceMenu)) 9 - import Dict 10 - import Dict.Ext as Dict 11 - import Element exposing (..) 12 - import Element.Attributes exposing (..) 13 - import Element.Events exposing (onClick, onInput, onWithOptions) 14 - import Element.Ext exposing (..) 15 - import Element.Input as Input 16 - import Element.Keyed 17 - import Element.Types exposing (Node) 18 - import Form.Styles exposing (Styles(Input)) 19 - import Html 20 - import Html.Attributes 21 - import Json.Decode as Decode 22 - import Layouts exposing (..) 23 - import List.Extra as List 24 - import List.Styles exposing (Styles(..)) 25 - import Material.Icons.Action as Icons 26 - import Material.Icons.Alert as Icons 27 - import Material.Icons.Av as Icons 28 - import Material.Icons.Content as Icons 29 - import Material.Icons.File as Icons 30 - import Material.Icons.Navigation as Icons 31 - import Material.Icons.Notification as Icons 32 - import Maybe.Extra 33 - import Mouse 34 - import Navigation.Types exposing (..) 35 - import Navigation.View as Navigation 36 - import Notifications.Types 37 - import Routing.Types exposing (Msg(RedirectTo)) 38 - import Sources.Services as Services 39 - import Sources.Services.Dropbox as Dropbox 40 - import Sources.Services.Google as Google 41 - import Sources.Types as Sources exposing (..) 42 - import Sources.Utils exposing (isViable) 43 - import Styles exposing (Styles(..)) 44 - import Types as TopLevel exposing (Msg(..)) 45 - import Variables exposing (colorDerivatives, colors, scaled) 46 - import Variations exposing (Variations(..)) 47 - 48 - 49 - 50 - -- 🍯 51 - 52 - 53 - entry : Sources.Page -> TopLevel.Model -> Node 54 - entry page model = 55 - case page of 56 - Edit _ -> 57 - lazySpread 58 - pageEdit 59 - model.sources.form 60 - 61 - Index -> 62 - lazySpread3 63 - pageIndex 64 - model.sources.collection 65 - ( model.sources.isProcessing, model.sources.processingErrors ) 66 - { isElectron = model.isElectron, isOnline = model.isOnline } 67 - 68 - New -> 69 - lazySpread3 70 - pageNew 71 - model.sources.form 72 - model.origin 73 - model.isElectron 74 - 75 - NewThroughRedirect service hash -> 76 - lazySpread3 77 - pageNew 78 - model.sources.form 79 - model.origin 80 - model.isElectron 81 - 82 - 83 - 84 - -- {Page} Index 85 - 86 - 87 - pageIndex : 88 - List Source 89 - -> ( IsProcessing, List ( SourceId, String ) ) 90 - -> ViabilityDependencies 91 - -> Node 92 - pageIndex sources ( isProcessing, processingErrors ) viabilityDependencies = 93 - column 94 - Zed 95 - [ height fill ] 96 - [ ------------------------------------ 97 - -- Navigation 98 - ------------------------------------ 99 - Navigation.insideCustom <| 100 - (++) 101 - -- Add 102 - -- 103 - [ ( Icon Icons.add 104 - , Label (Shown "Add a new source") 105 - -- 106 - , Sources.New 107 - |> Routing.Types.Sources 108 - |> Routing.Types.GoToPage 109 - |> RoutingMsg 110 - ) 111 - ] 112 - -- Other 113 - -- 114 - (if List.isEmpty sources then 115 - [] 116 - 117 - else if Maybe.Extra.isJust isProcessing then 118 - [ ( Icon Icons.sync 119 - , Label (Shown "Processing sources ...") 120 - , TopLevel.NoOp 121 - ) 122 - ] 123 - 124 - else 125 - [ ( Icon Icons.sync 126 - , Label (Shown "Process sources") 127 - , TopLevel.ProcessSources 128 - ) 129 - ] 130 - ) 131 - 132 - ------------------------------------ 133 - -- List 134 - ------------------------------------ 135 - , column 136 - Zed 137 - [ height fill 138 - , paddingXY (scaled 4) 0 139 - ] 140 - [ Layouts.h1 "Sources" 141 - 142 - -- Lists 143 - -- 144 - , if List.isEmpty sources then 145 - empty 146 - 147 - else 148 - Layouts.intro 149 - [ text """ 150 - A source is a place where your music is stored. 151 - """ 152 - , html (Html.br [] []) 153 - , text """ 154 - By connecting a source, the application will scan it 155 - and keep a list of all the music in it. 156 - """ 157 - , html (Html.br [] []) 158 - , text """ 159 - It will not copy anything. 160 - """ 161 - ] 162 - 163 - -- Check if sources are processing 164 - -- and if they have processing errors 165 - , let 166 - sortedSources = 167 - List.sortBy 168 - (\s -> Dict.fetch "name" "" s.data) 169 - sources 170 - 171 - sourcesWithContext = 172 - List.map 173 - (\s -> 174 - ( s 175 - , s 176 - |> isViable viabilityDependencies 177 - , isProcessing 178 - |> Maybe.andThen (List.find (.id >> (==) s.id)) 179 - |> Maybe.map (always True) 180 - |> Maybe.withDefault False 181 - , processingErrors 182 - |> List.find (Tuple.first >> (==) s.id) 183 - |> Maybe.map Tuple.second 184 - ) 185 - ) 186 - sortedSources 187 - in 188 - if List.isEmpty sources then 189 - -- No sources atm 190 - Layouts.emptyState 191 - Icons.add 192 - [ text "No sources have been added yet," 193 - , text "add one to get started." 194 - ] 195 - 196 - else 197 - -- Render list 198 - Element.Keyed.column 199 - (List Container) 200 - [] 201 - (List.indexedMap renderSource sourcesWithContext) 202 - ] 203 - ] 204 - 205 - 206 - renderSource : Int -> ( Source, Bool, Bool, Maybe String ) -> ( String, Node ) 207 - renderSource index ( source, sourceIsViable, isProcessing, processingError ) = 208 - let 209 - key = 210 - toString index 211 - in 212 - ( key 213 - , listItem 214 - [ attribute "rel" key 215 - , inlineStyle 216 - (if sourceIsViable then 217 - [] 218 - 219 - else 220 - [ ( "color", Color.Convert.colorToCssRgb colors.base04 ) ] 221 - ) 222 - ] 223 - [ el 224 - Zed 225 - [ width fill ] 226 - (source.data 227 - |> Dict.get "name" 228 - |> Maybe.withDefault source.id 229 - |> text 230 - ) 231 - , listItemActions 232 - [ -- Processing error 233 - -- 234 - case processingError of 235 - Just err -> 236 - el 237 - WithoutLineHeight 238 - [ attribute "title" err ] 239 - (16 |> Icons.error_outline colorDerivatives.error |> html) 240 - 241 - Nothing -> 242 - empty 243 - 244 - -- Is processing 245 - -- 246 - , if isProcessing == True then 247 - el 248 - WithoutLineHeight 249 - [ attribute "title" "Processing …" ] 250 - (16 |> Icons.sync colorDerivatives.text |> html) 251 - 252 - else 253 - empty 254 - 255 - -- Enabled/Disabled 256 - -- 257 - , if not sourceIsViable then 258 - el 259 - WithoutLineHeight 260 - [ attribute "title" "Disabled (not available on this platform)" ] 261 - (16 262 - |> Icons.not_interested colors.base04 263 - |> html 264 - ) 265 - 266 - else 267 - el 268 - WithoutLineHeight 269 - [ source 270 - |> ToggleSource 271 - |> SourcesMsg 272 - |> onClick 273 - 274 - -- 275 - , if source.enabled then 276 - attribute "title" "Enabled (click to disable)" 277 - 278 - else 279 - attribute "title" "Disabled (click to enable)" 280 - ] 281 - (if source.enabled then 282 - html (Icons.check colorDerivatives.text 16) 283 - 284 - else 285 - html (Icons.not_interested colorDerivatives.text 16) 286 - ) 287 - 288 - -- Settings 289 - -- 290 - , el 291 - WithoutLineHeight 292 - [ Mouse.position 293 - |> Decode.map (ShowSourceMenu source.id) 294 - |> Decode.map TopLevel.ContextMenuMsg 295 - |> onWithOptions 296 - "click" 297 - { stopPropagation = True 298 - , preventDefault = True 299 - } 300 - ] 301 - (16 302 - |> Icons.settings colorDerivatives.text 303 - |> html 304 - ) 305 - ] 306 - ] 307 - ) 308 - 309 - 310 - 311 - -- {Page} New 312 - 313 - 314 - pageNew : Sources.Form -> String -> Bool -> Node 315 - pageNew sForm origin isElectron = 316 - column 317 - Zed 318 - [ height fill ] 319 - (case sForm of 320 - NewForm step source -> 321 - [ ------------------------------------ 322 - -- Navigation 323 - ------------------------------------ 324 - Navigation.insideCustom 325 - (case step of 326 - 1 -> 327 - [ ( Icon Icons.arrow_back 328 - , Label (Hidden "Go back") 329 - -- 330 - , Sources.Index 331 - |> Routing.Types.Sources 332 - |> Routing.Types.GoToPage 333 - |> RoutingMsg 334 - ) 335 - ] 336 - 337 - _ -> 338 - let 339 - newStep = 340 - case source.service of 341 - Local -> 342 - 1 343 - 344 - _ -> 345 - step - 1 346 - in 347 - [ ( Icon Icons.arrow_back 348 - , Label (Shown "Take a step back") 349 - -- 350 - , SourcesMsg (AssignFormStep newStep) 351 - ) 352 - ] 353 - ) 354 - 355 - ------------------------------------ 356 - -- Form 357 - ------------------------------------ 358 - , within 359 - [ logoBackdrop, takeOver (pageNewForm step source origin isElectron) ] 360 - (takeOver empty) 361 - ] 362 - 363 - _ -> 364 - [ text "Cannot use this model.form on this page" ] 365 - ) 366 - 367 - 368 - pageNewForm : Int -> Source -> String -> Bool -> Node 369 - pageNewForm step source origin isElectron = 370 - case step of 371 - 1 -> 372 - pageNewStep1 source isElectron 373 - 374 - 2 -> 375 - pageNewStep2 source origin 376 - 377 - 3 -> 378 - pageNewStep3 source 379 - 380 - _ -> 381 - empty 382 - 383 - 384 - pageNewStep1 : Source -> Bool -> Node 385 - pageNewStep1 source isElectron = 386 - let 387 - msg = 388 - SourcesMsg (Sources.AssignFormStep 2) 389 - in 390 - column Zed 391 - [ center 392 - , height fill 393 - , onEnterKey msg 394 - , paddingXY (scaled 4) 0 395 - , spacing (scaled 8) 396 - , verticalCenter 397 - , width fill 398 - ] 399 - [ h2 H2 [] (text "Where is your music stored?") 400 - 401 - -- 402 - , Services.labels isElectron 403 - |> select (AssignFormService >> SourcesMsg) (toString source.service) 404 - |> el Zed [ maxWidth (px 350), width fill ] 405 - 406 - -- 407 - , btn 408 - Button 409 - [ case source.service of 410 - Local -> 411 - onClick (SourcesMsg RequestLocalPath) 412 - 413 - _ -> 414 - onClick msg 415 - ] 416 - (18 417 - |> Icons.arrow_forward colorDerivatives.success 418 - |> html 419 - |> el WithoutLineHeight [ padding (scaled -15) ] 420 - ) 421 - ] 422 - 423 - 424 - pageNewStep2 : Source -> String -> Node 425 - pageNewStep2 source origin = 426 - let 427 - msg = 428 - case source.service of 429 - Dropbox -> 430 - RoutingMsg (RedirectTo <| Dropbox.authorizationUrl origin source.data) 431 - 432 - Google -> 433 - RoutingMsg (RedirectTo <| Google.authorizationUrl origin source.data) 434 - 435 - _ -> 436 - if validateProperties source then 437 - SourcesMsg (Sources.AssignFormStep 3) 438 - 439 - else 440 - "I need more data in order to continue" 441 - |> Notifications.Types.Error 442 - |> ShowNotification 443 - in 444 - column Zed 445 - [ height fill 446 - , onEnterKey msg 447 - , paddingXY (scaled 4) 0 448 - , spacing (scaled 8) 449 - , verticalCenter 450 - , width fill 451 - ] 452 - [ h3 453 - H3 454 - [] 455 - (text "Where exactly?") 456 - 457 - -- 458 - , paragraph 459 - Columns 460 - [ width fill ] 461 - (renderSourceProperties source) 462 - 463 - -- 464 - , btn 465 - Button 466 - [ center, onClick msg ] 467 - (18 468 - |> Icons.arrow_forward colorDerivatives.success 469 - |> html 470 - |> el WithoutLineHeight [ padding (scaled -15) ] 471 - ) 472 - ] 473 - 474 - 475 - pageNewStep3 : Source -> Node 476 - pageNewStep3 source = 477 - let 478 - msg = 479 - if validateProperties source then 480 - SourcesMsg Sources.SubmitForm 481 - 482 - else 483 - "I need more data in order to continue" 484 - |> Notifications.Types.Error 485 - |> ShowNotification 486 - in 487 - column Zed 488 - [ center 489 - , height fill 490 - , onEnterKey msg 491 - , paddingXY (scaled 4) 0 492 - , spacing (scaled 6) 493 - , verticalCenter 494 - , width fill 495 - ] 496 - [ h2 H2 [] (text "One last thing") 497 - 498 - -- 499 - , lbl "What are we going to call this source?" 500 - 501 - -- 502 - , Input.text 503 - (Form Input) 504 - [ center 505 - , inputBottomPadding 506 - , inputTopPadding 507 - , maxWidth (px 420) 508 - , width fill 509 - ] 510 - { onChange = 511 - SourcesMsg << Sources.AssignFormProperty "name" 512 - , value = 513 - source.data 514 - |> Dict.get "name" 515 - |> Maybe.withDefault "" 516 - , label = 517 - Input.placeholder 518 - { text = 519 - source.service 520 - |> Services.properties 521 - |> List.reverse 522 - |> List.head 523 - |> Maybe.map (\( _, l, _, _ ) -> l) 524 - |> Maybe.withDefault "Label" 525 - , label = 526 - Input.hiddenLabel "name" 527 - } 528 - , options = [] 529 - } 530 - 531 - -- 532 - , paragraph 533 - Intro 534 - [ inlineStyle 535 - [ ( "text-align", "center" ) ] 536 - , paddingBottom (scaled -4) 537 - , paddingTop (scaled 4) 538 - ] 539 - [ 14 540 - |> Icons.warning colorDerivatives.text 541 - |> html 542 - |> el Zed [ moveDown 2, paddingRight (scaled -8) ] 543 - 544 - -- 545 - , bold "Make sure CORS is enabled" 546 - , lineBreak 547 - , text "You can find the instructions over " 548 - , html 549 - (Html.a 550 - [ Html.Attributes.class "is-styled-link" 551 - , Html.Attributes.href "/about#CORS" 552 - , Html.Attributes.target "_blank" 553 - ] 554 - [ Html.text "here" ] 555 - ) 556 - ] 557 - 558 - -- 559 - , btn 560 - Button 561 - [ onClick msg ] 562 - (text "Add source") 563 - ] 564 - 565 - 566 - 567 - -- {Page} Edit 568 - 569 - 570 - pageEdit : Sources.Form -> Node 571 - pageEdit sForm = 572 - column 573 - Zed 574 - [ height fill ] 575 - (case sForm of 576 - EditForm source -> 577 - [ ------------------------------------ 578 - -- Navigation 579 - ------------------------------------ 580 - Navigation.insideCustom 581 - [ ( Icon Icons.arrow_back 582 - , Label (Hidden "Go back") 583 - -- 584 - , Sources.Index 585 - |> Routing.Types.Sources 586 - |> Routing.Types.GoToPage 587 - |> RoutingMsg 588 - ) 589 - ] 590 - 591 - ------------------------------------ 592 - -- Form 593 - ------------------------------------ 594 - , within 595 - [ logoBackdrop, takeOver (pageEditForm source) ] 596 - (takeOver empty) 597 - ] 598 - 599 - _ -> 600 - [ text "Cannot use this model.form on this page" ] 601 - ) 602 - 603 - 604 - pageEditForm : Source -> Node 605 - pageEditForm source = 606 - let 607 - msg = 608 - if validateProperties source then 609 - SourcesMsg Sources.SubmitForm 610 - 611 - else 612 - "I need more data in order to continue" 613 - |> Notifications.Types.Error 614 - |> ShowNotification 615 - in 616 - column Zed 617 - [ height fill 618 - , onEnterKey msg 619 - , paddingXY (scaled 4) 0 620 - , spacing (scaled 8) 621 - , verticalCenter 622 - , width fill 623 - ] 624 - [ h3 625 - H3 626 - [] 627 - (text "Edit source") 628 - 629 - -- 630 - , paragraph 631 - Columns 632 - [ width fill ] 633 - (renderSourceProperties source) 634 - 635 - -- 636 - , btn 637 - Button 638 - [ center, onClick msg ] 639 - (text "Save") 640 - ] 641 - 642 - 643 - 644 - -- Properties 645 - 646 - 647 - propertyRenderer : Source -> ( String, String, String, Bool ) -> Node 648 - propertyRenderer source ( propKey, propLabel, propPlaceholder, isPassword ) = 649 - let 650 - input = 651 - if isPassword then 652 - Input.newPassword 653 - 654 - else 655 - Input.text 656 - in 657 - [ lbl propLabel 658 - 659 - -- 660 - , input 661 - (Form Input) 662 - [ center 663 - , inputBottomPadding 664 - , inputTopPadding 665 - , maxWidth (px 420) 666 - , width fill 667 - ] 668 - { onChange = 669 - SourcesMsg << Sources.AssignFormProperty propKey 670 - , value = 671 - source.data 672 - |> Dict.get propKey 673 - |> Maybe.withDefault "" 674 - , label = 675 - Input.placeholder 676 - { text = propPlaceholder 677 - , label = Input.hiddenLabel propKey 678 - } 679 - , options = [] 680 - } 681 - ] 682 - |> column Zed [ inlineStyle [ ( "display", "block" ) ] ] 683 - |> el ColumnsChild [ paddingXY 0 (scaled 1) ] 684 - 685 - 686 - renderSourceProperties : Source -> List Node 687 - renderSourceProperties source = 688 - source.service 689 - |> Services.properties 690 - |> List.filter sourcePropertiesFilterer 691 - |> List.map (propertyRenderer source) 692 - 693 - 694 - sourcePropertiesToIgnore : List String 695 - sourcePropertiesToIgnore = 696 - [ "accessToken", "authCode", "name", "refreshToken" ] 697 - 698 - 699 - sourcePropertiesFilterer : ( String, a, b, c ) -> Bool 700 - sourcePropertiesFilterer ( k, _, _, _ ) = 701 - List.notMember k sourcePropertiesToIgnore 702 - 703 - 704 - validateProperties : Source -> Bool 705 - validateProperties source = 706 - source.data 707 - |> Dict.toList 708 - |> List.filter (\( k, _ ) -> List.member k sourcePropertiesNotToValidate == False) 709 - |> List.all (\( _, v ) -> not <| String.isEmpty v) 710 - 711 - 712 - sourcePropertiesNotToValidate : List String 713 - sourcePropertiesNotToValidate = 714 - [ "directoryPath" 715 - , "folderId" 716 - , "host" 717 - , "password" 718 - , "username" 719 - ]
-77
src/Js/Workers/search.js
··· 1 - // 2 - // Search worker 3 - // (◡ ‿ ◡ ✿) 4 - // 5 - // This worker is responsible for searching through a `Track` collection. 6 - 7 - importScripts("/vendor/lunr.js"); 8 - 9 - 10 - let index; 11 - 12 - 13 - // 14 - // Incoming messages 15 - 16 - self.onmessage = event => { 17 - switch (event.data.action) { 18 - case "PERFORM_SEARCH": 19 - performSearch(event.data.data); 20 - break; 21 - 22 - case "UPDATE_SEARCH_INDEX": 23 - updateSearchIndex(event.data.data); 24 - break; 25 - } 26 - }; 27 - 28 - 29 - 30 - // 31 - // Track -> IndexedTrack 32 - 33 - const mapTrack = track => ({ 34 - id: track.id, 35 - album: track.tags.album, 36 - artist: track.tags.artist, 37 - title: track.tags.title 38 - }); 39 - 40 - 41 - 42 - // 43 - // Actions 44 - 45 - function performSearch(searchTerm) { 46 - let results = []; 47 - 48 - if (index) { 49 - results = index 50 - .search( 51 - searchTerm.replace(" *", "") 52 - ) 53 - .map(s => s.ref); 54 - } 55 - 56 - self.postMessage({ 57 - action: "PERFORM_SEARCH", 58 - data: results 59 - }); 60 - } 61 - 62 - 63 - function updateSearchIndex(input) { 64 - const tracks = typeof input == "string" 65 - ? JSON.parse(input) 66 - : input; 67 - 68 - index = lunr(function() { 69 - this.field("album"); 70 - this.field("artist"); 71 - this.field("title"); 72 - 73 - (tracks || []) 74 - .map(mapTrack) 75 - .forEach(t => this.add(t)); 76 - }); 77 - }