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 { msg, localized, str } from "@lit/localize";
import { property, state } from "lit/decorators.js";
type Message = {
@ -33,6 +34,7 @@ type CloseMessage = Message & {
* ></btrix-screencast>
* ```
*/
@localized()
export class Screencast extends LitElement {
static styles = css`
.wrapper {
@ -49,15 +51,30 @@ export class Screencast extends LitElement {
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 {
border: 1px solid var(--sl-color-neutral-100);
border: 1px solid var(--sl-panel-border-color);
border-radius: var(--sl-border-radius-medium);
cursor: pointer;
transition: opacity 0.1s border-color 0.1s;
overflow: hidden;
}
.screen:hover {
.screen[role="button"] {
cursor: pointer;
transition: opacity 0.1s border-color 0.1s;
}
.screen[role="button"]:hover {
opacity: 0.8;
border-color: var(--sl-color-neutral-300);
}
@ -66,19 +83,17 @@ export class Screencast extends LitElement {
margin: 0;
}
figcaption {
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);
.caption {
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 {
display: block;
font-size: var(--sl-font-size-small);
font-size: var(--sl-font-size-x-small);
line-height: 1;
/* Truncate: */
overflow: hidden;
@ -90,7 +105,12 @@ export class Screencast extends LitElement {
max-width: 40em;
}
img {
.frame {
background-color: var(--sl-color-neutral-50);
overflow: hidden;
}
.frame > img {
display: block;
width: 100%;
height: auto;
@ -109,11 +129,15 @@ export class Screencast extends LitElement {
@property({ type: String })
crawlId?: string;
@property({ type: Number })
scale: number = 1;
@property({ type: Array })
watchIPs: string[] = [];
// List of browser screens
@state()
private dataList: Array<ScreencastMessage> = [];
private dataList: Array<ScreencastMessage | null> = [];
@state()
private isConnecting: boolean = false;
@ -123,12 +147,15 @@ export class Screencast extends LitElement {
// Websocket connections
private wsMap: Map<string, WebSocket> = new Map();
// Page image data
private imageDataMap: Map<string, ScreencastMessage> = new Map();
private screenCount = 1;
// Map data order to screen data
private dataMap: { [index: number]: ScreencastMessage | null } = {};
// Map page ID to data order
private pageOrderMap: Map<string, number> = new Map();
// Number of available browsers.
// Multiply by scale to get available browser window count
private browsersCount = 0;
private screenWidth = 640;
private screenHeight = 480;
shouldUpdate(changedProperties: Map<string, any>) {
if (changedProperties.size === 1 && changedProperties.has("watchIPs")) {
@ -170,26 +197,56 @@ export class Screencast extends LitElement {
render() {
return html`
<div class="wrapper">
${this.isConnecting
${this.isConnecting || !this.dataList.length
? html`<div class="spinner">
<sl-spinner></sl-spinner>
</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
class="container"
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(
(pageData) => html` <figure
class="screen"
title="${pageData.url}"
role="button"
@click=${() => (this.focusedScreenData = pageData)}
>
<figcaption>${pageData.url}</figcaption>
<img src="data:image/png;base64,${pageData.data}" />
</figure>`
(pageData) =>
html` <figure
class="screen"
title=${pageData?.url || ""}
role=${pageData ? "button" : "presentation"}
@click=${pageData
? () => (this.focusedScreenData = pageData)
: () => {}}
>
<figcaption class="caption">
${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>`
)}
</div>
</div>
@ -272,8 +329,20 @@ export class Screencast extends LitElement {
message: InitMessage | ScreencastMessage | CloseMessage
) {
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.screenHeight = message.height;
} else {
const { id } = message;
@ -287,26 +356,39 @@ export class Screencast extends LitElement {
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) {
this.focusedScreenData = message;
}
this.dataMap[idx] = message;
this.updateDataList();
} else if (message.msg === "close") {
this.imageDataMap.delete(id);
this.updateDataList();
const idx = this.pageOrderMap.get(id);
if (idx !== undefined && idx !== null) {
this.dataMap[idx] = null;
this.updateDataList();
this.pageOrderMap.set(id, -1);
}
}
}
}
updateDataList() {
// keep same number of data entries (probably should only decrease if scale is reduced)
this.dataList = [
...this.imageDataMap.values(),
...this.dataList.slice(this.imageDataMap.size),
];
this.dataList = Object.values(this.dataMap);
}
unfocusScreen() {

View File

@ -8,6 +8,7 @@ import type { AuthState } from "../../utils/AuthService";
import LiteElement, { html } from "../../utils/LiteElement";
import { CopyButton } from "../../components/copy-button";
import type { Crawl } from "./types";
import { times } from "lodash";
type SectionName = "overview" | "watch" | "replay" | "files" | "logs";
@ -80,6 +81,16 @@ export class CrawlDetail extends LiteElement {
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 {
// Set initial active section based on URL #hash value
const hash = window.location.hash.slice(1);
@ -433,8 +444,8 @@ export class CrawlDetail extends LiteElement {
${isStarting
? html`<div class="rounded border p-3">
<p class="text-sm text-neutral-600">
${msg("Crawl is starting...")}
<p class="text-sm text-neutral-600 motion-safe:animate-pulse">
${msg("Crawl starting...")}
</p>
</div>`
: isRunning
@ -444,6 +455,7 @@ export class CrawlDetail extends LiteElement {
authToken=${authToken}
archiveId=${this.crawl.aid}
crawlId=${this.crawlId!}
scale=${this.crawl.scale}
.watchIPs=${this.crawl.watchIPs || []}
></btrix-screencast>
</div>
@ -852,6 +864,24 @@ export class CrawlDetail extends LiteElement {
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
* @param id ID of element to fullscreen