Support video widgets (youtube, vimeo, dailymotion)

Adds #89
This commit is contained in:
Travis Ralston 2017-10-10 20:44:09 -06:00
parent 38ac6ec4e9
commit c200020e55
18 changed files with 417 additions and 144 deletions

View File

@ -0,0 +1,7 @@
# All this configuration does is make "Youtube Widget" available in the UI
type: "widget"
integrationType: "youtube"
enabled: true
name: "YouTube Video"
about: "Embed a YouTube, Vimeo, or DailyMotion video"
avatar: "img/avatars/youtube.png"

48
package-lock.json generated
View File

@ -2096,6 +2096,16 @@
"minimalistic-crypto-utils": "1.0.1" "minimalistic-crypto-utils": "1.0.1"
} }
}, },
"embed-video": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/embed-video/-/embed-video-2.0.0.tgz",
"integrity": "sha1-1/JouzRkIg9pXbM6YCHhpgjI4fk=",
"requires": {
"fetch-ponyfill": "4.1.0",
"lodash.escape": "4.0.1",
"promise-polyfill": "6.0.2"
}
},
"emojis-list": { "emojis-list": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz",
@ -2107,6 +2117,14 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
"integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA="
}, },
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"requires": {
"iconv-lite": "0.4.15"
}
},
"enhanced-resolve": { "enhanced-resolve": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.3.0.tgz",
@ -2621,6 +2639,14 @@
"websocket-driver": "0.6.5" "websocket-driver": "0.6.5"
} }
}, },
"fetch-ponyfill": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz",
"integrity": "sha1-rjzl9zLGReq4fkroeTQUcJsjmJM=",
"requires": {
"node-fetch": "1.7.3"
}
},
"file-loader": { "file-loader": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.0.0.tgz", "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-1.0.0.tgz",
@ -3739,8 +3765,7 @@
"is-stream": { "is-stream": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
"dev": true
}, },
"is-svg": { "is-svg": {
"version": "2.1.0", "version": "2.1.0",
@ -4028,6 +4053,11 @@
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
}, },
"lodash.escape": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz",
"integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg="
},
"lodash.memoize": { "lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -4470,6 +4500,15 @@
"minimatch": "3.0.4" "minimatch": "3.0.4"
} }
}, },
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"requires": {
"encoding": "0.1.12",
"is-stream": "1.1.0"
}
},
"node-forge": { "node-forge": {
"version": "0.6.33", "version": "0.6.33",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.6.33.tgz", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.6.33.tgz",
@ -6555,6 +6594,11 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
}, },
"promise-polyfill": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-6.0.2.tgz",
"integrity": "sha1-2chtPcTcLfkBboiUbe/Wm0m0EWI="
},
"prompt": { "prompt": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/prompt/-/prompt-1.0.0.tgz", "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.0.0.tgz",

View File

@ -26,6 +26,7 @@
"db-migrate": "^0.10.0-beta.23", "db-migrate": "^0.10.0-beta.23",
"db-migrate-sqlite3": "^0.2.1", "db-migrate-sqlite3": "^0.2.1",
"dns-then": "^0.1.0", "dns-then": "^0.1.0",
"embed-video": "^2.0.0",
"express": "^4.15.4", "express": "^4.15.4",
"js-yaml": "^3.9.1", "js-yaml": "^3.9.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",

View File

@ -27,6 +27,8 @@ import { MyFilterPipe } from "./shared/my-filter.pipe";
import { GenericWidgetWrapperComponent } from "./widget_wrappers/generic/generic.component"; import { GenericWidgetWrapperComponent } from "./widget_wrappers/generic/generic.component";
import { ToggleFullscreenDirective } from "./shared/toggle-fullscreen.directive"; import { ToggleFullscreenDirective } from "./shared/toggle-fullscreen.directive";
import { FullscreenButtonComponent } from "./fullscreen-button/fullscreen-button.component"; import { FullscreenButtonComponent } from "./fullscreen-button/fullscreen-button.component";
import { YoutubeWidgetConfigComponent } from "./configs/widget/youtube/youtube-config.component";
import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.component";
@NgModule({ @NgModule({
imports: [ imports: [
@ -55,6 +57,8 @@ import { FullscreenButtonComponent } from "./fullscreen-button/fullscreen-button
GenericWidgetWrapperComponent, GenericWidgetWrapperComponent,
ToggleFullscreenDirective, ToggleFullscreenDirective,
FullscreenButtonComponent, FullscreenButtonComponent,
YoutubeWidgetConfigComponent,
VideoWidgetWrapperComponent,
// Vendor // Vendor
], ],
@ -73,6 +77,7 @@ import { FullscreenButtonComponent } from "./fullscreen-button/fullscreen-button
TravisCiConfigComponent, TravisCiConfigComponent,
IrcConfigComponent, IrcConfigComponent,
CustomWidgetConfigComponent, CustomWidgetConfigComponent,
YoutubeWidgetConfigComponent,
] ]
}) })
export class AppModule { export class AppModule {

View File

@ -2,11 +2,13 @@ import { RouterModule, Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component"; import { HomeComponent } from "./home/home.component";
import { RiotComponent } from "./riot/riot.component"; import { RiotComponent } from "./riot/riot.component";
import { GenericWidgetWrapperComponent } from "./widget_wrappers/generic/generic.component"; import { GenericWidgetWrapperComponent } from "./widget_wrappers/generic/generic.component";
import { VideoWidgetWrapperComponent } from "./widget_wrappers/video/video.component";
const routes: Routes = [ const routes: Routes = [
{path: "", component: HomeComponent}, {path: "", component: HomeComponent},
{path: "riot", component: RiotComponent}, {path: "riot", component: RiotComponent},
{path: "widgets/generic", component: GenericWidgetWrapperComponent}, {path: "widgets/generic", component: GenericWidgetWrapperComponent},
{path: "widgets/video", component: VideoWidgetWrapperComponent},
]; ];
export const routing = RouterModule.forRoot(routes); export const routing = RouterModule.forRoot(routes);

View File

@ -18,7 +18,7 @@
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="text" class="form-control" <input type="text" class="form-control"
placeholder="Custom widget URL" placeholder="Custom widget URL"
[(ngModel)]="widgetUrl" name="widgetUrl" [(ngModel)]="newWidgetUrl" name="newWidgetUrl"
[disabled]="isUpdating"> [disabled]="isUpdating">
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating"> <button type="submit" class="btn btn-success" [disabled]="isUpdating">

View File

@ -1,12 +1,10 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ModalComponent, DialogRef } from "ngx-modialog"; import { ModalComponent, DialogRef } from "ngx-modialog";
import { WidgetComponent, SCALAR_WIDGET_LINKS } from "../widget.component"; import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service"; import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component"; import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster"; import { ToasterService } from "angular2-toaster";
import { Widget, WIDGET_DIM_CUSTOM, WIDGET_SCALAR_CUSTOM } from "../../../shared/models/widget"; import { WIDGET_DIM_CUSTOM, WIDGET_SCALAR_CUSTOM } from "../../../shared/models/widget";
// TODO: A lot of this can probably be abstracted out for other widgets (even the UI), possibly even for other integrations
@Component({ @Component({
selector: "my-customwidget-config", selector: "my-customwidget-config",
@ -15,136 +13,20 @@ import { Widget, WIDGET_DIM_CUSTOM, WIDGET_SCALAR_CUSTOM } from "../../../shared
}) })
export class CustomWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> { export class CustomWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
public isLoading = true;
public isUpdating = false;
public widgets: Widget[];
public widgetUrl = "";
private toggledWidgets: string[] = [];
private wrapperUrl = "";
private requestedEditId: string = null;
constructor(public dialog: DialogRef<ConfigModalContext>, constructor(public dialog: DialogRef<ConfigModalContext>,
private toaster: ToasterService, toaster: ToasterService,
scalarService: ScalarService, scalarService: ScalarService,
window: Window) { window: Window) {
super(scalarService, dialog.context.roomId); super(
toaster,
this.requestedEditId = dialog.context.integrationId; scalarService,
dialog.context.roomId,
this.getWidgetsOfType(WIDGET_DIM_CUSTOM, WIDGET_SCALAR_CUSTOM).then(widgets => { window,
this.widgets = widgets; WIDGET_DIM_CUSTOM,
this.isLoading = false; WIDGET_SCALAR_CUSTOM,
this.isUpdating = false; dialog.context.integrationId,
"Custom Widget",
// Unwrap URLs for easy-editing "generic" // wrapper
for (let widget of this.widgets) { );
widget.url = this.getWrappedUrl(widget.url);
}
// See if we should request editing a particular widget
if (this.requestedEditId) {
for (let widget of this.widgets) {
if (widget.id === this.requestedEditId) {
console.log("Requesting edit for " + widget.id);
this.editWidget(widget);
}
}
}
});
this.wrapperUrl = window.location.origin + "/widgets/generic?url=";
}
private getWrappedUrl(url: string): string {
const urls = [this.wrapperUrl].concat(SCALAR_WIDGET_LINKS);
for (let scalarUrl of urls) {
if (url.startsWith(scalarUrl)) {
return decodeURIComponent(url.substring(scalarUrl.length));
}
}
return url;
}
private wrapUrl(url: string): string {
return this.wrapperUrl + encodeURIComponent(url);
}
public addWidget() {
let constructedWidget: Widget = {
id: "dimension-" + (new Date().getTime()),
url: this.wrapUrl(this.widgetUrl),
type: WIDGET_DIM_CUSTOM,
name: "Custom Widget",
};
this.isUpdating = true;
this.scalarApi.setWidget(this.roomId, constructedWidget)
.then(() => this.widgets.push(constructedWidget))
.then(() => constructedWidget.url = this.getWrappedUrl(constructedWidget.url)) // unwrap for immediate editing
.then(() => {
this.isUpdating = false;
this.widgetUrl = "";
this.toaster.pop("success", "Widget added!");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
public saveWidget(widget: Widget) {
if (widget.newUrl.trim().length === 0) {
this.toaster.pop("warning", "Please enter a URL for the widget");
return;
}
widget.name = widget.newName || "Custom Widget";
widget.url = this.wrapUrl(widget.newUrl);
this.isUpdating = true;
this.scalarApi.setWidget(this.roomId, widget)
.then(() => this.toggleWidget(widget))
.then(() => {
this.isUpdating = false;
this.toaster.pop("success", "Widget updated!");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
public removeWidget(widget: Widget) {
this.isUpdating = true;
this.scalarApi.deleteWidget(this.roomId, widget)
.then(() => this.widgets.splice(this.widgets.indexOf(widget), 1))
.then(() => {
this.isUpdating = false;
this.toaster.pop("success", "Widget deleted!");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
public editWidget(widget: Widget) {
widget.newName = widget.name || "Custom Widget";
widget.newUrl = widget.url;
this.toggleWidget(widget);
}
public toggleWidget(widget: Widget) {
let idx = this.toggledWidgets.indexOf(widget.id);
if (idx === -1) this.toggledWidgets.push(widget.id);
else this.toggledWidgets.splice(idx, 1);
}
public isWidgetToggled(widget: Widget) {
return this.toggledWidgets.indexOf(widget.id) !== -1;
} }
} }

View File

@ -1,19 +1,68 @@
import { ScalarService } from "../../shared/scalar.service"; import { ScalarService } from "../../shared/scalar.service";
import { Widget, ScalarToWidgets } from "../../shared/models/widget"; import { Widget, ScalarToWidgets } from "../../shared/models/widget";
import { ToasterService } from "angular2-toaster";
export const SCALAR_WIDGET_LINKS = [ const SCALAR_WIDGET_LINKS = [
"https://scalar-staging.riot.im/scalar/api/widgets/generic.html?url=", "https://scalar-staging.riot.im/scalar/api/widgets/__TYPE__.html?url=",
"https://scalar-staging.vector.im/scalar/api/widgets/generic.html?url=", "https://scalar-staging.vector.im/scalar/api/widgets/__TYPE__.html?url=",
"https://scalar-develop.riot.im/scalar/api/widgets/generic.html?url=", "https://scalar-develop.riot.im/scalar/api/widgets/__TYPE__.html?url=",
"https://demo.riot.im/scalar/api/widgets/generic.html?url=", "https://demo.riot.im/scalar/api/widgets/__TYPE__.html?url=",
]; ];
export class WidgetComponent { export class WidgetComponent {
constructor(protected scalarApi: ScalarService, protected roomId: string) { public isLoading = true;
public isUpdating = false;
public widgets: Widget[];
public newWidgetUrl: string = "";
public newWidgetName: string = "";
private toggledWidgetIds: string[] = [];
private wrapperUrl = "";
private scalarWrapperUrls: string[] = [];
constructor(protected toaster: ToasterService,
protected scalarApi: ScalarService,
protected roomId: string,
window: Window,
private primaryWidgetType: string,
alternateWidgetType: string,
requestedEditId: string,
private defaultName: string,
wrapperId = "generic",
scalarWrapperId = null) {
this.isLoading = true;
this.isUpdating = false;
this.wrapperUrl = window.location.origin + "/widgets/" + wrapperId + "?url=";
if (!scalarWrapperId) scalarWrapperId = wrapperId;
for (let widgetLink of SCALAR_WIDGET_LINKS) {
this.scalarWrapperUrls.push(widgetLink.replace("__TYPE__", scalarWrapperId));
} }
protected getWidgetsOfType(type: string, altType: string = null): Promise<Widget[]> { this.getWidgetsOfType(primaryWidgetType, alternateWidgetType).then(widgets => {
this.widgets = widgets;
this.isLoading = false;
this.isUpdating = false;
// Unwrap URLs for easy-editing
for (let widget of this.widgets) {
this.setWidgetUrl(widget);
}
// See if we should request editing a particular widget
if (requestedEditId) {
for (let widget of this.widgets) {
if (widget.id === requestedEditId) {
console.log("Requesting edit for " + widget.id);
this.editWidget(widget);
}
}
}
});
}
private getWidgetsOfType(type: string, altType: string): Promise<Widget[]> {
return this.scalarApi.getWidgets(this.roomId) return this.scalarApi.getWidgets(this.roomId)
.then(resp => ScalarToWidgets(resp)) .then(resp => ScalarToWidgets(resp))
.then(widgets => { .then(widgets => {
@ -27,4 +76,109 @@ export class WidgetComponent {
return filtered; return filtered;
}); });
} }
private getWrappedUrl(url: string): string {
const urls = [this.wrapperUrl].concat(this.scalarWrapperUrls);
console.log(urls);
for (let scalarUrl of urls) {
if (url.startsWith(scalarUrl)) {
return decodeURIComponent(url.substring(scalarUrl.length));
}
}
return url;
}
private wrapUrl(url: string): string {
return this.wrapperUrl + encodeURIComponent(url);
}
private setWidgetUrl(widget: Widget) {
console.log(widget);
widget.url = this.getWrappedUrl(widget.url);
// Use the Dimension-specific URL override if one is present
if (widget.data && widget.data.dimOriginalUrl) {
widget.url = widget.data.dimOriginalUrl;
}
}
public addWidget(data: any = null) {
let constructedWidget: Widget = {
id: "dimension-" + this.primaryWidgetType + "-" + (new Date().getTime()),
url: this.wrapUrl(this.newWidgetUrl),
type: this.primaryWidgetType,
name: this.newWidgetName || this.defaultName,
data: data,
};
this.isUpdating = true;
this.scalarApi.setWidget(this.roomId, constructedWidget)
.then(() => this.widgets.push(constructedWidget))
.then(() => this.setWidgetUrl(constructedWidget))
.then(() => {
this.isUpdating = false;
this.newWidgetUrl = "";
this.newWidgetName = "";
this.toaster.pop("success", "Widget added!");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
public saveWidget(widget: Widget) {
if (widget.newUrl.trim().length === 0) {
this.toaster.pop("warning", "Please enter a URL for the widget");
return;
}
widget.name = widget.newName || this.defaultName;
widget.url = this.wrapUrl(widget.newUrl);
this.isUpdating = true;
this.scalarApi.setWidget(this.roomId, widget)
.then(() => this.toggleWidget(widget))
.then(() => {
this.isUpdating = false;
this.toaster.pop("success", "Widget updated!");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
public removeWidget(widget: Widget) {
this.isUpdating = true;
this.scalarApi.deleteWidget(this.roomId, widget)
.then(() => this.widgets.splice(this.widgets.indexOf(widget), 1))
.then(() => {
this.isUpdating = false;
this.toaster.pop("success", "Widget deleted!");
})
.catch(err => {
this.toaster.pop("error", err.json().error);
console.error(err);
this.isUpdating = false;
});
}
public editWidget(widget: Widget) {
widget.newName = widget.name || this.defaultName;
widget.newUrl = widget.url;
this.toggleWidget(widget);
}
public toggleWidget(widget: Widget) {
let idx = this.toggledWidgetIds.indexOf(widget.id);
if (idx === -1) this.toggledWidgetIds.push(widget.id);
else this.toggledWidgetIds.splice(idx, 1);
}
public isWidgetToggled(widget: Widget) {
return this.toggledWidgetIds.indexOf(widget.id) !== -1;
}
} }

View File

@ -0,0 +1,65 @@
<div class="config-wrapper">
<img src="/img/close.svg" (click)="dialog.close()" class="close-icon">
<div class="config-header">
<img src="/img/avatars/youtube.png">
<h4>Configure video widgets</h4>
</div>
<div class="config-content" *ngIf="isLoading">
<div class="row">
<div class="col-md-12">
<p><i class="fa fa-circle-notch fa-spin"></i> Loading widgets...</p>
</div>
</div>
</div>
<div class="config-content" *ngIf="!isLoading">
<form (submit)="validateAndAddWidget()" novalidate name="addForm">
<div class="row">
<div class="col-md-8" style="margin-bottom: 12px;">
<div class="input-group input-group-sm">
<input type="text" class="form-control"
placeholder="YouTube, Vimeo, or DailyMotion video URL"
[(ngModel)]="newWidgetUrl" name="newWidgetUrl"
[disabled]="isUpdating">
<span class="input-group-btn">
<button type="submit" class="btn btn-success" [disabled]="isUpdating">
<i class="fa fa-plus-circle"></i> Add Widget
</button>
</span>
</div>
</div>
<div class="col-md-12 removable widget-item" *ngFor="let widget of widgets trackById">
{{ widget.name || widget.url }} <span class="text-muted" *ngIf="widget.ownerId">(added by {{ widget.ownerId }})</span>
<button type="button" class="btn btn-outline-info btn-sm" (click)="editWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-pencil"></i> Edit Widget
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="removeWidget(widget)"
style="margin-top: -5px;" [disabled]="isUpdating">
<i class="fa fa-times"></i> Remove Widget
</button>
<div *ngIf="isWidgetToggled(widget)">
<label>
Widget Name
<input type="text" class="form-control"
placeholder="YouTube Widget"
[(ngModel)]="widget.newName" name="widget-name-{{widget.id}}"
[disabled]="isUpdating">
</label>
<label>
Video URL
<input type="text" class="form-control"
placeholder="YouTube, Vimeo, or DailyMotion video URL"
[(ngModel)]="widget.newUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating">
</label>
<button type="button" class="btn btn-primary btn-sm" (click)="validateAndSaveWidget(widget)">Save
</button>
<button type="button" class="btn btn-outline btn-sm" (click)="toggleWidget(widget)">
Cancel
</button>
</div>
</div>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,4 @@
// component styles are encapsulated and only applied to their components
.widget-item {
margin-top: 3px;
}

View File

@ -0,0 +1,72 @@
import { Component } from "@angular/core";
import { ModalComponent, DialogRef } from "ngx-modialog";
import { WidgetComponent } from "../widget.component";
import { ScalarService } from "../../../shared/scalar.service";
import { ConfigModalContext } from "../../../integration/integration.component";
import { ToasterService } from "angular2-toaster";
import { Widget, WIDGET_SCALAR_YOUTUBE, WIDGET_DIM_YOUTUBE } from "../../../shared/models/widget";
import * as embed from "embed-video";
import * as $ from "jquery";
@Component({
selector: "my-youtubewidget-config",
templateUrl: "youtube-config.component.html",
styleUrls: ["youtube-config.component.scss", "./../../config.component.scss"],
})
export class YoutubeWidgetConfigComponent extends WidgetComponent implements ModalComponent<ConfigModalContext> {
constructor(public dialog: DialogRef<ConfigModalContext>,
toaster: ToasterService,
scalarService: ScalarService,
window: Window) {
super(
toaster,
scalarService,
dialog.context.roomId,
window,
WIDGET_DIM_YOUTUBE,
WIDGET_SCALAR_YOUTUBE,
dialog.context.integrationId,
"Youtube Widget",
"video", // wrapper
"youtube" // scalar wrapper
);
}
public validateAndAddWidget() {
const url = this.getSafeUrl(this.newWidgetUrl);
if (!url) {
this.toaster.pop("warning", "Please enter a YouTube, Vimeo, or DailyMotion video URL");
return;
}
const originalUrl = this.newWidgetUrl;
this.newWidgetUrl = url;
this.addWidget({dimOriginalUrl: originalUrl});
}
public validateAndSaveWidget(widget: Widget) {
const url = this.getSafeUrl(widget.newUrl);
if (!url) {
this.toaster.pop("warning", "Please enter a YouTube, Vimeo, or DailyMotion video URL");
return;
}
widget.data = {dimOriginalUrl: widget.newUrl};
widget.newUrl = url;
this.saveWidget(widget);
}
private getSafeUrl(url) {
const embedCode = embed(url);
if (!embedCode) {
return null;
}
// HACK: Grab the video URL from the iframe
url = $(embedCode).attr("src");
if (url.startsWith("//")) url = "https:" + url;
return url;
}
}

View File

@ -6,7 +6,7 @@ import { ToasterService } from "angular2-toaster";
import { Integration } from "../shared/models/integration"; import { Integration } from "../shared/models/integration";
import { IntegrationService } from "../shared/integration.service"; import { IntegrationService } from "../shared/integration.service";
import * as _ from "lodash"; import * as _ from "lodash";
import { WIDGET_DIM_CUSTOM } from "../shared/models/widget"; import { WIDGET_DIM_CUSTOM, WIDGET_DIM_YOUTUBE } from "../shared/models/widget";
import { IntegrationComponent } from "../integration/integration.component"; import { IntegrationComponent } from "../integration/integration.component";
@Component({ @Component({
@ -73,6 +73,9 @@ export class RiotComponent {
if (this.requestedScreen === "type_" + WIDGET_DIM_CUSTOM) { if (this.requestedScreen === "type_" + WIDGET_DIM_CUSTOM) {
type = "widget"; type = "widget";
integrationType = "customwidget"; integrationType = "customwidget";
} else if (this.requestedScreen === "type_" + WIDGET_DIM_YOUTUBE) {
type = "widget";
integrationType = "youtube";
} else { } else {
console.log("Unknown screen requested: " + this.requestedScreen); console.log("Unknown screen requested: " + this.requestedScreen);
} }

View File

@ -5,6 +5,7 @@ import { ContainerContent } from "ngx-modialog";
import { IrcConfigComponent } from "../configs/irc/irc-config.component"; import { IrcConfigComponent } from "../configs/irc/irc-config.component";
import { TravisCiConfigComponent } from "../configs/travisci/travisci-config.component"; import { TravisCiConfigComponent } from "../configs/travisci/travisci-config.component";
import { CustomWidgetConfigComponent } from "../configs/widget/custom_widget/custom_widget-config.component"; import { CustomWidgetConfigComponent } from "../configs/widget/custom_widget/custom_widget-config.component";
import { YoutubeWidgetConfigComponent } from "../configs/widget/youtube/youtube-config.component";
@Injectable() @Injectable()
export class IntegrationService { export class IntegrationService {
@ -19,7 +20,8 @@ export class IntegrationService {
"irc": true, "irc": true,
}, },
"widget": { "widget": {
"customwidget": true "customwidget": true,
"youtube": true,
}, },
}; };
@ -33,6 +35,7 @@ export class IntegrationService {
}, },
"widget": { "widget": {
"customwidget": CustomWidgetConfigComponent, "customwidget": CustomWidgetConfigComponent,
"youtube": YoutubeWidgetConfigComponent,
}, },
}; };

View File

@ -10,6 +10,7 @@ export const WIDGET_SCALAR_GRAFANA = "grafana";
// Dimension has its own set of types to ensure that we don't conflict with Scalar // Dimension has its own set of types to ensure that we don't conflict with Scalar
export const WIDGET_DIM_CUSTOM = "dimension-customwidget"; export const WIDGET_DIM_CUSTOM = "dimension-customwidget";
export const WIDGET_DIM_YOUTUBE = "dimension-youtube";
export interface Widget { export interface Widget {
id: string; id: string;

View File

@ -0,0 +1 @@
<iframe [src]="embedUrl" frameborder="0" allowfullscreen></iframe>

View File

@ -0,0 +1,10 @@
// component styles are encapsulated and only applied to their components
iframe {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,19 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
@Component({
selector: "my-video-widget-wrapper",
templateUrl: "video.component.html",
styleUrls: ["video.component.scss"],
})
export class VideoWidgetWrapperComponent {
public embedUrl: SafeUrl = null;
constructor(activatedRoute: ActivatedRoute, sanitizer: DomSanitizer) {
let params: any = activatedRoute.snapshot.queryParams;
this.embedUrl = sanitizer.bypassSecurityTrustResourceUrl(params.url);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB