Some minor W2G and general changes

This commit is contained in:
WolverinDEV 2020-08-08 15:20:32 +02:00
parent 516bcefc36
commit 6b623a4d11
13 changed files with 127 additions and 39 deletions

View file

@ -1,6 +1,9 @@
# Changelog: # Changelog:
* **09.08.20**
- Added a "watch to gather" context menu entry for clients
* **08.08.20** * **08.08.20**
- Added a watch to gether mode - Added a watch to gather mode
- Added API support for the popout able browsers for the native client - Added API support for the popout able browsers for the native client
* **05.08.20** * **05.08.20**

4
shared/img/icon_w2g.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="100%" version="1.1" viewBox="0 0 68 48" width="100%">
<path d="M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z" fill="#f00"></path>
<path d="M 45,24 27,14 27,34" fill="#fff"></path>
</svg>

After

Width:  |  Height:  |  Size: 506 B

View file

@ -36,7 +36,7 @@ import {guid} from "tc-shared/crypto/uid";
import {ServerEventLog} from "tc-shared/ui/frames/log/ServerEventLog"; import {ServerEventLog} from "tc-shared/ui/frames/log/ServerEventLog";
import {EventType} from "tc-shared/ui/frames/log/Definitions"; import {EventType} from "tc-shared/ui/frames/log/Definitions";
import {PluginCmdRegistry} from "tc-shared/connection/PluginCmdHandler"; import {PluginCmdRegistry} from "tc-shared/connection/PluginCmdHandler";
import {W2GPluginCmdHandler} from "tc-shared/video-viewer/W2GPluginHandler"; import {W2GPluginCmdHandler} from "tc-shared/video-viewer/W2GPlugin";
export enum DisconnectReason { export enum DisconnectReason {
HANDLER_DESTROYED, HANDLER_DESTROYED,

View file

@ -16,6 +16,14 @@ export interface ClientGlobalControlEvents {
connection?: ConnectionHandler connection?: ConnectionHandler
}, },
action_w2g: {
following: number,
handlerId: string
} | {
videoUrl: string,
handlerId: string
}
/* some more specific window openings */ /* some more specific window openings */
action_open_window_connect: { action_open_window_connect: {
new_tab: boolean new_tab: boolean

View file

@ -41,7 +41,7 @@ import "./ui/elements/ContextDivider";
import "./ui/elements/Tab"; import "./ui/elements/Tab";
import "./connection/CommandHandler"; import "./connection/CommandHandler";
import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler"; import {ConnectRequestData} from "tc-shared/ipc/ConnectHandler";
import {openVideoViewer} from "tc-shared/video-viewer/Controller"; import "./video-viewer/Controller";
declare global { declare global {
interface Window { interface Window {
@ -497,8 +497,6 @@ function main() {
modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false)); modal.close_listener.push(() => settings.changeGlobal(Settings.KEY_USER_IS_NEW, false));
} }
(window as any).spawnVideoPopout = openVideoViewer;
//spawnVideoPopout(server_connections.active_connection(), "https://www.youtube.com/watch?v=9683D18fyvs"); //spawnVideoPopout(server_connections.active_connection(), "https://www.youtube.com/watch?v=9683D18fyvs");
} }

View file

@ -8,8 +8,8 @@ import {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {spawn_context_menu} from "tc-shared/ui/elements/ContextMenu"; import {spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
import {copy_to_clipboard} from "tc-shared/utils/helpers"; import {copy_to_clipboard} from "tc-shared/utils/helpers";
import {openVideoViewer} from "tc-shared/video-viewer/Controller";
import {server_connections} from "tc-shared/ui/frames/connection_handlers"; import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {global_client_actions} from "tc-shared/events/GlobalEvents";
const playIcon = require("./yt-play-button.svg"); const playIcon = require("./yt-play-button.svg");
const cssStyle = require("./youtube.scss"); const cssStyle = require("./youtube.scss");
@ -36,7 +36,10 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
spawn_context_menu(event.pageX, event.pageY, { spawn_context_menu(event.pageX, event.pageY, {
callback: () => { callback: () => {
openVideoViewer(server_connections.active_connection(), text); global_client_actions.fire("action_w2g", {
videoUrl: text,
handlerId: server_connections.active_connection().handlerId
});
}, },
name: tr("Watch video"), name: tr("Watch video"),
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
@ -59,7 +62,10 @@ loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
> >
<img draggable={false} src={"https://img.youtube.com/vi/" + result[1] + "/hqdefault.jpg"} alt={"Video thumbnail"} title={tra("Youtube video {}", result[1])} /> <img draggable={false} src={"https://img.youtube.com/vi/" + result[1] + "/hqdefault.jpg"} alt={"Video thumbnail"} title={tra("Youtube video {}", result[1])} />
<button className={cssStyle.playButton} onClick={() => { <button className={cssStyle.playButton} onClick={() => {
openVideoViewer(server_connections.active_connection(), text); global_client_actions.fire("action_w2g", {
videoUrl: text,
handlerId: server_connections.active_connection().handlerId
});
}}> }}>
<HTMLRenderer purify={false}>{playIcon}</HTMLRenderer> <HTMLRenderer purify={false}>{playIcon}</HTMLRenderer>
</button> </button>

View file

@ -27,6 +27,8 @@ import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "tc-shared/ui/modal/ModalChangeVolumeNew"; import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "tc-shared/ui/modal/ModalChangeVolumeNew";
import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permission/ModalPermissionEditor"; import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permission/ModalPermissionEditor";
import {EventClient, EventType} from "tc-shared/ui/frames/log/Definitions"; import {EventClient, EventType} from "tc-shared/ui/frames/log/Definitions";
import {W2GPluginCmdHandler} from "tc-shared/video-viewer/W2GPlugin";
import {global_client_actions} from "tc-shared/events/GlobalEvents";
export enum ClientType { export enum ClientType {
CLIENT_VOICE, CLIENT_VOICE,
@ -508,6 +510,8 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
} }
showContextMenu(x: number, y: number, on_close: () => void = undefined) { showContextMenu(x: number, y: number, on_close: () => void = undefined) {
const w2gPlugin = this.channelTree.client.getPluginCmdRegistry().getPluginHandler<W2GPluginCmdHandler>(W2GPluginCmdHandler.kPluginChannel);
let trigger_close = true; let trigger_close = true;
contextmenu.spawn_context_menu(x, y, contextmenu.spawn_context_menu(x, y,
...this.contextmenu_info(), { ...this.contextmenu_info(), {
@ -519,6 +523,17 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
callback: () => { callback: () => {
this.open_text_chat(); this.open_text_chat();
} }
}, {
type: contextmenu.MenuEntryType.ENTRY,
name: tr("Watch clients video"),
icon_path: "img/icon_w2g.svg",
visible: w2gPlugin?.getCurrentWatchers().findIndex(e => e.clientId === this.clientId()) !== -1,
callback: () => {
global_client_actions.fire("action_w2g", {
following: this.clientId(),
handlerId: this.channelTree.client.handlerId
});
}
}, },
contextmenu.Entry.HR(), contextmenu.Entry.HR(),
{ {

View file

@ -2,6 +2,7 @@ import {ClientEntry} from "tc-shared/ui/client";
import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler"; import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
import {EventHandler, Registry} from "tc-shared/events"; import {EventHandler, Registry} from "tc-shared/events";
import { import {
PrivateConversationManagerEvents,
PrivateConversationInfo, PrivateConversationInfo,
PrivateConversationUIEvents PrivateConversationUIEvents
} from "tc-shared/ui/frames/side/PrivateConversationDefinitions"; } from "tc-shared/ui/frames/side/PrivateConversationDefinitions";
@ -252,6 +253,9 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
/* TODO: Move this somehow to the client itself? */ /* TODO: Move this somehow to the client itself? */
if(this.activeClient instanceof ClientEntry) if(this.activeClient instanceof ClientEntry)
this.activeClient.setUnread(timestamp !== undefined); this.activeClient.setUnread(timestamp !== undefined);
/* TODO: Eliminate this cross reference? */
this.connection.side_bar.info_frame().update_chat_counter();
} }
protected canClientAccessChat(): boolean { protected canClientAccessChat(): boolean {
@ -311,6 +315,7 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
} }
export class PrivateConversationManager extends AbstractChatManager<PrivateConversationUIEvents> { export class PrivateConversationManager extends AbstractChatManager<PrivateConversationUIEvents> {
public readonly events: Registry<PrivateConversationManagerEvents>;
public readonly htmlTag: HTMLDivElement; public readonly htmlTag: HTMLDivElement;
public readonly connection: ConnectionHandler; public readonly connection: ConnectionHandler;
@ -322,6 +327,7 @@ export class PrivateConversationManager extends AbstractChatManager<PrivateConve
constructor(connection: ConnectionHandler) { constructor(connection: ConnectionHandler) {
super(); super();
this.connection = connection; this.connection = connection;
this.events = new Registry<PrivateConversationManagerEvents>();
this.htmlTag = document.createElement("div"); this.htmlTag = document.createElement("div");
this.htmlTag.style.display = "flex"; this.htmlTag.style.display = "flex";

View file

@ -10,6 +10,8 @@ export type ModalType = "error" | "warning" | "info" | "none";
export interface ModalOptions { export interface ModalOptions {
destroyOnClose?: boolean; destroyOnClose?: boolean;
defaultSize?: { width: number, height: number };
} }
export interface ModalEvents { export interface ModalEvents {

View file

@ -4,9 +4,12 @@ import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
import {EventHandler, Registry} from "tc-shared/events"; import {EventHandler, Registry} from "tc-shared/events";
import {VideoViewerEvents} from "./Definitions"; import {VideoViewerEvents} from "./Definitions";
import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {W2GPluginCmdHandler, W2GWatcher, W2GWatcherFollower} from "tc-shared/video-viewer/W2GPluginHandler"; import {W2GPluginCmdHandler, W2GWatcher, W2GWatcherFollower} from "tc-shared/video-viewer/W2GPlugin";
import {ModalController} from "tc-shared/ui/react-elements/Modal"; import {ModalController} from "tc-shared/ui/react-elements/Modal";
import {settings, Settings} from "tc-shared/settings"; import {settings, Settings} from "tc-shared/settings";
import {global_client_actions} from "tc-shared/events/GlobalEvents";
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
import {createErrorModal} from "tc-shared/ui/elements/Modal";
const parseWatcherId = (id: string): { clientId: number, clientUniqueId: string} => { const parseWatcherId = (id: string): { clientId: number, clientUniqueId: string} => {
const [ clientIdString, clientUniqueId ] = id.split(" - "); const [ clientIdString, clientUniqueId ] = id.split(" - ");
@ -26,7 +29,7 @@ class VideoViewer {
private unregisterCallbacks = []; private unregisterCallbacks = [];
private destroyCalled = false; private destroyCalled = false;
constructor(connection: ConnectionHandler, initialUrl: string) { constructor(connection: ConnectionHandler) {
this.connection = connection; this.connection = connection;
this.events = new Registry<VideoViewerEvents>(); this.events = new Registry<VideoViewerEvents>();
@ -37,8 +40,7 @@ class VideoViewer {
throw tr("Missing video viewer plugin"); throw tr("Missing video viewer plugin");
} }
this.modal = spawnExternalModal("video-viewer", this.events, { handlerId: connection.handlerId, url: initialUrl }); this.modal = spawnExternalModal("video-viewer", this.events, { handlerId: connection.handlerId });
this.setWatchingVideo(initialUrl);
this.registerPluginListeners(); this.registerPluginListeners();
this.plugin.getCurrentWatchers().forEach(watcher => this.registerWatcherEvents(watcher)); this.plugin.getCurrentWatchers().forEach(watcher => this.registerWatcherEvents(watcher));
@ -64,8 +66,16 @@ class VideoViewer {
if(this.currentVideoUrl === url) if(this.currentVideoUrl === url)
return; return;
this.events.fire_async("notify_following", { watcherId: undefined }); this.plugin.setLocalWatcherStatus(url, { status: "paused" });
this.events.fire_async("notify_video", { url: url }); this.events.fire_async("notify_video", { url: url }); /* notify the new url */
}
setFollowing(target: W2GWatcher) {
if(this.plugin.getLocalFollowingWatcher() === target)
return;
this.plugin.setLocalFollowing(target, { status: "paused" });
this.events.fire_async("notify_video", { url: target.getCurrentVideo() }); /* notify the new url */
} }
async open() { async open() {
@ -291,45 +301,57 @@ class VideoViewer {
settings.changeGlobal(Settings.KEY_W2G_SIDEBAR_COLLAPSED, !event.shown); settings.changeGlobal(Settings.KEY_W2G_SIDEBAR_COLLAPSED, !event.shown);
} }
@EventHandler<VideoViewerEvents>("notify_video") @EventHandler<VideoViewerEvents>("notify_video")
private handleVideo(event: VideoViewerEvents["notify_video"]) { private handleVideo(event: VideoViewerEvents["notify_video"]) {
if(this.currentVideoUrl === event.url) if(this.currentVideoUrl === event.url)
return; return;
this.currentVideoUrl = event.url; this.currentVideoUrl = event.url;
const following = this.plugin.getLocalFollowingWatcher();
if(following)
this.plugin.setLocalFollowing(following, { status: "paused" });
else
this.plugin.setLocalWatcherStatus(this.currentVideoUrl, { status: "paused" });
this.notifyWatcherList(); this.notifyWatcherList();
} }
} }
let currentVideoViewer: VideoViewer; let currentVideoViewer: VideoViewer;
export function openVideoViewer(connection: ConnectionHandler, url: string) { global_client_actions.on("action_w2g", event => {
if(currentVideoViewer?.connection === connection) { const connection = server_connections.findConnection(event.handlerId);
currentVideoViewer.setWatchingVideo(url); if(connection === undefined) return;
currentVideoViewer.open(); /* draw focus */
return; const plugin = connection.getPluginCmdRegistry().getPluginHandler<W2GPluginCmdHandler>(W2GPluginCmdHandler.kPluginChannel);
} else if(currentVideoViewer) {
let watcher: W2GWatcher;
if('following' in event) {
watcher = plugin.getCurrentWatchers().find(e => e.clientId === event.following);
if(!watcher) {
createErrorModal(tr("Target client isn't watching anything"), tr("The target client isn't watching anything.")).open();
return;
}
}
if(currentVideoViewer && currentVideoViewer.connection !== connection) {
currentVideoViewer.destroy(); currentVideoViewer.destroy();
currentVideoViewer = undefined; currentVideoViewer = undefined;
} }
currentVideoViewer = new VideoViewer(connection, url); if(!currentVideoViewer) {
currentVideoViewer.events.on("notify_destroy", () => { currentVideoViewer = new VideoViewer(connection);
currentVideoViewer = undefined; currentVideoViewer.events.on("notify_destroy", () => {
}); currentVideoViewer = undefined;
});
}
if('following' in event) {
currentVideoViewer.setFollowing(watcher);
} else {
currentVideoViewer.setWatchingVideo(event.videoUrl);
}
currentVideoViewer.open().catch(error => { currentVideoViewer.open().catch(error => {
logError(LogCategory.GENERAL, tr("Failed to open video viewer: %o"), error); logError(LogCategory.GENERAL, tr("Failed to open video viewer: %o"), error);
currentVideoViewer.destroy(); currentVideoViewer.destroy();
currentVideoViewer = undefined; currentVideoViewer = undefined;
}); });
} });
window.onbeforeunload = () => { window.onbeforeunload = () => {
currentVideoViewer?.destroy(); currentVideoViewer?.destroy();

View file

@ -127,7 +127,7 @@ const WatcherInfo = React.memo((props: { events: Registry<VideoViewerEvents>, wa
if(Math.abs(expectedPlaytime - currentPlaytime) > 2) { if(Math.abs(expectedPlaytime - currentPlaytime) > 2) {
setStatus(Object.assign({ timestamp: Date.now() }, event.status)); setStatus(Object.assign({ timestamp: Date.now() }, event.status));
} else { } else {
/* keep the last value, its still close enought */ /* keep the last value, its still close enough */
setStatus({ setStatus({
status: "playing", status: "playing",
timestamp: status.timestamp, timestamp: status.timestamp,
@ -305,6 +305,8 @@ const PlayerController = React.memo((props: { events: Registry<VideoViewerEvents
const [ masterPlayerState, setWatcherPlayerState ] = useState<"playing" | "buffering" | "paused" | "stopped">("stopped"); const [ masterPlayerState, setWatcherPlayerState ] = useState<"playing" | "buffering" | "paused" | "stopped">("stopped");
const watcherTimestamp = useRef<number>(); const watcherTimestamp = useRef<number>();
const videoEnded = useRef(false);
const [ forcePause, setForcePause ] = useState(false); const [ forcePause, setForcePause ] = useState(false);
props.events.reactUse("notify_following", event => setMode(event.watcherId === undefined ? "watcher" : "follower")); props.events.reactUse("notify_following", event => setMode(event.watcherId === undefined ? "watcher" : "follower"));
@ -375,10 +377,19 @@ const PlayerController = React.memo((props: { events: Registry<VideoViewerEvents
kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onEnded()")); kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onEnded()"));
playerState.current = "stopped"; playerState.current = "stopped";
props.events.fire("notify_local_status", { status: { status: "stopped" } }); props.events.fire("notify_local_status", { status: { status: "stopped" } });
videoEnded.current = true;
player.current.seekTo(0, "seconds");
}} }}
onPause={() => { onPause={() => {
kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onPause()")); kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onPause()"));
if(videoEnded.current) {
videoEnded.current = false;
return;
}
playerState.current = "paused"; playerState.current = "paused";
props.events.fire("notify_local_status", { status: { status: "paused" } }); props.events.fire("notify_local_status", { status: { status: "paused" } });
}} }}
@ -386,6 +397,11 @@ const PlayerController = React.memo((props: { events: Registry<VideoViewerEvents
onPlay={() => { onPlay={() => {
kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onPlay()")); kLogPlayerEvents && log.trace(LogCategory.GENERAL, tr("ReactPlayer::onPlay()"));
if(videoEnded.current) {
/* it's just the seek to the beginning */
return;
}
if(mode === "follower") { if(mode === "follower") {
if(masterPlayerState !== "playing") { if(masterPlayerState !== "playing") {
setForcePause(true); setForcePause(true);
@ -399,6 +415,7 @@ const PlayerController = React.memo((props: { events: Registry<VideoViewerEvents
log.debug(LogCategory.GENERAL, tr("Player started, at second %d. Watcher is at %s. So sync: %o"), currentSeconds, expectedSeconds, doSync); log.debug(LogCategory.GENERAL, tr("Player started, at second %d. Watcher is at %s. So sync: %o"), currentSeconds, expectedSeconds, doSync);
doSync && player.current.seekTo(expectedSeconds, "seconds"); doSync && player.current.seekTo(expectedSeconds, "seconds");
} }
playerState.current = "playing"; playerState.current = "playing";
props.events.fire("notify_local_status", { status: { status: "playing", timestampBuffer: currentTime.current.buffer, timestampPlay: currentTime.current.play } }); props.events.fire("notify_local_status", { status: { status: "playing", timestampBuffer: currentTime.current.buffer, timestampPlay: currentTime.current.play } });
}} }}
@ -436,6 +453,13 @@ const PlayerController = React.memo((props: { events: Registry<VideoViewerEvents
loop={false} loop={false}
light={false} light={false}
config={{
youtube: {
playerVars: {
rel: 0
}
}
}}
playing={mode === "watcher" ? undefined : masterPlayerState === "playing" || forcePause} playing={mode === "watcher" ? undefined : masterPlayerState === "playing" || forcePause}
/> />
); );

View file

@ -195,6 +195,8 @@ export class W2GPluginCmdHandler extends PluginCmdHandler {
static readonly kStatusUpdateTimeout = 10000; static readonly kStatusUpdateTimeout = 10000;
readonly events: Registry<W2GEvents>; readonly events: Registry<W2GEvents>;
private readonly callbackWatcherEvents;
private currentWatchers: InternalW2GWatcher[] = []; private currentWatchers: InternalW2GWatcher[] = [];
private localPlayerStatus: PlayerStatus; private localPlayerStatus: PlayerStatus;
@ -202,8 +204,6 @@ export class W2GPluginCmdHandler extends PluginCmdHandler {
private localFollowing: InternalW2GWatcher | undefined; private localFollowing: InternalW2GWatcher | undefined;
private localStatusUpdateTimer: number; private localStatusUpdateTimer: number;
private callbackWatcherEvents;
constructor() { constructor() {
super(W2GPluginCmdHandler.kPluginChannel); super(W2GPluginCmdHandler.kPluginChannel);
this.events = new Registry<W2GEvents>(); this.events = new Registry<W2GEvents>();

View file

@ -89,15 +89,15 @@ class ExternalModalController extends AbstractExternalModalController {
"loader-abort": __build.mode === "debug" ? 1 : 0, "loader-abort": __build.mode === "debug" ? 1 : 0,
}; };
const options = this.getOptions();
const features = { const features = {
status: "no", status: "no",
location: "no", location: "no",
toolbar: "no", toolbar: "no",
menubar: "no", menubar: "no",
/* resizable: "yes",
width: 600, width: options.defaultSize?.width,
height: 400 height: options.defaultSize?.height
*/
}; };
let baseUrl = location.origin + location.pathname + "?"; let baseUrl = location.origin + location.pathname + "?";