Compare commits

...

23 Commits

Author SHA1 Message Date
5f319e39b7
fix readme issue referencement 2022-09-12 22:41:11 +02:00
f47646c66b
Update readme 2022-09-12 22:33:56 +02:00
b46ecacbd9
Fix bugs, add update system + tests + fix deployment/build system 2022-09-12 00:29:39 +02:00
981a11ce83
Added unit tests, created functions to check the availability of updates. 2022-09-11 13:38:36 +02:00
6b26b89c69
Restructuration + init test system 2022-09-11 11:52:41 +02:00
a3dd6698fc
Start writing readme.md 2022-09-06 21:41:03 +02:00
87ab654f55
fixing webpic internal run + add executable metadata 2022-09-06 14:30:04 +02:00
25f10a8c50
Open program in the center of the window at launch 2022-09-06 12:59:37 +02:00
f8f7832dd7
Major bug fixes, optimization + major advance 2022-09-05 23:13:38 +02:00
539b75cb09
The thread system not being sufficient remodulation of it 2022-09-05 16:14:05 +02:00
63e67772a3
Patch WebPicDownloading tool -> multithread integration 2022-09-04 22:29:30 +02:00
27b05b6184
Trying ro fix multi-threading issue 2022-09-04 17:38:45 +02:00
03a6d9b54f
Fixed the program stop bug. Adapted webpic script for multi-tasking + added comments 2022-09-04 15:28:28 +02:00
7002439532
Added propagation of the program stop event 2022-09-01 22:07:14 +02:00
69db2dbbee
Add readme + exe builder 2022-09-01 20:51:00 +02:00
7bc1157ffb
Ann top menu event + manage on quit event request 2022-09-01 18:05:58 +02:00
5c6609ff0a
fixing bugs with the textarea and background task 2022-09-01 11:47:34 +02:00
03e35c18a9
Minor advance 2022-08-31 23:53:24 +02:00
a93a88d344
Major advance + start adapting my webpic script for a graphic version 2022-08-31 21:05:20 +02:00
16794bf488
Fixing bugs & start creating interface 2022-08-31 12:07:43 +02:00
dce1b2f9a5
Frame switching system done 2022-08-30 21:12:04 +02:00
746b40fa18
fixing multi frame switching 2022-08-30 13:51:50 +02:00
e85b021383
Creation Webpic script + start creating graphic interface 2022-08-30 12:28:59 +02:00
21 changed files with 1949 additions and 3 deletions

406
.gitignore vendored
View File

@ -1,5 +1,5 @@
# Created by https://www.toptal.com/developers/gitignore/api/python ### Build ###
# Edit at https://www.toptal.com/developers/gitignore?templates=python app_version_file.txt
### Python ### ### Python ###
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
@ -13,6 +13,7 @@ __pycache__/
# Distribution / packaging # Distribution / packaging
.Python .Python
build/ build/
output/
develop-eggs/ develop-eggs/
dist/ dist/
downloads/ downloads/
@ -163,4 +164,403 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# End of https://www.toptal.com/developers/gitignore/api/python ### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
### VisualStudio Patch ###
# Additional files built by Visual Studio
# End of https://www.toptal.com/developers/gitignore/api/python,visualstudio

108
README.md Normal file
View File

