state-manager.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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.config = {
  30. savePath: this.getDefaultDownloadsPath(),
  31. defaultQuality: '1080p',
  32. defaultFormat: 'None',
  33. filenamePattern: '%(title)s.%(ext)s',
  34. cookieFile: null
  35. };
  36. this.ui = {
  37. isDownloading: false,
  38. selectedVideos: [],
  39. sortBy: 'createdAt',
  40. sortOrder: 'desc'
  41. };
  42. this.listeners = new Map();
  43. }
  44. /**
  45. * Get default downloads path based on platform
  46. * @returns {string} Default download path for current platform
  47. */
  48. getDefaultDownloadsPath() {
  49. const DEFAULT_PATHS = {
  50. darwin: '~/Downloads/GrabZilla_Videos',
  51. win32: 'C:\\Users\\Admin\\Desktop\\GrabZilla_Videos',
  52. linux: '~/Downloads/GrabZilla_Videos'
  53. };
  54. if (window.electronAPI) {
  55. const platform = window.electronAPI.getPlatform();
  56. return DEFAULT_PATHS[platform] || DEFAULT_PATHS.linux;
  57. }
  58. return DEFAULT_PATHS.win32;
  59. }
  60. /**
  61. * Add video to state with validation
  62. * @param {Video} video - Video object to add
  63. * @returns {Video} Added video object
  64. * @throws {Error} When video is invalid or URL already exists
  65. */
  66. addVideo(video) {
  67. if (!(video instanceof Video)) {
  68. throw new Error('Invalid video object');
  69. }
  70. // Check for duplicate URLs
  71. const existingVideo = this.videos.find(v => v.url === video.url);
  72. if (existingVideo) {
  73. throw new Error('Video URL already exists in the list');
  74. }
  75. this.videos.push(video);
  76. this.emit('videoAdded', { video });
  77. return video;
  78. }
  79. /**
  80. * Remove video from state by ID
  81. * @param {string} videoId - ID of video to remove
  82. * @returns {Video} Removed video object
  83. * @throws {Error} When video not found
  84. */
  85. removeVideo(videoId) {
  86. const index = this.videos.findIndex(v => v.id === videoId);
  87. if (index === -1) {
  88. throw new Error('Video not found');
  89. }
  90. const removedVideo = this.videos.splice(index, 1)[0];
  91. this.emit('videoRemoved', { video: removedVideo });
  92. return removedVideo;
  93. }
  94. /**
  95. * Update video properties by ID
  96. * @param {string} videoId - ID of video to update
  97. * @param {Object} properties - Properties to update
  98. * @returns {Video} Updated video object
  99. * @throws {Error} When video not found
  100. */
  101. updateVideo(videoId, properties) {
  102. const video = this.videos.find(v => v.id === videoId);
  103. if (!video) {
  104. throw new Error('Video not found');
  105. }
  106. const oldProperties = { ...video };
  107. video.update(properties);
  108. this.emit('videoUpdated', { video, oldProperties });
  109. return video;
  110. }
  111. /**
  112. * Get video by ID
  113. * @param {string} videoId - Video ID to find
  114. * @returns {Video|undefined} Video object or undefined if not found
  115. */
  116. getVideo(videoId) {
  117. return this.videos.find(v => v.id === videoId);
  118. }
  119. /**
  120. * Get all videos (defensive copy)
  121. * @returns {Video[]} Array of all video objects
  122. */
  123. getVideos() {
  124. return [...this.videos];
  125. }
  126. /**
  127. * Get videos filtered by status
  128. * @param {string} status - Status to filter by
  129. * @returns {Video[]} Array of videos with matching status
  130. */
  131. getVideosByStatus(status) {
  132. return this.videos.filter(v => v.status === status);
  133. }
  134. /**
  135. * Clear all videos from state
  136. * @returns {Video[]} Array of removed videos
  137. */
  138. clearVideos() {
  139. const removedVideos = [...this.videos];
  140. this.videos = [];
  141. this.ui.selectedVideos = [];
  142. this.emit('videosCleared', { removedVideos });
  143. return removedVideos;
  144. }
  145. /**
  146. * Update configuration with validation
  147. * @param {Object} newConfig - Configuration updates
  148. */
  149. updateConfig(newConfig) {
  150. const oldConfig = { ...this.config };
  151. Object.assign(this.config, newConfig);
  152. this.emit('configUpdated', { config: this.config, oldConfig });
  153. }
  154. /**
  155. * Update UI state
  156. * @param {Object} newUIState - UI state updates
  157. */
  158. updateUI(newUIState) {
  159. const oldUIState = { ...this.ui };
  160. Object.assign(this.ui, newUIState);
  161. this.emit('uiUpdated', { ui: this.ui, oldUIState });
  162. }
  163. /**
  164. * Register event listener
  165. * @param {string} event - Event name
  166. * @param {Function} callback - Event callback function
  167. */
  168. on(event, callback) {
  169. if (!this.listeners.has(event)) {
  170. this.listeners.set(event, []);
  171. }
  172. this.listeners.get(event).push(callback);
  173. }
  174. /**
  175. * Remove event listener
  176. * @param {string} event - Event name
  177. * @param {Function} callback - Event callback function to remove
  178. */
  179. off(event, callback) {
  180. if (this.listeners.has(event)) {
  181. const callbacks = this.listeners.get(event);
  182. const index = callbacks.indexOf(callback);
  183. if (index > -1) {
  184. callbacks.splice(index, 1);
  185. }
  186. }
  187. }
  188. /**
  189. * Emit event to all registered listeners
  190. * @param {string} event - Event name
  191. * @param {Object} data - Event data
  192. */
  193. emit(event, data) {
  194. if (this.listeners.has(event)) {
  195. this.listeners.get(event).forEach(callback => {
  196. try {
  197. callback(data);
  198. } catch (error) {
  199. console.error(`Error in event listener for ${event}:`, error);
  200. }
  201. });
  202. }
  203. }
  204. /**
  205. * Get application statistics
  206. * @returns {Object} Statistics object with counts by status
  207. */
  208. getStats() {
  209. return {
  210. total: this.videos.length,
  211. ready: this.getVideosByStatus('ready').length,
  212. downloading: this.getVideosByStatus('downloading').length,
  213. converting: this.getVideosByStatus('converting').length,
  214. completed: this.getVideosByStatus('completed').length,
  215. error: this.getVideosByStatus('error').length
  216. };
  217. }
  218. /**
  219. * Export state to JSON for persistence
  220. * @returns {Object} Serializable state object
  221. */
  222. toJSON() {
  223. return {
  224. videos: this.videos.map(v => v.toJSON()),
  225. config: this.config,
  226. ui: this.ui
  227. };
  228. }
  229. /**
  230. * Import state from JSON data
  231. * @param {Object} data - State data to import
  232. */
  233. fromJSON(data) {
  234. this.videos = data.videos.map(v => Video.fromJSON(v));
  235. this.config = { ...this.config, ...data.config };
  236. this.ui = { ...this.ui, ...data.ui };
  237. this.emit('stateImported', { data });
  238. }
  239. }