Compare commits
8 Commits
3b39ed1806
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ff7feb17 | ||
| e38b1b3052 | |||
|
|
df3cc15356 | ||
|
|
0bb4ef7d51 | ||
|
|
c4b90aaba6 | ||
|
|
aa2c23537c | ||
|
|
69b0fb3226 | ||
|
|
1814054cc2 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -9,6 +9,7 @@ __pycache__/
|
|||||||
*.pyc.*
|
*.pyc.*
|
||||||
*.pyo.*
|
*.pyo.*
|
||||||
|
|
||||||
# SUPERCOPY
|
SUPERCOPY.py
|
||||||
copy.md
|
copy.md
|
||||||
.vscode
|
.vscode
|
||||||
|
start.sh
|
||||||
@@ -20,7 +20,6 @@ A WIP desktop assistant for Linux and Windows.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- Python 3
|
- Python 3
|
||||||
- X11 Desktop (Linux only)
|
|
||||||
- [`fd`](https://github.com/sharkdp/fd)
|
- [`fd`](https://github.com/sharkdp/fd)
|
||||||
- `git`
|
- `git`
|
||||||
|
|
||||||
@@ -33,6 +32,8 @@ A WIP desktop assistant for Linux and Windows.
|
|||||||
- `--restart` Add a restart option to the right click menu
|
- `--restart` Add a restart option to the right click menu
|
||||||
- `--no-quit` Hide the quit option
|
- `--no-quit` Hide the quit option
|
||||||
- `--no-update` Don't update automatically on startup
|
- `--no-update` Don't update automatically on startup
|
||||||
5. To configure, go to `~/.config/CLARA/config.json` on Linux and `~/AppData/Roaming/CLARA/config.json` on Windows.
|
|
||||||
|
|
||||||
If you want to contribute in any way, PRs (and issues) welcome
|
> [!IMPORTANT]
|
||||||
|
> If on Wayland, make sure to add `QT_QPA_PLATFORM=xcb` at the beginning of the command
|
||||||
|
|
||||||
|
If you want to contribute in any way, PRs (and issues) welcome
|
||||||
|
|||||||
402
SUPERCOPY.py
402
SUPERCOPY.py
@@ -1,38 +1,372 @@
|
|||||||
files = [
|
import fnmatch
|
||||||
"core/app_launcher.py",
|
import os
|
||||||
"core/config.py",
|
import sys
|
||||||
"core/discord_presence.py",
|
|
||||||
"core/dukto.py",
|
|
||||||
"core/file_search.py",
|
|
||||||
"core/headers.py",
|
|
||||||
"core/http_share.py",
|
|
||||||
"core/updater.py",
|
|
||||||
"core/wayland_utils.py",
|
|
||||||
"core/web_search.py",
|
|
||||||
"strings/en.json",
|
|
||||||
"strings/personality_en.json",
|
|
||||||
"windows/app_launcher.py",
|
|
||||||
"windows/calculator.py",
|
|
||||||
"windows/file_search.py",
|
|
||||||
"windows/main_window.py",
|
|
||||||
"windows/text_viewer.py",
|
|
||||||
"windows/web_results.py",
|
|
||||||
"main.py",
|
|
||||||
"README.md",
|
|
||||||
"requirements.txt"
|
|
||||||
]
|
|
||||||
|
|
||||||
codeblock = "```"
|
|
||||||
|
|
||||||
copy = ""
|
def get_language(file_path):
|
||||||
|
extension_map = {
|
||||||
|
".html": "html",
|
||||||
|
".htm": "html",
|
||||||
|
".css": "css",
|
||||||
|
".js": "javascript",
|
||||||
|
".mjs": "javascript",
|
||||||
|
".cjs": "javascript",
|
||||||
|
".py": "python",
|
||||||
|
".pyc": "python",
|
||||||
|
".pyo": "python",
|
||||||
|
".md": "markdown",
|
||||||
|
".markdown": "markdown",
|
||||||
|
".txt": "text",
|
||||||
|
".json": "json",
|
||||||
|
".geojson": "json",
|
||||||
|
".xml": "xml",
|
||||||
|
".php": "php",
|
||||||
|
".phtml": "php",
|
||||||
|
".sql": "sql",
|
||||||
|
".sh": "bash",
|
||||||
|
".bash": "bash",
|
||||||
|
".zsh": "bash",
|
||||||
|
".fish": "fish",
|
||||||
|
".yml": "yaml",
|
||||||
|
".yaml": "yaml",
|
||||||
|
".toml": "toml",
|
||||||
|
".ini": "ini",
|
||||||
|
".cfg": "ini",
|
||||||
|
".conf": "ini",
|
||||||
|
".config": "ini",
|
||||||
|
".log": "text",
|
||||||
|
".bat": "batch",
|
||||||
|
".cmd": "batch",
|
||||||
|
".ps1": "powershell",
|
||||||
|
".psm1": "powershell",
|
||||||
|
".psd1": "powershell",
|
||||||
|
".rb": "ruby",
|
||||||
|
".gemspec": "ruby",
|
||||||
|
".go": "go",
|
||||||
|
".java": "java",
|
||||||
|
".class": "java",
|
||||||
|
".c": "c",
|
||||||
|
".h": "cpp",
|
||||||
|
".cpp": "cpp",
|
||||||
|
".cc": "cpp",
|
||||||
|
".cxx": "cpp",
|
||||||
|
".c++": "cpp",
|
||||||
|
".hpp": "cpp",
|
||||||
|
".hh": "cpp",
|
||||||
|
".hxx": "cpp",
|
||||||
|
".cs": "csharp",
|
||||||
|
".csx": "csharp",
|
||||||
|
".swift": "swift",
|
||||||
|
".kt": "kotlin",
|
||||||
|
".kts": "kotlin",
|
||||||
|
".rs": "rust",
|
||||||
|
".ts": "typescript",
|
||||||
|
".tsx": "typescript",
|
||||||
|
".mts": "typescript",
|
||||||
|
".cts": "typescript",
|
||||||
|
".jsx": "javascript",
|
||||||
|
".vue": "vue",
|
||||||
|
".scss": "scss",
|
||||||
|
".sass": "sass",
|
||||||
|
".less": "less",
|
||||||
|
".styl": "stylus",
|
||||||
|
".stylus": "stylus",
|
||||||
|
".graphql": "graphql",
|
||||||
|
".gql": "graphql",
|
||||||
|
".dockerfile": "dockerfile",
|
||||||
|
".dockerignore": "dockerignore",
|
||||||
|
".editorconfig": "ini",
|
||||||
|
".gitignore": "gitignore",
|
||||||
|
".gitattributes": "gitattributes",
|
||||||
|
".gitmodules": "gitmodules",
|
||||||
|
".prettierrc": "json",
|
||||||
|
".eslintrc": "json",
|
||||||
|
".babelrc": "json",
|
||||||
|
".npmignore": "gitignore",
|
||||||
|
".lock": "text",
|
||||||
|
".env": "env",
|
||||||
|
".env.local": "env",
|
||||||
|
".env.development": "env",
|
||||||
|
".env.production": "env",
|
||||||
|
".env.test": "env",
|
||||||
|
}
|
||||||
|
|
||||||
for file in files:
|
ext = os.path.splitext(file_path)[1].lower()
|
||||||
with open(file, "r", encoding="utf-8") as f:
|
return extension_map.get(ext, "")
|
||||||
lines = f.readlines()
|
|
||||||
copy += f"### {file}\n\n"
|
|
||||||
copy += f"{codeblock}python\n"
|
|
||||||
copy += "".join(lines)
|
|
||||||
copy += f"\n{codeblock}\n\n"
|
|
||||||
|
|
||||||
with open("copy.md", "w", encoding="utf-8") as f:
|
|
||||||
f.write(copy)
|
def should_exclude(file_path, root_dir):
|
||||||
|
"""Determine if a file should be excluded from copying."""
|
||||||
|
abs_path = os.path.abspath(file_path)
|
||||||
|
rel_path = os.path.relpath(abs_path, root_dir)
|
||||||
|
rel_path_forward = rel_path.replace(os.sep, "/")
|
||||||
|
basename = os.path.basename(file_path)
|
||||||
|
|
||||||
|
# Exclude specific files
|
||||||
|
exclude_files = {".pyc"}
|
||||||
|
if rel_path_forward in exclude_files or basename in exclude_files:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Exclude image files
|
||||||
|
image_extensions = {
|
||||||
|
".png",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".gif",
|
||||||
|
".svg",
|
||||||
|
".bmp",
|
||||||
|
".ico",
|
||||||
|
".tiff",
|
||||||
|
".tif",
|
||||||
|
".webp",
|
||||||
|
".heic",
|
||||||
|
".heif",
|
||||||
|
".avif",
|
||||||
|
".jfif",
|
||||||
|
".pjpeg",
|
||||||
|
".pjp",
|
||||||
|
".tga",
|
||||||
|
".psd",
|
||||||
|
".raw",
|
||||||
|
".cr2",
|
||||||
|
".nef",
|
||||||
|
".orf",
|
||||||
|
".sr2",
|
||||||
|
".arw",
|
||||||
|
".dng",
|
||||||
|
".rw2",
|
||||||
|
".raf",
|
||||||
|
".3fr",
|
||||||
|
".kdc",
|
||||||
|
".mef",
|
||||||
|
".mrw",
|
||||||
|
".pef",
|
||||||
|
".srw",
|
||||||
|
".x3f",
|
||||||
|
".r3d",
|
||||||
|
".fff",
|
||||||
|
".iiq",
|
||||||
|
".erf",
|
||||||
|
".nrw",
|
||||||
|
}
|
||||||
|
|
||||||
|
ext = os.path.splitext(file_path)[1].lower()
|
||||||
|
if ext in image_extensions:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def load_gitignore_patterns(root_dir):
|
||||||
|
patterns = []
|
||||||
|
gitignore_path = os.path.join(root_dir, ".gitignore")
|
||||||
|
|
||||||
|
if os.path.isfile(gitignore_path):
|
||||||
|
try:
|
||||||
|
with open(gitignore_path, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
# Skip empty lines and comments
|
||||||
|
if line and not line.startswith("#"):
|
||||||
|
# Remove trailing backslash for escaped #
|
||||||
|
if line.startswith(r"\#"):
|
||||||
|
line = line[1:]
|
||||||
|
patterns.append(line)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not read .gitignore: {e}")
|
||||||
|
|
||||||
|
return patterns
|
||||||
|
|
||||||
|
|
||||||
|
def is_ignored(path, patterns, root_dir):
|
||||||
|
if not patterns:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get relative path with forward slashes
|
||||||
|
rel_path = os.path.relpath(path, root_dir).replace(os.sep, "/")
|
||||||
|
|
||||||
|
# For directories, also check with trailing slash
|
||||||
|
if os.path.isdir(path):
|
||||||
|
rel_path_with_slash = rel_path + "/"
|
||||||
|
else:
|
||||||
|
rel_path_with_slash = rel_path
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
# Skip negation patterns (too complex for this script)
|
||||||
|
if pattern.startswith("!"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Directory pattern (ending with /)
|
||||||
|
if pattern.endswith("/"):
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
continue
|
||||||
|
pattern = pattern.rstrip("/")
|
||||||
|
# Match directory name or anything inside it
|
||||||
|
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(
|
||||||
|
rel_path_with_slash, pattern + "/*"
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Absolute pattern (starting with /) - match from root only
|
||||||
|
if pattern.startswith("/"):
|
||||||
|
pattern = pattern.lstrip("/")
|
||||||
|
if fnmatch.fnmatch(rel_path, pattern):
|
||||||
|
return True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Pattern without slash - matches at any level
|
||||||
|
if "/" not in pattern:
|
||||||
|
# Check basename
|
||||||
|
basename = os.path.basename(rel_path)
|
||||||
|
if fnmatch.fnmatch(basename, pattern):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Pattern with slash - relative path match
|
||||||
|
if fnmatch.fnmatch(rel_path, pattern):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_files_from_directory(
|
||||||
|
directory, recursive=False, root_dir=None, gitignore_patterns=None
|
||||||
|
):
|
||||||
|
if root_dir is None:
|
||||||
|
root_dir = os.getcwd()
|
||||||
|
|
||||||
|
if gitignore_patterns is None:
|
||||||
|
gitignore_patterns = []
|
||||||
|
|
||||||
|
files_list = []
|
||||||
|
abs_directory = os.path.abspath(directory)
|
||||||
|
|
||||||
|
if not os.path.exists(abs_directory):
|
||||||
|
print(f"Warning: Directory '{directory}' not found.")
|
||||||
|
return files_list
|
||||||
|
|
||||||
|
# Skip if directory itself is ignored
|
||||||
|
if is_ignored(abs_directory, gitignore_patterns, root_dir):
|
||||||
|
return files_list
|
||||||
|
|
||||||
|
if recursive:
|
||||||
|
for dirpath, dirnames, filenames in os.walk(abs_directory):
|
||||||
|
# Filter directories: exclude hidden and gitignored
|
||||||
|
dirnames[:] = [
|
||||||
|
d
|
||||||
|
for d in dirnames
|
||||||
|
if not d.startswith(".")
|
||||||
|
and not is_ignored(
|
||||||
|
os.path.join(dirpath, d), gitignore_patterns, root_dir
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Filter files
|
||||||
|
for filename in filenames:
|
||||||
|
if filename.startswith("."):
|
||||||
|
continue
|
||||||
|
|
||||||
|
full_path = os.path.join(dirpath, filename)
|
||||||
|
if (
|
||||||
|
os.path.isfile(full_path)
|
||||||
|
and not should_exclude(full_path, root_dir)
|
||||||
|
and not is_ignored(full_path, gitignore_patterns, root_dir)
|
||||||
|
):
|
||||||
|
files_list.append(full_path)
|
||||||
|
else:
|
||||||
|
for filename in os.listdir(abs_directory):
|
||||||
|
if filename.startswith("."):
|
||||||
|
continue
|
||||||
|
|
||||||
|
full_path = os.path.join(abs_directory, filename)
|
||||||
|
|
||||||
|
# Skip directories in non-recursive mode
|
||||||
|
if os.path.isdir(full_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
os.path.isfile(full_path)
|
||||||
|
and not should_exclude(full_path, root_dir)
|
||||||
|
and not is_ignored(full_path, gitignore_patterns, root_dir)
|
||||||
|
):
|
||||||
|
files_list.append(full_path)
|
||||||
|
|
||||||
|
return files_list
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root_dir = os.getcwd()
|
||||||
|
script_path = os.path.abspath(__file__)
|
||||||
|
output_file = "copy.md"
|
||||||
|
codeblock = "```"
|
||||||
|
|
||||||
|
# Load .gitignore patterns
|
||||||
|
gitignore_patterns = load_gitignore_patterns(root_dir)
|
||||||
|
if gitignore_patterns:
|
||||||
|
print(f"Loaded {len(gitignore_patterns)} patterns from .gitignore")
|
||||||
|
|
||||||
|
def is_output_file(path):
|
||||||
|
return os.path.abspath(path) == os.path.abspath(output_file)
|
||||||
|
|
||||||
|
# Directories to process: (path, recursive)
|
||||||
|
directories = [
|
||||||
|
("./", False), # Root directory
|
||||||
|
("assets/", True), # Archive directory (with subdirectories)
|
||||||
|
("core/", True), # Legacy directory
|
||||||
|
("strings/", True), # Maybe directory
|
||||||
|
("windows/", True),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_files = []
|
||||||
|
for directory, recursive in directories:
|
||||||
|
files = get_files_from_directory(
|
||||||
|
directory, recursive, root_dir, gitignore_patterns
|
||||||
|
)
|
||||||
|
files = [
|
||||||
|
f
|
||||||
|
for f in files
|
||||||
|
if not is_output_file(f) and os.path.abspath(f) != script_path
|
||||||
|
]
|
||||||
|
all_files.extend(files)
|
||||||
|
|
||||||
|
# Remove duplicates and sort
|
||||||
|
all_files = sorted(set(all_files))
|
||||||
|
|
||||||
|
markdown_content = ""
|
||||||
|
file_count = 0
|
||||||
|
|
||||||
|
for file_path in all_files:
|
||||||
|
try:
|
||||||
|
rel_path = os.path.relpath(file_path, root_dir)
|
||||||
|
language = get_language(file_path)
|
||||||
|
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
markdown_content += f"### {rel_path.replace(os.sep, '/')}\n\n"
|
||||||
|
markdown_content += (
|
||||||
|
f"{codeblock}{language}\n" if language else f"{codeblock}\n"
|
||||||
|
)
|
||||||
|
markdown_content += content
|
||||||
|
markdown_content += f"\n{codeblock}\n\n"
|
||||||
|
|
||||||
|
file_count += 1
|
||||||
|
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
print(f"Warning: Could not read {file_path} as text. Skipping.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {file_path}: {e}")
|
||||||
|
|
||||||
|
markdown_content += f"<!-- Processed {file_count} files -->\n"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(markdown_content)
|
||||||
|
print(f"Successfully created {output_file} with {file_count} files.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error writing to {output_file}: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import fnmatch
|
|
||||||
|
|
||||||
def _find_native(pattern: str, root: str):
|
def _find_native(pattern: str, root: str):
|
||||||
"""Native Python implementation of file search using os.walk."""
|
"""Native Python implementation of file search using os.walk."""
|
||||||
@@ -12,14 +12,17 @@ def _find_native(pattern: str, root: str):
|
|||||||
results.append(os.path.join(dirpath, filename))
|
results.append(os.path.join(dirpath, filename))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def find(pattern: str, root: str='/'):
|
|
||||||
|
def find(pattern: str, root: str = "/"):
|
||||||
path = os.path.expanduser(root)
|
path = os.path.expanduser(root)
|
||||||
|
|
||||||
if shutil.which('fd') is None:
|
if shutil.which("fd") is None:
|
||||||
return _find_native(f"*{pattern}*", path)
|
return _find_native(f"*{pattern}*", path)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
out = subprocess.check_output(['fd', pattern, path], text=True, errors='ignore')
|
out = subprocess.check_output(
|
||||||
|
["fd", pattern, path], text=True, errors="ignore"
|
||||||
|
)
|
||||||
return out.splitlines()
|
return out.splitlines()
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
|
||||||
from typing import Tuple, Optional
|
|
||||||
|
|
||||||
def is_wayland() -> bool:
|
|
||||||
session_type = os.environ.get('XDG_SESSION_TYPE', '').lower()
|
|
||||||
wayland_display = os.environ.get('WAYLAND_DISPLAY', '')
|
|
||||||
|
|
||||||
return session_type == 'wayland' or bool(wayland_display)
|
|
||||||
|
|
||||||
def get_screen_info() -> Tuple[int, int]:
|
|
||||||
if is_wayland():
|
|
||||||
try:
|
|
||||||
output = subprocess.check_output(['wlr-randr'], text=True, stderr=subprocess.DEVNULL)
|
|
||||||
for line in output.splitlines():
|
|
||||||
if 'current' in line.lower():
|
|
||||||
parts = line.split()
|
|
||||||
for part in parts:
|
|
||||||
if 'x' in part and part.replace('x', '').replace('px', '').isdigit():
|
|
||||||
dims = part.replace('px', '').split('x')
|
|
||||||
return int(dims[0]), int(dims[1])
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = subprocess.check_output(['swaymsg', '-t', 'get_outputs'], text=True)
|
|
||||||
outputs = json.loads(output)
|
|
||||||
if outputs and outputs[0].get('current_mode'):
|
|
||||||
mode = outputs[0]['current_mode']
|
|
||||||
return mode['width'], mode['height']
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError, KeyError, PermissionError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = subprocess.check_output(['kscreen-doctor', '-o'], text=True)
|
|
||||||
for line in output.splitlines():
|
|
||||||
if 'Output:' in line:
|
|
||||||
continue
|
|
||||||
if 'x' in line and '@' in line:
|
|
||||||
resolution = line.split('@')[0].strip().split()[-1]
|
|
||||||
if 'x' in resolution:
|
|
||||||
dims = resolution.split('x')
|
|
||||||
return int(dims[0]), int(dims[1])
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
output = subprocess.check_output(['xrandr'], text=True, stderr=subprocess.DEVNULL)
|
|
||||||
for line in output.splitlines():
|
|
||||||
if ' connected' in line and 'primary' in line:
|
|
||||||
parts = line.split()
|
|
||||||
for part in parts:
|
|
||||||
if 'x' in part and '+' in part:
|
|
||||||
dims = part.split('+')[0].split('x')
|
|
||||||
return int(dims[0]), int(dims[1])
|
|
||||||
elif ' connected' in line and '*' in line:
|
|
||||||
parts = line.split()
|
|
||||||
for i, part in enumerate(parts):
|
|
||||||
if 'x' in part and i > 0:
|
|
||||||
dims = part.split('x')
|
|
||||||
if dims[0].isdigit():
|
|
||||||
return int(dims[0]), int(dims[1].split('+')[0] if '+' in dims[1] else dims[1])
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, PermissionError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# maybe somehow right sometimes
|
|
||||||
return 1920, 1080
|
|
||||||
|
|
||||||
def set_window_bottom_right_wayland(window, width: int, height: int):
|
|
||||||
screen_w, screen_h = get_screen_info()
|
|
||||||
x = screen_w - width
|
|
||||||
y = screen_h - height
|
|
||||||
|
|
||||||
try:
|
|
||||||
window.move(x, y)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_wayland_compositor() -> Optional[str]:
|
|
||||||
desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
|
|
||||||
|
|
||||||
if 'sway' in desktop:
|
|
||||||
return 'sway'
|
|
||||||
elif 'kde' in desktop or 'plasma' in desktop:
|
|
||||||
return 'kwin'
|
|
||||||
elif 'gnome' in desktop:
|
|
||||||
return 'mutter'
|
|
||||||
elif 'hypr' in desktop:
|
|
||||||
return 'hyprland'
|
|
||||||
|
|
||||||
# detect from process list
|
|
||||||
try:
|
|
||||||
output = subprocess.check_output(['ps', 'aux'], text=True)
|
|
||||||
if 'sway' in output:
|
|
||||||
return 'sway'
|
|
||||||
elif 'kwin_wayland' in output:
|
|
||||||
return 'kwin'
|
|
||||||
elif 'gnome-shell' in output:
|
|
||||||
return 'mutter'
|
|
||||||
elif 'Hyprland' in output:
|
|
||||||
return 'hyprland'
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
@@ -1,50 +1,114 @@
|
|||||||
|
# TODO: Switch to s different search provider
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from typing import Optional, Dict, List, Any
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from core.headers import get_useragent
|
from core.headers import get_useragent
|
||||||
|
|
||||||
|
|
||||||
class MullvadLetaWrapper:
|
class MullvadLetaWrapper:
|
||||||
"""Wrapper for Mullvad Leta privacy-focused search engine."""
|
"""Wrapper for Mullvad Leta privacy-focused search engine."""
|
||||||
|
|
||||||
BASE_URL = "https://leta.mullvad.net/search"
|
BASE_URL = "https://leta.mullvad.net/search"
|
||||||
|
|
||||||
# Available search engines
|
# Available search engines
|
||||||
ENGINES = ["brave", "google"]
|
ENGINES = ["brave", "google"]
|
||||||
|
|
||||||
# Available countries (from the HTML)
|
# Available countries (from the HTML)
|
||||||
COUNTRIES = [
|
COUNTRIES = [
|
||||||
"ar", "au", "at", "be", "br", "ca", "cl", "cn", "dk", "fi",
|
"ar",
|
||||||
"fr", "de", "hk", "in", "id", "it", "jp", "kr", "my", "mx",
|
"au",
|
||||||
"nl", "nz", "no", "ph", "pl", "pt", "ru", "sa", "za", "es",
|
"at",
|
||||||
"se", "ch", "tw", "tr", "uk", "us"
|
"be",
|
||||||
|
"br",
|
||||||
|
"ca",
|
||||||
|
"cl",
|
||||||
|
"cn",
|
||||||
|
"dk",
|
||||||
|
"fi",
|
||||||
|
"fr",
|
||||||
|
"de",
|
||||||
|
"hk",
|
||||||
|
"in",
|
||||||
|
"id",
|
||||||
|
"it",
|
||||||
|
"jp",
|
||||||
|
"kr",
|
||||||
|
"my",
|
||||||
|
"mx",
|
||||||
|
"nl",
|
||||||
|
"nz",
|
||||||
|
"no",
|
||||||
|
"ph",
|
||||||
|
"pl",
|
||||||
|
"pt",
|
||||||
|
"ru",
|
||||||
|
"sa",
|
||||||
|
"za",
|
||||||
|
"es",
|
||||||
|
"se",
|
||||||
|
"ch",
|
||||||
|
"tw",
|
||||||
|
"tr",
|
||||||
|
"uk",
|
||||||
|
"us",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Available languages
|
# Available languages
|
||||||
LANGUAGES = [
|
LANGUAGES = [
|
||||||
"ar", "bg", "ca", "zh-hans", "zh-hant", "hr", "cs", "da", "nl",
|
"ar",
|
||||||
"en", "et", "fi", "fr", "de", "he", "hu", "is", "it", "jp",
|
"bg",
|
||||||
"ko", "lv", "lt", "nb", "pl", "pt", "ro", "ru", "sr", "sk",
|
"ca",
|
||||||
"sl", "es", "sv", "tr"
|
"zh-hans",
|
||||||
|
"zh-hant",
|
||||||
|
"hr",
|
||||||
|
"cs",
|
||||||
|
"da",
|
||||||
|
"nl",
|
||||||
|
"en",
|
||||||
|
"et",
|
||||||
|
"fi",
|
||||||
|
"fr",
|
||||||
|
"de",
|
||||||
|
"he",
|
||||||
|
"hu",
|
||||||
|
"is",
|
||||||
|
"it",
|
||||||
|
"jp",
|
||||||
|
"ko",
|
||||||
|
"lv",
|
||||||
|
"lt",
|
||||||
|
"nb",
|
||||||
|
"pl",
|
||||||
|
"pt",
|
||||||
|
"ro",
|
||||||
|
"ru",
|
||||||
|
"sr",
|
||||||
|
"sk",
|
||||||
|
"sl",
|
||||||
|
"es",
|
||||||
|
"sv",
|
||||||
|
"tr",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Time filters
|
# Time filters
|
||||||
TIME_FILTERS = ["d", "w", "m", "y"] # day, week, month, year
|
TIME_FILTERS = ["d", "w", "m", "y"] # day, week, month, year
|
||||||
|
|
||||||
def __init__(self, engine: str = "brave"):
|
def __init__(self, engine: str = "brave"):
|
||||||
"""
|
"""
|
||||||
Initialize the Mullvad Leta wrapper.
|
Initialize the Mullvad Leta wrapper.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
engine: Search engine to use ("brave" or "google")
|
engine: Search engine to use ("brave" or "google")
|
||||||
"""
|
"""
|
||||||
if engine not in self.ENGINES:
|
if engine not in self.ENGINES:
|
||||||
raise ValueError(f"Engine must be one of {self.ENGINES}")
|
raise ValueError(f"Engine must be one of {self.ENGINES}")
|
||||||
|
|
||||||
self.engine = engine
|
self.engine = engine
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|
||||||
def _get_headers(self) -> Dict[str, str]:
|
def _get_headers(self) -> Dict[str, str]:
|
||||||
"""Get request headers with user agent."""
|
"""Get request headers with user agent."""
|
||||||
return {
|
return {
|
||||||
@@ -59,45 +123,42 @@ class MullvadLetaWrapper:
|
|||||||
"sec-fetch-site": "same-origin",
|
"sec-fetch-site": "same-origin",
|
||||||
"sec-fetch-user": "?1",
|
"sec-fetch-user": "?1",
|
||||||
"upgrade-insecure-requests": "1",
|
"upgrade-insecure-requests": "1",
|
||||||
"user-agent": get_useragent()
|
"user-agent": get_useragent(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
country: Optional[str] = None,
|
country: Optional[str] = None,
|
||||||
language: Optional[str] = None,
|
language: Optional[str] = None,
|
||||||
last_updated: Optional[str] = None,
|
last_updated: Optional[str] = None,
|
||||||
page: int = 1
|
page: int = 1,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Perform a search on Mullvad Leta.
|
Perform a search on Mullvad Leta.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
query: Search query string
|
query: Search query string
|
||||||
country: Country code filter (e.g., "us", "uk")
|
country: Country code filter (e.g., "us", "uk")
|
||||||
language: Language code filter (e.g., "en", "fr")
|
language: Language code filter (e.g., "en", "fr")
|
||||||
last_updated: Time filter ("d", "w", "m", "y")
|
last_updated: Time filter ("d", "w", "m", "y")
|
||||||
page: Page number (default: 1)
|
page: Page number (default: 1)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing search results and metadata
|
Dictionary containing search results and metadata
|
||||||
"""
|
"""
|
||||||
if country and country not in self.COUNTRIES:
|
if country and country not in self.COUNTRIES:
|
||||||
raise ValueError(f"Invalid country code. Must be one of {self.COUNTRIES}")
|
raise ValueError(f"Invalid country code. Must be one of {self.COUNTRIES}")
|
||||||
|
|
||||||
if language and language not in self.LANGUAGES:
|
if language and language not in self.LANGUAGES:
|
||||||
raise ValueError(f"Invalid language code. Must be one of {self.LANGUAGES}")
|
raise ValueError(f"Invalid language code. Must be one of {self.LANGUAGES}")
|
||||||
|
|
||||||
if last_updated and last_updated not in self.TIME_FILTERS:
|
if last_updated and last_updated not in self.TIME_FILTERS:
|
||||||
raise ValueError(f"Invalid time filter. Must be one of {self.TIME_FILTERS}")
|
raise ValueError(f"Invalid time filter. Must be one of {self.TIME_FILTERS}")
|
||||||
|
|
||||||
# Build query parameters
|
# Build query parameters
|
||||||
params = {
|
params = {"q": query, "engine": self.engine}
|
||||||
"q": query,
|
|
||||||
"engine": self.engine
|
|
||||||
}
|
|
||||||
|
|
||||||
if country:
|
if country:
|
||||||
params["country"] = country
|
params["country"] = country
|
||||||
if language:
|
if language:
|
||||||
@@ -106,37 +167,37 @@ class MullvadLetaWrapper:
|
|||||||
params["lastUpdated"] = last_updated
|
params["lastUpdated"] = last_updated
|
||||||
if page > 1:
|
if page > 1:
|
||||||
params["page"] = str(page)
|
params["page"] = str(page)
|
||||||
|
|
||||||
# Set cookie for engine preference
|
# Set cookie for engine preference
|
||||||
cookies = {"engine": self.engine}
|
cookies = {"engine": self.engine}
|
||||||
|
|
||||||
# Make request
|
# Make request
|
||||||
response = self.session.get(
|
response = self.session.get(
|
||||||
self.BASE_URL,
|
self.BASE_URL,
|
||||||
params=params,
|
params=params,
|
||||||
headers=self._get_headers(),
|
headers=self._get_headers(),
|
||||||
cookies=cookies,
|
cookies=cookies,
|
||||||
timeout=10
|
timeout=10,
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# Parse results
|
# Parse results
|
||||||
return self._parse_results(response.text, query, page)
|
return self._parse_results(response.text, query, page)
|
||||||
|
|
||||||
def _parse_results(self, html: str, query: str, page: int) -> Dict[str, Any]:
|
def _parse_results(self, html: str, query: str, page: int) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Parse HTML response and extract search results.
|
Parse HTML response and extract search results.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
html: HTML response content
|
html: HTML response content
|
||||||
query: Original search query
|
query: Original search query
|
||||||
page: Current page number
|
page: Current page number
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing parsed results
|
Dictionary containing parsed results
|
||||||
"""
|
"""
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
|
||||||
results = {
|
results = {
|
||||||
"query": query,
|
"query": query,
|
||||||
"page": page,
|
"page": page,
|
||||||
@@ -144,100 +205,102 @@ class MullvadLetaWrapper:
|
|||||||
"results": [],
|
"results": [],
|
||||||
"infobox": None,
|
"infobox": None,
|
||||||
"news": [],
|
"news": [],
|
||||||
"cached": False
|
"cached": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if cached
|
# Check if cached
|
||||||
cache_notice = soup.find('p', class_='small')
|
cache_notice = soup.find("p", class_="small")
|
||||||
if cache_notice and 'cached' in cache_notice.text.lower():
|
if cache_notice and "cached" in cache_notice.text.lower():
|
||||||
results["cached"] = True
|
results["cached"] = True
|
||||||
|
|
||||||
# Extract regular search results
|
# Extract regular search results
|
||||||
articles = soup.find_all('article', class_='svelte-fmlk7p')
|
articles = soup.find_all("article", class_="svelte-fmlk7p")
|
||||||
for article in articles:
|
for article in articles:
|
||||||
result = self._parse_article(article)
|
result = self._parse_article(article)
|
||||||
if result:
|
if result:
|
||||||
results["results"].append(result)
|
results["results"].append(result)
|
||||||
|
|
||||||
# Extract infobox if present
|
# Extract infobox if present
|
||||||
infobox_div = soup.find('div', class_='infobox')
|
infobox_div = soup.find("div", class_="infobox")
|
||||||
if infobox_div:
|
if infobox_div:
|
||||||
results["infobox"] = self._parse_infobox(infobox_div)
|
results["infobox"] = self._parse_infobox(infobox_div)
|
||||||
|
|
||||||
# Extract news results
|
# Extract news results
|
||||||
news_div = soup.find('div', class_='news')
|
news_div = soup.find("div", class_="news")
|
||||||
if news_div:
|
if news_div:
|
||||||
news_articles = news_div.find_all('article')
|
news_articles = news_div.find_all("article")
|
||||||
for article in news_articles:
|
for article in news_articles:
|
||||||
news_item = self._parse_news_article(article)
|
news_item = self._parse_news_article(article)
|
||||||
if news_item:
|
if news_item:
|
||||||
results["news"].append(news_item)
|
results["news"].append(news_item)
|
||||||
|
|
||||||
# Check for next page
|
# Check for next page
|
||||||
next_button = soup.find('button', {'data-cy': 'next-button'})
|
next_button = soup.find("button", {"data-cy": "next-button"})
|
||||||
results["has_next_page"] = next_button is not None
|
results["has_next_page"] = next_button is not None
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _parse_article(self, article) -> Optional[Dict[str, str]]:
|
def _parse_article(self, article) -> Optional[Dict[str, str]]:
|
||||||
"""Parse a single search result article."""
|
"""Parse a single search result article."""
|
||||||
try:
|
try:
|
||||||
link_tag = article.find('a', href=True)
|
link_tag = article.find("a", href=True)
|
||||||
if not link_tag:
|
if not link_tag:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
title_tag = article.find('h3')
|
title_tag = article.find("h3")
|
||||||
snippet_tag = article.find('p', class_='result__body')
|
snippet_tag = article.find("p", class_="result__body")
|
||||||
cite_tag = article.find('cite')
|
cite_tag = article.find("cite")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"url": link_tag['href'],
|
"url": link_tag["href"],
|
||||||
"title": title_tag.get_text(strip=True) if title_tag else "",
|
"title": title_tag.get_text(strip=True) if title_tag else "",
|
||||||
"snippet": snippet_tag.get_text(strip=True) if snippet_tag else "",
|
"snippet": snippet_tag.get_text(strip=True) if snippet_tag else "",
|
||||||
"display_url": cite_tag.get_text(strip=True) if cite_tag else ""
|
"display_url": cite_tag.get_text(strip=True) if cite_tag else "",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error parsing article: {e}")
|
print(f"Error parsing article: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _parse_infobox(self, infobox_div) -> Dict[str, Any]:
|
def _parse_infobox(self, infobox_div) -> Dict[str, Any]:
|
||||||
"""Parse infobox information."""
|
"""Parse infobox information."""
|
||||||
infobox = {}
|
infobox = {}
|
||||||
|
|
||||||
title_tag = infobox_div.find('h1')
|
title_tag = infobox_div.find("h1")
|
||||||
if title_tag:
|
if title_tag:
|
||||||
infobox["title"] = title_tag.get_text(strip=True)
|
infobox["title"] = title_tag.get_text(strip=True)
|
||||||
|
|
||||||
subtitle_tag = infobox_div.find('h2')
|
subtitle_tag = infobox_div.find("h2")
|
||||||
if subtitle_tag:
|
if subtitle_tag:
|
||||||
infobox["subtitle"] = subtitle_tag.get_text(strip=True)
|
infobox["subtitle"] = subtitle_tag.get_text(strip=True)
|
||||||
|
|
||||||
url_tag = infobox_div.find('a', rel='noreferrer')
|
url_tag = infobox_div.find("a", rel="noreferrer")
|
||||||
if url_tag:
|
if url_tag:
|
||||||
infobox["url"] = url_tag['href']
|
infobox["url"] = url_tag["href"]
|
||||||
|
|
||||||
desc_tag = infobox_div.find('p')
|
desc_tag = infobox_div.find("p")
|
||||||
if desc_tag:
|
if desc_tag:
|
||||||
infobox["description"] = desc_tag.get_text(strip=True)
|
infobox["description"] = desc_tag.get_text(strip=True)
|
||||||
|
|
||||||
return infobox
|
return infobox
|
||||||
|
|
||||||
def _parse_news_article(self, article) -> Optional[Dict[str, str]]:
|
def _parse_news_article(self, article) -> Optional[Dict[str, str]]:
|
||||||
"""Parse a news article."""
|
"""Parse a news article."""
|
||||||
try:
|
try:
|
||||||
link_tag = article.find('a', href=True)
|
link_tag = article.find("a", href=True)
|
||||||
if not link_tag:
|
if not link_tag:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
title_tag = link_tag.find('h3')
|
title_tag = link_tag.find("h3")
|
||||||
cite_tag = link_tag.find('cite')
|
cite_tag = link_tag.find("cite")
|
||||||
time_tag = link_tag.find('time')
|
time_tag = link_tag.find("time")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"url": link_tag['href'],
|
"url": link_tag["href"],
|
||||||
"title": title_tag.get_text(strip=True) if title_tag else "",
|
"title": title_tag.get_text(strip=True) if title_tag else "",
|
||||||
"source": cite_tag.get_text(strip=True) if cite_tag else "",
|
"source": cite_tag.get_text(strip=True) if cite_tag else "",
|
||||||
"timestamp": time_tag['datetime'] if time_tag and time_tag.has_attr('datetime') else ""
|
"timestamp": time_tag["datetime"]
|
||||||
|
if time_tag and time_tag.has_attr("datetime")
|
||||||
|
else "",
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error parsing news article: {e}")
|
print(f"Error parsing news article: {e}")
|
||||||
@@ -248,23 +311,23 @@ class MullvadLetaWrapper:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Create wrapper instance
|
# Create wrapper instance
|
||||||
leta = MullvadLetaWrapper(engine="brave")
|
leta = MullvadLetaWrapper(engine="brave")
|
||||||
|
|
||||||
# Perform a search
|
# Perform a search
|
||||||
results = leta.search("python programming", country="us", language="en")
|
results = leta.search("python programming", country="us", language="en")
|
||||||
|
|
||||||
# Display results
|
# Display results
|
||||||
print(f"Query: {results['query']}")
|
print(f"Query: {results['query']}")
|
||||||
print(f"Engine: {results['engine']}")
|
print(f"Engine: {results['engine']}")
|
||||||
print(f"Cached: {results['cached']}")
|
print(f"Cached: {results['cached']}")
|
||||||
print(f"\nFound {len(results['results'])} results:\n")
|
print(f"\nFound {len(results['results'])} results:\n")
|
||||||
|
|
||||||
for i, result in enumerate(results['results'][:5], 1):
|
for i, result in enumerate(results["results"][:5], 1):
|
||||||
print(f"{i}. {result['title']}")
|
print(f"{i}. {result['title']}")
|
||||||
print(f" URL: {result['url']}")
|
print(f" URL: {result['url']}")
|
||||||
print(f" {result['snippet'][:100]}...\n")
|
print(f" {result['snippet'][:100]}...\n")
|
||||||
|
|
||||||
if results['news']:
|
if results["news"]:
|
||||||
print(f"\nNews ({len(results['news'])} items):")
|
print(f"\nNews ({len(results['news'])} items):")
|
||||||
for news in results['news'][:3]:
|
for news in results["news"][:3]:
|
||||||
print(f"- {news['title']}")
|
print(f"- {news['title']}")
|
||||||
print(f" {news['source']}\n")
|
print(f" {news['source']}\n")
|
||||||
|
|||||||
53
main.py
53
main.py
@@ -1,39 +1,44 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
import sys, json
|
import json
|
||||||
from pathlib import Path
|
import sys
|
||||||
from PySide6 import QtWidgets
|
|
||||||
import threading
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PySide6 import QtWidgets
|
||||||
|
|
||||||
from core.discord_presence import presence
|
|
||||||
from core.dukto import DuktoProtocol
|
|
||||||
from core.updater import update_repository, is_update_available
|
|
||||||
from core.app_launcher import list_apps
|
from core.app_launcher import list_apps
|
||||||
from core.config import config
|
from core.config import config
|
||||||
|
from core.discord_presence import presence
|
||||||
|
from core.dukto import DuktoProtocol
|
||||||
|
from core.updater import is_update_available, update_repository
|
||||||
from windows.main_window import MainWindow
|
from windows.main_window import MainWindow
|
||||||
|
|
||||||
STRINGS_PATH = Path(__file__).parent / "strings" / "personality_en.json"
|
STRINGS_PATH = Path(__file__).parent / "strings" / "personality_en.json"
|
||||||
|
|
||||||
|
|
||||||
def preload_apps():
|
def preload_apps():
|
||||||
print("Preloading application list...")
|
print("Preloading application list...")
|
||||||
list_apps()
|
list_apps()
|
||||||
print("Application list preloaded.")
|
print("Application list preloaded.")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(STRINGS_PATH, 'r', encoding='utf-8') as f:
|
with open(STRINGS_PATH, "r", encoding="utf-8") as f:
|
||||||
strings = json.load(f)
|
strings = json.load(f)
|
||||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||||
print(f"Error loading strings file: {e}")
|
print(f"Error loading strings file: {e}")
|
||||||
error_dialog = QtWidgets.QMessageBox()
|
error_dialog = QtWidgets.QMessageBox()
|
||||||
error_dialog.setIcon(QtWidgets.QMessageBox.Critical) #type: ignore
|
error_dialog.setIcon(QtWidgets.QMessageBox.Critical) # type: ignore
|
||||||
error_dialog.setText(f"Could not load required strings file from:\n{STRINGS_PATH}")
|
error_dialog.setText(
|
||||||
|
f"Could not load required strings file from:\n{STRINGS_PATH}"
|
||||||
|
)
|
||||||
error_dialog.setWindowTitle("Fatal Error")
|
error_dialog.setWindowTitle("Fatal Error")
|
||||||
error_dialog.exec()
|
error_dialog.exec()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
app.setApplicationName("CLARA")
|
app.setApplicationName("CLARA")
|
||||||
|
|
||||||
restart = "--restart" in sys.argv
|
restart = "--restart" in sys.argv
|
||||||
@@ -44,32 +49,30 @@ def main():
|
|||||||
update_available = is_update_available()
|
update_available = is_update_available()
|
||||||
if update_available:
|
if update_available:
|
||||||
update_repository()
|
update_repository()
|
||||||
|
|
||||||
# Start preloading apps in the background
|
# Start preloading apps in the background
|
||||||
preload_thread = threading.Thread(target=preload_apps, daemon=True)
|
preload_thread = threading.Thread(target=preload_apps, daemon=True)
|
||||||
preload_thread.start()
|
preload_thread.start()
|
||||||
|
|
||||||
dukto_handler = DuktoProtocol()
|
dukto_handler = DuktoProtocol()
|
||||||
dukto_handler.set_ports(
|
dukto_handler.set_ports(
|
||||||
udp_port=config.get("dukto_udp_port", 4644),
|
udp_port=config.get("dukto_udp_port", 4644),
|
||||||
tcp_port=config.get("dukto_tcp_port", 4644)
|
tcp_port=config.get("dukto_tcp_port", 4644),
|
||||||
)
|
)
|
||||||
|
|
||||||
pet = MainWindow(
|
pet = MainWindow(
|
||||||
dukto_handler=dukto_handler,
|
dukto_handler=dukto_handler,
|
||||||
strings=strings,
|
strings=strings,
|
||||||
config=config,
|
config=config,
|
||||||
restart=restart,
|
restart=restart,
|
||||||
no_quit=no_quit,
|
no_quit=no_quit,
|
||||||
)
|
)
|
||||||
|
|
||||||
if config.get("discord_presence", True):
|
if config.get("discord_presence", True):
|
||||||
presence.start()
|
presence.start()
|
||||||
|
|
||||||
dukto_handler.initialize()
|
dukto_handler.initialize()
|
||||||
dukto_handler.say_hello()
|
dukto_handler.say_hello()
|
||||||
|
|
||||||
pet.position_bottom_right()
|
|
||||||
|
|
||||||
pet.show()
|
pet.show()
|
||||||
|
|
||||||
@@ -79,4 +82,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
from PySide6 import QtCore, QtGui, QtWidgets
|
|
||||||
from pathlib import Path
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from pynput import keyboard
|
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from core.updater import update_repository, is_update_available
|
from pynput import keyboard
|
||||||
|
from PySide6 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
from core.config import Config
|
||||||
from core.discord_presence import presence
|
from core.discord_presence import presence
|
||||||
from core.dukto import Peer
|
from core.dukto import Peer
|
||||||
from core.file_search import find
|
from core.file_search import find
|
||||||
from core.web_search import MullvadLetaWrapper
|
|
||||||
from core.http_share import FileShareServer
|
from core.http_share import FileShareServer
|
||||||
from core.config import Config
|
from core.updater import is_update_available, update_repository
|
||||||
from core.wayland_utils import is_wayland, get_screen_info
|
from core.web_search import MullvadLetaWrapper
|
||||||
|
|
||||||
from windows.app_launcher import AppLauncherDialog
|
from windows.app_launcher import AppLauncherDialog
|
||||||
from windows.file_search import FileSearchResults
|
|
||||||
from windows.web_results import WebSearchResults
|
|
||||||
from windows.text_viewer import TextViewerDialog
|
|
||||||
from windows.calculator import CalculatorDialog
|
from windows.calculator import CalculatorDialog
|
||||||
from windows.config_window import ConfigWindow
|
from windows.config_window import ConfigWindow
|
||||||
|
from windows.file_search import FileSearchResults
|
||||||
|
from windows.text_viewer import TextViewerDialog
|
||||||
|
from windows.web_results import WebSearchResults
|
||||||
|
|
||||||
ASSET = Path(__file__).parent.parent / "assets" / "2ktan.png"
|
ASSET = Path(__file__).parent.parent / "assets" / "2ktan.png"
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QtWidgets.QMainWindow):
|
class MainWindow(QtWidgets.QMainWindow):
|
||||||
show_menu_signal = QtCore.Signal()
|
show_menu_signal = QtCore.Signal()
|
||||||
|
|
||||||
# Dukto signals
|
# Dukto signals
|
||||||
peer_added_signal = QtCore.Signal(Peer)
|
peer_added_signal = QtCore.Signal(Peer)
|
||||||
peer_removed_signal = QtCore.Signal(Peer)
|
peer_removed_signal = QtCore.Signal(Peer)
|
||||||
@@ -36,78 +36,82 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
send_start_signal = QtCore.Signal(str)
|
send_start_signal = QtCore.Signal(str)
|
||||||
send_complete_signal = QtCore.Signal(list)
|
send_complete_signal = QtCore.Signal(list)
|
||||||
dukto_error_signal = QtCore.Signal(str)
|
dukto_error_signal = QtCore.Signal(str)
|
||||||
|
|
||||||
# HTTP share signals
|
# HTTP share signals
|
||||||
http_download_signal = QtCore.Signal(str, str)
|
http_download_signal = QtCore.Signal(str, str)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
def __init__(self, dukto_handler, strings, config: Config, restart=False, no_quit=False):
|
self, dukto_handler, strings, config: Config, restart=False, no_quit=False
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.config = config
|
self.config = config
|
||||||
self.listener = None
|
self.listener = None
|
||||||
self.is_wayland = is_wayland()
|
|
||||||
|
|
||||||
self.restart = restart
|
self.restart = restart
|
||||||
self.no_quit = no_quit
|
self.no_quit = no_quit
|
||||||
|
|
||||||
# Configure window flags based on display server
|
flags = (
|
||||||
if self.is_wayland:
|
QtCore.Qt.FramelessWindowHint # type: ignore
|
||||||
# Wayland-compatible flags
|
| QtCore.Qt.WindowStaysOnTopHint # type: ignore
|
||||||
flags = (
|
| QtCore.Qt.Tool # type: ignore
|
||||||
QtCore.Qt.FramelessWindowHint #type: ignore
|
| QtCore.Qt.WindowDoesNotAcceptFocus # type: ignore
|
||||||
| QtCore.Qt.WindowStaysOnTopHint #type: ignore
|
)
|
||||||
| QtCore.Qt.Tool #type: ignore
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# X11 flags
|
|
||||||
flags = (
|
|
||||||
QtCore.Qt.FramelessWindowHint #type: ignore
|
|
||||||
| QtCore.Qt.WindowStaysOnTopHint #type: ignore
|
|
||||||
| QtCore.Qt.Tool #type: ignore
|
|
||||||
| QtCore.Qt.WindowDoesNotAcceptFocus #type: ignore
|
|
||||||
)
|
|
||||||
|
|
||||||
self.setWindowFlags(flags)
|
self.setWindowFlags(flags)
|
||||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground) #type: ignore
|
self.setAttribute(QtCore.Qt.WA_TranslucentBackground) # type: ignore
|
||||||
|
|
||||||
# On Wayland, set additional window properties to prevent Alt+Tab
|
# Load the image
|
||||||
if self.is_wayland:
|
|
||||||
self.setAttribute(QtCore.Qt.WA_X11NetWmWindowTypeUtility) #type: ignore
|
|
||||||
|
|
||||||
pix = QtGui.QPixmap(str(ASSET))
|
pix = QtGui.QPixmap(str(ASSET))
|
||||||
|
self.image_size = pix.size()
|
||||||
|
|
||||||
|
# Create a label for the image
|
||||||
self.label = QtWidgets.QLabel(self)
|
self.label = QtWidgets.QLabel(self)
|
||||||
self.label.setPixmap(pix)
|
self.label.setPixmap(pix)
|
||||||
self.label.resize(pix.size())
|
self.label.resize(pix.size())
|
||||||
self.resize(pix.size())
|
|
||||||
|
|
||||||
img = pix.toImage()
|
# Install event filter on the image to handle clicks
|
||||||
mask_img = img.createAlphaMask()
|
self.label.installEventFilter(self)
|
||||||
mask = QtGui.QBitmap.fromImage(mask_img)
|
|
||||||
self.setMask(mask)
|
|
||||||
|
|
||||||
self.dukto_handler = dukto_handler
|
self.dukto_handler = dukto_handler
|
||||||
self.progress_dialog = None
|
self.progress_dialog = None
|
||||||
|
|
||||||
# HTTP file sharing
|
# HTTP file sharing
|
||||||
http_port = self.config.get("http_share_port", 8080)
|
http_port = self.config.get("http_share_port", 8080)
|
||||||
self.http_share = FileShareServer(port=http_port)
|
self.http_share = FileShareServer(port=http_port)
|
||||||
self.http_share.on_download = lambda filename, ip: self.http_download_signal.emit(filename, ip)
|
self.http_share.on_download = (
|
||||||
|
lambda filename, ip: self.http_download_signal.emit(filename, ip)
|
||||||
|
)
|
||||||
|
|
||||||
# Connect Dukto callbacks to emit signals
|
# Connect Dukto callbacks to emit signals
|
||||||
self.dukto_handler.on_peer_added = lambda peer: self.peer_added_signal.emit(peer)
|
self.dukto_handler.on_peer_added = lambda peer: self.peer_added_signal.emit(
|
||||||
self.dukto_handler.on_peer_removed = lambda peer: self.peer_removed_signal.emit(peer)
|
peer
|
||||||
self.dukto_handler.on_receive_request = lambda ip: self.receive_request_signal.emit(ip)
|
)
|
||||||
self.dukto_handler.on_transfer_progress = lambda total, rec: self.progress_update_signal.emit(total, rec)
|
self.dukto_handler.on_peer_removed = lambda peer: self.peer_removed_signal.emit(
|
||||||
self.dukto_handler.on_receive_start = lambda ip: self.receive_start_signal.emit(ip)
|
peer
|
||||||
self.dukto_handler.on_receive_complete = lambda files, size: self.receive_complete_signal.emit(files, size)
|
)
|
||||||
self.dukto_handler.on_receive_text = lambda text, size: self.receive_text_signal.emit(text, size)
|
self.dukto_handler.on_receive_request = (
|
||||||
|
lambda ip: self.receive_request_signal.emit(ip)
|
||||||
|
)
|
||||||
|
self.dukto_handler.on_transfer_progress = (
|
||||||
|
lambda total, rec: self.progress_update_signal.emit(total, rec)
|
||||||
|
)
|
||||||
|
self.dukto_handler.on_receive_start = lambda ip: self.receive_start_signal.emit(
|
||||||
|
ip
|
||||||
|
)
|
||||||
|
self.dukto_handler.on_receive_complete = (
|
||||||
|
lambda files, size: self.receive_complete_signal.emit(files, size)
|
||||||
|
)
|
||||||
|
self.dukto_handler.on_receive_text = (
|
||||||
|
lambda text, size: self.receive_text_signal.emit(text, size)
|
||||||
|
)
|
||||||
self.dukto_handler.on_send_start = lambda ip: self.send_start_signal.emit(ip)
|
self.dukto_handler.on_send_start = lambda ip: self.send_start_signal.emit(ip)
|
||||||
self.dukto_handler.on_send_complete = lambda files: self.send_complete_signal.emit(files)
|
self.dukto_handler.on_send_complete = (
|
||||||
|
lambda files: self.send_complete_signal.emit(files)
|
||||||
|
)
|
||||||
self.dukto_handler.on_error = lambda msg: self.dukto_error_signal.emit(msg)
|
self.dukto_handler.on_error = lambda msg: self.dukto_error_signal.emit(msg)
|
||||||
|
|
||||||
# Connect signals to GUI slots
|
# Connect signals to GUI slots
|
||||||
self.peer_added_signal.connect(self.update_peer_menus)
|
self.peer_added_signal.connect(self.update_peer_menus)
|
||||||
self.peer_removed_signal.connect(self.update_peer_menus)
|
self.peer_removed_signal.connect(self.update_peer_menus)
|
||||||
@@ -123,9 +127,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
self.tray = QtWidgets.QSystemTrayIcon(self)
|
self.tray = QtWidgets.QSystemTrayIcon(self)
|
||||||
self.tray.setIcon(QtGui.QIcon(str(ASSET)))
|
self.tray.setIcon(QtGui.QIcon(str(ASSET)))
|
||||||
|
|
||||||
self.build_menus()
|
self.build_menus()
|
||||||
|
|
||||||
# always on top timer
|
# always on top timer
|
||||||
self.stay_on_top_timer = QtCore.QTimer(self)
|
self.stay_on_top_timer = QtCore.QTimer(self)
|
||||||
self.stay_on_top_timer.timeout.connect(self.ensure_on_top)
|
self.stay_on_top_timer.timeout.connect(self.ensure_on_top)
|
||||||
@@ -133,24 +137,47 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
# Super key
|
# Super key
|
||||||
self.show_menu_signal.connect(self.show_menu)
|
self.show_menu_signal.connect(self.show_menu)
|
||||||
if config.get("hotkey") != None and config.get("hotkey") != "none" and config.get("hotkey") != "":
|
if (
|
||||||
|
config.get("hotkey") is not None
|
||||||
|
and config.get("hotkey") != "none"
|
||||||
|
and config.get("hotkey") != ""
|
||||||
|
):
|
||||||
self.start_hotkey_listener()
|
self.start_hotkey_listener()
|
||||||
|
|
||||||
def position_bottom_right(self):
|
def showEvent(self, event):
|
||||||
"""Position window at bottom right corner."""
|
super().showEvent(event)
|
||||||
if self.is_wayland:
|
screen = QtWidgets.QApplication.primaryScreen()
|
||||||
# On Wayland, get screen info and position window
|
screen_geometry = screen.availableGeometry()
|
||||||
screen_w, screen_h = get_screen_info()
|
self.setGeometry(screen_geometry)
|
||||||
x = screen_w - self.width()
|
|
||||||
y = screen_h - self.height()
|
# Position the image bottom right
|
||||||
self.move(x, y)
|
label_x = screen_geometry.width() - self.image_size.width()
|
||||||
else:
|
label_y = screen_geometry.height() - self.image_size.height()
|
||||||
# On X11, use Qt's screen geometry
|
self.label.move(label_x, label_y)
|
||||||
screen_geometry = QtWidgets.QApplication.primaryScreen().availableGeometry()
|
|
||||||
pet_geometry = self.frameGeometry()
|
# Create a mask for the window based on the image position
|
||||||
x = screen_geometry.width() - pet_geometry.width()
|
self.update_mask()
|
||||||
y = screen_geometry.height() - pet_geometry.height()
|
|
||||||
self.move(x, y)
|
self.raise_()
|
||||||
|
|
||||||
|
def update_mask(self):
|
||||||
|
# Create a mask that only includes the image area
|
||||||
|
mask = QtGui.QRegion(0, 0, 0, 0) # Empty (duh, it says 0)
|
||||||
|
|
||||||
|
# Add the image region
|
||||||
|
image_rect = self.label.geometry()
|
||||||
|
|
||||||
|
pixmap = self.label.pixmap()
|
||||||
|
if pixmap and not pixmap.isNull():
|
||||||
|
img = pixmap.toImage()
|
||||||
|
alpha_mask = img.createAlphaMask()
|
||||||
|
bitmap_mask = QtGui.QBitmap.fromImage(alpha_mask)
|
||||||
|
|
||||||
|
image_region = QtGui.QRegion(bitmap_mask)
|
||||||
|
image_region.translate(image_rect.x(), image_rect.y())
|
||||||
|
mask = mask.united(image_region)
|
||||||
|
|
||||||
|
self.setMask(mask)
|
||||||
|
|
||||||
def build_menus(self):
|
def build_menus(self):
|
||||||
s = self.strings["main_window"]["right_menu"]
|
s = self.strings["main_window"]["right_menu"]
|
||||||
@@ -160,12 +187,18 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.left_menu.addAction(s["launch_app"], self.start_app_launcher)
|
self.left_menu.addAction(s["launch_app"], self.start_app_launcher)
|
||||||
self.left_menu.addAction(s["search_files"], self.start_file_search)
|
self.left_menu.addAction(s["search_files"], self.start_file_search)
|
||||||
self.left_menu.addAction(s["search_web"], self.start_web_search)
|
self.left_menu.addAction(s["search_web"], self.start_web_search)
|
||||||
self.left_menu.addAction(s.get("calculator", "Calculator"), self.start_calculator)
|
self.left_menu.addAction(
|
||||||
|
s.get("calculator", "Calculator"), self.start_calculator
|
||||||
|
)
|
||||||
self.left_menu.addSeparator()
|
self.left_menu.addSeparator()
|
||||||
share_menu_left = self.left_menu.addMenu(s["share_menu"])
|
share_menu_left = self.left_menu.addMenu(s["share_menu"])
|
||||||
self.share_files_submenu_left = share_menu_left.addMenu(s["share_files_submenu"])
|
self.share_files_submenu_left = share_menu_left.addMenu(
|
||||||
|
s["share_files_submenu"]
|
||||||
|
)
|
||||||
self.share_text_submenu_left = share_menu_left.addMenu(s["share_text_submenu"])
|
self.share_text_submenu_left = share_menu_left.addMenu(s["share_text_submenu"])
|
||||||
self.stop_share_action_left = share_menu_left.addAction("Stop Browser Share", self.stop_browser_share)
|
self.stop_share_action_left = share_menu_left.addAction(
|
||||||
|
"Stop Browser Share", self.stop_browser_share
|
||||||
|
)
|
||||||
self.left_menu.addSeparator()
|
self.left_menu.addSeparator()
|
||||||
|
|
||||||
# RIGHT MENU (Tray icon)
|
# RIGHT MENU (Tray icon)
|
||||||
@@ -176,30 +209,36 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
right_menu.addAction(s.get("calculator", "Calculator"), self.start_calculator)
|
right_menu.addAction(s.get("calculator", "Calculator"), self.start_calculator)
|
||||||
right_menu.addSeparator()
|
right_menu.addSeparator()
|
||||||
share_menu_right = right_menu.addMenu(s["share_menu"])
|
share_menu_right = right_menu.addMenu(s["share_menu"])
|
||||||
self.share_files_submenu_right = share_menu_right.addMenu(s["share_files_submenu"])
|
self.share_files_submenu_right = share_menu_right.addMenu(
|
||||||
self.share_text_submenu_right = share_menu_right.addMenu(s["share_text_submenu"])
|
s["share_files_submenu"]
|
||||||
self.stop_share_action_right = share_menu_right.addAction("Stop Browser Share", self.stop_browser_share)
|
)
|
||||||
|
self.share_text_submenu_right = share_menu_right.addMenu(
|
||||||
|
s["share_text_submenu"]
|
||||||
|
)
|
||||||
|
self.stop_share_action_right = share_menu_right.addAction(
|
||||||
|
"Stop Browser Share", self.stop_browser_share
|
||||||
|
)
|
||||||
right_menu.addSeparator()
|
right_menu.addSeparator()
|
||||||
right_menu.addAction(s.get("settings", "Settings"), self.start_config_window)
|
right_menu.addAction(s.get("settings", "Settings"), self.start_config_window)
|
||||||
right_menu.addAction(s["check_updates"], self.update_git)
|
right_menu.addAction(s["check_updates"], self.update_git)
|
||||||
|
|
||||||
if self.restart:
|
if self.restart:
|
||||||
right_menu.addAction(s["restart"], self.restart_application)
|
right_menu.addAction(s["restart"], self.restart_application)
|
||||||
right_menu.addAction(s["toggle_visibility"], self.toggle_visible)
|
right_menu.addAction(s["toggle_visibility"], self.toggle_visible)
|
||||||
right_menu.addSeparator()
|
right_menu.addSeparator()
|
||||||
if not self.no_quit:
|
if not self.no_quit:
|
||||||
right_menu.addAction(s["quit"], QtWidgets.QApplication.quit)
|
right_menu.addAction(s["quit"], QtWidgets.QApplication.quit)
|
||||||
|
|
||||||
self.tray.setContextMenu(right_menu)
|
self.tray.setContextMenu(right_menu)
|
||||||
self.tray.activated.connect(self.handle_tray_activated)
|
self.tray.activated.connect(self.handle_tray_activated)
|
||||||
self.tray.show()
|
self.tray.show()
|
||||||
|
|
||||||
self.update_peer_menus()
|
self.update_peer_menus()
|
||||||
self.update_share_menu_state()
|
self.update_share_menu_state()
|
||||||
|
|
||||||
def update_share_menu_state(self):
|
def update_share_menu_state(self):
|
||||||
s_menu = self.strings["main_window"]["right_menu"]
|
s_menu = self.strings["main_window"]["right_menu"]
|
||||||
|
|
||||||
is_sharing = self.http_share.is_running()
|
is_sharing = self.http_share.is_running()
|
||||||
has_shared_files = bool(self.http_share.shared_files)
|
has_shared_files = bool(self.http_share.shared_files)
|
||||||
has_shared_text = bool(self.http_share.shared_text)
|
has_shared_text = bool(self.http_share.shared_text)
|
||||||
@@ -211,34 +250,43 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
# Configure file share menus
|
# Configure file share menus
|
||||||
for menu in [self.share_files_submenu_left, self.share_files_submenu_right]:
|
for menu in [self.share_files_submenu_left, self.share_files_submenu_right]:
|
||||||
for action in menu.actions():
|
for action in menu.actions():
|
||||||
if hasattr(action, 'is_browser_action'):
|
if hasattr(action, "is_browser_action"):
|
||||||
menu.removeAction(action)
|
menu.removeAction(action)
|
||||||
|
|
||||||
action_text = "Add File(s)..." if has_shared_files else s_menu["via_browser"]
|
action_text = (
|
||||||
|
"Add File(s)..." if has_shared_files else s_menu["via_browser"]
|
||||||
|
)
|
||||||
browser_action = menu.addAction(action_text)
|
browser_action = menu.addAction(action_text)
|
||||||
browser_action.is_browser_action = True
|
browser_action.is_browser_action = True
|
||||||
browser_action.triggered.connect(self.start_file_share_browser)
|
browser_action.triggered.connect(self.start_file_share_browser)
|
||||||
|
|
||||||
if any(not a.isSeparator() and not hasattr(a, 'is_browser_action') for a in menu.actions()):
|
if any(
|
||||||
|
not a.isSeparator() and not hasattr(a, "is_browser_action")
|
||||||
|
for a in menu.actions()
|
||||||
|
):
|
||||||
if not any(a.isSeparator() for a in menu.actions()):
|
if not any(a.isSeparator() for a in menu.actions()):
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
# Configure text share menus
|
# Configure text share menus
|
||||||
for menu in [self.share_text_submenu_left, self.share_text_submenu_right]:
|
for menu in [self.share_text_submenu_left, self.share_text_submenu_right]:
|
||||||
for action in menu.actions():
|
for action in menu.actions():
|
||||||
if hasattr(action, 'is_browser_action'):
|
if hasattr(action, "is_browser_action"):
|
||||||
menu.removeAction(action)
|
menu.removeAction(action)
|
||||||
|
|
||||||
action_text = "Change Shared Text..." if has_shared_text else s_menu["via_browser"]
|
action_text = (
|
||||||
|
"Change Shared Text..." if has_shared_text else s_menu["via_browser"]
|
||||||
|
)
|
||||||
browser_action = menu.addAction(action_text)
|
browser_action = menu.addAction(action_text)
|
||||||
browser_action.is_browser_action = True
|
browser_action.is_browser_action = True
|
||||||
browser_action.triggered.connect(self.start_text_share_browser)
|
browser_action.triggered.connect(self.start_text_share_browser)
|
||||||
|
|
||||||
if any(not a.isSeparator() and not hasattr(a, 'is_browser_action') for a in menu.actions()):
|
if any(
|
||||||
|
not a.isSeparator() and not hasattr(a, "is_browser_action")
|
||||||
|
for a in menu.actions()
|
||||||
|
):
|
||||||
if not any(a.isSeparator() for a in menu.actions()):
|
if not any(a.isSeparator() for a in menu.actions()):
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
|
|
||||||
def show_menu(self):
|
def show_menu(self):
|
||||||
self.left_menu.popup(QtGui.QCursor.pos())
|
self.left_menu.popup(QtGui.QCursor.pos())
|
||||||
|
|
||||||
@@ -256,17 +304,19 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
keys = [key_map.get(k.strip().lower(), k.strip().lower()) for k in hotkey_str.split('+')]
|
keys = [
|
||||||
formatted_hotkey = '<' + '>+<'.join(keys) + '>' #type: ignore
|
key_map.get(k.strip().lower(), k.strip().lower())
|
||||||
|
for k in hotkey_str.split("+")
|
||||||
|
]
|
||||||
|
formatted_hotkey = "<" + ">+<".join(keys) + ">" # type: ignore
|
||||||
|
|
||||||
hotkey = keyboard.HotKey(
|
hotkey = keyboard.HotKey(
|
||||||
keyboard.HotKey.parse(formatted_hotkey),
|
keyboard.HotKey.parse(formatted_hotkey), on_activate
|
||||||
on_activate
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.listener = keyboard.Listener(
|
self.listener = keyboard.Listener(
|
||||||
on_press=hotkey.press, #type: ignore
|
on_press=hotkey.press, # type: ignore
|
||||||
on_release=hotkey.release #type: ignore
|
on_release=hotkey.release, # type: ignore
|
||||||
)
|
)
|
||||||
self.listener.start()
|
self.listener.start()
|
||||||
|
|
||||||
@@ -282,23 +332,27 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|
||||||
def ensure_on_top(self):
|
def ensure_on_top(self):
|
||||||
if self.isVisible() and not self.left_menu.isVisible() and not self.tray.contextMenu().isVisible():
|
if (
|
||||||
|
self.isVisible()
|
||||||
|
and not self.left_menu.isVisible()
|
||||||
|
and not self.tray.contextMenu().isVisible()
|
||||||
|
):
|
||||||
self.raise_()
|
self.raise_()
|
||||||
# Re-apply window flags to ensure staying on top on Wayland
|
|
||||||
if self.is_wayland:
|
|
||||||
self.setWindowFlag(QtCore.Qt.WindowStaysOnTopHint, True) #type: ignore
|
|
||||||
|
|
||||||
def showEvent(self, event):
|
def eventFilter(self, obj, event): # type: ignore
|
||||||
super().showEvent(event)
|
# Handle mouse events on the image
|
||||||
self.raise_()
|
if obj == self.label:
|
||||||
# Ensure bottom right positioning
|
if event.type() == QtCore.QEvent.MouseButtonPress: # type: ignore
|
||||||
QtCore.QTimer.singleShot(100, self.position_bottom_right)
|
if event.button() == QtCore.Qt.LeftButton: # type: ignore
|
||||||
|
self.left_menu.popup(event.globalPosition().toPoint())
|
||||||
|
return True
|
||||||
|
elif event.button() == QtCore.Qt.RightButton: # type: ignore
|
||||||
|
self.tray.contextMenu().popup(event.globalPosition().toPoint())
|
||||||
|
return True
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
def mousePressEvent(self, event: QtGui.QMouseEvent):
|
def mousePressEvent(self, event: QtGui.QMouseEvent):
|
||||||
if event.button() == QtCore.Qt.LeftButton: #type: ignore
|
pass
|
||||||
self.left_menu.popup(event.globalPosition().toPoint())
|
|
||||||
elif event.button() == QtCore.Qt.RightButton: #type: ignore
|
|
||||||
self.tray.contextMenu().popup(event.globalPosition().toPoint())
|
|
||||||
|
|
||||||
def handle_tray_activated(self, reason):
|
def handle_tray_activated(self, reason):
|
||||||
if reason == QtWidgets.QSystemTrayIcon.ActivationReason.Trigger:
|
if reason == QtWidgets.QSystemTrayIcon.ActivationReason.Trigger:
|
||||||
@@ -310,76 +364,116 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
def update_peer_menus(self):
|
def update_peer_menus(self):
|
||||||
s_main = self.strings["main_window"]
|
s_main = self.strings["main_window"]
|
||||||
no_peers_str = s_main["no_peers"]
|
no_peers_str = s_main["no_peers"]
|
||||||
|
|
||||||
peers = list(self.dukto_handler.peers.values())
|
peers = list(self.dukto_handler.peers.values())
|
||||||
|
|
||||||
for menu in [self.share_files_submenu_left, self.share_files_submenu_right, self.share_text_submenu_left, self.share_text_submenu_right]:
|
for menu in [
|
||||||
actions_to_remove = [a for a in menu.actions() if not a.isSeparator() and not hasattr(a, 'is_browser_action')]
|
self.share_files_submenu_left,
|
||||||
|
self.share_files_submenu_right,
|
||||||
|
self.share_text_submenu_left,
|
||||||
|
self.share_text_submenu_right,
|
||||||
|
]:
|
||||||
|
actions_to_remove = [
|
||||||
|
a
|
||||||
|
for a in menu.actions()
|
||||||
|
if not a.isSeparator() and not hasattr(a, "is_browser_action")
|
||||||
|
]
|
||||||
for action in actions_to_remove:
|
for action in actions_to_remove:
|
||||||
menu.removeAction(action)
|
menu.removeAction(action)
|
||||||
|
|
||||||
if not peers:
|
if not peers:
|
||||||
for menu in [self.share_files_submenu_left, self.share_files_submenu_right, self.share_text_submenu_left, self.share_text_submenu_right]:
|
for menu in [
|
||||||
|
self.share_files_submenu_left,
|
||||||
|
self.share_files_submenu_right,
|
||||||
|
self.share_text_submenu_left,
|
||||||
|
self.share_text_submenu_right,
|
||||||
|
]:
|
||||||
action = menu.addAction(no_peers_str)
|
action = menu.addAction(no_peers_str)
|
||||||
action.setEnabled(False)
|
action.setEnabled(False)
|
||||||
else:
|
else:
|
||||||
for peer in sorted(peers, key=lambda p: p.signature):
|
for peer in sorted(peers, key=lambda p: p.signature):
|
||||||
for files_menu, text_menu in [(self.share_files_submenu_left, self.share_text_submenu_left), (self.share_files_submenu_right, self.share_text_submenu_right)]:
|
for files_menu, text_menu in [
|
||||||
|
(self.share_files_submenu_left, self.share_text_submenu_left),
|
||||||
|
(self.share_files_submenu_right, self.share_text_submenu_right),
|
||||||
|
]:
|
||||||
file_action = files_menu.addAction(peer.signature)
|
file_action = files_menu.addAction(peer.signature)
|
||||||
text_action = text_menu.addAction(peer.signature)
|
text_action = text_menu.addAction(peer.signature)
|
||||||
file_action.triggered.connect(lambda checked=False, p=peer: self.start_file_send(p))
|
file_action.triggered.connect(
|
||||||
text_action.triggered.connect(lambda checked=False, p=peer: self.start_text_send(p))
|
lambda checked=False, p=peer: self.start_file_send(p)
|
||||||
|
)
|
||||||
|
text_action.triggered.connect(
|
||||||
|
lambda checked=False, p=peer: self.start_text_send(p)
|
||||||
|
)
|
||||||
|
|
||||||
self.update_share_menu_state()
|
self.update_share_menu_state()
|
||||||
|
|
||||||
def start_file_send(self, peer: Peer):
|
def start_file_send(self, peer: Peer):
|
||||||
dialog_title = self.strings["main_window"]["send_files_dialog_title"].format(peer_signature=peer.signature)
|
dialog_title = self.strings["main_window"]["send_files_dialog_title"].format(
|
||||||
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, dialog_title, str(Path.home()))
|
peer_signature=peer.signature
|
||||||
|
)
|
||||||
|
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(
|
||||||
|
self, dialog_title, str(Path.home())
|
||||||
|
)
|
||||||
if file_paths:
|
if file_paths:
|
||||||
self.dukto_handler.send_file(peer.address, file_paths, peer.port)
|
self.dukto_handler.send_file(peer.address, file_paths, peer.port)
|
||||||
|
|
||||||
def start_text_send(self, peer: Peer):
|
def start_text_send(self, peer: Peer):
|
||||||
dialog_title = self.strings["main_window"]["send_text_dialog_title"].format(peer_signature=peer.signature)
|
dialog_title = self.strings["main_window"]["send_text_dialog_title"].format(
|
||||||
|
peer_signature=peer.signature
|
||||||
|
)
|
||||||
dialog_label = self.strings["main_window"]["send_text_dialog_label"]
|
dialog_label = self.strings["main_window"]["send_text_dialog_label"]
|
||||||
text, ok = QtWidgets.QInputDialog.getMultiLineText(self, dialog_title, dialog_label)
|
text, ok = QtWidgets.QInputDialog.getMultiLineText(
|
||||||
|
self, dialog_title, dialog_label
|
||||||
|
)
|
||||||
if ok and text:
|
if ok and text:
|
||||||
self.dukto_handler.send_text(peer.address, text, peer.port)
|
self.dukto_handler.send_text(peer.address, text, peer.port)
|
||||||
|
|
||||||
def start_file_share_browser(self):
|
def start_file_share_browser(self):
|
||||||
s = self.strings["main_window"]
|
s = self.strings["main_window"]
|
||||||
is_adding = bool(self.http_share.shared_files)
|
is_adding = bool(self.http_share.shared_files)
|
||||||
|
|
||||||
dialog_title = "Select files to add" if is_adding else s["share_browser_dialog_title"]
|
dialog_title = (
|
||||||
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(self, dialog_title, str(Path.home()))
|
"Select files to add" if is_adding else s["share_browser_dialog_title"]
|
||||||
|
)
|
||||||
|
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(
|
||||||
|
self, dialog_title, str(Path.home())
|
||||||
|
)
|
||||||
|
|
||||||
if not file_paths:
|
if not file_paths:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if is_adding:
|
if is_adding:
|
||||||
self.http_share.add_files(file_paths)
|
self.http_share.add_files(file_paths)
|
||||||
self.tray.showMessage("Files Added", f"{len(file_paths)} file(s) added to the share.", QtWidgets.QSystemTrayIcon.Information, 2000) #type: ignore
|
self.tray.showMessage(
|
||||||
|
"Files Added",
|
||||||
|
f"{len(file_paths)} file(s) added to the share.",
|
||||||
|
QtWidgets.QSystemTrayIcon.Information, # type:ignore
|
||||||
|
2000,
|
||||||
|
) # type: ignore
|
||||||
else:
|
else:
|
||||||
url = self.http_share.share_files(file_paths)
|
url = self.http_share.share_files(file_paths)
|
||||||
main_text = s["share_browser_text_files"]
|
main_text = s["share_browser_text_files"]
|
||||||
info_text = s["share_browser_files_info"].format(count=len(file_paths))
|
info_text = s["share_browser_files_info"].format(count=len(file_paths))
|
||||||
if not self.http_share.shared_text:
|
if not self.http_share.shared_text:
|
||||||
self._show_sharing_dialog(url, main_text, info_text)
|
self._show_sharing_dialog(url, main_text, info_text)
|
||||||
|
|
||||||
self.update_share_menu_state()
|
self.update_share_menu_state()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QtWidgets.QMessageBox.critical(self, s["share_error_title"], s["share_error_text"].format(error=str(e)))
|
QtWidgets.QMessageBox.critical(
|
||||||
|
self, s["share_error_title"], s["share_error_text"].format(error=str(e))
|
||||||
|
)
|
||||||
|
|
||||||
def start_text_share_browser(self):
|
def start_text_share_browser(self):
|
||||||
s = self.strings["main_window"]
|
s = self.strings["main_window"]
|
||||||
is_changing = bool(self.http_share.shared_text)
|
is_changing = bool(self.http_share.shared_text)
|
||||||
|
|
||||||
text, ok = QtWidgets.QInputDialog.getMultiLineText(
|
text, ok = QtWidgets.QInputDialog.getMultiLineText(
|
||||||
self,
|
self,
|
||||||
s["share_text_browser_dialog_title"],
|
s["share_text_browser_dialog_title"],
|
||||||
s["share_text_browser_dialog_label"],
|
s["share_text_browser_dialog_label"],
|
||||||
self.http_share.shared_text or ""
|
self.http_share.shared_text or "",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (ok and text):
|
if not (ok and text):
|
||||||
@@ -395,34 +489,46 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
self.update_share_menu_state()
|
self.update_share_menu_state()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QtWidgets.QMessageBox.critical(self, s["share_error_title"], s["share_error_text"].format(error=str(e)))
|
QtWidgets.QMessageBox.critical(
|
||||||
|
self, s["share_error_title"], s["share_error_text"].format(error=str(e))
|
||||||
|
)
|
||||||
|
|
||||||
def stop_browser_share(self):
|
def stop_browser_share(self):
|
||||||
s = self.strings["main_window"]
|
s = self.strings["main_window"]
|
||||||
if self.http_share.is_running():
|
if self.http_share.is_running():
|
||||||
self.http_share.stop()
|
self.http_share.stop()
|
||||||
self.tray.showMessage(s["sharing_stopped_title"], s["sharing_stopped_text"], QtWidgets.QSystemTrayIcon.Information, 2000) #type: ignore
|
self.tray.showMessage(
|
||||||
|
s["sharing_stopped_title"],
|
||||||
|
s["sharing_stopped_text"],
|
||||||
|
QtWidgets.QSystemTrayIcon.Information, # type:ignore
|
||||||
|
2000,
|
||||||
|
) # type: ignore
|
||||||
self.update_share_menu_state()
|
self.update_share_menu_state()
|
||||||
|
|
||||||
def _show_sharing_dialog(self, url: str, main_text: str, info_text: str):
|
def _show_sharing_dialog(self, url: str, main_text: str, info_text: str):
|
||||||
s = self.strings["main_window"]
|
s = self.strings["main_window"]
|
||||||
msg = QtWidgets.QMessageBox(self)
|
msg = QtWidgets.QMessageBox(self)
|
||||||
msg.setIcon(QtWidgets.QMessageBox.Information) #type: ignore
|
msg.setIcon(QtWidgets.QMessageBox.Information) # type: ignore
|
||||||
msg.setWindowTitle(s["share_browser_title"])
|
msg.setWindowTitle(s["share_browser_title"])
|
||||||
msg.setText(main_text)
|
msg.setText(main_text)
|
||||||
msg.setInformativeText(f"{s['share_browser_url']}:\n\n{url}\n\n{info_text}")
|
msg.setInformativeText(f"{s['share_browser_url']}:\n\n{url}\n\n{info_text}")
|
||||||
|
|
||||||
copy_btn = msg.addButton(s["copy_url"], QtWidgets.QMessageBox.ActionRole) #type: ignore
|
copy_btn = msg.addButton(s["copy_url"], QtWidgets.QMessageBox.ActionRole) # type: ignore
|
||||||
open_btn = msg.addButton(s["open_browser"], QtWidgets.QMessageBox.ActionRole) #type: ignore
|
open_btn = msg.addButton(s["open_browser"], QtWidgets.QMessageBox.ActionRole) # type: ignore
|
||||||
msg.addButton(QtWidgets.QMessageBox.Ok) #type: ignore
|
msg.addButton(QtWidgets.QMessageBox.Ok) # type: ignore
|
||||||
|
|
||||||
msg.exec()
|
msg.exec()
|
||||||
|
|
||||||
clicked = msg.clickedButton()
|
clicked = msg.clickedButton()
|
||||||
if clicked == copy_btn:
|
if clicked == copy_btn:
|
||||||
clipboard = QtWidgets.QApplication.clipboard()
|
clipboard = QtWidgets.QApplication.clipboard()
|
||||||
clipboard.setText(url)
|
clipboard.setText(url)
|
||||||
self.tray.showMessage(s["url_copied_title"], s["url_copied_text"], QtWidgets.QSystemTrayIcon.Information, 2000) #type: ignore
|
self.tray.showMessage(
|
||||||
|
s["url_copied_title"],
|
||||||
|
s["url_copied_text"],
|
||||||
|
QtWidgets.QSystemTrayIcon.Information, # type: ignore
|
||||||
|
2000,
|
||||||
|
) # type: ignore
|
||||||
elif clicked == open_btn:
|
elif clicked == open_btn:
|
||||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
QtGui.QDesktopServices.openUrl(QtCore.QUrl(url))
|
||||||
|
|
||||||
@@ -432,17 +538,20 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.tray.showMessage(
|
self.tray.showMessage(
|
||||||
s["download_notification_title"],
|
s["download_notification_title"],
|
||||||
s["download_notification_text"].format(filename=filename, ip=client_ip),
|
s["download_notification_text"].format(filename=filename, ip=client_ip),
|
||||||
QtWidgets.QSystemTrayIcon.Information, #type: ignore
|
QtWidgets.QSystemTrayIcon.Information, # type: ignore
|
||||||
3000
|
3000,
|
||||||
)
|
)
|
||||||
|
|
||||||
def show_receive_confirmation(self, sender_ip: str):
|
def show_receive_confirmation(self, sender_ip: str):
|
||||||
reply = QtWidgets.QMessageBox.question(
|
reply = QtWidgets.QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
self.strings["main_window"]["receive_confirm_title"],
|
self.strings["main_window"]["receive_confirm_title"],
|
||||||
self.strings["main_window"]["receive_confirm_text"].format(sender_ip=sender_ip),
|
self.strings["main_window"]["receive_confirm_text"].format(
|
||||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
sender_ip=sender_ip
|
||||||
QtWidgets.QMessageBox.StandardButton.No
|
),
|
||||||
|
QtWidgets.QMessageBox.StandardButton.Yes
|
||||||
|
| QtWidgets.QMessageBox.StandardButton.No,
|
||||||
|
QtWidgets.QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||||
self.dukto_handler.approve_transfer()
|
self.dukto_handler.approve_transfer()
|
||||||
@@ -452,17 +561,23 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
@QtCore.Slot(str)
|
@QtCore.Slot(str)
|
||||||
def handle_receive_start(self, sender_ip: str):
|
def handle_receive_start(self, sender_ip: str):
|
||||||
s = self.strings["main_window"]["progress_dialog"]
|
s = self.strings["main_window"]["progress_dialog"]
|
||||||
self.progress_dialog = QtWidgets.QProgressDialog(s["receiving_label"], s["cancel_button"], 0, 100, self)
|
self.progress_dialog = QtWidgets.QProgressDialog(
|
||||||
self.progress_dialog.setWindowTitle(s["receiving_title"].format(sender_ip=sender_ip))
|
s["receiving_label"], s["cancel_button"], 0, 100, self
|
||||||
self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore
|
)
|
||||||
|
self.progress_dialog.setWindowTitle(
|
||||||
|
s["receiving_title"].format(sender_ip=sender_ip)
|
||||||
|
)
|
||||||
|
self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore
|
||||||
self.progress_dialog.show()
|
self.progress_dialog.show()
|
||||||
|
|
||||||
@QtCore.Slot(str)
|
@QtCore.Slot(str)
|
||||||
def handle_send_start(self, dest_ip: str):
|
def handle_send_start(self, dest_ip: str):
|
||||||
s = self.strings["main_window"]["progress_dialog"]
|
s = self.strings["main_window"]["progress_dialog"]
|
||||||
self.progress_dialog = QtWidgets.QProgressDialog(s["sending_label"], s["cancel_button"], 0, 100, self)
|
self.progress_dialog = QtWidgets.QProgressDialog(
|
||||||
|
s["sending_label"], s["cancel_button"], 0, 100, self
|
||||||
|
)
|
||||||
self.progress_dialog.setWindowTitle(s["sending_title"].format(dest_ip=dest_ip))
|
self.progress_dialog.setWindowTitle(s["sending_title"].format(dest_ip=dest_ip))
|
||||||
self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore
|
self.progress_dialog.setWindowModality(QtCore.Qt.WindowModal) # type: ignore
|
||||||
self.progress_dialog.show()
|
self.progress_dialog.show()
|
||||||
|
|
||||||
@QtCore.Slot(int, int)
|
@QtCore.Slot(int, int)
|
||||||
@@ -477,23 +592,28 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.progress_dialog.setValue(total_size)
|
self.progress_dialog.setValue(total_size)
|
||||||
self.progress_dialog.close()
|
self.progress_dialog.close()
|
||||||
self.progress_dialog = None
|
self.progress_dialog = None
|
||||||
|
|
||||||
s = self.strings["main_window"]
|
s = self.strings["main_window"]
|
||||||
QtWidgets.QMessageBox.information(self, s["receive_complete_title"], s["receive_complete_text"].format(count=len(received_files)))
|
QtWidgets.QMessageBox.information(
|
||||||
|
self,
|
||||||
|
s["receive_complete_title"],
|
||||||
|
s["receive_complete_text"].format(count=len(received_files)),
|
||||||
|
)
|
||||||
|
|
||||||
reply = QtWidgets.QMessageBox.question(
|
reply = QtWidgets.QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
s["open_folder_title"],
|
s["open_folder_title"],
|
||||||
s["open_folder_text"],
|
s["open_folder_text"],
|
||||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
|
||||||
QtWidgets.QMessageBox.StandardButton.Yes
|
QtWidgets.QMessageBox.StandardButton.Yes
|
||||||
|
| QtWidgets.QMessageBox.StandardButton.No,
|
||||||
|
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||||
)
|
)
|
||||||
|
|
||||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||||
receive_dir = str(Path.home() / "Received")
|
receive_dir = str(Path.home() / "Received")
|
||||||
url = QtCore.QUrl.fromLocalFile(receive_dir)
|
url = QtCore.QUrl.fromLocalFile(receive_dir)
|
||||||
QtGui.QDesktopServices.openUrl(url)
|
QtGui.QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
@QtCore.Slot(list)
|
@QtCore.Slot(list)
|
||||||
def handle_send_complete(self, sent_files: list):
|
def handle_send_complete(self, sent_files: list):
|
||||||
if self.progress_dialog:
|
if self.progress_dialog:
|
||||||
@@ -501,19 +621,25 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.progress_dialog.setValue(self.progress_dialog.maximum())
|
self.progress_dialog.setValue(self.progress_dialog.maximum())
|
||||||
self.progress_dialog.close()
|
self.progress_dialog.close()
|
||||||
self.progress_dialog = None
|
self.progress_dialog = None
|
||||||
|
|
||||||
s = self.strings["main_window"]
|
s = self.strings["main_window"]
|
||||||
if sent_files and sent_files[0] == "___DUKTO___TEXT___":
|
if sent_files and sent_files[0] == "___DUKTO___TEXT___":
|
||||||
QtWidgets.QMessageBox.information(self, s["send_complete_title"], s["send_complete_text_single"])
|
QtWidgets.QMessageBox.information(
|
||||||
|
self, s["send_complete_title"], s["send_complete_text_single"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
QtWidgets.QMessageBox.information(self, s["send_complete_title"], s["send_complete_text"].format(count=len(sent_files)))
|
QtWidgets.QMessageBox.information(
|
||||||
|
self,
|
||||||
|
s["send_complete_title"],
|
||||||
|
s["send_complete_text"].format(count=len(sent_files)),
|
||||||
|
)
|
||||||
|
|
||||||
@QtCore.Slot(str, int)
|
@QtCore.Slot(str, int)
|
||||||
def handle_receive_text(self, text: str, total_size: int):
|
def handle_receive_text(self, text: str, total_size: int):
|
||||||
if self.progress_dialog:
|
if self.progress_dialog:
|
||||||
self.progress_dialog.close()
|
self.progress_dialog.close()
|
||||||
self.progress_dialog = None
|
self.progress_dialog = None
|
||||||
|
|
||||||
dialog = TextViewerDialog(text, self.strings, self)
|
dialog = TextViewerDialog(text, self.strings, self)
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
|
||||||
@@ -522,7 +648,11 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
if self.progress_dialog:
|
if self.progress_dialog:
|
||||||
self.progress_dialog.close()
|
self.progress_dialog.close()
|
||||||
self.progress_dialog = None
|
self.progress_dialog = None
|
||||||
QtWidgets.QMessageBox.critical(self, self.strings["main_window"]["dukto_error_title"], self.strings["main_window"]["dukto_error_text"].format(error_msg=error_msg))
|
QtWidgets.QMessageBox.critical(
|
||||||
|
self,
|
||||||
|
self.strings["main_window"]["dukto_error_title"],
|
||||||
|
self.strings["main_window"]["dukto_error_text"].format(error_msg=error_msg),
|
||||||
|
)
|
||||||
|
|
||||||
def start_app_launcher(self):
|
def start_app_launcher(self):
|
||||||
self.app_launcher_dialog = AppLauncherDialog(self.strings, self)
|
self.app_launcher_dialog = AppLauncherDialog(self.strings, self)
|
||||||
@@ -544,16 +674,18 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
dialog.setWindowTitle(s["input_title"])
|
dialog.setWindowTitle(s["input_title"])
|
||||||
dialog.setLabelText(s["input_label"])
|
dialog.setLabelText(s["input_label"])
|
||||||
dialog.move(QtGui.QCursor.pos())
|
dialog.move(QtGui.QCursor.pos())
|
||||||
|
|
||||||
ok = dialog.exec()
|
ok = dialog.exec()
|
||||||
pattern = dialog.textValue()
|
pattern = dialog.textValue()
|
||||||
|
|
||||||
if ok and pattern:
|
if ok and pattern:
|
||||||
try:
|
try:
|
||||||
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
|
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
|
||||||
results = find(pattern, root='~')
|
results = find(pattern, root="~")
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
QtWidgets.QMessageBox.critical(self, s["search_error_title"], s["search_error_text"].format(e=e))
|
QtWidgets.QMessageBox.critical(
|
||||||
|
self, s["search_error_title"], s["search_error_text"].format(e=e)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
QtWidgets.QApplication.restoreOverrideCursor()
|
QtWidgets.QApplication.restoreOverrideCursor()
|
||||||
@@ -562,23 +694,37 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
self.results_dialog = FileSearchResults(results, self.strings, self)
|
self.results_dialog = FileSearchResults(results, self.strings, self)
|
||||||
self.results_dialog.show()
|
self.results_dialog.show()
|
||||||
else:
|
else:
|
||||||
reply = QtWidgets.QMessageBox.question(self, s["no_results_title"], s["no_results_home_text"],
|
reply = QtWidgets.QMessageBox.question(
|
||||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, QtWidgets.QMessageBox.StandardButton.No)
|
self,
|
||||||
|
s["no_results_title"],
|
||||||
|
s["no_results_home_text"],
|
||||||
|
QtWidgets.QMessageBox.StandardButton.Yes
|
||||||
|
| QtWidgets.QMessageBox.StandardButton.No,
|
||||||
|
QtWidgets.QMessageBox.StandardButton.No,
|
||||||
|
)
|
||||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||||
try:
|
try:
|
||||||
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
|
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
|
||||||
results = find(pattern, root='/')
|
results = find(pattern, root="/")
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
QtWidgets.QMessageBox.critical(self, s["search_error_title"], s["search_error_text"].format(e=e))
|
QtWidgets.QMessageBox.critical(
|
||||||
|
self,
|
||||||
|
s["search_error_title"],
|
||||||
|
s["search_error_text"].format(e=e),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
QtWidgets.QApplication.restoreOverrideCursor()
|
QtWidgets.QApplication.restoreOverrideCursor()
|
||||||
|
|
||||||
if results:
|
if results:
|
||||||
self.results_dialog = FileSearchResults(results, self.strings, self)
|
self.results_dialog = FileSearchResults(
|
||||||
|
results, self.strings, self
|
||||||
|
)
|
||||||
self.results_dialog.show()
|
self.results_dialog.show()
|
||||||
else:
|
else:
|
||||||
QtWidgets.QMessageBox.information(self, s["no_results_title"], s["no_results_root_text"])
|
QtWidgets.QMessageBox.information(
|
||||||
|
self, s["no_results_title"], s["no_results_root_text"]
|
||||||
|
)
|
||||||
|
|
||||||
def start_web_search(self):
|
def start_web_search(self):
|
||||||
s = self.strings["web_search"]
|
s = self.strings["web_search"]
|
||||||
@@ -589,66 +735,86 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||||||
|
|
||||||
ok = dialog.exec()
|
ok = dialog.exec()
|
||||||
query = dialog.textValue()
|
query = dialog.textValue()
|
||||||
|
|
||||||
if ok and query:
|
if ok and query:
|
||||||
try:
|
try:
|
||||||
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
|
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
|
||||||
search_engine = self.config.get("search_engine", "brave")
|
search_engine = self.config.get("search_engine", "brave")
|
||||||
leta = MullvadLetaWrapper(engine=search_engine)
|
leta = MullvadLetaWrapper(engine=search_engine)
|
||||||
results = leta.search(query)
|
results = leta.search(query)
|
||||||
|
|
||||||
if results and results.get('results'):
|
if results and results.get("results"):
|
||||||
self.web_results_dialog = WebSearchResults(results, self.strings, self)
|
self.web_results_dialog = WebSearchResults(
|
||||||
|
results, self.strings, self
|
||||||
|
)
|
||||||
self.web_results_dialog.show()
|
self.web_results_dialog.show()
|
||||||
else:
|
else:
|
||||||
QtWidgets.QMessageBox.information(self, s["no_results_title"], s["no_results_text"])
|
QtWidgets.QMessageBox.information(
|
||||||
|
self, s["no_results_title"], s["no_results_text"]
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QtWidgets.QMessageBox.critical(self, s["search_error_title"], s["search_error_text"].format(e=e))
|
QtWidgets.QMessageBox.critical(
|
||||||
|
self, s["search_error_title"], s["search_error_text"].format(e=e)
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
QtWidgets.QApplication.restoreOverrideCursor()
|
QtWidgets.QApplication.restoreOverrideCursor()
|
||||||
|
|
||||||
def update_git(self):
|
def update_git(self):
|
||||||
s = self.strings["main_window"]["updater"]
|
s = self.strings["main_window"]["updater"]
|
||||||
|
|
||||||
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
|
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
|
||||||
update_available = is_update_available()
|
update_available = is_update_available()
|
||||||
QtWidgets.QApplication.restoreOverrideCursor()
|
QtWidgets.QApplication.restoreOverrideCursor()
|
||||||
|
|
||||||
if not update_available:
|
if not update_available:
|
||||||
QtWidgets.QMessageBox.information(self, s["no_updates_title"], s["no_updates_text"])
|
QtWidgets.QMessageBox.information(
|
||||||
|
self, s["no_updates_title"], s["no_updates_text"]
|
||||||
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
reply = QtWidgets.QMessageBox.question(self, s["update_available_title"],
|
reply = QtWidgets.QMessageBox.question(
|
||||||
s["update_available_text"],
|
self,
|
||||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
s["update_available_title"],
|
||||||
QtWidgets.QMessageBox.StandardButton.Yes)
|
s["update_available_text"],
|
||||||
|
QtWidgets.QMessageBox.StandardButton.Yes
|
||||||
|
| QtWidgets.QMessageBox.StandardButton.No,
|
||||||
|
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||||
|
)
|
||||||
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
||||||
return
|
return
|
||||||
|
|
||||||
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) #type: ignore
|
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) # type: ignore
|
||||||
status, message = update_repository()
|
status, message = update_repository()
|
||||||
QtWidgets.QApplication.restoreOverrideCursor()
|
QtWidgets.QApplication.restoreOverrideCursor()
|
||||||
|
|
||||||
if status == "UPDATED":
|
if status == "UPDATED":
|
||||||
reply = QtWidgets.QMessageBox.question(self, s["update_success_title"],
|
reply = QtWidgets.QMessageBox.question(
|
||||||
s["update_success_text"].format(message=message),
|
self,
|
||||||
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
s["update_success_title"],
|
||||||
QtWidgets.QMessageBox.StandardButton.Yes)
|
s["update_success_text"].format(message=message),
|
||||||
|
QtWidgets.QMessageBox.StandardButton.Yes
|
||||||
|
| QtWidgets.QMessageBox.StandardButton.No,
|
||||||
|
QtWidgets.QMessageBox.StandardButton.Yes,
|
||||||
|
)
|
||||||
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
|
||||||
self.restart_application()
|
self.restart_application()
|
||||||
|
|
||||||
elif status == "FAILED":
|
elif status == "FAILED":
|
||||||
QtWidgets.QMessageBox.critical(self, s["update_failed_title"], s["update_failed_text"].format(message=message))
|
QtWidgets.QMessageBox.critical(
|
||||||
|
self,
|
||||||
|
s["update_failed_title"],
|
||||||
|
s["update_failed_text"].format(message=message),
|
||||||
|
)
|
||||||
|
|
||||||
def restart_application(self):
|
def restart_application(self):
|
||||||
presence.end()
|
presence.end()
|
||||||
self.dukto_handler.shutdown()
|
self.dukto_handler.shutdown()
|
||||||
if self.http_share.is_running():
|
if self.http_share.is_running():
|
||||||
self.http_share.stop()
|
self.http_share.stop()
|
||||||
|
|
||||||
args = [sys.executable] + sys.argv
|
args = [sys.executable] + sys.argv
|
||||||
|
|
||||||
subprocess.Popen(args)
|
subprocess.Popen(args)
|
||||||
|
|
||||||
QtWidgets.QApplication.quit()
|
QtWidgets.QApplication.quit()
|
||||||
|
|||||||
Reference in New Issue
Block a user