From 3fb80554ef32a7f8f21490bd2fc6016c8e5b7abe Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Mon, 15 Mar 2021 15:32:52 -0700 Subject: [PATCH] Gek/external actions (#827) * WIP External actions modal frontend * Add external action links * Allow modal to show/hide and use a dynamic url * Use external link object instead of just url for state * add style and placement to external action buttons * reformat and simplify tag list style as not to conflict with action buttons and make them look less actionable since they're not * fix bug to open modal * have Esc key close modal * fix style on modal * make modal bg darker * close modal when you click outside of it * fix zindex * Add support for external action icons and colors * Some external action modal sizing + loading spinner Co-authored-by: Ginger Wong --- controllers/admin/config.go | 20 ++++ controllers/admin/serverConfig.go | 24 +++-- controllers/config.go | 22 ++-- core/data/config.go | 22 ++++ models/externalAction.go | 17 ++++ router/router.go | 4 + webroot/img/loading.gif | Bin 0 -> 8648 bytes webroot/img/video-settings.png | Bin 465 -> 558 bytes webroot/js/app.js | 95 +++++++++++++++--- .../js/components/external-action-modal.js | 86 ++++++++++++++++ webroot/styles/app.css | 35 +++++++ 11 files changed, 288 insertions(+), 37 deletions(-) create mode 100644 models/externalAction.go create mode 100644 webroot/img/loading.gif create mode 100644 webroot/js/components/external-action-modal.js diff --git a/controllers/admin/config.go b/controllers/admin/config.go index 29aac58e0..4eec1b08c 100644 --- a/controllers/admin/config.go +++ b/controllers/admin/config.go @@ -452,6 +452,26 @@ func SetChatDisabled(w http.ResponseWriter, r *http.Request) { controllers.WriteSimpleResponse(w, true, "chat disabled status updated") } +// SetExternalActions will set the 3rd party actions for the web interface. +func SetExternalActions(w http.ResponseWriter, r *http.Request) { + type externalActionsRequest struct { + Value []models.ExternalAction `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var actions externalActionsRequest + if err := decoder.Decode(&actions); err != nil { + controllers.WriteSimpleResponse(w, false, "unable to update external actions with provided values") + return + } + + if err := data.SetExternalActions(actions.Value); err != nil { + controllers.WriteSimpleResponse(w, false, "unable to update external actions with provided values") + } + + controllers.WriteSimpleResponse(w, true, "external actions update") +} + func requirePOST(w http.ResponseWriter, r *http.Request) bool { if r.Method != controllers.POST { controllers.WriteSimpleResponse(w, false, r.Method+" not supported") diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go index fb721dd0c..6d99f001b 100644 --- a/controllers/admin/serverConfig.go +++ b/controllers/admin/serverConfig.go @@ -52,7 +52,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { Enabled: data.GetDirectoryEnabled(), InstanceURL: data.GetServerURL(), }, - S3: data.GetS3Config(), + S3: data.GetS3Config(), + ExternalActions: data.GetExternalActions(), } w.Header().Set("Content-Type", "application/json") @@ -63,16 +64,17 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { } type serverConfigAdminResponse struct { - InstanceDetails webConfigResponse `json:"instanceDetails"` - FFmpegPath string `json:"ffmpegPath"` - StreamKey string `json:"streamKey"` - WebServerPort int `json:"webServerPort"` - RTMPServerPort int `json:"rtmpServerPort"` - S3 models.S3 `json:"s3"` - VideoSettings videoSettings `json:"videoSettings"` - LatencyLevel int `json:"latencyLevel"` - YP yp `json:"yp"` - ChatDisabled bool `json:"chatDisabled"` + InstanceDetails webConfigResponse `json:"instanceDetails"` + FFmpegPath string `json:"ffmpegPath"` + StreamKey string `json:"streamKey"` + WebServerPort int `json:"webServerPort"` + RTMPServerPort int `json:"rtmpServerPort"` + S3 models.S3 `json:"s3"` + VideoSettings videoSettings `json:"videoSettings"` + LatencyLevel int `json:"latencyLevel"` + YP yp `json:"yp"` + ChatDisabled bool `json:"chatDisabled"` + ExternalActions []models.ExternalAction `json:"externalActions"` } type videoSettings struct { diff --git a/controllers/config.go b/controllers/config.go index a5016baf0..723681b7f 100644 --- a/controllers/config.go +++ b/controllers/config.go @@ -12,16 +12,17 @@ import ( ) type webConfigResponse struct { - Name string `json:"name"` - Summary string `json:"summary"` - Logo string `json:"logo"` - Tags []string `json:"tags"` - Version string `json:"version"` - NSFW bool `json:"nsfw"` - ExtraPageContent string `json:"extraPageContent"` - StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream - SocialHandles []models.SocialHandle `json:"socialHandles"` - ChatDisabled bool `json:"chatDisabled"` + Name string `json:"name"` + Summary string `json:"summary"` + Logo string `json:"logo"` + Tags []string `json:"tags"` + Version string `json:"version"` + NSFW bool `json:"nsfw"` + ExtraPageContent string `json:"extraPageContent"` + StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream + SocialHandles []models.SocialHandle `json:"socialHandles"` + ChatDisabled bool `json:"chatDisabled"` + ExternalActions []models.ExternalAction `json:"externalActions"` } // GetWebConfig gets the status of the server. @@ -50,6 +51,7 @@ func GetWebConfig(w http.ResponseWriter, r *http.Request) { StreamTitle: data.GetStreamTitle(), SocialHandles: socialHandles, ChatDisabled: data.GetChatDisabled(), + ExternalActions: data.GetExternalActions(), } if err := json.NewEncoder(w).Encode(configuration); err != nil { diff --git a/core/data/config.go b/core/data/config.go index 6a629a58c..9455748ef 100644 --- a/core/data/config.go +++ b/core/data/config.go @@ -34,6 +34,7 @@ const s3StorageConfigKey = "s3_storage_config" const videoLatencyLevel = "video_latency_level" const videoStreamOutputVariantsKey = "video_stream_output_variants" const chatDisabledKey = "chat_disabled" +const externalActionsKey = "external_actions" // GetExtraPageBodyContent will return the user-supplied body content. func GetExtraPageBodyContent() string { @@ -424,6 +425,27 @@ func GetChatDisabled() bool { return false } +// GetExternalActions will return the registered external actions. +func GetExternalActions() []models.ExternalAction { + configEntry, err := _datastore.Get(externalActionsKey) + if err != nil { + return []models.ExternalAction{} + } + + var externalActions []models.ExternalAction + if err := configEntry.getObject(&externalActions); err != nil { + return []models.ExternalAction{} + } + + return externalActions +} + +// SetExternalActions will save external actions. +func SetExternalActions(actions []models.ExternalAction) error { + var configEntry = ConfigEntry{Key: externalActionsKey, Value: actions} + return _datastore.Save(configEntry) +} + // VerifySettings will perform a sanity check for specific settings values. func VerifySettings() error { if GetStreamKey() == "" { diff --git a/models/externalAction.go b/models/externalAction.go new file mode 100644 index 000000000..5508e2096 --- /dev/null +++ b/models/externalAction.go @@ -0,0 +1,17 @@ +package models + +// ExternalAction is a link that will open as a 3rd party action. +type ExternalAction struct { + // URL is the URL to load. + URL string `json:"url"` + // Title is the name of this action, displayed in the modal. + Title string `json:"title"` + // Description is the description of this action. + Description string `json:"description"` + // Icon is the optional icon for the button associated with this action. + Icon string `json:"icon"` + // Color is the optional color for the button associated with this action. + Color string `json:"color"` + // OpenExternally states if the action should open a new tab/window instead of an internal modal. + OpenExternally bool `json:"openExternally"` +} diff --git a/router/router.go b/router/router.go index adc3c1f7c..e28c48cb3 100644 --- a/router/router.go +++ b/router/router.go @@ -150,6 +150,7 @@ func Start() error { // Connected clients http.HandleFunc("/api/integrations/clients", middleware.RequireAccessToken(models.ScopeHasAdminAccess, controllers.GetConnectedClients)) + // Logo path http.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(admin.SetLogoPath)) @@ -189,6 +190,9 @@ func Start() error { // reset the YP registration http.HandleFunc("/api/admin/yp/reset", middleware.RequireAdminAuth(admin.ResetYPRegistration)) + // set external action links + http.HandleFunc("/api/admin/config/externalactions", middleware.RequireAdminAuth(admin.SetExternalActions)) + port := config.WebServerPort log.Infof("Web server is listening on port %d.", port) diff --git a/webroot/img/loading.gif b/webroot/img/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..2846863ac1f5dc9a340b9037d7b3c79b5dc8c142 GIT binary patch literal 8648 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 literal 0 HcmV?d00001 diff --git a/webroot/img/video-settings.png b/webroot/img/video-settings.png index d37f489d88e3e206bbab7ff7680ad957de92f384..a7a97a05169c63b0d8f40e2d659aa771b5033e03 100644 GIT binary patch delta 335 zcmcb}ypCl;cRe42I(x`tWfleohCiMzjv*T7mrk_xc48E1lb-k85FE}bxNz=L9BtJLHpI2nZ@V6 z&N|y<`RPbW<&$Yl;&(ne^v~F{Q)R*0SDnf6x>YGJP6UVE_H+?4UdR$+@vKKkxb9ne zqdr5K1cUjW!t)KWKSB>I;+}8&<*@qK%gpgH5i!@-th`lve^=@EH!Gu}rB9s;iM_S` zebCjJPrt0x;>(p}ujLYx?$W-6Gcwp#>bAE{8T=U|GDQVf&7wKhM=3ke`-(eE*`pjj`O^yN_+#-@3Ny za`^ozdm<&-71CMHG(3J@leEf@Va}?Rp0PJp-eNWgDh(HBTX(h93Y>d( zV2);^;)M3o#+f1BekpGx4O9Hxq8@!HThbmCX@d%8Ug@fduKlY diff --git a/webroot/js/app.js b/webroot/js/app.js index 7d65875d4..47c3e193a 100644 --- a/webroot/js/app.js +++ b/webroot/js/app.js @@ -13,6 +13,9 @@ import { hasTouchScreen, getOrientation, } from './utils/helpers.js'; +import ExternalActionModal, { + ExternalActionButton, +} from './components/external-action-modal.js'; import { addNewlines, @@ -73,6 +76,8 @@ export default class App extends Component { windowWidth: window.innerWidth, windowHeight: window.innerHeight, orientation: getOrientation(this.hasTouchScreen), + + externalAction: null, }; // timers @@ -96,7 +101,10 @@ export default class App extends Component { this.disableChatInput = this.disableChatInput.bind(this); this.setCurrentStreamDuration = this.setCurrentStreamDuration.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyPressed = this.handleKeyPressed.bind(this); + this.displayExternalAction = this.displayExternalAction.bind(this); + this.closeExternalActionModal = this.closeExternalActionModal.bind(this); // player events this.handlePlayerReady = this.handlePlayerReady.bind(this); @@ -119,6 +127,7 @@ export default class App extends Component { if (this.hasTouchScreen) { window.addEventListener('orientationchange', this.handleWindowResize); } + window.addEventListener('keydown', this.handleKeyDown); window.addEventListener('keypress', this.handleKeyPressed); this.player = new OwncastPlayer(); this.player.setupPlayerCallbacks({ @@ -140,6 +149,7 @@ export default class App extends Component { window.removeEventListener('resize', this.handleWindowResize); window.removeEventListener('blur', this.handleWindowBlur); window.removeEventListener('focus', this.handleWindowFocus); + window.removeEventListener('keydown', this.handleKeyDown); window.removeEventListener('keypress', this.handleKeyPressed); if (this.hasTouchScreen) { window.removeEventListener('orientationchange', this.handleWindowResize); @@ -386,6 +396,12 @@ export default class App extends Component { } } + handleKeyDown(e) { + if (e.code === 'Escape' && this.state.externalAction !== null) { + this.closeExternalActionModal(); + } + } + handleKeyPressed(e) { if ( e.code === 'Space' && @@ -396,6 +412,35 @@ export default class App extends Component { } } + displayExternalAction(index) { + const { configData, username } = this.state; + const action = configData.externalActions[index]; + if (!action) { + return; + } + const { url: actionUrl, openExternally } = action || {}; + let url = new URL(actionUrl); + // Append url and username to params so the link knows where we came from and who we are. + url.searchParams.append('username', username); + url.searchParams.append('instance', window.location); + + if (openExternally) { + var win = window.open(url.toString(), '_blank'); + win.focus(); + return; + } + + this.setState({ + externalAction: action, + }); + } + + closeExternalActionModal() { + this.setState({ + externalAction: null, + }); + } + render(props, state) { const { chatInputEnabled, @@ -413,6 +458,7 @@ export default class App extends Component { websocket, windowHeight, windowWidth, + externalAction, } = state; const { @@ -424,23 +470,12 @@ export default class App extends Component { name, extraPageContent, chatDisabled, + externalActions, } = configData; const bgUserLogo = { backgroundImage: `url(${logo})` }; - const tagList = - tags !== null && tags.length > 0 - ? tags.map( - (tag, index) => html` -
  • - ${tag} -
  • - ` - ) - : null; + const tagList = tags !== null && tags.length > 0 && tags.join(' #'); const viewerCountMessage = streamOnline && viewerCount > 0 @@ -470,6 +505,32 @@ export default class App extends Component { ? null : html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `; + const externalActionButtons = + externalActions && + html`
    + ${externalActions.map( + function (action, index) { + return html`<${ExternalActionButton} + onClick=${this.displayExternalAction} + action=${action} + index=${index} + />`; + }.bind(this) + )} +
    `; + + const externalActionModal = externalAction + ? html`<${ExternalActionModal} + title=${this.state.externalAction.description || + this.state.externalAction.title} + url=${this.state.externalAction.url} + onClose=${this.closeExternalActionModal} + />` + : null; + return html`
    + ${externalActionButtons}

    ${name}

    @@ -567,9 +629,9 @@ export default class App extends Component { class="stream-summary my-4" dangerouslySetInnerHTML=${{ __html: summary }} >
    -
      - ${tagList} -
    +
    + ${tagList && `#${tagList}`} +
    + ${externalActionModal}
    `; } diff --git a/webroot/js/components/external-action-modal.js b/webroot/js/components/external-action-modal.js new file mode 100644 index 000000000..89267db34 --- /dev/null +++ b/webroot/js/components/external-action-modal.js @@ -0,0 +1,86 @@ +import { h } from '/js/web_modules/preact.js'; +import htm from '/js/web_modules/htm.js'; +const html = htm.bind(h); + +export default function ExternalActionModal({ url, title, onClose }) { + return html` +
    +
    + + + + +