@ -0,0 +1,108 @@
# WebPicDownloader
[![Donate][link-icon-coffee]][link-paypal-me] [![Website][link-icon-website]][link-website]
[link-icon-coffee]: https://img.shields.io/badge/%E2%98%95-Buy%20me%20a%20cup%20of%20coffee-991481.svg
[link-paypal-me]: https://www.paypal.me/EndMove/2.5eur
[link-icon-website]: https://img.shields.io/badge/%F0%9F%92%BB-My%20Web%20Site-0078D4.svg
[link-website]: https://www.endmove.eu/
## What is webpicdownloader ?
WebPicDownloader is a scraping tool that allows you to download all the images of a website. Basically WebPic is a Python script around which a graphical interface has been added to make it easier to use.
You will find [here](#windows-application) utility information to use the Windows application `WebPicDownloader.exe`. And [here](#use-python-script) information to use or implement the Python script `WebPicDownloader.py` in your application (without the graphical interface).
## Windows application
To use WebPic on windows nothing more simple, download the executable `.exe` of the last [release here](https://git.endmove.eu/EndMove/WebPicDownloader/releases) (be careful to download the latest release and not a pre-release).
Execute the file ``WebPicDownloader.exe`` and enjoy it! 👌
## Use Python script
To start, find the script to use or to add to your code [here](webpicdownloader/model/WebPicDownloader.py).
### CLI Run Requirements
To use the script check the following prerequisites.
* Python `>= 3.10.6` ;
* beautifulsoup4 `>= 4.11.1` ;
* bs4 (BeautifulSoup) `>= 0.0.1` ;
* urllib3 `>= 1.26.12` ;
### Console Use ?
If you just want to use the console version of the script without the built-in GUI then you just need to check the [prerequisites](#cli-run-requirements) and run the script as follows:
```python
python3 WebPicDownloader.py
```
### Integrate to your code ?
First of all you have to know that WebPicDownloader has a deamon worker that downloads all the images asynchronously (this allows you not to block your program when a download is in progress). This same worker will be automatically killed as soon as your program finishes. WebPicDownloader therefore provides a blocking stop function allowing you to wait for the end of the download. See the information below. The prerequisites are the same as if you were running the script from the command line, see [prerequisites](#cli-run-requirements).
#### Step 1
Instantiate your WebPicDownloader object like this:
```python
from WebPicDownloader import WebPicDownlodaer, MessageType
webpic = WebPicDownloader()
```
The constructor can take several parameters (`path: str, headers: dict, messenger, success, failure`) (see the documentation).
#### Step 2
Define the WebPicDownloader callback functions. There are 3 main ones, the first (messenger callback) will be called at each system event and takes the following parameters (`message: str, type: MessageType`). The second (success callback) will be called at the end of processing if no major errors occur, it takes the following parameters (`message: str`). The third and last function (failure callback) will be called if a major error occurs or the download fails, it takes the following parameter (`message: str`).
By default, these functions print their results with a simple `print(message)` in the console. In case you implement WebPicDownloader in a graphical program, you should by convention remove all printing from your application and therefore define your own callback functions for WebpicDownloder. Below is an example:
```python
from WebPicDownloader import WebPicDownlodaer, MessageType
# Consider instantiating before the main loop of your program is launched.
webpic = WebPicDownloader()
# Pay attention to the signature of the functions
webpic.set_success_callback(lambda message: print(f"Success ! [{message}]."))
webpic.set_failure_callback(lambda message: print(f"Success ! [{message}]."))
webpic.set_messenger_callback(lambda message, msg_type: print(f"[{msg_type}]: {message}."))
```
#### Step 3
Once WebPicDownloader instantiated and the callback functions configured, we have to launch the download and stop it. It is important to know that the script does not have a function to stop a download in progress, in fact the stop function will allow you to wait for the end of the download and then turn off the program or to kill the worker automatically when the main thread dies.
```python
from time import sleep
from WebPicDownloader import WebPicDownlodaer, MessageType
webpic = WebPicDownloader()
# ... callbacks ...
# Webpic will give the task to its worker and start downloading the images
webpic.start_downloading('https://www.endmove.eu/', 'EndMove-website-images')
# We wait for the worker to start the task (once the task has started it cannot be stopped)
sleep(1)
# Webpic will ask the program to stop in blocking mode (it will join the worker to wait for the end of its execution)
webpic.stop_downloading(True)
```
## Improvement (TODO LIST)
Here you will find some improvements I would like to add to the program, you can also participate by forking the repository and submitting a pull request.
- [x] Check for updates button.
- [ ] Integrated file explorer.
- [ ] Viewing the downloads already made.
- [ ] Redo WebPicDownlodaer script to support concurrent downloads, to be able to launch workers and share tasks via a download pool.
This program is only a free utility tool and has not been developed in depth. In a future version it would be interesting to manage concurrent downloads in a thread pool.

1
VERSION Normal file
View File

@ -0,0 +1 @@
1.0.0

7
app_metadata.yml Normal file
View File

@ -0,0 +1,7 @@
Version: 1.0.0
CompanyName: EndMove Corp.
FileDescription: Scraping tool to recover all the images of a website.
InternalName: webpic
LegalCopyright: © Copyright 2022 EndMove. All rights reserved.
OriginalFilename: WebPicDownloader.exe
ProductName: WebPicDownloader

101
build_config.json Normal file
View File

@ -0,0 +1,101 @@
{
"version": "auto-py-to-exe-configuration_v1",
"pyinstallerOptions": [
{
"optionDest": "noconfirm",
"value": true
},
{
"optionDest": "filenames",
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/main.py"
},
{
"optionDest": "onefile",
"value": true
},
{
"optionDest": "console",
"value": false
},
{
"optionDest": "icon_file",
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/webpicdownloader/assets/logo.ico"
},
{
"optionDest": "name",
"value": "WebPicDownloader"
},
{
"optionDest": "ascii",
"value": false
},
{
"optionDest": "clean_build",
"value": true
},
{
"optionDest": "strip",
"value": false
},
{
"optionDest": "noupx",
"value": false
},
{
"optionDest": "disable_windowed_traceback",
"value": false
},
{
"optionDest": "version_file",
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/app_version_file.txt"
},
{
"optionDest": "embed_manifest",
"value": true
},
{
"optionDest": "uac_admin",
"value": false
},
{
"optionDest": "uac_uiaccess",
"value": false
},
{
"optionDest": "win_private_assemblies",
"value": false
},
{
"optionDest": "win_no_prefer_redirects",
"value": false
},
{
"optionDest": "bootloader_ignore_signals",
"value": false
},
{
"optionDest": "argv_emulation",
"value": false
},
{
"optionDest": "datas",
"value": "C:/Users/super/Documents/Developpement/Python/WebPicDownloader/webpicdownloader/assets;webpicdownloader/assets/"
},
{
"optionDest": "excludes",
"value": "auto_py_to_exe"
},
{
"optionDest": "excludes",
"value": "pyinstaller"
},
{
"optionDest": "excludes",
"value": "unittest"
}
],
"nonPyinstallerOptions": {
"increaseRecursionLimit": true,
"manualArguments": ""
}
}

16
build_tool.py Normal file
View File

@ -0,0 +1,16 @@
import pyinstaller_versionfile
from auto_py_to_exe import __main__ as apte
from main import get_config
"""
This file allows you to start auto-py in order to quickly and easily build the solution
into a viable .exe. Moreover this script will compile the meta data for windows.
"""
if __name__ == '__main__':
pyinstaller_versionfile.create_versionfile_from_input_file(
output_file="app_version_file.txt",
input_file="app_metadata.yml",
version=get_config().get('app_version')
)
apte.__name__ = '__main__'
apte.run()

78
main.py Normal file
View File

@ -0,0 +1,78 @@
import os
import re
import sys
from webpicdownloader.model.WebPicDownloader import WebPicDownloader
from webpicdownloader.controller.HomeController import HomeController
from webpicdownloader.controller.InfoController import InfoController
from webpicdownloader.controller.MainController import MainController
from webpicdownloader.controller.Frames import Frames
from webpicdownloader.view.HomeView import HomeView
from webpicdownloader.view.InfoView import InfoView
from webpicdownloader.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:
"""
Retrieve the application configuration
"""
return {
'app_name': 'WebPicDownloader',
'app_folder': os.getcwd(),
'app_version': '1.0.0', # This version must match with the version.txt at root
'app_version_date': '2022-09-11',
'app_depo_version': 'https://git.endmove.eu/EndMove/WebPicDownloader/raw/branch/master/VERSION',
'app_depo_releases': 'https://git.endmove.eu/EndMove/WebPicDownloader/releases',
'sys_version_matcher': re.compile(r'^(\d{1,2}\.)(\d{1,2}\.)(\d{1,2})$'),
'sys_directory': get_sys_directory(),
'about_title': 'About WebPicDownloader',
'about_content': "This scraping software has been developed by EndMove\nand is fully open-source. The source code is available\nhere: https://git.endmove.eu/EndMove/WebPicDownloader\nEndMove is available at the following address for any\nrequest contact@endmove.eu. In case of problemsplease\nopen an issue on the repository.\n\nThe logo of the software was made by Gashila"
}
if __name__ == '__main__':
"""
WebPicDownloader is a program developed and maintened by EndMove under Apache 2.0 License. Stealing code is a crime !
Disclamer : The developer of this application can in no way be held responsible if his application is used for illegal purposes.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-08-30
"""
# configuration
config = get_config()
# Create utli/model
webpic = WebPicDownloader(path=config.get('app_folder'))
# Create app controllers
main_controller = MainController(config)
home_controller = HomeController(main_controller, webpic)
info_controller = InfoController(main_controller)
# Create app views
main_window = MainWindow(main_controller)
home_view = HomeView(main_window, home_controller)
info_controller = InfoView(main_window, info_controller)
# Add views to main window
main_window.add_view(Frames.HOME, home_view)
main_window.add_view(Frames.INFO, info_controller)
# Choose the launching view
main_window.show_frame(Frames.HOME)
# Start main windows looping (launch program)
main_window.mainloop()

11
run_tests.py Normal file
View File

@ -0,0 +1,11 @@
import unittest
"""
This file allows you to launch all the unit tests of the project.
Be careful these tests require internet.
"""
loader = unittest.TestLoader()
runner = unittest.TextTestRunner()
runner.run(loader.discover('./tests/'))

0
tests/__init__.py Normal file
View File

View File

@ -0,0 +1,48 @@
import unittest
from re import fullmatch, compile, Pattern
class TestUpdateRegex(unittest.TestCase):
"""
Test Class for Update Regex
Versions can vary between 0.0.0 and 99.99.99 and cannot exceed two units.
0.0.0 to 0.99.99 = BETA
1.0.0 to 99.99.99 = RELEASE
"""
regex: Pattern = compile(r'^(\d{1,2}\.)(\d{1,2}\.)(\d{1,2})$')
def test_first_version(self):
self.assertTrue(fullmatch(self.regex, '1.0.0'))
def test_patch_version(self):
self.assertTrue(fullmatch(self.regex, '1.0.00'))
self.assertTrue(fullmatch(self.regex, '1.0.1'))
self.assertTrue(fullmatch(self.regex, '1.0.12'))
self.assertTrue(fullmatch(self.regex, '1.0.54'))
def test_minor_version(self):
self.assertTrue(fullmatch(self.regex, '1.00.0'))
self.assertTrue(fullmatch(self.regex, '1.1.0'))
self.assertTrue(fullmatch(self.regex, '1.98.0'))
self.assertTrue(fullmatch(self.regex, '1.24.0'))
def test_major_version(self):
self.assertTrue(fullmatch(self.regex, '00.0.0'))
self.assertTrue(fullmatch(self.regex, '10.0.0'))
self.assertTrue(fullmatch(self.regex, '99.0.0'))
self.assertTrue(fullmatch(self.regex, '42.0.0'))
def test_bad_version(self):
self.assertFalse(fullmatch(self.regex, '1.0.0000'))
self.assertFalse(fullmatch(self.regex, '1.00000.0'))
self.assertFalse(fullmatch(self.regex, '100.0.0'))
self.assertFalse(fullmatch(self.regex, '152.124.15'))
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,74 @@
import unittest
from urllib.error import HTTPError, URLError
from test_update_regex import TestUpdateRegex
from webpicdownloader.util.UpdateUtils import fetch_version, check_for_update
class TestUpdateUtils(unittest.TestCase):
"""
Test Class for Update Utils
<!> can not be executed directly <!>
* regex -> use UpdateRegex unittest regex.
* good_url -> use a permalink VERSION file.
* bad_url -> bad link that point a 404 error.
"""
regex = TestUpdateRegex.regex
good_url = 'https://git.endmove.eu/EndMove/WebPicDownloader/raw/commit/6b26b89c6901841faaa09154c185d202223492c2/VERSION'
bad_url = 'https://git.endmove.eu/EndMove/WebPicDownloader/raw/commit/bad-commit/VERSION'
def test_fetch_version__good_url(self):
"""
We test the recovery of the available version with a URL
indicating a correct version file.
"""
self.assertTrue(fetch_version(self.good_url) == '1.0.0')
def test_fetch_version__bad_url(self):
"""
We test the recovery of the version with erroneous URLs,
or not leading to any specific file
"""
self.assertRaises(HTTPError, lambda: fetch_version(self.bad_url))
self.assertRaises(URLError, lambda: fetch_version('https://bad'))
self.assertRaises(ValueError, lambda: fetch_version('bad'))
def test_check_for_update__new_version(self):
"""
We check the availability of an update with softwares whose
versions are different from the one indicated by the repository
(thus available update).
"""
self.assertTrue(check_for_update('0.0.0', self.good_url, self.regex))
self.assertTrue(check_for_update('0.48.0', self.good_url, self.regex))
self.assertTrue(check_for_update('0.48.14', self.good_url, self.regex))
self.assertTrue(check_for_update('5.48.14', self.good_url, self.regex))
def test_check_for_update__no_new_version(self):
"""
We check the availability of an update with software that has the
same version as the one on the repository (so no update available).
"""
self.assertFalse(check_for_update('1.0.0', self.good_url, self.regex))
def test_check_for_update__bad_version(self):
"""
We check for the availability of an update with an incorrect version
in the repository.
"""
self.assertRaises(ValueError, lambda: check_for_update(
'1.0.0',
'https://git.endmove.eu/EndMove/WebPicDownloader/src/commit/6b26b89c6901841faaa09154c185d202223492c2/app_version_file.txt',
self.regex
))
def test_check_for_update__bad_url(self):
"""
We check the availability of an update with a bad repository url
"""
self.assertRaises(HTTPError, lambda: check_for_update('1.0.0', self.bad_url, self.regex))

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@ -0,0 +1,18 @@
from enum import Enum
class Frames(Enum):
"""
Enumeration - Frames
Lists the different windows of the program in order to facilitate
their call during the execution. Each parameter of the enumeration
represents a Frame/Tab.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-08-30
"""
HOME = 1 # Home view
INFO = 2 # Info & copyright view

View File

@ -0,0 +1,118 @@
from webpicdownloader.controller.MainController import MainController
from webpicdownloader.model.WebPicDownloader import MessageType, WebPicDownloader
class HomeController:
"""
Controller - HomeController
This controller handles all the interaction directly related to the download.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-08-30
"""
# Variables
__main_controller: MainController = None
__view = None
__webpic: WebPicDownloader = None
# Constructor
def __init__(self, controller: MainController, webpic: WebPicDownloader) -> None:
"""
Constructor
* :controller: -> The main application cpntroller.
* :webpic: -> The webpicdownloader instance.
"""
# Setub variables
self.__main_controller = controller
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
controller.subscribe_to_quite_event(self.on_quit)
# START View methods
def set_view(self, view) -> None:
"""
[function for view]
=> Define the view of this controller.
* :view: -> The view that this controller manage.
"""
self.__view = view
# END View method
# START View events
def on_download_requested(self, url: str, name: str) -> None:
"""
[event function for view]
=> Call this event method when the user requests to download
* :url: -> The url of the website to use for pic-download.
* :name: -> The name of the folder in which put pictures.
"""
if url.strip() and name.strip():
self.__view.set_interface_state(True)
self.__view.clear_logs()
self.__webpic.start_downloading(url, name)
else:
self.__view.show_error_message("Opss, the url or folder name are not valid!")
# END View events
# START Webpic events
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)
case MessageType.ERROR:
self.__view.show_error_message(message)
case MessageType.SUCCESS:
self.__view.show_success_message(message)
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.set_interface_state(False)
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.set_interface_state(False)
# END Webpic events
# START Controller methods
def on_quit(self) -> bool:
"""
[event function for controller]
=> Call this event when a request to exit is thrown.
"""
if self.__webpic.is_download_running():
if self.__main_controller.show_question_dialog(
"Are you sure?",
"Do you really want to quit while the download is running?\nThis will stop the download."
):
self.__webpic.stop_downloading() # hot stop deamon
return False
return True
self.__webpic.stop_downloading(block=True)
# END Controller methods

View File

@ -0,0 +1,54 @@
from webpicdownloader.controller.Frames import Frames
from webpicdownloader.controller.MainController import MainController
class InfoController:
"""
Controller - InfoController
This controller manages the display of information in the information view.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-08-30
"""
# Variables
__main_controller: MainController = None
__view = None
# Constructor
def __init__(self, controller: MainController) -> None:
"""
Constructor
* :controller: -> The main application cpntroller.
"""
# Setup variables
self.__main_controller = controller
# START View methods
def set_view(self, view) -> None:
"""
[function for view]
:view: -> The view that this controller manage.
"""
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
# START View events
def on_change_view(self, frame: Frames) -> 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)
# END View events

View File

@ -0,0 +1,142 @@
import os
import webbrowser
from webpicdownloader.controller.Frames import Frames
from webpicdownloader.util.UpdateUtils import check_for_update
class MainController:
"""
Controller - MainController
This controller manages all the main interaction, change of windows,
dialogs, stop... It is the main controller.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.1
@since 2022-09-11
"""
# Variables
__config: dict = None
__view = None
__quite_event_subscribers: list = None
# Constructor
def __init__(self, config: dict) -> None:
"""
Constructor
* :config: -> The application configuration (a dictionary).
"""
# Setup variables
self.__config = config
self.__quite_event_subscribers = []
# START View methods
def set_view(self, view) -> None:
"""
[function for view]
=> Allow to define the controller view.
* :view: -> The view that this controller manage and setup it.
"""
self.__view = view
view.set_window_title(self.get_config('app_name'))
def on_open_folder(self) -> None:
"""
[event function for view]
=> Event launch when you ask to open the current folder.
"""
os.startfile(self.get_config('app_folder')) # Open the file explorer on working dir
def on_quite(self) -> None:
"""
[event function for view]
=> Event launch when you ask to quit the program. This event is propagated
to the subscribers, they can eventually cancel the event
"""
for callback in self.__quite_event_subscribers:
if callback():
return
self.__view.close_window() # End the program
def on_check_for_update(self) -> None:
"""
[event function for view]
=> Event launched when a check for available updates is requested.
"""
try:
if check_for_update(self.get_config('app_version'), self.get_config('app_depo_version'), self.get_config('sys_version_matcher')):
if self.show_question_dialog(self.get_config('app_name'), "An update is available! Would you like to visit the release page to download the latest version?"):
webbrowser.open(self.get_config('app_depo_releases'))
else: self.show_information_dialog(self.get_config('app_name'), "No update available.")
except Exception as e:
self.show_information_dialog(self.get_config('app_name'), f"An error occured: {e}.", 'error')
def on_about(self) -> None:
"""
[event function for view]
=> Event launched when a request for more information arise.
"""
self.change_frame(Frames.INFO)
# END View methods
# START Controller methods
def change_frame(self, frame: Frames) -> None:
"""
[function for controller]
=> Allows you to request a frame change in the main window.
* :frame: -> The frame we want to display on the window instead of the current frame.
"""
self.__view.show_frame(frame)
def get_config(self, name: str):
"""
[function for controller]
=> Allows controllers to access the application's configuration.
* :name: -> The name of the configuration parameter for which we want to access the configured value.
* THROWABLE -> If the key is wrong, throw a ValueError.
"""
if self.__config.get(name):
return self.__config.get(name)
else:
raise ValueError("Unable to find a configuration with this name")
def show_question_dialog(self, title: str='title', message: str='question?', icon: str='question') -> bool:
"""
[function for controller]
=> Ask a question to the user and block until he answers with yes or no.
* :title: -> Title of the dialogue.
* :message: -> Message of the dialogue.
* :icon: -> Icon of the dialogue
"""
return self.__view.show_question_dialog(title, message, icon)
def show_information_dialog(self, title: str='title', message: str='informations!', icon: str='info') -> bool:
"""
[function for controller]
=> Display a pop-up information dialog to the user.
* :title: -> Title of the dialogue.
* :message: -> Message of the dialogue.
* :icon: -> Icon of the dialogue
"""
return self.__view.show_information_dialog(title, message, icon)
# END Controller methods
# START Controller events
def subscribe_to_quite_event(self, callback) -> None:
"""
[function for controller]
=> Subscription function allowing to be warned if a request to quit occurs.
In the case where the callback function returns False the process continues
but if the callback returns True the process is aborted.
* :callback: -> Callback function that will be called when a request to exit occurs.
"""
self.__quite_event_subscribers.append(callback)
# END Controller events

View File

@ -0,0 +1,333 @@
import os
from enum import Enum
from threading import Semaphore, Thread
from urllib import request
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):
"""
WebPicDownloader
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
in an application. It has been designed to be executed in an integrated thread
in an asynchronous way. This tool allows to define 3 callback functions, one for
events, one in case of success and one in case of failure. It also has an
integrated entry point allowing it to be directly executed in terminal mode.
@author EndMove <contact@endmove.eu>
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.2.1
@since 2022-09-05
"""
# Variables
__callbacks: dict = None # Callback dictionary
__settings: dict = None # Webpic basics settings
__dl_infos: dict = None # Download informations
__sem: Semaphore = None # Semaphore for the webpic worker
_exit: bool = None # When set to True quit the thread
# Constructor
def __init__(self, path: str = None, headers: dict = None, messenger = None,
success = None, failure = None) -> None:
"""
Constructor
=> It is important to initialize the WebPicDownloader object properly. The callback
functions can be initialized after the creation of the object.
* :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
of the requests sent by WebPic.
* :messenger: -> Callback function messenger (see setter).
* :success: -> Callback function success (see setter).
* :failure: -> Callback function failure (see setter).
"""
super().__init__(daemon=True, name='WebPic download worker')
self.__callbacks = {
'messenger': messenger if messenger else lambda msg, type: print(msg),
'success': success if success else lambda: print("Success!"),
'failure': failure if failure else lambda: print("failure!")
}
self.__settings = {
'root_path': path if path else os.getcwd(),
'headers': headers if headers else {
'User-Agent': "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
}
}
self.__dl_infos = {
'website_url': 'url',
'download_name': 'name',
'download_path': 'full_path',
'running': False
}
self.__sem = Semaphore(0)
self.__exit = False
self.start() # start deamon
# Internal functions
def __get_html(self, url: str) -> str:
"""
Internal Function #do-not-use#
=> Allow to retrieve the HTML content of a website.
* :url: -> The url of the site for which we want to get the content of the HTML page.
* RETURN -> Web page content.
"""
req = request.Request(url, headers=self.__settings.get('headers'))
response = request.urlopen(req)
return response.read().decode('utf-8')
def __find_all_img(self, html: str) -> ResultSet:
"""
Internal Function #do-not-use#
=> Allow to retrieve all images of an html page.
* :html: -> Html code in which to search for image balises.
* RETURN -> Iterable with all image balises.
"""
soup = BeautifulSoup(html, 'html.parser')
return soup.find_all('img')
def __find_img_link(self, img: Tag) -> str:
"""
Internal Function #do-not-use#
=> Allow to retrieve the link of a picture.
* :img: -> Image tag {@code bs4.Tag} for which to search the link of an image.
* RETURN -> Image link.
"""
if img.get('src'):
link = img.get('src')
elif img.get('data-src'):
link = img.get('data-src')
elif img.get('data-srcset'):
link = img.get('data-srcset')
elif img.get('data-fallback-src'):
link = img.get('data-fallback-src')
else:
raise ValueError("Unable to find image url")
if not 'http' in link:
raise ValueError("Bad image url")
return link
def __find_image_type(self, img_link: str) -> str:
"""
Internal Function #do-not-use#
=> Allow to retrieve the right image type (png, jpeg...)
* :img_link: -> Lien de l'image pourllaquel trouver le bon type.
* RETURN -> Type of image.
"""
type = img_link.split('.')[-1]
if '?' in type:
type = type.split('?')[0]
return type
def __download_img(self, url: str, filename: str) -> None:
"""
Internal Function #do-not-use#
=> Allow to download a picture from internet
* :url: -> Image url on the web.
* :filename: -> Full path with name of the future image.
"""
req = request.Request(url, headers=self.__settings.get('headers'))
raw_img = request.urlopen(req).read()
with open(filename, 'wb') as img:
img.write(raw_img)
def __initialize_folder(self, folder_path: str) -> None:
"""
Internal Function #do-not-use#
=> Checks if the folder in which to place the images to be uploaded exists and if
not chalk it up. An exception is raised if this folder already exists.
* :folder_path: -> Full path to the working folder (for the download task).
"""
if not os.path.exists(folder_path):
os.mkdir(folder_path)
else:
raise ValueError("The folder already exists, it may already contain images")
def __msg(self, message: str, type:MessageType=MessageType.LOG) -> None:
"""
Internal Function #do-not-use#
=> 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, type)
# Public functions
def set_success_callback(self, callback) -> None:
"""
Setter to define the callback function when the download succeeded.
* :callback: -> the callback function to call when the download is a success.
"""
self.__callbacks['success'] = callback
def set_failure_callback(self, callback) -> None:
"""
Setter to define the callback function called when the download fails.
* :callback: -> the callback function to call when the download is a failure.
"""
self.__callbacks['failure'] = callback
def set_messenger_callback(self, callback) -> None:
"""
Setter to define the callback function called when new messages arrive.
* :callback: -> the callback function to call when a message event is emited.
"""
self.__callbacks['messenger'] = callback
def start_downloading(self, url: str, name: str) -> None:
"""
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 not self.is_alive:
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:
self.__dl_infos['website_url'] = url
self.__dl_infos['download_name'] = name
self.__sem.release()
def stop_downloading(self, block=False) -> None:
"""
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.__sem.release()
if block:
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
def run(self) -> None:
while True:
self.__sem.acquire() # waiting the authorization to process
if self.__exit: # check if the exiting is requested
return
self.__dl_infos['running'] = True # indicate that the thread is busy
try:
# parse infos from url
html = self.__get_html(self.__dl_infos.get('website_url')) # website html
images = self.__find_all_img(html) # find all img balises ing html
# setting up download informaations
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
# init working directory
self.__initialize_folder(self.__dl_infos.get('download_path')) # Init download folder
self.__msg(f"WebPicDownloader found {tot_count} images on the website.")
# start images processing
for i, img in enumerate(images):
try:
self.__msg(f"Start downloading image {i}.")
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.__msg(f"Download of image {i}, done!")
dl_count += 1 # increment download counter
except Exception as err:
self.__msg(f"ERROR: Unable to process image {i} -> err[{err}].")
# end images processing
self.__msg(f"WebPicDownloader has processed {dl_count} images out of {tot_count}.")
self.__callbacks.get('success')() # success, launch callback
except Exception as err:
self.__msg(f"ERROR: An error occured -> err[{err}]")
self.__callbacks.get('failure')() # error, launch callback
self.__dl_infos['running'] = False # inficate that the thread is free
if __name__ == "__main__":
# Internal entry point for testing and console use.
import time
wpd = WebPicDownloader() # Instance of webpic
# Callback functions
def success():
print("\nDownload completed with success.")
def failed():
print("\nDownload completed with errors.")
# Set-up callback functions for webpic
wpd.set_success_callback(success)
wpd.set_failure_callback(failed)
# Ask for download
print("\nWelcome to WebPicDownloader!")
url = input("Website URL ? ")
name = input("Folder name ? ")
wpd.start_downloading(url, name) # Start downloading
time.sleep(1) # We wait for the download to start before ask to stop it
wpd.stop_downloading(block=True) # Stop downloading but block till the download end.

View File

@ -0,0 +1,37 @@
from re import fullmatch, Pattern
from urllib import request as http
from urllib.error import HTTPError
def fetch_version(url: str) -> str:
"""
Retrieve the latest webpicdownloader version available.
This method, only accept 200 http response.
* :url: -> Url of the "VERSION" file on the repository.
* RETURN -> the latests 'VERSION' file content.
* THROWABLE -> can raise HTTP or URL error see urllib doc.
"""
request = http.Request(
url=url,
headers={'User-Agent': "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/605.1.15 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/604.1"},
method='GET'
)
response = http.urlopen(request)
if response.getcode() != 200:
raise HTTPError("Bad response returned by server")
return response.read().decode('utf-8')
def check_for_update(version_current: str, version_url: str, version_pattern: Pattern) -> bool:
"""
Verify if a new version is available.
* :current: -> Current version
* :version_url: -> Url of the VERSION file on the repository.
* :version_pattern: -> Patter to match version retrieved with the version_url.
* RETURN -> True means a new version is available, False else.
"""
version = fetch_version(version_url)
if not fullmatch(version_pattern, version):
raise ValueError('The version retrieved from the remote server is not valid')
return version_current != version

View File

@ -0,0 +1,145 @@
import tkinter as tk
import tkinter.font as tfont
from tkinter import ttk
from tkinter import scrolledtext as tst
from webpicdownloader.controller.HomeController import HomeController
from webpicdownloader.view.MainWindow import MainWindow
class HomeView(ttk.Frame):
"""
View - HomeWindow
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
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.1
@since 2022-09-05
"""
# Variables
__controller: HomeController = None
# Constructor
def __init__(self, parent: MainWindow, controller: HomeController):
"""
Constructor
* :parent: -> The main windows container.
* :controller: -> The view controller
"""
super().__init__(parent)
# Init view
self.__init_content()
# Save and setup controller
self.__controller = controller
controller.set_view(self)
# START Internal functions
def __init_content(self) -> None:
"""
[internal function]
=> Initialize the view content.
"""
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=3)
# Website link
self.web_label = ttk.Label(self, text="Website URL:")
self.web_label.grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
self.web_entry = ttk.Entry(self, width=50) # show="-"
self.web_entry.grid(row=0, column=1, sticky=tk.E, padx=5, pady=5, ipadx=2, ipady=2)
# Download name
self.name_label = ttk.Label(self, text="Download Name:")
self.name_label.grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)
self.name_entry = ttk.Entry(self, width=50)
self.name_entry.grid(row=1, column=1, sticky=tk.E, padx=5, pady=5, ipadx=2, ipady=2)
# Logs area
log_textarea_font = tfont.Font(size=10)
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)
# Message state
self.message_label = ttk.Label(self, text='message label')
# Download button
self.download_button = ttk.Button(self, text="Start downloading", command=self.__event_button_download)
self.download_button.grid(row=5, column=1, sticky=tk.E, padx=5, pady=5, ipadx=5, ipady=2)
def __event_button_download(self) -> None:
"""
[internal function]
=> Function called when a download is requested.
"""
self.__controller.on_download_requested(self.web_entry.get(), self.name_entry.get())
# END Internal functions
# START Controller methods
def add_log(self, line: str) -> None:
"""
[function for controller]
=> 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.insert(tk.END, f"~ {line}\n")
self.log_textarea.see(tk.END)
self.log_textarea.configure(state=tk.DISABLED)
def clear_logs(self) -> None:
"""
[function for controller]
=> Clean the textarea where the logs are displayed.
"""
self.log_textarea.configure(state=tk.NORMAL)
self.log_textarea.delete('1.0', tk.END)
self.log_textarea.configure(state=tk.DISABLED)
def show_error_message(self, message) -> None:
"""
[function for controller]
=> Display an error message on the interface.
* :message: -> Message to display.
"""
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.after(25000, self.hide_message)
def show_success_message(self, message) -> None:
"""
[function for controller]
=> Display a success message on the interface.
* :message: -> Message to display.
"""
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.after(25000, self.hide_message)
def hide_message(self) -> None:
"""
[function for controller and this view]
=> Hide the message on the interface.
"""
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

View File

@ -0,0 +1,101 @@
import tkinter as tk
from tkinter import font
from tkinter import ttk
from webpicdownloader.controller.Frames import Frames
from webpicdownloader.controller.InfoController import InfoController
from webpicdownloader.view.MainWindow import MainWindow
class InfoView(ttk.Frame):
"""
View - InfoWindow
This view displays information about the program, as well as its version and release date.
@author Jérémi Nihart / EndMove
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.0
@since 2022-09-06
"""
# Variables
__controller: InfoController = None
# Constructor
def __init__(self, parent: MainWindow, controller: InfoController):
"""
Constructor
* :parent: -> The main windows container.
* :controller: -> The view controller
"""
super().__init__(parent)
# Init view
self.__init_content()
# Save and setup controller
self.__controller = controller
controller.set_view(self)
# START Internal functions
def __init_content(self) -> None:
"""
[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

@ -0,0 +1,154 @@
import tkinter as tk
from tkinter import messagebox
from webpicdownloader.controller.Frames import Frames
from webpicdownloader.controller.MainController import MainController
class MainWindow(tk.Tk):
"""
View - MainWindow
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
@link https://git.endmove.eu/EndMove/WebPicDownloader
@version 1.0.1
@since 2022-09-04
"""
# Variables
__controller: MainController = None
__views: dict = None
__frame_id: int = None
# Constructor
def __init__(self, controller: MainController) -> None:
"""
Constructor
* :controller: -> The main application cpntroller.
"""
super().__init__()
# Init view repository
self.__views = {}
# Save and setup main controller
self.__controller = controller
controller.set_view(self)
# Init view components & more
self.__init_window()
self.__init_top_menu()
self.__init_bind_protocol()
# START Internal methods
def __init_window(self) -> None:
"""
[internal function]
=> Initialize window parameters
"""
self.iconbitmap(f"{self.__controller.get_config('sys_directory')}\\webpicdownloader\\assets\\logo.ico") # App logo
window_width = 430 # App width
window_height = 305 # App height
x_cordinate = int((self.winfo_screenwidth()/2) - (window_width/2))
y_cordinate = int((self.winfo_screenheight()/2) - (window_height/2))
self.geometry(f"{window_width}x{window_height}+{x_cordinate}+{y_cordinate}") # App size and middle centering
self.resizable(False, False) # Disable app resizing
def __init_top_menu(self) -> None:
"""
[internal function]
=> Initialize top menu of the window.
"""
main_menu = tk.Menu(self)
# Top menu File item
col1_menu = tk.Menu(main_menu, tearoff=0)
col1_menu.add_command(label="Open app folder", command=self.__controller.on_open_folder)
col1_menu.add_separator()
col1_menu.add_command(label="Quit", command=self.__controller.on_quite)
main_menu.add_cascade(label="File", menu=col1_menu)
# Top menu Help item
col2_menu = tk.Menu(main_menu, tearoff=0)
col2_menu.add_command(label="Check for update", command=self.__controller.on_check_for_update)
col2_menu.add_command(label="About", command=self.__controller.on_about)
main_menu.add_cascade(label="Help", menu=col2_menu)
self.config(menu=main_menu)
def __init_bind_protocol(self) -> None:
"""
[internal function]
=> Initialize the function bindding on events of the main window.
"""
self.protocol("WM_DELETE_WINDOW", self.__controller.on_quite)
# END Internal methods
# START App methods
def add_view(self, frame, view) -> None:
"""
[function for app]
* :frame: -> the frame id of the view to add.
"""
self.__views[frame] = view
# END App methods
# START Controller methods
def set_window_title(self, title: str) -> None:
"""
[function for controller]
=> Sets the title of the main window.
* :title: -> Window title.
"""
self.title(title)
def show_frame(self, frame: Frames) -> None:
"""
[function for app & controller]
=> Allows to display the selected frame provided that it
has been previously added to the frame dictionary.
* :frame: -> the frame if of the view to display.
"""
if self.__views.get(frame):
if self.__frame_id:
self.__views.get(self.__frame_id).pack_forget()
self.__views.get(frame).pack(fill=tk.BOTH, expand=False)
self.__frame_id = frame
else:
raise ValueError("Unable to find the requested Frame")
def close_window(self) -> None:
"""
[function for controller]
=> Closes the main window and stops the program from the controller.
"""
self.destroy()
def show_question_dialog(self, title: str, message: str, icon: str='question') -> bool:
"""
[function for controller]
=> Display a question dialog to the user, which he can answer with yes or no.
* :title: -> Title of the dialogue.
* :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 True if (messagebox.askquestion(title, message, icon=icon) == "yes") else False
def show_information_dialog(self, title: str, message: str, icon: str='info') -> None:
"""
[function for controller]
=> Display an information dialog to the user.
* :title: -> Title of the dialogue.
* :message: -> Message of the dialogue displayed to the user.
* :icon: -> Icon of the dialogue displayed to the user.
"""
messagebox.showinfo(title, message, icon=icon)
# END Controller methods