index.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. /*-----------------------------------------------------------------------------
  2. | Copyright (c) Jupyter Development Team.
  3. | Distributed under the terms of the Modified BSD License.
  4. |----------------------------------------------------------------------------*/
  5. import {
  6. ILayoutRestorer,
  7. IRouter,
  8. JupyterFrontEnd,
  9. JupyterFrontEndPlugin
  10. } from '@jupyterlab/application';
  11. import {
  12. Dialog,
  13. ICommandPalette,
  14. ISplashScreen,
  15. IThemeManager,
  16. IWindowResolver,
  17. ThemeManager,
  18. WindowResolver
  19. } from '@jupyterlab/apputils';
  20. import {
  21. ISettingRegistry,
  22. IStateDB,
  23. SettingRegistry,
  24. StateDB,
  25. URLExt
  26. } from '@jupyterlab/coreutils';
  27. import { IMainMenu } from '@jupyterlab/mainmenu';
  28. import { CommandRegistry } from '@phosphor/commands';
  29. import { PromiseDelegate } from '@phosphor/coreutils';
  30. import { DisposableDelegate, IDisposable } from '@phosphor/disposable';
  31. import { Menu } from '@phosphor/widgets';
  32. import { Palette } from './palette';
  33. import { createRedirectForm } from './redirect';
  34. import '../style/index.css';
  35. /**
  36. * The interval in milliseconds that calls to save a workspace are debounced
  37. * to allow for multiple quickly executed state changes to result in a single
  38. * workspace save operation.
  39. */
  40. const WORKSPACE_SAVE_DEBOUNCE_INTERVAL = 750;
  41. /**
  42. * The interval in milliseconds before recover options appear during splash.
  43. */
  44. const SPLASH_RECOVER_TIMEOUT = 12000;
  45. /**
  46. * The command IDs used by the apputils plugin.
  47. */
  48. namespace CommandIDs {
  49. export const changeTheme = 'apputils:change-theme';
  50. export const loadState = 'apputils:load-statedb';
  51. export const recoverState = 'apputils:recover-statedb';
  52. export const reset = 'apputils:reset';
  53. export const resetOnLoad = 'apputils:reset-on-load';
  54. export const saveState = 'apputils:save-statedb';
  55. }
  56. /**
  57. * The default command palette extension.
  58. */
  59. const palette: JupyterFrontEndPlugin<ICommandPalette> = {
  60. activate: Palette.activate,
  61. id: '@jupyterlab/apputils-extension:palette',
  62. provides: ICommandPalette,
  63. autoStart: true
  64. };
  65. /**
  66. * The default command palette's restoration extension.
  67. *
  68. * #### Notes
  69. * The command palette's restoration logic is handled separately from the
  70. * command palette provider extension because the layout restorer dependency
  71. * causes the command palette to be unavailable to other extensions earlier
  72. * in the application load cycle.
  73. */
  74. const paletteRestorer: JupyterFrontEndPlugin<void> = {
  75. activate: Palette.restore,
  76. id: '@jupyterlab/apputils-extension:palette-restorer',
  77. requires: [ILayoutRestorer],
  78. autoStart: true
  79. };
  80. /**
  81. * The default setting registry provider.
  82. */
  83. const settings: JupyterFrontEndPlugin<ISettingRegistry> = {
  84. id: '@jupyterlab/apputils-extension:settings',
  85. activate: async (app: JupyterFrontEnd): Promise<ISettingRegistry> => {
  86. const connector = app.serviceManager.settings;
  87. const plugins = (await connector.list()).values;
  88. return new SettingRegistry({ connector, plugins });
  89. },
  90. autoStart: true,
  91. provides: ISettingRegistry
  92. };
  93. /**
  94. * The default theme manager provider.
  95. */
  96. const themes: JupyterFrontEndPlugin<IThemeManager> = {
  97. id: '@jupyterlab/apputils-extension:themes',
  98. requires: [ISettingRegistry, JupyterFrontEnd.IPaths],
  99. optional: [ISplashScreen],
  100. activate: (
  101. app: JupyterFrontEnd,
  102. settings: ISettingRegistry,
  103. paths: JupyterFrontEnd.IPaths,
  104. splash: ISplashScreen | null
  105. ): IThemeManager => {
  106. const host = app.shell;
  107. const commands = app.commands;
  108. const url = URLExt.join(paths.urls.base, paths.urls.themes);
  109. const key = themes.id;
  110. const manager = new ThemeManager({ key, host, settings, splash, url });
  111. // Keep a synchronously set reference to the current theme,
  112. // since the asynchronous setting of the theme in `changeTheme`
  113. // can lead to an incorrect toggle on the currently used theme.
  114. let currentTheme: string;
  115. // Set data attributes on the application shell for the current theme.
  116. manager.themeChanged.connect((sender, args) => {
  117. currentTheme = args.newValue;
  118. app.shell.dataset.themeLight = String(manager.isLight(currentTheme));
  119. app.shell.dataset.themeName = currentTheme;
  120. commands.notifyCommandChanged(CommandIDs.changeTheme);
  121. });
  122. commands.addCommand(CommandIDs.changeTheme, {
  123. label: args => {
  124. const theme = args['theme'] as string;
  125. return args['isPalette'] ? `Use ${theme} Theme` : theme;
  126. },
  127. isToggled: args => args['theme'] === currentTheme,
  128. execute: args => {
  129. const theme = args['theme'] as string;
  130. if (theme === manager.theme) {
  131. return;
  132. }
  133. manager.setTheme(theme);
  134. }
  135. });
  136. return manager;
  137. },
  138. autoStart: true,
  139. provides: IThemeManager
  140. };
  141. /**
  142. * The default theme manager's UI command palette and main menu functionality.
  143. *
  144. * #### Notes
  145. * This plugin loads separately from the theme manager plugin in order to
  146. * prevent blocking of the theme manager while it waits for the command palette
  147. * and main menu to become available.
  148. */
  149. const themesPaletteMenu: JupyterFrontEndPlugin<void> = {
  150. id: '@jupyterlab/apputils-extension:themes-palette-menu',
  151. requires: [IThemeManager],
  152. optional: [ICommandPalette, IMainMenu],
  153. activate: (
  154. app: JupyterFrontEnd,
  155. manager: IThemeManager,
  156. palette: ICommandPalette | null,
  157. mainMenu: IMainMenu | null
  158. ): void => {
  159. const commands = app.commands;
  160. // If we have a main menu, add the theme manager to the settings menu.
  161. if (mainMenu) {
  162. const themeMenu = new Menu({ commands });
  163. themeMenu.title.label = 'JupyterLab Theme';
  164. app.restored.then(() => {
  165. const command = CommandIDs.changeTheme;
  166. const isPalette = false;
  167. manager.themes.forEach(theme => {
  168. themeMenu.addItem({ command, args: { isPalette, theme } });
  169. });
  170. });
  171. mainMenu.settingsMenu.addGroup(
  172. [
  173. {
  174. type: 'submenu' as Menu.ItemType,
  175. submenu: themeMenu
  176. }
  177. ],
  178. 0
  179. );
  180. }
  181. // If we have a command palette, add theme switching options to it.
  182. if (palette) {
  183. app.restored.then(() => {
  184. const category = 'Settings';
  185. const command = CommandIDs.changeTheme;
  186. const isPalette = true;
  187. manager.themes.forEach(theme => {
  188. palette.addItem({ command, args: { isPalette, theme }, category });
  189. });
  190. });
  191. }
  192. },
  193. autoStart: true
  194. };
  195. /**
  196. * The default window name resolver provider.
  197. */
  198. const resolver: JupyterFrontEndPlugin<IWindowResolver> = {
  199. id: '@jupyterlab/apputils-extension:resolver',
  200. autoStart: true,
  201. provides: IWindowResolver,
  202. requires: [JupyterFrontEnd.IPaths, IRouter],
  203. activate: async (
  204. _: JupyterFrontEnd,
  205. paths: JupyterFrontEnd.IPaths,
  206. router: IRouter
  207. ) => {
  208. const { hash, path, search } = router.current;
  209. const query = URLExt.queryStringToObject(search || '');
  210. const solver = new WindowResolver();
  211. const match = path.match(new RegExp(`^${paths.urls.workspaces}([^?\/]+)`));
  212. const workspace = (match && decodeURIComponent(match[1])) || '';
  213. const candidate = Private.candidate(paths, workspace);
  214. try {
  215. await solver.resolve(candidate);
  216. } catch (error) {
  217. // Window resolution has failed so the URL must change. Return a promise
  218. // that never resolves to prevent the application from loading plugins
  219. // that rely on `IWindowResolver`.
  220. return new Promise<IWindowResolver>(() => {
  221. // If the user has requested `autoredirect` create a new workspace name.
  222. if ('autoredirect' in query) {
  223. const { base, workspaces } = paths.urls;
  224. const pool =
  225. 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  226. const random = pool[Math.floor(Math.random() * pool.length)];
  227. const path = URLExt.join(base, workspaces, `auto-${random}`);
  228. // Clone the originally requested workspace after redirecting.
  229. query['clone'] = workspace;
  230. // Change the URL and trigger a hard reload to re-route.
  231. const url = path + URLExt.objectToQueryString(query) + (hash || '');
  232. router.navigate(url, { hard: true, silent: true });
  233. return;
  234. }
  235. // Launch a dialog to ask the user for a new workspace name.
  236. console.warn('Window resolution failed:', error);
  237. Private.redirect(router, paths, workspace);
  238. });
  239. }
  240. // If the user has requested `autoredirect` remove the query parameter.
  241. if ('autoredirect' in query) {
  242. // Maintain the query string parameters but remove `autoredirect`.
  243. delete query['autoredirect'];
  244. // Silently scrub the URL.
  245. const url = path + URLExt.objectToQueryString(query) + (hash || '');
  246. router.navigate(url, { silent: true });
  247. }
  248. return solver;
  249. }
  250. };
  251. /**
  252. * The default splash screen provider.
  253. */
  254. const splash: JupyterFrontEndPlugin<ISplashScreen> = {
  255. id: '@jupyterlab/apputils-extension:splash',
  256. autoStart: true,
  257. provides: ISplashScreen,
  258. activate: app => {
  259. return {
  260. show: (light = true) => {
  261. const { commands, restored } = app;
  262. return Private.showSplash(restored, commands, CommandIDs.reset, light);
  263. }
  264. };
  265. }
  266. };
  267. /**
  268. * The default state database for storing application state.
  269. */
  270. const state: JupyterFrontEndPlugin<IStateDB> = {
  271. id: '@jupyterlab/apputils-extension:state',
  272. autoStart: true,
  273. provides: IStateDB,
  274. requires: [JupyterFrontEnd.IPaths, IRouter, IWindowResolver],
  275. optional: [ISplashScreen],
  276. activate: (
  277. app: JupyterFrontEnd,
  278. paths: JupyterFrontEnd.IPaths,
  279. router: IRouter,
  280. resolver: IWindowResolver,
  281. splash: ISplashScreen | null
  282. ) => {
  283. let debouncer: number;
  284. let resolved = false;
  285. const { commands, serviceManager } = app;
  286. const { workspaces } = serviceManager;
  287. const workspace = resolver.name;
  288. const transform = new PromiseDelegate<StateDB.DataTransform>();
  289. const db = new StateDB({
  290. namespace: app.namespace,
  291. transform: transform.promise,
  292. windowName: workspace
  293. });
  294. commands.addCommand(CommandIDs.recoverState, {
  295. execute: async ({ global }) => {
  296. const immediate = true;
  297. const silent = true;
  298. // Clear the state silently so that the state changed signal listener
  299. // will not be triggered as it causes a save state.
  300. await db.clear(silent);
  301. // If the user explictly chooses to recover state, all of local storage
  302. // should be cleared.
  303. if (global) {
  304. try {
  305. window.localStorage.clear();
  306. console.log('Cleared local storage');
  307. } catch (error) {
  308. console.warn('Clearing local storage failed.', error);
  309. // To give the user time to see the console warning before redirect,
  310. // do not set the `immediate` flag.
  311. return commands.execute(CommandIDs.saveState);
  312. }
  313. }
  314. return commands.execute(CommandIDs.saveState, { immediate });
  315. }
  316. });
  317. // Conflate all outstanding requests to the save state command that happen
  318. // within the `WORKSPACE_SAVE_DEBOUNCE_INTERVAL` into a single promise.
  319. let conflated: PromiseDelegate<void> | null = null;
  320. commands.addCommand(CommandIDs.saveState, {
  321. label: () => `Save Workspace (${workspace})`,
  322. execute: ({ immediate }) => {
  323. const timeout = immediate ? 0 : WORKSPACE_SAVE_DEBOUNCE_INTERVAL;
  324. const id = workspace;
  325. const metadata = { id };
  326. // Only instantiate a new conflated promise if one is not outstanding.
  327. if (!conflated) {
  328. conflated = new PromiseDelegate<void>();
  329. }
  330. if (debouncer) {
  331. window.clearTimeout(debouncer);
  332. }
  333. debouncer = window.setTimeout(async () => {
  334. // Prevent a race condition between the timeout and saving.
  335. if (!conflated) {
  336. return;
  337. }
  338. const data = await db.toJSON();
  339. try {
  340. await workspaces.save(id, { data, metadata });
  341. conflated.resolve(undefined);
  342. } catch (error) {
  343. conflated.reject(error);
  344. }
  345. conflated = null;
  346. }, timeout);
  347. return conflated.promise;
  348. }
  349. });
  350. const listener = (sender: any, change: StateDB.Change) => {
  351. commands.execute(CommandIDs.saveState);
  352. };
  353. commands.addCommand(CommandIDs.loadState, {
  354. execute: async (args: IRouter.ILocation) => {
  355. // Since the command can be executed an arbitrary number of times, make
  356. // sure it is safe to call multiple times.
  357. if (resolved) {
  358. return;
  359. }
  360. const { hash, path, search } = args;
  361. const { urls } = paths;
  362. const query = URLExt.queryStringToObject(search || '');
  363. const clone =
  364. typeof query['clone'] === 'string'
  365. ? query['clone'] === ''
  366. ? urls.defaultWorkspace
  367. : URLExt.join(urls.base, urls.workspaces, query['clone'])
  368. : null;
  369. const source = clone || workspace;
  370. try {
  371. const saved = await workspaces.fetch(source);
  372. // If this command is called after a reset, the state database
  373. // will already be resolved.
  374. if (!resolved) {
  375. resolved = true;
  376. transform.resolve({ type: 'overwrite', contents: saved.data });
  377. }
  378. } catch (error) {
  379. console.warn(`Fetching workspace (${workspace}) failed:`, error);
  380. // If the workspace does not exist, cancel the data transformation
  381. // and save a workspace with the current user state data.
  382. if (!resolved) {
  383. resolved = true;
  384. transform.resolve({ type: 'cancel', contents: null });
  385. }
  386. }
  387. // Any time the local state database changes, save the workspace.
  388. if (workspace) {
  389. db.changed.connect(
  390. listener,
  391. db
  392. );
  393. }
  394. const immediate = true;
  395. if (source === clone) {
  396. // Maintain the query string parameters but remove `clone`.
  397. delete query['clone'];
  398. const url = path + URLExt.objectToQueryString(query) + hash;
  399. const cloned = commands
  400. .execute(CommandIDs.saveState, { immediate })
  401. .then(() => router.stop);
  402. // After the state has been cloned, navigate to the URL.
  403. cloned.then(() => {
  404. router.navigate(url, { silent: true });
  405. });
  406. return cloned;
  407. }
  408. // After the state database has finished loading, save it.
  409. return commands.execute(CommandIDs.saveState, { immediate });
  410. }
  411. });
  412. commands.addCommand(CommandIDs.reset, {
  413. label: 'Reset Application State',
  414. execute: async () => {
  415. const global = true;
  416. try {
  417. await commands.execute(CommandIDs.recoverState, { global });
  418. } catch (error) {
  419. /* Ignore failures and redirect. */
  420. }
  421. router.reload();
  422. }
  423. });
  424. commands.addCommand(CommandIDs.resetOnLoad, {
  425. execute: (args: IRouter.ILocation) => {
  426. const { hash, path, search } = args;
  427. const query = URLExt.queryStringToObject(search || '');
  428. const reset = 'reset' in query;
  429. const clone = 'clone' in query;
  430. if (!reset) {
  431. return;
  432. }
  433. // If a splash provider exists, launch the splash screen.
  434. const loading = splash
  435. ? splash.show()
  436. : new DisposableDelegate(() => undefined);
  437. // If the state database has already been resolved, resetting is
  438. // impossible without reloading.
  439. if (resolved) {
  440. return router.reload();
  441. }
  442. // Empty the state database.
  443. resolved = true;
  444. transform.resolve({ type: 'clear', contents: null });
  445. // Maintain the query string parameters but remove `reset`.
  446. delete query['reset'];
  447. const silent = true;
  448. const hard = true;
  449. const url = path + URLExt.objectToQueryString(query) + hash;
  450. const cleared = commands
  451. .execute(CommandIDs.recoverState)
  452. .then(() => router.stop); // Stop routing before new route navigation.
  453. // After the state has been reset, navigate to the URL.
  454. if (clone) {
  455. cleared.then(() => {
  456. router.navigate(url, { silent, hard });
  457. });
  458. } else {
  459. cleared.then(() => {
  460. router.navigate(url, { silent });
  461. loading.dispose();
  462. });
  463. }
  464. return cleared;
  465. }
  466. });
  467. router.register({
  468. command: CommandIDs.loadState,
  469. pattern: /.?/,
  470. rank: 30 // High priority: 30:100.
  471. });
  472. router.register({
  473. command: CommandIDs.resetOnLoad,
  474. pattern: /(\?reset|\&reset)($|&)/,
  475. rank: 20 // High priority: 20:100.
  476. });
  477. // Clean up state database when the window unloads.
  478. window.addEventListener('beforeunload', () => {
  479. const silent = true;
  480. db.clear(silent).catch(() => {
  481. /* no-op */
  482. });
  483. });
  484. return db;
  485. }
  486. };
  487. /**
  488. * Export the plugins as default.
  489. */
  490. const plugins: JupyterFrontEndPlugin<any>[] = [
  491. palette,
  492. paletteRestorer,
  493. resolver,
  494. settings,
  495. state,
  496. splash,
  497. themes,
  498. themesPaletteMenu
  499. ];
  500. export default plugins;
  501. /**
  502. * The namespace for module private data.
  503. */
  504. namespace Private {
  505. /**
  506. * Generate a workspace name candidate.
  507. *
  508. * @param workspace - A potential workspace name parsed from the URL.
  509. *
  510. * @returns A workspace name candidate.
  511. */
  512. export function candidate(
  513. paths: JupyterFrontEnd.IPaths,
  514. workspace = ''
  515. ): string {
  516. return workspace
  517. ? URLExt.join(paths.urls.base, paths.urls.workspaces, workspace)
  518. : paths.urls.defaultWorkspace;
  519. }
  520. /**
  521. * Create a splash element.
  522. */
  523. function createSplash(): HTMLElement {
  524. const splash = document.createElement('div');
  525. const galaxy = document.createElement('div');
  526. const logo = document.createElement('div');
  527. splash.id = 'jupyterlab-splash';
  528. galaxy.id = 'galaxy';
  529. logo.id = 'main-logo';
  530. galaxy.appendChild(logo);
  531. ['1', '2', '3'].forEach(id => {
  532. const moon = document.createElement('div');
  533. const planet = document.createElement('div');
  534. moon.id = `moon${id}`;
  535. moon.className = 'moon orbit';
  536. planet.id = `planet${id}`;
  537. planet.className = 'planet';
  538. moon.appendChild(planet);
  539. galaxy.appendChild(moon);
  540. });
  541. splash.appendChild(galaxy);
  542. return splash;
  543. }
  544. /**
  545. * A debouncer for recovery attempts.
  546. */
  547. let debouncer = 0;
  548. /**
  549. * The recovery dialog.
  550. */
  551. let dialog: Dialog<any>;
  552. /**
  553. * Allows the user to clear state if splash screen takes too long.
  554. */
  555. function recover(fn: () => void): void {
  556. if (dialog) {
  557. return;
  558. }
  559. dialog = new Dialog({
  560. title: 'Loading...',
  561. body: `The loading screen is taking a long time.
  562. Would you like to clear the workspace or keep waiting?`,
  563. buttons: [
  564. Dialog.cancelButton({ label: 'Keep Waiting' }),
  565. Dialog.warnButton({ label: 'Clear Workspace' })
  566. ]
  567. });
  568. dialog
  569. .launch()
  570. .then(result => {
  571. if (result.button.accept) {
  572. return fn();
  573. }
  574. dialog.dispose();
  575. dialog = null;
  576. debouncer = window.setTimeout(() => {
  577. recover(fn);
  578. }, SPLASH_RECOVER_TIMEOUT);
  579. })
  580. .catch(() => {
  581. /* no-op */
  582. });
  583. }
  584. /**
  585. * Allows the user to clear state if splash screen takes too long.
  586. */
  587. export async function redirect(
  588. router: IRouter,
  589. paths: JupyterFrontEnd.IPaths,
  590. workspace: string,
  591. warn = false
  592. ): Promise<void> {
  593. const form = createRedirectForm(warn);
  594. const dialog = new Dialog({
  595. title: 'Please use a different workspace.',
  596. body: form,
  597. focusNodeSelector: 'input',
  598. buttons: [Dialog.okButton({ label: 'Switch Workspace' })]
  599. });
  600. const result = await dialog.launch();
  601. dialog.dispose();
  602. if (!result.value) {
  603. return redirect(router, paths, workspace, true);
  604. }
  605. // Navigate to a new workspace URL and abandon this session altogether.
  606. const page = paths.urls.page;
  607. const workspaces = paths.urls.workspaces;
  608. const prefix = (workspace ? workspaces : page).length + workspace.length;
  609. const rest = router.current.request.substring(prefix);
  610. const url = URLExt.join(workspaces, result.value, rest);
  611. router.navigate(url, { hard: true, silent: true });
  612. // This promise will never resolve because the application navigates
  613. // away to a new location. It only exists to satisfy the return type
  614. // of the `redirect` function.
  615. return new Promise<void>(() => undefined);
  616. }
  617. /**
  618. * The splash element.
  619. */
  620. const splash = createSplash();
  621. /**
  622. * The splash screen counter.
  623. */
  624. let splashCount = 0;
  625. /**
  626. * Show the splash element.
  627. *
  628. * @param ready - A promise that must be resolved before splash disappears.
  629. *
  630. * @param commands - The application's command registry.
  631. *
  632. * @param recovery - A command that recovers from a hanging splash.
  633. *
  634. * @param light - A flag indicating whether the theme is light or dark.
  635. */
  636. export function showSplash(
  637. ready: Promise<any>,
  638. commands: CommandRegistry,
  639. recovery: string,
  640. light: boolean
  641. ): IDisposable {
  642. splash.classList.remove('splash-fade');
  643. splash.classList.toggle('light', light);
  644. splash.classList.toggle('dark', !light);
  645. splashCount++;
  646. if (debouncer) {
  647. window.clearTimeout(debouncer);
  648. }
  649. debouncer = window.setTimeout(() => {
  650. if (commands.hasCommand(recovery)) {
  651. recover(() => {
  652. commands.execute(recovery);
  653. });
  654. }
  655. }, SPLASH_RECOVER_TIMEOUT);
  656. document.body.appendChild(splash);
  657. return new DisposableDelegate(() => {
  658. ready.then(() => {
  659. if (--splashCount === 0) {
  660. if (debouncer) {
  661. window.clearTimeout(debouncer);
  662. debouncer = 0;
  663. }
  664. if (dialog) {
  665. dialog.dispose();
  666. dialog = null;
  667. }
  668. splash.classList.add('splash-fade');
  669. window.setTimeout(() => {
  670. document.body.removeChild(splash);
  671. }, 500);
  672. }
  673. });
  674. });
  675. }
  676. }