Major bug fixes, optimization + major advance

This commit is contained in:
Jérémi N ‘EndMove’ 2022-09-05 23:13:38 +02:00
parent 539b75cb09
commit f8f7832dd7
Signed by: EndMove
GPG Key ID: 65C4A02E1F5371A4
11 changed files with 322 additions and 161 deletions

BIN
assets/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@ -17,6 +17,10 @@
"optionDest": "console", "optionDest": "console",
"value": false "value": false
}, },
{
"optionDest": "icon_file",
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/assets/logo.ico"
},
{ {
"optionDest": "name", "optionDest": "name",
"value": "WebPicDownloader_v1.0.0" "value": "WebPicDownloader_v1.0.0"
@ -68,6 +72,10 @@
{ {
"optionDest": "argv_emulation", "optionDest": "argv_emulation",
"value": false "value": false
},
{
"optionDest": "datas",
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/assets;assets/"
} }
], ],
"nonPyinstallerOptions": { "nonPyinstallerOptions": {

View File

@ -14,5 +14,5 @@ class Frames(Enum):
@version 1.0.0 @version 1.0.0
@since 2022-08-30 @since 2022-08-30
""" """
Home = 1 # Home view HOME = 1 # Home view
Info = 2 # Info & copyright view INFO = 2 # Info & copyright view

View File

@ -1,14 +1,12 @@
import time
from controller.MainController import MainController from controller.MainController import MainController
from model.WebPicDownloader import WebPicDownloader from model.WebPicDownloader import MessageType, WebPicDownloader
from util.AsyncTask import AsyncTask
class HomeController: class HomeController:
""" """
Controller - HomeController Controller - HomeController
desc... This controller handles all the interaction directly related to the download.
@author Jérémi Nihart / EndMove @author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader @link https://git.endmove.eu/EndMove/WebPicDownloader
@ -16,13 +14,12 @@ class HomeController:
@since 2022-08-30 @since 2022-08-30
""" """
# Variables # Variables
__main_controller = None __main_controller: MainController = None
__view = None __view = None
__webpic: WebPicDownloader = None __webpic: WebPicDownloader = None
__download_task = None
# Constructor # Constructor
def __init__(self, controller: MainController, webpic) -> None: def __init__(self, controller: MainController, webpic: WebPicDownloader) -> None:
""" """
Constructor Constructor
@ -33,6 +30,11 @@ class HomeController:
self.__main_controller = controller self.__main_controller = controller
self.__webpic = webpic self.__webpic = webpic
# setup webpic event
webpic.set_messenger_callback(self.on_webpic_messenger)
webpic.set_success_callback(self.on_webpic_success)
webpic.set_failure_callback(self.on_webpic_failure)
# Subscribe to events # Subscribe to events
controller.subscribe_to_quite_event(self.on_quit) controller.subscribe_to_quite_event(self.on_quit)
@ -45,21 +47,9 @@ class HomeController:
* :view: -> The view that this controller manage. * :view: -> The view that this controller manage.
""" """
self.__view = view self.__view = view
self.__webpic.set_messenger_callback(self.on_webpic_messenger)
self.__webpic.set_success_callback(self.on_webpic_success)
self.__webpic.set_failure_callback(self.on_webpic_failure)
# END View method # END View method
# START View events # START View events
def on_change_view(self, frame) -> None:
"""
[event function for view]
=> Call this event method when the user requests to change the window.
* :frame: -> The frame we want to launch.
"""
self.__main_controller.change_frame(frame)
def on_download_requested(self, url: str, name: str) -> None: def on_download_requested(self, url: str, name: str) -> None:
""" """
[event function for view] [event function for view]
@ -69,26 +59,45 @@ class HomeController:
* :name: -> The name of the folder in which put pictures. * :name: -> The name of the folder in which put pictures.
""" """
if url.strip() and name.strip(): if url.strip() and name.strip():
self.__view.set_interface_state(True)
self.__view.clear_logs()
self.__webpic.start_downloading(url, name) self.__webpic.start_downloading(url, name)
else: else:
self.__view.show_error_message("Opss, the url or folder name are not valid!") self.__view.show_error_message("Opss, the url or folder name are not valid!")
# END View events # END View events
# START Webpic events # START Webpic events
def on_webpic_messenger(self, message: str) -> None: def on_webpic_messenger(self, message: str, type) -> None:
""" """
[event function for webpic]
=> This event is called to communicate a message.
* :message: -> Message that webpic send to the controller.
* :type: -> Type of message that webpic send to the controller.
""" """
match type:
case MessageType.LOG:
self.__view.add_log(message) self.__view.add_log(message)
case MessageType.ERROR:
self.__view.show_error_message(message)
case MessageType.SUCCESS:
self.__view.show_success_message(message)
def on_webpic_success(self) -> None: def on_webpic_success(self) -> None:
""" """
[event function for webpic]
=> This event is called to indicate that the download has finished successfully.
""" """
self.__view.show_success_message("The download has been successfully completed.") self.__view.show_success_message("The download has been successfully completed.")
self.__view.set_interface_state(False)
def on_webpic_failure(self) -> None: def on_webpic_failure(self) -> None:
""" """
[event function for webpic]
=> This event is called to indicate that there was a problem during the download.
""" """
self.__view.show_error_message("A critical error preventing the download occurred, check the logs.") self.__view.show_error_message("A critical error preventing the download occurred, check the logs.")
self.__view.set_interface_state(False)
# END Webpic events # END Webpic events
# START Controller methods # START Controller methods
@ -97,44 +106,13 @@ class HomeController:
[event function for controller] [event function for controller]
=> Call this event when a request to exit is thrown. => Call this event when a request to exit is thrown.
""" """
if self.__webpic.is_alive(): if self.__webpic.is_download_running():
if self.__main_controller.show_question_dialog( if self.__main_controller.show_question_dialog(
"Are you sure?", "Are you sure?",
"Do you really want to quit while the download is running?\nThis will stop the download." "Do you really want to quit while the download is running?\nThis will stop the download."
): ):
self.__webpic.stop_downloading() self.__webpic.stop_downloading() # hot stop deamon
time.sleep(4)
return False return False
else:
return True return True
print("Quit... homecontroller END") # REMOVE self.__webpic.stop_downloading(block=True)
# END Controller methods # END Controller methods
# START Task methods
def __async_task_start(self, url, name) -> None:
"""
[CallBack start function]
=> Start Callback function for asynctask, be careful once executed in asynctask this
function will keep its controller context. In short it's as if the thread was
launched in the controller and the execution never left it.
* :url: -> Url for webpic.
* :name: -> Working dir name for webpic.
"""
print("start callback called") # REMOVE
self.__view.clear_logs()
if self.__webpic.download(url, name):
self.__view.show_success_message("The download has been successfully completed.")
else:
self.__view.show_error_message("A critical error preventing the download occurred, check the logs.")
def __async_task_stop(self) -> None:
"""
[CallBack stop function]
=> End Callback function for asynctask, be careful once executed in asynctask this
function will keep its controller context. In short it's as if the thread was
launched in the controller and the execution never left it.
"""
print("stop callback called") # REMOVE
self.__webpic.stop()
# END Task methods

View File

@ -1,3 +1,4 @@
from controller.Frames import Frames
from controller.MainController import MainController from controller.MainController import MainController
@ -5,7 +6,7 @@ class InfoController:
""" """
Controller - InfoController Controller - InfoController
desc... This controller manages the display of information in the information view.
@author Jérémi Nihart / EndMove @author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader @link https://git.endmove.eu/EndMove/WebPicDownloader
@ -13,11 +14,17 @@ class InfoController:
@since 2022-08-30 @since 2022-08-30
""" """
# Variables # Variables
__main_controller = None __main_controller: MainController = None
__view = None __view = None
# Constructor # Constructor
def __init__(self, controller: MainController) -> None: def __init__(self, controller: MainController) -> None:
"""
Constructor
* :controller: -> The main application cpntroller.
"""
# Setup variables
self.__main_controller = controller self.__main_controller = controller
# START View methods # START View methods
@ -28,14 +35,20 @@ class InfoController:
:view: -> The view that this controller manage. :view: -> The view that this controller manage.
""" """
self.__view = view self.__view = view
self.__view.set_title(self.__main_controller.get_config('about_title'))
self.__view.set_content(self.__main_controller.get_config('about_content'))
self.__view.set_version(
f"version: {self.__main_controller.get_config('app_version')} - {self.__main_controller.get_config('app_version_date')}"
)
# END View method # END View method
# START View events # START View events
def on_change_view(self, frame) -> None: def on_change_view(self, frame: Frames) -> None:
""" """
[event function for view] [event function for view]
=> Call this event method when the user requests to change the window.
:frame: -> The frame we want to launch. * :frame: -> The frame we want to launch.
""" """
self.__main_controller.change_frame(frame) self.__main_controller.change_frame(frame)
# END View events # END View events

View File

@ -1,11 +1,13 @@
import os import os
from controller.Frames import Frames
class MainController: class MainController:
""" """
Controller - MainController Controller - MainController
TODO desc... This controller manages all the main interaction, change of windows,
dialogs, stop... It is the main controller.
@author Jérémi Nihart / EndMove @author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader @link https://git.endmove.eu/EndMove/WebPicDownloader
@ -44,7 +46,7 @@ class MainController:
[event function for view] [event function for view]
=> Event launch when you ask to open the current folder. => Event launch when you ask to open the current folder.
""" """
os.startfile(self.get_config('app_folder')) os.startfile(self.get_config('app_folder')) # Open the file explorer on working dir
def on_quite(self) -> None: def on_quite(self) -> None:
""" """
@ -70,12 +72,11 @@ class MainController:
[event function for view] [event function for view]
=> Event launched when a request for more information arise. => Event launched when a request for more information arise.
""" """
# TODO on_about self.change_frame(Frames.INFO)
print("on_about")
# END View methods # END View methods
# START Controller methods # START Controller methods
def change_frame(self, frame) -> None: def change_frame(self, frame: Frames) -> None:
""" """
[function for controller] [function for controller]
=> Allows you to request a frame change in the main window. => Allows you to request a frame change in the main window.

41
main.py
View File

@ -1,4 +1,5 @@
import os import os
import sys
from controller.HomeController import HomeController from controller.HomeController import HomeController
from controller.InfoController import InfoController from controller.InfoController import InfoController
from controller.MainController import MainController from controller.MainController import MainController
@ -9,11 +10,38 @@ from view.InfoView import InfoView
from view.MainWindow import MainWindow from view.MainWindow import MainWindow
def get_sys_directory() -> str:
"""
Recover the path of the application's resources.
"""
try:
directory = sys._MEIPASS
except Exception:
directory = os.getcwd()
return directory
def get_config() -> dict: def get_config() -> dict:
"""
Retrieve the application configuration
"""
return { return {
'app_name': "WebPicDownloader", 'app_name': 'WebPicDownloader',
'app_folder': os.getcwd(), 'app_folder': os.getcwd(),
'app_version': "1.0.0" # This version must match with the version.txt at root 'app_version': '1.0.0', # This version must match with the version.txt at root
'app_version_date': '2022-09-05',
'sys_directory': get_sys_directory(),
'about_title': 'About WebPicDownloader',
'about_content':
"""This scraping software has been developed by EndMove
and is fully open-source. The source code is available
here: https://git.endmove.eu/EndMove/WebPicDownloader
EndMove is available at the following address for any
request contact@endmove.eu. In case of problemsplease
open an issue on the repository.
The logo of the software was made by Gashila"""
} }
if __name__ == '__main__': if __name__ == '__main__':
@ -30,7 +58,7 @@ if __name__ == '__main__':
config = get_config() config = get_config()
# Create utli/model # Create utli/model
webpic = WebPicDownloader(path=config.get('app_folder'), asynchrone=True) webpic = WebPicDownloader(path=config.get('app_folder'))
# Create app controllers # Create app controllers
main_controller = MainController(config) main_controller = MainController(config)
@ -43,11 +71,12 @@ if __name__ == '__main__':
info_controller = InfoView(main_window, info_controller) info_controller = InfoView(main_window, info_controller)
# Add views to main window # Add views to main window
main_window.add_view(Frames.Home, home_view) main_window.add_view(Frames.HOME, home_view)
main_window.add_view(Frames.Info, info_controller) main_window.add_view(Frames.INFO, info_controller)
# Choose the launching view # Choose the launching view
main_window.show_frame(Frames.Home) main_window.show_frame(Frames.HOME)
# Start main windows looping (launch program) # Start main windows looping (launch program)
main_window.mainloop() main_window.mainloop()

View File

@ -1,10 +1,31 @@
import os import os
from enum import Enum
from threading import Semaphore, Thread from threading import Semaphore, Thread
from urllib import request from urllib import request
from urllib.error import HTTPError, URLError
from bs4 import BeautifulSoup, Tag, ResultSet from bs4 import BeautifulSoup, Tag, ResultSet
class MessageType(Enum):
"""
MessageType
Is an enumeration to define the different types of messages sent by the webpic messenger.
There are 3 types of messages.
- log -> log
- error -> err
- success -> suc
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-09-05
"""
LOG = 'log'
ERROR = 'err'
SUCCESS = 'suc'
class WebPicDownloader(Thread): class WebPicDownloader(Thread):
""" """
WebPicDownloader WebPicDownloader
@ -12,19 +33,18 @@ class WebPicDownloader(Thread):
Webpicdownloader is a scraping tool that allows you to browse a web page, Webpicdownloader is a scraping tool that allows you to browse a web page,
find the images and download them. This tool is easily usable and implementable find the images and download them. This tool is easily usable and implementable
in an application. It has been designed to be executed in an integrated thread in an application. It has been designed to be executed in an integrated thread
in an asynchronous way as well as more classically in a synchronous way. This in an asynchronous way. This tool allows to define 3 callback functions, one for
tool allows to define 3 callback functions, one for events, one in case of events, one in case of success and one in case of failure. It also has an
success and one in case of failure. It also has an integrated entry point integrated entry point allowing it to be directly executed in terminal mode.
allowing it to be directly executed in terminal mode.
@author EndMove <contact@endmove.eu> @author EndMove <contact@endmove.eu>
@version 1.2.0 @version 1.2.1
""" """
# Variables # Variables
__callbacks: dict = None # Callback dictionary __callbacks: dict = None # Callback dictionary
__settings: dict = None # __settings: dict = None # Webpic basics settings
__dl_infos: dict = None # __dl_infos: dict = None # Download informations
__sem: Semaphore = None # __sem: Semaphore = None # Semaphore for the webpic worker
_exit: bool = None # When set to True quit the thread _exit: bool = None # When set to True quit the thread
@ -39,7 +59,6 @@ class WebPicDownloader(Thread):
* :path: -> Folder in which the tool will create the download folders and place the images. * :path: -> Folder in which the tool will create the download folders and place the images.
* :headers: -> Dictionary allowing to define the different parameters present in the header * :headers: -> Dictionary allowing to define the different parameters present in the header
of the requests sent by WebPic. of the requests sent by WebPic.
* :asynchronous: -> True: launch the download in a thread, False: the opposite.
* :messenger: -> Callback function messenger (see setter). * :messenger: -> Callback function messenger (see setter).
* :success: -> Callback function success (see setter). * :success: -> Callback function success (see setter).
* :failure: -> Callback function failure (see setter). * :failure: -> Callback function failure (see setter).
@ -60,8 +79,6 @@ class WebPicDownloader(Thread):
'website_url': 'url', 'website_url': 'url',
'download_name': 'name', 'download_name': 'name',
'download_path': 'full_path', 'download_path': 'full_path',
'tot_image_count': 0,
'dl_image_count': 0,
'running': False 'running': False
} }
self.__sem = Semaphore(0) self.__sem = Semaphore(0)
@ -69,6 +86,7 @@ class WebPicDownloader(Thread):
self.start() # start deamon self.start() # start deamon
# Internal functions # Internal functions
def __get_html(self, url: str) -> str: def __get_html(self, url: str) -> str:
""" """
@ -82,6 +100,7 @@ class WebPicDownloader(Thread):
response = request.urlopen(req) response = request.urlopen(req)
return response.read().decode('utf-8') return response.read().decode('utf-8')
def __find_all_img(self, html: str) -> ResultSet: def __find_all_img(self, html: str) -> ResultSet:
""" """
Internal Function #do-not-use# Internal Function #do-not-use#
@ -93,6 +112,7 @@ class WebPicDownloader(Thread):
soup = BeautifulSoup(html, 'html.parser') soup = BeautifulSoup(html, 'html.parser')
return soup.find_all('img') return soup.find_all('img')
def __find_img_link(self, img: Tag) -> str: def __find_img_link(self, img: Tag) -> str:
""" """
Internal Function #do-not-use# Internal Function #do-not-use#
@ -115,6 +135,7 @@ class WebPicDownloader(Thread):
raise ValueError("Bad image url") raise ValueError("Bad image url")
return link return link
def __find_image_type(self, img_link: str) -> str: def __find_image_type(self, img_link: str) -> str:
""" """
Internal Function #do-not-use# Internal Function #do-not-use#
@ -128,6 +149,7 @@ class WebPicDownloader(Thread):
type = type.split('?')[0] type = type.split('?')[0]
return type return type
def __download_img(self, url: str, filename: str) -> None: def __download_img(self, url: str, filename: str) -> None:
""" """
Internal Function #do-not-use# Internal Function #do-not-use#
@ -141,6 +163,7 @@ class WebPicDownloader(Thread):
with open(filename, 'wb') as img: with open(filename, 'wb') as img:
img.write(raw_img) img.write(raw_img)
def __initialize_folder(self, folder_path: str) -> None: def __initialize_folder(self, folder_path: str) -> None:
""" """
Internal Function #do-not-use# Internal Function #do-not-use#
@ -154,12 +177,17 @@ class WebPicDownloader(Thread):
else: else:
raise ValueError("The folder already exists, it may already contain images") raise ValueError("The folder already exists, it may already contain images")
def __msg(self, message: str) -> None:
def __msg(self, message: str, type:MessageType=MessageType.LOG) -> None:
""" """
Internal Function #do-not-use# Internal Function #do-not-use#
=> Use the messenger callback to send a message. => Use the messenger callback to send a message.
* :message: -> the message to send through callback
* :type: -> message type, can be ['log', 'err', 'suc']
""" """
self.__callbacks.get('messenger')(message) self.__callbacks.get('messenger')(message, type)
# Public functions # Public functions
def set_success_callback(self, callback) -> None: def set_success_callback(self, callback) -> None:
@ -170,6 +198,7 @@ class WebPicDownloader(Thread):
""" """
self.__callbacks['success'] = callback self.__callbacks['success'] = callback
def set_failure_callback(self, callback) -> None: def set_failure_callback(self, callback) -> None:
""" """
Setter to define the callback function called when the download fails. Setter to define the callback function called when the download fails.
@ -178,6 +207,7 @@ class WebPicDownloader(Thread):
""" """
self.__callbacks['failure'] = callback self.__callbacks['failure'] = callback
def set_messenger_callback(self, callback) -> None: def set_messenger_callback(self, callback) -> None:
""" """
Setter to define the callback function called when new messages arrive. Setter to define the callback function called when new messages arrive.
@ -186,74 +216,103 @@ class WebPicDownloader(Thread):
""" """
self.__callbacks['messenger'] = callback self.__callbacks['messenger'] = callback
def start_downloading(self, url: str, name: str) -> None: def start_downloading(self, url: str, name: str) -> None:
""" """
TODO desc Start downloading all pictures of a website.
* :url: -> The url of the website to annalyse.
* :folder_name: -> The name of the folder in which to upload the photos.
""" """
if self.__dl_infos.get('running'): if not self.is_alive:
print("bussy") self.__msg("Opss, the download thread is not running, please restart webpic.", MessageType.ERROR)
elif self.__dl_infos.get('running'):
self.__msg("Opss, the download thread is busy.", MessageType.ERROR)
else: else:
self.__dl_infos['website_url'] = url self.__dl_infos['website_url'] = url
self.__dl_infos['download_name'] = name self.__dl_infos['download_name'] = name
self.__sem.release() self.__sem.release()
def stop_downloading(self, block=False) -> None: def stop_downloading(self, block=False) -> None:
""" """
TODO DESC Stops the download after the current item is processed and exit the downloading thread.
<!> Attention once called it will not be possible any more to download. <!>
* :block: -> If true, the function will block until the worker has finished working, if
False(default value), the stop message will be thrown and the program will continue.
""" """
self.__exit = True self.__exit = True
self.__sem.release() self.__sem.release()
if block: if block:
self.join() self.join()
def is_download_running(self) -> bool:
"""
Indique si un téléchargement est en cours
* RETURN -> True if yes, False else.
"""
return self.__dl_infos['running'];
# Thread corp function # Thread corp function
def run(self) -> None: def run(self) -> None:
while True: while True:
self.__sem.acquire() self.__sem.acquire() # waiting the authorization to process
if self.__exit: if self.__exit: # check if the exiting is requested
return return
self.__dl_infos['running'] = True # reserv run self.__dl_infos['running'] = True # indicate that the thread is busy
try: try:
# parse infos from url
html = self.__get_html(self.__dl_infos.get('website_url')) # website html html = self.__get_html(self.__dl_infos.get('website_url')) # website html
images = self.__find_all_img(html) # find all img balises ing html images = self.__find_all_img(html) # find all img balises ing html
self.__dl_infos['tot_image_count'] = len(images) # count total image # setting up download informaations
self.__dl_infos['dl_image_count'] = 0 # set download count to 0 tot_count = len(images) # count total image
dl_count = 0 # set download count to 0
self.__dl_infos['download_path'] = f"{self.__settings.get('root_path')}/{self.__dl_infos.get('download_name')}/" # format path self.__dl_infos['download_path'] = f"{self.__settings.get('root_path')}/{self.__dl_infos.get('download_name')}/" # format path
# init working directory
self.__initialize_folder(self.__dl_infos.get('download_path')) # Init download folder self.__initialize_folder(self.__dl_infos.get('download_path')) # Init download folder
self.__msg(f"WebPicDownloader found {self.__dl_infos.get('tot_image_count')} images on the website.") self.__msg(f"WebPicDownloader found {tot_count} images on the website.")
# process pictures # start images processing
for i, img in enumerate(images): for i, img in enumerate(images):
try: try:
self.__msg(f"Start downloading image {i}.") self.__msg(f"Start downloading image {i}.")
img_link = self.__find_img_link(img) # find image link img_link = self.__find_img_link(img) # find image link
self.__download_img(img_link, f"{self.__dl_infos.get('download_path')}image-{i}.{self.__find_image_type(img_link)}") # download the image self.__download_img(img_link, f"{self.__dl_infos.get('download_path')}image-{i}.{self.__find_image_type(img_link)}") # download the image
self.__msg(f"Download of image {i}, done!") self.__msg(f"Download of image {i}, done!")
self.__dl_infos['dl_image_count'] += 1 # increment download counter dl_count += 1 # increment download counter
except Exception as err: except Exception as err:
self.__msg(f"ERROR: Unable to process image {i} -> err[{err}].") self.__msg(f"ERROR: Unable to process image {i} -> err[{err}].")
self.__msg(f"WebPicDownloader has processed {self.__dl_infos.get('dl_image_count')} images out of {self.__dl_infos.get('tot_image_count')}.") # end images processing
self.__msg(f"WebPicDownloader has processed {dl_count} images out of {tot_count}.")
self.__callbacks.get('success')() # success, launch callback self.__callbacks.get('success')() # success, launch callback
except Exception as err: except Exception as err:
self.__msg(f"ERROR: An error occured -> err[{err}]") self.__msg(f"ERROR: An error occured -> err[{err}]")
self.__callbacks.get('failure')() # error, launch callback self.__callbacks.get('failure')() # error, launch callback
self.__dl_infos['running'] = False # free run
self.__dl_infos['running'] = False # inficate that the thread is free
if __name__ == "__main__": if __name__ == "__main__":
# Internal entry point for testing and consol use. # Internal entry point for testing and consol use.
wpd = WebPicDownloader() wpd = WebPicDownloader()
def lol(msg):
pass
wpd.set_messenger_callback(lol)
while True: while True:
url = input("Website URL ? ") url = input("Website URL ? ")
name = input("Folder name ? ") name = input("Folder name ? ")
wpd.start_downloading(url, name) wpd.start_downloading(url, name)
if "n" == input("Do you want to continue [Y/n] ? ").lower(): if "n" == input("Do you want to continue [Y/n] ? ").lower():
wpd.stop_downloading()
break break
wpd.stop_downloading(block=True)
print("Good bye !") print("Good bye !")

