config.service.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import Store from 'electron-store'
  2. import * as fs from 'fs'
  3. import * as path from 'path'
  4. import { app } from 'electron'
  5. import { WE_APP_ID } from '@shared/constants'
  6. import type { DisplayMode, EngineFlagPreset, EngineFlags, SteamIdentity } from '@shared/types'
  7. interface AppConfig {
  8. workshopPath: string | null
  9. defaultFps: number | null
  10. lweRepoUrl: string | null
  11. lweRepoBranch: string | null
  12. lweCmakeArgs: string | null
  13. backupPath: string | null
  14. autoUnsubscribeAfterBackup: boolean
  15. trayEnabled: boolean
  16. autostartEnabled: boolean
  17. autostartMinimized: boolean
  18. autostartPlaylistId: string | null
  19. killLweOnQuit: boolean
  20. recommendedFpsEnabled: boolean
  21. recommendedWebFpsEnabled: boolean
  22. audioScreen: string | null
  23. ambientVolume: number | null
  24. /** Default --audio-sensitivity multiplier for audio-reactive objects with no per-wallpaper override */
  25. defaultAudioSensitivity: number
  26. /** Passes --render-debug no-puppet-animation to LWE - freezes puppet (.mdl) meshes at their bind pose, for troubleshooting */
  27. disablePuppetAnimation: boolean
  28. /** Passes --disable-animations to LWE - freezes all scene time (scripts, particles, effects and puppet meshes) */
  29. disableAnimations: boolean
  30. steamIdentity: SteamIdentity
  31. /** steamId64 of creators whose workshop items are hidden from browsing by default */
  32. ignoredCreators: string[]
  33. /** Launch-only engine flags for every wallpaper; disableAnimations lives in its own key above */
  34. engineFlags: EngineFlags
  35. engineFlagPresets: EngineFlagPreset[]
  36. displayMode: DisplayMode
  37. }
  38. const store = new Store<AppConfig>({
  39. name: 'config',
  40. defaults: {
  41. workshopPath: null,
  42. defaultFps: null,
  43. recommendedFpsEnabled: false,
  44. recommendedWebFpsEnabled: true,
  45. lweRepoUrl: null,
  46. lweRepoBranch: null,
  47. lweCmakeArgs: null,
  48. backupPath: null,
  49. autoUnsubscribeAfterBackup: false,
  50. trayEnabled: false,
  51. autostartEnabled: false,
  52. autostartMinimized: false,
  53. autostartPlaylistId: null,
  54. killLweOnQuit: false,
  55. audioScreen: null,
  56. ambientVolume: null,
  57. defaultAudioSensitivity: 1,
  58. disablePuppetAnimation: false,
  59. disableAnimations: false,
  60. steamIdentity: 'wallpaper-engine',
  61. ignoredCreators: [],
  62. engineFlags: {},
  63. engineFlagPresets: [],
  64. displayMode: 'shared'
  65. }
  66. })
  67. export function getConfiguredWorkshopPath(): string | null {
  68. return store.get('workshopPath')
  69. }
  70. export function setConfiguredWorkshopPath(p: string): void {
  71. store.set('workshopPath', p)
  72. }
  73. export function isWorkshopPathConfigured(): boolean {
  74. return store.get('workshopPath') !== null
  75. }
  76. export function getConfiguredBackupPath(): string | null {
  77. return store.get('backupPath')
  78. }
  79. export function setConfiguredBackupPath(p: string): void {
  80. store.set('backupPath', p)
  81. }
  82. export function isBackupPathConfigured(): boolean {
  83. return store.get('backupPath') !== null
  84. }
  85. export function getAutoUnsubscribeAfterBackup(): boolean {
  86. return store.get('autoUnsubscribeAfterBackup')
  87. }
  88. export function setAutoUnsubscribeAfterBackup(enabled: boolean): void {
  89. store.set('autoUnsubscribeAfterBackup', enabled)
  90. }
  91. export function getTrayEnabled(): boolean {
  92. return store.get('trayEnabled')
  93. }
  94. export function setTrayEnabled(enabled: boolean): void {
  95. store.set('trayEnabled', enabled)
  96. }
  97. export function getAutostartEnabled(): boolean {
  98. return store.get('autostartEnabled')
  99. }
  100. export function getAutostartMinimized(): boolean {
  101. return store.get('autostartMinimized')
  102. }
  103. export function setAutostart(enabled: boolean, minimized: boolean): void {
  104. store.set('autostartEnabled', enabled)
  105. store.set('autostartMinimized', minimized)
  106. }
  107. export function getAutostartPlaylistId(): string | null {
  108. return store.get('autostartPlaylistId')
  109. }
  110. export function setAutostartPlaylistId(id: string | null): void {
  111. store.set('autostartPlaylistId', id)
  112. }
  113. export function getKillLweOnQuit(): boolean {
  114. return store.get('killLweOnQuit')
  115. }
  116. export function setKillLweOnQuit(enabled: boolean): void {
  117. store.set('killLweOnQuit', enabled)
  118. }
  119. /** How this process identifies itself to Steam - 'wallpaper-engine' registers under WE's own
  120. * app id (full workshop compatibility, but Steam treats this process as WE and closes it on
  121. * logout); 'standalone' registers under a neutral test app id so Steam leaves it running.
  122. * Only read once at startup (steam.service.ts initSteam()) - changing it takes effect on relaunch. */
  123. export function getSteamIdentity(): SteamIdentity {
  124. return store.get('steamIdentity')
  125. }
  126. export function setSteamIdentity(identity: SteamIdentity): void {
  127. store.set('steamIdentity', identity)
  128. }
  129. export function getIgnoredCreators(): string[] {
  130. return store.get('ignoredCreators')
  131. }
  132. export function ignoreCreator(steamId: string): void {
  133. const current = store.get('ignoredCreators')
  134. if (!current.includes(steamId)) store.set('ignoredCreators', [...current, steamId])
  135. }
  136. export function unignoreCreator(steamId: string): void {
  137. store.set('ignoredCreators', store.get('ignoredCreators').filter((id) => id !== steamId))
  138. }
  139. export function getDefaultFps(): number | null {
  140. return store.get('defaultFps')
  141. }
  142. export function setDefaultFps(fps: number | null): void {
  143. store.set('defaultFps', fps)
  144. }
  145. /** When enabled, video wallpapers without a manual FPS override are launched at their own native frame rate. */
  146. export function getRecommendedFpsEnabled(): boolean {
  147. return store.get('recommendedFpsEnabled')
  148. }
  149. export function setRecommendedFpsEnabled(enabled: boolean): void {
  150. store.set('recommendedFpsEnabled', enabled)
  151. }
  152. /** When enabled, web wallpapers without a manual FPS override launch at RECOMMENDED_WEB_FPS
  153. * instead of the default limit - the page only produces a frame when the engine presents one,
  154. * so a lower default makes the animation itself choppy. */
  155. export function getRecommendedWebFpsEnabled(): boolean {
  156. return store.get('recommendedWebFpsEnabled')
  157. }
  158. export function setRecommendedWebFpsEnabled(enabled: boolean): void {
  159. store.set('recommendedWebFpsEnabled', enabled)
  160. }
  161. /** Screen name (matches --screen-root/--audio-screen) that alone produces audio; null = every screen can. */
  162. export function getAudioScreen(): string | null {
  163. return store.get('audioScreen')
  164. }
  165. export function setAudioScreen(screen: string | null): void {
  166. store.set('audioScreen', screen)
  167. }
  168. /** 0-128, applied to non-video (scene sound + web) backgrounds instead of the per-wallpaper volume; null = same volume. */
  169. export function getAmbientVolume(): number | null {
  170. return store.get('ambientVolume')
  171. }
  172. export function setAmbientVolume(volume: number | null): void {
  173. store.set('ambientVolume', volume)
  174. }
  175. /** Freezes puppet (.mdl) meshes at their bind pose (--render-debug no-puppet-animation) - troubleshooting only. */
  176. export function getDisablePuppetAnimation(): boolean {
  177. return store.get('disablePuppetAnimation')
  178. }
  179. export function setDisablePuppetAnimation(disabled: boolean): void {
  180. store.set('disablePuppetAnimation', disabled)
  181. }
  182. export function getDisableAnimations(): boolean {
  183. return store.get('disableAnimations')
  184. }
  185. export function getGlobalEngineFlags(): EngineFlags {
  186. return { ...store.get('engineFlags'), disableAnimations: store.get('disableAnimations') }
  187. }
  188. export function setGlobalEngineFlags(flags: EngineFlags): void {
  189. const { disableAnimations, ...rest } = flags
  190. store.set('engineFlags', rest)
  191. store.set('disableAnimations', !!disableAnimations)
  192. }
  193. export function getEngineFlagPresets(): EngineFlagPreset[] {
  194. return store.get('engineFlagPresets')
  195. }
  196. export function setEngineFlagPresets(presets: EngineFlagPreset[]): void {
  197. store.set('engineFlagPresets', presets.filter((p) => !p.builtIn))
  198. }
  199. /** 'shared' runs one engine across every screen, 'per-screen' one engine (and playlist) per screen */
  200. export function getDisplayMode(): DisplayMode {
  201. return store.get('displayMode')
  202. }
  203. export function setDisplayMode(mode: DisplayMode): void {
  204. store.set('displayMode', mode)
  205. }
  206. /** Default --audio-sensitivity multiplier for audio-reactive objects with no per-wallpaper override. */
  207. export function getDefaultAudioSensitivity(): number {
  208. return store.get('defaultAudioSensitivity')
  209. }
  210. export function setDefaultAudioSensitivity(multiplier: number): void {
  211. store.set('defaultAudioSensitivity', multiplier)
  212. }
  213. /** Custom linux-wallpaperengine repo (git URL or local path); null = official repo. */
  214. export function getLweRepoUrl(): string | null {
  215. return store.get('lweRepoUrl')
  216. }
  217. /** Branch/tag to build from the custom repo; null = default branch. */
  218. export function getLweRepoBranch(): string | null {
  219. return store.get('lweRepoBranch')
  220. }
  221. export function setLweRepo(url: string | null, branch: string | null): void {
  222. store.set('lweRepoUrl', url?.trim() || null)
  223. store.set('lweRepoBranch', branch?.trim() || null)
  224. }
  225. /** Extra cmake arguments (e.g. "-DENABLE_KDE_EXPERIMENTAL_FEATURES=ON"), appended to the build. */
  226. export function getLweCmakeArgs(): string | null {
  227. return store.get('lweCmakeArgs')
  228. }
  229. export function setLweCmakeArgs(args: string | null): void {
  230. store.set('lweCmakeArgs', args?.trim() || null)
  231. }
  232. export function getWeConfigPath(): string {
  233. return path.join(app.getPath('userData'), 'we-config.json')
  234. }
  235. /**
  236. * Import a Wallpaper Engine config.json:
  237. * 1. Copies the file to our data dir
  238. * 2. Replaces all old workshop content paths with the configured workshop path
  239. * 3. Returns the path to the fixed copy
  240. */
  241. export function importWEConfigFile(sourcePath: string): string {
  242. const workshopPath = getConfiguredWorkshopPath()
  243. if (!workshopPath) throw new Error('Workshop path must be configured before importing')
  244. let content = fs.readFileSync(sourcePath, 'utf8')
  245. // The WE config uses paths like:
  246. // D:/Games/SteamLibrary/steamapps/workshop/content/431960/...
  247. // C:/Program Files (x86)/Steam/steamapps/workshop/content/431960/...
  248. // We need to replace everything up to and including "431960/" with the configured workshop path + "/"
  249. const appId = String(WE_APP_ID)
  250. const pattern = new RegExp(
  251. `[A-Za-z]:[^"]*?/steamapps/workshop/content/${appId}/|` +
  252. `/[^"]*?/steamapps/workshop/content/${appId}/`,
  253. 'g'
  254. )
  255. const normalizedTarget = workshopPath.endsWith('/') ? workshopPath : workshopPath + '/'
  256. content = content.replace(pattern, normalizedTarget)
  257. const destPath = getWeConfigPath()
  258. fs.writeFileSync(destPath, content, 'utf8')
  259. console.log(`[Config] Imported WE config from ${sourcePath} to ${destPath}`)
  260. return destPath
  261. }
  262. export function createFreshConfig(): string {
  263. const destPath = getWeConfigPath()
  264. const skeleton = {
  265. '~': {
  266. general: {
  267. browser: { folders: [] },
  268. playlists: []
  269. }
  270. }
  271. }
  272. fs.writeFileSync(destPath, JSON.stringify(skeleton, null, '\t'), 'utf8')
  273. console.log(`[Config] Created fresh config at ${destPath}`)
  274. return destPath
  275. }