state-manager.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /**
  2. * @fileoverview Application state management with event system
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. * @since 2024-01-01
  6. */
  7. /**
  8. * APPLICATION STATE MANAGEMENT
  9. *
  10. * Central state object managing all application data
  11. *
  12. * Structure:
  13. * - videos: Array of video objects in download queue
  14. * - config: User preferences and settings
  15. * - ui: Interface state and user interactions
  16. *
  17. * Mutation Rules:
  18. * - Only modify state through designated functions
  19. * - Emit events on state changes for UI updates
  20. * - Validate all state changes before applying
  21. */
  22. class AppState {
  23. /**
  24. * Creates new AppState instance
  25. * @param {Object} config - Initial configuration
  26. */
  27. constructor() {
  28. this.videos = [];
  29. this.history = []; // Array of completed download history entries
  30. this.config = {
  31. savePath: this.getDefaultDownloadsPath(),
  32. defaultQuality: '1080p',
  33. defaultFormat: 'None',
  34. filenamePattern: '%(title)s.%(ext)s',
  35. cookieFile: null,
  36. maxHistoryEntries: 100 // Maximum number of history entries to keep
  37. };
  38. this.ui = {
  39. isDownloading: false,
  40. selectedVideos: [],
  41. sortBy: 'createdAt',
  42. sortOrder: 'desc'
  43. };
  44. this.listeners = new Map();
  45. }
  46. /**
  47. * Get default downloads path based on platform
  48. * @returns {string} Default download path for current platform
  49. */
  50. getDefaultDownloadsPath() {
  51. const DEFAULT_PATHS = {
  52. darwin: '~/Downloads/GrabZilla_Videos',
  53. win32: 'C:\\Users\\Admin\\Desktop\\GrabZilla_Videos',
  54. linux: '~/Downloads/GrabZilla_Videos'
  55. };
  56. if (window.electronAPI) {
  57. const platform = window.electronAPI.getPlatform();
  58. return DEFAULT_PATHS[platform] || DEFAULT_PATHS.linux;
  59. }
  60. return DEFAULT_PATHS.win32;
  61. }
  62. /**
  63. * Add video to state with validation
  64. * @param {Video} video - Video object to add
  65. * @returns {Video} Added video object
  66. * @throws {Error} When video is invalid or URL already exists
  67. */
  68. addVideo(video) {
  69. if (!(video instanceof Video)) {
  70. throw new Error('Invalid video object');
  71. }
  72. // Check for duplicate URLs
  73. const existingVideo = this.videos.find(v => v.url === video.url);
  74. if (existingVideo) {
  75. throw new Error('Video URL already exists in the list');
  76. }
  77. this.videos.push(video);
  78. this.emit('videoAdded', { video });
  79. return video;
  80. }
  81. /**
  82. * Remove video from state by ID
  83. * @param {string} videoId - ID of video to remove
  84. * @returns {Video} Removed video object
  85. * @throws {Error} When video not found
  86. */
  87. removeVideo(videoId) {
  88. const index = this.videos.findIndex(v => v.id === videoId);
  89. if (index === -1) {
  90. throw new Error('Video not found');
  91. }
  92. const removedVideo = this.videos.splice(index, 1)[0];
  93. this.emit('videoRemoved', { video: removedVideo });
  94. return removedVideo;
  95. }
  96. /**
  97. * Update video properties by ID
  98. * @param {string} videoId - ID of video to update
  99. * @param {Object} properties - Properties to update
  100. * @returns {Video} Updated video object
  101. * @throws {Error} When video not found
  102. */
  103. updateVideo(videoId, properties) {
  104. const video = this.videos.find(v => v.id === videoId);
  105. if (!video) {
  106. throw new Error('Video not found');
  107. }
  108. const oldProperties = { ...video };
  109. video.update(properties);
  110. this.emit('videoUpdated', { video, oldProperties });
  111. return video;
  112. }
  113. /**
  114. * Get video by ID
  115. * @param {string} videoId - Video ID to find
  116. * @returns {Video|undefined} Video object or undefined if not found
  117. */
  118. getVideo(videoId) {
  119. return this.videos.find(v => v.id === videoId);
  120. }
  121. /**
  122. * Get all videos (defensive copy)
  123. * @returns {Video[]} Array of all video objects
  124. */
  125. getVideos() {
  126. return [...this.videos];
  127. }
  128. /**
  129. * Get videos filtered by status
  130. * @param {string} status - Status to filter by
  131. * @returns {Video[]} Array of videos with matching status
  132. */
  133. getVideosByStatus(status) {
  134. return this.videos.filter(v => v.status === status);
  135. }
  136. /**
  137. * Clear all videos from state
  138. * @returns {Video[]} Array of removed videos
  139. */
  140. clearVideos() {
  141. const removedVideos = [...this.videos];
  142. this.videos = [];
  143. this.ui.selectedVideos = [];
  144. this.emit('videosCleared', { removedVideos });
  145. return removedVideos;
  146. }
  147. /**
  148. * Update configuration with validation
  149. * @param {Object} newConfig - Configuration updates
  150. */
  151. updateConfig(newConfig) {
  152. const oldConfig = { ...this.config };
  153. Object.assign(this.config, newConfig);
  154. this.emit('configUpdated', { config: this.config, oldConfig });
  155. }
  156. /**
  157. * Update UI state
  158. * @param {Object} newUIState - UI state updates
  159. */
  160. updateUI(newUIState) {
  161. const oldUIState = { ...this.ui };
  162. Object.assign(this.ui, newUIState);
  163. this.emit('uiUpdated', { ui: this.ui, oldUIState });
  164. }
  165. /**
  166. * Register event listener
  167. * @param {string} event - Event name
  168. * @param {Function} callback - Event callback function
  169. */
  170. on(event, callback) {
  171. if (!this.listeners.has(event)) {
  172. this.listeners.set(event, []);
  173. }
  174. this.listeners.get(event).push(callback);
  175. }
  176. /**
  177. * Remove event listener
  178. * @param {string} event - Event name
  179. * @param {Function} callback - Event callback function to remove
  180. */
  181. off(event, callback) {
  182. if (this.listeners.has(event)) {
  183. const callbacks = this.listeners.get(event);
  184. const index = callbacks.indexOf(callback);
  185. if (index > -1) {
  186. callbacks.splice(index, 1);
  187. }
  188. }
  189. }
  190. /**
  191. * Emit event to all registered listeners
  192. * @param {string} event - Event name
  193. * @param {Object} data - Event data
  194. */
  195. emit(event, data) {
  196. if (this.listeners.has(event)) {
  197. this.listeners.get(event).forEach(callback => {
  198. try {
  199. callback(data);
  200. } catch (error) {
  201. console.error(`Error in event listener for ${event}:`, error);
  202. }
  203. });
  204. }
  205. }
  206. /**
  207. * Get application statistics
  208. * @returns {Object} Statistics object with counts by status
  209. */
  210. getStats() {
  211. return {
  212. total: this.videos.length,
  213. ready: this.getVideosByStatus('ready').length,
  214. downloading: this.getVideosByStatus('downloading').length,
  215. converting: this.getVideosByStatus('converting').length,
  216. completed: this.getVideosByStatus('completed').length,
  217. error: this.getVideosByStatus('error').length
  218. };
  219. }
  220. /**
  221. * Add completed video to download history
  222. * @param {Video} video - Completed video to add to history
  223. */
  224. addToHistory(video) {
  225. const historyEntry = {
  226. id: video.id,
  227. url: video.url,
  228. title: video.title,
  229. thumbnail: video.thumbnail,
  230. duration: video.duration,
  231. quality: video.quality,
  232. format: video.format,
  233. filename: video.filename,
  234. downloadedAt: new Date().toISOString()
  235. };
  236. // Add to beginning of history array
  237. this.history.unshift(historyEntry);
  238. // Keep only maxHistoryEntries
  239. if (this.history.length > this.config.maxHistoryEntries) {
  240. this.history = this.history.slice(0, this.config.maxHistoryEntries);
  241. }
  242. this.emit('historyUpdated', { entry: historyEntry });
  243. }
  244. /**
  245. * Get all history entries
  246. * @returns {Array} Array of history entries
  247. */
  248. getHistory() {
  249. return this.history;
  250. }
  251. /**
  252. * Clear all history
  253. */
  254. clearHistory() {
  255. this.history = [];
  256. this.emit('historyCleared');
  257. }
  258. /**
  259. * Remove specific history entry
  260. * @param {string} entryId - ID of history entry to remove
  261. */
  262. removeHistoryEntry(entryId) {
  263. const index = this.history.findIndex(entry => entry.id === entryId);
  264. if (index !== -1) {
  265. const removed = this.history.splice(index, 1)[0];
  266. this.emit('historyEntryRemoved', { entry: removed });
  267. }
  268. }
  269. /**
  270. * Export state to JSON for persistence
  271. * @returns {Object} Serializable state object
  272. */
  273. toJSON() {
  274. return {
  275. videos: this.videos.map(v => v.toJSON()),
  276. history: this.history,
  277. config: this.config,
  278. ui: this.ui
  279. };
  280. }
  281. /**
  282. * Import state from JSON data
  283. * @param {Object} data - State data to import
  284. */
  285. fromJSON(data) {
  286. this.videos = data.videos.map(v => Video.fromJSON(v));
  287. this.history = data.history || [];
  288. this.config = { ...this.config, ...data.config };
  289. this.ui = { ...this.ui, ...data.ui };
  290. this.emit('stateImported', { data });
  291. }
  292. }