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:
parent
418c07bf0d
commit
a8757e2e50
@ -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` `}
|
||||||
|
</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() {
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user