/ src / modules / presenter / presenter.py
presenter.py
  1  '''
  2  	QuickSave allows you to download videos from YotuTube
  3  	Copyright (C) 2025  Andrés Chaparro
  4  
  5  	This program is free software: you can redistribute it and/or modify
  6  	it under the terms of the GNU General Public License as published by
  7  	the Free Software Foundation, either version 3 of the License, or
  8  	(at your option) any later version.
  9  
 10  	This program is distributed in the hope that it will be useful,
 11  	but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13  	GNU General Public License for more details.
 14  
 15  	You should have received a copy of the GNU General Public License
 16  	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 17  '''
 18  
 19  # Importing necesary utilities from kivy 
 20  from kivy.lang import Builder
 21  from kivymd.app import MDApp
 22  from kivy.clock import Clock
 23  
 24  from kivy.uix.widget import Widget
 25  from kivy.core.window import Window
 26  
 27  import os
 28  
 29  # Importing the MDDropMenu
 30  from kivymd.uix.menu import MDDropdownMenu
 31  
 32  # Importing the MDButton
 33  from kivymd.uix.button import (
 34  	MDButton, 
 35  	MDButtonText, 
 36  	MDButtonIcon
 37  )
 38  
 39  # Importing the MDList
 40  from kivymd.uix.list import (
 41  	MDListItem, 
 42  	MDListItemHeadlineText, 
 43  	MDListItemLeadingIcon, 
 44  	MDListItemSupportingText
 45  )
 46  
 47  # Importing the MDDialog
 48  from kivymd.uix.dialog import (
 49  	MDDialog,
 50  	MDDialogIcon,
 51  	MDDialogHeadlineText,
 52  	MDDialogSupportingText,
 53  	MDDialogButtonContainer,
 54  	MDDialogContentContainer,
 55  )
 56  
 57  # Importing the MDTextField
 58  from kivymd.uix.textfield import (
 59  	MDTextField,
 60  	MDTextFieldLeadingIcon,
 61  	MDTextFieldHintText,
 62  	MDTextFieldHelperText,
 63  	MDTextFieldTrailingIcon,
 64  	MDTextFieldMaxLengthText,
 65  )
 66  
 67  # Importing custom components
 68  from modules.components.drawer_label import DrawerLabel
 69  from modules.components.drawer_item import DrawerItem
 70  
 71  # Importing the utilities
 72  '''
 73  	Allows you to open the browser
 74  '''
 75  from modules.utils.open_from_browser import OpenBrowser 
 76  '''
 77  	Display a message in the interface
 78  '''
 79  from modules.utils.show_snackbar import ShowSnackbar
 80  '''
 81  	Manage the MDFileManager for easy use
 82  '''
 83  from modules.utils.file_manager_tool import FileManagerTool
 84  '''
 85  	Allows you to download videos from YouTube
 86  '''
 87  from modules.utils.youtube_tool import YoutubeTool
 88  
 89  # Importing persistence
 90  from modules.persistence.config_json import ConfigJson
 91  
 92  # Importing messages
 93  from screens.message_on_start import MessageOnStart
 94  
 95  #-----------
 96  
 97  # Verify the platform
 98  from kivy import platform
 99  
