Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 49 additions & 25 deletions app/components-react/highlighter/ImportStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import { Services } from 'components-react/service-provider';
import { ListInput, TextInput } from 'components-react/shared/inputs';
import Form from 'components-react/shared/inputs/Form';
import * as remote from '@electron/remote';
import { SUPPORTED_FILE_TYPES } from 'services/highlighter/constants';
import {
HIGHLIGHTER_APP_NAME,
REPLAY_APP_NAME,
SUPPORTED_FILE_TYPES,
} from 'services/highlighter/constants';
import { EGame } from 'services/highlighter/models/ai-highlighter.models';
import {
IStreamInfoForAiHighlighter,
TInstalledHighlighterApp,
TOpenedFrom,
} from 'services/highlighter/models/highlighter.models';
import { $t } from 'services/i18n';
Expand Down Expand Up @@ -35,7 +40,7 @@ export function ImportStreamModal({
streamInfo?: IStreamInfoForAiHighlighter;
}) {
const { HighlighterService, UsageStatisticsService, IncrementalRolloutService } = Services;
const [replayInstalled, setReplayInstalled] = useState<boolean | null>(null);
const [installedApp, setInstalledApp] = useState<TInstalledHighlighterApp | null>(null);
const [showingInstallFlow, setShowingInstallFlow] = useState(false);
const [pendingImport, setPendingImport] = useState<{
game: EGame;
Expand All @@ -44,8 +49,8 @@ export function ImportStreamModal({
} | null>(null);

useEffect(() => {
HighlighterService.isStreamlabsReplayInstalled().then(installed => {
setReplayInstalled(installed);
HighlighterService.getInstalledHighlighterApp().then(app => {
setInstalledApp(app);
});
}, []);

Expand Down Expand Up @@ -147,15 +152,20 @@ export function ImportStreamModal({
return;
}

// If Replay isn't installed yet, defer the import: stash the details, kick off the
// install, and replay it via onInstallComplete. This keeps every entry point (Go-live
// and the Highlighter page) on the same flow — game + title first, install second,
// then Replay opens directly on the import screen with the game and video.
const isInstalled = await HighlighterService.isStreamlabsReplayInstalled();
if (!isInstalled) {
// With neither app installed, hand the import to the installer instead of deeplinking it:
// the video and game go into the install origin marker, and Replay picks them up on its
// first launch. This keeps every entry point (Go-live and the Highlighter page) on the
// same flow — game + title first, install second, then Replay opens directly on the import
// screen with the game and video. pendingImport is only kept so a retry writes the same
// marker and so onInstallComplete can close this modal.
//
// With either app installed the import is deeplinked, and the service picks the protocol —
// a Highlighter user is sent to Highlighter, where their data still lives.
const app = await HighlighterService.getInstalledHighlighterApp();
if (app === 'none') {
setPendingImport({ game, filePath: filePath[0], streamId: id });
setShowingInstallFlow(true);
HighlighterService.installStreamlabsReplay();
HighlighterService.actions.installStreamlabsReplay({ videoPath: filePath[0], game });
return;
}

Expand Down Expand Up @@ -183,23 +193,32 @@ export function ImportStreamModal({
return (
<MigrationNotice
variant="modal"
installOriginMetadata={
pendingImport
? { videoPath: pendingImport.filePath, game: pendingImport.game }
: undefined
Comment on lines +196 to +199
}
onCancel={() => {
setShowingInstallFlow(false);
closeModal(true);
}}
onInstallComplete={() => {
setReplayInstalled(true);
setInstalledApp('replay');
setShowingInstallFlow(false);

// If there's a pending import, execute it now
// Deliberately no import deeplink here. The install origin marker already carried the
// video and game, and Replay acts on it when the installer launches it, so sending the
// link as well would open the same import a second time. Only the tracking the
// deeplink would have recorded is kept.
if (pendingImport) {
HighlighterService.actions.openReplayImport(
pendingImport.filePath,
pendingImport.game,
UsageStatisticsService.recordAnalyticsEvent('AIHighlighter', {
type: 'ReplayImport',
openedFrom,
pendingImport.streamId,
inputValue,
);
streamId: pendingImport.streamId,
game: pendingImport.game,
// Separates marker hand-offs from deeplinked imports in the ReplayImport numbers
via: 'install-marker',
});
setPendingImport(null);
closeModal(false);
}
Expand All @@ -211,7 +230,7 @@ export function ImportStreamModal({
// Show the install UI only once the user has committed to importing (game + title
// selected, then startImport triggers the install). Applies to every entry point so
// the import form is always shown first, never the install flow.
if (migrationEnabled && replayInstalled === false && showingInstallFlow) {
if (migrationEnabled && installedApp === 'none' && showingInstallFlow) {
return (
<HypeWrapper gameConfig={gameConfig} isAnimating={false} artwork={artwork}>
{renderMigrationNotice()}
Expand Down Expand Up @@ -365,11 +384,16 @@ export function ImportStreamModal({
</Button>
</div>
<div className={styles.explainerTextWrapper}>
{replayInstalled ? (
<p className={styles.explainerText}> Continuing will open Streamlabs Highlighter</p>
) : (
<p className={styles.explainerText}> Continuing will install Streamlabs Highlighter</p>
)}
<p className={styles.explainerText}>
{' '}
{installedApp === 'replay' &&
$t('Continuing will open %{appName}', { appName: REPLAY_APP_NAME })}
{installedApp === 'highlighter' &&
$t('Continuing will open %{appName}', { appName: HIGHLIGHTER_APP_NAME })}
{installedApp !== 'replay' &&
installedApp !== 'highlighter' &&
$t('Continuing will install %{appName}', { appName: REPLAY_APP_NAME })}
</p>
</div>
</HypeWrapper>
);
Expand Down
7 changes: 5 additions & 2 deletions app/components-react/highlighter/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function SettingsView({
EAvailableFeatures.highlighterMigration,
);

const [isReplayInstalled, setIsReplayInstalled] = useState(false);
const [isReplayInstalled, setIsReplayInstalled] = useState<boolean | null>(null);

useEffect(() => {
if (!migrationEnabled) return;
Expand All @@ -56,7 +56,8 @@ export default function SettingsView({
outputDisplay: StreamingService.views.outputDisplay,
}));

const isInstalled = migrationEnabled ? isReplayInstalled : v.highlighterVersion !== '';
const installationCheckComplete = !migrationEnabled || isReplayInstalled !== null;
const isInstalled = migrationEnabled ? isReplayInstalled === true : v.highlighterVersion !== '';

const disableAIHighlighter =
(v.isVerticalRecording || v.isVerticalReplayBuffer) && v.outputDisplay === 'vertical';
Expand Down Expand Up @@ -197,6 +198,8 @@ export default function SettingsView({
toggleUseAiHighlighter();
}

if (!installationCheckComplete) return <div className={styles.settingsViewRoot} />;

return (
<div className={styles.settingsViewRoot}>
<div style={{ display: 'flex', padding: 20 }}>
Expand Down
12 changes: 12 additions & 0 deletions app/components-react/highlighter/migration/MigrationNotice.m.less
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@
// Feature Carousel
// =================================================================================================

.app-detection {
width: 100%;
opacity: 0;
pointer-events: none;
transition: opacity 160ms ease;
}

.app-detection-ready {
opacity: 1;
pointer-events: auto;
}
Comment on lines +171 to +181

.carousel-wrapper {
display: flex;
background-color: var(--dark-background);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import { Services } from 'components-react/service-provider';
import ModalInstallationFlow from './ModalInstallationFlow';
import PageInstallationFlow from './PageInstallationFlow';
import { EAvailableFeatures } from 'services/incremental-rollout';
import { IReplayInstallOriginMetadata } from 'services/highlighter/models/highlighter.models';

interface IMigrationNoticeProps {
variant?: 'page' | 'modal';
onShowAllClips?: () => void;
onCancel?: () => void;
onInstallComplete?: () => void;
/** Hand-off data for the install origin marker, used when the user retries a failed install. */
installOriginMetadata?: IReplayInstallOriginMetadata;
}

export default function MigrationNotice(props: IMigrationNoticeProps) {
Expand Down Expand Up @@ -37,7 +40,11 @@ export default function MigrationNotice(props: IMigrationNoticeProps) {

if (variant === 'modal') {
return (
<ModalInstallationFlow onCancel={handleCancel} onInstallComplete={props.onInstallComplete} />
<ModalInstallationFlow
onCancel={handleCancel}
onInstallComplete={props.onInstallComplete}
installOriginMetadata={props.installOriginMetadata}
/>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,35 @@ import React, { useEffect } from 'react';
import { Button } from 'antd';
import cx from 'classnames';
import styles from './MigrationNotice.m.less';
import { REPLAY_APP_NAME } from 'services/highlighter/constants';
import { HIGHLIGHTER_APP_NAME, REPLAY_APP_NAME } from 'services/highlighter/constants';
import { $t } from 'services/i18n';
import Translate from 'components-react/shared/Translate';
import SectionHeader from './SectionHeader';
import { useInstallState, getStatusText } from './useInstallState';
import { IReplayInstallOriginMetadata } from 'services/highlighter/models/highlighter.models';

interface IModalInstallationFlowProps {
onCancel: () => void;
onInstallComplete?: () => void;
/** Hand-off data for the install origin marker, so a retry writes the same marker as the first
* attempt. */
installOriginMetadata?: IReplayInstallOriginMetadata;
}

export default function ModalInstallationFlow(props: IModalInstallationFlowProps) {
const {
step,
progress,
installedApp,
isInstalled,
isInstalling,
isRecorderRunning,
handleOpenOrInstall,
handleRetry,
handleCancel,
} = useInstallState();
} = useInstallState(props.installOriginMetadata);

const appName = installedApp === 'highlighter' ? HIGHLIGHTER_APP_NAME : REPLAY_APP_NAME;

useEffect(() => {
if (step === 'done' && props.onInstallComplete) {
Expand Down Expand Up @@ -57,24 +64,23 @@ export default function ModalInstallationFlow(props: IModalInstallationFlowProps

return (
<div className={styles.migrationNoticeModal}>
<SectionHeader
title={$t('%{appName} is required', { appName: REPLAY_APP_NAME })}
onClose={props.onCancel}
/>
<SectionHeader title={$t('%{appName} is required', { appName })} onClose={props.onCancel} />
<p className={styles.subtitle}>
{$t('Install %{appName} to import and detect game highlights.', {
appName: REPLAY_APP_NAME,
})}
{installedApp === 'highlighter'
? $t('Open %{appName} to import and detect game highlights.', { appName })
: $t('Install %{appName} to import and detect game highlights.', { appName })}
</p>
<div className={styles.actions}>
<Button
size="large"
onClick={() => handleOpenOrInstall('modal')}
style={{ width: '100%' }}
>
{isInstalled
? $t('Open %{appName}', { appName: REPLAY_APP_NAME })
: $t('Install %{appName}', { appName: REPLAY_APP_NAME })}
{installedApp === 'replay' && $t('Open %{appName}', { appName: REPLAY_APP_NAME })}
{installedApp === 'highlighter' &&
$t('Open %{appName}', { appName: HIGHLIGHTER_APP_NAME })}
{(installedApp === 'none' || installedApp === null) &&
$t('Install %{appName}', { appName: REPLAY_APP_NAME })}
</Button>
</div>
</div>
Expand Down
Loading
Loading