index.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { app, BrowserWindow, net, protocol, shell } from 'electron'
  2. import path from 'path'
  3. import { pathToFileURL } from 'url'
  4. import { IpcChannels } from '@shared/ipc-channels'
  5. import { initSteam, isSteamRunning, startVotedItemsSync } from './services/steam.service'
  6. import { initLibrary, checkUnavailableWallpapers, backfillResolutions } from './services/library.service'
  7. import { startWatcher } from './services/watcher.service'
  8. import { registerAllHandlers } from './ipc'
  9. import { initDesktopIcons, cleanupDesktopIcons } from './services/desktop-icons.service'
  10. import { getTrayEnabled, getAutostartPlaylistId, getKillLweOnQuit } from './services/config.service'
  11. import { createTray } from './services/tray.service'
  12. import { initPlaylistPlayer, startPlaylist } from './services/playlist-player.service'
  13. import { killAllLweProcesses, getLweStatus } from './services/lwe.service'
  14. import { initDisplay } from './services/display.service'
  15. import { initSchedule } from './services/schedule.service'
  16. const startMinimized = process.argv.includes('--minimized')
  17. let isQuitting = false
  18. let hasKilledLweOnQuit = false
  19. const BACKGROUND_SYNC_INTERVAL_MS = 3 * 60 * 1000
  20. // Keeps "liked" state and library availability fresh without the user having to restart the
  21. // app or click "Check unavailable" - votes cast on Steam directly (client, community website)
  22. // and items taken down from the Workshop both eventually show up on their own.
  23. function startBackgroundSync(win: BrowserWindow): void {
  24. startVotedItemsSync((ids) => {
  25. if (!win.isDestroyed()) win.webContents.send(IpcChannels.EVENT_VOTED_IDS_CHANGED, ids)
  26. })
  27. const syncResolutions = async (): Promise<void> => {
  28. if (await backfillResolutions() && !win.isDestroyed()) {
  29. win.webContents.send(IpcChannels.EVENT_LIBRARY_CHANGED)
  30. }
  31. }
  32. win.webContents.once('did-finish-load', () => void syncResolutions())
  33. setInterval(async () => {
  34. if (win.isDestroyed()) return
  35. const running = isSteamRunning()
  36. win.webContents.send(IpcChannels.EVENT_STEAM_STATUS, running)
  37. if (!running) return
  38. try {
  39. const { changed } = await checkUnavailableWallpapers()
  40. if (changed && !win.isDestroyed()) win.webContents.send(IpcChannels.EVENT_LIBRARY_CHANGED)
  41. await syncResolutions()
  42. } catch {
  43. // transient Steam hiccup - try again next tick
  44. }
  45. }, BACKGROUND_SYNC_INTERVAL_MS)
  46. }
  47. function createWindow(): BrowserWindow {
  48. const win = new BrowserWindow({
  49. width: 1280,
  50. height: 800,
  51. minWidth: 900,
  52. minHeight: 600,
  53. backgroundColor: '#0f0f0f',
  54. titleBarStyle: 'hiddenInset',
  55. show: !(startMinimized && getTrayEnabled()),
  56. webPreferences: {
  57. preload: path.join(__dirname, '../preload/index.cjs'),
  58. contextIsolation: true,
  59. nodeIntegration: false,
  60. sandbox: false
  61. }
  62. })
  63. win.webContents.setWindowOpenHandler(({ url }) => {
  64. shell.openExternal(url)
  65. return { action: 'deny' }
  66. })
  67. // When the tray is enabled, closing the window hides it instead of quitting so the
  68. // tray icon remains the only way back in (and the app keeps running for playlists).
  69. win.on('close', (event) => {
  70. if (getTrayEnabled() && !isQuitting) {
  71. event.preventDefault()
  72. win.hide()
  73. }
  74. })
  75. if (process.env.NODE_ENV === 'development' && process.env['ELECTRON_RENDERER_URL']) {
  76. win.loadURL(process.env['ELECTRON_RENDERER_URL'])
  77. } else {
  78. win.loadFile(path.join(__dirname, '../renderer/index.html'))
  79. }
  80. return win
  81. }
  82. app.whenReady().then(() => {
  83. protocol.handle('wallpaper', async (request) => {
  84. const filePath = decodeURIComponent(request.url.replace('wallpaper://', ''))
  85. try {
  86. return await net.fetch(pathToFileURL(filePath).toString())
  87. } catch {
  88. // File not on disk yet (e.g. still downloading) - a rejected fetch here
  89. // would otherwise surface as an uncaught net::ERR_FILE_NOT_FOUND
  90. return new Response(null, { status: 404 })
  91. }
  92. })
  93. const steamOk = initSteam()
  94. if (!steamOk) {
  95. console.warn('[App] Steam unavailable, subscription features disabled')
  96. }
  97. initLibrary()
  98. const win = createWindow()
  99. registerAllHandlers(win)
  100. startWatcher(win)
  101. startBackgroundSync(win)
  102. initDesktopIcons()
  103. initDisplay(win)
  104. const resumed = initPlaylistPlayer(win)
  105. if (getTrayEnabled()) createTray(win)
  106. const autostartPlaylistId = getAutostartPlaylistId()
  107. if (!resumed && autostartPlaylistId && getLweStatus().installed) {
  108. try {
  109. startPlaylist(autostartPlaylistId)
  110. } catch (err) {
  111. console.error('[App] Failed to autostart playlist:', err)
  112. }
  113. }
  114. initSchedule(win)
  115. })
  116. app.on('before-quit', (event) => {
  117. isQuitting = true
  118. if (getKillLweOnQuit() && !hasKilledLweOnQuit) {
  119. event.preventDefault()
  120. hasKilledLweOnQuit = true
  121. killAllLweProcesses().finally(() => app.quit())
  122. }
  123. })
  124. app.on('window-all-closed', () => {
  125. cleanupDesktopIcons()
  126. if (process.platform !== 'darwin') app.quit()
  127. })
  128. app.on('activate', () => {
  129. if (BrowserWindow.getAllWindows().length === 0) {
  130. createWindow()
  131. } else {
  132. BrowserWindow.getAllWindows()[0].show()
  133. }
  134. })