View File

@ -3,23 +3,26 @@ import tkinter.font as tfont
from tkinter import ttk from tkinter import ttk
from tkinter import scrolledtext as tst from tkinter import scrolledtext as tst
from controller.HomeController import HomeController from controller.HomeController import HomeController
from view.MainWindow import MainWindow
class HomeView(ttk.Frame): class HomeView(ttk.Frame):
""" """
View - MainWindow View - HomeWindow
dec... This view allows you to start the scraping/downloading process,
as well as to display the progress of the process.
@author Jérémi Nihart / EndMove @author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader @link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0 @version 1.0.1
@since 2022-08-30 @since 2022-09-05
""" """
# Variables # Variables
__controller: HomeController = None __controller: HomeController = None
# Constructor # Constructor
def __init__(self, parent, controller: HomeController): def __init__(self, parent: MainWindow, controller: HomeController):
""" """
Constructor Constructor
@ -28,14 +31,14 @@ class HomeView(ttk.Frame):
""" """
super().__init__(parent) super().__init__(parent)
# Save and setup main controller
self.__controller = controller
controller.set_view(self)
# Init view # Init view
self.__init_content() self.__init_content()
# START Internal function # Save and setup controller
self.__controller = controller
controller.set_view(self)
# START Internal functions
def __init_content(self) -> None: def __init_content(self) -> None:
""" """
[internal function] [internal function]
@ -61,6 +64,7 @@ class HomeView(ttk.Frame):
self.log_textarea = tst.ScrolledText(self, font=log_textarea_font, wrap=tk.WORD, state=tk.DISABLED, width=40, height=8)#, font=("Times New Roman", 15)) self.log_textarea = tst.ScrolledText(self, font=log_textarea_font, wrap=tk.WORD, state=tk.DISABLED, width=40, height=8)#, font=("Times New Roman", 15))
self.log_textarea.grid(row=3, column=0, columnspan=2, sticky=tk.EW, pady=10, padx=10) self.log_textarea.grid(row=3, column=0, columnspan=2, sticky=tk.EW, pady=10, padx=10)
# Message state
self.message_label = ttk.Label(self, text='message label') self.message_label = ttk.Label(self, text='message label')
# Download button # Download button
@ -73,13 +77,15 @@ class HomeView(ttk.Frame):
=> Function called when a download is requested. => Function called when a download is requested.
""" """
self.__controller.on_download_requested(self.web_entry.get(), self.name_entry.get()) self.__controller.on_download_requested(self.web_entry.get(), self.name_entry.get())
# END Internal function # END Internal functions
# START Controller methods # START Controller methods
def add_log(self, line: str) -> None: def add_log(self, line: str) -> None:
""" """
[function for controller] [function for controller]
TODO desc => Add a log in the textarea where the logs are displayed.
* :line: -> Log message to add.
""" """
self.log_textarea.configure(state=tk.NORMAL) self.log_textarea.configure(state=tk.NORMAL)
self.log_textarea.insert(tk.END, f"~ {line}\n") self.log_textarea.insert(tk.END, f"~ {line}\n")
@ -89,7 +95,7 @@ class HomeView(ttk.Frame):
def clear_logs(self) -> None: def clear_logs(self) -> None:
""" """
[function for controller] [function for controller]
TODO desc => Clean the textarea where the logs are displayed.
""" """
self.log_textarea.configure(state=tk.NORMAL) self.log_textarea.configure(state=tk.NORMAL)
self.log_textarea.delete('1.0', tk.END) self.log_textarea.delete('1.0', tk.END)
@ -98,25 +104,42 @@ class HomeView(ttk.Frame):
def show_error_message(self, message) -> None: def show_error_message(self, message) -> None:
""" """
[function for controller] [function for controller]
TODO desc => Display an error message on the interface.
* :message: -> Message to display.
""" """
self.message_label.configure(text=message, foreground='red') self.message_label.configure(text=message, foreground='red')
self.message_label.grid(row=4, column=0, columnspan=2, sticky=tk.NS, padx=2, pady=2) self.message_label.grid(row=4, column=0, columnspan=2, sticky=tk.NS, padx=2, pady=2)
self.message_label.after(30000, self.hide_message) self.message_label.after(25000, self.hide_message)
def show_success_message(self, message) -> None: def show_success_message(self, message) -> None:
""" """
[function for controller] [function for controller]
TODO desc => Display a success message on the interface.
* :message: -> Message to display.
""" """
self.message_label.configure(text=message, foreground='green') self.message_label.configure(text=message, foreground='green')
self.message_label.grid(row=4, column=0, columnspan=2, sticky=tk.NS, padx=2, pady=2) self.message_label.grid(row=4, column=0, columnspan=2, sticky=tk.NS, padx=2, pady=2)
self.message_label.after(30000, self.hide_message) self.message_label.after(25000, self.hide_message)
def hide_message(self) -> None: def hide_message(self) -> None:
""" """
[function for controller] [function for controller and this view]
TODO desc => Hide the message on the interface.
""" """
self.message_label.grid_forget() self.message_label.grid_forget()
def set_interface_state(self, disable: bool) -> None:
"""
[function for controller]
=> Allows to change the status of the interface with which the user
interacts by activating/deactivating it.
* :disabled: -> True: interface disabled, False: interface enabled.
"""
state = tk.DISABLED if disable else tk.NORMAL
self.web_entry['state'] = state
self.name_entry['state'] = state
self.download_button['state'] = state
# END Controller methods # END Controller methods

