diff --git a/src/Applications/UI.elm b/src/Applications/UI.elm index f66d01b1..9c280a78 100644 --- a/src/Applications/UI.elm +++ b/src/Applications/UI.elm @@ -25,6 +25,7 @@ import Html.Styled.Events exposing (onClick) import Html.Styled.Lazy as Lazy import Json.Decode import Json.Encode +import List.Extra as List import Maybe.Extra as Maybe import Notifications import Process @@ -32,6 +33,8 @@ import Return2 exposing (..) import Return3 import Sources import Sources.Encoding +import Sources.Services.Dropbox +import Sources.Services.Google import Tachyons.Classes as T import Task import Time @@ -55,6 +58,8 @@ import UI.Reply as Reply exposing (Reply(..)) import UI.Settings as Settings import UI.Settings.Page import UI.Sources as Sources +import UI.Sources.ContextMenu as Sources +import UI.Sources.Form import UI.Sources.Page import UI.Svg.Elements import UI.Tracks as Tracks @@ -89,45 +94,45 @@ init flags url key = let maybePage = Page.fromUrl url + + page = + Maybe.withDefault Page.Index maybePage in - ( ----------------------------------------- - -- Initial model - ----------------------------------------- - { contextMenu = Nothing - , currentTime = Time.millisToPosix flags.initialTime - , isLoading = True - , navKey = key - , notifications = [] - , page = Maybe.withDefault Page.Index maybePage - , url = url - , viewport = flags.viewport - - -- Audio - -------- - , audioDuration = 0 - , audioHasStalled = False - , audioIsLoading = False - , audioIsPlaying = False - - -- Children - ----------- - , authentication = Authentication.initialModel url - , backdrop = Backdrop.initialModel - , equalizer = Equalizer.initialModel - , queue = Queue.initialModel - , sources = Sources.initialModel (Maybe.andThen Page.sources maybePage) - , tracks = Tracks.initialModel - } - ----------------------------------------- - -- Initial command - ----------------------------------------- - , case maybePage of - Just _ -> - Cmd.none + { contextMenu = Nothing + , currentTime = Time.millisToPosix flags.initialTime + , isLoading = True + , navKey = key + , notifications = [] + , page = page + , url = url + , viewport = flags.viewport + + -- Audio + -------- + , audioDuration = 0 + , audioHasStalled = False + , audioIsLoading = False + , audioIsPlaying = False + + -- Children + ----------- + , authentication = Authentication.initialModel url + , backdrop = Backdrop.initialModel + , equalizer = Equalizer.initialModel + , queue = Queue.initialModel + , sources = Sources.initialModel + , tracks = Tracks.initialModel + } + |> update + (PageChanged page) + |> addCommand + (case maybePage of + Just _ -> + Cmd.none - Nothing -> - Nav.replaceUrl key "/" - ) + Nothing -> + Nav.replaceUrl key "/" + ) @@ -222,7 +227,7 @@ update msg model = SignOut -> { model | authentication = Authentication.initialModel model.url - , sources = Sources.initialModel (Page.sources Page.Index) + , sources = Sources.initialModel , tracks = Tracks.initialModel } |> update (BackdropMsg Backdrop.Default) @@ -363,6 +368,73 @@ update msg model = ShowNotification notification -> UI.Notifications.show notification model + ----------------------------------------- + -- Page Transitions + ----------------------------------------- + PageChanged (Page.Sources (UI.Sources.Page.NewThroughRedirect service args)) -> + let + ( sources, form, defaultContext ) = + ( model.sources + , model.sources.form + , UI.Sources.Form.defaultContext + ) + in + { defaultContext + | data = + case service of + Sources.Dropbox -> + Sources.Services.Dropbox.authorizationSourceData args + + Sources.Google -> + Sources.Services.Google.authorizationSourceData args + + _ -> + defaultContext.data + , service = + service + } + |> (\c -> { form | context = c, step = UI.Sources.Form.How }) + |> (\f -> { sources | form = f }) + |> (\s -> { model | sources = s }) + |> return + + PageChanged (Page.Sources (UI.Sources.Page.Edit sourceId)) -> + let + isLoading = + model.isLoading + + maybeSource = + List.find (.id >> (==) sourceId) model.sources.collection + in + case ( isLoading, maybeSource ) of + ( False, Just source ) -> + let + ( sources, form ) = + ( model.sources + , model.sources.form + ) + + newForm = + { form | context = source } + + newSources = + { sources | form = newForm } + in + return { model | sources = newSources } + + ( False, Nothing ) -> + return model + + ( True, _ ) -> + -- Redirect away from edit-source page + UI.Sources.Page.Index + |> Page.Sources + |> ChangeUrlUsingPage + |> updateWithModel model + + PageChanged _ -> + return model + ----------------------------------------- -- URL ----------------------------------------- @@ -385,7 +457,9 @@ update msg model = UrlChanged url -> case Page.fromUrl url of Just page -> - return { model | page = page, url = url } + { model | page = page, url = url } + |> return + |> andThen (update <| PageChanged page) Nothing -> returnWithModel model (Nav.replaceUrl model.navKey "/") @@ -432,6 +506,9 @@ translateReply reply model = ShowMoreAuthenticationOptions coordinates -> return { model | contextMenu = Just (Authentication.moreOptionsMenu coordinates) } + ShowSourceContextMenu coordinates source -> + return { model | contextMenu = Just (Sources.sourceMenu source coordinates) } + ShowTracksContextMenu coordinates tracks -> return { model | contextMenu = Just (Tracks.trackMenu tracks coordinates) } @@ -542,6 +619,18 @@ translateReply reply model = |> TracksMsg |> updateWithModel model + ReplaceSourceInCollection source -> + let + sources = + model.sources + in + model.sources.collection + |> List.map (\s -> ifThenElse (s.id == source.id) source s) + |> (\c -> { sources | collection = c }) + |> (\s -> { model | sources = s }) + |> return + |> andThen (translateReply SaveSources) + ----------------------------------------- -- User Data ----------------------------------------- diff --git a/src/Applications/UI/Core.elm b/src/Applications/UI/Core.elm index 03a47b7d..4be94615 100644 --- a/src/Applications/UI/Core.elm +++ b/src/Applications/UI/Core.elm @@ -127,6 +127,10 @@ type Msg | DismissNotification { id : Int } | RemoveNotification { id : Int } | ShowNotification (Notification Msg) + ----------------------------------------- + -- Page Transitions + ----------------------------------------- + | PageChanged Page ----------------------------------------- -- URL ----------------------------------------- diff --git a/src/Applications/UI/List.elm b/src/Applications/UI/List.elm index 0d993aba..fce0af20 100644 --- a/src/Applications/UI/List.elm +++ b/src/Applications/UI/List.elm @@ -4,9 +4,9 @@ import Chunky exposing (..) import Color exposing (Color) import Color.Ext as Color import Css exposing (px, solid) +import Html.Events.Extra.Mouse as Mouse exposing (onClick) import Html.Styled as Html exposing (Html, fromUnstyled) -import Html.Styled.Attributes exposing (css, style) -import Html.Styled.Events exposing (onClick) +import Html.Styled.Attributes as Attributes exposing (css, style, title) import Tachyons.Classes as T import UI.Kit import VirtualDom @@ -18,7 +18,8 @@ import VirtualDom type alias Action msg = { icon : Color -> Int -> VirtualDom.Node msg - , msg : msg + , msg : Mouse.Event -> msg + , title : String } @@ -54,11 +55,16 @@ item { label, actions } = , chunk [ T.flex, T.items_center ] (List.map - (\{ icon, msg } -> + (\action -> brick - [ onClick msg, style "line-height" "0" ] - [ T.pointer ] - [ fromUnstyled (icon UI.Kit.colors.text 16) ] + [ Attributes.fromUnstyled (onClick action.msg) + , style "line-height" "0" + , title action.title + ] + [ T.ml2 + , T.pointer + ] + [ fromUnstyled (action.icon UI.Kit.colors.text 16) ] ) actions ) diff --git a/src/Applications/UI/Notifications.elm b/src/Applications/UI/Notifications.elm index 7fe79087..166476a4 100644 --- a/src/Applications/UI/Notifications.elm +++ b/src/Applications/UI/Notifications.elm @@ -7,8 +7,9 @@ import Css.Ext as Css import Css.Global import Css.Transitions exposing (transition) import Html.Styled as Html exposing (Html) -import Html.Styled.Attributes exposing (css) +import Html.Styled.Attributes exposing (css, rel) import Html.Styled.Events exposing (onDoubleClick) +import Html.Styled.Lazy import Notifications exposing (..) import Process import Tachyons.Classes as T @@ -79,7 +80,7 @@ view notifications = , T.z_9999 ] (List.map - notificationView + (Html.Styled.Lazy.lazy notificationView) (List.reverse notifications) ) @@ -132,7 +133,7 @@ notificationView notification = T.o_0 else - "" + T.o_100 ] [ contents notification , if options.sticky then diff --git a/src/Applications/UI/Page.elm b/src/Applications/UI/Page.elm index e62ba910..e24acd6e 100644 --- a/src/Applications/UI/Page.elm +++ b/src/Applications/UI/Page.elm @@ -65,6 +65,9 @@ toString page = ----------------------------------------- -- Sources ----------------------------------------- + Sources (Sources.Edit sourceId) -> + "/sources/edit/" ++ sourceId + Sources Sources.Index -> "/sources" @@ -128,19 +131,20 @@ route = ----------------------------------------- -- Queue ----------------------------------------- - , map (Queue Queue.History) (s "queue" s "history") , map (Queue Queue.Index) (s "queue") + , map (Queue Queue.History) (s "queue" s "history") ----------------------------------------- -- Settings ----------------------------------------- - , map (Settings Settings.ImportExport) (s "settings" s "import-export") , map (Settings Settings.Index) (s "settings") + , map (Settings Settings.ImportExport) (s "settings" s "import-export") ----------------------------------------- -- Sources ----------------------------------------- , map (Sources Sources.Index) (s "sources") + , map (Sources << Sources.Edit) (s "sources" s "edit" string) , map (Sources Sources.New) (s "sources" s "new") -- Oauth diff --git a/src/Applications/UI/Reply.elm b/src/Applications/UI/Reply.elm index 80f93296..4a5ff158 100644 --- a/src/Applications/UI/Reply.elm +++ b/src/Applications/UI/Reply.elm @@ -21,6 +21,7 @@ type Reply -- Context Menu ----------------------------------------- | ShowMoreAuthenticationOptions Coordinates + | ShowSourceContextMenu Coordinates Source | ShowTracksContextMenu Coordinates (List IdentifiedTrack) ----------------------------------------- -- Notifications @@ -44,6 +45,7 @@ type Reply | ExternalSourceAuthorization (String -> String) | ProcessSources | RemoveTracksWithSourceId String + | ReplaceSourceInCollection Source ----------------------------------------- -- User Data ----------------------------------------- diff --git a/src/Applications/UI/Sources.elm b/src/Applications/UI/Sources.elm index 8b22e3dc..ec8eeca4 100644 --- a/src/Applications/UI/Sources.elm +++ b/src/Applications/UI/Sources.elm @@ -1,7 +1,10 @@ module UI.Sources exposing (Model, Msg(..), initialModel, update, view) import Chunky exposing (..) +import Conditional exposing (ifThenElse) +import Coordinates exposing (Coordinates) import Dict.Ext as Dict +import Html.Events.Extra.Mouse as Mouse import Html.Styled as Html exposing (Html, text) import Json.Decode as Json import Material.Icons.Action as Icons @@ -34,11 +37,11 @@ type alias Model = } -initialModel : Maybe Sources.Page -> Model -initialModel maybePage = +initialModel : Model +initialModel = { collection = [] , currentTime = Time.millisToPosix 0 - , form = Form.initialModel maybePage + , form = Form.initialModel , isProcessing = False , processingNotificationId = Nothing } @@ -60,8 +63,13 @@ type Msg -- Collection ----------------------------------------- | AddToCollection Source - | RemoveFromCollection String + | RemoveFromCollection { sourceId : String } | UpdateSourceData Json.Value + ----------------------------------------- + -- Individual + ----------------------------------------- + | SourceContextMenu Source Mouse.Event + | ToggleActivation { sourceId : String } update : Msg -> Model -> Return Model Msg Reply @@ -107,7 +115,7 @@ update msg model = , UI.Reply.ProcessSources ] - RemoveFromCollection sourceId -> + RemoveFromCollection { sourceId } -> model.collection |> List.filter (.id >> (/=) sourceId) |> (\c -> { model | collection = c }) @@ -137,6 +145,31 @@ update msg model = |> return |> addReply UI.Reply.SaveSources + ----------------------------------------- + -- Individual + ----------------------------------------- + SourceContextMenu source mouseEvent -> + let + coordinates = + Coordinates.fromTuple mouseEvent.clientPos + in + returnRepliesWithModel + model + [ ShowSourceContextMenu coordinates source ] + + ToggleActivation { sourceId } -> + model.collection + |> List.map + (\source -> + ifThenElse + (source.id == sourceId) + { source | enabled = not source.enabled } + source + ) + |> (\collection -> { model | collection = collection }) + |> return + |> addReply SaveSources + -- 🗺 @@ -149,6 +182,9 @@ view page model = Index -> index model + Edit sourceId -> + List.map (Html.map FormMsg) (Form.edit model.form) + New -> List.map (Html.map FormMsg) (Form.new model.form) @@ -215,7 +251,24 @@ index model = sourceActions : Source -> List (UI.List.Action Msg) sourceActions source = - [ { icon = Icons.close - , msg = RemoveFromCollection source.id + [ { icon = + if source.enabled then + Icons.check + + else + Icons.block + , msg = always (ToggleActivation { sourceId = source.id }) + , title = + if source.enabled then + "Enabled (click to disable)" + + else + "Disabled (click to enable)" + } + + -- + , { icon = Icons.settings + , msg = SourceContextMenu source + , title = "Menu" } ] diff --git a/src/Applications/UI/Sources/ContextMenu.elm b/src/Applications/UI/Sources/ContextMenu.elm new file mode 100644 index 00000000..8e685365 --- /dev/null +++ b/src/Applications/UI/Sources/ContextMenu.elm @@ -0,0 +1,32 @@ +module UI.Sources.ContextMenu exposing (sourceMenu) + +import ContextMenu exposing (..) +import Coordinates exposing (Coordinates) +import Material.Icons.Action as Icons +import Material.Icons.Image as Icons +import Sources exposing (Source) +import UI.Core exposing (Msg(..)) +import UI.Page +import UI.Sources as Sources +import UI.Sources.Page + + + +-- 🔱 + + +sourceMenu : Source -> Coordinates -> ContextMenu Msg +sourceMenu source = + ContextMenu + [ ( Icons.edit + , "Edit" + , source.id + |> UI.Sources.Page.Edit + |> UI.Page.Sources + |> ChangeUrlUsingPage + ) + , ( Icons.delete + , "Remove" + , SourcesMsg (Sources.RemoveFromCollection { sourceId = source.id }) + ) + ] diff --git a/src/Applications/UI/Sources/Form.elm b/src/Applications/UI/Sources/Form.elm index 64fab651..61358c81 100644 --- a/src/Applications/UI/Sources/Form.elm +++ b/src/Applications/UI/Sources/Form.elm @@ -1,4 +1,4 @@ -module UI.Sources.Form exposing (FormStep(..), Model, Msg(..), initialModel, new, takeStepBackwards, takeStepForwards, update) +module UI.Sources.Form exposing (FormStep(..), Model, Msg(..), defaultContext, edit, initialModel, new, takeStepBackwards, takeStepForwards, update) import Browser.Navigation as Nav import Chunky exposing (..) @@ -19,8 +19,8 @@ import Sources.Services.Google import Tachyons.Classes as T import UI.Kit exposing (ButtonType(..), select) import UI.Navigation exposing (..) -import UI.Page -import UI.Reply exposing (Reply) +import UI.Page as Page +import UI.Reply as Reply exposing (Reply) import UI.Sources.Page as Sources @@ -40,31 +40,10 @@ type FormStep | By -initialModel : Maybe Sources.Page -> Model -initialModel maybePage = - { step = - case maybePage of - Just (Sources.NewThroughRedirect _ _) -> - How - - _ -> - Where - , context = - case maybePage of - Just (Sources.NewThroughRedirect Dropbox args) -> - { defaultContext - | data = Sources.Services.Dropbox.authorizationSourceData args - , service = Dropbox - } - - Just (Sources.NewThroughRedirect Google args) -> - { defaultContext - | data = Sources.Services.Google.authorizationSourceData args - , service = Google - } - - _ -> - defaultContext +initialModel : Model +initialModel = + { step = Where + , context = defaultContext } @@ -90,6 +69,8 @@ defaultService = type Msg = AddSource | Bypass + | EditSource + | ReturnToIndex | SelectService String | SetData String String | TakeStep @@ -102,13 +83,26 @@ update msg model = AddSource -> returnRepliesWithModel { model | step = Where, context = defaultContext } - [ UI.Reply.GoToPage (UI.Page.Sources Sources.Index) - , UI.Reply.AddSourceToCollection model.context + [ Reply.GoToPage (Page.Sources Sources.Index) + , Reply.AddSourceToCollection model.context ] Bypass -> return model + EditSource -> + returnRepliesWithModel + { model | step = Where, context = defaultContext } + [ Reply.ReplaceSourceInCollection model.context + , Reply.ProcessSources + , Reply.GoToPage (Page.Sources Sources.Index) + ] + + ReturnToIndex -> + returnRepliesWithModel + model + [ Reply.GoToPage (Page.Sources Sources.Index) ] + SelectService serviceKey -> case Services.keyToType serviceKey of Just service -> @@ -144,13 +138,13 @@ update msg model = ( Where, Dropbox ) -> model.context.data |> Sources.Services.Dropbox.authorizationUrl - |> UI.Reply.ExternalSourceAuthorization + |> Reply.ExternalSourceAuthorization |> returnReplyWithModel model ( Where, Google ) -> model.context.data |> Sources.Services.Google.authorizationUrl - |> UI.Reply.ExternalSourceAuthorization + |> Reply.ExternalSourceAuthorization |> returnReplyWithModel model _ -> @@ -205,7 +199,7 @@ newWhere { context } = UI.Navigation.local [ ( Icon Icons.arrow_back , Label "Back to list" Hidden - , GoToPage (UI.Page.Sources Sources.Index) + , GoToPage (Page.Sources Sources.Index) ) ] @@ -345,6 +339,63 @@ newBy { context } = +-- EDIT + + +edit : Model -> List (Html Msg) +edit { context } = + [ ----------------------------------------- + -- Navigation + ----------------------------------------- + UI.Navigation.local + [ ( Icon Icons.arrow_back + , Label "Go Back" Shown + , PerformMsg ReturnToIndex + ) + ] + + ----------------------------------------- + -- Content + ----------------------------------------- + , (\h -> form [ chunk [ T.tl, T.w_100 ] [ UI.Kit.canister h ] ]) + [ UI.Kit.h3 "Edit source" + + -- Fields + --------- + , let + properties = + Services.properties context.service + + dividingPoint = + toFloat (List.length properties) / 2 + + ( listA, listB ) = + List.splitAt (ceiling dividingPoint) properties + in + chunk + [ T.flex, T.pt3 ] + [ chunk + [ T.flex_grow_1, T.pr3 ] + (List.map (renderProperty context) listA) + , chunk + [ T.flex_grow_1, T.pl3 ] + (List.map (renderProperty context) listB) + ] + + -- Button + --------- + , chunk + [ T.mt3, T.tc ] + [ UI.Kit.button + Normal + EditSource + (text "Save") + ] + ] + ] + + + -- PROPERTIES diff --git a/src/Applications/UI/Sources/Page.elm b/src/Applications/UI/Sources/Page.elm index d48e27e9..df4f88cd 100644 --- a/src/Applications/UI/Sources/Page.elm +++ b/src/Applications/UI/Sources/Page.elm @@ -9,5 +9,6 @@ import Sources exposing (Service) type Page = Index + | Edit String | New | NewThroughRedirect Service { codeOrToken : Maybe String, state : Maybe String } diff --git a/src/Applications/UI/Tracks.elm b/src/Applications/UI/Tracks.elm index 8594aa33..3c17f1ed 100644 --- a/src/Applications/UI/Tracks.elm +++ b/src/Applications/UI/Tracks.elm @@ -106,7 +106,8 @@ update msg model = return model SetEnabledSourceIds sourceIds -> - return { model | enabledSourceIds = sourceIds } + reviseCollection identify + { model | enabledSourceIds = sourceIds } SetNowPlaying maybeIdentifiedTrack -> let diff --git a/src/Library/Notifications.elm b/src/Library/Notifications.elm index 64e70193..4154459d 100644 --- a/src/Library/Notifications.elm +++ b/src/Library/Notifications.elm @@ -108,7 +108,7 @@ errorWithCode content code actions = Html.pre [ style "font-size" "11px" ] [ T.bg_black_50, T.br2, T.mb0, T.mt3, T.pa2 ] - [ Html.code [ class T.v_mid ] [ Html.text code ] ] + [ slab Html.code [] [ T.ws_normal, T.v_mid ] [ Html.text code ] ] ] )