Screencast UX enhancements (#251)

* animate starting state
* consistent fixed-size slots for each browser (url + screencast)
* add tooltip for expected number of browsers (workers x scale)
This commit is contained in:
sua yoo 2022-06-15 18:50:14 -07:00 committed by GitHub
parent 418c07bf0d
commit a8757e2e50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 154 additions and 42 deletions

View File

@ -1,4 +1,5 @@
import { LitElement, html, css } from "lit"; import { LitElement, html, css } from "lit";
import { msg, localized, str } from "@lit/localize";
import { property, state } from "lit/decorators.js"; import { property, state } from "lit/decorators.js";
type Message = { type Message = {
@ -33,6 +34,7 @@ type CloseMessage = Message & {
* ></btrix-screencast> * ></btrix-screencast>
* ``` * ```
*/ */
@localized()
export class Screencast extends LitElement { export class Screencast extends LitElement {
static styles = css` static styles = css`
.wrapper { .wrapper {
@ -49,15 +51,30 @@ export class Screencast extends LitElement {
gap: 0.5rem; gap: 0.5rem;
} }
.screen-count {
color: var(--sl-color-neutral-400);
font-size: var(--sl-font-size-small);
margin-bottom: var(--sl-spacing-x-small);
}
.screen-count span,
.screen-count sl-icon {
display: inline-block;
vertical-align: middle;
}
.screen { .screen {
border: 1px solid var(--sl-color-neutral-100); border: 1px solid var(--sl-panel-border-color);
border-radius: var(--sl-border-radius-medium); border-radius: var(--sl-border-radius-medium);
cursor: pointer;
transition: opacity 0.1s border-color 0.1s;
overflow: hidden; overflow: hidden;
} }
.screen:hover { .screen[role="button"] {
cursor: pointer;
transition: opacity 0.1s border-color 0.1s;
}
.screen[role="button"]:hover {
opacity: 0.8; opacity: 0.8;
border-color: var(--sl-color-neutral-300); border-color: var(--sl-color-neutral-300);
} }
@ -66,19 +83,17 @@ export class Screencast extends LitElement {
margin: 0; margin: 0;
} }
figcaption { .caption {
flex: 1;
border-bottom-width: 1px;
border-bottom-color: var(--sl-panel-border-color);
color: var(--sl-color-neutral-600);
font-size: var(--sl-font-size-small);
padding: var(--sl-spacing-x-small); padding: var(--sl-spacing-x-small);
flex: 1;
border-bottom: 1px solid var(--sl-panel-border-color);
color: var(--sl-color-neutral-600);
} }
figcaption, .caption,
.dialog-label { .dialog-label {
display: block; display: block;
font-size: var(--sl-font-size-small); font-size: var(--sl-font-size-x-small);
line-height: 1; line-height: 1;
/* Truncate: */ /* Truncate: */
overflow: hidden; overflow: hidden;
@ -90,7 +105,12 @@ export class Screencast extends LitElement {
max-width: 40em; max-width: 40em;
} }
img { .frame {
background-color: var(--sl-color-neutral-50);
overflow: hidden;
}
.frame > img {
display: block; display: block;
width: 100%; width: 100%;
height: auto; height: auto;
@ -109,11 +129,15 @@ export class Screencast extends LitElement {
@property({ type: String }) @property({ type: String })
crawlId?: string; crawlId?: string;
@property({ type: Number })
scale: number = 1;
@property({ type: Array }) @property({ type: Array })
watchIPs: string[] = []; watchIPs: string[] = [];
// List of browser screens
@state() @state()
private dataList: Array<ScreencastMessage> = []; private dataList: Array<ScreencastMessage | null> = [];
@state() @state()
private isConnecting: boolean = false; private isConnecting: boolean = false;
@ -123,12 +147,15 @@ export class Screencast extends LitElement {
// Websocket connections // Websocket connections
private wsMap: Map<string, WebSocket> = new Map(); private wsMap: Map<string, WebSocket> = new Map();
// Map data order to screen data
// Page image data private dataMap: { [index: number]: ScreencastMessage | null } = {};
private imageDataMap: Map<string, ScreencastMessage> = new Map(); // Map page ID to data order
private pageOrderMap: Map<string, number> = new Map();
private screenCount = 1; // Number of available browsers.
// Multiply by scale to get available browser window count
private browsersCount = 0;
private screenWidth = 640; private screenWidth = 640;
private screenHeight = 480;
shouldUpdate(changedProperties: Map<string, any>) { shouldUpdate(changedProperties: Map<string, any>) {
if (changedProperties.size === 1 && changedProperties.has("watchIPs")) { if (changedProperties.size === 1 && changedProperties.has("watchIPs")) {
@ -170,25 +197,55 @@ export class Screencast extends LitElement {
render() { render() {
return html` return html`
<div class="wrapper"> <div class="wrapper">
${this.isConnecting ${this.isConnecting || !this.dataList.length
? html`<div class="spinner"> ? html`<div class="spinner">
<sl-spinner></sl-spinner> <sl-spinner></sl-spinner>
</div> ` </div> `
: ""} : html`
<div class="screen-count">
<span
>${msg(
str`Running in ${
this.browsersCount * this.scale
} browser windows`
)}</span
>
<sl-tooltip
content=${msg(
str`${this.browsersCount} browsers × ${this.scale} crawlers. Number of crawlers corresponds to scale.`
)}
><sl-icon name="info-circle"></sl-icon
></sl-tooltip>
</div>
`}
<div <div
class="container" class="container"
style="grid-template-columns: repeat(${this style="grid-template-columns: repeat(${this
.screenCount}, minmax(0, 1fr))" .browsersCount}, minmax(0, 1fr)); grid-template-rows: repeat(${this
.scale}, minmax(2rem, auto))"
> >
${this.dataList.map( ${this.dataList.map(
(pageData) => html` <figure (pageData) =>
html` <figure
class="screen" class="screen"
title="${pageData.url}" title=${pageData?.url || ""}
role="button" role=${pageData ? "button" : "presentation"}
@click=${() => (this.focusedScreenData = pageData)} @click=${pageData
? () => (this.focusedScreenData = pageData)
: () => {}}
> >
<figcaption>${pageData.url}</figcaption> <figcaption class="caption">
<img src="data:image/png;base64,${pageData.data}" /> ${pageData?.url || html`&nbsp;`}
</figcaption>
<div
class="frame"
style="aspect-ratio: ${this.screenWidth / this.screenHeight}"
>
${pageData
? html`<img src="data:image/png;base64,${pageData.data}" />`
: ""}
</div>
</figure>` </figure>`
)} )}
</div> </div>
@ -272,8 +329,20 @@ export class Screencast extends LitElement {
message: InitMessage | ScreencastMessage | CloseMessage message: InitMessage | ScreencastMessage | CloseMessage
) { ) {
if (message.msg === "init") { if (message.msg === "init") {
this.screenCount = message.browsers; this.dataList = Array.from(
{ length: message.browsers * this.scale },
() => null
);
this.dataMap = this.dataList.reduce(
(acc, val, i) => ({
...acc,
[i]: val,
}),
{}
);
this.browsersCount = message.browsers;
this.screenWidth = message.width; this.screenWidth = message.width;
this.screenHeight = message.height;
} else { } else {
const { id } = message; const { id } = message;
@ -287,26 +356,39 @@ export class Screencast extends LitElement {
this.isConnecting = false; this.isConnecting = false;
} }
this.imageDataMap.set(id, message); let idx = this.pageOrderMap.get(id);
if (idx === undefined) {
// Find and fill first empty slot
idx = this.dataList.indexOf(null);
if (idx === -1) {
console.debug("no empty slots");
}
this.pageOrderMap.set(id, idx);
}
if (this.focusedScreenData?.id === id) { if (this.focusedScreenData?.id === id) {
this.focusedScreenData = message; this.focusedScreenData = message;
} }
this.dataMap[idx] = message;
this.updateDataList(); this.updateDataList();
} else if (message.msg === "close") { } else if (message.msg === "close") {
this.imageDataMap.delete(id); const idx = this.pageOrderMap.get(id);
if (idx !== undefined && idx !== null) {
this.dataMap[idx] = null;
this.updateDataList(); this.updateDataList();
this.pageOrderMap.set(id, -1);
}
} }
} }
} }
updateDataList() { updateDataList() {
// keep same number of data entries (probably should only decrease if scale is reduced) this.dataList = Object.values(this.dataMap);
this.dataList = [
...this.imageDataMap.values(),
...this.dataList.slice(this.imageDataMap.size),
];
} }
unfocusScreen() { unfocusScreen() {

View File

@ -8,6 +8,7 @@ import type { AuthState } from "../../utils/AuthService";
import LiteElement, { html } from "../../utils/LiteElement"; import LiteElement, { html } from "../../utils/LiteElement";
import { CopyButton } from "../../components/copy-button"; import { CopyButton } from "../../components/copy-button";
import type { Crawl } from "./types"; import type { Crawl } from "./types";
import { times } from "lodash";
type SectionName = "overview" | "watch" | "replay" | "files" | "logs"; type SectionName = "overview" | "watch" | "replay" | "files" | "logs";
@ -80,6 +81,16 @@ export class CrawlDetail extends LiteElement {
this.fetchCrawl(); this.fetchCrawl();
} }
updated(changedProperties: Map<string, any>) {
const prevCrawl = changedProperties.get("crawl");
if (prevCrawl && this.crawl) {
if (prevCrawl.state === "running" && this.crawl.state !== "running") {
this.crawlDone();
}
}
}
connectedCallback(): void { connectedCallback(): void {
// Set initial active section based on URL #hash value // Set initial active section based on URL #hash value
const hash = window.location.hash.slice(1); const hash = window.location.hash.slice(1);
@ -433,8 +444,8 @@ export class CrawlDetail extends LiteElement {
${isStarting ${isStarting
? html`<div class="rounded border p-3"> ? html`<div class="rounded border p-3">
<p class="text-sm text-neutral-600"> <p class="text-sm text-neutral-600 motion-safe:animate-pulse">
${msg("Crawl is starting...")} ${msg("Crawl starting...")}
</p> </p>
</div>` </div>`
: isRunning : isRunning
@ -444,6 +455,7 @@ export class CrawlDetail extends LiteElement {
authToken=${authToken} authToken=${authToken}
archiveId=${this.crawl.aid} archiveId=${this.crawl.aid}
crawlId=${this.crawlId!} crawlId=${this.crawlId!}
scale=${this.crawl.scale}
.watchIPs=${this.crawl.watchIPs || []} .watchIPs=${this.crawl.watchIPs || []}
></btrix-screencast> ></btrix-screencast>
</div> </div>
@ -852,6 +864,24 @@ export class CrawlDetail extends LiteElement {
window.clearTimeout(this.timerId); window.clearTimeout(this.timerId);
} }
/** Callback when crawl is no longer running */
private crawlDone() {
if (!this.crawl) return;
this.notify({
message: msg(
html`Done crawling <strong>${this.crawl.configName}</strong>.`
),
type: "success",
icon: "check2-circle",
});
if (this.sectionName === "watch") {
// Show replay tab
this.sectionName = "replay";
}
}
/** /**
* Enter fullscreen mode * Enter fullscreen mode
* @param id ID of element to fullscreen * @param id ID of element to fullscreen