Compare commits

...

22 Commits

Author SHA1 Message Date
Vxrtrauter 912d9d8c00 fix: Add shutdown helper call and modify exit behavior in updater 2026-04-07 02:26:15 +02:00
Vxrtrauter ce2cd9cdd8 Update release workflow to use RELEASE_TOKEN instead of GITHUB_TOKEN 2026-04-07 01:48:59 +02:00
Vxrtrauter 5e6e96a8cd Add dynamic release name using short SHA in release workflow 2026-04-07 01:45:26 +02:00
Vxrtrauter 8161e28d67 Merge branch 'main' of https://github.com/KeksPirates/SoftwareManager 2026-04-07 01:13:45 +02:00
Vxrtrauter 6253aa0eb0 Add release workflow and simplify dev workflow
Remove automatic main push trigger from dev workflow (now only workflow_dispatch) and add a new release workflow (.github/workflows/release.yml). The release workflow triggers on tag pushes, builds artifacts for Linux, Windows and macOS using a matrix, sets up Python/uv, installs build tools (Nuitka/PyInstaller/UPX where needed), injects a short commit SHA into build_info.json, packages artifacts, uploads them, generates checksums, and creates a GitHub release with the built assets.
2026-04-07 01:13:38 +02:00
shayaa c063c226db Merge pull request #53 from KeksPirates/fix/image-resizing
Fix/image resizing, add accent color support
2026-04-07 01:09:02 +02:00
Vxrtrauter 929fdf69e5 Update and revert image path in settings dialog
Capture the original state.image_path at dialog start, connect the Image Path QLineEdit's textChanged signal to update state.image_path immediately, and restore the original image_path if the settings dialog is cancelled. This matches existing undo behavior for width/offset/opacity and prevents partial image path edits from persisting when the dialog is rejected.
2026-04-07 01:07:22 +02:00
shayaa 48b9919b7a Merge pull request #52 from KeksPirates/feat/docs
Feat/docs
2026-04-06 23:15:04 +02:00
Vxrtrauter c43fd006fd Add About page and expand docs content/styles
Add a new docs/about page (docs/docs.html) with site header/footer and basic About layout. Expand the Docs tab in docs/index.html with a Features section, General Information, and Sources table (including a link to the server repo and usage notes). Update docs/main.css to add styles for .sources-text, .docs-grid, .docs-tips and headings to support the new documentation layout and improve visual presentation.
2026-04-05 08:27:41 +02:00
Vxrtrauter 12506dd1df feat: Add customizable accent color setting
Introduce an accent color preference across the app: add a settings UI input (with restart note), persist it to AppState and config (read/write), and accept it in save_settings. Integrate the value into theming: compute a semi-transparent selection color, apply it to table selection styles, and pass it to qdarktheme as the primary color (including when window transparency is enabled). Also apply the accent to the tracker combo selection styling as a UI polish.
2026-04-05 06:57:00 +02:00
Vxrtrauter 9e3d1cf90a feat: Add image position presets and sliders
Introduce image positioning presets and UI improvements for image overlay control. image.py: compute overlay coordinates for top-left/top-right/bottom-left/bottom-right/center positions and respect corner offset and opacity. settings.py: replace spinboxes with horizontal sliders (with value labels and custom style), add Wallpaper Mode checkbox and Position combo, wire controls to state and include them in the settings tab; ensure dialog rollback restores new temp settings. config/config.py & utils/settings.py: persist and load new image_as_wallpaper and image_position settings and extend save_settings to accept them. data/state.py: add new state fields and properties for image_position, custom position flags and x/y coordinates; change default image_as_wallpaper to false. These changes enable preset positioning, improved UX for sizing/offset/opacity, and persistence of new image options.
2026-04-05 06:12:54 +02:00
Vxrtrauter 2ad02435a1 feat/fix: "wallpaper" transparency handling
Introduce dynamic wallpaper transparency support for the image overlay. Adds QStackedWidget import and a new _set_wallpaper_transparency method that toggles WA_TranslucentBackground on the main window and key child widgets, updates stylesheets (with originals preserved/restored), and uses darkdetect to adapt combo popup background. Adds state fields (_wallpaper_active, _original_stylesheets), sets overlay opacity from state, and calls the transparency toggler when loading/hiding the overlay to avoid redundant updates.
2026-04-05 05:22:24 +02:00
Vxrtrauter f11837217c feat: add basic "wallpaper" functionality 2026-04-05 04:02:10 +02:00
Vxrtrauter 1a7de70c0b feat: add image opacity, config migration, settings
Add image overlay opacity support and related UI/settings, plus robust config migration and error handling.

Key changes:
- UI: image overlay now uses QGraphicsOpacityEffect, hides when disabled, and updates opacity on state changes (src/interface/dialogs/image.py).
- New NotificationPopup dialog for user alerts (src/interface/dialogs/notificationpopup.py).
- Settings dialog: added Image Opacity control, adjusted labels, persist/rollback behavior on cancel, and include opacity when saving (src/interface/dialogs/settings.py).
- State: added properties for image_opacity and image_enabled with change notifications; renamed image_width setter param for clarity (src/utils/data/state.py).
- Config: switch from config.yml to config.ini, implement one-time migration, typed read/write of values, validation/error handling, automatic backups of corrupted files and user notification (src/utils/config/config.py).
- Settings save: accept and persist image_opacity (src/utils/config/settings.py).
- Startup: initialize QApplication earlier and pass app to run_gui (src/main.py).
- Minor: removed an extra blank line in log handler and small import cleanups.