View File

@ -1,7 +1,10 @@
import tkinter as tk import tkinter as tk
from tkinter import font
from tkinter import ttk from tkinter import ttk
from controller.Frames import Frames from controller.Frames import Frames
from controller.InfoController import InfoController from controller.InfoController import InfoController
from view.MainWindow import MainWindow
class InfoView(ttk.Frame): class InfoView(ttk.Frame):
""" """
@ -18,7 +21,7 @@ class InfoView(ttk.Frame):
__controller: InfoController = None __controller: InfoController = None
# Constructor # Constructor
def __init__(self, parent, controller: InfoController): def __init__(self, parent: MainWindow, controller: InfoController):
""" """
Constructor Constructor
@ -27,33 +30,72 @@ class InfoView(ttk.Frame):
""" """
super().__init__(parent) super().__init__(parent)
# create widgets # Init view
# label self.__init_content()
self.label = ttk.Label(self, text='Email:')
self.label.grid(row=1, column=0)
# email entry # Save and setup controller
# self.email_var = tk.StringVar()
# self.email_entry = ttk.Entry(self, textvariable=self.email_var, width=30)
# self.email_entry.grid(row=1, column=1, sticky=tk.NSEW)
# save button
self.save_button = ttk.Button(self, text='just a button', command=self.event_btn)
self.save_button.grid(row=1, column=3, padx=10)
# message
self.message_label = ttk.Label(self, text='Je suis super man comment allez vous heheheh je suis toutou', foreground='red')
self.message_label.grid(row=2, column=0, sticky=tk.EW)
# place this frame
# self.grid(row=0, column=0, padx=5, pady=5, sticky=tk.NSEW)
# self.pack(fill='both', expand=True)
# Save and setup main controller
self.__controller = controller self.__controller = controller
controller.set_view(self) controller.set_view(self)
def event_btn(self) -> None: # START Internal functions
print("you clicked on the button that is on the info view!") def __init_content(self) -> None:
print("got redirected to the home view :D") """
self.__controller.on_change_view(Frames.Home) [internal function]
=> Initialize the view content.
"""
self.columnconfigure(0, weight=4)
# Back button
self.back_button = ttk.Button(self, text="Back", command=self.__event_button_back)
self.back_button.grid(row=0, column=0, sticky=tk.E, padx=5, pady=5, ipadx=1, ipady=1)
# About title
self.title_label_font = font.Font(self, size=16, weight=font.BOLD)
self.title_label = ttk.Label(self, text="A title", font=self.title_label_font)
self.title_label.grid(row=1, column=0, sticky=tk.NS, padx=2, pady=2)
# About content
self.content_label_font = font.Font(self, size=10)
self.content_label = ttk.Label(self, wraplength=400, justify='center', text='A long text', font=self.content_label_font, foreground='blue')
self.content_label.grid(row=2, column=0, sticky=tk.NS)
# About version
self.version_label = ttk.Label(self, text='version : 1.0.0 - 02-02-2022')
self.version_label.grid(row=3, column=0, sticky=tk.NS, pady=15)
def __event_button_back(self) -> None:
"""
[internal function]
=> Function called when back button pressed.
"""
self.__controller.on_change_view(Frames.HOME)
# END Internal functions
# START Controller methods
def set_title(self, title: str) -> None:
"""
[function for controller]
=> Define view/page info : title
* :title: -> Title for the view.
"""
self.title_label.configure(text=title)
def set_content(self, content: str) -> None:
"""
[function for controller]
=> Define view/page info : content
* :content: -> Content for the view.
"""
self.content_label.configure(text=content)
def set_version(self, version: str) -> None:
"""
[function for controller]
=> Define view/page info : version
* :version: -> Version for the view.
"""
self.version_label.configure(text=version)
# END Controller methods

