make openExternal use main api

and improve offline mode
pull/2905/head
Mikael Finstad 1 month ago
parent 260601db0a
commit 2606013b64
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -45,7 +45,7 @@ LosslessCut --settings-json '{customFfPath:"/path/to/folder/containing/ffmpeg_an
## Other options
- `--locales-path` Customise path to locales (useful for [translators](translation.md)).
- `--disable-networking` Turn off all network requests (see [#1418](https://github.com/mifi/lossless-cut/issues/1418)).
- `--disable-networking` Make app completely offline (see [#1418](https://github.com/mifi/lossless-cut/issues/1418)).
- `--http-api` Start the [HTTP server with an API](api.md) to control LosslessCut, optionally specifying a port (default `8080`).
- `--keyboard-action` Run a keyboard action (see below.)
- `--config-dir` Path to a directory where the `config.json` file will be stored and loaded from. Note: don't include `config.json` in the path (only the directory containing it).

@ -16,6 +16,7 @@ import logger from './logger.js';
import { parseFfmpegProgressLine } from './progress.js';
import { formatFfmpegNumber, getHwaccelArgs, parseFfprobeDuration } from '../common/util.js';
import { getFfmpegJpegQuality } from './ffmpegUtil.js';
import { throwIfDisabledNetworking } from './networking.js';
// cannot use process.kill: https://github.com/sindresorhus/execa/issues/1177
@ -735,6 +736,8 @@ export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIn
}
export async function downloadMediaUrl(url: string, outPath: string) {
throwIfDisabledNetworking();
// User agent taken from https://techblog.willshouse.com/2012/01/03/most-common-user-agents/
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
const args = [

@ -38,6 +38,7 @@ import type { ApiActionRequest } from '../common/types.js';
import * as ffmpeg from './ffmpeg.js';
import * as compatPlayer from './compatPlayer.js';
import { downloadMediaUrl } from './ffmpeg.js';
import { hasDisabledNetworking, setDisableNetworking } from './networking.js';
electronUnhandled({ showDialog: true, logger: (err) => logger.error('electron-unhandled', err) });
@ -68,7 +69,6 @@ let mainWindow: BrowserWindow | null;
let askBeforeClose = false;
let rendererReady = false;
let newVersion: string | undefined;
let disableNetworking: boolean;
const openFiles = (paths: string[]) => mainWindow!.webContents.send('openFiles', paths);
@ -218,9 +218,17 @@ function createWindow() {
mainWindow.on('move', debouncedSaveWindowState);
}
async function openExternal(url: string) {
if (hasDisabledNetworking()) {
logger.warn('openExternal blocked because networking is disabled', url);
return;
}
await shell.openExternal(url);
}
function updateMenu() {
assert(mainWindow);
menu({ app, mainWindow, newVersion, isStoreBuild });
menu({ app, mainWindow, newVersion, isStoreBuild, openExternal });
}
async function changeLanguage(language: string | null) {
@ -370,7 +378,7 @@ async function init() {
if (filesToOpen.length === 0) filesToOpen = argv._.map(String);
const { settingsJson } = argv;
({ disableNetworking } = argv);
setDisableNetworking(argv['disableNetworking']);
if (settingsJson != null) {
logger.info('initializing settings', settingsJson);
@ -403,7 +411,7 @@ async function init() {
const enableUpdateCheck = configStore.get('enableUpdateCheck');
if (!disableNetworking && enableUpdateCheck && !isStoreBuild) {
if (!hasDisabledNetworking() && enableUpdateCheck && !isStoreBuild) {
newVersion = await checkNewVersion();
// newVersion = '1.2.3';
if (newVersion) updateMenu();
@ -426,8 +434,6 @@ function quitApp() {
timers.setTimeout(1000).then(() => electron.app.quit());
}
const hasDisabledNetworking = () => !!disableNetworking;
const setProgressBar = (v: number) => mainWindow?.setProgressBar(v);
function sendOsNotification(options: NotificationConstructorOptions) {
@ -447,6 +453,7 @@ const remoteApi = {
sendOsNotification,
writeClipboardText: (text: string) => electron.clipboard.writeText(text),
readClipboardText: () => electron.clipboard.readText(),
openExternal,
};
export type RemoteApi = typeof remoteApi;

@ -14,8 +14,12 @@ const esc = (val: string) => val.replaceAll('&', '&&');
const { Menu } = electron;
export default ({ app, mainWindow, newVersion, isStoreBuild }: {
app: Electron.App, mainWindow: BrowserWindow, newVersion?: string | undefined, isStoreBuild: boolean,
export default ({ app, mainWindow, newVersion, isStoreBuild, openExternal }: {
app: Electron.App,
mainWindow: BrowserWindow,
newVersion?: string | undefined,
isStoreBuild: boolean,
openExternal: (url: string) => void,
}) => {
// todo TS mainWindow.webContents.send
const menu: (MenuItemConstructorOptions | MenuItem)[] = [
@ -444,15 +448,15 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: {
submenu: [
{
label: esc(t('How to use')),
click() { electron.shell.openExternal(usageUrl); },
click() { openExternal(usageUrl); },
},
{
label: esc(t('FAQ')),
click() { electron.shell.openExternal(faqUrl); },
click() { openExternal(faqUrl); },
},
{
label: esc(t('Troubleshooting')),
click() { electron.shell.openExternal(troubleshootingUrl); },
click() { openExternal(troubleshootingUrl); },
},
{
label: esc(t('Keyboard & mouse shortcuts')),
@ -462,7 +466,7 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: {
},
{
label: esc(t('Learn More')),
click() { electron.shell.openExternal(homepageUrl); },
click() { openExternal(homepageUrl); },
},
{ type: 'separator' },
{
@ -471,11 +475,11 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: {
},
{
label: esc(t('Feature request')),
click() { electron.shell.openExternal(featureRequestUrl); },
click() { openExternal(featureRequestUrl); },
},
...(!isStoreBuild ? [{
label: esc(`${t('Donate')} ❤️`),
click() { electron.shell.openExternal(thanksUrl); },
click() { openExternal(thanksUrl); },
}] : []),
{ type: 'separator' },
{
@ -489,7 +493,7 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: {
{ type: 'separator' },
{
label: esc(t('Licenses')),
click() { electron.shell.openExternal(licensesUrl); },
click() { openExternal(licensesUrl); },
},
...(process.platform !== 'darwin' ? [{ role: 'about' as const, label: esc(t('About LosslessCut')) }] : []),
],
@ -502,7 +506,7 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: {
submenu: [
{
label: esc(t('Download {{version}}', { version: newVersion })),
click() { electron.shell.openExternal(getReleaseUrl(newVersion)); },
click() { openExternal(getReleaseUrl(newVersion)); },
},
],
});

@ -0,0 +1,13 @@
let disableNetworking: boolean;
export const hasDisabledNetworking = () => !!disableNetworking;
export function throwIfDisabledNetworking() {
if (hasDisabledNetworking()) {
throw new Error('Networking is disabled');
}
}
export function setDisableNetworking(value: boolean) {
disableNetworking = value;
}

@ -12,8 +12,7 @@ import type { KeyBinding } from '../../common/types';
import { splitKeyboardKeys } from './util';
import { getModifier } from './hooks/useTimelineScroll';
import Kbd from './components/Kbd';
const { shell } = window.require('@electron/remote');
import mainApi from './mainApi';
function Keys({ keys }: { keys: string | undefined }) {
@ -89,7 +88,7 @@ function NoFileLoaded({ mifiLink, currentCutSeg, onClick, darkMode, keyBindingBy
<div style={{ position: 'relative', margin: '.3em', width: '24em', height: '8em' }}>
<iframe src={`${mifiLink.loadUrl}#dark=${darkMode ? 'true' : 'false'}`} title="iframe" style={{ background: 'rgba(0,0,0,0)', border: 'none', pointerEvents: 'none', width: '100%', height: '100%', position: 'absolute', colorScheme: 'initial' }} />
{/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
<div style={{ width: '100%', height: '100%', position: 'absolute', cursor: 'pointer' }} title={t('Open link in browser')} role="button" onClick={(e) => { e.stopPropagation(); if ('targetUrl' in mifiLink && typeof mifiLink.targetUrl === 'string') shell.openExternal(mifiLink.targetUrl); }} />
<div style={{ width: '100%', height: '100%', position: 'absolute', cursor: 'pointer' }} title={t('Open link in browser')} role="button" onClick={(e) => { e.stopPropagation(); if ('targetUrl' in mifiLink && typeof mifiLink.targetUrl === 'string') mainApi.openExternal(mifiLink.targetUrl); }} />
</div>
) : undefined}
</motion.div>

@ -34,8 +34,7 @@ import type { Frame } from '../ffmpeg';
import type { FindNearestKeyframeTime } from '../hooks/useKeyframes';
import { troubleshootingUrl } from '../../../common/constants';
import OutDirSelector from './OutDirSelector';
const { shell } = window.require('@electron/remote');
import mainApi from '../mainApi';
const noticeStyle: CSSProperties = { marginBottom: '.5em' };
@ -107,7 +106,7 @@ function renderGenericNotice(notice: GenericNotice) {
<Notice notice={notice} />
</td>
<td>
{url != null && <IoIosHelpCircle style={{ cursor: 'pointer', fontSize: '1.5em', flexShrink: 0, color: primaryTextColor }} title={i18n.t('Learn more')} role="button" tabIndex={0} onClick={() => shell.openExternal(url)} />}
{url != null && <IoIosHelpCircle style={{ cursor: 'pointer', fontSize: '1.5em', flexShrink: 0, color: primaryTextColor }} title={i18n.t('Learn more')} role="button" tabIndex={0} onClick={() => mainApi.openExternal(url)} />}
</td>
</tr>
);

@ -17,8 +17,7 @@ import Button from './Button';
import * as Dialog from './Dialog';
import { dangerColor, warningColor } from '../colors';
import { exportedFileNameTemplateHelpUrl } from '../../../common/constants';
const { shell } = window.require('@electron/remote');
import mainApi from '../mainApi';
const formatVariable = (variable: string) => `\${${variable}}`;
@ -231,7 +230,7 @@ function FileNameTemplateEditor(opts: {
<div style={{ fontSize: '.9em', color: 'var(--gray-11)', display: 'flex', gap: '.3em', flexWrap: 'wrap', alignItems: 'center', marginBottom: '.7em' }}>
{`${t('Variables')}:`}
<IoIosHelpCircle fontSize="1.3em" color="var(--gray-12)" role="button" cursor="pointer" onClick={() => shell.openExternal(exportedFileNameTemplateHelpUrl)} />
<IoIosHelpCircle fontSize="1.3em" color="var(--gray-12)" role="button" cursor="pointer" onClick={() => mainApi.openExternal(exportedFileNameTemplateHelpUrl)} />
{availableVariables.map((variable) => (
<span key={variable} role="button" style={{ cursor: 'copy', marginRight: '.2em', textDecoration: 'underline', textDecorationStyle: 'dashed', fontSize: '.9em' }} onClick={() => onVariableClick(variable)}>{variable}</span>
))}

@ -14,8 +14,6 @@ import Warning from './Warning';
import mainApi from '../mainApi';
const { shell } = window.require('@electron/remote');
const activeColor = 'var(--gray-12)';
const emptyObject = {};
@ -171,7 +169,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
{tagInfo?.[tag] && !editingThis && (
<Tooltip content={tagInfo[tag].description}>
<IconButton size="1" variant="ghost" color="gray" style={{ cursor: tagInfo[tag]?.url ? 'pointer' : undefined }} onClick={() => tagInfo[tag]?.url && shell.openExternal(tagInfo[tag].url)}><FaInfo /></IconButton>
<IconButton size="1" variant="ghost" color="gray" style={{ cursor: tagInfo[tag]?.url ? 'pointer' : undefined }} onClick={() => tagInfo[tag]?.url && mainApi.openExternal(tagInfo[tag].url)}><FaInfo /></IconButton>
</Tooltip>
)}

@ -12,13 +12,12 @@ import Button from './Button';
import styles from './WhatsNew.module.css';
import { compareReleasesUrl, getReleaseUrl } from '../../../common/constants';
import mainApi from '../mainApi';
// see also generateVersions.ts
const versions: { version: string, highlightsMd?: string | undefined }[] = versionsJson;
const { shell } = window.require('@electron/remote');
export default function WhatsNew() {
const { t } = useTranslation();
const { lastAppVersion, setLastAppVersion } = useUserSettings();
@ -53,8 +52,8 @@ export default function WhatsNew() {
<div key={version} className={styles['version']}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '1em' }}>
{!(isMasBuild && matchingVersions.length === 1) && <h2>v{version}</h2>}
<Button onClick={() => shell.openExternal(getReleaseUrl(version))}><FaFile style={{ verticalAlign: 'middle' }} /> {t('All release notes')}</Button>
<Button onClick={() => shell.openExternal(compareReleasesUrl(prevVersion, version))}><FaCode style={{ verticalAlign: 'middle' }} /> {t('All code changes')}</Button>
<Button onClick={() => mainApi.openExternal(getReleaseUrl(version))}><FaFile style={{ verticalAlign: 'middle' }} /> {t('All release notes')}</Button>
<Button onClick={() => mainApi.openExternal(compareReleasesUrl(prevVersion, version))}><FaCode style={{ verticalAlign: 'middle' }} /> {t('All code changes')}</Button>
</div>

@ -32,10 +32,10 @@ import { UserFacingError } from '../../errors';
import { editSegmentByExpressionHelpUrl, selectSegmentByExpressionHelpUrl } from '../../../common/constants';
import type { Segment as ScopeSegment } from '../../../common/userTypes';
import type { FfmpegHwAccel } from '../../../common/types';
import mainApi from '../mainApi';
const remote = window.require('@electron/remote');
const { ffmpeg: { blackDetect, silenceDetect } } = remote.require('./index.js');
const { shell } = remote;
type ParameterDialogParameters = Record<string, string>;
@ -272,7 +272,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
<Dialog.Description>{description}</Dialog.Description>
{docUrl && <p><Button onClick={() => shell.openExternal(docUrl)}><FaLink style={{ fontSize: '.8em' }} /> {t('Read more')}</Button></p>}
{docUrl && <p><Button onClick={() => mainApi.openExternal(docUrl)}><FaLink style={{ fontSize: '.8em' }} /> {t('Read more')}</Button></p>}
<form onSubmit={handleSubmit}>
{Object.entries(parametersIn).map(([key, parameter], i) => {
@ -803,7 +803,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
{ name: i18n.t('Markers'), code: 'segment.end == null' },
]}
title={i18n.t('Select segments by expression')}
description={<Trans>Enter a JavaScript expression which will be evaluated for each segment. Segments for which the expression evaluates to &quot;true&quot; will be selected. <button type="button" className="link-button" onClick={() => shell.openExternal(selectSegmentByExpressionHelpUrl)}>View available syntax.</button></Trans>}
description={<Trans>Enter a JavaScript expression which will be evaluated for each segment. Segments for which the expression evaluates to &quot;true&quot; will be selected. <button type="button" className="link-button" onClick={() => mainApi.openExternal(selectSegmentByExpressionHelpUrl)}>View available syntax.</button></Trans>}
variables={['segment.index', 'segment.label', 'segment.start', 'segment.end', 'segment.duration', 'segment.tags.*']}
/>
),
@ -873,7 +873,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
{ name: i18n.t('Convert markers to segments'), code: '{ ...(segment.end == null && { end: segment.start + 5 }) }' },
]}
title={i18n.t('Edit segments by expression')}
description={<Trans>Enter a JavaScript expression which will be evaluated for each selected segment. Returned properties will be edited. <button type="button" className="link-button" onClick={() => shell.openExternal(editSegmentByExpressionHelpUrl)}>View available syntax.</button></Trans>}
description={<Trans>Enter a JavaScript expression which will be evaluated for each selected segment. Returned properties will be edited. <button type="button" className="link-button" onClick={() => mainApi.openExternal(editSegmentByExpressionHelpUrl)}>View available syntax.</button></Trans>}
variables={['segment.index', 'segment.label', 'segment.start', 'segment.end', 'segment.tags.*']}
/>
),

@ -13,8 +13,7 @@ import Action from '../components/Action';
import ExpressionDialog from '../components/ExpressionDialog';
import type { ShowGenericDialog } from '../components/GenericDialog';
import { changeEnabledStreamsExpressionHelpUrl } from '../../../common/constants';
const { shell } = window.require('@electron/remote');
import mainApi from '../mainApi';
export default function useStreamsMeta({ mainStreams, externalFilesMeta, filePath, autoExportExtraStreams, showGenericDialog }: {
@ -131,7 +130,7 @@ export default function useStreamsMeta({ mainStreams, externalFilesMeta, filePat
{ name: i18n.t('1st, 2nd and 3rd track'), code: 'track.index >= 0 && track.index <= 2' },
]}
title={i18n.t('Toggle tracks by expression')}
description={<><Trans>Enter a JavaScript filter expression which will be evaluated for each track of the current file. Tracks for which the expression evaluates to &quot;true&quot; will be selected or deselected. You may also the <Action name="toggleStripCurrentFilter" /> keyboard action to run this filter.</Trans> <button type="button" className="link-button" onClick={() => shell.openExternal(changeEnabledStreamsExpressionHelpUrl)}>View available syntax.</button></>}
description={<><Trans>Enter a JavaScript filter expression which will be evaluated for each track of the current file. Tracks for which the expression evaluates to &quot;true&quot; will be selected or deselected. You may also the <Action name="toggleStripCurrentFilter" /> keyboard action to run this filter.</Trans> <button type="button" className="link-button" onClick={() => mainApi.openExternal(changeEnabledStreamsExpressionHelpUrl)}>View available syntax.</button></>}
inputValue={enabledStreamsFilter ?? ''}
/>
),

@ -5,9 +5,9 @@ import CopyClipboardButton from './components/CopyClipboardButton';
import { isStoreBuild, isMasBuild, isWindowsStoreBuild, isExecaError, appVersion, appPath } from './util';
import getSwal from './swal';
import { discussionsUrl, githubUrl, publicBugReportUrl, supportEmail } from '../../common/constants';
import mainApi from './mainApi';
const remote = window.require('@electron/remote');
const { shell } = remote;
const { platform, arch } = remote.require('./index.js');
@ -20,14 +20,14 @@ export function openSendReportDialog({ err, message, state }: {
}) {
const reportInstructions = isStoreBuild
? (
<p><Trans>Please send an email to <span className="link-button" role="button" onClick={() => shell.openExternal(`mailto:${supportEmail}`)}>{supportEmail}</span> where you describe what you were doing.</Trans></p>
<p><Trans>Please send an email to <span className="link-button" role="button" onClick={() => mainApi.openExternal(`mailto:${supportEmail}`)}>{supportEmail}</span> where you describe what you were doing.</Trans></p>
) : (
<Trans>
<p>
If you&apos;re having a problem or question about LosslessCut, please first check the links in the <b>Help</b> menu. If you cannot find any resolution, you may ask a question in <span className="link-button" role="button" onClick={() => shell.openExternal(discussionsUrl)}>GitHub discussions</span> or on <span className="link-button" role="button" onClick={() => shell.openExternal(githubUrl)}>Discord.</span>
If you&apos;re having a problem or question about LosslessCut, please first check the links in the <b>Help</b> menu. If you cannot find any resolution, you may ask a question in <span className="link-button" role="button" onClick={() => mainApi.openExternal(discussionsUrl)}>GitHub discussions</span> or on <span className="link-button" role="button" onClick={() => mainApi.openExternal(githubUrl)}>Discord.</span>
</p>
<p>
If you believe that you found a bug in LosslessCut, you may <span className="link-button" role="button" onClick={() => shell.openExternal(publicBugReportUrl)}>report a bug</span>.
If you believe that you found a bug in LosslessCut, you may <span className="link-button" role="button" onClick={() => mainApi.openExternal(publicBugReportUrl)}>report a bug</span>.
</p>
</Trans>
);

Loading…
Cancel
Save