These changes enable adjustable overlay transparency, improve config reliability, and surface errors to users while preserving/backuping old configs.
2026-04-04 07:36:03 +02:00
Vxrtrauter a3ff23147f Restore image settings on cancel; live sync
Save original image_width/image_offset when opening Settings and restore them if the dialog is cancelled. Add valueChanged handlers to immediately update state.image_width and state.image_offset, enforce an image width maximum of 2500, and refactor Save wiring to a dedicated handler that calls save_settings(dialog.accept, ...). Connect dialog.finished to restore temp values on rejection and clean up dialog layout/logic.
2026-04-04 05:36:16 +02:00
Vxrtrauter fa4e753861 fix: braces in api docs 2026-04-04 04:54:58 +02:00
Vxrtrauter b0bbb98927 Convert docs to single-page tabs and update styles
Merge API and About pages into a single SPA-style index: removed docs/about.html and docs/api.html and moved their content into docs/index.html as tabbed sections (start, api, docs, notfound). Add client-side routing and history handling via a new script to support deep links and pushState, plus a Netlify-style docs/_redirects rule to serve index for all routes. Update styles in docs/main.css to target the new tab UI (.tab, .tablinks, .logo-text) and add styles for the 404 view. Minor markup and layout tweaks (images, buttons, response examples) to match the consolidated page.
2026-04-04 04:52:40 +02:00
KeksNino fda45efcb5 refactor: create a helper function to create widgets in settings dialog 2026-04-04 03:52:20 +02:00
KeksNino 3de2110a3e fix: redraws pixmap after changing image settings so restart is no longer required 2026-04-04 02:51:45 +02:00
Vxrtrauter cb7e74a224 Add configurable image overlay settings
Introduce configurable image overlay options and persist them. Added new state fields (image_width, image_offset, image_enabled) with defaults and wired them into the image overlay: the image display now respects the enabled flag, uses image_width for scaling and image_offset for placement. Extended the Settings dialog (resized, API field sizing) with a new Image tab containing enable checkbox, width and offset spinboxes, and included these values when saving. Updated config read/write to include an Image section so settings persist across runs and extended save_settings to accept image_width and image_offset. Minor import/order cleanup in image dialog.
2026-04-04 02:06:06 +02:00
Vxrtrauter 5fe3502bd6 add initial website structure 2026-03-23 21:36:21 +01:00
KeksNino b59768bcff fix image resizing and general code of image class 2026-03-22 01:41:16 +01:00
23 changed files with 1699 additions and 297 deletions
-3
View File
@@ -1,9 +1,6 @@
name: Build Executables (Dev)
on:
push:
branches:
- main
workflow_dispatch:
inputs:
version:
+237
View File
@@ -0,0 +1,237 @@
name: Release
on:
push:
tags:
- "*"
permissions:
contents: write
env:
PYTHON_VERSION: "3.12"
APP_NAME: "SoftwareManager"
jobs:
build:
name: Build ${{ matrix.os }}
strategy:
matrix:
include:
- os: ubuntu-22.04
artifact_name: SoftwareManager-${{ github.sha }}-linux
- os: windows-latest
artifact_name: SoftwareManager-${{ github.sha }}-windows
- os: macos-latest
artifact_name: SoftwareManager-${{ github.sha }}-macos
runs-on: ${{ matrix.os }}
outputs:
short_sha: ${{ steps.version.outputs.short_sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "0.9.7"
enable-cache: true
- name: Install Python dependencies
run: |
uv pip install -r requirements.txt
uv pip install Pillow
env:
UV_SYSTEM_PYTHON: 1
- name: Install Nuitka (Windows only)
if: runner.os == 'Windows'
run: uv pip install nuitka
env:
UV_SYSTEM_PYTHON: 1
- name: Generate version
id: version
shell: bash
run: |
SHORT_SHA="${{ github.sha }}"
SHORT_SHA="${SHORT_SHA:0:7}"
echo "VERSION=$SHORT_SHA" >> $GITHUB_ENV
echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT
- name: Inject commit hash into code
shell: bash
run: |
cat > build_info.json << EOF
{
"version": "${{ env.VERSION }}"
}
EOF
- name: Install UPX (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
curl -sL https://github.com/upx/upx/releases/download/v5.1.0/upx-5.1.0-win64.zip -o upx.zip
7z x upx.zip -oupx-dir
UPX_DIR="$(pwd)/upx-dir/upx-5.1.0-win64"
echo "$UPX_DIR" >> $GITHUB_PATH
export PATH="$UPX_DIR:$PATH"
echo "UPX_BINARY=$UPX_DIR/upx.exe" >> $GITHUB_ENV
upx --version | head -1
- name: Convert icon for Windows
if: runner.os == 'Windows'
shell: bash
run: |
python -c "
from PIL import Image
img = Image.open('src/interface/assets/logo.png')
img.save('app_icon.ico', format='ICO', sizes=[(256,256),(128,128),(64,64),(48,48),(32,32),(16,16)])
print('Icon converted successfully')
"
- name: Build with Nuitka (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
python -m nuitka \
--standalone \
--assume-yes-for-downloads \
--windows-console-mode=disable \
--windows-icon-from-ico=app_icon.ico \
--enable-plugin=pyside6 \
--enable-plugin=upx \
--upx-binary="${{ env.UPX_BINARY }}" \
--noinclude-qt-translations \
--include-data-files=build_info.json=build_info.json \
--output-filename=${{ env.APP_NAME }}.exe \
--output-dir=nuitka-build \
src/main.py
- name: Build with PyInstaller (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
pyinstaller --onefile --windowed \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Build with PyInstaller (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
pyinstaller --onefile \
--add-data "build_info.json:." \
--exclude-module PyQt6 \
--exclude-module PyQt5 \
--hidden-import=backports.tarfile \
--hidden-import=backports \
--hidden-import=jaraco.context \
--hidden-import=jaraco.text \
--hidden-import=jaraco.functools \
--name ${{ env.APP_NAME }} \
src/main.py
- name: Rename artifact
shell: bash
run: |
mkdir -p dist-final
if [ "$RUNNER_OS" == "Windows" ]; then
mv nuitka-build/main.dist dist-final/${{ env.APP_NAME }}
else
chmod +x dist/${{ env.APP_NAME }}
mv dist/${{ env.APP_NAME }} dist-final/${{ matrix.artifact_name }}
fi
- name: Create ZIP (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path "dist-final/${{ env.APP_NAME }}/*" -DestinationPath "dist-final/${{ matrix.artifact_name }}.zip"
- name: Build Installer (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
SETUP_NAME="SoftwareManager-${{ env.VERSION }}-windows-setup"
iscc //DMyAppVersion="${{ env.VERSION }}" \
//DMySourceDir="dist-final\\${{ env.APP_NAME }}" \
//DMyOutputDir="dist-final" \
//DMyOutputFilename="${SETUP_NAME}" \
installer.iss
echo "SETUP_NAME=${SETUP_NAME}" >> $GITHUB_ENV
- name: Upload build artifact (Windows)
if: runner.os == 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: |
dist-final/${{ matrix.artifact_name }}.zip
dist-final/${{ env.SETUP_NAME }}.exe
retention-days: 1
- name: Upload build artifact (Linux/macOS)
if: runner.os != 'Windows'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-build
path: dist-final/${{ matrix.artifact_name }}
retention-days: 1
release:
name: Create Release
needs: build
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release files
run: |
mkdir -p release
SHORT_SHA="${{ needs.build.outputs.short_sha }}"
cp artifacts/ubuntu-22.04-build/SoftwareManager-*-linux release/SoftwareManager-${SHORT_SHA}-linux
cp artifacts/windows-latest-build/SoftwareManager-*-windows.zip release/SoftwareManager-${SHORT_SHA}-windows.zip
cp artifacts/windows-latest-build/SoftwareManager-*-windows-setup.exe release/SoftwareManager-${SHORT_SHA}-windows-setup.exe
cp artifacts/macos-latest-build/SoftwareManager-*-macos release/SoftwareManager-${SHORT_SHA}-macos
chmod +x release/SoftwareManager-*-linux release/SoftwareManager-*-macos
- name: Generate checksums
run: |
cd release
sha256sum * > SHA256SUMS.txt
- name: Create Release
uses: softprops/action-gh-release@v2
with:
name: ${{ needs.build.outputs.short_sha }}
generate_release_notes: true
files: |
release/SoftwareManager-${{ needs.build.outputs.short_sha }}-linux
release/SoftwareManager-${{ needs.build.outputs.short_sha }}-windows.zip
release/SoftwareManager-${{ needs.build.outputs.short_sha }}-windows-setup.exe
release/SoftwareManager-${{ needs.build.outputs.short_sha }}-macos
release/SHA256SUMS.txt
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+1
View File
@@ -0,0 +1 @@
/* /index.html 200
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+238
View File
@@ -0,0 +1,238 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="main.css"/>
<title>SoftwareManager - Main</title>
<!-- <link rel="icon" type="image/x-icon" href="/assets/icons/favicon.ico"> -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
<header>
<div class="logo-text">
<h1>KeksPirates</h1>
</div>
<nav class="tab">
<a class="tablinks" onclick="openTab(event, 'start')" id="defaultOpen">Home</a>
<a class="tablinks" onclick="openTab(event, 'api')">API</a>
<a class="tablinks" onclick="openTab(event, 'docs')">Docs</a>
<!-- <a class="tablinks" onclick="openTab(event, 'notfound')">404</a> -->
</nav>
</header>
<main>
<div id="start" class="tabcontent">
<div class="introduction-frame">
<div class="introduction-text inline-block">
<h1 class="hometitle"><br><span style="color: #db65d5">SoftwareManager</span></h1>
<p class="title_subtext">Simply search and download software from various sources</p>
<a href="https://github.com/KeksPirates/SoftwareManager" class="btn_big github"><img class="github-button" src="assets/icons/GitHub_Invertocat_White.png">GitHub</a>
<a href="https://github.com/KeksPirates/SoftwareManager/releases/latest" class="btn_big download"><img class="download-button" src="assets/icons/download_icon_black.png">Download</a>
</div>
<div class="introduction-image inline-block">
<div class="softwaremanager-scene">
<img class="softwaremanager-image" src="assets/softwaremanager/search_tab_rounded.png">
</div>
<img class="softwaremanager-image-2" src="assets/softwaremanager/search_tab_rounded.png">
</div>
</div>
</div>
<div id="api" class="tabcontent">
<h1 class="title"><span class="json-string">API Documentation</span> </h1>
<p>This API allows you to search for posts on rutracker.</p>
<div class="base-information-frame">
<h3>API URL</h3>
<p class="api-url">https://api.michijackson.xyz/</p>
<br>
<h3>Endpoint</h3>
<p class="endpoint-url"><span class="request-type">GET</span><span class="endpoint-type-container">/search?q=<span class="endpoint-parameter">{query}</span></span></p>
<div class="endpoint-parameters-container">
<div class="endpoint-parameters-header">
<p>Parameter</p>
<p>Type</p>
<p>Required</p>
<p>Description</p>
</div>
<div class="endpoint-parameters-content">
<p class="endpoint-parameter">q</p>
<p class="endpoint-type">String</p>
<p class="endpoint-required">True</p>
<p class="endpoint-description">Search query</p>
</div>
</div>
</div>
<div class="request-frame">
<h2>Example Request</h2>
<div class="request-type-container">
<p><span class="request-type">GET</span> <span class="request-type-subcontainer">api.michijackson.xyz/search?q=keks</span></p>
</div>
</div>
<div class="api-response-frame">
<h3>Response</h3>
<pre class="api-response"><span class="json-punctuation">{</span>
"cached"<span class="json-punctuation">:</span> <span class="json-boolean">false</span><span class="json-punctuation">,</span>
"count"<span class="json-punctuation">:</span> <span class="json-integer">1</span><span class="json-punctuation">,</span>
"data"<span class="json-punctuation">:</span> <span class="json-punctuation">[</span>
<span class="json-punctuation">{</span>
"author"<span class="json-punctuation">:</span> <span class="json-string">"Viterrson"</span><span class="json-punctuation">,</span>
"id"<span class="json-punctuation">:</span> <span class="json-integer">1</span><span class="json-punctuation">,</span>
"leechers"<span class="json-punctuation">:</span> <span class="json-integer">0</span><span class="json-punctuation">,</span>
"seeders"<span class="json-punctuation">:</span> <span class="json-integer">3</span><span class="json-punctuation">,</span>
"title"<span class="json-punctuation">:</span> <span class="json-string">"(Hard - Rock) Keks - Keks - 1983, MP3, 320 kbps"</span><span class="json-punctuation">,</span>
"url"<span class="json-punctuation">:</span> <span class="json-string">"https://rutracker.org/forum/viewtopic.php?t=3835559"</span>
<span class="json-punctuation">}</span>
<span class="json-punctuation">],</span>
"query"<span class="json-punctuation">:</span> <span class="json-string">"keks"</span><span class="json-punctuation">,</span>
"success"<span class="json-punctuation">:</span> <span class="json-boolean">true</span>
<span class="json-punctuation">}</span></pre>
</div>
</div>
<div id="docs" class="tabcontent">
<h1 class="title"><span style="color: #db65d5">Docs</span></h1>
<p>SoftwareManager is a desktop app to search, browse, and download software & games from multiple sources in one place.</p>
<div class="base-information-frame">
<h2>Features</h2>
<div class="endpoint-parameters-container docs-tips">
<p class="sources-text">• Searching & Downloading Software from various pages</p>
<p class="sources-text">• Network Interface Binding</p>
<p class="sources-text">• Simple UI, Speed Limiting etc.</p>
<p class="sources-text">• No Logins required</p>
</div>
</div>
<div class="base-information-frame">
<h2>General Information</h2>
<div class="endpoint-parameters-container docs-tips">
<p class="sources-text">• Rutracker is the default source since it is the largest index overall.</p>
<p class="sources-text">• Use m0nkrus if you want Adobe software, it directly gets the posts from his Telegram channel.</p>
<p class="sources-text">• Torrents start seeding after completion and start seeding on application start.</p>
<p class="sources-text">• MacOS Builds haven't been properly tested - We're thankful to receive feedback.</p>
<p class="sources-text">• If you want to host your own server, clone our <a href="https://github.com/KeksPirates/SoftwareManager-Server" style="color: #db65d5;">server repository</a>, enter your cookie for rutracker.org and run server.py. Detailed Instructions on how to get your rutracker cookie are in the README of the server repo.</p>
<p class="sources-text">• SoftwareManager is still in development and most likely contains issues. Please report any bugs you find via GitHub. Contributions are also greatly appreciated.</p>
</div>
</div>
<div class="base-information-frame">
<h2>Sources</h2>
<br>
<p>Use the dropdown menu in the top right to select your source.</p>
<div class="endpoint-parameters-container docs-grid">
<div class="endpoint-parameters-header">
<p>Source</p>
<p>Category</p>
<p>Notes</p>
</div>
<div class="endpoint-parameters-content">
<p class="sources-text">RuTracker</p>
<p class="sources-text">Software & games</p>
<p class="sources-text">Has an extremely large amount of Software. Relies on an API for searching.</p>
</div>
<div class="endpoint-parameters-content">
<p class="sources-text">UzTracker</p>
<p class="sources-text">Software & games</p>
<p class="sources-text">Similar to RuTracker, smaller. Use this as fallback if RuTracker is down or there's API issues.</p>
</div>
<div class="endpoint-parameters-content">
<p class="sources-text">m0nkrus</p>
<p class="sources-text">Adobe Software</p>
<p class="sources-text">Lots of curated releases. Focused on mainly professional Software.</p>
</div>
<div class="endpoint-parameters-content">
<p class="sources-text">SteamRip</p>
<p class="sources-text">Games</p>
<p class="sources-text">Currently very experimental due to download link retrieveal. May not always work as expected.</p>
</div>
</div>
</div>
</div>
<div id="notfound" class="tabcontent">
<div class="notfound-frame">
<h1 class="notfound-title">404</h1>
<br>
<h1 class="notfound-subtext">(╯°□°)╯︵ ┻━┻</h1>
<a class="btn_big download" onclick="openTab(event, 'start')" style="cursor: pointer;">Go Home</a>
</div>
</div>
</main>
<footer>
<small>KeksPirates - SoftwareManager</small>
</footer>
<script>
function openTab(evt, tabName, pushToHistory = true) {
var i, tabcontent, tablinks;
tabcontent = document.getElementsByClassName("tabcontent");
for (i = 0; i < tabcontent.length; i++) {
tabcontent[i].style.display = "none";
}
tablinks = document.getElementsByClassName("tablinks");
for (i = 0; i < tablinks.length; i++) {
tablinks[i].className = tablinks[i].className.replace(" active", "");
}
document.getElementById(tabName).style.display = "block";
let activeButton = document.querySelector(`.tablinks[onclick*="${tabName}"]`);
if (activeButton) {
activeButton.className += " active";
}
if (pushToHistory) {
let path = "/";
if (tabName === 'api') path = "/api";
if (tabName === 'docs') path = "/docs";
history.pushState({ tab: tabName }, "", path);
}
}
window.addEventListener('popstate', function(event) {
if (event.state && event.state.tab) {
openTab(null, event.state.tab, false);
} else {
openTab(null, 'start', false);
}
});
window.onload = function() {
let currentPath = window.location.pathname;
if (currentPath === '/api') {
openTab(null, 'api', false);
} else if (currentPath === '/docs') {
openTab(null, 'docs', false);
} else {
history.replaceState({ tab: 'start' }, "", "/");
openTab(null, 'start', false);
}
};
</script>
</body>
</html>
+476
View File
@@ -0,0 +1,476 @@
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--main-font: "Manrope", sans-serif;
}
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
display: flex;
flex-direction: column;
min-height: 100vh;
background: #0f1011;
}
header {
height: 65px;
background: #1a1c1d;
color: #fff;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.tab a {
display: inline-block;
padding: 0.4rem 0.8rem;
margin-left: 0.5rem;
text-decoration: none;
color: #fafafa;
border-radius: 8px;
box-shadow: 0 0 6px 1px #dd06cb00;
font-family: var(--main-font);
transition: background 0.5s, color 0.5s;
cursor: pointer;
}
nav a:hover {
background: #db65d517;
color: #ffd1fd;
}
nav a.active {
background: #db65d55d;
color: #fdd2fc;
}
.logo-text h1 {
font-family: var(--main-font);
font-size: 1.2rem;
font-weight: 700;
letter-spacing: -0.02em;
background-image: linear-gradient(147deg, #da84d5, #db65d5);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
main {
font-family: var(--main-font);
color: #fafafa;
padding: 3rem;
}
main h1.hometitle {
font-size: 4.0rem;
line-height: 4rem;
margin-top: 1.8rem;
}
main p.title_subtext {
margin-top: 0.5rem;
font-size: 1.2rem;
}
.softwaremanager-scene {
perspective: 700px;
width: fit-content;
}
main img.softwaremanager-image {
margin-top: 0rem;
user-select: none;
transform: rotateX(8deg) rotateY(-12deg) rotateZ(2deg) scale(0.85) translateX(-1rem);
box-shadow: 15px 20px 15px #0f0f0f;
/* transition: transform 0.4s ease-out; */
}
/* main img.softwaremanager-image:hover {
transform: rotateX(8deg) rotateY(-13deg) rotateZ(2deg) scale(0.86) translateX(-1rem);
} */
/* transform: rotateX(8443deg) rotateY(-14234233deg) rotateZ(2deg) scale(4.863) translateX(-133rem); lmao */
main .introduction-text {
position: relative;
text-align: right;
margin-right: -3rem;
margin-top: 3rem;
z-index: 2;
user-select: none;
}
.introduction-image {
position: relative;
z-index: 1;
}
.inline-block {
display: inline-block;
vertical-align: top;
}
main .introduction-frame {
text-align: center;
position: relative;
font-size: 0
}
main .introduction-frame > * {
font-size: 1rem;
}
main .btn_big {
font-family: var(--main-font);
background: #1a1c1d;
padding: 0.7rem 1.4rem;
margin-top: 1.5rem;
border: none;
border-radius: 10px;
align-items: center;
display: inline-flex;
color: #fafafa;
text-decoration: none;
font-weight: bold;
user-select: none;
}
main .btn_big img {
height: 20px;
width: auto;
margin-right: 0.5rem;
}
main .btn_big.github {
margin-right: 1rem;
transition: background 0.5s;
}
main .btn_big.github:hover {
background: #18191a;
}
main .btn_big.download {
background: #ca62c5;
color: #0f1011;
transition: background 0.5s;
}
main .btn_big.download:hover {
background: #b551b0;
}
main .introduction-text h1,
main .introduction-text p {
user-select: text;
}
.softwaremanager-image-2 {
display: none;
}
footer {
color: #585858;
font-family: var(--main-font);
align-self: center;
padding: 1rem;
position: fixed;
bottom: 0;
user-select: none;
}
::selection {
background: #db65d528;
}
.base-information-frame {
background: #1a1c1d;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.program-preview {
box-sizing: inherit;
background: #1a1c1d;
width: auto;
height: 20rem;
border-radius: 10px;
}
pre.api-response {
font-family: monospace;
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.api-response-frame {
background: #1a1c1d;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.request-type {
color: #fdd2fc;
padding: 1px 6px;
background: #db65d55d;
border-radius: 6px;
}
.request-type-container {
background: #1a1c1d;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.endpoint-type-container {
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.request-type-subcontainer {
background: #1a1c1d;
padding: 0.5rem;
border-radius: 10px;
margin-top: 1rem;
}
.api-url {
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
}
.endpoint-url {
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
color: #8f8d8d
}
.endpoint-parameters-container {
background: #151718;
padding: 1rem;
border-radius: 10px;
margin-top: 1rem;
color: #8f8d8d;
}
.endpoint-parameters-header {
margin-top: -0.3rem;
margin-bottom: 0.9rem;
font-size: 0.9rem;
grid-template-columns: 9rem 9rem 9rem 9rem;
display: grid;
}
.endpoint-parameters-content {
font-size: 1rem;
grid-template-columns: 9rem 9rem 9rem 9rem;
display: grid;
}
.request-frame {
margin-top: 1rem;
}
.endpoint-parameter { color: #ffab4a; }
.endpoint-type { color: #db65d5 }
.endpoint-required { color: #66ff80}
.endpoint-description { color: #b1b0b0 }
.json-punctuation { color: #5c5c5c; }
.json-string { color: #db65d5; }
.json-boolean { color: #ffab4a; }
.json-integer { color: #66ff80; }
.sources-text {
color: #ffffff;
font-size: 1rem;
}
.docs-grid .endpoint-parameters-header,
.docs-grid .endpoint-parameters-content {
grid-template-columns: 10rem 14rem 1fr;
}
.docs-grid .endpoint-parameters-header p {
color: #8f8d8d;
}
.docs-grid .endpoint-parameters-content {
padding: 0.5rem 0;
border-top: 1px solid #ffffff0a;
}
.docs-grid .endpoint-parameters-content:first-of-type {
border-top: none;
}
.docs-tips .sources-text {
padding: 0.25rem 0;
}
.base-information-frame h2 {
color: #db65d5;
}
.notfound-frame {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 75vh;
text-align: center;
gap: 10px;
}
.notfound-title {
font-size: 4.0rem;
line-height: 4rem;
margin-top: 1.8rem;
background-image: linear-gradient(135deg, #c574c1, #b851b2);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
}
.notfound-subtext {
font-size: 2rem;
}
@media(max-width: 560px) {
header {
padding: 1rem;
}
main {
margin: 0rem;
padding: 1rem;
}
main .introduction {
display: block;
margin-top: -1rem;
padding: 1rem;
}
main .introduction-text {
position: relative;
text-align: center;
margin-top: 0rem;
z-index: 2;
user-select: none;
margin-right: 0rem;
padding-bottom: 2rem;
}
main h1.hometitle {
font-size: 2rem;
line-height: 2.2rem;
text-align: left;
}
main p.title_subtext {
text-align: left;
}
.introduction-image {
display: none;
}
main .btn_big {
display: block;
margin: 1rem auto;
}
main .btn_big.github{
margin-right: 0rem;
}
}
@media (min-width: 561px) and (max-width: 1393px) {
header {
padding: 1rem;
}
main {
margin: 0rem;
padding: 1rem;
}
main .introduction {
display: flex;
margin-top: -1rem;
padding: 1rem;
flex-direction: column;
align-items: center;
}
main .introduction-text {
position: relative;
text-align: center;
margin-top: 0rem;
z-index: 2;
user-select: none;
margin-left: 2rem;
margin-right: 2rem;
padding-bottom: 2rem;
}
main h1.hometitle {
font-size: 2rem;
line-height: 2.2rem;
text-align: center;
}
main p.title_subtext {
text-align: center;
}
.softwaremanager-image {
display: none;
}
.softwaremanager-image-2 {
display: block;
width: 100%;
height: auto;
margin-left: auto;
margin-right: auto;
}
.introduction-image {
width: 100%;
max-width: 50rem;
user-select: none;
}
main .introduction .inline-block {
display: block;
}
}
+9
View File
@@ -66,6 +66,15 @@ Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Code]
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
begin
Exec('taskkill', '/F /IM {#MyAppExeName}', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Result := '';
end;
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
Filename: "{app}\{#MyAppExeName}"; Flags: nowait skipifnotsilent
+172 -14
View File
@@ -1,29 +1,187 @@
from PySide6.QtWidgets import QLabel, QGraphicsOpacityEffect, QStackedWidget
from PySide6.QtCore import Qt, QSize, QEvent, QObject
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import QLabel
from utils.data.state import state
from PySide6.QtCore import Qt
import darkdetect
import os
class Image():
class Image(QObject):
def __init__(self, parent):
super().__init__(parent)
self.application = parent
self.overlay_label = QLabel(parent)
self.overlay_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
self.opacity_effect = QGraphicsOpacityEffect()
self.overlay_label.setGraphicsEffect(self.opacity_effect)
if state.image_path is not None and os.path.exists(state.image_path):
self.image = QImage(state.image_path)
self.image = self.image.scaledToWidth(300, Qt.TransformationMode.SmoothTransformation)
self._current_image_path = None
self._wallpaper_active = False
self._original_stylesheets = {}
parent.installEventFilter(self)
state.image_changed.connect(self.update_image_overlay)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.setPixmap(self.pixmap)
if state.image_path and os.path.exists(state.image_path):
self._load_and_display(state.image_path)
def eventFilter(self, obj, event):
if obj == self.application and event.type() == QEvent.Type.Resize:
if self._current_image_path:
self._load_and_display(self._current_image_path)
return False
def _set_wallpaper_transparency(self, enabled):
if self._wallpaper_active == enabled:
return
self._wallpaper_active = enabled
parent = self.application
central = parent.centralWidget()
if central:
central.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
for attr in ['tab_wrapper', 'corner_widget', 'titlebar']:
w = getattr(parent, attr, None)
if w:
w.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
if hasattr(parent, 'tabs'):
tabs = parent.tabs
tabs.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
for i in range(tabs.count()):
page = tabs.widget(i)
if page:
page.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
stack = tabs.findChild(QStackedWidget)
if stack:
stack.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
tab_bar = tabs.tabBar()
if tab_bar:
tab_bar.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
if 'tabs' not in self._original_stylesheets:
self._original_stylesheets['tabs'] = tabs.styleSheet()
if enabled:
tabs.setStyleSheet(
self._original_stylesheets['tabs']
+ "\nQTabWidget::pane { background: transparent; }"
+ "\nQTabBar { background: transparent; }"
+ "\nQTabBar::tab { background: transparent; }"
)
else:
tabs.setStyleSheet(self._original_stylesheets['tabs'])
# Searchbar
if hasattr(parent, 'searchbar'):
if 'searchbar' not in self._original_stylesheets:
self._original_stylesheets['searchbar'] = parent.searchbar.styleSheet()
if enabled:
parent.searchbar.setStyleSheet("QLineEdit { background-color: transparent; }")
else:
parent.searchbar.setStyleSheet(self._original_stylesheets['searchbar'])
# Download button
if hasattr(parent, 'dlbutton'):
if 'dlbutton' not in self._original_stylesheets:
self._original_stylesheets['dlbutton'] = parent.dlbutton.styleSheet()
if enabled:
parent.dlbutton.setStyleSheet("QPushButton { background-color: transparent; }")
else:
parent.dlbutton.setStyleSheet(self._original_stylesheets['dlbutton'])
# Labels
for attr in ['emptyResults', 'emptyDownload']:
w = getattr(parent, attr, None)
if w:
w.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, enabled)
if hasattr(parent, 'tracker_list'):
if 'tracker_list' not in self._original_stylesheets:
self._original_stylesheets['tracker_list'] = parent.tracker_list.styleSheet()
if enabled:
popup_bg = '#1e1e1e' if darkdetect.isDark() else '#ffffff'
parent.tracker_list.setStyleSheet(
"QComboBox { background-color: transparent; }"
f" QComboBox QAbstractItemView {{ background-color: {popup_bg}; }}"
)
else:
parent.tracker_list.setStyleSheet(self._original_stylesheets['tracker_list'])
def _load_and_display(self, image_path):
if state.image_enabled is not True:
self.overlay_label.hide()
self._set_wallpaper_transparency(False)
return
self._current_image_path = image_path
parent = self.application
image = QImage(image_path)
if image.isNull():
self.overlay_label.hide()
return
if getattr(state, "image_as_wallpaper", False):
scaled_image = image.scaled(
parent.size(),
Qt.AspectRatioMode.KeepAspectRatioByExpanding,
Qt.TransformationMode.SmoothTransformation
)
crop_x = (scaled_image.width() - parent.width()) // 2
crop_y = (scaled_image.height() - parent.height()) // 2
scaled_image = scaled_image.copy(crop_x, crop_y, parent.width(), parent.height())
pixmap = QPixmap.fromImage(scaled_image)
self.overlay_label.setPixmap(pixmap)
self.overlay_label.setGeometry(0, 0, parent.width(), parent.height())
self.overlay_label.lower()
self.opacity_effect.setOpacity(state.image_opacity / 100)
self._set_wallpaper_transparency(True)
else:
self._set_wallpaper_transparency(False)
scaled_image = image.scaledToWidth(
state.image_width, Qt.TransformationMode.SmoothTransformation
)
max_width = parent.width()
max_height = parent.height()
if scaled_image.width() > max_width or scaled_image.height() > max_height:
scaled_image = scaled_image.scaled(
QSize(max_width, max_height),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
pixmap = QPixmap.fromImage(scaled_image)
self.overlay_label.setPixmap(pixmap)
self.overlay_label.adjustSize()
self.overlay_label.raise_()
x = parent.width() - self.overlay_label.width()
y = parent.height() - self.overlay_label.height()
pos = state.image_position
off = int(state.image_offset)
w = self.overlay_label.width()
h = self.overlay_label.height()
if pos == "top-left":
x, y = off, off
elif pos == "top-right":
x, y = parent.width() - w - off, off
elif pos == "bottom-left":
x, y = off, parent.height() - h - off
elif pos == "center":
x, y = (parent.width() - w) // 2, (parent.height() - h) // 2
else: # bottom-right (default)
x, y = parent.width() - w - off, parent.height() - h - off
self.opacity_effect.setOpacity(state.image_opacity / 100)
self.overlay_label.move(x, y)
self.overlay_label.show()
def update_image_overlay(self, new_image_path):
self.image = QImage(new_image_path)
self.pixmap = QPixmap.fromImage(self.image)
self.overlay_label.setPixmap(self.pixmap)
self.overlay_label.adjustSize()
self._load_and_display(new_image_path)
@@ -0,0 +1,17 @@
from PySide6.QtWidgets import QMessageBox
import qdarktheme
class NotificationPopup(QMessageBox):
def __init__(self, title, text, infotext=None, parent=None):
super().__init__(parent)
qdarktheme.setup_theme("auto")
self.setIcon(QMessageBox.Icon.Information)
self.setWindowTitle(title)
self.setText(text)
if infotext is not None:
self.setInformativeText(infotext)
self.setStandardButtons(QMessageBox.StandardButton.Ok)
+289 -233
View File
@@ -13,8 +13,7 @@ from PySide6.QtWidgets import (
QDialog,
QLabel,
QHBoxLayout,
QSpinBox,
QCheckBox,
QSpinBox, QSlider, QCheckBox,
QFileDialog,
QComboBox,
)
@@ -25,248 +24,305 @@ SVG_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path
def settings_dialog(self):
temp_image_path = state.image_path
temp_image_width = state.image_width
temp_image_offset = state.image_offset
temp_image_opacity = state.image_opacity
temp_image_enabled = state.image_enabled
temp_image_as_wallpaper = state.image_as_wallpaper
temp_image_position = state.image_position
consoleLog("Settings dialog opened")
dialog = QDialog(self)
dialog.setWindowTitle("Settings")
dialog.setFixedSize(700, 450)
dialog_layout = QVBoxLayout()
dialog.setLayout(dialog_layout)
if state.window_transparency and platform.system() != "Windows" and dialog:
dialog.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
def close_settings():
dialog.reject()
update_checkbox_container = QWidget()
update_checkbox_layout = QHBoxLayout()
# Ignore updates checkbox
if platform.system() == "Windows":
update_checkbox = QCheckBox()
update_checkbox_container.setLayout(update_checkbox_layout)
update_checkbox_layout.addWidget(QLabel("Ignore Updates: "))
update_checkbox_layout.addStretch()
update_checkbox.setChecked(state.ignore_updates)
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
update_checkbox_layout.addWidget(update_checkbox)
# Auto-resume downloads checkbox
autoresume_container = QWidget()
autoresume_layout = QHBoxLayout()
autoresume_checkbox = QCheckBox()
autoresume_container.setLayout(autoresume_layout)
autoresume_layout.addWidget(QLabel("Auto-Resume Downloads: "))
autoresume_layout.addStretch()
autoresume_checkbox.setChecked(state.autoresume)
autoresume_layout.addWidget(autoresume_checkbox)
# Transparent window checkbox
transparent_window_container = QWidget()
transparent_window_layout = QHBoxLayout()
transparent_window_checkbox = QCheckBox()
transparent_window_container.setLayout(transparent_window_layout)
transparent_window_layout.addWidget(QLabel("Window Transparency (requires restart) (Linux/MacOS only): "))
transparent_window_layout.addStretch()
transparent_window_checkbox.setChecked(state.window_transparency)
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
transparent_window_layout.addWidget(transparent_window_checkbox)
# API Server Setting
api_url_container = QWidget()
api_url_layout = QHBoxLayout()
api_url = QLineEdit()
api_url_layout.addWidget(QLabel("API Server URL:"))
api_url_layout.addWidget(api_url)
api_url_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
api_url_container.setLayout(api_url_layout)
api_url.setText(state.api_url)
# Download Path
download_path_container = QWidget()
download_path_layout = QHBoxLayout()
download_path = QLineEdit()
download_path_layout.addWidget(QLabel("Download Path:"))
download_path_layout.addWidget(download_path)
download_path_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
download_path_container.setLayout(download_path_layout)
download_path.setText(state.download_path)
def browse_download_path():
dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path)
if dir_path:
download_path.setText(dir_path)
browse_button = QPushButton()
browse_button.setFixedSize(36, 36)
browse_button.setIconSize(QSize(24, 24))
browse_button.setIcon(svg_icon(SVG_FOLDER, 24))
browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
browse_button.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
download_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_download_path)
# Image Path
image_path_container = QWidget()
image_path_layout = QHBoxLayout()
image_path = QLineEdit()
image_path_layout.addWidget(QLabel("Image Path (requires restart, experimental):"))
image_path_layout.addWidget(image_path)
image_path_container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
image_path_container.setLayout(image_path_layout)
image_path.setText(state.image_path)
def browse_image_path():
file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0]
if file_path:
image_path.setText(file_path)
browse_button = QPushButton()
browse_button.setFixedSize(36, 36)
browse_button.setIconSize(QSize(24, 24))
browse_button.setIcon(svg_icon(SVG_FOLDER, 24))
browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
browse_button.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
image_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_image_path)
# Speed Limiting
down_speed_limit_container = QWidget()
down_speed_limit_layout = QHBoxLayout()
down_speed_limit_layout.addWidget(QLabel("Max Download Speed (KiB, 0 for unlimited): "))
down_speed_limit = QSpinBox()
down_speed_limit.setMinimum(0)
down_speed_limit.setMaximum(10000000)
down_speed_limit.setValue(state.down_speed_limit)
down_speed_limit_container.setLayout(down_speed_limit_layout)
down_speed_limit_layout.addWidget(down_speed_limit)
down_speed_limit.setFixedWidth(180)
down_speed_limit.setFixedHeight(30)
up_speed_limit_container = QWidget()
up_speed_limit_layout = QHBoxLayout()
up_speed_limit_layout.addWidget(QLabel("Max Upload Speed (KiB, 0 for unlimited): "))
up_speed_limit = QSpinBox()
up_speed_limit.setMinimum(0)
up_speed_limit.setMaximum(10000000)
up_speed_limit.setValue(state.up_speed_limit)
up_speed_limit_container.setLayout(up_speed_limit_layout)
up_speed_limit_layout.addWidget(up_speed_limit)
up_speed_limit.setFixedWidth(180)
up_speed_limit.setFixedHeight(30)
# Connection Configs
max_connections_container = QWidget()
max_connections_layout = QHBoxLayout()
max_connections_layout.addWidget(QLabel("Max Connections: "))
max_connections = QSpinBox()
max_connections.setMinimum(0)
max_connections.setMaximum(10000000)
max_connections.setValue(state.max_connections)
max_connections_container.setLayout(max_connections_layout)
max_connections_layout.addWidget(max_connections)
max_connections.setFixedWidth(180)
max_connections.setFixedHeight(30)
# Download Configs
max_downloads_container = QWidget()
max_downloads_layout = QHBoxLayout()
max_downloads_layout.addWidget(QLabel("Max Downloads: "))
max_downloads = QSpinBox()
max_downloads.setMinimum(0)
max_downloads.setMaximum(10000000)
max_downloads.setValue(state.max_downloads)
max_downloads_container.setLayout(max_downloads_layout)
max_downloads_layout.addWidget(max_downloads)
max_downloads.setFixedWidth(180)
max_downloads.setFixedHeight(30)
# Interface Binding
interface_container = QWidget()
interface_layout = QHBoxLayout()
interface_label = QLabel("Network Interface:")
interface_layout.addWidget(interface_label)
interface_select = QComboBox()
interface_select.addItems(["None"] + state.interfaces)
target = state.bound_interface if state.bound_interface else "None"
index = interface_select.findText(target)
if index >= 0:
interface_select.setCurrentIndex(index)
else:
interface_select.setCurrentIndex(0)
interface_select.setFixedWidth(180)
interface_select.setFixedHeight(30)
interface_layout.addWidget(interface_select)
interface_container.setLayout(interface_layout)
# Save / Cancel buttons
def create_widget(widget_type, label_text, **kwargs):
container = QWidget()
layout = QHBoxLayout()
container.setLayout(layout)
layout.addWidget(QLabel(label_text))
save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel")
save_btn.clicked.connect(lambda: save_settings(
close_settings,
widget = widget_type()
if widget_type in (QSpinBox,):
layout.addWidget(widget)
widget.setMinimum(kwargs.get("minimum", 0))
widget.setMaximum(kwargs.get("maximum", 10000000))
widget.setFixedWidth(kwargs.get("width", 180))
widget.setFixedHeight(kwargs.get("height", 30))
elif widget_type in (QCheckBox,):
layout.addStretch()
layout.addWidget(widget)
elif widget_type in (QLineEdit,):
layout.addWidget(widget)
container.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
if "width" in kwargs:
widget.setFixedWidth(kwargs["width"])
if "height" in kwargs:
widget.setFixedHeight(kwargs["height"])
elif widget_type in (QComboBox,):
layout.addWidget(widget)
widget.setFixedWidth(kwargs.get("width", 180))
widget.setFixedHeight(kwargs.get("height", 30))
return container, widget
consoleLog("Settings dialog opened")
dialog = QDialog(self)
dialog.setWindowTitle("Settings")
dialog.setFixedSize(580, 400)
dialog_layout = QVBoxLayout()
dialog.setLayout(dialog_layout)
if state.window_transparency and platform.system() != "Windows" and dialog:
dialog.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# Ignore Updates checkbox
update_checkbox_container, update_checkbox = create_widget(QCheckBox, "Ignore Updates: ")
if platform.system() == "Windows":
update_checkbox.setChecked(state.ignore_updates)
update_checkbox.toggled.connect(lambda checked: setattr(state, 'ignore_updates', checked))
# Autoresume Container Checkbox
autoresume_container, autoresume_checkbox = create_widget(QCheckBox, "Auto-Resume Downloads: ")
autoresume_checkbox.setChecked(state.autoresume)
autoresume_checkbox.toggled.connect(lambda checked: setattr(state, 'autoresume', checked))
# Transparent Window Checkbox
transparent_window_container, transparent_window_checkbox = create_widget(QCheckBox, "Window Transparency (requires restart) (Linux/MacOS only): ")
transparent_window_checkbox.setChecked(state.window_transparency)
transparent_window_checkbox.toggled.connect(lambda checked: setattr(state, 'window_transparency', checked))
# Accent Color
accent_color_container, accent_color_input = create_widget(QLineEdit, "Accent Color (requires restart): ", width=180, height=30)
accent_color_input.setPlaceholderText("e.g. #fca7d7")
accent_color_input.setText(state.accent_color)
# API URL Widget
api_url_container, api_url = create_widget(QLineEdit, "API Server URL: ", width=180, height=30)
api_url.setText(state.api_url)
# Download Path Widget
download_path_container, download_path = create_widget(QLineEdit, "Download Path: ")
download_path.setText(state.download_path)
download_path_layout = download_path_container.layout()
def browse_download_path():
dir_path = QFileDialog.getExistingDirectory(dialog, "Select Download Directory", state.download_path)
if dir_path:
download_path.setText(dir_path)
browse_button = create_widget(QPushButton, "", width=36, height=36)[1]
browse_button.setIconSize(QSize(24, 24))
browse_button.setIcon(svg_icon(SVG_FOLDER, 24))
browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
browse_button.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
download_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_download_path)
# Image Path
image_path_container, image_path = create_widget(QLineEdit, "Image Path: ")
image_path.setText(state.image_path)
image_path_layout = image_path_container.layout()
image_path.textChanged.connect(lambda text: setattr(state, 'image_path', text))
def browse_image_path():
file_path = QFileDialog.getOpenFileName(dialog, "Select Image File", state.image_path, "Image Files (*.png *.jpg)")[0]
if file_path:
image_path.setText(file_path)
browse_button = create_widget(QPushButton, "", width=36, height=36)[1]
browse_button.setIconSize(QSize(24, 24))
browse_button.setIcon(svg_icon(SVG_FOLDER, 24))
browse_button.setCursor(Qt.CursorShape.PointingHandCursor)
browse_button.setStyleSheet("""
QPushButton {
border: none;
background: transparent;
padding: 0px;
}
""")
image_path_layout.addWidget(browse_button)
browse_button.clicked.connect(browse_image_path)
# Enable Image Checkbox
enable_image_container, enable_image_checkbox = create_widget(QCheckBox, "Enable Image (requires image path): ", width=180, height=30)
enable_image_checkbox.setChecked(state.image_enabled)
enable_image_checkbox.toggled.connect(lambda checked: setattr(state, 'image_enabled', checked))
# Image Mode Checkbox
image_mode_container, image_mode_checkbox = create_widget(QCheckBox, "Wallpaper Mode: ")
image_mode_checkbox.setChecked(state.image_as_wallpaper)
image_mode_checkbox.toggled.connect(lambda checked: setattr(state, 'image_as_wallpaper', checked))
# Image Position Preset
positions = ["bottom-right", "bottom-left", "top-right", "top-left", "center"]
image_position_container, image_position_combo = create_widget(QComboBox, "Position: ", width=180, height=30)
image_position_combo.addItems(positions)
idx = image_position_combo.findText(state.image_position)
image_position_combo.setCurrentIndex(idx if idx >= 0 else 0)
image_position_combo.currentTextChanged.connect(lambda val: setattr(state, 'image_position', val))
_slider_style = """
QSlider::groove:horizontal {
height: 3px;
background: palette(mid);
border-radius: 1px;
}
QSlider::handle:horizontal {
width: 10px;
height: 10px;
margin: -4px 0;
border-radius: 5px;
}
"""
# Image Width Slider
image_width_container = QWidget()
image_width_layout = QHBoxLayout(image_width_container)
image_width_layout.addWidget(QLabel("Image Width: "))
image_width = QSlider(Qt.Orientation.Horizontal)
image_width.setFixedWidth(180)
image_width.setStyleSheet(_slider_style)
image_width.setMinimum(0)
image_width.setMaximum(2500)
image_width.setValue(state.image_width)
image_width_val = QLabel(str(state.image_width))
image_width_val.setFixedWidth(36)
image_width.valueChanged.connect(lambda val: (setattr(state, 'image_width', val), image_width_val.setText(str(val))))
image_width_layout.addWidget(image_width_val)
image_width_layout.addWidget(image_width)
# Image Offset Slider
image_offset_container = QWidget()
image_offset_layout = QHBoxLayout(image_offset_container)
image_offset_layout.addWidget(QLabel("Corner Offset: "))
image_offset = QSlider(Qt.Orientation.Horizontal)
image_offset.setFixedWidth(180)
image_offset.setStyleSheet(_slider_style)
image_offset.setMinimum(0)
image_offset.setMaximum(500)
image_offset.setValue(state.image_offset)
image_offset_val = QLabel(str(state.image_offset))
image_offset_val.setFixedWidth(36)
image_offset.valueChanged.connect(lambda val: (setattr(state, 'image_offset', val), image_offset_val.setText(str(val))))
image_offset_layout.addWidget(image_offset_val)
image_offset_layout.addWidget(image_offset)
# Image Opacity Slider
image_opacity_container = QWidget()
image_opacity_layout = QHBoxLayout(image_opacity_container)
image_opacity_layout.addWidget(QLabel("Image Opacity: "))
image_opacity = QSlider(Qt.Orientation.Horizontal)
image_opacity.setFixedWidth(180)
image_opacity.setStyleSheet(_slider_style)
image_opacity.setMinimum(0)
image_opacity.setMaximum(100)
image_opacity.setValue(state.image_opacity)
image_opacity_val = QLabel(str(state.image_opacity))
image_opacity_val.setFixedWidth(36)
image_opacity.valueChanged.connect(lambda val: (setattr(state, 'image_opacity', val), image_opacity_val.setText(str(val))))
image_opacity_layout.addWidget(image_opacity_val)
image_opacity_layout.addWidget(image_opacity)
# Speed Limiting
down_speed_limit_container, down_speed_limit = create_widget(QSpinBox, "Max Download Speed (KiB, 0 for unlimited): ", width=180, height=30)
down_speed_limit.setMinimum(0)
down_speed_limit.setValue(state.down_speed_limit)
up_speed_limit_container, up_speed_limit = create_widget(QSpinBox, "Max Upload Speed (KiB, 0 for unlimited): ", width=180, height=30)
up_speed_limit.setMinimum(0)
up_speed_limit.setValue(state.up_speed_limit)
# Connection Configs
max_connections_container, max_connections = create_widget(QSpinBox, "Max Connections: ", width=180, height=30)
max_connections.setMinimum(0)
max_connections.setValue(state.max_connections)
# Download Configs
max_downloads_container, max_downloads = create_widget(QSpinBox, "Max Downloads: ", width=180, height=30)
max_downloads.setMinimum(0)
max_downloads.setValue(state.max_downloads)
# Interface Binding
interface_container = QWidget()
interface_layout = QHBoxLayout()
interface_label = QLabel("Network Interface:")
interface_layout.addWidget(interface_label)
interface_select = QComboBox()
interface_select.addItems(["None"] + state.interfaces)
target = state.bound_interface if state.bound_interface else "None"
index = interface_select.findText(target)
if index >= 0:
interface_select.setCurrentIndex(index)
else:
interface_select.setCurrentIndex(0)
interface_select.setFixedWidth(180)
interface_select.setFixedHeight(30)
interface_layout.addWidget(interface_select)
interface_container.setLayout(interface_layout)
# Save / Cancel buttons
layout = QHBoxLayout()
save_btn = QPushButton("Save")
cancel_btn = QPushButton("Cancel")
save_btn.clicked.connect(lambda: handle_save())
def handle_save():
save_settings(
dialog.accept,
api_url.text(),
download_path.text(),
down_speed_limit.value(),
down_speed_limit.value(),
up_speed_limit.value(),
image_path.text(),
autoresume_checkbox.isChecked(),
max_connections.value(),
max_downloads.value(),
interface_select.currentText()))
interface_select.currentText(),
image_width.value(),
image_offset.value(),
image_opacity.value(),
image_as_wallpaper=image_mode_checkbox.isChecked(),
image_position=image_position_combo.currentText(),
accent_color=accent_color_input.text().strip()
)
cancel_btn.clicked.connect(dialog.reject)
layout.addWidget(cancel_btn)
layout.addWidget(save_btn)
cancel_btn.clicked.connect(dialog.reject)
layout.addWidget(cancel_btn)
layout.addWidget(save_btn)
tabs = QtWidgets.QTabWidget()
create_tab("General", [autoresume_container, update_checkbox_container, transparent_window_container], tabs=tabs, stretch=True)
create_tab("Paths", [download_path_container, image_path_container], tabs=tabs, stretch=True)
create_tab("Network", [interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container], tabs=tabs, stretch=True)
tabs = QtWidgets.QTabWidget()
create_tab("General", [autoresume_container, update_checkbox_container, transparent_window_container, accent_color_container], tabs=tabs, stretch=True)
create_tab("Image", [enable_image_container, image_mode_container, image_position_container, image_width_container, image_offset_container, image_opacity_container], tabs=tabs, stretch=True)
create_tab("Paths", [download_path_container, image_path_container], tabs=tabs, stretch=True)
create_tab("Network", [interface_container, max_connections_container, max_downloads_container, up_speed_limit_container, down_speed_limit_container, api_url_container], tabs=tabs, stretch=True)
dialog_layout.addWidget(tabs)
dialog_layout.addLayout(layout)
dialog.exec()
dialog_layout.addWidget(tabs)
dialog_layout.addLayout(layout)
def on_dialog_finished(result): # Undo Image changes if "Save" button is not pressed
if result == QtWidgets.QDialog.DialogCode.Rejected:
state.image_path = temp_image_path
state.image_width = temp_image_width
state.image_offset = temp_image_offset
state.image_opacity = temp_image_opacity
state.image_enabled = temp_image_enabled
state.image_as_wallpaper = temp_image_as_wallpaper
state.image_position = temp_image_position
dialog.finished.connect(on_dialog_finished)
dialog.exec()
+20 -1
View File
@@ -1,4 +1,6 @@
from PySide6.QtGui import QColor
from utils.logging.logs import consoleLog
from utils.data.state import state
import darkdetect
def _is_dark_mode():
@@ -27,15 +29,32 @@ def _theme_colors():
}
def _accent_selection_color(alpha: float = 0.35) -> str:
if not state.accent_color:
return None
try:
hex_color = state.accent_color.lstrip('#')
if len(hex_color) != 6:
return None
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
return f"rgba({r}, {g}, {b}, {alpha})"
except ValueError:
consoleLog(f"Invalid Color: {state.accent_color}")
return None
def _table_stylesheet(view_type="QTableWidget"):
c = _theme_colors()
dark = _is_dark_mode()
color_rule = "" if dark else f"color: {c['text']};"
selected_bg = _accent_selection_color() or c["selected"]
selection_color_rule = f"selection-background-color: {selected_bg};" if state.accent_color else ""
return f"""
{view_type} {{
border: none;
outline: 0;
font-size: 13px;
{selection_color_rule}
{color_rule}
}}
{view_type}::item {{
@@ -45,7 +64,7 @@ def _table_stylesheet(view_type="QTableWidget"):
{color_rule}
}}
{view_type}::item:selected {{
background: {c["selected"]};
background: {selected_bg};
outline: none;
border: none;
border-bottom: 1px solid {c["border"]};
+5
View File
@@ -6,6 +6,7 @@ from interface.assets.base64_icons import settings_white_base64
from interface.assets.base64_icons import settings_black_base64
from interface.dialogs.downloadlist import download_list_update
from interface.dialogs.update import get_version, UpdateDialog
from interface.dialogs.theme import _accent_selection_color
from utils.logging.logs import consoleLog, flush_log_buffer
from interface.dialogs.downloadmodel import DownloadModel
from interface.dialogs.settings import settings_dialog
@@ -177,6 +178,10 @@ class MainWindow(QtWidgets.QMainWindow, QWidget):
self.tracker_list.addItems(list(state.trackers.keys()))
self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor)
self.tracker_list.activated.connect(self.set_tracker)
if state.accent_color:
self.tracker_list.setStyleSheet(
f"QComboBox QAbstractItemView::item:selected {{ background: {_accent_selection_color()}; }}"
)
self.corner_layout.addWidget(self.tracker_list)
# Settings button
+13 -6
View File
@@ -20,15 +20,18 @@ import signal
import time
import sys
def run_gui():
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
qdarktheme.setup_theme("auto")
def run_gui(app):
custom_colors = {}
if state.accent_color:
custom_colors["primary"] = state.accent_color
qdarktheme.setup_theme("auto", custom_colors=custom_colors if custom_colors else None)
widget = MainWindow()
# Check OS for window transparency compatibility and apply
if state.window_transparency and platform.system() != "Windows":
widget.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
qdarktheme.setup_theme("auto", custom_colors={"background": "#00000000"})
transparent_colors = {"background": "#00000000", **custom_colors}
qdarktheme.setup_theme("auto", custom_colors=transparent_colors)
set_main_window(widget)
widget.show()
@@ -42,7 +45,11 @@ def keyboardinterrupthandler(signum, frame):
def main():
# Begin counting startup time
start_time = time.perf_counter()
# Parse saved files
# Initialize UI Engine
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
# Parse saved files
read_config()
logs = get_download_logs()
_, downloads = split_data(logs)
@@ -65,7 +72,7 @@ def main():
elapsed = time.perf_counter() - start_time
consoleLog(f"Initialization completed in {elapsed:.2f}s. Launching GUI")
# Launch GUI
run_gui()
run_gui(app)
if __name__ == "__main__":
main()
+106 -36
View File
@@ -1,6 +1,9 @@
from interface.dialogs.notificationpopup import NotificationPopup
from utils.logging.logs import consoleLog
from utils.data.state import state
import configparser
import platform
import time
import os
@@ -8,24 +11,34 @@ def create_config():
config = configparser.ConfigParser()
config["General"] = {
"debug": True,
"ignore_updates": f"{state.ignore_updates}",
"autoresume": f"{state.autoresume}",
"window_transparency": f"{state.window_transparency}"
"debug": str(state.debug),
"ignore_updates": str(state.ignore_updates),
"autoresume": str(state.autoresume),
"window_transparency": str(state.window_transparency),
"accent_color": str(state.accent_color)
}
config["Network"] = {
"api_url": f"{state.api_url}",
"bound_interface": f"{state.bound_interface}" if state.bound_interface is not None else "None",
"download_speed_limit": f"{state.down_speed_limit}",
"upload_speed_limit": f"{state.up_speed_limit}",
"max_connections": f"{state.max_connections}",
"max_downloads": f"{state.max_downloads}"
"api_url": str(state.api_url),
"bound_interface": str(state.bound_interface) if state.bound_interface is not None else "None",
"download_speed_limit": str(state.down_speed_limit),
"upload_speed_limit": str(state.up_speed_limit),
"max_connections": str(state.max_connections),
"max_downloads": str(state.max_downloads)
}
config["Paths"] = {
"download_path": f"{state.download_path}",
"image_path": f"{state.image_path}"
"download_path": str(state.download_path),
"image_path": str(state.image_path)
}
config["Image"] = {
"enable_image": str(state.image_enabled),
"image_width": str(state.image_width),
"image_offset": str(state.image_offset),
"image_opacity": str(state.image_opacity),
"image_as_wallpaper": str(state.image_as_wallpaper),
"image_position": str(state.image_position)
}
if platform.system() == "Windows":
@@ -36,45 +49,102 @@ def create_config():
state.settings_path = os.path.join(config_dir, "SoftwareManager")
os.makedirs(state.settings_path, exist_ok=True)
with open(os.path.join(state.settings_path, "config.yml"), 'w') as cf:
with open(os.path.join(state.settings_path, "config.ini"), 'w') as cf:
config.write(cf)
def read_config():
config = configparser.ConfigParser()
if platform.system() == "Windows":
config_dir = os.environ.get("APPDATA", os.path.expanduser("~\\AppData\\Roaming"))
else:
config_dir = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
state.settings_path = os.path.join(config_dir, "SoftwareManager")
config_file = os.path.join(state.settings_path, "config.yml")
# Temporary migration logic, keep for a while
old_config_file = os.path.join(state.settings_path, "config.yml")
new_config_file = os.path.join(state.settings_path, "config.ini")
if not os.path.exists(config_file):
if os.path.exists(old_config_file):
try:
os.replace(old_config_file, new_config_file)
consoleLog("Successfully migrated config.yml to config.ini")
except Exception as e:
consoleLog(f"Failed to migrate config file: {e}")
if not os.path.exists(new_config_file):
create_config()
return
try:
config.read(new_config_file)
# General
state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
state.accent_color = config.get("General", "accent_color", fallback=state.accent_color)
config.read(config_file)
# General
state.debug = config.getboolean("General", "debug", fallback=state.debug)
state.ignore_updates = config.getboolean("General", "ignore_updates", fallback=state.ignore_updates)
state.autoresume = config.getboolean("General", "autoresume", fallback=state.autoresume)
state.window_transparency = config.getboolean("General", "window_transparency", fallback=state.window_transparency)
# Network
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
if state.bound_interface == "None":
state.bound_interface = None
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
# Network
state.api_url = config.get("Network", "api_url", fallback=state.api_url)
state.bound_interface = config.get("Network", "bound_interface", fallback=state.bound_interface)
if state.bound_interface == "None":
state.bound_interface = None
state.down_speed_limit = config.getint("Network", "download_speed_limit", fallback=state.down_speed_limit)
state.up_speed_limit = config.getint("Network", "upload_speed_limit", fallback=state.up_speed_limit)
state.max_connections = config.getint("Network", "max_connections", fallback=state.max_connections)
state.max_downloads = config.getint("Network", "max_downloads", fallback=state.max_downloads)
# Paths
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
# Paths
state.download_path = config.get("Paths", "download_path", fallback=state.download_path)
state.image_path = config.get("Paths", "image_path", fallback=state.image_path)
# Image
state.image_enabled = config.getboolean("Image", "enable_image", fallback=state.image_enabled)
state.image_width = config.getint("Image", "image_width", fallback=state.image_width)
state.image_offset = config.getint("Image", "image_offset", fallback=state.image_offset)
state.image_opacity = config.getint("Image", "image_opacity", fallback=state.image_opacity)
state.image_as_wallpaper = config.getboolean("Image", "image_as_wallpaper", fallback=state.image_as_wallpaper)
state.image_position = config.get("Image", "image_position", fallback=state.image_position)
create_config()
create_config()
except configparser.Error as e:
consoleLog(f"Error: Configuration file corrupted ({e}). Resetting to defaults.")
backup_config(new_config_file) # Back up corrupted config
create_config() # Create new config file
NotificationPopup(
title="Config Reset",
text="Your configuration file has been reset due to a structural error.",
infotext="A backup of your old configuration file is available in the settings folder."
).exec()
except ValueError as e:
consoleLog(f"Error: Invalid data types in configuration file ({e}). Resetting to defaults.")
backup_config(new_config_file) # Back up corrupted config
create_config() # Create new config file
NotificationPopup(
title="Config Reset",
text="Your configuration file has been reset due to invalid data types.",
infotext="A backup of your old configuration file is available in the settings folder."
).exec()
except Exception as e:
consoleLog(f"Unexpected error occurred while loading settings: {e}")
NotificationPopup(
title="Unexpected Error",
text="An unknown error occurred while loading your settings.",
infotext=f"System returned: {e}\n\nThe application may not function as expected."
).exec()
def backup_config(config_path):
if os.path.exists(config_path):
timestamp = time.strftime("%Y%m%d-%H%M%S")
backup_path = f"{config_path}.{timestamp}.bak"
try:
os.replace(config_path, backup_path)
consoleLog(f"Successfully created backup of corrupted config: {backup_path}")
except Exception as e:
consoleLog(f"Failed to create backup of corrupted config: {e}")
+14 -2
View File
@@ -5,7 +5,7 @@ from utils.data.state import state
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None, bound_interface=None):
def save_settings(close=lambda: None, apiurl=None, download_path=None, down_speed_limit=None, up_speed_limit=None, image_path=None, autoresume=None, max_connections=None, max_downloads=None, bound_interface=None, image_width=None, image_offset=None, image_opacity=None, image_as_wallpaper=None, image_position=None, accent_color=None):
if apiurl is not None:
state.api_url = apiurl
if download_path is not None:
@@ -24,8 +24,20 @@ def save_settings(close=lambda: None, apiurl=None, download_path=None, down_spee
state.max_downloads = max_downloads
if bound_interface is not None:
state.bound_interface = None if bound_interface == "None" else bound_interface
if image_width is not None:
state.image_width = image_width
if image_offset is not None:
state.image_offset = image_offset
if image_opacity is not None:
state.image_opacity = image_opacity
if image_as_wallpaper is not None:
state.image_as_wallpaper = image_as_wallpaper
if image_position is not None:
state.image_position = image_position
if accent_color is not None:
state.accent_color = accent_color
update_settings()
update_settings() # Update LibTorrent Session Settings
consoleLog("Saved Settings")
create_config()
close()
+99
View File
@@ -25,11 +25,20 @@ class AppState(QObject):
# GUI
self.window_transparency: bool = False
self.accent_color: str = ""
self.trackertable: QTableWidget
self.interfaces: List = []
self.active_interfaces: List = []
self.bound_interface: Any = None
# Image
self._image_enabled: bool = False
self._image_width: int = 300 # Default to 300px
self._image_offset: int = 50
self._image_opacity: int = 100
self._image_as_wallpaper: bool = False
self._image_position: str = "bottom-right" # top-left, top-right, bottom-left, bottom-right, center
# Trackers / Scraping
self.posts: list[Dict[str,str]] | None = None # titles, urls, author, seeders, leechers
self.currenttracker: str = "rutracker"
@@ -58,4 +67,94 @@ class AppState(QObject):
self._image_path = new_path
self.image_changed.emit(new_path)
@property
def image_offset(self) -> int:
return self._image_offset
@image_offset.setter
def image_offset(self, new_offset: int):
if new_offset != self._image_offset:
self._image_offset = new_offset
self.image_changed.emit(self._image_path)
@property
def image_width(self) -> int:
return self._image_width
@image_width.setter
def image_width(self, new_width: int):
if new_width != self._image_width:
self._image_width = new_width
self.image_changed.emit(self._image_path)
@property
def image_opacity(self) -> int:
return self._image_opacity
@image_opacity.setter
def image_opacity(self, new_opacity: int):
if new_opacity != self._image_opacity:
self._image_opacity = new_opacity
self.image_changed.emit(self._image_path)
@property
def image_enabled(self) -> bool:
return self._image_enabled
@image_enabled.setter
def image_enabled(self, new_state: bool):
if new_state != self._image_enabled:
self._image_enabled = new_state
self.image_changed.emit(self._image_path)
@property
def image_as_wallpaper(self) -> bool:
return self._image_as_wallpaper
@image_as_wallpaper.setter
def image_as_wallpaper(self, new_state: bool):
if new_state != self._image_as_wallpaper:
self._image_as_wallpaper = new_state
self.image_changed.emit(self._image_path)
@property
def image_position(self) -> str:
return self._image_position
@image_position.setter
def image_position(self, new_pos: str):
if new_pos != self._image_position:
self._image_position = new_pos
self.image_changed.emit(self._image_path)
@property
def image_custom_position(self) -> bool:
return self._image_custom_position
@image_custom_position.setter
def image_custom_position(self, new_state: bool):
if new_state != self._image_custom_position:
self._image_custom_position = new_state
self.image_changed.emit(self._image_path)
@property
def image_x(self) -> int:
return self._image_x
@image_x.setter
def image_x(self, val: int):
if val != self._image_x:
self._image_x = val
self.image_changed.emit(self._image_path)
@property
def image_y(self) -> int:
return self._image_y
@image_y.setter
def image_y(self, val: int):
if val != self._image_y:
self._image_y = val
self.image_changed.emit(self._image_path)
state = AppState()
-1
View File
@@ -3,7 +3,6 @@ from utils.logging.logs import consoleLog, remove_download_log
import os
def split_data(data):
count = data.count
downloads = data.data
+3 -1
View File
@@ -1,4 +1,5 @@
from network.direct_download.handle import DirectDownloadHandle
from utils.general.shutdown import closehelper
from utils.logging.logs import consoleLog
from utils.data.state import state
from PySide6.QtCore import Qt
@@ -91,6 +92,7 @@ def download_update(assets: list):
progress.setValue(100)
QtWidgets.QApplication.processEvents()
closehelper()
subprocess.Popen([installer_path, "/VERYSILENT", "/SUPPRESSMSGBOXES", "/SP-", "/CLOSEAPPLICATIONS"])
time.sleep(1)
sys.exit(0)
os._exit(0)