View File

@ -1,5 +1,8 @@
import os
import sys
import tkinter as tk import tkinter as tk
from tkinter import messagebox from tkinter import messagebox
from controller.Frames import Frames
from controller.MainController import MainController from controller.MainController import MainController
@ -7,7 +10,8 @@ class MainWindow(tk.Tk):
""" """
View - MainWindow View - MainWindow
TODO dec... This view is the main view of the application, it manages the different frames/views
of the application, captures the events to send them to the main controller.
@author Jérémi Nihart / EndMove @author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader @link https://git.endmove.eu/EndMove/WebPicDownloader
@ -47,8 +51,9 @@ class MainWindow(tk.Tk):
=> Initialize window parameters => Initialize window parameters
""" """
# self.title('My tkinter app') # self.title('My tkinter app')
# self.geometry('450x250') self.geometry('430x310')
self.resizable(False, False) self.resizable(False, False)
self.iconbitmap(f"{self.__controller.get_config('sys_directory')}\\assets\logo.ico")
# self.config(bg='#f7ef38') # self.config(bg='#f7ef38')
def __init_top_menu(self) -> None: def __init_top_menu(self) -> None:
@ -99,7 +104,7 @@ class MainWindow(tk.Tk):
""" """
self.title(title) self.title(title)
def show_frame(self, frame) -> None: def show_frame(self, frame: Frames) -> None:
""" """
[function for app & controller] [function for app & controller]
=> Allows to display the selected frame provided that it => Allows to display the selected frame provided that it
@ -125,20 +130,23 @@ class MainWindow(tk.Tk):
def show_question_dialog(self, title: str, message: str, icon: str='question') -> bool: def show_question_dialog(self, title: str, message: str, icon: str='question') -> bool:
""" """
[function for controller] [function for controller]
=> TODO DESC => Display a question dialog to the user, which he can answer with yes or no.
* :message: -> * :title: -> Title of the dialogue.
* RETURN -> * :message: -> Message of the dialogue displayed to the user.
* :icon: -> Icon of the dialogue displayed to the user.
* RETURN -> True id the user selected yes, False else.
""" """
return messagebox.askquestion(title, message, icon=icon) return True if (messagebox.askquestion(title, message, icon=icon) == "yes") else False
def show_information_dialog(self, title: str, message: str, icon: str='information') -> None: def show_information_dialog(self, title: str, message: str, icon: str='information') -> None:
""" """
[function for controller] [function for controller]
=> TODO DESC => Display an information dialog to the user.
* :message: -> * :title: -> Title of the dialogue.
* RETURN -> * :message: -> Message of the dialogue displayed to the user.
* :icon: -> Icon of the dialogue displayed to the user.
""" """
messagebox.showinfo(self, title, message, icon=icon) messagebox.showinfo(self, title, message, icon=icon)
# END Controller methods # END Controller methods