├── .github └── workflows │ ├── Nuitka-Linux.yml │ ├── Nuitka-Windows.yml │ ├── Nuitka-macOS.yml │ ├── PyInstaller-Linux.yml │ ├── PyInstaller-Windows.yml │ └── PyInstaller-macOS.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── artemis-workspace.code-workspace ├── documentation ├── ArtemisLogoSmall.png ├── apple.png ├── linux.png └── win.png ├── requirements └── requirements.txt ├── spec_files ├── Linux │ ├── Artemis.spec │ ├── Artemis_onedir.spec │ ├── artemis3.svg │ ├── build.sh │ ├── create_shortcut.sh │ ├── raspbian_build.sh │ └── updater.spec ├── README.md ├── Windows │ ├── Artemis3.ico │ ├── Artemis_onedir.spec │ ├── artemis.spec │ ├── build.bat │ ├── excluded_files.txt │ └── updater.spec ├── __get_hash_code.py └── macOS │ ├── Artemis.spec │ └── Artemis3.icns ├── src ├── .flake8 ├── acfvalue.py ├── artemis.py ├── artemis.ui ├── audio_player.py ├── cacert.pem ├── clickable_progress_bar.py ├── constants.py ├── default_imgs.qrc ├── default_imgs_rc.py ├── default_pics │ ├── Artemis3.500px.png │ ├── Artemis3.ico │ ├── Artemis3.png │ ├── nosignalselected.png │ └── spectrumnotavailable.png ├── double_text_button.py ├── download_db_window.ui ├── download_window.py ├── downloadtargetfactory.py ├── executable_utilities.py ├── filters.py ├── fixed_aspect_ratio_label.py ├── fixed_aspect_ratio_widget.py ├── loggingconf.py ├── os_utilities.py ├── settings.py ├── spaceweathermanager.py ├── switchable_label.py ├── themes │ ├── acqua │ │ ├── acqua.qss │ │ ├── colors.txt │ │ └── icons │ │ │ ├── down-arrow.png │ │ │ ├── down-arrow_hover.png │ │ │ ├── down-arrow_off.png │ │ │ ├── search_icon.png │ │ │ ├── up-arrow.png │ │ │ ├── up-arrow_hover.png │ │ │ ├── up-arrow_off.png │ │ │ └── volume.png │ ├── console_style │ │ ├── colors.txt │ │ ├── console_style.qss │ │ └── icons │ │ │ ├── down-arrow.png │ │ │ ├── down-arrow_hover.png │ │ │ ├── down-arrow_off.png │ │ │ ├── search_icon.png │ │ │ ├── up-arrow.png │ │ │ ├── up-arrow_hover.png │ │ │ ├── up-arrow_off.png │ │ │ └── volume.png │ ├── dark │ │ ├── colors.txt │ │ ├── dark.qss │ │ └── icons │ │ │ ├── down-arrow.png │ │ │ ├── down-arrow_hover.png │ │ │ ├── down-arrow_off.png │ │ │ ├── off.png │ │ │ ├── off_press.png │ │ │ ├── on.png │ │ │ ├── on_press.png │ │ │ ├── search_icon.png │ │ │ ├── up-arrow.png │ │ │ ├── up-arrow_hover.png │ │ │ ├── up-arrow_off.png │ │ │ └── volume.png │ ├── elegant_dark │ │ ├── colors.txt │ │ ├── elegant_dark.qss │ │ └── icons │ │ │ ├── down-arrow.png │ │ │ ├── down-arrow_hover.png │ │ │ ├── down-arrow_off.png │ │ │ ├── search_icon.png │ │ │ ├── up-arrow.png │ │ │ ├── up-arrow_hover.png │ │ │ ├── up-arrow_off.png │ │ │ └── volume.png │ ├── material_design_dark │ │ ├── colors.txt │ │ ├── icons │ │ │ ├── down-arrow.png │ │ │ ├── down-arrow_hover.png │ │ │ ├── down-arrow_off.png │ │ │ ├── off.png │ │ │ ├── off_press.png │ │ │ ├── on.png │ │ │ ├── on_press.png │ │ │ ├── search_icon.png │ │ │ ├── up-arrow.png │ │ │ ├── up-arrow_hover.png │ │ │ ├── up-arrow_off.png │ │ │ └── volume.png │ │ └── material_design_dark.qss │ └── material_design_light │ │ ├── colors.txt │ │ ├── icons │ │ ├── down-arrow.png │ │ ├── down-arrow_hover.png │ │ ├── down-arrow_off.png │ │ ├── off.png │ │ ├── off_press.png │ │ ├── on.png │ │ ├── on_press.png │ │ ├── search_icon.png │ │ ├── up-arrow.png │ │ ├── up-arrow_hover.png │ │ ├── up-arrow_off.png │ │ └── volume.png │ │ └── material_design_light.qss ├── themesmanager.py ├── threads.py ├── updater.py ├── updatescontroller.py ├── urlbutton.py ├── utilities.py ├── versioncontroller.py ├── weatherdata.py └── web_utilities.py └── unused_installation_scripts ├── Linux ├── artemis3.svg ├── deploy_linux.sh └── requirements_lin.txt ├── README.md └── Windows ├── artemis3.ico ├── deploy_win.bat └── requirements_win.txt /.github/workflows/Nuitka-Linux.yml: -------------------------------------------------------------------------------- 1 | name: Nuitka - Linux 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | linux-nuitka: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Use Python 3.9 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.9' 16 | 17 | - name: Install dependencies 18 | run: pip install -r ./requirements/requirements.txt 19 | 20 | - name: Install Nuitka 21 | run: | 22 | pip install Nuitka ordered-set zstandard 23 | 24 | - name: Building Artemis 25 | run: | 26 | MOD_PATH=$(python -c 'import site; print(site.getsitepackages()[0])') 27 | cd src 28 | 29 | python -m nuitka --standalone --onefile --assume-yes-for-downloads --follow-imports --enable-plugin=pyqt5 --enable-plugin=numpy --include-data-dir=themes=themes --include-data-dir=default_pics=default_pics --include-data-dir=$MOD_PATH/qtawesome=qtawesome --include-data-files=artemis.ui=artemis.ui --include-data-files=cacert.pem=cacert.pem --include-data-files=clickable_progress_bar.py=clickable_progress_bar.py --include-data-files=default_imgs.qrc=default_imgs.qrc --include-data-files=default_imgs_rc.py=default_imgs_rc.py --include-data-files=double_text_button.py=double_text_button.py --include-data-files=download_db_window.ui=download_db_window.ui --include-data-files=fixed_aspect_ratio_label.py=fixed_aspect_ratio_label.py --include-data-files=fixed_aspect_ratio_widget.py=fixed_aspect_ratio_widget.py --linux-onefile-icon=../spec_files/Linux/artemis3.svg --show-modules --disable-console artemis.py 30 | python -m nuitka --standalone --onefile --assume-yes-for-downloads --follow-imports --enable-plugin=pyqt5 --include-data-dir=$MOD_PATH/qtawesome=qtawesome --include-data-files=default_imgs_rc.py=default_imgs_rc.py --linux-onefile-icon=../spec_files/Linux/artemis3.svg --show-modules --disable-console updater.py 31 | 32 | chmod 755 artemis.bin 33 | chmod 755 updater.bin 34 | 35 | mkdir Artemis 36 | mv artemis.bin Artemis/Artemis 37 | mv themes Artemis/themes 38 | mv artemis.ui Artemis/artemis.ui 39 | mv cacert.pem Artemis/cacert.pem 40 | mv download_db_window.ui Artemis/download_db_window.ui 41 | mv updater.bin Artemis/_ArtemisUpdater 42 | cp ../spec_files/Linux/create_shortcut.sh Artemis/create_shortcut.sh 43 | cp ../spec_files/Linux/artemis3.svg Artemis/artemis3.svg 44 | 45 | echo "Create complete archive" 46 | tar -czvf ArtemisWebDownlaod_linux.tar.gz Artemis 47 | 48 | echo "Create single archives" 49 | cd Artemis 50 | tar -czvf Artemis_linux.tar.gz Artemis 51 | tar -czvf _ArtemisUpdater_linux.tar.gz _ArtemisUpdater 52 | 53 | echo "Get size and sha256" 54 | python ../../spec_files/__get_hash_code.py Artemis_linux.tar.gz _ArtemisUpdater_linux.tar.gz ../ArtemisWebDownlaod_linux.tar.gz > checksum.txt 55 | 56 | - uses: actions/upload-artifact@v3 57 | with: 58 | name: Artemis_Linux 59 | path: | 60 | ./src/Artemis/Artemis_linux.tar.gz 61 | ./src/Artemis/_ArtemisUpdater_linux.tar.gz 62 | ./src/ArtemisWebDownlaod_linux.tar.gz 63 | ./src/Artemis/checksum.txt 64 | -------------------------------------------------------------------------------- /.github/workflows/Nuitka-Windows.yml: -------------------------------------------------------------------------------- 1 | name: Nuitka - Windows 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | artemis_version: 7 | description: 'Version' 8 | required: true 9 | default: '3.0.0' 10 | type: string 11 | 12 | jobs: 13 | windows-nuitka: 14 | runs-on: windows-2022 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Use Python 3.9 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.9' 22 | 23 | - name: Install dependencies 24 | run: pip install -r ./requirements/requirements.txt 25 | 26 | - name: Install Nuitka 27 | run: | 28 | pip install Nuitka ordered-set zstandard 29 | 30 | - name: Building Artemis 31 | run: | 32 | $MOD_PATH = python -c 'import site; print(site.getsitepackages()[1])' 33 | CD src 34 | MKDIR Artemis 35 | 36 | python -m nuitka --standalone --onefile --disable-console --assume-yes-for-downloads --follow-imports --enable-plugin=pyqt5 --enable-plugin=numpy --include-data-dir=themes=themes --include-data-dir=default_pics=default_pics --include-data-dir=$MOD_PATH\qtawesome=qtawesome --include-data-files=artemis.ui=artemis.ui --include-data-files=cacert.pem=cacert.pem --include-data-files=clickable_progress_bar.py=clickable_progress_bar.py --include-data-files=default_imgs.qrc=default_imgs.qrc --include-data-files=default_imgs_rc.py=default_imgs_rc.py --include-data-files=double_text_button.py=double_text_button.py --include-data-files=download_db_window.ui=download_db_window.ui --include-data-files=fixed_aspect_ratio_label.py=fixed_aspect_ratio_label.py --include-data-files=fixed_aspect_ratio_widget.py=fixed_aspect_ratio_widget.py --include-data-files=$MOD_PATH\pygame\libogg-0.dll=libogg-0.dll --include-data-files=$MOD_PATH\pygame\libopus-0.dll=libopus-0.dll --include-data-files=$MOD_PATH\pygame\libopusfile-0.dll=libopusfile-0.dll --include-data-files=$MOD_PATH\pygame\libvorbisfile-3.dll=libvorbisfile-3.dll --include-data-files=$MOD_PATH\pygame\libvorbis-0.dll=libvorbis-0.dll --windows-icon-from-ico=default_pics\Artemis3.ico --show-modules --windows-company-name=Aresvalley.com --windows-product-name=Artemis --windows-file-version=${{github.event.inputs.artemis_version}} --windows-product-version=${{github.event.inputs.artemis_version}} --windows-file-description=Artemis artemis.py 37 | python -m nuitka --standalone --onefile --disable-console --assume-yes-for-downloads --follow-imports --enable-plugin=pyqt5 --include-data-dir=$MOD_PATH\qtawesome=qtawesome --include-data-files=default_imgs_rc.py=default_imgs_rc.py --windows-icon-from-ico=default_pics\Artemis3.ico --show-modules --windows-company-name=Aresvalley.com --windows-product-name=Artemis --windows-file-version=${{github.event.inputs.artemis_version}} --windows-product-version=${{github.event.inputs.artemis_version}} --windows-file-description=Artemis updater.py 38 | 39 | MOVE artemis.exe Artemis.exe 40 | ECHO "Compress files themes+Artemis.exe -> Artemis.zip" 41 | $compress = @{ 42 | Path = ".\Artemis.exe", ".\themes" 43 | CompressionLevel = "Optimal" 44 | DestinationPath = ".\Artemis.zip" 45 | } 46 | Compress-Archive @compress 47 | 48 | MOVE updater.exe _ArtemisUpdater.exe 49 | ECHO "Compress _ArtemisUpdater.exe -> ArtemisUpdater.zip" 50 | $compress = @{ 51 | Path = ".\_ArtemisUpdater.exe" 52 | CompressionLevel = "Optimal" 53 | DestinationPath = ".\ArtemisUpdater.zip" 54 | } 55 | Compress-Archive @compress 56 | 57 | MOVE Artemis.exe Artemis\Artemis.exe 58 | MOVE _ArtemisUpdater.exe Artemis\_ArtemisUpdater.exe 59 | MOVE themes Artemis\themes 60 | MOVE artemis.ui Artemis\artemis.ui 61 | MOVE cacert.pem Artemis\cacert.pem 62 | MOVE download_db_window.ui Artemis\download_db_window.ui 63 | 64 | ECHO "Compress all files for website bundle" 65 | $compress = @{ 66 | Path = "Artemis" 67 | CompressionLevel = "Optimal" 68 | DestinationPath = ".\Artemis_v${{github.event.inputs.artemis_version}}_x64.zip" 69 | } 70 | Compress-Archive @compress 71 | 72 | python ..\spec_files\__get_hash_code.py Artemis.zip ArtemisUpdater.zip Artemis_v${{github.event.inputs.artemis_version}}_x64.zip > checksum_SHA256.txt 73 | 74 | - uses: actions/upload-artifact@v3 75 | with: 76 | name: Artemis_v${{github.event.inputs.artemis_version}}_x64 77 | path: | 78 | .\src\Artemis.zip 79 | .\src\ArtemisUpdater.zip 80 | .\src\Artemis_v${{github.event.inputs.artemis_version}}_x64.zip 81 | .\src\checksum_SHA256.txt 82 | -------------------------------------------------------------------------------- /.github/workflows/Nuitka-macOS.yml: -------------------------------------------------------------------------------- 1 | name: Nuitka - MacOS 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | macos-nuitka: 8 | runs-on: macos-11 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Use Python 3.9 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.9' 16 | 17 | - name: Install dependencies 18 | run: pip install -r ./requirements/requirements.txt 19 | 20 | - name: Install PyInstaller 21 | run: | 22 | pip install Nuitka ordered-set zstandard 23 | 24 | - name: Build Artemis 25 | run: | 26 | MOD_PATH=$(python -c 'import site; print(site.getsitepackages()[0])') 27 | cd src 28 | python -m nuitka --standalone --macos-create-app-bundle --assume-yes-for-downloads --follow-imports --enable-plugin=pyqt5 --enable-plugin=numpy --include-data-dir=themes=themes --include-data-dir=default_pics=default_pics --include-data-dir=$MOD_PATH/qtawesome=qtawesome --include-data-files=artemis.ui=artemis.ui --include-data-files=cacert.pem=cacert.pem --include-data-files=clickable_progress_bar.py=clickable_progress_bar.py --include-data-files=default_imgs.qrc=default_imgs.qrc --include-data-files=default_imgs_rc.py=default_imgs_rc.py --include-data-files=double_text_button.py=double_text_button.py --include-data-files=download_db_window.ui=download_db_window.ui --include-data-files=fixed_aspect_ratio_label.py=fixed_aspect_ratio_label.py --include-data-files=fixed_aspect_ratio_widget.py=fixed_aspect_ratio_widget.py --show-modules --macos-signed-app-name=eu.aresvalley.artemis --macos-app-name=Artemis --macos-app-version=3.2.4 artemis.py 29 | 30 | - uses: actions/upload-artifact@v3 31 | with: 32 | name: Artemis_MacOS 33 | path: | 34 | ./src/artemis.app 35 | -------------------------------------------------------------------------------- /.github/workflows/PyInstaller-Linux.yml: -------------------------------------------------------------------------------- 1 | name: PyInstaller - Linux 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | linux-pyinstaller: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Use Python 3.9 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.9' 16 | 17 | - name: Install dependencies 18 | run: pip install -r ./requirements/requirements.txt 19 | 20 | - name: Install PyInstaller 21 | run: | 22 | pip install pyinstaller 23 | 24 | - name: Build Artemis main executable 25 | run: | 26 | cd ./spec_files/Linux 27 | mkdir output 28 | mkdir output/artemis 29 | pyinstaller Artemis.spec 30 | mv -v ./dist/Artemis ./output/Artemis 31 | rm -rfv dist build 32 | 33 | pyinstaller updater.spec 34 | mv -v ./dist/_ArtemisUpdater ./output/_ArtemisUpdater 35 | rm -rfv dist build 36 | 37 | echo "Create single archives" 38 | cd output 39 | cp -r ../../../src/themes artemis/themes 40 | rm -f artemis/themes/__current_theme 41 | cp Artemis artemis/Artemis 42 | cp _ArtemisUpdater artemis/_ArtemisUpdater 43 | 44 | tar -czvf Artemis_linux.tar.gz Artemis -C artemis themes 45 | tar -czvf _ArtemisUpdater_linux.tar.gz ./_ArtemisUpdater 46 | 47 | echo "Create full archive for website" 48 | 49 | cp ../artemis3.svg artemis 50 | cp ../create_shortcut.sh artemis 51 | 52 | tar -czvf ArtemisWebDownlaod_linux.tar.gz artemis 53 | 54 | echo "Get size and sha256" 55 | python ../../__get_hash_code.py Artemis_linux.tar.gz _ArtemisUpdater_linux.tar.gz ArtemisWebDownlaod_linux.tar.gz > checksum.txt 56 | 57 | - uses: actions/upload-artifact@v3 58 | with: 59 | name: Artemis_Linux 60 | path: | 61 | ./spec_files/Linux/output/Artemis_linux.tar.gz 62 | ./spec_files/Linux/output/_ArtemisUpdater_linux.tar.gz 63 | ./spec_files/Linux/output/ArtemisWebDownlaod_linux.tar.gz 64 | ./spec_files/Linux/output/checksum.txt 65 | -------------------------------------------------------------------------------- /.github/workflows/PyInstaller-Windows.yml: -------------------------------------------------------------------------------- 1 | name: PyInstaller - Windows 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | windows-pyinstaller: 8 | runs-on: windows-2022 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Use Python 3.9 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.9' 16 | 17 | - name: Install dependencies 18 | run: pip install -r ./requirements/requirements.txt 19 | 20 | - name: Install PyInstaller 21 | run: | 22 | pip install pyinstaller 23 | 24 | - name: Build Artemis main executables 25 | run: | 26 | CD spec_files\Windows 27 | 28 | ECHO "Building Artemis executable..." 29 | MKDIR output 30 | pyinstaller artemis.spec 31 | MOVE dist\Artemis.exe .\output\Artemis.exe 32 | RMDIR -recurse -force dist 33 | RMDIR -recurse -force build 34 | 35 | ECHO "Building updater..." 36 | pyinstaller updater.spec 37 | MOVE dist\_ArtemisUpdater.exe .\output\_ArtemisUpdater.exe 38 | RMDIR -recurse -force dist 39 | RMDIR -recurse -force build 40 | 41 | CD output 42 | MKDIR Artemis 43 | XCOPY /y Artemis.exe Artemis\ 44 | XCOPY /e /k /y ..\..\..\src\themes Artemis\themes\ /EXCLUDE:..\excluded_files.txt 45 | XCOPY /y _ArtemisUpdater.exe Artemis\ 46 | 47 | ECHO "Compress files themes+Artemis.exe -> Artemis.zip" 48 | $compress = @{ 49 | Path = ".\Artemis.exe", "..\..\..\src\themes" 50 | CompressionLevel = "Optimal" 51 | DestinationPath = ".\Artemis_win.zip" 52 | } 53 | Compress-Archive @compress 54 | 55 | $compress = @{ 56 | Path = ".\_ArtemisUpdater.exe" 57 | CompressionLevel = "Optimal" 58 | DestinationPath = ".\_ArtemisUpdater_win.zip" 59 | } 60 | Compress-Archive @compress 61 | 62 | ECHO "Compress all files for website download" 63 | $compress = @{ 64 | Path = "Artemis" 65 | CompressionLevel = "Optimal" 66 | DestinationPath = ".\ArtemisWebsite_win.zip" 67 | } 68 | Compress-Archive @compress 69 | 70 | python ..\..\__get_hash_code.py Artemis_win.zip _ArtemisUpdater_win.zip ArtemisWebsite_win.zip > checksum.txt 71 | 72 | - uses: actions/upload-artifact@v3 73 | with: 74 | name: Artemis_Windows 75 | path: | 76 | .\spec_files\Windows\output\Artemis_win.zip 77 | .\spec_files\Windows\output\_ArtemisUpdater_win.zip 78 | .\spec_files\Windows\output\ArtemisWebsite_win.zip 79 | .\spec_files\Windows\output\checksum.txt 80 | -------------------------------------------------------------------------------- /.github/workflows/PyInstaller-macOS.yml: -------------------------------------------------------------------------------- 1 | name: PyInstaller - MacOS 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | macos-pyinstaller: 8 | runs-on: macos-11 9 | steps: 10 | - uses: actions/checkout@v3 11 | 12 | - name: Use Python 3.9 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.9' 16 | 17 | - name: Install dependencies 18 | run: pip install -r ./requirements/requirements.txt 19 | 20 | - name: Install PyInstaller 21 | run: | 22 | pip install pyinstaller 23 | 24 | - name: Build Artemis main executable 25 | run: | 26 | cd ./spec_files/macOS 27 | pyinstaller Artemis.spec 28 | ls -lart 29 | 30 | - uses: actions/upload-artifact@v3 31 | with: 32 | name: Artemis_MacOS 33 | path: | 34 | ./spec_files/macOS/dist 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __PYCache__ 2 | Data 3 | src/themes/__current_theme 4 | designer.bat 5 | launch.bat 6 | .vscode/ 7 | .code-workspace 8 | spec_files/**/output 9 | *.txt 10 | *.json 11 | info.log 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and the format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 3 | 4 | The first release is [3.0.0] because this is actually the third major version (completely rewritten) of the software. 5 | 6 | ## [3.2.4] - 2022-09-30 7 | ### Fixed 8 | - Fixed crash on opening the Rx/Tx Condition tab 9 | 10 | ## [3.2.3] - 2022-09-29 11 | ### Added 12 | - Add auto-packaging feature using GitHub actions for Windows OS (experimental) 13 | ### Fixed 14 | - Fix crash for playing audio ([#34](https://github.com/AresValley/Artemis/pull/34)) 15 | 16 | ## [3.2.2] - 2022-07-29 17 | ### Fixed 18 | - Fixed crash on startup or if checking for updates without an internet connection ([#23](https://github.com/AresValley/Artemis/pull/23)) 19 | - Updated dependencies for security reasons (urllib3) and to address the main application failure to launch under certain conditions. 20 | 21 | ## [3.2.1] - 2020-04-25 22 | ### Added 23 | - Add some basic logging to the application. Also for severe errors, track them in info.log file in local folder. 24 | - Add Raspberry PI support ([#18](https://github.com/AresValley/Artemis/pull/18), [#20](https://github.com/AresValley/Artemis/pull/20)) 25 | 26 | ### Fixed 27 | - Support new `JSON` format for some forecast data ([#21](https://github.com/AresValley/Artemis/pull/14)). 28 | - Fixed categorization for very low x-ray flux according to NOAA format. 29 | - Remove the `exclusive` parameter in a PyQt function ([#16](https://github.com/AresValley/Artemis/pull/16)). 30 | 31 | 32 | ## [3.2.0] - 2019-12-14 33 | 34 | ### Added 35 | - The default font can be changed ([#14](https://github.com/AresValley/Artemis/pull/14)). 36 | - Move `Themes` into `Settings`. 37 | - Better settings management in `settings.json`. 38 | 39 | ### Fixed 40 | - Fix a bug in the space weather. An inactive k-index caused a crash. 41 | 42 | ## [3.1.0] - 2019-10-21 43 | ### Added 44 | - Automatic updates. From this version Artemis can update itself if a new version is available. Works only when running the executable version (disabled when running from source). The feature is partially unavailable for Mac, you can only download the new version. 45 | - The software version displayed has now a `.Dev` appended when running from script (_e.g._ 3.1.0.Dev) to differentiate from an actual binary executable. The `.Dev` thus implies that the running version of the software could not correspond to a particular release. 46 | - The `*.spec` files can be executed without copying the source code into 47 | their folder. 48 | - Add a link to the GitHub repository in the action bar. 49 | - Add support for signals with multiple-value acf ([#9](https://github.com/AresValley/Artemis/pull/9)). This partially breaks the backward compatibility because the database changed structure. 50 | 51 | ### Fixed 52 | - Adding the `Artemis` folder to `PATH` as the expected behaviour. Prior to this fix, Artemis could not find the `Data` and `themes` folders if started from outside the `Artemis` folder. 53 | - The audio buttons are of the same dimension also for high resolution screens ([#13](https://github.com/AresValley/Artemis/pull/13)) 54 | - An audio sample can be paused and a different one can be played without a program crash ([#12](https://github.com/AresValley/Artemis/pull/12)) 55 | 56 | ## [3.0.1] - 2019-8-1 57 | ### Added 58 | - The audio player has now a loop button ([#3](https://github.com/AresValley/Artemis/pull/3)). 59 | - The project has now a Changelog file. 60 | 61 | ### Fixed 62 | - Added SSL certificates for all downloads. Avoid a crash of the program for certain systems ([#6](https://github.com/AresValley/Artemis/pull/6)). 63 | - Start the application in maximized mode. The label in the propagation data are well displayed ([#7](https://github.com/AresValley/Artemis/pull/7)). 64 | - Compile the executable for Linux on an older version to avoid GLIBC compatibilities issues ([#8](https://github.com/AresValley/Artemis/pull/8)). 65 | 66 | ## [3.0.0] - 2019-07-23 67 | First release. 68 | 69 | 70 | 71 | [Unreleased]: https://github.com/AresValley/Artemis/compare/v3.2.1...HEAD 72 | [3.2.1]: https://github.com/AresValley/Artemis/compare/v3.2.0...v3.2.1 73 | [3.2.0]: https://github.com/AresValley/Artemis/compare/v3.1.0...v3.2.0 74 | [3.1.0]: https://github.com/AresValley/Artemis/compare/v3.0.1...v3.1.0 75 | [3.0.1]: https://github.com/AresValley/Artemis/compare/v3.0.0...v3.0.1 76 | [3.0.0]: https://github.com/AresValley/Artemis/releases/tag/v3.0.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ARTEMIS 3 ![LICENSE](https://img.shields.io/github/license/AresValley/Artemis.svg?style=flat-square) ![ISSUE](https://img.shields.io/github/issues/AresValley/Artemis.svg?style=flat-square) ![LANGUAGE](https://img.shields.io/github/languages/top/AresValley/Artemis.svg?style=flat-square) 4 | 5 | *Radio Signals Recognition Manual* 6 | 7 | ## ARTEMIS. In a nutshell. 8 | 9 | In short, ARTEMIS is a signals hunter software and a useful aid for radio listeners! The analysis of real-time spectra (from your SDR, for instance) is made simple: you can take advantage using one of the largest RF signal database (with over 370 records). Compare several signals properties (such as frequency, bandwidth, modulation, etc.) and verify what you are searching for through a waterfall/audio sample. A collection of filters allows you to narrow your search, making the identification of unknown signals, odd buzzes or weird noises way easier. 10 | 11 | ## Table of contents 12 | 13 | - [Run the software](#Run-the-software) 14 | - [Run from binary](#Run-from-binary) 15 | - [Run from source code](#Run-from-source-code) 16 | - [Compile from source code](#Compile-from-source-code) 17 | - [Database](#database) 18 | - [Syntax](#syntax) 19 | - [Multiple Items fields (Location, Modulation)](#multiple-items-fields-location-modulation) 20 | - [Themes](#themes) 21 | - [License](#license) 22 | - [Thanks](#thanks) 23 | 24 | ## Run the software 25 | 26 | Artemis 3 is entirely written in Python, so if you already have Python 3.7.0+ installed in your system, you can directly run the main script. Otherwise you can download a binary executable (see below). 27 | 28 | ### Run from binary 29 | **If you don't know what you want or you are not sure where to look, this is for you.** 30 | 31 | Basically, this is the easiest, smooth, and clean way to run Artemis 3. A Python installation is not required. 32 | For more information, follow [the main page of Artemis 3](https://aresvalley.com/artemis/) (detailed documentation at the end of the main page). 33 | 34 | **Requirements:** 35 | - Windows 7/8/8.1/10 36 | - Linux: Ubuntu 20.04+, Mint 20+, Fedora 32+ and many other. **You need at least version 2.31 of the GLIBC system library** ([details](https://github.com/AresValley/Artemis/tree/master/spec_files)) 37 | - macOS 11+ (Big Sur or later) 38 | 39 | ### Run from source code 40 | Run the software from the source code with the Python interpreter is the simplest and natural way to run Artemis 3. 41 | 42 | **Requirements:** 43 | - Python (ver. 3.7.0+) 44 | - Python libraries (in `requirements/requirements.txt`) 45 | 46 | 1. Download and install Python (ver. 3.7.0+) from the official [website](https://www.python.org/downloads/). Be sure to select the flag `Add Python 3.x to PATH` during the first part of the installation. 47 | 48 | 2. Install the necessary Python libraries with PIP. Open a console in Artemis/requirements folder and type: 49 | 50 | ``` 51 | pip install -r requirements.txt --user 52 | ``` 53 | 54 | 3. After that launch the software in the Artemis folder with: 55 | 56 | ``` 57 | python3 artemis.py 58 | ``` 59 | 60 | ### Compile from source code 61 | If you want to compile Artemis yourself from the source code follow the instructions in the [spec_files/README](spec_files/README.md) file. 62 | 63 | ## Database 64 | 65 | The database (db.csv) is directly extracted from sigidwiki.com with a DB parser and reworked to a standard format defined as follow. Artemis DB is a human-readable csv file where the delimiter is the character `*` (Asterisk, Unicode: U+002A). The new entry (separation between signals) is the End Of Line (EOL) escape sequence `\n`. Every signal is directly connected to spectra and audio sample stored in **Spectra** and **Audio** folders, respectively. Every signal is composed of 12 columns: 66 | 67 | | Column | Description | Unit of Measurement | Multiple Items | Type| 68 | | :-: | :-: | :-: | :-: | :-: | 69 | | 1 | Signal name | - | - | string | 70 | | 2 | Freq. Lower Limit | Hz | - | integer | 71 | | 3 | Freq. Upper Limit | Hz | - | integer | 72 | | 4 | Mode | - | - | string | 73 | | 5 | Band. Lower Limit | Hz | - | integer | 74 | | 6 | Band. Upper Limit | Hz | - | integer | 75 | | 7 | Location | - | ✔ | string | 76 | | 8 | sigidwiki URL | - | - | string | 77 | | 9 | Description | - | - | string | 78 | | 10 | Modulation | - | ✔ | string | 79 | | 11 | ID Code | - | - | integer | 80 | | 12 | Auto-correlation function | ms | ✔ | string | 81 | 82 | ### Syntax 83 | 84 | 1. **Signal Name**: The name of the signal. A simple string that describes in short the analyzed signal. Special characters are allowed (except the main delimiter `*`). 85 | 2. **Frequency (Lower Limit)**: The frequency lower bound expressed in Hertz. 86 | 3. **Frequency (Upper Limit)**: The same as above but this express the frequency upper bound of the received signal. 87 | 88 | * In the case of a single frequency transmitter/service the **Freq. Lower Limit** and the **Freq. Upper Limit** must be coincident (same value) 89 | * Transmission with different protocols must be added in two or more distinct entry. **DO NOT USE** the same signal page to add different transmission protocols (with different frequencies, bandwidth, modes,...). For example, NOAA-19 satellite transimit images and data with two different protocols: 90 | 91 | * APT (Analog): 137.1000 MHz - 137.3125 MHz 92 | * HRPT (Digital): 1698 MHz - 1707 MHz 93 | 94 | Add two separate entry (APT and HRPT) with the correct Lower and Upper bound frequency. **DO NOT ADD** a single signal entry with a Freq. Lower Bound of 137.100 MHz and the Upper Bound of 1707 MHz. 95 | 96 | 4. **Mode**: This field reports the way how a signals has been decoded during the reception. 97 | 5. **Bandwidth (Lower Limit)**: As reported above for frequency (points 2 and 3). Also here the value is reported in Hz. 98 | 6. **Bandwidth (Upper Limit)**: As reported above for frequency (points 2 and 3). 99 | 7. **Location**: This is the location where the signal is distributed/received. Avoid the usage of the precise location of the TX station or very small town (very rare). It's a good habit to use nations/continents or special location (worldwide). 100 | 8. **sigidiwki URL**: The sigidwiki URL of the selected signal. This is a direct connection to the online database where further details of the signal are collected. 101 | 9. **Description**: The short description is used to explain the purpose of the signal and some other useful details. 102 | 10. **Modulation**: The modulation is the way how the information have been encoded into the main signal (carrier). Several modification of the properties (Amplitude, Frequecy, ...) are possible and a tx station could transmit with different modulations. 103 | 11. **ID Code**: The category code, known as ID Code, is a sequence of 0/1 and its main purpose is to assign the signal to their families/categories. It's formed by 17 digits: 104 | 105 | |1|2|3|4|5|6|7|8|9|10| 106 | |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| 107 | | Military | Radar | Active | Inactive | HAM | Commercial | Aviation | Marine | Analogue | Digital| 108 | 109 | |11|12|13|14|15|16|17| 110 | |:-:|:-:|:-:|:-:|:-:|:-:|:-:| 111 | | Trunked | Utility | Sat | Navigation | Interfering | Number Stations | Time Signal | 112 | 113 | 12. **Auto-correlation funtion (ACF)**: The ACF is an awesome discriminator when the signal is composed of redundant pattern that continouosly repeats. Unfortunately, for this reason, ACF is not always available. The time value is reported in **ms** and, in some cases, could have multiple values for a single signal. An extended description with an example signal analysis is available here: https://aresvalley.com/documentation/ 114 | 115 | 116 | ``` 117 | ... ID Code * ACF1 Description - ACF1 Value (in ms) ; ACF2 Description - ACF2 Value (in ms) ; ... 118 | ``` 119 | 120 | **Example 1 (D-STAR):** 121 | 122 | ``` 123 | ... ID Code * Superframe - 420 ; Frame - 20 124 | ``` 125 | **Example 2 (EDACS):** 126 | 127 | ``` 128 | ... ID Code * edacs48 - 60 ; edacs96 - 30 129 | ``` 130 | 131 | **Special case:** variable ACFs are allowed and a brief explanation can be reported instead of ACF Value. A nice example is the * [SSTV](https://www.sigidwiki.com/wiki/Slow-Scan_Television_(SSTV) "SSTV") transmission where the ACF is directly related to the number of lines per minute. 132 | 133 | ### Multiple Items fields (Location, Modulation) 134 | The necessity to manage a multiple Location/Modulation search pushed us to implement a fictitious 'secondary delimiter' chosen to be the `;` character. For instance: 135 | 136 | ``` 137 | ... Band. Upper Limit * Location 1 ; Location 2 ; ... * sigidwiki URL ... 138 | ``` 139 | or 140 | ``` 141 | ... Description * Modulation 1 ; Modulation 2 ; ... * ID Code ... 142 | ``` 143 | 144 | ## Themes 145 | The only folder with the pre-built package is the `themes` one. In this way the themes are fully customizable and you can add your own. New themes (in the `themes` folder) will appear automatically in the main menu and the last used theme will be saved as the favorite one (a restart of Artemis will use the last used theme). 146 | 147 | Some of the available themes were adapted from https://github.com/GTRONICK/QSS. 148 | 149 | ## License 150 | This program (ARTEMIS 3, 2014-2022) is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 151 | 152 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 153 | 154 | You should have received a copy of the GNU General Public License along with this program. If not, see: www.gnu.org/licenses 155 | 156 | ## Thanks 157 | * **Marco Dalla Tiezza** - *Artemis I-II developer, DB parsing, Website* 158 | * [**Alessandro Ceccato**](https://github.com/alessandro90 "GitHub profile") - *Artemis III lead developer* 159 | * **Paolo Romani (IZ1MLL)** - *Lead β Tester, RF specialist* 160 | * **Carl Colena** - *Sigidwiki admin, β Tester, Signals expert* 161 | * [**Marco Bortoli**](https://github.com/marbort "GitHub profile") - *macOS deployment, β Tester* 162 | * [**Eric Wiessner (KI7POL)**](https://github.com/WheezyE "GitHub profile") - *ARM port (Raspberry Pi3B+ and Pi4B)* 163 | * [**Pierpaolo Pravatto**](https://github.com/ppravatto "GitHub profile") - *Wiki page, β Tester* 164 | * [**Francesco Capostagno**](https://github.com/fcapostagno "GitHub profile"), **Luca**, **Pietro** - *β Tester* 165 | -------------------------------------------------------------------------------- /artemis-workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /documentation/ArtemisLogoSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/documentation/ArtemisLogoSmall.png -------------------------------------------------------------------------------- /documentation/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/documentation/apple.png -------------------------------------------------------------------------------- /documentation/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/documentation/linux.png -------------------------------------------------------------------------------- /documentation/win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/documentation/win.png -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.23.1 2 | pandas>=1.4.3 3 | certifi>=2022.6.15 4 | aiohttp>=3.8.1 5 | urllib3>=1.26.9 6 | pygame>=2.1.2 7 | QtAwesome>=1.1.1 8 | PyQt5>=5.15.7 9 | -------------------------------------------------------------------------------- /spec_files/Linux/Artemis.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import glob 4 | import os 5 | 6 | 7 | block_cipher = None 8 | 9 | 10 | SRC_PATH = "../../src/" 11 | 12 | data_file = [ 13 | (f, '.') for f in glob.glob(SRC_PATH + '*.[pu][yi]') 14 | if f.split('/')[-1] != "artemis.py" and f.split('/')[-1] != "updater.py" 15 | ] 16 | data_file.append((SRC_PATH + 'cacert.pem', '.')) 17 | 18 | a = Analysis([SRC_PATH + 'artemis.py'], # noqa: 821 19 | pathex=[os.getcwd()], 20 | binaries=[], 21 | datas=data_file, 22 | hiddenimports=[], 23 | hookspath=[], 24 | runtime_hooks=[], 25 | excludes=[], 26 | win_no_prefer_redirects=False, 27 | win_private_assemblies=False, 28 | cipher=block_cipher, 29 | noarchive=False) 30 | pyz = PYZ(a.pure, # noqa: 821 31 | a.zipped_data, 32 | cipher=block_cipher) 33 | exe = EXE(pyz, # noqa: 821 34 | a.scripts, 35 | a.binaries, 36 | a.zipfiles, 37 | a.datas, 38 | [], 39 | name='Artemis', 40 | debug=False, 41 | bootloader_ignore_signals=False, 42 | strip=False, 43 | upx=True, 44 | runtime_tmpdir=None, 45 | console=False) 46 | -------------------------------------------------------------------------------- /spec_files/Linux/Artemis_onedir.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import glob 4 | import os 5 | 6 | 7 | block_cipher = None 8 | 9 | 10 | SRC_PATH = "../../src/" 11 | 12 | data_file = [ 13 | (f, '.') for f in glob.glob(SRC_PATH + '*.[pu][yi]') 14 | if f.split('/')[-1] != "artemis.py" 15 | ] 16 | data_file.append((SRC_PATH + 'cacert.pem', '.')) 17 | 18 | a = Analysis([SRC_PATH + 'artemis.py'], # noqa: 821 19 | pathex=[os.getcwd()], 20 | binaries=[], 21 | datas=data_file, 22 | hiddenimports=[], 23 | hookspath=[], 24 | runtime_hooks=[], 25 | excludes=[], 26 | win_no_prefer_redirects=False, 27 | win_private_assemblies=False, 28 | cipher=block_cipher, 29 | noarchive=False) 30 | pyz = PYZ(a.pure, # noqa: 821 31 | a.zipped_data, 32 | cipher=block_cipher) 33 | exe = EXE(pyz, # noqa: 821 34 | a.scripts, 35 | [], 36 | exclude_binaries=True, 37 | name='Artemis', 38 | debug=False, 39 | bootloader_ignore_signals=False, 40 | strip=False, 41 | upx=True, 42 | console=False) 43 | coll = COLLECT(exe, # noqa: 821 44 | a.binaries, 45 | a.zipfiles, 46 | a.datas, 47 | strip=False, 48 | upx=True, 49 | name='Artemis') 50 | -------------------------------------------------------------------------------- /spec_files/Linux/build.sh: -------------------------------------------------------------------------------- 1 | echo "Build Artemis executable.." 2 | 3 | rm -rf output 4 | 5 | mkdir output 6 | mkdir output/artemis 7 | 8 | pyinstaller Artemis.spec 9 | 10 | mv -v ./dist/Artemis ./output/Artemis 11 | rm -rfv dist build 12 | 13 | echo "Build _ArtemisUpdater.." 14 | 15 | pyinstaller updater.spec 16 | 17 | mv -v ./dist/_ArtemisUpdater ./output/_ArtemisUpdater 18 | rm -rfv dist build 19 | 20 | echo "Create single archives" 21 | cd output 22 | 23 | cp -r ../../../src/themes artemis/themes 24 | rm -f artemis/themes/__current_theme 25 | cp Artemis artemis/Artemis 26 | cp _ArtemisUpdater artemis/_ArtemisUpdater 27 | 28 | tar -czvf Artemis_linux.tar.gz Artemis -C artemis themes 29 | tar -czvf _ArtemisUpdater_linux.tar.gz ./_ArtemisUpdater 30 | 31 | echo "Create full archive for website" 32 | 33 | cp ../artemis3.svg artemis 34 | cp ../create_shortcut.sh artemis 35 | 36 | tar -czvf ArtemisWebDownlaod_linux.tar.gz artemis 37 | 38 | echo "Get size and sha256" 39 | python ../../__get_hash_code.py Artemis_linux.tar.gz _ArtemisUpdater_linux.tar.gz ArtemisWebDownlaod_linux.tar.gz 40 | 41 | cd .. 42 | echo "Done." 43 | -------------------------------------------------------------------------------- /spec_files/Linux/create_shortcut.sh: -------------------------------------------------------------------------------- 1 | clear 2 | echo " 3 | =================================== 4 | Artemis 3 Shortcut Creator 5 | LINUX 6 | =================================== 7 | " 8 | 9 | # Set the correct permissions for Artemis folder 10 | echo "Gaining admin privileges and set folder read/write permission..." 11 | echo "" 12 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 13 | sudo chmod 700 $DIR 14 | 15 | # Generation of shortcut 16 | file=/home/$USER/.local/share/applications/artemis.desktop 17 | if [ -e "$file" ]; then 18 | echo "A shortcut of Artemis 3 is already present:" 19 | echo "" 20 | echo "R) Remove the shortcut and the icon file" 21 | echo "U) Update the shortcut" 22 | echo "" 23 | read -p "" doit 24 | case $doit in 25 | u|U) 26 | echo "[Desktop Entry]" >> /home/$USER/.local/share/applications/artemis.desktop 27 | echo "Name=Artemis" >> /home/$USER/.local/share/applications/artemis.desktop 28 | echo "Type=Application" >> /home/$USER/.local/share/applications/artemis.desktop 29 | echo "StartupWMClass=artemis3" >> /home/$USER/.local/share/applications/artemis.desktop 30 | echo "Exec=sh -c \"cd $DIR && ./Artemis\" " >> /home/$USER/.local/share/applications/artemis.desktop 31 | echo "Terminal=false" >> /home/$USER/.local/share/applications/artemis.desktop 32 | echo "Icon=artemis3" >> /home/$USER/.local/share/applications/artemis.desktop 33 | sudo cp ./artemis3.svg /usr/share/icons/ 34 | echo "Link Updated!" 35 | ;; 36 | r|R) 37 | sudo rm /home/$USER/.local/share/applications/artemis.desktop 38 | sudo rm /usr/share/icons/artemis3.svg 39 | echo "Link and icon removed!" 40 | ;; 41 | *) echo "Sorry! Invalid option $REPLY";; 42 | esac 43 | else 44 | echo "[Desktop Entry]" >> /home/$USER/.local/share/applications/artemis.desktop 45 | echo "Name=Artemis" >> /home/$USER/.local/share/applications/artemis.desktop 46 | echo "Type=Application" >> /home/$USER/.local/share/applications/artemis.desktop 47 | echo "StartupWMClass=artemis3" >> /home/$USER/.local/share/applications/artemis.desktop 48 | echo "Exec=sh -c \"cd $DIR && ./Artemis\" " >> /home/$USER/.local/share/applications/artemis.desktop 49 | echo "Terminal=false" >> /home/$USER/.local/share/applications/artemis.desktop 50 | echo "Icon=artemis3" >> /home/$USER/.local/share/applications/artemis.desktop 51 | sudo cp ./artemis3.svg /usr/share/icons/ 52 | echo " 53 | Link copied in: /home/$USER/.local/share/applications/artemis.desktop 54 | Icon copied in: /usr/share/icons/artemis3.svg 55 | " 56 | fi 57 | 58 | echo " 59 | ================================ 60 | SETTING COMPLETE 61 | ================================ 62 | " 63 | -------------------------------------------------------------------------------- /spec_files/Linux/updater.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import os 4 | 5 | 6 | BASE_FOLDER = "../../src/" 7 | block_cipher = None 8 | 9 | data_file = [ 10 | (os.path.join(BASE_FOLDER, "download_db_window.ui"), "."), 11 | (os.path.join(BASE_FOLDER, "download_window.py"), "."), 12 | (os.path.join(BASE_FOLDER, "utilities.py"), "."), 13 | (os.path.join(BASE_FOLDER, "versioncontroller.py"), "."), 14 | (os.path.join(BASE_FOLDER, "downloadtargetfactory.py"), "."), 15 | (os.path.join(BASE_FOLDER, "executable_utilities.py"), "."), 16 | (os.path.join(BASE_FOLDER, "os_utilities.py"), "."), 17 | (os.path.join(BASE_FOLDER, "web_utilities.py"), "."), 18 | (os.path.join(BASE_FOLDER, "constants.py"), "."), 19 | (os.path.join(BASE_FOLDER, "threads.py"), "."), 20 | (os.path.join(BASE_FOLDER, "default_imgs_rc.py"), "."), 21 | (os.path.join(BASE_FOLDER, "cacert.pem"), "."), 22 | ] 23 | a = Analysis([os.path.join(BASE_FOLDER, 'updater.py')], # noqa: 821 24 | pathex=[os.getcwd()], 25 | binaries=[], 26 | datas=data_file, 27 | hiddenimports=[], 28 | hookspath=[], 29 | runtime_hooks=[], 30 | excludes=[], 31 | win_no_prefer_redirects=False, 32 | win_private_assemblies=False, 33 | cipher=block_cipher, 34 | noarchive=False) 35 | pyz = PYZ(a.pure, # noqa: 821 36 | a.zipped_data, 37 | cipher=block_cipher) 38 | exe = EXE(pyz, # noqa: 821 39 | a.scripts, 40 | a.binaries, 41 | a.zipfiles, 42 | a.datas, 43 | [], 44 | name='_ArtemisUpdater', 45 | debug=False, 46 | bootloader_ignore_signals=False, 47 | strip=False, 48 | upx=True, 49 | runtime_tmpdir=None, 50 | console=False, 51 | icon='Artemis3.ico') 52 | -------------------------------------------------------------------------------- /spec_files/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ARTEMIS 3 ![LICENSE](https://img.shields.io/github/license/AresValley/Artemis.svg?style=flat-square) ![ISSUE](https://img.shields.io/github/issues/AresValley/Artemis.svg?style=flat-square) ![LANGUAGE](https://img.shields.io/github/languages/top/AresValley/Artemis.svg?style=flat-square) 4 | 5 | *Radio Signals Recognition Manual* 6 | 7 | ## ARTEMIS 3 .SPEC FILES 8 | 9 | Artemis 3 .spec files are used by the package **pyinstaller** (https://www.pyinstaller.org/) to build a single standalone executable (or a one-dir package). Every external dependency is already embedded into the bundle. The interpreter of Python 3 is also included. 10 | 11 | ## Requirements 12 | - Python 3.7.0+ 13 | - Pyinstaller 14 | 15 | **IMPORTANT:** *To generate the standalone and the one-dir package, you must use an operating system that coincides with the target one (pyinstaller doesn't allow cross-compilation).* 16 | 17 | **IMPORTANT (LINUX COMPILING):** *The executable that PyInstaller builds is not fully static, in that it still depends on the system libc. **Under Linux, the ABI of GLIBC is backward compatible, but not forward compatible. So if you link against a newer GLIBC, you can't run the resulting executable on an older system**. The supplied binary bootloader should work with older GLIBC. However, the libpython.so and other dynamic libraries still depend on the newer GLIBC. The solution is to compile the Python interpreter with its modules (and also probably bootloader) on the oldest system you have around so that it gets linked with the oldest version of GLIBC.* (Source: PyInstaller) 18 | 19 | **NOTE.** Depending on the number of packages installed in your python environemnt, the size of the executables file can vary significantly. In order to get the smallest size possible it is recommend to initialize a fresh virtual environment with just the requirements and pyinstaller. 20 | 21 | ## Package Building (standalone aka one-file, high portability, **suggested**) 22 | 1. Download/clone the git repository. 23 | 2. In the `spec_files/` folder open a terminal and type 24 | ``` 25 | pyinstaller Artemis.spec 26 | ``` 27 | 3. An Artemis executable should be produced in the `dist/` folder. The `build/` folder 28 | can be deleted. 29 | 30 | ## Package Building (one-dir, shorter startup time, low portability) 31 | 1. Download/clone the git repository. 32 | 2. In the `spec_files/` folder open a terminal and type 33 | ``` 34 | pyinstaller Artemis_onedir.spec 35 | ``` 36 | 3. An Artemis executable should be produced in `dist/Artemis/`. The `build/` can 37 | be deleted. 38 | 39 | 40 | You can save a copy of the executable in a folder of you choice. At startup it will ask you to download the database and also warn you that the `themes` folder is missing. To avoid this, copy `src/Data` and `src/themes` in the folder containing the executable. 41 | 42 | ## Build scripts (Windows, Linux, macOS) 43 | Provided you satisfy the requirements (see [requirements.txt](../requirements/requirements.txt)) and have pyinstaller installed, running a `build.*` script in `` folder will produce an `output` folder with: 44 | 45 | - Executable versions of Artemis and the updater; 46 | - compressed versions of the same files; 47 | - a folder called `Artemis/` containing the executables and the `theme` folder 48 | - a compressed version of the folder 49 | 50 | At the end of the process the script writes on standard output the size and sha256 code for the compressed files. 51 | 52 | **NOTE.** For Windows you will need a 7z installation. Also check the path hardcoded in `/Windows/build.bat`. 53 | 54 | ## Build script (Raspberry Pi) 55 | Thanks to [**Eric Wiessner (KI7POL)**](https://github.com/WheezyE "GitHub profile"), an automatic script to compile Artemis (and meet all the Artemis' build requirements) is available in the Linux folder `Linux/raspbian_build.sh`. The script will proceed as follows: 56 | 57 | - Detect which Raspbian operating system is being used (Buster or Stretch), installing PyEnv on the system (if PyEnv isn't already installed). 58 | - Installation of a Python v3.7.0 virtual environment inside of PyEnv (so that Artemis' pip modules do not conflict with Raspbian's System Python). 59 | - Installation of pip modules (Artemis requirements) within the previously built virtual Python 3.7.0 (modules are specific to Buster or Stretch). 60 | - Canonical Artemis building for Linux OS (using build.sh). 61 | 62 | **The complete instructions, a troubleshooting guide and many other usefull details are available in the script itself.** 63 | 64 | If the script is re-run, it will skip over parts it has already installed. Options are included to clean up after the script is run. In the worst case scenario, this script takes 14 hours on a Pi 0W, but much less time on multi-core Pi's. 65 | -------------------------------------------------------------------------------- /spec_files/Windows/Artemis3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/spec_files/Windows/Artemis3.ico -------------------------------------------------------------------------------- /spec_files/Windows/Artemis_onedir.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import glob 4 | import os 5 | 6 | 7 | block_cipher = None 8 | 9 | 10 | SRC_PATH = "../../src/" 11 | 12 | data_file = [ 13 | (f, '.') for f in glob.glob(SRC_PATH + '*.[pu][yi]') 14 | if f.split('/')[-1] != "artemis.py" 15 | ] 16 | data_file.append((SRC_PATH + 'cacert.pem', '.')) 17 | 18 | a = Analysis(SRC_PATH + ['artemis.py'], # noqa: 821 19 | pathex=[os.getcwd()], 20 | binaries=[], 21 | datas=data_file, 22 | hiddenimports=[], 23 | hookspath=[], 24 | runtime_hooks=[], 25 | excludes=[], 26 | win_no_prefer_redirects=False, 27 | win_private_assemblies=False, 28 | cipher=block_cipher, 29 | noarchive=False) 30 | pyz = PYZ(a.pure, # noqa: 821 31 | a.zipped_data, 32 | cipher=block_cipher) 33 | exe = EXE(pyz, # noqa: 821 34 | a.scripts, 35 | [], 36 | exclude_binaries=True, 37 | name='Artemis', 38 | debug=False, 39 | bootloader_ignore_signals=False, 40 | strip=False, 41 | upx=True, 42 | console=False, 43 | icon='Artemis3.ico') 44 | coll = COLLECT(exe, # noqa: 821 45 | a.binaries, 46 | a.zipfiles, 47 | a.datas, 48 | strip=False, 49 | upx=True, 50 | name='Artemis') 51 | -------------------------------------------------------------------------------- /spec_files/Windows/artemis.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import glob 4 | import os 5 | 6 | 7 | block_cipher = None 8 | 9 | 10 | SRC_PATH = "../../src/" 11 | 12 | data_file = [ 13 | (f, '.') for f in glob.glob(SRC_PATH + '*.[pu][yi]') 14 | if f.split('/')[-1] != "artemis.py" and f.split('/')[-1] != "updater.py" 15 | ] 16 | data_file.append((SRC_PATH + 'cacert.pem', '.')) 17 | 18 | a = Analysis([SRC_PATH + 'artemis.py'], # noqa: 821 19 | pathex=[os.getcwd()], 20 | binaries=[], 21 | datas=data_file, 22 | hiddenimports=[], 23 | hookspath=[], 24 | runtime_hooks=[], 25 | excludes=[], 26 | win_no_prefer_redirects=False, 27 | win_private_assemblies=False, 28 | cipher=block_cipher, 29 | noarchive=False) 30 | pyz = PYZ(a.pure, # noqa: 821 31 | a.zipped_data, 32 | cipher=block_cipher) 33 | exe = EXE(pyz, # noqa: 821 34 | a.scripts, 35 | a.binaries, 36 | a.zipfiles, 37 | a.datas, 38 | [], 39 | name='Artemis', 40 | debug=False, 41 | bootloader_ignore_signals=False, 42 | strip=False, 43 | upx=True, 44 | runtime_tmpdir=None, 45 | console=False, 46 | icon='Artemis3.ico', 47 | uac_admin=True) 48 | -------------------------------------------------------------------------------- /spec_files/Windows/build.bat: -------------------------------------------------------------------------------- 1 | ECHO OFF 2 | ECHO Building Artemis executable... 3 | RMDIR /s /q output 4 | MKDIR output 5 | pyinstaller artemis.spec 6 | ECHO Remove directories 7 | MOVE dist\Artemis.exe .\output\Artemis.exe 8 | RMDIR /s /q dist 9 | RMDIR /s /q build 10 | ECHO ************* 11 | ECHO ************* 12 | ECHO Building updater... 13 | pyinstaller updater.spec 14 | ECHO Remove directories 15 | MOVE dist\_ArtemisUpdater.exe .\output\_ArtemisUpdater.exe 16 | RMDIR /s /q dist 17 | RMDIR /s /q build 18 | CD output 19 | MKDIR Artemis 20 | XCOPY /y Artemis.exe Artemis\ 21 | XCOPY /e /k /y ..\..\..\src\themes Artemis\themes\ /EXCLUDE:..\excluded_files.txt 22 | XCOPY /y _ArtemisUpdater.exe Artemis\ 23 | ECHO "Compress files themes+Artemis.exe -> Artemis.zip" 24 | "C:\Program Files\7-Zip\7z.exe" a Artemis_win.zip Artemis.exe ..\..\..\src\themes 25 | "C:\Program Files\7-Zip\7z.exe" a _ArtemisUpdater_win.zip _ArtemisUpdater.exe 26 | ECHO "Compress all files for website download" 27 | "C:\Program Files\7-Zip\7z.exe" a ArtemisWebsite_win.zip Artemis\ 28 | python ..\..\__get_hash_code.py Artemis_win.zip _ArtemisUpdater_win.zip ArtemisWebsite_win.zip 29 | CD .. 30 | ECHO Done. -------------------------------------------------------------------------------- /spec_files/Windows/excluded_files.txt: -------------------------------------------------------------------------------- 1 | __current_theme -------------------------------------------------------------------------------- /spec_files/Windows/updater.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | import os 4 | 5 | 6 | BASE_FOLDER = "../../src/" 7 | block_cipher = None 8 | 9 | data_file = [ 10 | (os.path.join(BASE_FOLDER, "download_db_window.ui"), "."), 11 | (os.path.join(BASE_FOLDER, "download_window.py"), "."), 12 | (os.path.join(BASE_FOLDER, "utilities.py"), "."), 13 | (os.path.join(BASE_FOLDER, "versioncontroller.py"), "."), 14 | (os.path.join(BASE_FOLDER, "downloadtargetfactory.py"), "."), 15 | (os.path.join(BASE_FOLDER, "executable_utilities.py"), "."), 16 | (os.path.join(BASE_FOLDER, "os_utilities.py"), "."), 17 | (os.path.join(BASE_FOLDER, "web_utilities.py"), "."), 18 | (os.path.join(BASE_FOLDER, "constants.py"), "."), 19 | (os.path.join(BASE_FOLDER, "threads.py"), "."), 20 | (os.path.join(BASE_FOLDER, "default_imgs_rc.py"), "."), 21 | (os.path.join(BASE_FOLDER, "cacert.pem"), "."), 22 | ] 23 | a = Analysis([os.path.join(BASE_FOLDER, 'updater.py')], # noqa: 821 24 | pathex=[os.getcwd()], 25 | binaries=[], 26 | datas=data_file, 27 | hiddenimports=[], 28 | hookspath=[], 29 | runtime_hooks=[], 30 | excludes=[], 31 | win_no_prefer_redirects=False, 32 | win_private_assemblies=False, 33 | cipher=block_cipher, 34 | noarchive=False) 35 | pyz = PYZ(a.pure, # noqa: 821 36 | a.zipped_data, 37 | cipher=block_cipher) 38 | exe = EXE(pyz, # noqa: 821 39 | a.scripts, 40 | a.binaries, 41 | a.zipfiles, 42 | a.datas, 43 | [], 44 | name='_ArtemisUpdater', 45 | debug=False, 46 | bootloader_ignore_signals=False, 47 | strip=False, 48 | upx=True, 49 | runtime_tmpdir=None, 50 | console=False, 51 | icon='Artemis3.ico', 52 | uac_admin=True) 53 | -------------------------------------------------------------------------------- /spec_files/__get_hash_code.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import sys 3 | 4 | 5 | """Print on stadard output the size in KB and sha256 codes of a list 6 | of command line-provided file names.""" 7 | 8 | print() 9 | 10 | try: 11 | fnames = sys.argv[1:] 12 | except Exception: 13 | print("Provide a valid filename.") 14 | exit() 15 | 16 | for fname in fnames: 17 | try: 18 | with open(fname, mode='rb') as f: 19 | target = f.read() 20 | except Exception: 21 | print("File not found") 22 | exit() 23 | 24 | code = hashlib.sha256() 25 | code.update(target) 26 | hash_code = code.hexdigest() 27 | 28 | print("File name:", fname) 29 | print("Size (KB):", round(len(target) / 1024, 3)) 30 | print("Hash code:", hash_code) 31 | print("-" * 80) 32 | -------------------------------------------------------------------------------- /spec_files/macOS/Artemis.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | import glob 4 | import os 5 | 6 | 7 | block_cipher = None 8 | 9 | 10 | SRC_PATH = "../../src/" 11 | 12 | data_file = [ 13 | (f, '.') for f in glob.glob(SRC_PATH + '*.[pu][yi]') 14 | if f.split('/')[-1] != "artemis.py" 15 | ] 16 | data_file.extend(((SRC_PATH + 'cacert.pem', '.'), (SRC_PATH + 'themes', './themes'))) 17 | 18 | a = Analysis([SRC_PATH + 'artemis.py'], # noqa: 821 19 | pathex=[os.getcwd()], 20 | binaries=[], 21 | datas=data_file, 22 | hiddenimports=[], 23 | hookspath=[], 24 | runtime_hooks=[], 25 | excludes=[], 26 | win_no_prefer_redirects=False, 27 | win_private_assemblies=False, 28 | cipher=block_cipher, 29 | noarchive=False) 30 | pyz = PYZ(a.pure, # noqa: 821 31 | a.zipped_data, 32 | cipher=block_cipher) 33 | exe = EXE(pyz, # noqa: 821 34 | a.scripts, 35 | [], 36 | exclude_binaries=True, 37 | name='Artemis', 38 | debug=False, 39 | bootloader_ignore_signals=False, 40 | strip=False, 41 | upx=True, 42 | console=False) 43 | coll = COLLECT(exe, # noqa: 821 44 | a.binaries, 45 | a.zipfiles, 46 | a.datas, 47 | strip=False, 48 | upx=True, 49 | name='Artemis') 50 | app = BUNDLE(coll, # noqa: 821 51 | name='Artemis.app', 52 | icon='Artemis3.icns', 53 | bundle_identifier=None) 54 | -------------------------------------------------------------------------------- /spec_files/macOS/Artemis3.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/spec_files/macOS/Artemis3.icns -------------------------------------------------------------------------------- /src/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E221, E501, W605, W504 3 | -------------------------------------------------------------------------------- /src/acfvalue.py: -------------------------------------------------------------------------------- 1 | from constants import Constants 2 | 3 | 4 | class ACFValue: 5 | """Handle complex/multiple ACF values.""" 6 | 7 | def __init__(self, value): 8 | """Given a string describing an acf value, build an object with the 9 | following attrributes: 10 | - is_numeric: whether the value is a number or a string; 11 | - numeric_value: the numeric value (if any, zero otherwise); 12 | - unknown: whether the value is unknown.""" 13 | if value == Constants.UNKNOWN: 14 | self._value = value 15 | self._description = "" 16 | self._string = self._value 17 | self.is_numeric = False 18 | self.unknown = True 19 | self.numeric_value = 0.0 20 | else: 21 | self.unknown = False 22 | if Constants.ACF_SEPARATOR in value: 23 | description, acf_value = value.split(Constants.ACF_SEPARATOR) 24 | self._description = description 25 | self._value = acf_value 26 | self._string = f"{self._description}: {self._value}" 27 | else: 28 | self._description = "" 29 | self._value = value 30 | self._string = self._value 31 | if self._value.isdigit(): 32 | self.numeric_value = float(self._value) 33 | self.is_numeric = True 34 | self._string += " ms" 35 | else: 36 | self.is_numeric = False 37 | self.numeric_value = 0.0 38 | 39 | @classmethod 40 | def list_from_series(cls, series): 41 | """Parse all acf values from the database. 42 | 43 | Accept an iterable of ACFValues. 44 | Return a list of lists of ACFValues.""" 45 | entries = [] 46 | for entry in series: 47 | entries.append([ 48 | cls(value.rstrip('ms').strip()) for value in entry.split(Constants.FIELD_SEPARATOR) 49 | ]) 50 | return entries 51 | 52 | @staticmethod 53 | def concat_strings(acf_list_values): 54 | """Concatenate a list of ACFValues to be displayed.""" 55 | return '\n'.join(s._string for s in acf_list_values) 56 | -------------------------------------------------------------------------------- /src/audio_player.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | from pygame import mixer 4 | from PyQt5.QtCore import QTimer, pyqtSlot, QObject 5 | 6 | import qtawesome as qta 7 | from constants import Constants 8 | 9 | 10 | class AudioPlayer(QObject): 11 | """Subclass QObject. Audio player widget for the audio samples. 12 | 13 | The only public methods are the __init__ 14 | method, set_audio_player, which loads the current file and refresh. 15 | Everything else is managed internally.""" 16 | 17 | _TIME_STEP = 500 # Milliseconds. 18 | 19 | def __init__(self, play, 20 | pause, 21 | stop, 22 | volume, 23 | loop, 24 | audio_progress, 25 | active_color, 26 | inactive_color): 27 | """Initialize the player.""" 28 | super().__init__() 29 | self._active_color = active_color 30 | self._inactive_color = inactive_color 31 | self._paused = False 32 | self._first_call = True 33 | self._play = play 34 | self._pause = pause 35 | self._stop = stop 36 | self._volume = volume 37 | self._loop = loop 38 | self._audio_progress = audio_progress 39 | self._audio_file = None 40 | self._timer = QTimer() 41 | self._timer.timeout.connect(self._update_bar) 42 | self._play.clicked.connect(self._play_audio) 43 | self._pause.clicked.connect(self._pause_audio) 44 | self._stop.clicked.connect(self._stop_audio) 45 | self._volume.valueChanged.connect(self._set_volume) 46 | self._loop.clicked.connect(self._set_loop_icon) 47 | self._play.setIconSize(self._play.size()) 48 | self._pause.setIconSize(self._pause.size()) 49 | self._stop.setIconSize(self._stop.size()) 50 | self._loop.setIconSize(self._loop.size()) 51 | self.refresh(active_color, inactive_color) 52 | 53 | @pyqtSlot() 54 | def _set_loop_icon(self): 55 | """Set the icon for the loop audio button.""" 56 | if self._loop.isChecked(): 57 | loop_icon = qta.icon( 58 | 'fa5s.redo-alt', 59 | color=self._active_color, 60 | color_disabled=self._inactive_color, 61 | animation=qta.Spin(self._loop) 62 | ) 63 | else: 64 | loop_icon = qta.icon( 65 | 'fa5s.redo-alt', 66 | color=self._active_color, 67 | color_disabled=self._inactive_color 68 | ) 69 | self._loop.setIcon(loop_icon) 70 | 71 | def refresh(self, active_color, inactive_color): 72 | """Repaint the buttons of the widgetd after the theme has changed.""" 73 | self._active_color = active_color 74 | self._inactive_color = inactive_color 75 | self._play.setIcon(qta.icon('fa5s.play', 76 | color=active_color, 77 | color_disabled=inactive_color)) 78 | self._pause.setIcon(qta.icon('fa5s.pause', 79 | color=active_color, 80 | color_disabled=inactive_color)) 81 | self._stop.setIcon(qta.icon('fa5s.stop', 82 | color=active_color, 83 | color_disabled=inactive_color)) 84 | self._set_loop_icon() 85 | 86 | @pyqtSlot() 87 | def _set_volume(self): 88 | """Set the volume of the audio samples.""" 89 | if mixer.get_init(): 90 | mixer.music.set_volume( 91 | self._volume.value() / self._volume.maximum() 92 | ) 93 | 94 | def _reset_audio_widget(self): 95 | """Reset the widget. Stop all playing samples.""" 96 | self._first_call = True 97 | self._paused = False 98 | if mixer.get_init(): 99 | if mixer.music.get_busy(): 100 | mixer.music.stop() 101 | self._timer.stop() 102 | mixer.quit() 103 | self._audio_progress.reset() 104 | self._enable_buttons(False, False, False) 105 | 106 | @pyqtSlot() 107 | def _update_bar(self): 108 | """Update the progress bar.""" 109 | pos = mixer.music.get_pos() 110 | if pos == -1: 111 | self._timer.stop() 112 | self._audio_progress.reset() 113 | if self._loop.isChecked(): 114 | self._play_audio() 115 | self._enable_buttons(False, True, True) 116 | else: 117 | self._enable_buttons(True, False, False) 118 | else: 119 | self._audio_progress.setValue(pos) 120 | 121 | def _set_max_progress_bar(self): 122 | """Set the maximum value of the progress bar.""" 123 | self._audio_progress.setMaximum( 124 | math.ceil(mixer.Sound(self._audio_file).get_length() * 1000) 125 | ) 126 | 127 | def set_audio_player(self, fname=""): 128 | """Set the current audio sample.""" 129 | self._reset_audio_widget() 130 | full_name = os.path.join( 131 | Constants.AUDIO_FOLDER, 132 | fname + '.ogg' 133 | ) 134 | if os.path.exists(full_name): 135 | self._play.setEnabled(True) 136 | self._audio_file = full_name 137 | 138 | @pyqtSlot() 139 | def _play_audio(self): 140 | """Play the audio sample.""" 141 | if not self._paused: 142 | if self._first_call: 143 | self._first_call = False 144 | mixer.init(48000, -16, 1, 1024) 145 | mixer.music.load(self._audio_file) 146 | self._set_volume() 147 | self._set_max_progress_bar() 148 | mixer.music.play() 149 | else: 150 | mixer.music.unpause() 151 | self._paused = False 152 | self._timer.start(self._TIME_STEP) 153 | self._enable_buttons(False, True, True) 154 | 155 | @pyqtSlot() 156 | def _stop_audio(self): 157 | """Stop the audio sample.""" 158 | mixer.music.stop() 159 | self._audio_progress.reset() 160 | self._timer.stop() 161 | self._enable_buttons(True, False, False) 162 | 163 | @pyqtSlot() 164 | def _pause_audio(self): 165 | """Pause the audio sample.""" 166 | mixer.music.pause() 167 | self._timer.stop() 168 | self._paused = True 169 | self._enable_buttons(True, False, False) 170 | 171 | def _enable_buttons(self, play_en, pause_en, stop_en): 172 | """Set the three buttons status.""" 173 | self._play.setEnabled(play_en) 174 | self._pause.setEnabled(pause_en) 175 | self._stop.setEnabled(stop_en) 176 | -------------------------------------------------------------------------------- /src/clickable_progress_bar.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QProgressBar 2 | from PyQt5.QtCore import Qt, pyqtSignal 3 | from constants import Constants 4 | 5 | 6 | class ClickableProgressBar(QProgressBar): 7 | """Subclass QProgressBar. Clickable progress bar class.""" 8 | 9 | clicked = pyqtSignal() 10 | 11 | def __init__(self, parent=None): 12 | """Initialize the instance.""" 13 | self._text = '' 14 | super().__init__(parent) 15 | 16 | def text(self): 17 | """Return the text displayed on the bar.""" 18 | return self._text 19 | 20 | def set_idle(self): 21 | """Set the bar to a non-downloading status.""" 22 | self._text = Constants.CLICK_TO_UPDATE_STR 23 | self.setMaximum(self.minimum() + 1) 24 | 25 | def set_updating(self): 26 | """Set the bar to a downloading status.""" 27 | self._text = Constants.UPDATING_STR 28 | self.setMaximum(self.minimum()) 29 | 30 | def mousePressEvent(self, event): 31 | """Override QWidget.mousePressEvent. Detect a click on the bar.""" 32 | if event.button() == Qt.LeftButton: 33 | self.clicked.emit() 34 | else: 35 | super().mousePressEvent(event) 36 | -------------------------------------------------------------------------------- /src/constants.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from enum import Enum, auto 3 | import os.path 4 | from executable_utilities import get_executable_path 5 | 6 | 7 | __BASE_FOLDER__ = get_executable_path() 8 | 9 | 10 | class SupportedOs: 11 | """Supported operating systems.""" 12 | WINDOWS = "windows" 13 | LINUX = "linux" 14 | MAC = "mac" 15 | RASPBIAN = "raspberry" 16 | 17 | 18 | class Ftype: 19 | """Container class to differentiate between frequency and band.""" 20 | 21 | FREQ = "freq" 22 | BAND = "band" 23 | 24 | 25 | class GfdType(Enum): 26 | """Enum class to differentiate the possible GFD search criterias.""" 27 | 28 | FREQ = auto() 29 | LOC = auto() 30 | 31 | 32 | class DownloadTarget(Enum): 33 | """Enum class to distinguish the object being downloaded.""" 34 | 35 | DATA_FOLDER = auto() 36 | DB = auto() 37 | SOFTWARE = auto() 38 | UPDATER = auto() 39 | 40 | 41 | class Signal: 42 | """Container class for the signal property names.""" 43 | 44 | NAME = "name" 45 | INF_FREQ = "inf_freq" 46 | SUP_FREQ = "sup_freq" 47 | MODE = "mode" 48 | INF_BAND = "inf_band" 49 | SUP_BAND = "sup_band" 50 | LOCATION = "location" 51 | URL = "url" 52 | DESCRIPTION = "description" 53 | MODULATION = "modulation" 54 | CATEGORY_CODE = "category_code" 55 | ACF = "acf" 56 | WIKI_CLICKED = "url_clicked" 57 | 58 | 59 | class Database: 60 | """Container class for the database-related constants.""" 61 | 62 | LINK_LOC = "https://aresvalley.com/Storage/Artemis/Database/data.zip" 63 | LINK_REF = "https://aresvalley.com/Storage/Artemis/Database/data.zip.log" 64 | NAME = "db.csv" 65 | NAMES = (Signal.NAME, 66 | Signal.INF_FREQ, 67 | Signal.SUP_FREQ, 68 | Signal.MODE, 69 | Signal.INF_BAND, 70 | Signal.SUP_BAND, 71 | Signal.LOCATION, 72 | Signal.URL, 73 | Signal.DESCRIPTION, 74 | Signal.MODULATION, 75 | Signal.CATEGORY_CODE, 76 | Signal.ACF) 77 | DELIMITER = "*" 78 | STRINGS = (Signal.INF_FREQ, 79 | Signal.SUP_FREQ, 80 | Signal.MODE, 81 | Signal.INF_BAND, 82 | Signal.SUP_BAND, 83 | Signal.CATEGORY_CODE, 84 | Signal.ACF,) 85 | 86 | 87 | class ForecastColors: 88 | """Container class for the forecast labels colors.""" 89 | 90 | WARNING_COLOR = "#F95423" 91 | KP9_COLOR = "#FFCCCB" 92 | KP8_COLOR = "#FFCC9A" 93 | KP7_COLOR = "#FFFECD" 94 | KP6_COLOR = "#CDFFCC" 95 | KP5_COLOR = "#BEE3FE" 96 | 97 | 98 | _Band = namedtuple("Band", ["lower", "upper"]) 99 | 100 | 101 | class Constants: 102 | """Container class for several constants of the software.""" 103 | 104 | EXECUTABLE_NAME = os.path.join(__BASE_FOLDER__, "Artemis") 105 | UPDATER_SOFTWARE = os.path.join(__BASE_FOLDER__, "_ArtemisUpdater") 106 | CLICK_TO_UPDATE_STR = "Click to update" 107 | VERSION_LINK = "https://aresvalley.com/Storage/Artemis/Package/latest_versions.json" 108 | SIGIDWIKI = "https://www.sigidwiki.com/wiki/Signal_Identification_Guide" 109 | ADD_SIGNAL_LINK = "https://www.sigidwiki.com/index.php/Special:FormEdit/Signal/?preload=Signal_Identification_Wiki:Signal_form_preload_text" 110 | FORUM_LINK = "https://aresvalley.com/community/" 111 | ARESVALLEY_LINK = "https://aresvalley.com/" 112 | GITHUB_REPO = "https://github.com/AresValley/Artemis" 113 | RTL_SDL_LINK = "https://www.rtl-sdr.com/" 114 | UPDATING_STR = "Updating..." 115 | ACF_DOCS = "https://aresvalley.com/documentation/" 116 | FORECAST_PROBABILITIES = "https://services.swpc.noaa.gov/text/sgarf.txt" 117 | SPACE_WEATHER_XRAY = "https://services.swpc.noaa.gov/json/goes/primary/xrays-1-day.json" 118 | SPACE_WEATHER_PROT_EL = "https://services.swpc.noaa.gov/json/goes/primary/integral-protons-1-day.json" 119 | SPACE_WEATHER_AK_INDEX = "https://services.swpc.noaa.gov/text/wwv.txt" 120 | SPACE_WEATHER_SGAS = "https://services.swpc.noaa.gov/text/sgas.txt" 121 | SPACE_WEATHER_GEO_STORM = "https://services.swpc.noaa.gov/text/3-day-forecast.txt" 122 | SPACE_WEATHER_INFO = "https://www.swpc.noaa.gov/sites/default/files/images/NOAAscales.pdf" 123 | SPACE_WEATHER_IMGS = ["https://www.mmmonvhf.de/eme/eme.png", 124 | "https://www.mmmonvhf.de/ms/ms.png", 125 | "https://www.mmmonvhf.de/es/es.png", 126 | "https://www.mmmonvhf.de/solar/solar.png", 127 | "https://amunters.home.xs4all.nl/eskip50status.gif", 128 | "https://amunters.home.xs4all.nl/eskip70status.gif", 129 | "https://amunters.home.xs4all.nl/eskipstatus.gif", 130 | "https://amunters.home.xs4all.nl/eskipstatusNA.gif", 131 | "https://amunters.home.xs4all.nl/aurorastatus.gif"] 132 | SEARCH_LABEL_IMG = "search_icon.png" 133 | VOLUME_LABEL_IMG = "volume.png" 134 | SPECTRA_EXT = ".png" 135 | ACTIVE = "active" 136 | INACTIVE = "inactive" 137 | LABEL_ON_COLOR = "on" 138 | LABEL_OFF_COLOR = "off" 139 | TEXT_COLOR = "text" 140 | _ELF = _Band(0, 30) # Formally it is (3, 30) Hz. 141 | _SLF = _Band(30, 300) 142 | _ULF = _Band(300, 3000) 143 | _VLF = _Band(3000, 30000) 144 | _LF = _Band(30 * 10**3, 300 * 10**3) 145 | _MF = _Band(300 * 10 ** 3, 3000 * 10**3) 146 | _HF = _Band(3 * 10**6, 30 * 10**6) 147 | _VHF = _Band(30 * 10**6, 300 * 10**6) 148 | _UHF = _Band(300 * 10**6, 3000 * 10**6) 149 | _SHF = _Band(3 * 10**9, 30 * 10**9) 150 | _EHF = _Band(30 * 10**9, 300 * 10**9) 151 | BANDS = (_ELF, _SLF, _ULF, _VLF, _LF, _MF, _HF, _VHF, _UHF, _SHF, _EHF) 152 | MAX_DIGITS = 3 153 | RANGE_SEPARATOR = ' ÷ ' 154 | GFD_SITE = "http://qrg.globaltuners.com/" 155 | CONVERSION_FACTORS = {"Hz": 1, 156 | "kHz": 1000, 157 | "MHz": 1000000, 158 | "GHz": 1000000000} 159 | MODES = {"FM": ("NFM", "WFM"), 160 | "AM": (), 161 | "CW": (), 162 | "SK": ("FSK", "PSK", "MSK"), 163 | "SB": ("LSB", "USB", "DSB"), 164 | "Chirp Spread Spectrum": (), 165 | "FHSS-TDM": (), 166 | "RAW": (), 167 | "SC-FDMA": ()} 168 | APPLY = "Apply" 169 | REMOVE = "Remove" 170 | UNKNOWN = "N/A" 171 | EXTRACTING_MSG = "Extracting..." 172 | EXTRACTING_CODE = -1 173 | ZERO_INITIAL_SPEED = -1 174 | ZERO_FINAL_SPEED = -2 175 | NOT_AVAILABLE = "spectrumnotavailable.png" 176 | NOT_SELECTED = "nosignalselected.png" 177 | FIELD_SEPARATOR = ";" 178 | ACF_SEPARATOR = " - " 179 | DATA_FOLDER = os.path.join(__BASE_FOLDER__, "Data") 180 | SPECTRA_FOLDER = os.path.join(DATA_FOLDER, "Spectra") 181 | AUDIO_FOLDER = os.path.join(DATA_FOLDER, "Audio") 182 | DEFAULT_IMGS_FOLDER = os.path.join(":", "pics", "default_pics") 183 | DEFAULT_NOT_SELECTED = os.path.join(DEFAULT_IMGS_FOLDER, NOT_SELECTED) 184 | DEFAULT_NOT_AVAILABLE = os.path.join(DEFAULT_IMGS_FOLDER, NOT_AVAILABLE) 185 | FONT_FILE = os.path.join(__BASE_FOLDER__, 'font.json') 186 | SETTINGS_FILE = os.path.join(__BASE_FOLDER__, "settings.json") 187 | 188 | 189 | class Messages: 190 | """Container class for messages to be displayed.""" 191 | 192 | FEATURE_NOT_AVAILABLE = "Feature not available" 193 | SCRIPT_NOT_UPDATE = "When running from source, software updates\ncannot be checked." 194 | UPDATES_AVAILABALE = "Updates available" 195 | UPDATES_MSG = "Do you want to install the updates now?" 196 | UP_TO_DATE = "Already up to date" 197 | UP_TO_DATE_MSG = "No newer version to download." 198 | DB_NEW_VER = "New version available" 199 | DB_NEW_VER_MSG = "A new version of the database is available for download." 200 | NO_DB_AVAIL = "No database detected." 201 | NO_DB = "No database" 202 | DOWNLOAD_NOW_QUESTION = "Do you want to download it now?" 203 | DOWNLOAD_ANYWAY_QUESTION = "Do you want to download it anyway?" 204 | NO_CONNECTION = "No connection" 205 | NO_CONNECTION_MSG = "Unable to establish an internet connection." 206 | BAD_DOWNLOAD = "Something went wrong" 207 | BAD_DOWNLOAD_MSG = "Something went wrong with the download.\nCheck your internet connection and try again." 208 | SLOW_CONN = "Slow internet connection" 209 | SLOW_CONN_MSG = "Your internet connection is unstable or too slow." 210 | NEW_VERSION_AVAILABLE = "New software version" 211 | NEW_VERSION_MSG = lambda v: f"The software version {v} is available." # noqa: E731 212 | DOWNLOAD_SUGG_MSG = "Download new version now?" 213 | SCREEN_UPDATE_FAIL = "Unable to update the data" 214 | SCREEN_UPDATE_FAIL_MSG = "Downloaded data currupted or invalid" 215 | 216 | 217 | class ThemeConstants: 218 | """Container class for all the theme-related constants.""" 219 | 220 | EXTENSION = ".qss" 221 | ICONS_FOLDER = "icons" 222 | DEFAULT = "dark" 223 | COLORS = "colors.txt" 224 | COLOR_SEPARATOR = "=" 225 | DEFAULT_ACTIVE_COLOR = "#000000" 226 | DEFAULT_INACTIVE_COLOR = "#9f9f9f" 227 | DEFAULT_OFF_COLORS = "#000000", "#434343" 228 | DEFAULT_ON_COLORS = "#4b79a1", "#283e51" 229 | DEFAULT_TEXT_COLOR = "#ffffff" 230 | THEME_NOT_FOUND = "Theme not found" 231 | MISSING_THEME = "Missing theme folder." 232 | MISSING_THEME_FOLDER = "Themes folder not found.\nOnly the basic theme is available." 233 | THEME_FOLDER_NOT_FOUND = "Themes folder not found" 234 | FOLDER = os.path.join(__BASE_FOLDER__, "themes") 235 | DEFAULT_ICONS_PATH = os.path.join(FOLDER, DEFAULT, ICONS_FOLDER) 236 | DEFAULT_SEARCH_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.SEARCH_LABEL_IMG) 237 | DEFAULT_VOLUME_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.VOLUME_LABEL_IMG) 238 | DEFAULT_THEME_PATH = os.path.join(FOLDER, DEFAULT) 239 | -------------------------------------------------------------------------------- /src/default_imgs.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | default_pics/Artemis3.500px.png 4 | 5 | 6 | default_pics/nosignalselected.png 7 | default_pics/spectrumnotavailable.png 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/default_pics/Artemis3.500px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/default_pics/Artemis3.500px.png -------------------------------------------------------------------------------- /src/default_pics/Artemis3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/default_pics/Artemis3.ico -------------------------------------------------------------------------------- /src/default_pics/Artemis3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/default_pics/Artemis3.png -------------------------------------------------------------------------------- /src/default_pics/nosignalselected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/default_pics/nosignalselected.png -------------------------------------------------------------------------------- /src/default_pics/spectrumnotavailable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/default_pics/spectrumnotavailable.png -------------------------------------------------------------------------------- /src/double_text_button.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QPushButton 2 | from PyQt5.QtCore import pyqtSlot 3 | 4 | 5 | class DoubleTextButton(QPushButton): 6 | """Subclass QPushButton. 7 | 8 | A click will deactivate/activate a series of 'slave' widgets depending 9 | on the 'checked' status of the button.""" 10 | 11 | def __init__(self, parent=None): 12 | """Extends QPushButton.__init__.""" 13 | super().__init__(parent) 14 | self.clicked.connect(self._manage_click) 15 | 16 | def set_texts(self, text_a, text_b): 17 | """Set the two texts to be displayed.""" 18 | self._text_a = text_a 19 | self._text_b = text_b 20 | 21 | def set_slave_filters(self, simple_ones=None, 22 | radio_1=None, 23 | ruled_by_radio_1=None, 24 | radio_2=None, 25 | ruled_by_radio_2=None): 26 | """Set all the 'slave' widgets. 27 | 28 | Keyword arguments: 29 | simple_ones -- a list of widgets. 30 | radio_1 -- a radio button. 31 | ruled_by_radio_1 -- a list of widgets whose status depend upon radio_1. 32 | radio_2 -- a radio button. 33 | ruled_by_radio_2 -- a list of widgets whose status depend upon radio_2.""" 34 | self._simple_ones = simple_ones 35 | self._ruled_by_radio_1 = ruled_by_radio_1 36 | self._radio_1 = radio_1 37 | self._ruled_by_radio_2 = ruled_by_radio_2 38 | self._radio_2 = radio_2 39 | 40 | @pyqtSlot() 41 | def _manage_click(self): 42 | """Set the status of all the 'slave widgets' based on the status of the instance.""" 43 | if self.isChecked(): 44 | self.setText(self._text_b) 45 | enable = False 46 | else: 47 | self.setText(self._text_a) 48 | enable = True 49 | for f in self._simple_ones: 50 | f.setEnabled(enable) 51 | radio_btns = self._radio_1, self._radio_2 52 | ruled_widgets = self._ruled_by_radio_1, self._ruled_by_radio_2 53 | for radio_btn, ruled_by in zip(radio_btns, ruled_widgets): 54 | if ruled_by: 55 | for f in ruled_by: 56 | if radio_btn.isChecked(): 57 | f.setEnabled(enable) 58 | else: 59 | f.setEnabled(False) 60 | -------------------------------------------------------------------------------- /src/download_db_window.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 185 11 | 12 | 13 | 14 | Downloading 15 | 16 | 17 | 18 | :/icon/default_pics/Artemis3.500px.png:/icon/default_pics/Artemis3.500px.png 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 12 29 | 30 | 31 | 32 | Downloading updates 33 | Please wait... 34 | 35 | 36 | 37 | Qt::AlignCenter 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 12 46 | 47 | 48 | 49 | status 50 | 51 | 52 | Qt::AlignCenter 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 12 61 | 62 | 63 | 64 | Speed 65 | 66 | 67 | Qt::AlignCenter 68 | 69 | 70 | 71 | 72 | 73 | 74 | 0 75 | 76 | 77 | 0 78 | 79 | 80 | -1 81 | 82 | 83 | false 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 12 92 | 93 | 94 | 95 | 96 | 97 | 98 | Cancel 99 | 100 | 101 | true 102 | 103 | 104 | false 105 | 106 | 107 | false 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/download_window.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import uic 2 | from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal 3 | from PyQt5.QtWidgets import QWidget 4 | from threads import DownloadThread, ThreadStatus 5 | from utilities import pop_up 6 | from executable_utilities import resource_path 7 | from constants import Constants, Messages 8 | 9 | 10 | Ui_Download_window, _ = uic.loadUiType( 11 | resource_path("download_db_window.ui") 12 | ) 13 | 14 | 15 | class DownloadWindow(QWidget, Ui_Download_window): 16 | """Subclass QWidget and Ui_Download_window. It is the window displayed during 17 | downloads and software updates.""" 18 | 19 | complete = pyqtSignal() 20 | closed = pyqtSignal() 21 | _PROGRESS_CONEVERSION_FACTOR = 1024 22 | 23 | def __init__(self): 24 | """Initialize the window.""" 25 | super().__init__() 26 | self.setupUi(self) 27 | self.setWindowFlags( 28 | # Qt.Window | 29 | Qt.CustomizeWindowHint | 30 | Qt.WindowTitleHint | 31 | Qt.WindowCloseButtonHint | 32 | Qt.WindowStaysOnTopHint 33 | ) 34 | 35 | self._no_internet_msg = pop_up(self, title=Messages.NO_CONNECTION, 36 | text=Messages.NO_CONNECTION_MSG, 37 | connection=self.close) 38 | 39 | self._bad_db_download_msg = pop_up(self, title=Messages.BAD_DOWNLOAD, 40 | text=Messages.BAD_DOWNLOAD_MSG, 41 | connection=self.close) 42 | 43 | self._slow_conn_msg = pop_up(self, title=Messages.SLOW_CONN, 44 | text=Messages.SLOW_CONN_MSG, 45 | connection=self.close) 46 | 47 | self._download_thread = DownloadThread() 48 | self._download_thread.finished.connect(self._wait_close) 49 | self._download_thread.progress.connect(self._display_progress) 50 | self._download_thread.speed_progress.connect(self._display_speed) 51 | self.closed.connect(self._download_thread.set_exit) 52 | self.cancel_btn.clicked.connect(self._terminate_process) 53 | self._size = 0 54 | self.target = None 55 | 56 | def _prepare_progress_bar(self, size): 57 | """Prepare the progress bar for the upcoming download.""" 58 | self._progress_bar.setMinimum(0) 59 | self._progress_bar.setMaximum(size) 60 | self._progress_bar.setValue(0) 61 | 62 | def activate(self, target): 63 | """Start the download thread.""" 64 | self._size = target.size 65 | self.target = target.target 66 | self._prepare_progress_bar(target.size) 67 | self._download_thread.start(target) 68 | self.show() 69 | 70 | def _download_format_str(self, n): 71 | """Return a well-formatted string with the downloaded MB.""" 72 | return f"Downloaded: {n} MB" 73 | 74 | @pyqtSlot(float) 75 | def _display_speed(self, speed): 76 | """Display the download speed.""" 77 | ret = "Speed: " 78 | if speed == Constants.ZERO_INITIAL_SPEED: 79 | ret += "Calculating..." 80 | elif speed == 0.0: 81 | ret += "VERY SLOW" 82 | elif speed == Constants.ZERO_FINAL_SPEED: 83 | ret = "" 84 | else: 85 | ret += f"{speed} MB/s" 86 | self.speed_lbl.setText(ret) 87 | 88 | @pyqtSlot(int) 89 | def _display_progress(self, progress): 90 | """Display the downloaded MB.""" 91 | if progress != Constants.EXTRACTING_CODE: 92 | self.status_lbl.setText(self._download_format_str(progress)) 93 | elif progress == Constants.EXTRACTING_CODE: 94 | self.status_lbl.setText(Constants.EXTRACTING_MSG) 95 | if self._size > 0: 96 | self._progress_bar.setValue(progress * self._PROGRESS_CONEVERSION_FACTOR) 97 | 98 | def show(self): 99 | """Extends QWidget.show. Set downloaded MB and speed to zero.""" 100 | self._display_progress(0) 101 | self._display_speed(Constants.ZERO_INITIAL_SPEED) 102 | super().show() 103 | 104 | def _stop_thread(self): 105 | """Ask the download thread to stop.""" 106 | if self._download_thread.isRunning(): 107 | self.closed.emit() 108 | self._download_thread.wait() 109 | 110 | @pyqtSlot() 111 | def _terminate_process(self): 112 | """Terminate the download thread and close.""" 113 | self._stop_thread() 114 | self.close() 115 | 116 | @pyqtSlot() 117 | def _wait_close(self): 118 | """Decide the action based on the download thread status and close.""" 119 | if self._download_thread.status is ThreadStatus.OK: 120 | self.complete.emit() 121 | self.close() 122 | elif self._download_thread.status is ThreadStatus.NO_CONNECTION_ERR: 123 | self._no_internet_msg.show() 124 | elif self._download_thread.status is ThreadStatus.BAD_DOWNLOAD_ERR: 125 | self._bad_db_download_msg.show() 126 | elif self._download_thread.status is ThreadStatus.SLOW_CONN_ERR: 127 | self._slow_conn_msg.show() 128 | else: 129 | self.close() 130 | 131 | def reject(self): 132 | """Extends QWidget.reject. Terminate the download thread.""" 133 | self._stop_thread() 134 | super().reject() 135 | -------------------------------------------------------------------------------- /src/downloadtargetfactory.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from shutil import rmtree 3 | from os import remove 4 | import os.path 5 | import stat 6 | from constants import ( 7 | Constants, 8 | Database, 9 | __BASE_FOLDER__, 10 | ThemeConstants, 11 | DownloadTarget, 12 | SupportedOs, 13 | ) 14 | from os_utilities import get_os 15 | from web_utilities import get_folder_hash_code 16 | 17 | from zipfile import ZipFile 18 | from tarfile import TarFile 19 | 20 | 21 | class _ZipExtractor: 22 | """Extractor class for zip files. 23 | 24 | Exposes a static method which can be used as a context manager.""" 25 | @staticmethod 26 | @contextmanager 27 | def open(fileobj): 28 | zipped = ZipFile(fileobj) 29 | try: 30 | yield zipped 31 | finally: 32 | zipped.close() 33 | 34 | 35 | class _TarExtractor: 36 | """Extractor class for tar files. 37 | 38 | Exposes a static method which can be used as a context manager.""" 39 | @staticmethod 40 | @contextmanager 41 | def open(fileobj): 42 | tarfile = TarFile.open(fileobj=fileobj) 43 | try: 44 | yield tarfile 45 | finally: 46 | tarfile.close() 47 | 48 | 49 | EXTRACTORS = { 50 | SupportedOs.WINDOWS: _ZipExtractor, 51 | SupportedOs.LINUX: _TarExtractor, 52 | SupportedOs.RASPBIAN: _TarExtractor, 53 | # No extractor for MacOs, just download the file through the browser. 54 | } 55 | 56 | 57 | def _on_rmtree_error(func, path, excinfo): 58 | """Function called whenever rmtree fails.""" 59 | os.chmod(path, stat.S_IWRITE) 60 | func(path) 61 | 62 | 63 | def _delete_data_folder(): 64 | """Delete the Data folder.""" 65 | if os.path.exists(Constants.DATA_FOLDER): 66 | rmtree(Constants.DATA_FOLDER, onerror=_on_rmtree_error) 67 | 68 | 69 | def _delete_updater(): 70 | """Delete the updater program.""" 71 | if os.path.exists(Constants.UPDATER_SOFTWARE): 72 | remove(Constants.UPDATER_SOFTWARE) 73 | 74 | 75 | def _delete_software(): 76 | """Delete the main program and the themes folder.""" 77 | if os.path.exists(Constants.EXECUTABLE_NAME): 78 | remove(Constants.EXECUTABLE_NAME) # Remove Artemis executable. 79 | if os.path.exists(ThemeConstants.FOLDER): # One could not have the theme folder for some reason. 80 | rmtree(ThemeConstants.FOLDER, onerror=_on_rmtree_error) 81 | 82 | 83 | class _DataFolderInfo: 84 | """Simple class to implement the interface of a 'target' object for the data folder: 85 | 86 | - url; 87 | - hash_code; 88 | - size.""" 89 | def __init__(self): 90 | self.url = Database.LINK_LOC 91 | self.hash_code = get_folder_hash_code() 92 | self.size = 0 93 | 94 | 95 | class _BaseDownloadTarget: 96 | """Base class for the '_Download*Target' objects. 97 | 98 | Contains all the attributes needed by DownloadWindow and DownloadThread 99 | to do the job.""" 100 | def __init__(self, target, dest_path, target_enum, Extractor, delete_files): 101 | self.url = target.url 102 | self.hash_code = target.hash_code 103 | self.size = target.size 104 | self.dest_path = dest_path 105 | self.target = target_enum 106 | self.Extractor = Extractor 107 | self.delete_files = delete_files 108 | 109 | 110 | class _DownloadDataFolderTarget(_BaseDownloadTarget): 111 | """Extend _BaseDownloadTarget. Represent the data folder.""" 112 | def __init__(self, data_folder_info, dest_path=__BASE_FOLDER__): 113 | super().__init__( 114 | target=data_folder_info, 115 | dest_path=dest_path, 116 | target_enum=DownloadTarget.DATA_FOLDER, 117 | Extractor=_ZipExtractor, 118 | delete_files=_delete_data_folder 119 | ) 120 | 121 | 122 | class _DownloadSoftwareTarget(_BaseDownloadTarget): 123 | """Extends _BaseDownloadTarget. Represents the main software.""" 124 | def __init__(self, software, dest_path=__BASE_FOLDER__): 125 | super().__init__( 126 | target=software, 127 | dest_path=dest_path, 128 | target_enum=DownloadTarget.SOFTWARE, 129 | Extractor=EXTRACTORS[get_os()], 130 | delete_files=_delete_software 131 | ) 132 | 133 | 134 | class _DownloadUpdaterTarget(_BaseDownloadTarget): 135 | """Extends _BaseDownloadTarget. Represents the updater software.""" 136 | def __init__(self, updater, dest_path=__BASE_FOLDER__): 137 | super().__init__( 138 | target=updater, 139 | dest_path=dest_path, 140 | target_enum=DownloadTarget.UPDATER, 141 | Extractor=EXTRACTORS[get_os()], 142 | delete_files=_delete_updater 143 | ) 144 | 145 | 146 | def get_download_target(target_type, target=None): 147 | """Return a Download*Obj based on the target download. 148 | 149 | These objects expose a common interface: 150 | Attributes: 151 | - url; 152 | - hash_code; 153 | - dest_path; 154 | - target: an element of the enum DownloadTarget; 155 | - Extractor: an object which exposes an 'open(fileobj)' method 156 | to extract compressed files; 157 | - delete_files: a function to remove the old files.""" 158 | if target_type is DownloadTarget.DATA_FOLDER: 159 | return _DownloadDataFolderTarget(_DataFolderInfo()) 160 | elif target_type is DownloadTarget.UPDATER and target is not None: 161 | return _DownloadUpdaterTarget(target) 162 | elif target_type is DownloadTarget.SOFTWARE and target is not None: 163 | return _DownloadSoftwareTarget(target) 164 | else: 165 | raise Exception("ERROR: Invalid download target!") 166 | -------------------------------------------------------------------------------- /src/executable_utilities.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from shutil import which 3 | import os 4 | import os.path 5 | 6 | 7 | def _is_executable_version(): 8 | """Return whether the binary version is running.""" 9 | return hasattr(sys, "_MEIPASS") or "__compiled__" in globals() 10 | 11 | 12 | IS_BINARY = _is_executable_version() 13 | 14 | 15 | def get_executable_path(): 16 | """Check whether the executable is in the PATH folder. 17 | 18 | Return the full path or just an ampty string if it is not found 19 | in the PATH folder.""" 20 | path = which("Artemis") 21 | if path is not None: 22 | return os.path.dirname(path) 23 | else: # Assume that the executable is in the cwd. 24 | return os.curdir 25 | 26 | 27 | def resource_path(relative_path): 28 | """Get absolute path to resource, works for dev and for PyInstaller.""" 29 | try: 30 | base_path = sys._MEIPASS 31 | except Exception: 32 | base_path = os.path.abspath(".") 33 | return os.path.join(base_path, relative_path) 34 | -------------------------------------------------------------------------------- /src/fixed_aspect_ratio_label.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QLabel 2 | from PyQt5.QtCore import Qt 3 | 4 | 5 | class FixedAspectRatioLabel(QLabel): 6 | """Subclass QLabel. A resizable label class.""" 7 | 8 | def __init__(self, parent=None): 9 | """Initialize the instance. Set the pixmap to None.""" 10 | super().__init__(parent) 11 | self.pixmap = None 12 | 13 | def set_default_stylesheet(self): 14 | """Set the initial stylesheet of the label.""" 15 | self.setStyleSheet("""border-width: 1px; 16 | border-style: solid; 17 | border-color: black;""") 18 | 19 | def make_transparent(self): 20 | """Make the label transparent. 21 | 22 | Remove text and border.""" 23 | self.setText('') 24 | self.setStyleSheet("border-width: 0px;") 25 | 26 | def apply_pixmap(self): 27 | """Apply a scaled pixmap without modifying the dimension of the original one.""" 28 | if self.pixmap: 29 | self.setPixmap( 30 | self.pixmap.scaled( 31 | self.size(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation 32 | ) 33 | ) 34 | 35 | def rescale(self, size): 36 | """Rescale the widget and the displayed pixmap to the given size.""" 37 | self.resize(size) 38 | self.apply_pixmap() 39 | -------------------------------------------------------------------------------- /src/fixed_aspect_ratio_widget.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QWidget 2 | from PyQt5.QtCore import QSize 3 | 4 | 5 | class FixedAspectRatioWidget(QWidget): 6 | """Subclass QWidget. Keep all the internal labels to a fixed aspect ratio.""" 7 | 8 | SPACE = 10 9 | 10 | def __init__(self, parent=None): 11 | """Initialize the instance.""" 12 | super().__init__(parent) 13 | self.labels = [] 14 | 15 | def resizeEvent(self, event): 16 | """Override QWidget.resizeEvent. Rescale all the internal widgets.""" 17 | h, w = self.height(), self.width() 18 | h_lbl = h / 9 - self.SPACE 19 | w_lbl = 5 * h_lbl 20 | w_pad = w - 10 21 | if w_lbl > w_pad: 22 | w_lbl = w_pad 23 | h_lbl = w_pad / 5 24 | 25 | for label in self.labels: 26 | label.rescale(QSize(w_lbl, h_lbl)) 27 | -------------------------------------------------------------------------------- /src/loggingconf.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.config 3 | from constants import __BASE_FOLDER__ 4 | import os.path 5 | 6 | """Import the module to initialize the logging configuration. 7 | 8 | It is imported only for its side effects.""" 9 | 10 | 11 | _LOGGING_CONFIG = { 12 | 'version': 1, 13 | 'formatters': { 14 | 'general': { 15 | 'format': '%(asctime)s::%(levelname)s::%(module)s::%(funcName)s::%(message)s', 16 | 'datefmt': '%d/%m/%Y %I:%M:%S %p', 17 | }, 18 | }, 19 | 'handlers': { 20 | 'console': { 21 | 'level': 'INFO', 22 | 'formatter': 'general', 23 | 'class': 'logging.StreamHandler', 24 | 'stream': 'ext://sys.stdout', 25 | }, 26 | 'file': { 27 | 'class': 'logging.FileHandler', 28 | 'level': 'ERROR', 29 | 'filename': os.path.join(__BASE_FOLDER__, 'info.log'), 30 | 'mode': 'w', 31 | 'encoding': 'utf8', 32 | 'formatter': 'general', 33 | }, 34 | }, 35 | 'root': { 36 | 'level': 'DEBUG', 37 | 'handlers': ['console', 'file'], 38 | }, 39 | # Add loggers if required 40 | # 'loggers': {} 41 | } 42 | 43 | logging.config.dictConfig(_LOGGING_CONFIG) 44 | -------------------------------------------------------------------------------- /src/os_utilities.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import platform 3 | from constants import SupportedOs 4 | 5 | 6 | def _is_mac_os(): 7 | """Return True if running OS is Mac.""" 8 | return sys.platform == 'darwin' 9 | 10 | 11 | def _is_win_os(): 12 | """Return True if running OS is Windows.""" 13 | return sys.platform == 'win32' 14 | 15 | 16 | def _is_linux_os(): 17 | """Return True if running OS is Linux.""" 18 | return sys.platform == 'linux' 19 | 20 | 21 | IS_MAC = _is_mac_os() 22 | IS_LINUX = _is_linux_os() 23 | IS_WINDOWS = _is_win_os() 24 | IS_RASPBIAN = IS_LINUX and 'arm' in platform.machine().lower() 25 | 26 | 27 | def get_os(): 28 | """Get the name of the current running operating system.""" 29 | if IS_WINDOWS: 30 | return SupportedOs.WINDOWS 31 | elif IS_LINUX: 32 | if IS_RASPBIAN: 33 | return SupportedOs.RASPBIAN 34 | return SupportedOs.LINUX 35 | elif IS_MAC: 36 | return SupportedOs.MAC 37 | else: 38 | return None 39 | -------------------------------------------------------------------------------- /src/settings.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from constants import Constants 3 | import json 4 | import logging 5 | 6 | 7 | class Settings: 8 | """Dynamically save and load the settings of the application.""" 9 | 10 | def __init__(self): 11 | self._dct = {} 12 | 13 | def load(self): 14 | """Load the setiings.json file.""" 15 | if not os.path.exists(Constants.SETTINGS_FILE): 16 | return 17 | try: 18 | with open(Constants.SETTINGS_FILE, 'r') as settings_file: 19 | self._dct = json.load(settings_file) 20 | except FileNotFoundError: 21 | logging.info("No settings.json file") 22 | pass # Invalid file. 23 | 24 | def save(self, **kwargs): 25 | """Save the settings.json file. 26 | 27 | Also update the current settings specified in kwargs. 28 | New settings can be dynamically added via this method.""" 29 | for k, v in kwargs.items(): 30 | self._dct[k] = v 31 | with open(Constants.SETTINGS_FILE, mode='w') as settings_file: 32 | json.dump( 33 | self._dct, 34 | settings_file, 35 | sort_keys=True, 36 | indent=4 37 | ) 38 | 39 | def __getattr__(self, attr): 40 | """Return the corresponding setting. 41 | 42 | Return None if there is no such setting yet.""" 43 | return self._dct.get(attr, None) 44 | -------------------------------------------------------------------------------- /src/switchable_label.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QLabel 2 | from constants import ForecastColors 3 | 4 | 5 | class _BaseSwitchableLabel(QLabel): 6 | """Subclass QLabel. Base class for the switchable labels.""" 7 | 8 | def __init__(self, parent=None): 9 | """Set is_on to False and level to 0.""" 10 | super().__init__(parent) 11 | self.is_on = False 12 | self.level = 0 13 | 14 | def switch_on(self): 15 | """Set is_on to True.""" 16 | self.is_on = True 17 | 18 | def switch_off(self): 19 | """Set is_on to False.""" 20 | self.is_on = False 21 | 22 | 23 | class SwitchableLabel(_BaseSwitchableLabel): 24 | """Subclass _BaseSwitchableLabel.""" 25 | 26 | def __init__(self, parent=None): 27 | """Define text and colors attributes.""" 28 | super().__init__(parent) 29 | self.switch_on_colors = () 30 | self.switch_off_colors = () 31 | self.text_color = '' 32 | 33 | def switch_on(self): 34 | """Extend _BaseSwitchableLabel.switch_on. 35 | 36 | Apply the active state colors.""" 37 | super().switch_on() 38 | self._apply_colors(*self.switch_on_colors) 39 | 40 | def switch_off(self): 41 | """Extend _BaseSwitchableLabel.switch_off. 42 | 43 | Apply the inactive state colors.""" 44 | super().switch_off() 45 | self._apply_colors(*self.switch_off_colors) 46 | 47 | def _apply_colors(self, start, end): 48 | """Set text and background color of the label.""" 49 | self.setStyleSheet( 50 | f""" 51 | color:{self.text_color}; 52 | background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,stop:0 {start} ,stop: 1 {end}); 53 | """ 54 | ) 55 | 56 | 57 | class SingleColorSwitchableLabel(_BaseSwitchableLabel): 58 | """Subclass _BaseSwitchableLabel.""" 59 | 60 | THRESHOLD = 30 61 | 62 | def __init__(self, parent=None): 63 | """Set default active color.""" 64 | super().__init__(parent) 65 | self.active_color = ForecastColors.WARNING_COLOR 66 | 67 | def switch_on(self): 68 | """Extend _BaseSwitchableLabel.switch_on. 69 | 70 | Apply the active state color if level >= THRESHOLD.""" 71 | if self.level >= self.THRESHOLD: 72 | super().switch_on() 73 | self.setStyleSheet(f"color: {self.active_color}") 74 | 75 | def switch_off(self): 76 | """Extend _BaseSwitchableLabel.switch_off. 77 | 78 | Apply an empty stylesheet.""" 79 | super().switch_off() 80 | self.setStyleSheet("") 81 | 82 | 83 | class MultiColorSwitchableLabel(_BaseSwitchableLabel): 84 | """Subclass _BaseSwitchableLabel.""" 85 | 86 | LEVEL_COLORS = { 87 | 9: ForecastColors.KP9_COLOR, 88 | 8: ForecastColors.KP8_COLOR, 89 | 7: ForecastColors.KP7_COLOR, 90 | 6: ForecastColors.KP6_COLOR, 91 | 5: ForecastColors.KP5_COLOR 92 | } 93 | 94 | MIN_LEVEL = list(LEVEL_COLORS.keys())[-1] 95 | MAX_LEVEL = list(LEVEL_COLORS.keys())[0] 96 | 97 | def __init__(self, parent=None): 98 | """Initialize the instance.""" 99 | super().__init__(parent) 100 | 101 | def switch_on(self): 102 | """Extend _BaseSwitchableLabel.switch_on. 103 | 104 | Apply the active state color based on LEVEL_COLORS.""" 105 | if self.MIN_LEVEL <= self.level <= self.MAX_LEVEL: 106 | super().switch_on() 107 | self.setStyleSheet( 108 | f"""color: {self.LEVEL_COLORS[self.level]}; 109 | text-decoration: underline;""" 110 | ) 111 | 112 | def switch_off(self): 113 | """Extend _BaseSwitchableLabel.switch_off. 114 | 115 | Apply an empty stylesheet.""" 116 | super().switch_off() 117 | self.setStyleSheet("") 118 | 119 | 120 | class SwitchableLabelsIterable: 121 | """Iterable class of _BaseSwitchableLabel.""" 122 | 123 | def __init__(self, *labels): 124 | """Set the labels to iterate through.""" 125 | self.labels = labels 126 | 127 | def __iter__(self): 128 | """Define the iterator.""" 129 | for lab in self.labels: 130 | yield lab 131 | 132 | def switch_on(self, label): 133 | """Switch on the label 'label'. Switch off all the other labels.""" 134 | for lab in self.labels: 135 | if lab is label: 136 | lab.switch_on() 137 | else: 138 | lab.switch_off() 139 | 140 | def switch_off_all(self): 141 | """Switch off all the labels.""" 142 | for lab in self.labels: 143 | lab.switch_off() 144 | 145 | def set(self, attr, value): 146 | """Set the attribute 'attr' equal to 'value' for all the labels.""" 147 | for lab in self.labels: 148 | setattr(lab, attr, value) 149 | 150 | def refresh(self): 151 | """Refresh the state of all the labels. 152 | 153 | Used after the applied theme has changed.""" 154 | for lab in self.labels: 155 | if lab.is_on: 156 | lab.switch_on() 157 | else: 158 | lab.switch_off() 159 | -------------------------------------------------------------------------------- /src/themes/acqua/colors.txt: -------------------------------------------------------------------------------- 1 | active=#228eff 2 | inactive=#808086 3 | off=#3a7bd5, #3a6073 4 | on=#00d2ff, #928dab 5 | text=#ffffff -------------------------------------------------------------------------------- /src/themes/acqua/icons/down-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/acqua/icons/down-arrow.png -------------------------------------------------------------------------------- /src/themes/acqua/icons/down-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/acqua/icons/down-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/acqua/icons/down-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/acqua/icons/down-arrow_off.png -------------------------------------------------------------------------------- /src/themes/acqua/icons/search_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/acqua/icons/search_icon.png -------------------------------------------------------------------------------- /src/themes/acqua/icons/up-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/acqua/icons/up-arrow.png -------------------------------------------------------------------------------- /src/themes/acqua/icons/up-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/acqua/icons/up-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/acqua/icons/up-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/acqua/icons/up-arrow_off.png -------------------------------------------------------------------------------- /src/themes/acqua/icons/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/acqua/icons/volume.png -------------------------------------------------------------------------------- /src/themes/console_style/colors.txt: -------------------------------------------------------------------------------- 1 | active= #ff9900 2 | inactive= #616161 3 | on=#fc4a1a, #f7b733 4 | off= #000000, #434343 5 | text=#ffffff -------------------------------------------------------------------------------- /src/themes/console_style/console_style.qss: -------------------------------------------------------------------------------- 1 | /* 2 | Dark Console Style Sheet for QT Applications 3 | Author: Jaime A. Quiroga P. 4 | Company: GTRONICK 5 | Last updated: 24/05/2018, 17:12. 6 | Available at: https://github.com/GTRONICK/QSS/blob/master/ConsoleStyle.qss 7 | */ 8 | QWidget { 9 | background-color:rgb(0, 0, 0); 10 | color: rgb(240, 240, 240); 11 | border-color: rgb(58, 58, 58); 12 | } 13 | 14 | QPlainTextEdit { 15 | background-color:rgb(0, 0, 0); 16 | color: rgb(200, 200, 200); 17 | selection-background-color: rgb(255, 153, 0); 18 | selection-color: rgb(0, 0, 0); 19 | } 20 | 21 | QFrame[frameShape="4"], 22 | QFrame[frameShape="5"] 23 | { 24 | border: none; 25 | background: #FFFFFF; 26 | max-width: 1px; 27 | } 28 | 29 | QSlider::sub-page:horizontal { 30 | background-color: #ff9900; 31 | } 32 | QSlider::sub-page:vertical { 33 | background-color: #ff9900; 34 | } 35 | 36 | QComboBox { 37 | border: 0px solid transparent; 38 | border-radius: 2px; 39 | padding: 1px 6px 1px 6px; 40 | min-width: 2em; 41 | } 42 | 43 | QComboBox:!editable { 44 | selection-background-color: transparent; 45 | selection-color: #FFFFFF; 46 | background-color: transparent; 47 | } 48 | 49 | QComboBox:disabled { 50 | color: #616161; 51 | } 52 | 53 | QComboBox:!editable:on, QComboBox::drop-down:editable:on { 54 | color: #ffffff; 55 | background-color: transparent; 56 | selection-background-color: transparent; 57 | } 58 | 59 | QComboBox:on { 60 | padding-top: 3px; 61 | padding-left: 4px; 62 | } 63 | 64 | QComboBox::drop-down { 65 | background-color: transparent; 66 | subcontrol-origin: padding; 67 | subcontrol-position: top right; 68 | width: 20px; 69 | border-top-right-radius: 2px; 70 | border-bottom-right-radius: 2px; 71 | } 72 | 73 | QComboBox::down-arrow:enabled { 74 | image: url("./themes/console_style/icons/down-arrow.png"); 75 | } 76 | 77 | QComboBox::down-arrow:disabled { 78 | image: url("./themes/console_style/icons/down-arrow_off.png"); 79 | } 80 | 81 | QComboBox::down-arrow:hover { 82 | image: url("./themes/console_style/icons/down-arrow_hover.png"); 83 | } 84 | 85 | QComboBox::down-arrow:on { 86 | top: 1px; 87 | left: 1px; 88 | } 89 | 90 | QComboBox QAbstractItemView { 91 | background-color: #232629; 92 | } 93 | 94 | QListWidget { 95 | background-color: transparent; 96 | border: 0px solid transparent; 97 | border-bottom: 2px solid #ff9900; 98 | color: #ffffff; 99 | } 100 | 101 | QListView { 102 | background-color: transparent; 103 | color: #ffffff; 104 | outline: 0; 105 | border: 0px solid transparent; 106 | } 107 | QListView::item:hover { 108 | color: rgb(200, 200, 200); 109 | background: transparent; 110 | } 111 | 112 | QListView::item:selected { 113 | color: #ff9900; 114 | background: transparent; 115 | } 116 | 117 | QListView::item:disabled { 118 | color: #616161; 119 | background: transparent; 120 | } 121 | 122 | QListView::item:disabled:selected { 123 | color: #ff9900; 124 | background: transparent; 125 | } 126 | 127 | QScrollBar:horizontal { 128 | background: transparent; 129 | height: 10px; 130 | margin: 0; 131 | } 132 | 133 | QScrollBar:vertical { 134 | background: transparent; 135 | width: 10px; 136 | margin: 0; 137 | } 138 | 139 | QScrollBar::handle:horizontal { 140 | background: #616161; 141 | min-width: 16px; 142 | border-radius: 5px; 143 | } 144 | 145 | QScrollBar::handle:vertical { 146 | background: #616161; 147 | min-height: 16px; 148 | border-radius: 5px; 149 | } 150 | 151 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, 152 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 153 | background: none; 154 | } 155 | 156 | QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, 157 | QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { 158 | border: none; 159 | background: none; 160 | } 161 | 162 | QLineEdit { 163 | border-style: outset; 164 | border-radius: 10px; 165 | border-width: 1px; 166 | border-color: #ff9900; 167 | background-color: transparent; 168 | color: #ffffff; 169 | } 170 | 171 | QTreeView { 172 | background-color: transparent; 173 | selection-background-color: transparent; 174 | border: 0px; 175 | } 176 | 177 | QTreeView::item { 178 | background-color: transparent; 179 | color: #ffffff; 180 | } 181 | 182 | QTreeView::item:hover { 183 | border-right: 2px solid #ff9900; 184 | color: rgb(200, 200, 200); 185 | } 186 | 187 | QTreeView::item:selected { 188 | color: #ff9900; 189 | } 190 | 191 | QTreeView::item:active{ 192 | background: transparent; 193 | } 194 | 195 | QTreeView::item:disabled{ 196 | color: rgb(200, 200, 200); 197 | } 198 | 199 | QTreeView::item:selected:disabled{ 200 | color: #ff9900; 201 | } 202 | 203 | QTabWidget::pane { 204 | border-top: 1px solid #000000; 205 | } 206 | 207 | QSpinBox { 208 | background-color: transparent; 209 | color: #ffffff; 210 | border-width: 0px; 211 | } 212 | 213 | QSpinBox:disabled { 214 | color: #616161; 215 | border-width: 0px; 216 | } 217 | 218 | QSpinBox::up-button { 219 | subcontrol-origin: border; 220 | subcontrol-position: top right; 221 | width: 16px; 222 | image: url("./themes/console_style/icons/up-arrow.png"); 223 | border-width: 0px; 224 | } 225 | 226 | QSpinBox::up-button:hover { 227 | image: url("./themes/console_style/icons/up-arrow_hover.png"); 228 | } 229 | 230 | QSpinBox::up-button:pressed { 231 | image: url("./themes/console_style/icons/up-arrow.png"); 232 | } 233 | 234 | QSpinBox::up-button:disabled { 235 | image: url("./themes/console_style/icons/up-arrow_off.png"); 236 | } 237 | 238 | QSpinBox::down-button { 239 | subcontrol-origin: border; 240 | subcontrol-position: bottom right; 241 | width: 16px; 242 | image: url("./themes/console_style/icons/down-arrow.png"); 243 | border-width: 0px; 244 | border-top-width: 0; 245 | } 246 | 247 | QSpinBox::down-button:hover { 248 | image: url("./themes/console_style/icons/down-arrow_hover.png"); 249 | } 250 | 251 | QSpinBox::down-button:pressed { 252 | image: url("./themes/console_style/icons/down-arrow.png"); 253 | } 254 | 255 | QSpinBox::down-button:disabled { 256 | image: url("./themes/console_style/icons/down-arrow_off.png"); 257 | } 258 | 259 | QTabBar::tab { 260 | background-color:rgb(0, 0, 0); 261 | border-style: outset; 262 | border-width: 1px; 263 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 264 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 265 | border-bottom-color: rgb(58, 58, 58); 266 | border-bottom-width: 1px; 267 | border-top-width: 0px; 268 | border-style: solid; 269 | color: rgb(255, 153, 0); 270 | padding: 4px; 271 | } 272 | 273 | QTabBar::tab:selected, QTabBar::tab:hover { 274 | color: rgb(255, 255, 255); 275 | background-color:rgb(0, 0, 0); 276 | border-color:rgb(42, 42, 42); 277 | margin-left: 0px; 278 | margin-right: 0px; 279 | border-bottom-right-radius:4px; 280 | border-bottom-left-radius:4px; 281 | } 282 | 283 | QTabBar::tab:last:selected { 284 | background-color:rgb(0, 0, 0); 285 | border-color:rgb(42, 42, 42); 286 | margin-left: 0px; 287 | margin-right: 0px; 288 | border-bottom-right-radius:4px; 289 | border-bottom-left-radius:4px; 290 | } 291 | 292 | QTabBar::tab:!selected { 293 | margin-bottom: 4px; 294 | border-bottom-right-radius:4px; 295 | border-bottom-left-radius:4px; 296 | } 297 | 298 | QPushButton{ 299 | /* border-style: outset; 300 | border-width: 2px; */ 301 | /* border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 302 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 303 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 304 | border-bottom-color: rgb(58, 58, 58); 305 | border-bottom-width: 1px; 306 | border-style: solid; */ 307 | color: rgb(255, 255, 255); 308 | padding: 6px; 309 | /* background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255)); */ 310 | background-color: transparent; 311 | } 312 | 313 | QPushButton:hover{ 314 | /* border-style: outset; 315 | border-width: 2px; 316 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); 317 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); 318 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); 319 | border-bottom-color: rgb(115, 115, 115); 320 | border-bottom-width: 1px; 321 | border-style: solid; */ 322 | color: rgb(200, 200, 200); 323 | padding: 6px; 324 | /* background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(107, 107, 107, 255), stop:1 rgba(157, 157, 157, 255)); */ 325 | background-color: transparent; 326 | } 327 | 328 | QPushButton:pressed{ 329 | /* border-style: outset; 330 | border-width: 2px; 331 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(62, 62, 62, 255), stop:1 rgba(22, 22, 22, 255)); 332 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 333 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 334 | border-bottom-color: rgb(58, 58, 58); 335 | border-bottom-width: 1px; 336 | border-style: solid; */ 337 | color: rgb(255, 255, 255); 338 | padding: 6px; 339 | /* background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255)); */ 340 | background-color: transparent; 341 | } 342 | 343 | QPushButton:disabled{ 344 | /* border-style: outset; 345 | border-width: 2px; 346 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 347 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 348 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 349 | border-bottom-color: rgb(58, 58, 58); 350 | border-bottom-width: 1px; 351 | border-style: solid; */ 352 | color: #616161; 353 | padding: 6px; 354 | /* background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(57, 57, 57, 255), stop:1 rgba(77, 77, 77, 255)); */ 355 | background-color: transparent; 356 | } 357 | 358 | QPushButton:checked { 359 | color: #ff9900; 360 | } 361 | 362 | QProgressBar { 363 | text-align: center; 364 | color: rgb(255, 255, 255); 365 | background-color: #616161; 366 | border-width: 1px; 367 | border-radius: 10px; 368 | border-color: #616161; 369 | border-style: inset; 370 | } 371 | 372 | QProgressBar::chunk { 373 | background-color: #ff9900; 374 | border-radius: 10px; 375 | } 376 | 377 | QMenuBar { 378 | background:rgb(0, 0, 0); 379 | color: rgb(255, 153, 0); 380 | } 381 | 382 | QMenuBar::item { 383 | spacing: 3px; 384 | padding: 1px 4px; 385 | background: transparent; 386 | } 387 | 388 | QMenuBar::item:selected { 389 | background:rgb(115, 115, 115); 390 | } 391 | 392 | QMenu { 393 | border-width: 2px; 394 | border-radius: 10px; 395 | border-color: rgb(255, 153, 0); 396 | border-style: outset; 397 | } 398 | 399 | QMenu::item { 400 | spacing: 3px; 401 | padding: 3px 15px; 402 | } 403 | 404 | QMenu::item:selected { 405 | spacing: 3px; 406 | padding: 3px 15px; 407 | background:rgb(115, 115, 115); 408 | color:rgb(255, 255, 255); 409 | border-width: 1px; 410 | border-radius: 10px; 411 | border-color: rgb(58, 58, 58); 412 | border-style: inset; 413 | } 414 | -------------------------------------------------------------------------------- /src/themes/console_style/icons/down-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/console_style/icons/down-arrow.png -------------------------------------------------------------------------------- /src/themes/console_style/icons/down-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/console_style/icons/down-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/console_style/icons/down-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/console_style/icons/down-arrow_off.png -------------------------------------------------------------------------------- /src/themes/console_style/icons/search_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/console_style/icons/search_icon.png -------------------------------------------------------------------------------- /src/themes/console_style/icons/up-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/console_style/icons/up-arrow.png -------------------------------------------------------------------------------- /src/themes/console_style/icons/up-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/console_style/icons/up-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/console_style/icons/up-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/console_style/icons/up-arrow_off.png -------------------------------------------------------------------------------- /src/themes/console_style/icons/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/console_style/icons/volume.png -------------------------------------------------------------------------------- /src/themes/dark/colors.txt: -------------------------------------------------------------------------------- 1 | active=#4545e5 2 | inactive=#546E7A 3 | off=#283048,#859398 4 | on=#4776e6, #8e54e9 5 | text=#ffffff 6 | -------------------------------------------------------------------------------- /src/themes/dark/dark.qss: -------------------------------------------------------------------------------- 1 | /************************************* 2 | Main Window and Splitters 3 | **************************************/ 4 | QWidget:window { 5 | background-color: #232629; 6 | } 7 | 8 | QSplitter::handle { 9 | background-color: transparent; 10 | } 11 | 12 | /************************************* 13 | Main menu (Bar) 14 | **************************************/ 15 | QMenuBar { 16 | background-color: transparent; 17 | color: #AFBDC4; 18 | } 19 | 20 | QMenuBar::item { 21 | background-color: transparent; 22 | } 23 | 24 | QMenuBar::item:disabled { 25 | color: gray; 26 | } 27 | 28 | QMenuBar::item:selected { 29 | color: #FFFFFF; 30 | border-bottom: 2px solid #4545e5; 31 | } 32 | 33 | QMenuBar::item:pressed { 34 | color: #FFFFFF; 35 | border-bottom: 2px solid #4545e5; 36 | } 37 | 38 | QToolBar { 39 | background-color: transparent; 40 | border: 1px solid transparent; 41 | } 42 | 43 | QToolBar:handle { 44 | background-color: transparent; 45 | border-left: 2px dotted #80CBC4; 46 | color: transparent; 47 | } 48 | 49 | QToolBar::separator { 50 | border: 0; 51 | } 52 | 53 | QMenu { 54 | background-color: #263238; 55 | color: #AFBDC4; 56 | } 57 | 58 | QMenu::item:selected { 59 | color: #FFFFFF; 60 | } 61 | 62 | QMenu::item:pressed { 63 | color: #FFFFFF; 64 | } 65 | 66 | QMenu::separator { 67 | background-color: transparent; 68 | height: 1px; 69 | margin-left: 10px; 70 | margin-right: 10px; 71 | margin-top: 5px; 72 | margin-bottom: 5px; 73 | } 74 | 75 | /************************************* 76 | TabBar 77 | **************************************/ 78 | QTabBar { 79 | background: transparent; 80 | } 81 | 82 | QTabWidget::pane { 83 | border: 0px solid transparent; 84 | background: transparent; 85 | } 86 | 87 | QTabBar::tab { 88 | background-color: transparent; 89 | border: 0px solid transparent; 90 | border-radius: 10px; 91 | color: #AFBDC4; 92 | padding-left: 10px; 93 | padding-right: 10px; 94 | padding-top: 3px; 95 | padding-bottom: 3px; 96 | } 97 | 98 | QTabBar::tab:hover { 99 | background: #4545e5; 100 | color: #FFFFFF; 101 | } 102 | 103 | QTabBar::tab:selected { 104 | background: #4545e5; 105 | color: #FFFFFF; 106 | } 107 | 108 | QStackedWidget { 109 | background: #232629; 110 | } 111 | 112 | QSlider::sub-page:horizontal { 113 | background-color: #4545e5; 114 | } 115 | QSlider::sub-page:vertical { 116 | background-color: #4545e5; 117 | } 118 | 119 | /************************************* 120 | Progressbar 121 | **************************************/ 122 | QProgressBar 123 | { 124 | border: 2px solid grey; 125 | border-radius: 5px; 126 | text-align: center; 127 | } 128 | 129 | QProgressBar::chunk 130 | { 131 | background-color: #88cc00; 132 | width: 2.15px; 133 | margin: 0.5px; 134 | } 135 | 136 | /************************************* 137 | Labels and Rich Text boxes 138 | **************************************/ 139 | QLabel { 140 | background-color: transparent; 141 | color: #CFD8DC; 142 | } 143 | 144 | QDialog { 145 | background-color: transparent; 146 | color: #949a9c; 147 | } 148 | 149 | QTextBrowser { 150 | background-color: transparent; 151 | color: #949a9c; 152 | } 153 | 154 | /************************************* 155 | Search Bar 156 | **************************************/ 157 | QLineEdit { 158 | border: 2px solid #4545e5 ; 159 | border-radius: 10px; 160 | background-color: transparent; 161 | color: #CFD8DC; 162 | } 163 | 164 | QLineEdit:hover { 165 | border-width: 1px; 166 | border-radius: 10px; 167 | border-style: solid; 168 | border-color: #4545e5 ; 169 | } 170 | 171 | QLineEdit:focus { 172 | border-width: 1px; 173 | border-radius: 10px; 174 | border-style: solid; 175 | border-color: #4545e5 ; 176 | } 177 | 178 | /************************************* 179 | Scroll bars 180 | **************************************/ 181 | QScrollBar:horizontal { 182 | background: transparent; 183 | height: 10px; 184 | margin: 0; 185 | } 186 | 187 | QScrollBar:vertical { 188 | background: transparent; 189 | width: 10px; 190 | margin: 0; 191 | } 192 | 193 | QScrollBar::handle:horizontal { 194 | background: #374146; 195 | min-width: 16px; 196 | border-radius: 5px; 197 | } 198 | 199 | QScrollBar::handle:vertical { 200 | background: #374146; 201 | min-height: 16px; 202 | border-radius: 5px; 203 | } 204 | 205 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, 206 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 207 | background: none; 208 | } 209 | 210 | QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, 211 | QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { 212 | border: none; 213 | background: none; 214 | } 215 | 216 | /************************************* 217 | List 218 | **************************************/ 219 | QListWidget { 220 | background-color: transparent; 221 | border: 0px solid transparent; 222 | border-bottom: 2px solid #80CBC4; 223 | color: #AFBDC4; 224 | } 225 | 226 | QListView { 227 | background-color: transparent; 228 | color: #AFBDC4; 229 | outline: 0; 230 | border: 0px solid transparent; 231 | } 232 | QListView::item:hover { 233 | color: #FFFFFF; 234 | background: transparent; 235 | } 236 | 237 | QListView::item:selected { 238 | color: #4545e5; 239 | background: transparent; 240 | } 241 | 242 | QListView::item:disabled { 243 | color: #546E7A; 244 | background: transparent; 245 | } 246 | 247 | QListView::item:disabled:selected { 248 | color: #88cc00; 249 | background: transparent; 250 | } 251 | 252 | /************************************* 253 | Buttons 254 | **************************************/ 255 | QPushButton { 256 | background-color: transparent; 257 | color: #AFBDC4; 258 | border: 1px solid transparent; 259 | padding: 4px 22px; 260 | } 261 | 262 | QPushButton:hover { 263 | color: #FFFFFF; 264 | } 265 | 266 | QPushButton:pressed { 267 | color: #FFFFFF; 268 | } 269 | 270 | QPushButton:disabled { 271 | color:#546E7A; 272 | } 273 | 274 | QPushButton:checked { 275 | color: #4545e5; 276 | } 277 | 278 | /************************************* 279 | ComboBox 280 | **************************************/ 281 | QComboBox { 282 | border: 0px solid transparent; 283 | border-radius: 2px; 284 | padding: 1px 6px 1px 6px; 285 | min-width: 2em; 286 | } 287 | 288 | QComboBox:!editable { 289 | selection-background-color: transparent; 290 | color: #AFBDC4; 291 | selection-color: #FFFFFF; 292 | background-color: transparent; 293 | } 294 | 295 | QComboBox:disabled { 296 | color: #546E7A; 297 | } 298 | 299 | QComboBox:!editable:on, QComboBox::drop-down:editable:on { 300 | color: #AFBDC4; 301 | background-color: transparent; 302 | selection-background-color: transparent; 303 | } 304 | 305 | QComboBox:on { 306 | padding-top: 3px; 307 | padding-left: 4px; 308 | } 309 | 310 | QComboBox::drop-down { 311 | background-color: transparent; 312 | subcontrol-origin: padding; 313 | subcontrol-position: top right; 314 | width: 20px; 315 | border-top-right-radius: 2px; 316 | border-bottom-right-radius: 2px; 317 | } 318 | 319 | QComboBox::down-arrow:enabled { 320 | image: url("./themes/dark/icons/down-arrow.png"); 321 | } 322 | 323 | QComboBox::down-arrow:disabled { 324 | image: url("./themes/dark/icons/down-arrow_off.png"); 325 | } 326 | 327 | QComboBox::down-arrow:hover { 328 | image: url("./themes/dark/icons/down-arrow_hover.png"); 329 | } 330 | 331 | QComboBox::down-arrow:on { 332 | top: 1px; 333 | left: 1px; 334 | } 335 | 336 | QComboBox QAbstractItemView { 337 | background-color: #232629; 338 | } 339 | 340 | /************************************* 341 | RadioButton 342 | **************************************/ 343 | QRadioButton{ 344 | color: #AFBDC4; 345 | } 346 | 347 | QRadioButton:disabled{ 348 | color: #546E7A; 349 | } 350 | 351 | QRadioButton::indicator{ 352 | width: 50px; 353 | height: 50px; 354 | } 355 | 356 | QRadioButton::indicator::unchecked { 357 | image: url("./themes/dark/icons/off.png"); 358 | } 359 | 360 | QRadioButton::indicator:unchecked:hover { 361 | image: url("./themes/dark/icons/off_press.png"); 362 | } 363 | 364 | QRadioButton::indicator:unchecked:pressed { 365 | image: url("./themes/dark/icons/off_press.png"); 366 | } 367 | 368 | QRadioButton::indicator::checked { 369 | image: url("./themes/dark/icons/on.png"); 370 | } 371 | 372 | QRadioButton::indicator:checked:hover { 373 | image: url("./themes/dark/icons/on_press.png"); 374 | } 375 | 376 | QRadioButton::indicator:checked:pressed { 377 | image: url("./themes/dark/icons/on_press.png"); 378 | } 379 | 380 | /************************************* 381 | SpinBox 382 | **************************************/ 383 | QSpinBox { 384 | background-color: transparent; 385 | color: #AFBDC4; 386 | border-width: 0px; 387 | } 388 | 389 | QSpinBox:disabled { 390 | color: #546E7A; 391 | border-width: 0px; 392 | } 393 | 394 | QSpinBox::up-button { 395 | subcontrol-origin: border; 396 | subcontrol-position: top right; 397 | width: 16px; 398 | image: url("./themes/dark/icons/up-arrow.png"); 399 | border-width: 0px; 400 | } 401 | 402 | QSpinBox::up-button:hover { 403 | image: url("./themes/dark/icons/up-arrow_hover.png"); 404 | } 405 | 406 | QSpinBox::up-button:pressed { 407 | image: url("./themes/dark/icons/up-arrow.png"); 408 | } 409 | 410 | QSpinBox::up-button:disabled { 411 | image: url("./themes/dark/icons/up-arrow_off.png"); 412 | } 413 | 414 | QSpinBox::down-button { 415 | subcontrol-origin: border; 416 | subcontrol-position: bottom right; 417 | width: 16px; 418 | image: url("./themes/dark/icons/down-arrow.png"); 419 | border-width: 0px; 420 | border-top-width: 0; 421 | } 422 | 423 | QSpinBox::down-button:hover { 424 | image: url("./themes/dark/icons/down-arrow_hover.png"); 425 | } 426 | 427 | QSpinBox::down-button:pressed { 428 | image: url("./themes/dark/icons/down-arrow.png"); 429 | } 430 | 431 | QSpinBox::down-button:disabled { 432 | image: url("./themes/dark/icons/down-arrow_off.png"); 433 | } 434 | 435 | /************************************* 436 | TreeViewMenu (Mode) 437 | **************************************/ 438 | QTreeView { 439 | background-color: transparent; 440 | selection-background-color: transparent; 441 | border: 0px; 442 | } 443 | 444 | QTreeView::item { 445 | background-color: transparent; 446 | color: #AFBDC4; 447 | } 448 | 449 | QTreeView::item:hover { 450 | border-right: 2px solid #4545e5; 451 | color: #FFFFFF; 452 | } 453 | 454 | QTreeView::item:selected { 455 | color: #4545e5; 456 | } 457 | 458 | QTreeView::item:active{ 459 | background: transparent; 460 | } 461 | 462 | QTreeView::item:disabled{ 463 | color: #546E7A; 464 | } 465 | 466 | QTreeView::item:selected:disabled{ 467 | color: #4545e5; 468 | } 469 | -------------------------------------------------------------------------------- /src/themes/dark/icons/down-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/down-arrow.png -------------------------------------------------------------------------------- /src/themes/dark/icons/down-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/down-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/dark/icons/down-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/down-arrow_off.png -------------------------------------------------------------------------------- /src/themes/dark/icons/off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/off.png -------------------------------------------------------------------------------- /src/themes/dark/icons/off_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/off_press.png -------------------------------------------------------------------------------- /src/themes/dark/icons/on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/on.png -------------------------------------------------------------------------------- /src/themes/dark/icons/on_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/on_press.png -------------------------------------------------------------------------------- /src/themes/dark/icons/search_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/search_icon.png -------------------------------------------------------------------------------- /src/themes/dark/icons/up-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/up-arrow.png -------------------------------------------------------------------------------- /src/themes/dark/icons/up-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/up-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/dark/icons/up-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/up-arrow_off.png -------------------------------------------------------------------------------- /src/themes/dark/icons/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/dark/icons/volume.png -------------------------------------------------------------------------------- /src/themes/elegant_dark/colors.txt: -------------------------------------------------------------------------------- 1 | active= #00ff00 2 | inactive= #9f9f9f 3 | on=#fdfc47, #24fe41 4 | off=#f2f2f2,#eaeaea 5 | text=#000000 -------------------------------------------------------------------------------- /src/themes/elegant_dark/elegant_dark.qss: -------------------------------------------------------------------------------- 1 | /* 2 | ElegantDark Style Sheet for QT Applications 3 | Author: Jaime A. Quiroga P. 4 | Company: GTRONICK 5 | Last updated: 17/04/2018 6 | Available at: https://github.com/GTRONICK/QSS/blob/master/ElegantDark.qss 7 | */ 8 | QMainWindow { 9 | background-color:rgb(82, 82, 82); 10 | } 11 | 12 | QWidget{ 13 | background-color: rgb(82, 82, 82) 14 | } 15 | 16 | QTextEdit { 17 | background-color:rgb(42, 42, 42); 18 | color: rgb(0, 255, 0); 19 | } 20 | 21 | QSplitter::handle { 22 | background-color: transparent; 23 | } 24 | 25 | QSlider::sub-page:horizontal { 26 | background-color: #00ff00; 27 | } 28 | QSlider::sub-page:vertical { 29 | background-color: #00ff00; 30 | } 31 | 32 | QTreeView { 33 | background-color: transparent; 34 | selection-background-color: transparent; 35 | border: 0px; 36 | color: #AFBDC4; 37 | } 38 | 39 | QTreeView::item { 40 | background-color: transparent; 41 | } 42 | 43 | QTreeView::item:hover { 44 | color: #FFFFFF; 45 | } 46 | 47 | QTreeView::item:selected { 48 | color: #00ff00; 49 | } 50 | 51 | QTreeView::item:active{ 52 | background: transparent; 53 | } 54 | 55 | QTreeView::item:disabled{ 56 | color: #000000; 57 | } 58 | 59 | QTreeView::item:selected:disabled{ 60 | color: #00ff00; 61 | } 62 | 63 | QSpinBox { 64 | background-color: transparent; 65 | color: #AFBDC4; 66 | border-width: 0px; 67 | } 68 | 69 | QSpinBox:disabled { 70 | color: #000000; 71 | border-width: 0px; 72 | } 73 | 74 | QSpinBox::up-button { 75 | subcontrol-origin: border; 76 | subcontrol-position: top right; 77 | width: 16px; 78 | image: url("./themes/elegant_dark/icons/up-arrow.png"); 79 | border-width: 0px; 80 | } 81 | 82 | QSpinBox::up-button:hover { 83 | image: url("./themes/elegant_dark/icons/up-arrow_hover.png"); 84 | } 85 | 86 | QSpinBox::up-button:pressed { 87 | image: url("./themes/elegant_dark/icons/up-arrow.png"); 88 | } 89 | 90 | QSpinBox::up-button:disabled { 91 | image: url("./themes/elegant_dark/icons/up-arrow_off.png"); 92 | } 93 | 94 | QSpinBox::down-button { 95 | subcontrol-origin: border; 96 | subcontrol-position: bottom right; 97 | width: 16px; 98 | image: url("./themes/elegant_dark/icons/down-arrow.png"); 99 | border-width: 0px; 100 | border-top-width: 0; 101 | } 102 | 103 | QSpinBox::down-button:hover { 104 | image: url("./themes/elegant_dark/icons/down-arrow_hover.png"); 105 | } 106 | 107 | QSpinBox::down-button:pressed { 108 | image: url("./themes/elegant_dark/icons/down-arrow.png"); 109 | } 110 | 111 | QSpinBox::down-button:disabled { 112 | image: url("./themes/elegant_dark/icons/down-arrow_off.png"); 113 | } 114 | 115 | QPushButton{ 116 | /*border-style: outset; 117 | border-width: 2px; 118 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 119 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 120 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 121 | border-bottom-color: rgb(58, 58, 58); 122 | border-bottom-width: 1px; 123 | border-style: solid;*/ 124 | border: 0px; 125 | color: rgb(255, 255, 255); 126 | padding: 2px; 127 | background-color: transparent; 128 | } 129 | QPushButton:hover{ 130 | /*border-style: outset; 131 | border-width: 2px; 132 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); 133 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); 134 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(180, 180, 180, 255), stop:1 rgba(110, 110, 110, 255)); 135 | border-bottom-color: rgb(115, 115, 115); 136 | border-bottom-width: 1px; 137 | border-style: solid;*/ 138 | border: 0px; 139 | color: #AFAFAF; 140 | padding: 2px; 141 | } 142 | QPushButton:pressed{ 143 | /*border-style: outset; 144 | border-width: 2px; 145 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(62, 62, 62, 255), stop:1 rgba(22, 22, 22, 255)); 146 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 147 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 148 | border-bottom-color: rgb(58, 58, 58); 149 | border-bottom-width: 1px; 150 | border-style: solid;*/ 151 | border: 0px; 152 | color: rgb(255, 255, 255); 153 | padding: 2px; 154 | } 155 | QPushButton:disabled{ 156 | /*border-style: outset; 157 | border-width: 2px; 158 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 159 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 160 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 161 | border-bottom-color: rgb(58, 58, 58); 162 | border-bottom-width: 1px; 163 | border-style: solid;*/ 164 | border: 0px; 165 | color: rgb(0, 0, 0); 166 | padding: 2px; 167 | } 168 | QPushButton:checked { 169 | border: 0px; 170 | color: #00ff00; 171 | } 172 | 173 | QLineEdit{ 174 | border-width: 1px; border-radius: 4px; 175 | border-color: rgb(58, 58, 58); 176 | border-style: inset; 177 | padding: 0 8px; 178 | color: rgb(255, 255, 255); 179 | background:rgb(100, 100, 100); 180 | selection-background-color: rgb(187, 187, 187); 181 | selection-color: rgb(60, 63, 65); 182 | } 183 | 184 | QLabel{ 185 | color:rgb(255,255,255); 186 | } 187 | 188 | QRadioButton{ 189 | color: #FFFFFF; 190 | } 191 | 192 | QRadioButton:hover{ 193 | color: #AFAFAF; 194 | } 195 | 196 | QComboBox { 197 | border: 0px solid transparent; 198 | border-radius: 2px; 199 | padding: 1px 6px 1px 6px; 200 | min-width: 2em; 201 | } 202 | 203 | QComboBox:!editable { 204 | selection-background-color: transparent; 205 | color: #AFBDC4; 206 | selection-color: #FFFFFF; 207 | background-color: transparent; 208 | } 209 | 210 | QComboBox:disabled { 211 | color: #000000; 212 | } 213 | 214 | QComboBox:!editable:on, QComboBox::drop-down:editable:on { 215 | color: #AFBDC4; 216 | background-color: transparent; 217 | selection-background-color: transparent; 218 | } 219 | 220 | QComboBox:on { 221 | padding-top: 3px; 222 | padding-left: 4px; 223 | } 224 | 225 | QComboBox::drop-down { 226 | background-color: transparent; 227 | subcontrol-origin: padding; 228 | subcontrol-position: top right; 229 | width: 20px; 230 | border-top-right-radius: 2px; 231 | border-bottom-right-radius: 2px; 232 | } 233 | 234 | QComboBox::down-arrow:enabled { 235 | image: url("./themes/elegant_dark/icons/down-arrow.png"); 236 | } 237 | 238 | QComboBox::down-arrow:disabled { 239 | image: url("./themes/elegant_dark/icons/down-arrow_off.png"); 240 | } 241 | 242 | QComboBox::down-arrow:hover { 243 | image: url("./themes/elegant_dark/icons/down-arrow_hover.png"); 244 | } 245 | 246 | QComboBox::down-arrow:on { 247 | top: 1px; 248 | left: 1px; 249 | } 250 | 251 | QComboBox QAbstractItemView { 252 | background-color: rgb(100,100,100); 253 | } 254 | 255 | QProgressBar { 256 | text-align: center; 257 | color: rgb(240, 240, 240); 258 | border-width: 1px; 259 | border-radius: 10px; 260 | border-color: rgb(58, 58, 58); 261 | border-style: inset; 262 | background-color:rgb(77,77,77); 263 | } 264 | QProgressBar::chunk { 265 | background-color: #00ff00; 266 | border-radius: 5px; 267 | } 268 | QMenuBar { 269 | background:rgb(82, 82, 82); 270 | } 271 | QMenuBar::item { 272 | color:rgb(223,219,210); 273 | spacing: 3px; 274 | padding: 1px 4px; 275 | background: transparent; 276 | } 277 | 278 | QMenuBar::item:selected { 279 | background:rgb(115, 115, 115); 280 | } 281 | QMenu::item:selected { 282 | color:rgb(255,255,255); 283 | border-width:2px; 284 | border-style:solid; 285 | padding-left:18px; 286 | padding-right:8px; 287 | padding-top:2px; 288 | padding-bottom:3px; 289 | background:qlineargradient(spread:pad, x1:0.5, y1:0.7, x2:0.5, y2:0.3, stop:0 rgba(87, 97, 106, 255), stop:1 rgba(93, 103, 113, 255)); 290 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 291 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 292 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(62, 62, 62, 255)); 293 | border-bottom-color: rgb(58, 58, 58); 294 | border-bottom-width: 1px; 295 | } 296 | QMenu::item { 297 | color:rgb(223,219,210); 298 | background-color:rgb(78,78,78); 299 | padding-left:20px; 300 | padding-top:4px; 301 | padding-bottom:4px; 302 | padding-right:10px; 303 | } 304 | QMenu{ 305 | background-color:rgb(78,78,78); 306 | } 307 | QTabWidget { 308 | color:rgb(0,0,0); 309 | background-color:rgb(247,246,246); 310 | } 311 | QTabWidget::pane { 312 | /* border-color: rgb(77,77,77); */ 313 | background-color:rgb(101,101,101); 314 | /* border-style: solid; 315 | border-width: 1px; 316 | border-radius: 6px; */ 317 | } 318 | QTabBar::tab { 319 | padding:2px; 320 | color:rgb(250,250,250); 321 | background-color: qlineargradient(spread:pad, x1:0.5, y1:1, x2:0.5, y2:0, stop:0 rgba(77, 77, 77, 255), stop:1 rgba(97, 97, 97, 255)); 322 | border-style: solid; 323 | border-width: 2px; 324 | border-top-right-radius:4px; 325 | border-top-left-radius:4px; 326 | border-top-color: qlineargradient(spread:pad, x1:0.5, y1:0.6, x2:0.5, y2:0.4, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(95, 92, 93, 255)); 327 | border-right-color: qlineargradient(spread:pad, x1:0.4, y1:0.5, x2:0.6, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(95, 92, 93, 255)); 328 | border-left-color: qlineargradient(spread:pad, x1:0.6, y1:0.5, x2:0.4, y2:0.5, stop:0 rgba(115, 115, 115, 255), stop:1 rgba(95, 92, 93, 255)); 329 | border-bottom-color: rgb(101,101,101); 330 | } 331 | QTabBar::tab:selected, QTabBar::tab:last:selected, QTabBar::tab:hover { 332 | background-color:rgb(101,101,101); 333 | margin-left: 0px; 334 | margin-right: 1px; 335 | } 336 | QTabBar::tab:!selected { 337 | margin-top: 1px; 338 | margin-right: 1px; 339 | } 340 | 341 | QStatusBar { 342 | color:rgb(240,240,240); 343 | } 344 | 345 | QTextBrowser { 346 | background-color: transparent; 347 | } 348 | 349 | QListWidget { 350 | background-color: transparent; 351 | border: 0px solid transparent; 352 | } 353 | 354 | QListView { 355 | background-color: transparent; 356 | color: #AFBDC4; 357 | outline: 0; 358 | border: 0px solid transparent; 359 | } 360 | QListView::item:hover { 361 | color: #FFFFFF; 362 | background: transparent; 363 | } 364 | 365 | QListView::item:selected { 366 | color: #00ff00; 367 | background: transparent; 368 | } 369 | 370 | QListView::item:disabled { 371 | color: #9f9f9f; 372 | background: transparent; 373 | } 374 | 375 | QListView::item:disabled:selected { 376 | color: #00ff00; 377 | background: transparent; 378 | } 379 | 380 | QScrollBar:horizontal { 381 | background: transparent; 382 | height: 10px; 383 | margin: 0; 384 | } 385 | 386 | QScrollBar:vertical { 387 | background: transparent; 388 | width: 10px; 389 | margin: 0; 390 | } 391 | 392 | QScrollBar::handle:horizontal { 393 | background: rgb(101,101,101); 394 | min-width: 16px; 395 | border-radius: 5px; 396 | } 397 | 398 | QScrollBar::handle:vertical { 399 | background: rgb(101,101,101); 400 | min-height: 16px; 401 | border-radius: 5px; 402 | } 403 | 404 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, 405 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 406 | background: none; 407 | } 408 | 409 | QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, 410 | QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { 411 | border: none; 412 | background: none; 413 | } 414 | -------------------------------------------------------------------------------- /src/themes/elegant_dark/icons/down-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/elegant_dark/icons/down-arrow.png -------------------------------------------------------------------------------- /src/themes/elegant_dark/icons/down-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/elegant_dark/icons/down-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/elegant_dark/icons/down-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/elegant_dark/icons/down-arrow_off.png -------------------------------------------------------------------------------- /src/themes/elegant_dark/icons/search_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/elegant_dark/icons/search_icon.png -------------------------------------------------------------------------------- /src/themes/elegant_dark/icons/up-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/elegant_dark/icons/up-arrow.png -------------------------------------------------------------------------------- /src/themes/elegant_dark/icons/up-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/elegant_dark/icons/up-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/elegant_dark/icons/up-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/elegant_dark/icons/up-arrow_off.png -------------------------------------------------------------------------------- /src/themes/elegant_dark/icons/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/elegant_dark/icons/volume.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/colors.txt: -------------------------------------------------------------------------------- 1 | active=#88cc00 2 | inactive=#546E7A 3 | off=#304352,#d7d2cc 4 | on=#3ca55c,#b5ac49 5 | -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/down-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/down-arrow.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/down-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/down-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/down-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/down-arrow_off.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/off.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/off_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/off_press.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/on.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/on_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/on_press.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/search_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/search_icon.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/up-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/up-arrow.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/up-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/up-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/up-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/up-arrow_off.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/icons/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_dark/icons/volume.png -------------------------------------------------------------------------------- /src/themes/material_design_dark/material_design_dark.qss: -------------------------------------------------------------------------------- 1 | /* Palette 2 | 3 | Background: #29353B 4 | Sec. Menu bkg: #263238 5 | 6 | Label: #AFBDC4 7 | Label Selected/hover: #FFFFFF 8 | Label Pressed: #FFFFFF 9 | Selection: #88cc00 10 | 11 | ScrollBars: #374146 12 | 13 | Signal detail Labels: #CFD8DC 14 | Signal detail Dialogs: #949a9c 15 | 16 | Disabled: #546E7A 17 | 18 | /************************************* 19 | Main Window and Splitters 20 | **************************************/ 21 | QWidget:window { 22 | background-color: #29353B; 23 | } 24 | 25 | QSplitter::handle { 26 | background-color: transparent; 27 | } 28 | 29 | QSlider::sub-page:horizontal { 30 | background-color: #88cc00; 31 | } 32 | QSlider::sub-page:vertical { 33 | background-color: #88cc00; 34 | } 35 | 36 | /************************************* 37 | Main menu (Bar) 38 | **************************************/ 39 | QMenuBar { 40 | background-color: transparent; 41 | color: #AFBDC4; 42 | } 43 | 44 | QMenuBar::item { 45 | background-color: transparent; 46 | } 47 | 48 | QMenuBar::item:disabled { 49 | color: gray; 50 | } 51 | 52 | QMenuBar::item:selected { 53 | color: #FFFFFF; 54 | border-bottom: 2px solid #88cc00; 55 | } 56 | 57 | QMenuBar::item:pressed { 58 | color: #FFFFFF; 59 | border-bottom: 2px solid #88cc00; 60 | } 61 | 62 | QToolBar { 63 | background-color: transparent; 64 | border: 1px solid transparent; 65 | } 66 | 67 | QToolBar:handle { 68 | background-color: transparent; 69 | border-left: 2px dotted #80CBC4; 70 | color: transparent; 71 | } 72 | 73 | QToolBar::separator { 74 | border: 0; 75 | } 76 | 77 | QMenu { 78 | background-color: #263238; 79 | color: #AFBDC4; 80 | } 81 | 82 | QMenu::item:selected { 83 | color: #FFFFFF; 84 | } 85 | 86 | QMenu::item:pressed { 87 | color: #FFFFFF; 88 | } 89 | 90 | QMenu::separator { 91 | background-color: transparent; 92 | height: 1px; 93 | margin-left: 10px; 94 | margin-right: 10px; 95 | margin-top: 5px; 96 | margin-bottom: 5px; 97 | } 98 | 99 | /************************************* 100 | TabBar 101 | **************************************/ 102 | QTabBar { 103 | background: transparent; 104 | } 105 | 106 | QTabWidget::pane { 107 | background: transparent; 108 | } 109 | 110 | QTabBar::tab { 111 | background: transparent; 112 | border: 0px solid transparent; 113 | border-bottom: 2px solid transparent; 114 | color: #AFBDC4; 115 | padding-left: 10px; 116 | padding-right: 10px; 117 | padding-top: 3px; 118 | padding-bottom: 3px; 119 | } 120 | 121 | QTabBar::tab:hover { 122 | background-color: transparent; 123 | border: 0px solid transparent; 124 | border-bottom: 2px solid #88cc00; 125 | color: #FFFFFF; 126 | } 127 | 128 | QTabBar::tab:selected { 129 | background-color: transparent; 130 | border: 0px solid transparent; 131 | border-top: none; 132 | border-bottom: 2px solid #88cc00; 133 | color: #FFFFFF; 134 | } 135 | 136 | QStackedWidget { 137 | background: #29353B; 138 | } 139 | 140 | /************************************* 141 | Progressbar 142 | **************************************/ 143 | QProgressBar 144 | { 145 | border: 2px solid grey; 146 | border-radius: 5px; 147 | text-align: center; 148 | } 149 | 150 | QProgressBar::chunk 151 | { 152 | background-color: #88cc00; 153 | width: 2.15px; 154 | margin: 0.5px; 155 | } 156 | 157 | /************************************* 158 | Labels and Rich Text boxes 159 | **************************************/ 160 | QLabel { 161 | background-color: transparent; 162 | color: #CFD8DC; 163 | } 164 | 165 | QDialog { 166 | background-color: transparent; 167 | color: #949a9c; 168 | } 169 | 170 | QTextBrowser { 171 | background-color: transparent; 172 | color: #949a9c; 173 | } 174 | 175 | /************************************* 176 | Search Bar 177 | **************************************/ 178 | QLineEdit { 179 | background-color: transparent; 180 | selection-background-color: #669900; 181 | color: #669900; 182 | border-width: 1px; 183 | border-style: solid; 184 | border-color: transparent transparent #669900 transparent; 185 | } 186 | 187 | QLineEdit:hover { 188 | border-width: 2px; 189 | border-color: transparent transparent #88cc00 transparent; 190 | } 191 | 192 | QLineEdit:focus { 193 | border-width: 2px; 194 | border-color: transparent transparent #88cc00 transparent; 195 | } 196 | 197 | /************************************* 198 | Scroll bars 199 | **************************************/ 200 | QScrollBar:horizontal { 201 | background: transparent; 202 | height: 10px; 203 | margin: 0; 204 | } 205 | 206 | QScrollBar:vertical { 207 | background: transparent; 208 | width: 10px; 209 | margin: 0; 210 | } 211 | 212 | QScrollBar::handle:horizontal { 213 | background: #374146; 214 | min-width: 16px; 215 | border-radius: 5px; 216 | } 217 | 218 | QScrollBar::handle:vertical { 219 | background: #374146; 220 | min-height: 16px; 221 | border-radius: 5px; 222 | } 223 | 224 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, 225 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 226 | background: none; 227 | } 228 | 229 | QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, 230 | QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { 231 | border: none; 232 | background: none; 233 | } 234 | 235 | /************************************* 236 | List 237 | **************************************/ 238 | QListWidget { 239 | background-color: transparent; 240 | border: 0px solid transparent; 241 | border-bottom: 2px solid #80CBC4; 242 | color: #AFBDC4; 243 | } 244 | 245 | QListView { 246 | background-color: transparent; 247 | color: #AFBDC4; 248 | outline: 0; 249 | border: 0px solid transparent; 250 | } 251 | QListView::item:hover { 252 | color: #FFFFFF; 253 | background: transparent; 254 | } 255 | 256 | QListView::item:selected { 257 | color: #88cc00; 258 | background: transparent; 259 | } 260 | 261 | QListView::item:disabled { 262 | color: #546E7A; 263 | background: transparent; 264 | } 265 | 266 | QListView::item:disabled:selected { 267 | color: #88cc00; 268 | background: transparent; 269 | } 270 | 271 | /************************************* 272 | Buttons 273 | **************************************/ 274 | QPushButton { 275 | background-color: transparent; 276 | color: #AFBDC4; 277 | border: 1px solid transparent; 278 | padding: 4px 22px; 279 | } 280 | 281 | QPushButton:hover { 282 | border-left: 2px solid #88cc00; 283 | border-right: 2px solid #88cc00; 284 | color: #FFFFFF; 285 | } 286 | 287 | QPushButton:pressed { 288 | color: #FFFFFF; 289 | } 290 | 291 | QPushButton:disabled { 292 | color:#546E7A; 293 | } 294 | 295 | QPushButton:checked { 296 | color: #88cc00; 297 | } 298 | 299 | /************************************* 300 | ComboBox 301 | **************************************/ 302 | QComboBox { 303 | border: 0px solid transparent; 304 | border-radius: 2px; 305 | padding: 1px 6px 1px 6px; 306 | min-width: 2em; 307 | } 308 | 309 | QComboBox:!editable { 310 | selection-background-color: transparent; 311 | color: #AFBDC4; 312 | selection-color: #FFFFFF; 313 | background-color: transparent; 314 | } 315 | 316 | QComboBox:disabled { 317 | color: #546E7A; 318 | } 319 | 320 | QComboBox:!editable:on, QComboBox::drop-down:editable:on { 321 | color: #AFBDC4; 322 | background-color: transparent; 323 | selection-background-color: transparent; 324 | } 325 | 326 | QComboBox:on { 327 | padding-top: 3px; 328 | padding-left: 4px; 329 | } 330 | 331 | QComboBox::drop-down { 332 | background-color: transparent; 333 | subcontrol-origin: padding; 334 | subcontrol-position: top right; 335 | width: 20px; 336 | border-top-right-radius: 2px; 337 | border-bottom-right-radius: 2px; 338 | } 339 | 340 | QComboBox::down-arrow:enabled { 341 | image: url("./themes/material_design_dark/icons/down-arrow.png"); 342 | } 343 | 344 | QComboBox::down-arrow:disabled { 345 | image: url("./themes/material_design_dark/icons/down-arrow_off.png"); 346 | } 347 | 348 | QComboBox::down-arrow:hover { 349 | image: url("./themes/material_design_dark/icons/down-arrow_hover.png"); 350 | } 351 | 352 | QComboBox::down-arrow:on { 353 | top: 1px; 354 | left: 1px; 355 | } 356 | 357 | QComboBox QAbstractItemView { 358 | background-color: #29353B; 359 | } 360 | 361 | /************************************* 362 | RadioButton 363 | **************************************/ 364 | QRadioButton{ 365 | color: #AFBDC4; 366 | } 367 | 368 | QRadioButton:disabled{ 369 | color: #546E7A; 370 | } 371 | 372 | QRadioButton::indicator{ 373 | width: 50px; 374 | height: 50px; 375 | } 376 | 377 | QRadioButton::indicator::unchecked { 378 | image: url("./themes/material_design_dark/icons/off.png"); 379 | } 380 | 381 | QRadioButton::indicator:unchecked:hover { 382 | image: url("./themes/material_design_dark/icons/off_press.png"); 383 | } 384 | 385 | QRadioButton::indicator:unchecked:pressed { 386 | image: url("./themes/material_design_dark/icons/off_press.png"); 387 | } 388 | 389 | QRadioButton::indicator::checked { 390 | image: url("./themes/material_design_dark/icons/on.png"); 391 | } 392 | 393 | QRadioButton::indicator:checked:hover { 394 | image: url("./themes/material_design_dark/icons/on_press.png"); 395 | } 396 | 397 | QRadioButton::indicator:checked:pressed { 398 | image: url("./themes/material_design_dark/icons/on_press.png"); 399 | } 400 | 401 | /************************************* 402 | SpinBox 403 | **************************************/ 404 | QSpinBox { 405 | background-color: transparent; 406 | color: #AFBDC4; 407 | border-width: 0px; 408 | } 409 | 410 | QSpinBox:disabled { 411 | color: #546E7A; 412 | border-width: 0px; 413 | } 414 | 415 | QSpinBox::up-button { 416 | subcontrol-origin: border; 417 | subcontrol-position: top right; 418 | width: 16px; 419 | image: url("./themes/material_design_dark/icons/up-arrow.png"); 420 | border-width: 0px; 421 | } 422 | 423 | QSpinBox::up-button:hover { 424 | image: url("./themes/material_design_dark/icons/up-arrow_hover.png"); 425 | } 426 | 427 | QSpinBox::up-button:pressed { 428 | image: url("./themes/material_design_dark/icons/up-arrow.png"); 429 | } 430 | 431 | QSpinBox::up-button:disabled { 432 | image: url("./themes/material_design_dark/icons/up-arrow_off.png"); 433 | } 434 | 435 | QSpinBox::down-button { 436 | subcontrol-origin: border; 437 | subcontrol-position: bottom right; 438 | width: 16px; 439 | image: url("./themes/material_design_dark/icons/down-arrow.png"); 440 | border-width: 0px; 441 | border-top-width: 0; 442 | } 443 | 444 | QSpinBox::down-button:hover { 445 | image: url("./themes/material_design_dark/icons/down-arrow_hover.png"); 446 | } 447 | 448 | QSpinBox::down-button:pressed { 449 | image: url("./themes/material_design_dark/icons/down-arrow.png"); 450 | } 451 | 452 | QSpinBox::down-button:disabled { 453 | image: url("./themes/material_design_dark/icons/down-arrow_off.png"); 454 | } 455 | 456 | /************************************* 457 | TreeViewMenu (Mode) 458 | **************************************/ 459 | QTreeView { 460 | background-color: transparent; 461 | selection-background-color: transparent; 462 | border: 0px; 463 | } 464 | 465 | QTreeView::item { 466 | background-color: transparent; 467 | color: #AFBDC4; 468 | } 469 | 470 | QTreeView::item:hover { 471 | border-right: 2px solid #88cc00; 472 | color: #FFFFFF; 473 | } 474 | 475 | QTreeView::item:selected { 476 | color: #88cc00; 477 | } 478 | 479 | QTreeView::item:active{ 480 | background: transparent; 481 | } 482 | 483 | QTreeView::item:disabled{ 484 | color: #546E7A; 485 | } 486 | 487 | QTreeView::item:selected:disabled{ 488 | color: #88cc00; 489 | } 490 | -------------------------------------------------------------------------------- /src/themes/material_design_light/colors.txt: -------------------------------------------------------------------------------- 1 | active=#6ECE12 2 | inactive=#b3b3cc 3 | off=#948e99,#2e1437 4 | on=#b993d6,#8ca6db 5 | -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/down-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/down-arrow.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/down-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/down-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/down-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/down-arrow_off.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/off.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/off_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/off_press.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/on.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/on_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/on_press.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/search_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/search_icon.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/up-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/up-arrow.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/up-arrow_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/up-arrow_hover.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/up-arrow_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/up-arrow_off.png -------------------------------------------------------------------------------- /src/themes/material_design_light/icons/volume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/src/themes/material_design_light/icons/volume.png -------------------------------------------------------------------------------- /src/themes/material_design_light/material_design_light.qss: -------------------------------------------------------------------------------- 1 | /* Palette 2 | 3 | Background: #F5F5F5 4 | Sec. Menu bkg: #DCDCDC 5 | 6 | Label: #29353B 7 | Label Selected/hover: #000000 8 | Label Pressed: #000000 9 | Selection: #6ECE12 10 | 11 | ScrollBars: #DCDCDC 12 | 13 | Labels: #29353B 14 | Dialogs: #29353B 15 | 16 | Disabled: #b3b3cc 17 | 18 | /************************************* 19 | Main Window and Splitters 20 | **************************************/ 21 | QWidget:window { 22 | background-color: #F5F5F5; 23 | } 24 | 25 | QSplitter::handle { 26 | background-color: transparent; 27 | } 28 | 29 | QSlider::sub-page:horizontal { 30 | background-color: #6ECE12; 31 | } 32 | QSlider::sub-page:vertical { 33 | background-color: #6ECE12; 34 | } 35 | 36 | /************************************* 37 | Main menu (Bar) 38 | **************************************/ 39 | QMenuBar { 40 | background-color: transparent; 41 | color: #29353B; 42 | } 43 | 44 | QMenuBar::item { 45 | background-color: transparent; 46 | } 47 | 48 | QMenuBar::item:disabled { 49 | color: gray; 50 | } 51 | 52 | QMenuBar::item:selected { 53 | color: #000000; 54 | border-bottom: 2px solid #6ECE12; 55 | } 56 | 57 | QMenuBar::item:pressed { 58 | color: #000000; 59 | border-bottom: 2px solid #6ECE12; 60 | } 61 | 62 | QToolBar { 63 | background-color: transparent; 64 | border: 1px solid transparent; 65 | } 66 | 67 | QToolBar:handle { 68 | background-color: transparent; 69 | border-left: 2px dotted #80CBC4; 70 | color: transparent; 71 | } 72 | 73 | QToolBar::separator { 74 | border: 0; 75 | } 76 | 77 | QMenu { 78 | background-color: #DCDCDC; 79 | color: #29353B; 80 | } 81 | 82 | QMenu::item:selected { 83 | color: #000000; 84 | } 85 | 86 | QMenu::item:pressed { 87 | color: #000000; 88 | } 89 | 90 | QMenu::separator { 91 | background-color: transparent; 92 | height: 1px; 93 | margin-left: 10px; 94 | margin-right: 10px; 95 | margin-top: 5px; 96 | margin-bottom: 5px; 97 | } 98 | 99 | /************************************* 100 | TabBar 101 | **************************************/ 102 | QTabBar { 103 | background: transparent; 104 | } 105 | 106 | QTabWidget::pane { 107 | background: transparent; 108 | } 109 | 110 | QTabBar::tab { 111 | background: transparent; 112 | border: 0px solid transparent; 113 | border-bottom: 2px solid transparent; 114 | color: #29353B; 115 | padding-left: 10px; 116 | padding-right: 10px; 117 | padding-top: 3px; 118 | padding-bottom: 3px; 119 | } 120 | 121 | QTabBar::tab:hover { 122 | background-color: transparent; 123 | border: 0px solid transparent; 124 | border-bottom: 2px solid #6ECE12; 125 | color: #000000; 126 | } 127 | 128 | QTabBar::tab:selected { 129 | background-color: transparent; 130 | border: 0px solid transparent; 131 | border-top: none; 132 | border-bottom: 2px solid #6ECE12; 133 | color: #000000; 134 | } 135 | 136 | QStackedWidget { 137 | background: #F5F5F5; 138 | } 139 | 140 | /************************************* 141 | Progressbar 142 | **************************************/ 143 | QProgressBar 144 | { 145 | border: 2px solid grey; 146 | border-radius: 5px; 147 | text-align: center; 148 | } 149 | 150 | QProgressBar::chunk 151 | { 152 | background-color: #6ECE12; 153 | width: 2.15px; 154 | margin: 0.5px; 155 | } 156 | 157 | /************************************* 158 | Labels and Rich Text boxes 159 | **************************************/ 160 | QLabel { 161 | background-color: transparent; 162 | color: #29353B; 163 | } 164 | 165 | QDialog { 166 | background-color: transparent; 167 | color: #29353B; 168 | } 169 | 170 | QTextBrowser { 171 | background-color: transparent; 172 | color: #29353B; 173 | } 174 | 175 | /************************************* 176 | Search Bar 177 | **************************************/ 178 | QLineEdit { 179 | background-color: transparent; 180 | selection-background-color: #669900; 181 | color: #669900; 182 | border-width: 1px; 183 | border-style: solid; 184 | border-color: transparent transparent #669900 transparent; 185 | } 186 | 187 | QLineEdit:hover { 188 | border-width: 2px; 189 | border-color: transparent transparent #6ECE12 transparent; 190 | } 191 | 192 | QLineEdit:focus { 193 | border-width: 2px; 194 | border-color: transparent transparent #6ECE12 transparent; 195 | } 196 | 197 | /************************************* 198 | Scroll bars 199 | **************************************/ 200 | QScrollBar:horizontal { 201 | background: transparent; 202 | height: 10px; 203 | margin: 0; 204 | } 205 | 206 | QScrollBar:vertical { 207 | background: transparent; 208 | width: 10px; 209 | margin: 0; 210 | } 211 | 212 | QScrollBar::handle:horizontal { 213 | background: #DCDCDC; 214 | min-width: 16px; 215 | border-radius: 5px; 216 | } 217 | 218 | QScrollBar::handle:vertical { 219 | background: #DCDCDC; 220 | min-height: 16px; 221 | border-radius: 5px; 222 | } 223 | 224 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal, 225 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 226 | background: none; 227 | } 228 | 229 | QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, 230 | QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { 231 | border: none; 232 | background: none; 233 | } 234 | 235 | /************************************* 236 | List 237 | **************************************/ 238 | QListWidget { 239 | background-color: transparent; 240 | border: 0px solid transparent; 241 | border-bottom: 2px solid #80CBC4; 242 | color: #29353B; 243 | } 244 | 245 | QListView { 246 | background-color: transparent; 247 | color: #29353B; 248 | outline: 0; 249 | border: 0px solid transparent; 250 | } 251 | QListView::item:hover { 252 | color: #000000; 253 | background: transparent; 254 | } 255 | 256 | QListView::item:selected { 257 | color: #6ECE12; 258 | background: transparent; 259 | } 260 | 261 | QListView::item:disabled { 262 | color: #b3b3cc; 263 | background: transparent; 264 | } 265 | 266 | QListView::item:disabled:selected { 267 | color: #6ECE12; 268 | background: transparent; 269 | } 270 | 271 | /************************************* 272 | Buttons 273 | **************************************/ 274 | QPushButton { 275 | background-color: transparent; 276 | color: #29353B; 277 | border: 1px solid transparent; 278 | padding: 4px 22px; 279 | } 280 | 281 | QPushButton:hover { 282 | border-left: 2px solid #6ECE12; 283 | border-right: 2px solid #6ECE12; 284 | color: #000000; 285 | } 286 | 287 | QPushButton:pressed { 288 | color: #000000; 289 | } 290 | 291 | QPushButton:disabled { 292 | color:#b3b3cc; 293 | } 294 | 295 | QPushButton:checked { 296 | color: #6ECE12; 297 | } 298 | 299 | /************************************* 300 | ComboBox 301 | **************************************/ 302 | QComboBox { 303 | border: 0px solid transparent; 304 | border-radius: 2px; 305 | padding: 1px 6px 1px 6px; 306 | min-width: 2em; 307 | } 308 | 309 | QComboBox:!editable { 310 | selection-background-color: transparent; 311 | color: #29353B; 312 | selection-color: #000000; 313 | background-color: transparent; 314 | } 315 | 316 | QComboBox:disabled { 317 | color: #b3b3cc; 318 | } 319 | 320 | QComboBox:!editable:on, QComboBox::drop-down:editable:on { 321 | color: #29353B; 322 | background-color: transparent; 323 | selection-background-color: transparent; 324 | } 325 | 326 | QComboBox:on { 327 | padding-top: 3px; 328 | padding-left: 4px; 329 | } 330 | 331 | QComboBox::drop-down { 332 | background-color: transparent; 333 | subcontrol-origin: padding; 334 | subcontrol-position: top right; 335 | width: 20px; 336 | border-top-right-radius: 2px; 337 | border-bottom-right-radius: 2px; 338 | } 339 | 340 | QComboBox::down-arrow:enabled { 341 | image: url("./themes/material_design_light/icons/down-arrow.png"); 342 | } 343 | 344 | QComboBox::down-arrow:disabled { 345 | image: url("./themes/material_design_light/icons/down-arrow_off.png"); 346 | } 347 | 348 | QComboBox::down-arrow:hover { 349 | image: url("./themes/material_design_light/icons/down-arrow_hover.png"); 350 | } 351 | 352 | QComboBox::down-arrow:on { 353 | top: 1px; 354 | left: 1px; 355 | } 356 | 357 | QComboBox QAbstractItemView { 358 | background-color: #F5F5F5; 359 | } 360 | 361 | /************************************* 362 | RadioButton 363 | **************************************/ 364 | QRadioButton{ 365 | color: #29353B; 366 | } 367 | 368 | QRadioButton:disabled{ 369 | color: #b3b3cc; 370 | } 371 | 372 | QRadioButton::indicator{ 373 | width: 50px; 374 | height: 50px; 375 | } 376 | 377 | QRadioButton::indicator::unchecked { 378 | image: url("./themes/material_design_light/icons/off.png"); 379 | } 380 | 381 | QRadioButton::indicator:unchecked:hover { 382 | image: url("./themes/material_design_light/icons/off_press.png"); 383 | } 384 | 385 | QRadioButton::indicator:unchecked:pressed { 386 | image: url("./themes/material_design_light/icons/off_press.png"); 387 | } 388 | 389 | QRadioButton::indicator::checked { 390 | image: url("./themes/material_design_light/icons/on.png"); 391 | } 392 | 393 | QRadioButton::indicator:checked:hover { 394 | image: url("./themes/material_design_light/icons/on_press.png"); 395 | } 396 | 397 | QRadioButton::indicator:checked:pressed { 398 | image: url("./themes/material_design_light/icons/on_press.png"); 399 | } 400 | 401 | /************************************* 402 | SpinBox 403 | **************************************/ 404 | QSpinBox { 405 | background-color: transparent; 406 | color: #29353B; 407 | border-width: 0px; 408 | } 409 | 410 | QSpinBox:disabled { 411 | color: #b3b3cc; 412 | border-width: 0px; 413 | } 414 | 415 | QSpinBox::up-button { 416 | subcontrol-origin: border; 417 | subcontrol-position: top right; 418 | width: 16px; 419 | image: url("./themes/material_design_light/icons/up-arrow.png"); 420 | border-width: 0px; 421 | } 422 | 423 | QSpinBox::up-button:hover { 424 | image: url("./themes/material_design_light/icons/up-arrow_hover.png"); 425 | } 426 | 427 | QSpinBox::up-button:pressed { 428 | image: url("./themes/material_design_light/icons/up-arrow.png"); 429 | } 430 | 431 | QSpinBox::up-button:disabled { 432 | image: url("./themes/material_design_light/icons/up-arrow_off.png"); 433 | } 434 | 435 | QSpinBox::down-button { 436 | subcontrol-origin: border; 437 | subcontrol-position: bottom right; 438 | width: 16px; 439 | image: url("./themes/material_design_light/icons/down-arrow.png"); 440 | border-width: 0px; 441 | border-top-width: 0; 442 | } 443 | 444 | QSpinBox::down-button:hover { 445 | image: url("./themes/material_design_light/icons/down-arrow_hover.png"); 446 | } 447 | 448 | QSpinBox::down-button:pressed { 449 | image: url("./themes/material_design_light/icons/down-arrow.png"); 450 | } 451 | 452 | QSpinBox::down-button:disabled { 453 | image: url("./themes/material_design_light/icons/down-arrow_off.png"); 454 | } 455 | 456 | /************************************* 457 | TreeViewMenu (Mode) 458 | **************************************/ 459 | QTreeView { 460 | background-color: transparent; 461 | selection-background-color: transparent; 462 | border: 0px; 463 | } 464 | 465 | QTreeView::item { 466 | background-color: transparent; 467 | color: #29353B; 468 | } 469 | 470 | QTreeView::item:hover { 471 | border-right: 2px solid #6ECE12; 472 | color: #000000; 473 | } 474 | 475 | QTreeView::item:selected { 476 | color: #6ECE12; 477 | } 478 | 479 | QTreeView::item:active{ 480 | background: transparent; 481 | } 482 | 483 | QTreeView::item:disabled{ 484 | color: #b3b3cc; 485 | } 486 | 487 | QTreeView::item:selected:disabled{ 488 | color: #6ECE12; 489 | } 490 | -------------------------------------------------------------------------------- /src/updater.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import os.path 4 | import sys 5 | from PyQt5.QtCore import QObject, QProcess 6 | from PyQt5.QtGui import QPixmap 7 | from PyQt5.QtWidgets import QApplication, qApp 8 | from download_window import DownloadWindow 9 | from constants import Constants, DownloadTarget 10 | from downloadtargetfactory import get_download_target 11 | 12 | 13 | __VERSION__ = "0.0.1" 14 | 15 | 16 | # Global stylesheet. 17 | stylesheet = """ 18 | /************************************* 19 | Main Window and Splitters 20 | **************************************/ 21 | QWidget:window { 22 | background-color: #29353B; 23 | } 24 | 25 | /************************************* 26 | Main menu (Bar) 27 | **************************************/ 28 | QMenuBar { 29 | background-color: transparent; 30 | color: #AFBDC4; 31 | } 32 | 33 | QMenuBar::item { 34 | background-color: transparent; 35 | } 36 | 37 | QMenuBar::item:disabled { 38 | color: gray; 39 | } 40 | 41 | QMenuBar::item:selected { 42 | color: #FFFFFF; 43 | border-bottom: 2px solid #88cc00; 44 | } 45 | 46 | QMenuBar::item:pressed { 47 | color: #FFFFFF; 48 | border-bottom: 2px solid #88cc00; 49 | } 50 | 51 | QToolBar { 52 | background-color: transparent; 53 | border: 1px solid transparent; 54 | } 55 | 56 | QToolBar:handle { 57 | background-color: transparent; 58 | border-left: 2px dotted #80CBC4; 59 | color: transparent; 60 | } 61 | 62 | QToolBar::separator { 63 | border: 0; 64 | } 65 | 66 | QMenu { 67 | background-color: #263238; 68 | color: #AFBDC4; 69 | } 70 | 71 | QMenu::item:selected { 72 | color: #FFFFFF; 73 | } 74 | 75 | QMenu::item:pressed { 76 | color: #FFFFFF; 77 | } 78 | 79 | QMenu::separator { 80 | background-color: transparent; 81 | height: 1px; 82 | margin-left: 10px; 83 | margin-right: 10px; 84 | margin-top: 5px; 85 | margin-bottom: 5px; 86 | } 87 | 88 | 89 | /************************************* 90 | Progressbar 91 | **************************************/ 92 | QProgressBar 93 | { 94 | border: 2px solid grey; 95 | border-radius: 5px; 96 | text-align: center; 97 | } 98 | 99 | QProgressBar::chunk 100 | { 101 | background-color: #88cc00; 102 | width: 2.15px; 103 | margin: 0.5px; 104 | } 105 | 106 | /************************************* 107 | Labels and Rich Text boxes 108 | **************************************/ 109 | QLabel { 110 | background-color: transparent; 111 | color: #CFD8DC; 112 | } 113 | 114 | QDialog { 115 | background-color: transparent; 116 | color: #949a9c; 117 | } 118 | 119 | QTextBrowser { 120 | background-color: transparent; 121 | color: #949a9c; 122 | } 123 | 124 | /************************************* 125 | Buttons 126 | **************************************/ 127 | QPushButton { 128 | background-color: transparent; 129 | color: #AFBDC4; 130 | border: 1px solid transparent; 131 | padding: 4px 22px; 132 | } 133 | 134 | QPushButton:hover { 135 | border-left: 2px solid #88cc00; 136 | border-right: 2px solid #88cc00; 137 | color: #FFFFFF; 138 | } 139 | 140 | QPushButton:pressed { 141 | color: #FFFFFF; 142 | } 143 | 144 | QPushButton:disabled { 145 | color:#546E7A; 146 | } 147 | 148 | QPushButton:checked { 149 | color: #88cc00; 150 | } 151 | """ 152 | 153 | 154 | class _ArtemisUpdater(QObject): 155 | """Updater of the main software.""" 156 | 157 | def __init__(self, target): 158 | super().__init__() 159 | self.target = get_download_target(DownloadTarget.SOFTWARE, target) 160 | self.download_window = DownloadWindow() 161 | self.download_window.setStyleSheet(stylesheet) 162 | self.download_window.cancel_btn.clicked.connect(qApp.quit) 163 | self.download_window.complete.connect(self.start_main_program) 164 | 165 | def start(self): 166 | """Close the main program and start the download.""" 167 | self.download_window.activate(self.target) 168 | 169 | def init_ok(self): 170 | return self.target.url and self.target.hash_code and self.target.size > 0 171 | 172 | def start_main_program(self): 173 | """Restart the (updated) main program and close the updater.""" 174 | self.download_window.setVisible(False) 175 | artemis = QProcess() 176 | try: 177 | artemis.startDetached(Constants.EXECUTABLE_NAME) 178 | except BaseException: 179 | pass 180 | qApp.quit() 181 | 182 | 183 | if __name__ == '__main__': 184 | parser = argparse.ArgumentParser(prog='Artemis Updater') 185 | parser.add_argument("url", nargs="?", default="", type=str, help="Download url") 186 | parser.add_argument("hash_code", nargs="?", default="", type=str, help="sha256 of the file") 187 | parser.add_argument("size", nargs="?", default=0, type=int, help="Size (KB) of the file") 188 | parser.add_argument('--version', action='version', version=__VERSION__) 189 | args = parser.parse_args() 190 | 191 | my_app = QApplication(sys.argv) 192 | ARTEMIS_ICON = os.path.join(":", "icon", "default_pics", "Artemis3.500px.png") 193 | img = QPixmap(ARTEMIS_ICON) 194 | updater = _ArtemisUpdater(args) 195 | 196 | if not updater.init_ok(): 197 | updater.start_main_program() 198 | else: 199 | updater.start() 200 | sys.exit(my_app.exec_()) 201 | -------------------------------------------------------------------------------- /src/updatescontroller.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess as sp 3 | import webbrowser 4 | from PyQt5.QtCore import QObject, pyqtSlot, QProcess 5 | from PyQt5.QtWidgets import QMessageBox, qApp 6 | from constants import Constants, Messages, DownloadTarget 7 | from downloadtargetfactory import get_download_target 8 | from utilities import pop_up 9 | from os_utilities import IS_MAC 10 | from executable_utilities import IS_BINARY 11 | from threads import UpdatesControllerThread 12 | from versioncontroller import VersionController 13 | 14 | 15 | class UpdatesController(QObject): 16 | 17 | def __init__(self, current_version, owner): 18 | super().__init__() 19 | self._owner = owner 20 | self._download_window = self._owner.download_window 21 | self._current_version = current_version 22 | self.version_controller = VersionController() 23 | self._updates_thread = UpdatesControllerThread(self.version_controller) 24 | self._updates_thread.on_success.connect(self._startup_updates_check) 25 | 26 | def start(self): 27 | """Start the thread.""" 28 | if IS_BINARY: 29 | self._updates_thread.start() 30 | 31 | @pyqtSlot() 32 | def start_verify_software_version(self): 33 | if not IS_BINARY: 34 | pop_up( 35 | self._owner, 36 | title=Messages.FEATURE_NOT_AVAILABLE, 37 | text=Messages.SCRIPT_NOT_UPDATE 38 | ).show() 39 | return 40 | if not self._download_window.isVisible(): 41 | self._updates_thread.start() 42 | 43 | @pyqtSlot(bool) 44 | def _verify_software_version(self, success): 45 | """Verify if there is a new software version. 46 | 47 | Otherwise notify the user that the software is up to date.""" 48 | if not self._download_window.isVisible(): 49 | if success: 50 | new_version_found = self._check_new_version() 51 | if not new_version_found: 52 | pop_up( 53 | self._owner, 54 | title=Messages.UP_TO_DATE, 55 | text=Messages.UP_TO_DATE_MSG 56 | ).show() 57 | else: 58 | pop_up( 59 | self._owner, 60 | title=Messages.NO_CONNECTION, 61 | text=Messages.NO_CONNECTION_MSG 62 | ).show() 63 | 64 | @pyqtSlot(bool) 65 | def _startup_updates_check(self, success): 66 | self._updates_thread.on_success.disconnect() 67 | self._updates_thread.on_success.connect(self._verify_software_version) 68 | if success: 69 | if not self._check_new_version(): 70 | # Check for a new version of the updater only if Artemis is up to date. 71 | self._check_updater_version() 72 | 73 | def _check_new_version(self): 74 | """Check whether there is a new software version available. 75 | 76 | Does something only if the running program is a compiled version.""" 77 | if not IS_BINARY: 78 | return None 79 | latest_version = self.version_controller.software.version 80 | if latest_version is None: 81 | return False 82 | if latest_version == self._current_version: 83 | return False 84 | answer = pop_up( 85 | self._owner, 86 | title=Messages.NEW_VERSION_AVAILABLE, 87 | text=Messages.NEW_VERSION_MSG(latest_version), 88 | informative_text=Messages.DOWNLOAD_SUGG_MSG, 89 | is_question=True, 90 | ).exec() 91 | if answer == QMessageBox.Yes: 92 | if IS_MAC: 93 | webbrowser.open(self.version_controller.software.url) 94 | else: 95 | updater = QProcess() 96 | command = Constants.UPDATER_SOFTWARE + " " + \ 97 | self.version_controller.software.url + \ 98 | " " + self.version_controller.software.hash_code + \ 99 | " " + str(self.version_controller.software.size) 100 | try: 101 | updater.startDetached(command) 102 | except BaseException: 103 | logging.error("Unable to start updater") 104 | pass 105 | else: 106 | qApp.quit() 107 | return True 108 | 109 | def _check_updater_version(self): 110 | """Check is a new version of the updater is available. 111 | 112 | If so, ask to download the new version. 113 | If the software is not a compiled version, the function is a NOP.""" 114 | if not IS_BINARY or IS_MAC: 115 | return None 116 | latest_updater_version = self.version_controller.updater.version 117 | try: 118 | with sp.Popen( 119 | [Constants.UPDATER_SOFTWARE, "--version"], 120 | encoding="UTF-8", 121 | stdout=sp.PIPE, 122 | stderr=sp.STDOUT, 123 | stdin=sp.DEVNULL # Needed to avoid OsError: [WinError 6] The handle is invalid. 124 | ) as proc: 125 | updater_version = proc.stdout.read().rstrip("\r\n") # Strip any possible newline, to be sure. 126 | except Exception: 127 | logging.error("Unable to query the updater") 128 | updater_version = latest_updater_version 129 | if latest_updater_version is None: 130 | return None 131 | if updater_version != latest_updater_version: 132 | answer = pop_up( 133 | self._owner, 134 | title=Messages.UPDATES_AVAILABALE, 135 | text=Messages.UPDATES_MSG, 136 | is_question=True, 137 | ).exec() 138 | if answer == QMessageBox.Yes: 139 | self._download_window.activate( 140 | get_download_target( 141 | DownloadTarget.UPDATER, 142 | self.version_controller.updater 143 | ) 144 | ) 145 | -------------------------------------------------------------------------------- /src/urlbutton.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QPushButton 2 | from collections import namedtuple 3 | from enum import Enum, auto 4 | 5 | 6 | class UrlButton(QPushButton): 7 | """Define the behaviour of the wiki button.""" 8 | 9 | class State(Enum): 10 | """Possible states of the button.""" 11 | ACTIVE = auto() 12 | INACTIVE = auto() 13 | CLICKED = auto() 14 | 15 | _UrlColors = namedtuple( 16 | "UrlColors", 17 | [ 18 | "INACTIVE", 19 | "ACTIVE", 20 | "CLICKED", 21 | "ACTIVE_HOVER", 22 | "CLICKED_HOVER", 23 | ] 24 | ) 25 | _COLORS = _UrlColors( 26 | "#9f9f9f", 27 | "#4c75ff", 28 | "#942ccc", 29 | "#808FFF", 30 | "#DE78FF", 31 | ) 32 | 33 | def __init__(self, *args, **kwargs): 34 | super().__init__(*args, **kwargs) 35 | 36 | def set_enabled(self, state): 37 | """Enable the button and set the stylesheet.""" 38 | super().setEnabled(True) 39 | if state is self.State.ACTIVE: 40 | color = self._COLORS.ACTIVE 41 | else: 42 | color = self._COLORS.CLICKED 43 | self.setStyleSheet(f""" 44 | QPushButton {{ 45 | border: 0px; 46 | background-color: transparent; 47 | color: {color}; 48 | }} 49 | QPushButton::hover {{ 50 | border: 0px; 51 | background-color: transparent; 52 | color: {self._COLORS.ACTIVE_HOVER}; 53 | }} 54 | """) 55 | 56 | def set_disabled(self): 57 | """Disable the button and set the stylesheet.""" 58 | super().setEnabled(False) 59 | self.setStyleSheet(f""" 60 | QPushButton:disabled {{ 61 | border: 0px; 62 | background-color: transparent; 63 | color: {self._COLORS.INACTIVE}; 64 | }} 65 | """) 66 | 67 | def set_clicked(self): 68 | """Apply the stylesheet for the clicked state.""" 69 | self.setStyleSheet(f""" 70 | QPushButton {{ 71 | border: 0px; 72 | background-color: transparent; 73 | color: {self._COLORS.CLICKED}; 74 | }} 75 | QPushButton::hover {{ 76 | border: 0px; 77 | background-color: transparent; 78 | color: {self._COLORS.CLICKED_HOVER}; 79 | }} 80 | """) 81 | -------------------------------------------------------------------------------- /src/utilities.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from functools import partial 3 | import hashlib 4 | from PyQt5.QtWidgets import QMessageBox 5 | from constants import Constants, Signal 6 | 7 | 8 | class UniqueMessageBox(QMessageBox): 9 | """Subclass of QMessageBox. Overrides only the exec method. 10 | 11 | Only one instance of this class can execute super().exec() exec at a given time. 12 | If another instance is the the exec loop, calling exec simply return None.""" 13 | 14 | _open_message = False 15 | _font = None 16 | 17 | @classmethod 18 | def set_font(cls, font): 19 | """Store the font for all UniqueMessageBox(es).""" 20 | cls._font = font 21 | 22 | def __init__(self, *args, **kwargs): 23 | super().__init__(*args, **kwargs) 24 | 25 | def setFont(self, font): 26 | """Extends QMessageBox.setFont. Apply the font only if it is not None.""" 27 | if font is not None: 28 | super().setFont(font) 29 | 30 | def exec(self): 31 | """Overrides QMessageBox.exec. Call the parent method if there are no 32 | other instances executing exec; also set the current font. 33 | Otherwise return None,""" 34 | if self.__class__._open_message: 35 | return None 36 | self.setFont(self._font) 37 | self.__class__._open_message = True 38 | answer = super().exec() 39 | self.__class__._open_message = False 40 | return answer 41 | 42 | def show(self): 43 | """Extends QMessageBox.show(). 44 | 45 | Set the font before showing the message.""" 46 | self.setFont(self._font) 47 | super().show() 48 | 49 | 50 | def uncheck_and_emit(button): 51 | """Set the button to the unchecked state and emit the clicked signal.""" 52 | if button.isChecked(): 53 | button.setChecked(False) 54 | button.clicked.emit() 55 | 56 | 57 | def show_matching_strings(list_elements, text): 58 | """Show all elements of QListWidget that matches (even partially) a target text. 59 | 60 | Arguments: 61 | list_elements -- the QListWidget 62 | text -- the target text.""" 63 | for index in range(list_elements.count()): 64 | item = list_elements.item(index) 65 | if text.lower() in item.text().lower() or item.isSelected(): 66 | item.setHidden(False) 67 | else: 68 | item.setHidden(True) 69 | 70 | 71 | def get_field_entries(db_entry, separator=Constants.FIELD_SEPARATOR): 72 | """Take a database entry and optionally a separator string. 73 | 74 | Return a list obtained by splitting the signal field with separator.""" 75 | return [ 76 | x.strip() for x in db_entry.split(separator) 77 | ] 78 | 79 | 80 | def pop_up(instance, title, text, 81 | informative_text=None, 82 | connection=None, 83 | is_question=False, 84 | default_btn=QMessageBox.Yes): 85 | """Return a QMessageBox object. 86 | 87 | Keyword arguments: 88 | informative_text -- possible informative text to be displayed. 89 | connection -- a callable to connect the message when emitting the finished signal. 90 | is_question -- whether the message contains a question. 91 | default_btn -- the default button for the possible answer to the question.""" 92 | msg = UniqueMessageBox(instance) 93 | msg.setWindowTitle(title) 94 | msg.setText(text) 95 | if informative_text: 96 | msg.setInformativeText(informative_text) 97 | if connection: 98 | msg.finished.connect(connection) 99 | if is_question: 100 | msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) 101 | msg.setDefaultButton(default_btn) 102 | msg.adjustSize() 103 | return msg 104 | 105 | 106 | def checksum_ok(data, reference_hash_code): 107 | """Check whether the checksum of the 'data' argument is correct. 108 | 109 | Expects a sha256 code as argument.""" 110 | if reference_hash_code is None: 111 | raise ValueError("ERROR: Invalid hash code.") 112 | code = hashlib.sha256() 113 | code.update(data) 114 | return code.hexdigest() == reference_hash_code 115 | 116 | 117 | def connect_events_to_func(events_to_connect, fun_to_connect, fun_args): 118 | """Connect all elements of events_to_connect to the callable fun_to_connect. 119 | 120 | fun_args is a list of fun_to_connect arguments.""" 121 | if fun_args is not None: 122 | for event in events_to_connect: 123 | event.connect(partial(fun_to_connect, *fun_args)) 124 | else: 125 | for event in events_to_connect: 126 | event.connect(fun_to_connect) 127 | 128 | 129 | def filters_limit(spinbox, filter_unit, confidence, sign=1): 130 | """Return the actual limit of a numerical filter.""" 131 | band_filter = spinbox.value() * Constants.CONVERSION_FACTORS[filter_unit.currentText()] 132 | return band_filter + sign * (confidence.value() * band_filter) // 100 133 | 134 | 135 | def is_undef_freq(current_signal): 136 | """Return whether the lower or upper frequency of a signal is undefined.""" 137 | lower_freq = current_signal.at[Signal.INF_FREQ] 138 | upper_freq = current_signal.at[Signal.SUP_FREQ] 139 | return lower_freq == Constants.UNKNOWN or upper_freq == Constants.UNKNOWN 140 | 141 | 142 | def is_undef_band(current_signal): 143 | """Return whether the lower or upper band of a signal is undefined.""" 144 | lower_band = current_signal.at[Signal.INF_BAND] 145 | upper_band = current_signal.at[Signal.SUP_BAND] 146 | return lower_band == Constants.UNKNOWN or upper_band == Constants.UNKNOWN 147 | 148 | 149 | def _change_unit(str_num): 150 | """Return a scale factor given the number of digits of a numeric string.""" 151 | digits = len(str_num) 152 | if digits < 4: 153 | return 1 154 | elif digits < 7: 155 | return 1000 156 | elif digits < 10: 157 | return 10**6 158 | else: 159 | return 10**9 160 | 161 | 162 | def format_numbers(lower, upper): 163 | """Return the string which displays the numeric limits of a filter.""" 164 | units = {1: 'Hz', 1000: 'kHz', 10**6: 'MHz', 10**9: 'GHz'} 165 | lower_factor = _change_unit(lower) 166 | upper_factor = _change_unit(upper) 167 | pre_lower = lower 168 | pre_upper = upper 169 | lower = safe_cast(lower, int) / lower_factor 170 | upper = safe_cast(upper, int) / upper_factor 171 | if lower.is_integer(): 172 | lower = int(lower) 173 | else: 174 | lower = round(lower, 2) 175 | if upper.is_integer(): 176 | upper = int(upper) 177 | else: 178 | upper = round(upper, 2) 179 | if pre_lower != pre_upper: 180 | return f"{lower:,} {units[lower_factor]} - {upper:,} {units[upper_factor]}" 181 | else: 182 | return f"{lower:,} {units[lower_factor]}" 183 | 184 | 185 | def safe_cast(value, cast_type, default=-1): 186 | """Call 'cast_type(value)' and return the result. 187 | 188 | If the operation fails return 'default'. 189 | Should be used to perform 'safe casts'. 190 | Keyword argument: 191 | default -- default value returned if the cast fails. 192 | """ 193 | try: 194 | r = cast_type(value) 195 | except Exception: 196 | logging.error("Cast type failure") 197 | r = default 198 | finally: 199 | return r 200 | 201 | 202 | def get_file_extension(file): 203 | """Return the extension of a file. Return None if there is not such property.""" 204 | components = file.split('.') 205 | if len(components) > 1: 206 | return components[-1] 207 | return None 208 | 209 | 210 | def get_value_from_list_of_dicts(iterable, callable_ok, key_value): 211 | """Return a value from a dict inside a list of dicts. 212 | 213 | The iterable is reversed first, then the value corresponding to the key key_value 214 | is returned from the first dict for which callable_ok(dict) returns True""" 215 | for d in reversed(iterable): 216 | if callable_ok(d): 217 | return d[key_value] 218 | -------------------------------------------------------------------------------- /src/versioncontroller.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import json 3 | from constants import Constants 4 | from os_utilities import get_os 5 | from web_utilities import download_file 6 | 7 | 8 | """This module exposes just one class: VersionController. 9 | 10 | All the relevant information can be accessed with the dot notation on an instance of such class, e.g.: 11 | version_controller.software.hash_code 12 | is the hash_code of the latest release of the software running on the current OS.""" 13 | 14 | 15 | def _download_versions_file(): 16 | """Download the json file containing all the information 17 | about the latest version of the software. Return a dictionary 18 | containing only the information for the running OS. 19 | 20 | Return a dictionary from a json with the following structure: 21 | { 22 | "windows": { 23 | "software": { 24 | "version": "...", 25 | "url": "...", 26 | "hash_code": "...", 27 | "size": ... 28 | }, 29 | "updater": { 30 | "version": "...", 31 | "url": "...", 32 | "hash_code": "...", 33 | "size": ... 34 | } 35 | }, 36 | "linux": { 37 | "software": { 38 | "version": "...", 39 | "url": "...", 40 | "hash_code": "...", 41 | "size": ... 42 | }, 43 | "updater": { 44 | "version": "...", 45 | "url": "...", 46 | "hash_code": "...", 47 | "size": ... 48 | } 49 | }, 50 | "mac": { 51 | "software": { 52 | "version": "...", 53 | "url": "...", 54 | "hash_code": "...", 55 | "size": ... 56 | }, 57 | "updater": { 58 | "version": "...", 59 | "url": "...", 60 | "hash_code": "...", 61 | "size": ... 62 | } 63 | } 64 | "raspberry": { 65 | "software": { 66 | "version": "...", 67 | "url": "...", 68 | "hash_code": "...", 69 | "size": ... 70 | }, 71 | "updater": { 72 | "version": "...", 73 | "url": "...", 74 | "hash_code": "...", 75 | "size": ... 76 | } 77 | } 78 | } 79 | """ 80 | try: 81 | version_dict = json.load( 82 | BytesIO(download_file(Constants.VERSION_LINK)) 83 | )[get_os()] 84 | except Exception: 85 | return None 86 | else: 87 | return version_dict 88 | 89 | 90 | class VersionController: 91 | """Dynamically create attributes corresponding to elements of a dictionary. 92 | 93 | Used to get updates information.""" 94 | 95 | def __init__(self, dct=None): 96 | """Initialize the dictionary""" 97 | self._dct = dct 98 | 99 | def __getattr__(self, attr): 100 | """Override super().__getattr__. Dynamically create new attributes 101 | corresponding to elements of the diciotnary.""" 102 | if self._dct is None: 103 | if not self.update(): 104 | return None 105 | dct_element = self._dct.get(attr, None) 106 | if dct_element is None: 107 | return None 108 | if isinstance(dct_element, dict): 109 | setattr(self, attr, type(self)(dct_element)) 110 | else: 111 | setattr(self, attr, dct_element) 112 | return getattr(self, attr) 113 | 114 | def update(self): 115 | """Reset the dictionary to the correspondig json file containing 116 | the latest version information. Call this function inside a Qthread.""" 117 | dct = _download_versions_file() 118 | if dct is not None: 119 | self._dct = dct 120 | return True 121 | else: 122 | return False 123 | -------------------------------------------------------------------------------- /src/web_utilities.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import urllib3 5 | from constants import Database 6 | from executable_utilities import IS_BINARY 7 | 8 | 9 | def get_cacert_file(): 10 | """Return the path to the cacert.pem file. 11 | PyInstaller requires _MEIPASS, Nuitka does not.""" 12 | if IS_BINARY and not "__compiled__" in globals(): 13 | ca_certs = os.path.join(sys._MEIPASS, 'cacert.pem') 14 | else: 15 | ca_certs = 'cacert.pem' 16 | return ca_certs 17 | 18 | 19 | def get_pool_manager(): 20 | """Return a urllib3.PoolManager object.""" 21 | return urllib3.PoolManager(ca_certs=get_cacert_file()) 22 | 23 | 24 | def download_file(url, encoding=""): 25 | resp = get_pool_manager().request( 26 | 'GET', 27 | url, 28 | preload_content=True, 29 | timeout=4.0 30 | ).data 31 | if encoding: 32 | return resp.decode(encoding) 33 | return resp 34 | 35 | 36 | def _download_multiline_file_as_list(url=Database.LINK_REF): 37 | """Download a text file and return the last line as a list. 38 | 39 | The downloaded file is a csv file with columns (last version == last line): 40 | data.zip_SHA256 | db.csv_SHA256 | Version | Creation_date""" 41 | try: 42 | return download_file(url, encoding="UTF-8").splitlines()[-1].split(Database.DELIMITER) 43 | except Exception: 44 | logging.error("Database metadata download failure") 45 | return None 46 | 47 | 48 | def get_folder_hash_code(): 49 | f = _download_multiline_file_as_list() 50 | if f is not None: 51 | return f[0] 52 | return None 53 | 54 | 55 | def get_db_hash_code(): 56 | f = _download_multiline_file_as_list() 57 | if f is not None: 58 | return f[1] 59 | return None 60 | -------------------------------------------------------------------------------- /unused_installation_scripts/Linux/deploy_linux.sh: -------------------------------------------------------------------------------- 1 | clear 2 | echo " 3 | =================================== 4 | Artemis 3 Deploy Script 5 | LINUX 6 | =================================== 7 | " 8 | 9 | # Set the correct permissions for Artemis folder 10 | echo "Gaining admin privileges and set folder read/write permission..." 11 | echo "" 12 | sudo chmod 700 $PWD/../../ 13 | # Download necessary libraries with pip3 14 | read -p "Install the necessary Python libraries? [Y,N]..." doit 15 | case $doit in 16 | y|Y) pip3 install -r requirements_lin.txt --user > log;; 17 | esac 18 | 19 | # Generation of shortcut 20 | echo "" 21 | read -p "Create a desktop shortcut? [Y/N]..." doit 22 | case $doit in 23 | y|Y) 24 | cat << EOR > /home/$USER/.local/share/applications/artemis.desktop 25 | #!/usr/bin/env xdg-open 26 | [Desktop Entry] 27 | Name=Artemis 28 | StartupWMClass=artemis3 29 | Exec=sh -c "cd $PWD/../../ && python3 artemis.py" 30 | Terminal=False 31 | Icon=artemis3 32 | Type=Application 33 | EOR 34 | sudo cp ./artemis3.svg /usr/share/icons/ 35 | echo " 36 | Link copied in: /home/$USER/.local/share/applications/artemis.desktop 37 | Icon copied in: /usr/share/icons/artemis3.svg 38 | " 39 | ;; 40 | n|N) ;; 41 | *) echo "invalid option $REPLY";; 42 | esac 43 | echo " 44 | ================================ 45 | SETTING COMPLETE 46 | ================================ 47 | " 48 | -------------------------------------------------------------------------------- /unused_installation_scripts/Linux/requirements_lin.txt: -------------------------------------------------------------------------------- 1 | pandas>=0.24.2 2 | certifi>=2019.6.16 3 | aiohttp>=3.5.4 4 | pygame>=1.9.6 5 | QtAwesome>=0.5.7 6 | urllib3>=1.25.3 7 | PyQt5>=5.12.3 8 | -------------------------------------------------------------------------------- /unused_installation_scripts/README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ARTEMIS 3 ![LICENSE](https://img.shields.io/github/license/AresValley/Artemis.svg?style=flat-square) ![ISSUE](https://img.shields.io/github/issues/AresValley/Artemis.svg?style=flat-square) ![LANGUAGE](https://img.shields.io/github/languages/top/AresValley/Artemis.svg?style=flat-square) 4 | 5 | *Radio Signals Recognition Manual* 6 | 7 | ## ARTEMIS 3 - Unused Deployment Scripts 8 | 9 | This folder contains a third option to run Artemis 3 on your pc. The method of installation is based on an automatic script that set privileges, install dependencies and create a working shortcut to your desktop/menu. 10 | 11 | **For the sake of completeness, the documentation is available below, but we strongly discourage any attempt to use it.** 12 | 13 | ## Run using deploy script 14 | 15 | 16 | 17 | > ### Windows: 18 | > 19 | > 1. Windows don't offer a native version of Python. Download and install Python 3 (> 3.7) from the official website (https://www.python.org/downloads/). Be sure to select the flag `Add Python 3.x to PATH` during the first part of the installation. To verify the correct installation of Python open a CMD terminal (Open the **run** windows with **Win+R** and type **cmd**) and check the version of the just installed python 3 with: 20 | > ``` 21 | > python --version 22 | > ``` 23 | > 2. Use the `clone or download` button (https://github.com/AresValley/Artemis/archive/master.zip) to download the source code of Artemis 3. 24 | > 3. Extract the .zip and place Artemis folder where you prefer. The code must always be accompanied by a `themes` folder. 25 | > 4. To install the necessary libraries open the `Artemis/deploy/Windows` folder. Run (with a double click) the script `deploy_win.bat`. The script will: 26 | > 27 | > * Set the correct read/write privileges for Artemis folder. The main folder **must have the reading/writing permission** to download the Signals Database. 28 | > * Install the required Python 3 libraries with pip3. 29 | > * Generate a .pyw file (script launcher without console), and it will create a shortcut on the desktop. 30 | 31 | 32 | 33 | 34 | > ### Linux: 35 | > 36 | > 1. Linux already offers a native version of python on board. Please verify the presence of Python 3 and check the version (> 3.7) opening a terminal and typing: 37 | > ``` 38 | > python --version 39 | > ``` 40 | > If, for some reasons python, it is not present in your system follow the specific instructions to install it on your distro. For the common Linux OS: 41 | > * **Ubuntu**, **Mint**: `sudo apt-get install python3.7` 42 | > * **Fedora**: `sudo dnf install python37` 43 | > 2. Use the `clone or download` button (https://github.com/AresValley/Artemis/archive/master.zip) to download the source code of Artemis 3. 44 | > 3. Extract the .zip where you like (use `unzip Artemis-master.zip`). The code must always be accompanied by a `themes` folder. 45 | > 4. To install the necessary libraries open the `Artemis/deploy/Linux` folder. Run the script `deploy_linux.sh` typing in a terminal: 46 | > ``` 47 | > cd PATH / TO / ARTEMIS / FOLDER /deploy/Linux 48 | > sh deploy_linux.sh 49 | > ``` 50 | > 51 | > 5. Follow the terminal instructions. At the end, you will find a shortcut to Artemis 3 in the main menu. The script `deploy_linux.sh` will: 52 | > 53 | > * Set the correct read/write privileges for Artemis folder. The main folder **must have the reading/writing permission** to download the Signals Database. 54 | > * Install the required Python 3 libraries with pip3. 55 | > * Generate a .desktop file (script launcher without console) in `$HOME/.local/share/applications` and it will copy the .svg Artemis icon in `/usr/share/icons/`. 56 | 57 | 58 | 59 | 60 | > ### MacOS: 61 | > 62 | > 1. To Be Completed... 63 | -------------------------------------------------------------------------------- /unused_installation_scripts/Windows/artemis3.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/Artemis/b41ad0e8f2c39b131529994c56fba9a96bef891d/unused_installation_scripts/Windows/artemis3.ico -------------------------------------------------------------------------------- /unused_installation_scripts/Windows/deploy_win.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo =================================== 3 | echo Artemis 3 Deploy Script 4 | echo WINDOWS 5 | echo =================================== 6 | REM Check and gain admin permissions 7 | IF "%PROCESSOR_ARCHITECTURE%" EQU "amd64" ( 8 | >nul 2>&1 "%SYSTEMROOT%\SysWOW64\cacls.exe" "%SYSTEMROOT%\SysWOW64\config\system" 9 | ) ELSE ( 10 | >nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system" 11 | ) 12 | 13 | if '%errorlevel%' NEQ '0' ( 14 | echo Requesting administrative privileges... 15 | goto UACPrompt 16 | ) else ( goto gotAdmin ) 17 | 18 | :UACPrompt 19 | echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs" 20 | set params= %* 21 | echo UAC.ShellExecute "cmd.exe", "/c ""%~s0"" %params:"=""%", "", "runas", 1 >> "%temp%\getadmin.vbs" 22 | 23 | "%temp%\getadmin.vbs" 24 | del "%temp%\getadmin.vbs" 25 | exit /B 26 | 27 | :gotAdmin 28 | pushd "%CD%" 29 | CD /D "%~dp0" 30 | echo: 31 | 32 | REM Set the correct permissions for Artemis folder 33 | set artemis_path=%~dp0..\.. 34 | icacls "%artemis_path%" /grant %USERNAME%:(OI)(CI)F /T > log 35 | echo Gaining admin privileges and set folder read/write permission... DONE! 36 | 37 | REM Download necessary libraries with pip3 38 | echo: 39 | set choice=Y 40 | set /p choice=Install the necessary Python libraries? [Y,N]... 41 | echo: 42 | if /I '%choice%'=='Y' pip3 install -r %~dp0requirements_win.txt --no-color >> log 43 | 44 | REM Generation of shortcut 45 | echo: 46 | set choice=Y 47 | set /p choice=Create a desktop shortcut? [Y/N]... 48 | if /I '%choice%'=='N' goto end 49 | 50 | IF EXIST "%artemis_path%\Artemis.py" ( 51 | ren "%artemis_path%\Artemis.py" "Artemis.pyw" 52 | ) 53 | 54 | echo Set oWS = WScript.CreateObject("WScript.Shell") > CreateShortcut.vbs 55 | echo sLinkFile = "%USERPROFILE%\Desktop\Artemis.lnk" >> CreateShortcut.vbs 56 | echo Set oLink = oWS.CreateShortcut(sLinkFile) >> CreateShortcut.vbs 57 | echo oLink.TargetPath = "%artemis_path%\Artemis.pyw" >> CreateShortcut.vbs 58 | echo oLink.WorkingDirectory = "%artemis_path%" >> CreateShortcut.vbs 59 | echo oLink.IconLocation = "%~dp0artemis3.ico" >> CreateShortcut.vbs 60 | echo oLink.Save >> CreateShortcut.vbs 61 | cscript /nologo CreateShortcut.vbs 62 | del CreateShortcut.vbs 63 | :end 64 | echo: 65 | echo ================================ 66 | echo SETTING COMPLETE 67 | echo ================================ 68 | echo: 69 | pause -------------------------------------------------------------------------------- /unused_installation_scripts/Windows/requirements_win.txt: -------------------------------------------------------------------------------- 1 | pandas>=0.24.2 2 | certifi>=2019.6.16 3 | aiohttp>=3.5.4 4 | pygame>=1.9.6 5 | QtAwesome>=0.5.7 6 | urllib3>=1.25.3 7 | PyQt5>=5.12.3 8 | --------------------------------------------------------------------------------