100  ''' 
101  	Requesting the necesary permissions on Android.
102  	It is also important to set these permissions in
103  	the buildozer.spec.
104  '''
105  if platform == 'android':
106  	from android.permissions import request_permissions, Permission
107  	
108  	request_permissions([
109  		Permission.INTERNET,
110  		Permission.READ_EXTERNAL_STORAGE,
111  		Permission.WRITE_EXTERNAL_STORAGE,
112  		Permission.ACCESS_FINE_LOCATION
113  	])
114  
115  # ---------
116  
117  # Presenter class, main activity of kivy
118  class Presenter(MDApp):
119  	# Global attributes
120  	band = True
121  	data = object
122  	account = object
123  	password = object
124  	message_on_start = True
125  	
126  	def __init__(self, **kwargs):
127  		super().__init__(**kwargs)
128  		'''
129  			Handles keyboard or screen events, if not
130  			handled there may be exceptions when
131  			interacting with the app
132  		'''
133  		Window.bind(on_keyboard=self.events)
134  		
135  		'''
136  			Message on startup, only
137  			displayed the first time
138  		'''
139  		self.message_on_start = MessageOnStart()
140  		
141  		'''
142  			The FileManagerTool will allows you
143  			to choose the save path for your videos
144  		'''
145  		self.file_manager_tool = FileManagerTool()
146  		
147  		# Persistence
148  		self.configurations = ConfigJson()
149  		
150  		# Account in the json
151  		self.account = 'user'
152  		
153  		'''
154  			Displaying the startup message if it has not
155  			been displayed before
156  		'''
157  		if self.configurations.load_topic(self.account, 'message_on_start'):
158  			Clock.schedule_once(self.message_on_start.start, 0)
159  			
160  			# Change the startup message from true to false
161  			self.configurations.update_data(self.account, 'message_on_start', False)
162  		
163  		# Initiating persistence
164  		Clock.schedule_once(self.persistence_data, 0)
165  	
166  	def build(self):
167  		
168  		# Loading each of the screens
169  		Builder.load_file("screens/drawer_item.kv")
170  		Builder.load_file("screens/drawer_label.kv")
171  		Builder.load_file("screens/settings_screen.kv")
172  		Builder.load_file("screens/first_screen.kv")
173  		return Builder.load_file("screens/main.kv")
174  
175  	def show_file_manager(self):
176  		# Show file manager
177  		Clock.schedule_once(self.file_manager_tool.open_file_manager, 0)
178  
179  	def persistence_data(self, dt):
180  		# Putting the first elements on the screen
181  		self.start_field()
182  		
183  		# Create the default user
184  		for name in self.configurations.load_users():
185  			self.account = name
186  		
187  		if not (self.configurations.store.exists(self.account)):
188  			self.configurations.save_user()
189  			self.account = 'user'
190  			self.password = '1234'
191  		
192  		# Load the theme
193  		self.theme_cls.theme_style = self.configurations.load_topic(self.account, 'theme')
194  		
195  		# Load the color scheme
196  		self.theme_cls.primary_palette = self.configurations.load_topic(self.account, 'color_scheme')
197  
198  	def start_field(self):
199  		self.text_field_url = MDTextField(
200  			MDTextFieldLeadingIcon(
201  				icon="link",
202  			),
203  			MDTextFieldHintText(
204  				text="URL: ",
205  			),
206  			mode="outlined",
207  			size_hint_x=None,
208  			width="240dp",
209  			pos_hint={"center_x": 0.5, "center_y": 0.5},
210  		)
211  		
212  		self.root.get_screen('first').ids.box.add_widget(
213  			self.text_field_url
214  		)
215  		
216  		# Button for get data
217  		button_get_data = MDButton(
218  			MDButtonIcon(
219  				icon="chevron-down-circle-outline",
220  			),
221  			MDButtonText(
222  				text="Obtener",
223  			),
224  			
225  			style="elevated",
226  			pos_hint={"center_x": 0.5, "center_y": 0.5},
227  		)
228  		
229  		button_get_data.bind(
230  			on_release=self.get_metadata
231  		)
232  		self.root.get_screen('first').ids.box.add_widget(
233  			button_get_data
234  		)
235  	
236  	
237  	def get_metadata(self, *args):
238  		'''
239  			This method is responsible for
240  			obtaining the video data,
241  			sorting it, and displaying it on
242  			the screen
243  		'''
244  		self.youtube_tool = YoutubeTool(self.text_field_url.text)
245  		self.root.get_screen('first').ids.box.clear_widgets()
246  		self.start_field()
247  		
248  		# Get metadata
249  		metadata = self.youtube_tool.get_metadata()
250  		try:
251  			'''
252  				Put the id text field and a
253  				button to download the video,
254  				this is at the top
255  			'''
256  			self.text_field_id = MDTextField(
257  				MDTextFieldLeadingIcon(
258  					icon="identifier",
259  				),
260  				MDTextFieldHintText(
261  					text="ID: ",
262  				),
263  				mode="outlined",
264  				size_hint_x=None,
265  				width="240dp",
266  				pos_hint={"center_x": 0.5, "center_y": 0.5},
267  			)
268  			
269  			self.root.get_screen('first').ids.box.add_widget(
270  				self.text_field_id
271  			)
272  			
273  			button_download = MDButton(
274  				MDButtonIcon(
275  					icon="download",
276  				),
277  				MDButtonText(
278  					text="Descargar",
279  				),
280  				
281  				style="elevated",
282  				pos_hint={"center_x": 0.5, "center_y": 0.5},
283  			)
284  			button_download.bind(
285  				on_release=self.download_video
286  			)
287  			self.root.get_screen('first').ids.box.add_widget(
288  				button_download
289  			)
290  			'''
291  				Organize the highest quality data 
292  				to the lowest quality data
293  			'''
294  			metadata['formats'].reverse()
295  			
296  			'''
297  				Puts each of the video
298  				data organized into items
299  			'''
300  			for fmt in metadata['formats']:
301  				size_mb = f"{int(fmt['filesize']) / (1024 * 1024):.2f} MB" if isinstance(fmt['filesize'], int) else fmt['filesize']
302  				self.root.get_screen('first').ids.box.add_widget(
303  					MDListItem(
304  						MDListItemLeadingIcon(
305  							icon="multimedia",
306  						),
307  						MDListItemHeadlineText(
308  							text=f"{metadata['title']}",
309  						),
310  						MDListItemSupportingText(
311  							text=f"ID: {fmt['format_id']} | {fmt['ext']} | {fmt['resolution']} | {size_mb}",
312  						),
313  					)
314  				)
315  
316  		except Exception as e:
317  			self.root.get_screen('first').ids.box.clear_widgets()
318  			self.start_field()
319  			ShowSnackbar.show(f"No se pudo obtener los datos")
320  			print(f"Error in get_metadata: {e}")
321  	
322  	def download_video(self, *args):
323  		'''
324  			This method is responsible
325  			for downloading the videos
326  		'''
327  		self.youtube_tool.download(
328  			self.text_field_id.text, 
329  			self.configurations.load_topic(self.account, 'path')
330  		)
331  
332  	def open_browser(self, url):
333  		"""
334  			Method to open 
335  			the browser
336  		"""
337  		OpenBrowser.open(url)
338  	
339  	def switch_theme_style(self):
340  		'''
341  			This is responsible
342  			for changing the subject
343  		'''
344  		self.theme_cls.theme_style = (
345  			"Dark" if self.theme_cls.theme_style == "Light" else "Light"
346  		)
347  		
348  		# Displays a message on the screen
349  		ShowSnackbar.show(f"Tema actualizado a {self.theme_cls.theme_style}")
350  		
351  		# Save the theme in the json
352  		self.configurations.update_data(self.account, 'theme', self.theme_cls.theme_style)
353  
354  	
355  	def menu_open(self):
356  		'''
357  			A menu to choose color
358  			schemes
359  		'''
360  		color_scheme = ['Aqua', 'Gold', 'Lime', 'Red', 'Orange', 'Magenta', 'Chocolate', 'Beige']
361  
362  		menu_items = [
363  			{
364  				"text": color_scheme[i],
365  				"on_release": lambda x=color_scheme[i]: self.menu_callback(x),
366  			} for i in range(len(color_scheme))
367  		]
368  		
369  		MDDropdownMenu(
370  			position="auto",
371  			caller=self.root.get_screen('settings').ids.colors_button, items=menu_items
372  		).open()
373  	
374  	def menu_callback(self, color_scheme):
375  		# Update the color scheme
376  		ShowSnackbar.show(f"Esquema de color actualizado a {color_scheme}")
377  		self.theme_cls.primary_palette = color_scheme
378  		
379  		self.configurations.update_data(self.account, 'color_scheme', color_scheme)
380  	
381  	def on_keyboard(self, window, key, scancode, codepoint, modifier):
382  		# Logic for handling keys
383  		if key == 27:  # key ESC
384  			# logic ...
385  			return True  # Indicates that the event was handled correctly
386  		return False  # The event was not handled
387  
388  	def events(self, instance, keyboard, keycode, text, modifiers):
389  		'''Called when buttons are pressed on the mobile device.'''
390  		try:
391  			if keyboard in (1001, 27):
392  				if self.manager_open:
393  					self.file_manager.back()
394  			return True
395  		except Exception as e:
396  			print(str(f"Error {e}"))
397  		return True
398  
399  
400  
401  
402  
403  
404  
405  
406