Implement: Inactive activity indicator on progress bar (#1039)
* update utils.ts: add a tool function to detect inactive periods * update Controller.svelte: add a fixed div element as an indicator * update Controller.svelte: add one blank space at the end * update Controller.svelte: add a variable inactivePeriods and use util function to get inactive periods * update Controller.svelte: add width property for inactive activity indicators * update Controller.svelte: combine calculation value with indicator UI * update utils.ts: fix error https://github.com/HurricaHjz/rrweb_2120_ga_3/pull/5#discussion_r1008677230 and add comments update Controller.svelte: apply Zihan's suggestion https://github.com/HurricaHjz/rrweb_2120_ga_3/pull/5#discussion_r1008678403 * update Controller.svelte: make the color of indicator customizable update index.d.ts: add type definition for the color option Co-authored-by: u7149141 <fengyun5264@outlook.com> Co-authored-by: Jerry Zhang <u7305891@anu.edu.au> Co-authored-by: fengyun5264 <115444501+fengyun5264@users.noreply.github.com> Co-authored-by: Zihan Meng <u7354208@anu.edu.au> Co-authored-by: HurricaHjz <105645379+HurricaHjz@users.noreply.github.com> Co-authored-by: u6924169 <u6924169@anu.edu.au> Co-authored-by: Majia0712 <55265314+MengZihan712@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
createEventDispatcher,
|
createEventDispatcher,
|
||||||
afterUpdate,
|
afterUpdate,
|
||||||
} from 'svelte';
|
} from 'svelte';
|
||||||
import { formatTime } from './utils';
|
import { formatTime, getInactivePeriods } from './utils';
|
||||||
import Switch from './components/Switch.svelte';
|
import Switch from './components/Switch.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
export let speedOption: number[];
|
export let speedOption: number[];
|
||||||
export let speed = speedOption.length ? speedOption[0] : 1;
|
export let speed = speedOption.length ? speedOption[0] : 1;
|
||||||
export let tags: Record<string, string> = {};
|
export let tags: Record<string, string> = {};
|
||||||
|
export let inactiveColor: string;
|
||||||
|
|
||||||
let currentTime = 0;
|
let currentTime = 0;
|
||||||
$: {
|
$: {
|
||||||
@@ -59,6 +60,21 @@
|
|||||||
background: string;
|
background: string;
|
||||||
position: string;
|
position: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the tag position (percent) to be displayed on the progress bar.
|
||||||
|
* @param startTime - The start time of the session.
|
||||||
|
* @param endTime - The end time of the session.
|
||||||
|
* @param tagTime - The time of the tag.
|
||||||
|
* @returns The position of the tag. unit: percentage
|
||||||
|
*/
|
||||||
|
function position(startTime: number, endTime: number, tagTime: number) {
|
||||||
|
const sessionDuration = endTime - startTime;
|
||||||
|
const eventDuration = endTime - tagTime;
|
||||||
|
const eventPosition = 100 - (eventDuration / sessionDuration) * 100;
|
||||||
|
return eventPosition.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
let customEvents: CustomEvent[];
|
let customEvents: CustomEvent[];
|
||||||
$: customEvents = (() => {
|
$: customEvents = (() => {
|
||||||
const { context } = replayer.service.state;
|
const { context } = replayer.service.state;
|
||||||
@@ -67,15 +83,6 @@
|
|||||||
const end = context.events[totalEvents - 1].timestamp;
|
const end = context.events[totalEvents - 1].timestamp;
|
||||||
const customEvents: CustomEvent[] = [];
|
const customEvents: CustomEvent[] = [];
|
||||||
|
|
||||||
// calculate tag position.
|
|
||||||
const position = (startTime: number, endTime: number, tagTime: number) => {
|
|
||||||
const sessionDuration = endTime - startTime;
|
|
||||||
const eventDuration = endTime - tagTime;
|
|
||||||
const eventPosition = 100 - (eventDuration / sessionDuration) * 100;
|
|
||||||
|
|
||||||
return eventPosition.toFixed(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
// loop through all the events and find out custom event.
|
// loop through all the events and find out custom event.
|
||||||
context.events.forEach((event) => {
|
context.events.forEach((event) => {
|
||||||
/**
|
/**
|
||||||
@@ -95,6 +102,43 @@
|
|||||||
return customEvents;
|
return customEvents;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
let inactivePeriods: {
|
||||||
|
name: string;
|
||||||
|
background: string;
|
||||||
|
position: string;
|
||||||
|
width: string;
|
||||||
|
}[];
|
||||||
|
$: inactivePeriods = (() => {
|
||||||
|
try {
|
||||||
|
const { context } = replayer.service.state;
|
||||||
|
const totalEvents = context.events.length;
|
||||||
|
const start = context.events[0].timestamp;
|
||||||
|
const end = context.events[totalEvents - 1].timestamp;
|
||||||
|
const periods = getInactivePeriods(context.events);
|
||||||
|
// calculate the indicator width.
|
||||||
|
const getWidth = (
|
||||||
|
startTime: number,
|
||||||
|
endTime: number,
|
||||||
|
tagStart: number,
|
||||||
|
tagEnd: number,
|
||||||
|
) => {
|
||||||
|
const sessionDuration = endTime - startTime;
|
||||||
|
const eventDuration = tagEnd - tagStart;
|
||||||
|
const width = (eventDuration / sessionDuration) * 100;
|
||||||
|
return width.toFixed(2);
|
||||||
|
};
|
||||||
|
return periods.map((period) => ({
|
||||||
|
name: 'inactive period',
|
||||||
|
background: inactiveColor,
|
||||||
|
position: `${position(start, end, period[0])}%`,
|
||||||
|
width: `${getWidth(start, end, period[0], period[1])}%`,
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
// For safety concern, if there is any error, the main function won't be affected.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const loopTimer = () => {
|
const loopTimer = () => {
|
||||||
stopTimer();
|
stopTimer();
|
||||||
|
|
||||||
@@ -193,7 +237,6 @@
|
|||||||
replayer.play(timeOffset);
|
replayer.play(timeOffset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleProgressClick = (event: MouseEvent) => {
|
const handleProgressClick = (event: MouseEvent) => {
|
||||||
if (speedState === 'skipping') {
|
if (speedState === 'skipping') {
|
||||||
return;
|
return;
|
||||||
@@ -207,7 +250,7 @@
|
|||||||
percent = 1;
|
percent = 1;
|
||||||
}
|
}
|
||||||
const timeOffset = meta.totalTime * percent;
|
const timeOffset = meta.totalTime * percent;
|
||||||
finished = false
|
finished = false;
|
||||||
goto(timeOffset);
|
goto(timeOffset);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,8 +273,8 @@
|
|||||||
export const triggerUpdateMeta = () => {
|
export const triggerUpdateMeta = () => {
|
||||||
return Promise.resolve().then(() => {
|
return Promise.resolve().then(() => {
|
||||||
meta = replayer.getMetaData();
|
meta = replayer.getMetaData();
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
playerState = replayer.service.state.value;
|
playerState = replayer.service.state.value;
|
||||||
@@ -384,17 +427,27 @@
|
|||||||
class="rr-progress"
|
class="rr-progress"
|
||||||
class:disabled={speedState === 'skipping'}
|
class:disabled={speedState === 'skipping'}
|
||||||
bind:this={progress}
|
bind:this={progress}
|
||||||
on:click={handleProgressClick}>
|
on:click={handleProgressClick}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="rr-progress__step"
|
class="rr-progress__step"
|
||||||
bind:this={step}
|
bind:this={step}
|
||||||
style="width: {percentage}" />
|
style="width: {percentage}"
|
||||||
|
/>
|
||||||
|
{#each inactivePeriods as period}
|
||||||
|
<div
|
||||||
|
title={period.name}
|
||||||
|
style="width: {period.width};height: 4px;position: absolute;background: {period.background};left:
|
||||||
|
{period.position};"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
{#each customEvents as event}
|
{#each customEvents as event}
|
||||||
<div
|
<div
|
||||||
title={event.name}
|
title={event.name}
|
||||||
style="width: 10px;height: 5px;position: absolute;top:
|
style="width: 10px;height: 5px;position: absolute;top:
|
||||||
2px;transform: translate(-50%, -50%);background: {event.background};left:
|
2px;transform: translate(-50%, -50%);background: {event.background};left:
|
||||||
{event.position};" />
|
{event.position};"
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<div class="rr-progress__handler" style="left: {percentage}" />
|
<div class="rr-progress__handler" style="left: {percentage}" />
|
||||||
@@ -411,7 +464,8 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
width="16"
|
width="16"
|
||||||
height="16">
|
height="16"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M682.65984 128q53.00224 0 90.50112 37.49888t37.49888 90.50112l0
|
d="M682.65984 128q53.00224 0 90.50112 37.49888t37.49888 90.50112l0
|
||||||
512q0 53.00224-37.49888 90.50112t-90.50112
|
512q0 53.00224-37.49888 90.50112t-90.50112
|
||||||
@@ -426,7 +480,8 @@
|
|||||||
12.4928-30.16704l0-512q0-17.67424-12.4928-30.16704t-30.16704-12.4928zM682.65984
|
12.4928-30.16704l0-512q0-17.67424-12.4928-30.16704t-30.16704-12.4928zM682.65984
|
||||||
213.34016q-17.67424 0-30.16704 12.4928t-12.4928 30.16704l0 512q0
|
213.34016q-17.67424 0-30.16704 12.4928t-12.4928 30.16704l0 512q0
|
||||||
17.67424 12.4928 30.16704t30.16704 12.4928 30.16704-12.4928
|
17.67424 12.4928 30.16704t30.16704 12.4928 30.16704-12.4928
|
||||||
12.4928-30.16704l0-512q0-17.67424-12.4928-30.16704t-30.16704-12.4928z" />
|
12.4928-30.16704l0-512q0-17.67424-12.4928-30.16704t-30.16704-12.4928z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{:else}
|
{:else}
|
||||||
<svg
|
<svg
|
||||||
@@ -436,10 +491,12 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
width="16"
|
width="16"
|
||||||
height="16">
|
height="16"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M170.65984 896l0-768 640 384zM644.66944
|
d="M170.65984 896l0-768 640 384zM644.66944
|
||||||
512l-388.66944-233.32864 0 466.65728z" />
|
512l-388.66944-233.32864 0 466.65728z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
@@ -447,7 +504,8 @@
|
|||||||
<button
|
<button
|
||||||
class:active={s === speed && speedState !== 'skipping'}
|
class:active={s === speed && speedState !== 'skipping'}
|
||||||
on:click={() => setSpeed(s)}
|
on:click={() => setSpeed(s)}
|
||||||
disabled={speedState === 'skipping'}>
|
disabled={speedState === 'skipping'}
|
||||||
|
>
|
||||||
{s}x
|
{s}x
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -455,7 +513,8 @@
|
|||||||
id="skip"
|
id="skip"
|
||||||
bind:checked={skipInactive}
|
bind:checked={skipInactive}
|
||||||
disabled={speedState === 'skipping'}
|
disabled={speedState === 'skipping'}
|
||||||
label="skip inactive" />
|
label="skip inactive"
|
||||||
|
/>
|
||||||
<button on:click={() => dispatch('fullscreen')}>
|
<button on:click={() => dispatch('fullscreen')}>
|
||||||
<svg
|
<svg
|
||||||
class="icon"
|
class="icon"
|
||||||
@@ -464,10 +523,10 @@
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
width="16"
|
width="16"
|
||||||
height="16">
|
height="16"
|
||||||
|
>
|
||||||
<defs>
|
<defs>
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</defs>
|
</defs>
|
||||||
<path
|
<path
|
||||||
@@ -478,7 +537,8 @@
|
|||||||
48s-21.6 48-48 48l-224 0c-26.4 0-48-21.6-48-48l0-224c0-26.4 21.6-48
|
48s-21.6 48-48 48l-224 0c-26.4 0-48-21.6-48-48l0-224c0-26.4 21.6-48
|
||||||
48-48 26.4 0 48 21.6 48 48L164 792l253.6-253.6c18.4-18.4 48.8-18.4
|
48-48 26.4 0 48 21.6 48 48L164 792l253.6-253.6c18.4-18.4 48.8-18.4
|
||||||
68 0 18.4 18.4 18.4 48.8 0 68L231.2 860z"
|
68 0 18.4 18.4 18.4 48.8 0 68L231.2 860z"
|
||||||
p-id="1286" />
|
p-id="1286"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
export let speed = 1;
|
export let speed = 1;
|
||||||
export let showController = true;
|
export let showController = true;
|
||||||
export let tags: Record<string, string> = {};
|
export let tags: Record<string, string> = {};
|
||||||
|
// color of inactive periods indicator
|
||||||
|
export let inactiveColor = '#D4D4D4';
|
||||||
|
|
||||||
let replayer: Replayer;
|
let replayer: Replayer;
|
||||||
|
|
||||||
@@ -229,6 +231,7 @@
|
|||||||
{speedOption}
|
{speedOption}
|
||||||
{skipInactive}
|
{skipInactive}
|
||||||
{tags}
|
{tags}
|
||||||
|
{inactiveColor}
|
||||||
on:fullscreen={() => toggleFullscreen()}
|
on:fullscreen={() => toggleFullscreen()}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { EventType, IncrementalSource } from 'rrweb';
|
||||||
|
import type { eventWithTime } from 'rrweb/typings/types';
|
||||||
|
|
||||||
export function inlineCss(cssObj: Record<string, string>): string {
|
export function inlineCss(cssObj: Record<string, string>): string {
|
||||||
let style = '';
|
let style = '';
|
||||||
Object.keys(cssObj).forEach((key) => {
|
Object.keys(cssObj).forEach((key) => {
|
||||||
@@ -141,3 +144,40 @@ export function typeOf(
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||||
return map[toString.call(obj)];
|
return map[toString.call(obj)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forked from 'rrweb' replay/index.ts. The original function is not exported.
|
||||||
|
* Determine whether the event is a user interaction event
|
||||||
|
* @param event - event to be determined
|
||||||
|
* @returns true if the event is a user interaction event
|
||||||
|
*/
|
||||||
|
function isUserInteraction(event: eventWithTime): boolean {
|
||||||
|
if (event.type !== EventType.IncrementalSnapshot) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
event.data.source > IncrementalSource.Mutation &&
|
||||||
|
event.data.source <= IncrementalSource.Input
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forked from 'rrweb' replay/index.ts. A const threshold of inactive time.
|
||||||
|
const SKIP_TIME_THRESHOLD = 10 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get periods of time when no user interaction happened from a list of events.
|
||||||
|
* @param events - all events
|
||||||
|
* @returns periods of time consist with [start time, end time]
|
||||||
|
*/
|
||||||
|
export function getInactivePeriods(events: eventWithTime[]) {
|
||||||
|
const inactivePeriods: [number, number][] = [];
|
||||||
|
let lastActiveTime = events[0].timestamp;
|
||||||
|
for (const event of events) {
|
||||||
|
if (!isUserInteraction(event)) continue;
|
||||||
|
if (event.timestamp - lastActiveTime > SKIP_TIME_THRESHOLD) {
|
||||||
|
inactivePeriods.push([lastActiveTime, event.timestamp]);
|
||||||
|
}
|
||||||
|
lastActiveTime = event.timestamp;
|
||||||
|
}
|
||||||
|
return inactivePeriods;
|
||||||
|
}
|
||||||
|
|||||||
5
packages/rrweb-player/typings/index.d.ts
vendored
5
packages/rrweb-player/typings/index.d.ts
vendored
@@ -50,6 +50,11 @@ export type RRwebPlayerOptions = {
|
|||||||
* @defaultValue `{}`
|
* @defaultValue `{}`
|
||||||
*/
|
*/
|
||||||
tags?: Record<string, string>;
|
tags?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Customize the color of inactive periods indicator in the progress bar with a valid CSS color string.
|
||||||
|
* @defaultValue `#D4D4D4`
|
||||||
|
*/
|
||||||
|
inactiveColor?: string;
|
||||||
} & Partial<playerConfig>;
|
} & Partial<playerConfig>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user