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 <omqmail@gmail.com>
This commit is contained in:
parent
84f74f0353
commit
3fb80554ef
@ -452,6 +452,26 @@ func SetChatDisabled(w http.ResponseWriter, r *http.Request) {
|
|||||||
controllers.WriteSimpleResponse(w, true, "chat disabled status updated")
|
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 {
|
func requirePOST(w http.ResponseWriter, r *http.Request) bool {
|
||||||
if r.Method != controllers.POST {
|
if r.Method != controllers.POST {
|
||||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||||
|
@ -52,7 +52,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
Enabled: data.GetDirectoryEnabled(),
|
Enabled: data.GetDirectoryEnabled(),
|
||||||
InstanceURL: data.GetServerURL(),
|
InstanceURL: data.GetServerURL(),
|
||||||
},
|
},
|
||||||
S3: data.GetS3Config(),
|
S3: data.GetS3Config(),
|
||||||
|
ExternalActions: data.GetExternalActions(),
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@ -63,16 +64,17 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type serverConfigAdminResponse struct {
|
type serverConfigAdminResponse struct {
|
||||||
InstanceDetails webConfigResponse `json:"instanceDetails"`
|
InstanceDetails webConfigResponse `json:"instanceDetails"`
|
||||||
FFmpegPath string `json:"ffmpegPath"`
|
FFmpegPath string `json:"ffmpegPath"`
|
||||||
StreamKey string `json:"streamKey"`
|
StreamKey string `json:"streamKey"`
|
||||||
WebServerPort int `json:"webServerPort"`
|
WebServerPort int `json:"webServerPort"`
|
||||||
RTMPServerPort int `json:"rtmpServerPort"`
|
RTMPServerPort int `json:"rtmpServerPort"`
|
||||||
S3 models.S3 `json:"s3"`
|
S3 models.S3 `json:"s3"`
|
||||||
VideoSettings videoSettings `json:"videoSettings"`
|
VideoSettings videoSettings `json:"videoSettings"`
|
||||||
LatencyLevel int `json:"latencyLevel"`
|
LatencyLevel int `json:"latencyLevel"`
|
||||||
YP yp `json:"yp"`
|
YP yp `json:"yp"`
|
||||||
ChatDisabled bool `json:"chatDisabled"`
|
ChatDisabled bool `json:"chatDisabled"`
|
||||||
|
ExternalActions []models.ExternalAction `json:"externalActions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type videoSettings struct {
|
type videoSettings struct {
|
||||||
|
@ -12,16 +12,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type webConfigResponse struct {
|
type webConfigResponse struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Logo string `json:"logo"`
|
Logo string `json:"logo"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
NSFW bool `json:"nsfw"`
|
NSFW bool `json:"nsfw"`
|
||||||
ExtraPageContent string `json:"extraPageContent"`
|
ExtraPageContent string `json:"extraPageContent"`
|
||||||
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
|
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
|
||||||
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
||||||
ChatDisabled bool `json:"chatDisabled"`
|
ChatDisabled bool `json:"chatDisabled"`
|
||||||
|
ExternalActions []models.ExternalAction `json:"externalActions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebConfig gets the status of the server.
|
// GetWebConfig gets the status of the server.
|
||||||
@ -50,6 +51,7 @@ func GetWebConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
StreamTitle: data.GetStreamTitle(),
|
StreamTitle: data.GetStreamTitle(),
|
||||||
SocialHandles: socialHandles,
|
SocialHandles: socialHandles,
|
||||||
ChatDisabled: data.GetChatDisabled(),
|
ChatDisabled: data.GetChatDisabled(),
|
||||||
|
ExternalActions: data.GetExternalActions(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(configuration); err != nil {
|
if err := json.NewEncoder(w).Encode(configuration); err != nil {
|
||||||
|
@ -34,6 +34,7 @@ const s3StorageConfigKey = "s3_storage_config"
|
|||||||
const videoLatencyLevel = "video_latency_level"
|
const videoLatencyLevel = "video_latency_level"
|
||||||
const videoStreamOutputVariantsKey = "video_stream_output_variants"
|
const videoStreamOutputVariantsKey = "video_stream_output_variants"
|
||||||
const chatDisabledKey = "chat_disabled"
|
const chatDisabledKey = "chat_disabled"
|
||||||
|
const externalActionsKey = "external_actions"
|
||||||
|
|
||||||
// GetExtraPageBodyContent will return the user-supplied body content.
|
// GetExtraPageBodyContent will return the user-supplied body content.
|
||||||
func GetExtraPageBodyContent() string {
|
func GetExtraPageBodyContent() string {
|
||||||
@ -424,6 +425,27 @@ func GetChatDisabled() bool {
|
|||||||
return false
|
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.
|
// VerifySettings will perform a sanity check for specific settings values.
|
||||||
func VerifySettings() error {
|
func VerifySettings() error {
|
||||||
if GetStreamKey() == "" {
|
if GetStreamKey() == "" {
|
||||||
|
17
models/externalAction.go
Normal file
17
models/externalAction.go
Normal file
@ -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"`
|
||||||
|
}
|
@ -150,6 +150,7 @@ func Start() error {
|
|||||||
|
|
||||||
// Connected clients
|
// Connected clients
|
||||||
http.HandleFunc("/api/integrations/clients", middleware.RequireAccessToken(models.ScopeHasAdminAccess, controllers.GetConnectedClients))
|
http.HandleFunc("/api/integrations/clients", middleware.RequireAccessToken(models.ScopeHasAdminAccess, controllers.GetConnectedClients))
|
||||||
|
|
||||||
// Logo path
|
// Logo path
|
||||||
http.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(admin.SetLogoPath))
|
http.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(admin.SetLogoPath))
|
||||||
|
|
||||||
@ -189,6 +190,9 @@ func Start() error {
|
|||||||
// reset the YP registration
|
// reset the YP registration
|
||||||
http.HandleFunc("/api/admin/yp/reset", middleware.RequireAdminAuth(admin.ResetYPRegistration))
|
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
|
port := config.WebServerPort
|
||||||
|
|
||||||
log.Infof("Web server is listening on port %d.", port)
|
log.Infof("Web server is listening on port %d.", port)
|
||||||
|
BIN
webroot/img/loading.gif
Normal file
BIN
webroot/img/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 558 B |
@ -13,6 +13,9 @@ import {
|
|||||||
hasTouchScreen,
|
hasTouchScreen,
|
||||||
getOrientation,
|
getOrientation,
|
||||||
} from './utils/helpers.js';
|
} from './utils/helpers.js';
|
||||||
|
import ExternalActionModal, {
|
||||||
|
ExternalActionButton,
|
||||||
|
} from './components/external-action-modal.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addNewlines,
|
addNewlines,
|
||||||
@ -73,6 +76,8 @@ export default class App extends Component {
|
|||||||
windowWidth: window.innerWidth,
|
windowWidth: window.innerWidth,
|
||||||
windowHeight: window.innerHeight,
|
windowHeight: window.innerHeight,
|
||||||
orientation: getOrientation(this.hasTouchScreen),
|
orientation: getOrientation(this.hasTouchScreen),
|
||||||
|
|
||||||
|
externalAction: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// timers
|
// timers
|
||||||
@ -96,7 +101,10 @@ export default class App extends Component {
|
|||||||
this.disableChatInput = this.disableChatInput.bind(this);
|
this.disableChatInput = this.disableChatInput.bind(this);
|
||||||
this.setCurrentStreamDuration = this.setCurrentStreamDuration.bind(this);
|
this.setCurrentStreamDuration = this.setCurrentStreamDuration.bind(this);
|
||||||
|
|
||||||
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
this.handleKeyPressed = this.handleKeyPressed.bind(this);
|
this.handleKeyPressed = this.handleKeyPressed.bind(this);
|
||||||
|
this.displayExternalAction = this.displayExternalAction.bind(this);
|
||||||
|
this.closeExternalActionModal = this.closeExternalActionModal.bind(this);
|
||||||
|
|
||||||
// player events
|
// player events
|
||||||
this.handlePlayerReady = this.handlePlayerReady.bind(this);
|
this.handlePlayerReady = this.handlePlayerReady.bind(this);
|
||||||
@ -119,6 +127,7 @@ export default class App extends Component {
|
|||||||
if (this.hasTouchScreen) {
|
if (this.hasTouchScreen) {
|
||||||
window.addEventListener('orientationchange', this.handleWindowResize);
|
window.addEventListener('orientationchange', this.handleWindowResize);
|
||||||
}
|
}
|
||||||
|
window.addEventListener('keydown', this.handleKeyDown);
|
||||||
window.addEventListener('keypress', this.handleKeyPressed);
|
window.addEventListener('keypress', this.handleKeyPressed);
|
||||||
this.player = new OwncastPlayer();
|
this.player = new OwncastPlayer();
|
||||||
this.player.setupPlayerCallbacks({
|
this.player.setupPlayerCallbacks({
|
||||||
@ -140,6 +149,7 @@ export default class App extends Component {
|
|||||||
window.removeEventListener('resize', this.handleWindowResize);
|
window.removeEventListener('resize', this.handleWindowResize);
|
||||||
window.removeEventListener('blur', this.handleWindowBlur);
|
window.removeEventListener('blur', this.handleWindowBlur);
|
||||||
window.removeEventListener('focus', this.handleWindowFocus);
|
window.removeEventListener('focus', this.handleWindowFocus);
|
||||||
|
window.removeEventListener('keydown', this.handleKeyDown);
|
||||||
window.removeEventListener('keypress', this.handleKeyPressed);
|
window.removeEventListener('keypress', this.handleKeyPressed);
|
||||||
if (this.hasTouchScreen) {
|
if (this.hasTouchScreen) {
|
||||||
window.removeEventListener('orientationchange', this.handleWindowResize);
|
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) {
|
handleKeyPressed(e) {
|
||||||
if (
|
if (
|
||||||
e.code === 'Space' &&
|
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) {
|
render(props, state) {
|
||||||
const {
|
const {
|
||||||
chatInputEnabled,
|
chatInputEnabled,
|
||||||
@ -413,6 +458,7 @@ export default class App extends Component {
|
|||||||
websocket,
|
websocket,
|
||||||
windowHeight,
|
windowHeight,
|
||||||
windowWidth,
|
windowWidth,
|
||||||
|
externalAction,
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -424,23 +470,12 @@ export default class App extends Component {
|
|||||||
name,
|
name,
|
||||||
extraPageContent,
|
extraPageContent,
|
||||||
chatDisabled,
|
chatDisabled,
|
||||||
|
externalActions,
|
||||||
} = configData;
|
} = configData;
|
||||||
|
|
||||||
const bgUserLogo = { backgroundImage: `url(${logo})` };
|
const bgUserLogo = { backgroundImage: `url(${logo})` };
|
||||||
|
|
||||||
const tagList =
|
const tagList = tags !== null && tags.length > 0 && tags.join(' #');
|
||||||
tags !== null && tags.length > 0
|
|
||||||
? tags.map(
|
|
||||||
(tag, index) => html`
|
|
||||||
<li
|
|
||||||
key="tag${index}"
|
|
||||||
class="tag rounded-sm text-gray-100 bg-gray-700 text-xs uppercase mr-3 mb-2 p-2 whitespace-no-wrap"
|
|
||||||
>
|
|
||||||
${tag}
|
|
||||||
</li>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const viewerCountMessage =
|
const viewerCountMessage =
|
||||||
streamOnline && viewerCount > 0
|
streamOnline && viewerCount > 0
|
||||||
@ -470,6 +505,32 @@ export default class App extends Component {
|
|||||||
? null
|
? null
|
||||||
: html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `;
|
: html` <${VideoPoster} offlineImage=${logo} active=${streamOnline} /> `;
|
||||||
|
|
||||||
|
const externalActionButtons =
|
||||||
|
externalActions &&
|
||||||
|
html`<div
|
||||||
|
id="external-actions-container"
|
||||||
|
class="flex flex-row align-center"
|
||||||
|
>
|
||||||
|
${externalActions.map(
|
||||||
|
function (action, index) {
|
||||||
|
return html`<${ExternalActionButton}
|
||||||
|
onClick=${this.displayExternalAction}
|
||||||
|
action=${action}
|
||||||
|
index=${index}
|
||||||
|
/>`;
|
||||||
|
}.bind(this)
|
||||||
|
)}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
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`
|
return html`
|
||||||
<div
|
<div
|
||||||
id="app-container"
|
id="app-container"
|
||||||
@ -555,6 +616,7 @@ export default class App extends Component {
|
|||||||
<div
|
<div
|
||||||
class="user-content-header border-b border-gray-500 border-solid"
|
class="user-content-header border-b border-gray-500 border-solid"
|
||||||
>
|
>
|
||||||
|
${externalActionButtons}
|
||||||
<h2 class="font-semibold text-5xl">
|
<h2 class="font-semibold text-5xl">
|
||||||
<span class="streamer-name text-indigo-600">${name}</span>
|
<span class="streamer-name text-indigo-600">${name}</span>
|
||||||
</h2>
|
</h2>
|
||||||
@ -567,9 +629,9 @@ export default class App extends Component {
|
|||||||
class="stream-summary my-4"
|
class="stream-summary my-4"
|
||||||
dangerouslySetInnerHTML=${{ __html: summary }}
|
dangerouslySetInnerHTML=${{ __html: summary }}
|
||||||
></div>
|
></div>
|
||||||
<ul id="tag-list" class="tag-list flex flex-row flex-wrap my-4">
|
<div id="tag-list" class="tag-list text-gray-600 mb-3">
|
||||||
${tagList}
|
${tagList && `#${tagList}`}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -592,6 +654,7 @@ export default class App extends Component {
|
|||||||
chatInputEnabled=${chatInputEnabled && !chatDisabled}
|
chatInputEnabled=${chatInputEnabled && !chatDisabled}
|
||||||
instanceTitle=${name}
|
instanceTitle=${name}
|
||||||
/>
|
/>
|
||||||
|
${externalActionModal}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
86
webroot/js/components/external-action-modal.js
Normal file
86
webroot/js/components/external-action-modal.js
Normal file
@ -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`
|
||||||
|
<div class="fixed inset-0 overflow-y-auto" style="z-index: 9999">
|
||||||
|
<div
|
||||||
|
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
|
||||||
|
>
|
||||||
|
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
|
||||||
|
<div
|
||||||
|
onClick=${() => onClose()}
|
||||||
|
class="absolute inset-0 bg-gray-900 bg-opacity-75"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- This element is to trick the browser into centering the modal contents. -->
|
||||||
|
<span
|
||||||
|
class="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||||
|
aria-hidden="true"
|
||||||
|
>​</span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:align-middle w-screen md:max-w-2xl lg:max-w-2xl"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="modal-headline"
|
||||||
|
>
|
||||||
|
<div class="bg-white ">
|
||||||
|
<div
|
||||||
|
class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex justify-between items-center"
|
||||||
|
>
|
||||||
|
<h3 class="font-bold hidden md:block">${title}</h3>
|
||||||
|
<span class="" onclick=${onClose}>
|
||||||
|
<svg
|
||||||
|
class="h-12 w-12 fill-current text-grey hover:text-grey-darkest"
|
||||||
|
role="button"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<title>Close</title>
|
||||||
|
<path
|
||||||
|
d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TODO: Show a loading spinner while the iframe loads -->
|
||||||
|
<iframe
|
||||||
|
style="height: 70vh"
|
||||||
|
width="100%"
|
||||||
|
allowpaymentrequest="true"
|
||||||
|
allowfullscreen="false"
|
||||||
|
sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
|
||||||
|
src=${url}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExternalActionButton({ index, action, onClick }) {
|
||||||
|
const { title, icon, color = undefined } = action;
|
||||||
|
const logo =
|
||||||
|
icon &&
|
||||||
|
html`
|
||||||
|
<span class="external-action-icon"><img src=${icon} alt="" /></span>
|
||||||
|
`;
|
||||||
|
const bgcolor = color && { backgroundColor: `${color}` };
|
||||||
|
const handleClick = () => onClick(index);
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class="external-action-button rounded-sm flex flex-row justify-center items-center overflow-hidden bg-gray-800"
|
||||||
|
data-index=${index}
|
||||||
|
onClick=${handleClick}
|
||||||
|
style=${bgcolor}
|
||||||
|
>
|
||||||
|
${logo}
|
||||||
|
<span class="external-action-label">${title}</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
@ -82,6 +82,37 @@ header {
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#external-actions-container {
|
||||||
|
margin: 1em 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
.external-action-button {
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: .88em;
|
||||||
|
padding: .25em .75em;
|
||||||
|
margin: .35em;
|
||||||
|
margin-left: 0;
|
||||||
|
display: flex;
|
||||||
|
max-width: 250px;
|
||||||
|
padding-top: .3em;
|
||||||
|
}
|
||||||
|
.external-action-button:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.external-action-icon {
|
||||||
|
margin: .25em .5em .25em 0;
|
||||||
|
}
|
||||||
|
.external-action-icon img {
|
||||||
|
height: 1.5em;
|
||||||
|
width: 1.5em;
|
||||||
|
}
|
||||||
|
.external-action-label {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ************************************************ */
|
/* ************************************************ */
|
||||||
|
|
||||||
@ -245,3 +276,7 @@ header {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
iframe{
|
||||||
|
background:url(/img/loading.gif) center center no-repeat; height: 100%;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user