Compare commits
29 Commits
a8d56e5c3c
...
e03c67245f
Author | SHA1 | Date |
---|---|---|
Jaka Perovšek | e03c67245f | |
Jaka Perovšek | 3743dfbdaa | |
Jaka Perovšek | fd0e5ddc3c | |
Jaka Perovšek | 841fd1cbfa | |
Jaka Perovšek | d6bcdb9ecc | |
Jaka Perovšek | a15c9bc220 | |
Jaka Perovšek | 24279524c7 | |
Jaka Perovšek | c31e2f2b88 | |
Jaka Perovšek | ccf1b2b53f | |
Jaka Perovšek | e489737f5f | |
Jaka Perovšek | ed9834ea37 | |
Jaka Perovšek | 4814eacbad | |
Jaka Perovšek | 3036478f1d | |
Jaka Perovšek | a3e6c32ae9 | |
Jaka Perovšek | e8ffff4739 | |
Jaka Perovšek | bdc9270ba8 | |
Jaka Perovšek | ad974b3573 | |
Jaka Perovšek | cc94838ad3 | |
Jaka Perovšek | a40fd85822 | |
Jaka Perovšek | 73ae09c438 | |
Jaka Perovšek | 66cda77233 | |
Jaka Perovšek | 93d7e0dce2 | |
Jaka Perovšek | 2f6b1a8a33 | |
Jaka Perovšek | 2e68a5eaf2 | |
Jaka Perovšek | 6981f710be | |
Jaka Perovšek | 1fce5f2d69 | |
Jaka Perovšek | ca878de953 | |
Jaka Perovšek | fa6e4a7316 | |
Jaka Perovšek | 906f3c073c |
|
@ -8,4 +8,6 @@ build
|
|||
__pycache__
|
||||
utils
|
||||
barotrauma-sunken-tapes
|
||||
barotrauma-sunken-tapes.zip
|
||||
barotrauma-sunken-tapes.zip
|
||||
brainstorming.svg
|
||||
poetry.lock
|
|
@ -92,7 +92,7 @@ def get_ffmpeg_version():
|
|||
ffmpeg_version = fp.text
|
||||
del fp
|
||||
|
||||
return ffmpeg_version
|
||||
return "6.0"
|
||||
|
||||
|
||||
def fetch_and_cut_song(tape, ffmpeg_version):
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
import jinja2 as j2
|
||||
import yaml
|
||||
import shutil
|
||||
import requests
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
import certifi
|
||||
import math
|
||||
import vdf
|
||||
from PIL import Image
|
||||
from mutagen.oggvorbis import OggVorbis
|
||||
from distutils.dir_util import copy_tree
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
|
||||
|
||||
|
||||
def rmfulldir(dirpath):
|
||||
try:
|
||||
shutil.rmtree(dirpath)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def download_via_requests(url_source, file_name):
|
||||
response = requests.get(url_source, stream=True, verify=certifi.where())
|
||||
|
||||
with open(file_name, 'wb') as out_file:
|
||||
shutil.copyfileobj(response.raw, out_file)
|
||||
del response
|
||||
|
||||
|
||||
def get_ffmpeg_version():
|
||||
url_ffmpeg_version = "https://www.gyan.dev/ffmpeg/builds/release-version"
|
||||
|
||||
fp = requests.get(url_ffmpeg_version, verify=certifi.where())
|
||||
ffmpeg_version = fp.text
|
||||
del fp
|
||||
|
||||
return ffmpeg_version
|
||||
|
||||
|
||||
def stream_command(args, *, stdout_handler=logging.info, check=True, text=True,
|
||||
stdout=PIPE, stderr=STDOUT, **kwargs):
|
||||
"""Mimic subprocess.run, while processing the command output in real time."""
|
||||
with Popen(args, text=text, stdout=stdout, stderr=stderr, **kwargs) as process:
|
||||
for line in process.stdout:
|
||||
stdout_handler(line[:-1])
|
||||
return_code = process.poll()
|
||||
if check and return_code:
|
||||
raise CalledProcessError(return_code, process.args)
|
||||
return CompletedProcess(process.args, return_code)
|
||||
|
||||
|
||||
class Deployer:
|
||||
def __init__(self, logging_handler: logging.Handler = None):
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
|
||||
format='[%(asctime)s| %(levelname)s| %(processName)s] %(message)s')
|
||||
|
||||
self.logger = logging.getLogger()
|
||||
|
||||
if logging_handler is not None:
|
||||
self.logger.addHandler(logging_handler)
|
||||
|
||||
self.utils_dir = "./utils"
|
||||
self.build_dir = "./build"
|
||||
self.music_dir = "./build/music"
|
||||
self.source_dir = "./source"
|
||||
self.install_dir = self.find_install_dir()
|
||||
|
||||
def find_install_dir(self):
|
||||
self.logger.info("Trying to find Barotraumma install folder")
|
||||
library_folders = Path("C:\Program Files (x86)\Steam\steamapps\libraryfolders.vdf")
|
||||
game_id = "602960"
|
||||
data = vdf.load(open(library_folders))
|
||||
install_path = ""
|
||||
for (id, folder) in data["libraryfolders"].items():
|
||||
if game_id in folder["apps"]:
|
||||
install_path = Path(folder["path"]) / Path("steamapps\common\Barotrauma")
|
||||
|
||||
self.logger.info(f"Barotrauma is installed in {install_path.as_posix()}")
|
||||
|
||||
return install_path.as_posix()
|
||||
|
||||
def download_ffmpeg(self, clean=False):
|
||||
if clean:
|
||||
rmfulldir(f"{self.utils_dir}/ffmpeg-" + get_ffmpeg_version() + "-full_build")
|
||||
|
||||
url_ffmpeg_source = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z"
|
||||
out_ffmpeg_archive = f"{self.utils_dir}/ffmpeg-release-full.7z.exe"
|
||||
|
||||
self.download_and_extract(url_ffmpeg_source, out_ffmpeg_archive)
|
||||
|
||||
def download_git(self, clean=False):
|
||||
if clean:
|
||||
rmfulldir(f"{self.utils_dir}/git")
|
||||
|
||||
url_git_source = ("https://github.com/git-for-windows/git/releases/download/"
|
||||
"v2.46.2.windows.1/PortableGit-2.46.2-64-bit.7z.exe")
|
||||
out_git_archive = f"{self.utils_dir}/PortableGit-2.46.2-64-bit.7z.exe"
|
||||
|
||||
self.download_and_extract(url_git_source, out_git_archive, subdir="/git")
|
||||
|
||||
def download_and_extract(self, url_source, out_archive, subdir=""):
|
||||
self.logger.info(f"Downloading {url_source}. This may take a while.")
|
||||
|
||||
download_via_requests(url_source, out_archive)
|
||||
time.sleep(0.7)
|
||||
self.logger.info("Download complete.")
|
||||
|
||||
self.logger.info(f"Extracting {out_archive} with 7z")
|
||||
extract = [f"{self.utils_dir}/7z/7za.exe", "x", out_archive, "-o" f"{self.utils_dir}{subdir}"]
|
||||
|
||||
stream_command(extract, stdout_handler=self.logger.info)
|
||||
|
||||
time.sleep(0.7)
|
||||
self.logger.info("Removing " + out_archive)
|
||||
os.remove(out_archive)
|
||||
|
||||
def update(self):
|
||||
self.logger.info(f"Checking for updates via git pull.")
|
||||
pull = [f"{self.utils_dir}/git/bin/git.exe", "pull"]
|
||||
stream_command(pull, stdout_handler=self.logger.info)
|
||||
|
||||
def fetch_and_cut_song(self, tape, ffmpeg_version):
|
||||
python_executable = f"{self.utils_dir}/python-3.9.7-embed-amd64/python.exe"
|
||||
script = "./fetch_song.py"
|
||||
|
||||
if type(tape["source"]) == str:
|
||||
shutil.copy(tape["source"], f"{self.music_dir}/{tape['identifier']}.ogg")
|
||||
else:
|
||||
fetch = [python_executable, script, tape["source"]["url"], "-x", "--audio-format", "vorbis",
|
||||
"--audio-quality", "0", "-o", f"{self.build_dir}/tmp_music/{tape['identifier']}.ogg"]
|
||||
|
||||
# this is done in a separate python script because subprocess.call makes sure that the
|
||||
# download process is finished before trying to cut the song
|
||||
|
||||
stream_command(fetch, stdout_handler=self.logger.info)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
cut = [f"{self.utils_dir}/ffmpeg-" + ffmpeg_version + "-full_build/bin/ffmpeg.exe",
|
||||
"-y", "-ss", f"{tape['source']['start']}",
|
||||
"-i", f"{self.build_dir}/tmp_music/{tape['identifier']}.ogg", "-acodec", "libvorbis",
|
||||
"-ac", "1", "-af", f"volume={tape['source']['volume']}dB",
|
||||
f"{self.music_dir}/{tape['identifier']}.ogg"]
|
||||
|
||||
if tape["source"]["end"] != -1:
|
||||
cut = cut[:-7] + ["-to", f"{tape['source']['end']}"] + cut[-7:]
|
||||
|
||||
try:
|
||||
stream_command(cut, stdout_handler=self.logger.info)
|
||||
except FileNotFoundError:
|
||||
self.logger.error("ffmpeg not in utils directory.")
|
||||
return
|
||||
|
||||
walkman = [f"{self.utils_dir}/ffmpeg-" + ffmpeg_version + "-full_build/bin/ffmpeg.exe",
|
||||
"-i", f"{self.music_dir}/{tape['identifier']}.ogg",
|
||||
"-af", f"highpass=f=300,lowpass=f=12000",
|
||||
f"{self.music_dir}/{tape['identifier']}-walkman.ogg"]
|
||||
|
||||
try:
|
||||
stream_command(walkman, stdout_handler=self.logger.info)
|
||||
except FileNotFoundError:
|
||||
self.logger.error("ffmpeg not in utils directory.")
|
||||
return
|
||||
|
||||
def assemble_png_images(self, tapes, outfile: str, resize=None):
|
||||
img_names = [f"{self.source_dir}/images/{tape['identifier']}.png" for tape in tapes]
|
||||
|
||||
images = [Image.open(x) for x in img_names]
|
||||
|
||||
if resize is not None:
|
||||
for i, (im, tape) in enumerate(zip(images, tapes)):
|
||||
im.thumbnail((128, 82), Image.ANTIALIAS)
|
||||
if resize == (64, 41) and tape["icon_resize"] == "blur":
|
||||
im.thumbnail(resize, Image.ANTIALIAS)
|
||||
else:
|
||||
im.thumbnail(resize, Image.NEAREST)
|
||||
images[i] = im
|
||||
|
||||
widths, heights = zip(*(i.size for i in images))
|
||||
|
||||
columns = int((len(images)) ** 0.5)
|
||||
rows = int(math.ceil(len(images) / columns))
|
||||
|
||||
total_width = max(widths) * columns
|
||||
max_height = max(heights) * rows
|
||||
|
||||
new_im = Image.new('RGBA', (total_width, max_height))
|
||||
|
||||
for i, im in enumerate(images):
|
||||
x_offset = max(widths) * (i % columns)
|
||||
y_offset = max(heights) * math.floor(i / columns)
|
||||
new_im.paste(im, (x_offset, y_offset))
|
||||
|
||||
new_im.save(f"{self.build_dir}/{outfile}.png")
|
||||
|
||||
def prepare_music(self, data):
|
||||
try:
|
||||
os.mkdir(f'{self.music_dir}')
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
ffmpeg_version = get_ffmpeg_version()
|
||||
|
||||
self.logger.info("Downloading and cutting the songs.")
|
||||
for i, tape in enumerate(data):
|
||||
if not (os.path.exists(f"{self.music_dir}/{tape['identifier']}.ogg") or os.path.exists(
|
||||
f"{self.music_dir}/{tape['identifier']}-walkman.ogg")):
|
||||
self.logger.info(f"{i + 1}/{len(data)} Downloading: {tape['name']}")
|
||||
self.fetch_and_cut_song(tape, ffmpeg_version)
|
||||
else:
|
||||
self.logger.info(f"{i + 1}/{len(data)} Already exists: {tape['name']}")
|
||||
|
||||
self.logger.info(f"Removing temporary music folder.")
|
||||
rmfulldir(f"{self.build_dir}/tmp_music/")
|
||||
|
||||
self.logger.info(f"Copying the sound effects to build.")
|
||||
copy_tree(f"{self.source_dir}/sound_effects", f"{self.build_dir}/sound_effects")
|
||||
|
||||
def prepare_images(self, data):
|
||||
self.logger.info(f"Assembling covers and icons into png files.")
|
||||
self.assemble_png_images(data, "covers")
|
||||
self.assemble_png_images(data, "icons", resize=(64, 41))
|
||||
self.assemble_png_images(data, "sprites", resize=(33, 21))
|
||||
|
||||
self.logger.info(f"Copying other images.")
|
||||
shutil.copy(f"{self.source_dir}/images/players_icons.png", f"{self.build_dir}/players_icons.png")
|
||||
shutil.copy(f"{self.source_dir}/images/players_sprites.png", f"{self.build_dir}/players_sprites.png")
|
||||
shutil.copy(f"{self.source_dir}/images/PreviewImage.png", f"{self.build_dir}/PreviewImage.png")
|
||||
|
||||
def build_xml_code(self, data, config):
|
||||
self.logger.info(f"Calculate the value that lets you use the songs n-times.")
|
||||
song_lengths = [OggVorbis(f"{self.music_dir}/{tape['identifier']}.ogg").info.length
|
||||
for tape in data]
|
||||
|
||||
use_lengths = [song_length * n for song_length, n in
|
||||
zip(song_lengths, [tape["no_of_uses"] for tape in data])]
|
||||
|
||||
condition_delta = [f"{1 / use_length:0.5f}" for use_length in use_lengths]
|
||||
affliction_delta = [100 / song_length for song_length in song_lengths]
|
||||
|
||||
columns = int((len(data)) ** 0.5)
|
||||
positions = [{"column": i % columns, "row": math.floor(i / columns)} for i in range(len(data))]
|
||||
|
||||
self.logger.info(f"Creating jinja environment.")
|
||||
# create jinja2 environment
|
||||
j2env = j2.Environment(loader=j2.FileSystemLoader(Path(".")))
|
||||
j2env.globals.update(zip=zip)
|
||||
|
||||
# load the template file
|
||||
template0 = j2env.get_template(f"{self.source_dir}/filelist_template.xml")
|
||||
template1 = j2env.get_template(f"{self.source_dir}/sunken_tapes_template.xml")
|
||||
template2 = j2env.get_template(f"{self.source_dir}/sunken_tapes_style_template.xml")
|
||||
|
||||
self.logger.info(f"Rendering the xml files.")
|
||||
with open(f"{self.build_dir}/filelist.xml", "w+", encoding="utf8") as output_file:
|
||||
# render the template
|
||||
output_file.write(template0.render(config=config, tapes=data))
|
||||
|
||||
with open(f"{self.build_dir}/{config['slug']}.xml", "w+", encoding="utf8") as output_file:
|
||||
# render the template
|
||||
output_file.write(template1.render(tapes=data, config=config,
|
||||
condition_delta=condition_delta,
|
||||
affliction_delta=affliction_delta,
|
||||
song_lengths=song_lengths,
|
||||
positions=positions))
|
||||
|
||||
with open(f"{self.build_dir}/{config['slug']}_style.xml", "w+", encoding="utf8") as output_file:
|
||||
# render the template
|
||||
output_file.write(template2.render(tapes=data, config=config, positions=positions))
|
||||
|
||||
def deploy(self, local_mod_name: str, config):
|
||||
try:
|
||||
os.mkdir(self.build_dir)
|
||||
except FileExistsError:
|
||||
self.logger.info(f"Removing old XML files in {self.build_dir}/:")
|
||||
for f in Path(self.build_dir).glob("*.xml"):
|
||||
self.logger.info(f" {f}")
|
||||
os.remove(f)
|
||||
pass
|
||||
|
||||
self.logger.info("Reading tapes.yaml")
|
||||
data_file = Path(f"{self.source_dir}/tapes.yaml")
|
||||
|
||||
# load yaml file
|
||||
with data_file.open(encoding='utf-8') as fr:
|
||||
data = yaml.load(fr, Loader=yaml.SafeLoader)
|
||||
|
||||
self.prepare_music(data)
|
||||
self.prepare_images(data)
|
||||
self.build_xml_code(data, config)
|
||||
|
||||
mod_directory = f"{self.install_dir}/LocalMods/{local_mod_name}/"
|
||||
|
||||
self.logger.info(f"Removing the old installed mod directory {mod_directory}.")
|
||||
rmfulldir(mod_directory)
|
||||
|
||||
self.logger.info(f"Copying the new build.")
|
||||
if Path(f"{self.install_dir}").is_dir():
|
||||
copy_tree(self.build_dir, mod_directory)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"{self.install_dir} does not exist. Set up the correct Barotrauma installation directory")
|
||||
|
||||
self.logger.info(f"Done!")
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M22 11V3h-7v3H9V3H2v8h7V8h2v10h4v3h7v-8h-7v3h-2V8h2v3z"/></svg>
|
After Width: | Height: | Size: 192 B |
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="ic_arrow_left_alt_24px.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="734"
|
||||
inkscape:window-height="480"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="12"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M 7.99,13 H 20 V 11 H 7.99 V 8 L 4,12 7.99,16 Z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.01 11H4v2h12.01v3L20 12l-3.99-4z"/></svg>
|
After Width: | Height: | Size: 174 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/></svg>
|
After Width: | Height: | Size: 368 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path clip-rule="evenodd" d="M0 0h24v24H0z" fill="none"/><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"/></svg>
|
After Width: | Height: | Size: 326 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M38 6H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4zM20 34L10 24l2.83-2.83L20 28.34l15.17-15.17L38 16 20 34z"/></svg>
|
After Width: | Height: | Size: 248 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M38 10v28H10V10h28m0-4H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4z"/></svg>
|
After Width: | Height: | Size: 209 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
After Width: | Height: | Size: 239 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M38 12.83L35.17 10 24 21.17 12.83 10 10 12.83 21.17 24 10 35.17 12.83 38 24 26.83 35.17 38 38 35.17 26.83 24z"/></svg>
|
After Width: | Height: | Size: 210 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/></svg>
|
After Width: | Height: | Size: 318 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M6 34.5V42h7.5l22.13-22.13-7.5-7.5L6 34.5zm35.41-20.41c.78-.78.78-2.05 0-2.83l-4.67-4.67c-.78-.78-2.05-.78-2.83 0l-3.66 3.66 7.5 7.5 3.66-3.66z"/></svg>
|
After Width: | Height: | Size: 244 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M12 38c0 2.21 1.79 4 4 4h16c2.21 0 4-1.79 4-4V14H12v24zM38 8h-7l-2-2H19l-2 2h-7v4h28V8z"/></svg>
|
After Width: | Height: | Size: 188 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>
|
After Width: | Height: | Size: 264 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M18 32.34L9.66 24l-2.83 2.83L18 38l24-24-2.83-2.83z"/></svg>
|
After Width: | Height: | Size: 152 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M6 34.5V42h7.5l22.13-22.13-7.5-7.5L6 34.5zm35.41-20.41c.78-.78.78-2.05 0-2.83l-4.67-4.67c-.78-.78-2.05-.78-2.83 0l-3.66 3.66 7.5 7.5 3.66-3.66z"/></svg>
|
After Width: | Height: | Size: 244 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M24 4C12.96 4 4 12.95 4 24s8.96 20 20 20 20-8.95 20-20S35.04 4 24 4zm2 30h-4v-4h4v4zm0-8h-4V14h4v12z"/></svg>
|
After Width: | Height: | Size: 201 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>
|
After Width: | Height: | Size: 430 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z"/></svg>
|
After Width: | Height: | Size: 274 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
|
After Width: | Height: | Size: 229 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M40 12H24l-4-4H8c-2.21 0-3.98 1.79-3.98 4L4 36c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V16c0-2.21-1.79-4-4-4zm0 24H8V16h32v20z"/></svg>
|
After Width: | Height: | Size: 225 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
|
After Width: | Height: | Size: 258 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2z"/></svg>
|
After Width: | Height: | Size: 194 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><polygon points="17.59,18 19,16.59 14.42,12 19,7.41 17.59,6 11.59,12"/><polygon points="11,18 12.41,16.59 7.83,12 12.41,7.41 11,6 5,12"/></g></g></svg>
|
After Width: | Height: | Size: 323 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><polygon points="6.41,6 5,7.41 9.58,12 5,16.59 6.41,18 12.41,12"/><polygon points="13,6 11.59,7.41 16.17,12 11.59,16.59 13,18 19,12"/></g></g></svg>
|
After Width: | Height: | Size: 320 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 3h18v2H3z"/></svg>
|
After Width: | Height: | Size: 152 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 19h12v2H6z"/></svg>
|
After Width: | Height: | Size: 153 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M30.83 14.83L28 12 16 24l12 12 2.83-2.83L21.66 24z"/></svg>
|
After Width: | Height: | Size: 151 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M20 12l-2.83 2.83L26.34 24l-9.17 9.17L20 36l12-12z"/></svg>
|
After Width: | Height: | Size: 151 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/></svg>
|
After Width: | Height: | Size: 263 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
|
After Width: | Height: | Size: 268 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24" x="0" y="0"/><path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M13.88,11.54l-4.96,4.96l-1.41-1.41 l4.96-4.96L10.34,8l5.65,0.01L16,13.66L13.88,11.54z"/></svg>
|
After Width: | Height: | Size: 356 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M28 20H4v4h24v-4zm0-8H4v4h24v-4zm8 16v-8h-4v8h-8v4h8v8h4v-8h8v-4h-8zM4 32h16v-4H4v4z"/></svg>
|
After Width: | Height: | Size: 185 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g/><g><path d="M17,19.22H5V7h7V5H5C3.9,5,3,5.9,3,7v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-7h-2V19.22z"/><path d="M19,2h-2v3h-3c0.01,0.01,0,2,0,2h3v2.99c0.01,0.01,2,0,2,0V7h3V5h-3V2z"/><rect height="2" width="8" x="7" y="9"/><polygon points="7,12 7,14 15,14 15,12 12,12"/><rect height="2" width="8" x="7" y="15"/></g></g></svg>
|
After Width: | Height: | Size: 496 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M16,9V4l1,0c0.55,0,1-0.45,1-1v0c0-0.55-0.45-1-1-1H7C6.45,2,6,2.45,6,3v0 c0,0.55,0.45,1,1,1l1,0v5c0,1.66-1.34,3-3,3h0v2h5.97v7l1,1l1-1v-7H19v-2h0C17.34,12,16,10.66,16,9z" fill-rule="evenodd"/></g></svg>
|
After Width: | Height: | Size: 379 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M36.79 21.2C33.11 17.97 28.29 16 23 16c-9.3 0-17.17 6.06-19.92 14.44L7.81 32c2.1-6.39 8.1-11 15.19-11 3.91 0 7.46 1.44 10.23 3.77L26 32h18V14l-7.21 7.2z"/></svg>
|
After Width: | Height: | Size: 253 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
After Width: | Height: | Size: 340 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>
|
After Width: | Height: | Size: 280 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M6 10h4V6c-2.21 0-4 1.79-4 4zm0 16h4v-4H6v4zm8 16h4v-4h-4v4zM6 18h4v-4H6v4zM26 6h-4v4h4V6zm12 0v4h4c0-2.21-1.79-4-4-4zM10 42v-4H6c0 2.21 1.79 4 4 4zm-4-8h4v-4H6v4zM18 6h-4v4h4V6zm4 36h4v-4h-4v4zm16-16h4v-4h-4v4zm0 16c2.21 0 4-1.79 4-4h-4v4zm0-24h4v-4h-4v4zm0 16h4v-4h-4v4zm-8 8h4v-4h-4v4zm0-32h4V6h-4v4zM14 34h20V14H14v20zm4-16h12v12H18V18z"/></svg>
|
After Width: | Height: | Size: 441 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/><path d="M20.41,8.41l-4.83-4.83C15.21,3.21,14.7,3,14.17,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V9.83 C21,9.3,20.79,8.79,20.41,8.41z M7,7h7v2H7V7z M17,17H7v-2h10V17z M17,13H7v-2h10V13z"/></g></svg>
|
After Width: | Height: | Size: 376 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M25 16c-5.29 0-10.11 1.97-13.8 5.2L4 14v18h18l-7.23-7.23C17.54 22.44 21.09 21 25 21c7.09 0 13.09 4.61 15.19 11l4.73-1.56C42.17 22.06 34.3 16 25 16z"/></svg>
|
After Width: | Height: | Size: 248 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M24 9C14 9 5.46 15.22 2 24c3.46 8.78 12 15 22 15 10.01 0 18.54-6.22 22-15-3.46-8.78-11.99-15-22-15zm0 25c-5.52 0-10-4.48-10-10s4.48-10 10-10 10 4.48 10 10-4.48 10-10 10zm0-16c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6z"/></svg>
|
After Width: | Height: | Size: 328 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z"/></svg>
|
After Width: | Height: | Size: 232 B |
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="ic_widgets_grey_24px.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1379"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="12"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z"
|
||||
id="path4"
|
||||
style="fill:#808080" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
version="1.1"
|
||||
id="svg6"
|
||||
sodipodi:docname="ic_widgets_red_24px.svg"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1411"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="-4.0677967"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="2560"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg6" />
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z"
|
||||
id="path4"
|
||||
style="fill:#ff0000" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,608 @@
|
|||
import sys
|
||||
import getpass
|
||||
import logging
|
||||
from threading import Thread
|
||||
|
||||
from tape_editor_widgets import *
|
||||
from widgets import LabelWebLink
|
||||
from deploy import Deployer, get_ffmpeg_version
|
||||
|
||||
from PIL import ImageGrab
|
||||
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
|
||||
class OptionsWidget(QWidget):
|
||||
def __init__(self, parent: QWidget):
|
||||
super().__init__()
|
||||
|
||||
self.parent = parent
|
||||
self.logging_handler = SignalHandler()
|
||||
self.deployer = Deployer(logging_handler=self.logging_handler)
|
||||
|
||||
options = [("Buffs", "Some songs affect the characters with strengthen\nand haste afflictions."),
|
||||
("De-buffs", "Some songs affect the characters with psychosis \naffliction."),
|
||||
("Walkmans", "Include personal music players (walkmans)"),
|
||||
("Durability", "Playing and throwing cassettes damages them."),
|
||||
("Repair", "Crafting mechanics to repair broken tapes by \nduplicating them with error correction.")]
|
||||
|
||||
# Name options
|
||||
self.name_line_edit = QLineEdit()
|
||||
self.name_line_edit.setText(f"{getpass.getuser()} mixtape")
|
||||
self.name_hbox = QHBoxLayout()
|
||||
self.name_hbox.addWidget(self.name_line_edit)
|
||||
self.name_gbox = QGroupBox("Local mod")
|
||||
self.name_gbox.setCheckable(True)
|
||||
self.name_gbox.setLayout(self.name_hbox)
|
||||
|
||||
# resolution
|
||||
self.width_spinbox = QSpinBox()
|
||||
self.width_spinbox.setRange(100, 100000)
|
||||
self.width_spinbox.setPrefix("Width: ")
|
||||
self.width_spinbox.setSuffix(" px")
|
||||
|
||||
self.height_spinbox = QSpinBox()
|
||||
self.height_spinbox.setRange(100, 100000)
|
||||
self.height_spinbox.setPrefix("Height: ")
|
||||
self.height_spinbox.setSuffix(" px")
|
||||
|
||||
self.detect_pushbutton = QPushButton("Detect screen resolution")
|
||||
self.detect_pushbutton.clicked.connect(self.update_screen_res)
|
||||
self.update_screen_res()
|
||||
|
||||
self.resolution_hbox = QVBoxLayout()
|
||||
self.resolution_hbox.addWidget(self.width_spinbox)
|
||||
self.resolution_hbox.addWidget(self.height_spinbox)
|
||||
self.resolution_hbox.addWidget(self.detect_pushbutton)
|
||||
|
||||
self.tape_covers_gbox = QGroupBox("Tape covers")
|
||||
self.tape_covers_gbox.setCheckable(True)
|
||||
self.tape_covers_gbox.setLayout(self.resolution_hbox)
|
||||
|
||||
# Checkbox options
|
||||
self.cbox_buffs = TelegraphingCheckbox(options[0][0], options[0][1])
|
||||
self.cbox_debuffs = TelegraphingCheckbox(options[1][0], options[1][1])
|
||||
self.cbox_walkmans = TelegraphingCheckbox(options[2][0], options[2][1])
|
||||
self.cbox_durability = TelegraphingCheckbox(options[3][0], options[3][1])
|
||||
self.cbox_repair = TelegraphingCheckbox(options[4][0], options[4][1])
|
||||
|
||||
self.label_options_description = QLabel()
|
||||
self.label_options_description.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
self.label_options_description.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
self.label_options_description.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
self.options_vbox = QVBoxLayout()
|
||||
|
||||
for cbox in [self.cbox_buffs,
|
||||
self.cbox_debuffs,
|
||||
self.cbox_walkmans,
|
||||
self.cbox_durability,
|
||||
self.cbox_repair]:
|
||||
self.options_vbox.addWidget(cbox)
|
||||
cbox.hover.connect(self.label_options_description.setText)
|
||||
cbox.toggle()
|
||||
|
||||
self.options_hbox = QHBoxLayout()
|
||||
self.options_hbox.addLayout(self.options_vbox)
|
||||
self.options_hbox.addWidget(self.label_options_description)
|
||||
|
||||
self.options_gbox = QGroupBox("Other options")
|
||||
self.options_gbox.setLayout(self.options_hbox)
|
||||
|
||||
# Update widget
|
||||
self.update_widget = UpdateWidget(self, self.deployer)
|
||||
# Install widget
|
||||
self.install_widget = InstallWidget(self, self.deployer)
|
||||
# Console widget
|
||||
self.console_widget = ConsoleWidget(self)
|
||||
|
||||
self.logging_handler.emitter.log_message.connect(self.console_widget.console.appendPlainText)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.name_gbox)
|
||||
layout.addWidget(self.tape_covers_gbox)
|
||||
layout.addWidget(self.options_gbox)
|
||||
layout.addWidget(self.update_widget)
|
||||
layout.addWidget(self.install_widget)
|
||||
layout.addWidget(self.console_widget)
|
||||
layout.addStretch()
|
||||
|
||||
# def update_label_description(self):
|
||||
|
||||
def update_screen_res(self):
|
||||
img = ImageGrab.grab()
|
||||
self.width_spinbox.setValue(img.size[0])
|
||||
self.height_spinbox.setValue(img.size[1])
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return {"name": self.name_line_edit.text(),
|
||||
"tape_covers": self.tape_covers_gbox.isChecked(),
|
||||
"resoultion_x": self.width_spinbox.value(),
|
||||
"resolution_y": self.height_spinbox.value(),
|
||||
"buffs": self.cbox_buffs.isChecked(),
|
||||
"debuffs": self.cbox_debuffs.isChecked(),
|
||||
"walkmans": self.cbox_walkmans.isChecked(),
|
||||
"durability": self.cbox_durability.isChecked(),
|
||||
"repair": self.cbox_repair.isChecked()}
|
||||
|
||||
|
||||
# TODO: implement random local mod name
|
||||
|
||||
|
||||
class CodeEditorWidget(QWidget):
|
||||
def __init__(self, parent: QWidget):
|
||||
super().__init__()
|
||||
|
||||
self.parent = parent
|
||||
|
||||
self.tapes_yaml = Path("./source/tapes.yaml")
|
||||
self.filename_string = f"<tt style=\"font-family: consolas;\"><b>{self.tapes_yaml.name}</b></tt>"
|
||||
|
||||
self.tapes_file_watcher = QFileSystemWatcher()
|
||||
self.tapes_file_watcher.addPath(self.tapes_yaml.as_posix())
|
||||
self.tapes_file_watcher.fileChanged.connect(self.external_file_changed)
|
||||
|
||||
self.external_editor_pushbutton = QPushButton()
|
||||
self.external_editor_pushbutton.setIcon(QIcon("./gui/icons/ic_open_in_new_24px.svg"))
|
||||
self.external_editor_pushbutton.setToolTip("Edit in external editor")
|
||||
self.external_editor_pushbutton.clicked.connect(self.open_file)
|
||||
self.reload_code_pushbutton = QPushButton()
|
||||
self.reload_code_pushbutton.setIcon(QIcon("./gui/icons/ic_refresh_24px.svg"))
|
||||
self.reload_code_pushbutton.setToolTip("Reload the external file")
|
||||
self.reload_code_pushbutton.clicked.connect(self.load_file)
|
||||
self.save_code_pushbutton = QPushButton()
|
||||
self.save_code_pushbutton.setIcon(QIcon("./gui/icons/ic_save_24px.svg"))
|
||||
self.save_code_pushbutton.setToolTip("Save setup")
|
||||
self.save_code_pushbutton.clicked.connect(self.save_file)
|
||||
self.transfer_to_validator_pushbutton = QPushButton()
|
||||
self.transfer_to_validator_pushbutton.setIcon(QIcon("./gui/icons/ic_keyboard_double_arrow_right_24px.svg"))
|
||||
self.transfer_to_validator_pushbutton.setToolTip("Apply the code.")
|
||||
self.transfer_to_validator_pushbutton.clicked.connect(self.apply_code)
|
||||
|
||||
self.status_same = f"<b>Status:</b> yaml setup matches {self.filename_string} file"
|
||||
self.status_external_file_changed = f"<b>Status:</b> {self.filename_string} file was edited externally"
|
||||
self.status_editor_code_changed = "<b>Status:</b> yaml setup not yet saved"
|
||||
|
||||
self.status_label = QLabel(self.status_same)
|
||||
|
||||
self.editor = CodeEditor()
|
||||
self.editor.setFont("Consolas")
|
||||
self.editor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||
self.editor.setMinimumWidth(400)
|
||||
self.editor.textChanged.connect(self.yaml_setup_changed)
|
||||
|
||||
self.button_hbox = QHBoxLayout()
|
||||
self.button_hbox.addWidget(self.external_editor_pushbutton)
|
||||
self.button_hbox.addWidget(self.reload_code_pushbutton)
|
||||
self.button_hbox.addWidget(self.save_code_pushbutton)
|
||||
self.button_hbox.addWidget(self.status_label)
|
||||
self.button_hbox.addStretch()
|
||||
self.button_hbox.addWidget(self.transfer_to_validator_pushbutton)
|
||||
# self.button_hbox.addStretch()
|
||||
|
||||
self.editor_vbox = QVBoxLayout()
|
||||
self.editor_vbox.addLayout(self.button_hbox)
|
||||
self.editor_vbox.addWidget(self.editor)
|
||||
|
||||
self.editor_gbox = QGroupBox("tapes.yaml editor")
|
||||
self.editor_gbox.setLayout(self.editor_vbox)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.editor_gbox)
|
||||
|
||||
self.load_file()
|
||||
|
||||
def load_file(self):
|
||||
with open(self.tapes_yaml, encoding="utf-8") as fp:
|
||||
text = fp.read()
|
||||
self.editor.setPlainText(text)
|
||||
|
||||
def save_file(self):
|
||||
with open(self.tapes_yaml, "w+", encoding="utf-8") as fp:
|
||||
fp.write(self.editor.toPlainText())
|
||||
|
||||
def open_file(self):
|
||||
os.startfile(self.tapes_yaml)
|
||||
|
||||
def check_if_setups_match(self):
|
||||
with open(self.tapes_yaml, encoding="utf-8") as fp:
|
||||
external_text = fp.read()
|
||||
|
||||
contents_match = external_text == self.editor.toPlainText()
|
||||
self.save_code_pushbutton.setDisabled(contents_match)
|
||||
self.reload_code_pushbutton.setDisabled(contents_match)
|
||||
|
||||
if contents_match:
|
||||
self.status_label.setText(self.status_same)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def external_file_changed(self):
|
||||
if not self.check_if_setups_match():
|
||||
self.status_label.setText(self.status_external_file_changed)
|
||||
|
||||
def yaml_setup_changed(self):
|
||||
if not self.check_if_setups_match():
|
||||
self.status_label.setText(self.status_editor_code_changed)
|
||||
|
||||
def apply_code(self):
|
||||
try:
|
||||
tapes = yaml.safe_load(self.editor.toPlainText())
|
||||
self.parent.validation_widget.load_tapes(tapes)
|
||||
|
||||
except yaml.scanner.ScannerError as e:
|
||||
logging.error(f"Error applying yaml code:\n\n{str(e)}")
|
||||
|
||||
|
||||
class TapeEditWidget(QWidget):
|
||||
def __init__(self, tape: dict, number: int | None = None):
|
||||
super().__init__()
|
||||
|
||||
self.tape = tape
|
||||
|
||||
self.label_nr = QLabel(f"#{number}")
|
||||
self.label_nr.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.label_nr.setStyleSheet("background-color: #cfd8dc;")
|
||||
change_font_size(self.label_nr, 1.41)
|
||||
|
||||
self.identifier = IdentifierEditWidget(self.tape)
|
||||
self.cover = CoverEditWidget(self.tape)
|
||||
self.edit_name = NameEditWidget(self.tape)
|
||||
self.edit_source = SourceEditWidget(self.tape)
|
||||
self.process = ProcessEditWidget(self.tape)
|
||||
self.economy = EconomyEditWidget(self.tape)
|
||||
self.spawn = SpawnEditWidget(self.tape)
|
||||
self.afflictions = AfflictionsEditWidget(self.tape)
|
||||
self.crafting = CraftingEditWidget(self.tape)
|
||||
|
||||
layout = QGridLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 5)
|
||||
|
||||
for i, (text, widget) in enumerate([(f"", self.identifier),
|
||||
("Cover", self.cover),
|
||||
("Name", self.edit_name),
|
||||
("Source", self.edit_source),
|
||||
("Process", self.process),
|
||||
("Economy", self.economy),
|
||||
("Spawn", self.spawn),
|
||||
("Crafting", self.crafting),
|
||||
("Afflictions", self.afflictions),
|
||||
("Note", QPlainTextEdit(self.tape["note"]))]):
|
||||
label = self.label_nr if i == 0 else QLabel(text)
|
||||
|
||||
if i == 0:
|
||||
layout.addWidget(label, i, 0)
|
||||
else:
|
||||
label_layout = QVBoxLayout()
|
||||
label_layout.addWidget(label)
|
||||
label_layout.addStretch()
|
||||
layout.addLayout(label_layout, i, 0)
|
||||
|
||||
layout.addWidget(widget, i, 1)
|
||||
|
||||
|
||||
class TapeListWidget(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(1, 0, 0, 0)
|
||||
|
||||
def append(self, widget):
|
||||
self.layout.addWidget(widget)
|
||||
|
||||
def clear(self):
|
||||
for i, widget in enumerate(self.children()):
|
||||
if i > 0:
|
||||
self.layout.removeWidget(widget)
|
||||
widget.deleteLater()
|
||||
|
||||
|
||||
class ValidationWidget(QWidget):
|
||||
def __init__(self, parent: QWidget):
|
||||
super().__init__()
|
||||
|
||||
self.parent = parent
|
||||
|
||||
self.transfer_to_code_pushbutton = QPushButton()
|
||||
self.transfer_to_code_pushbutton.setIcon(QIcon("./gui/icons/ic_keyboard_double_arrow_left_24px.svg"))
|
||||
self.transfer_to_code_pushbutton.setToolTip("Update the code")
|
||||
self.transfer_to_code_pushbutton.clicked.connect(self.save_tapes)
|
||||
self.jump_spinbox = QSpinBox()
|
||||
# self.search_lineedit = QLineEdit()
|
||||
# self.search_lineedit.setPlaceholderText("Search ...")
|
||||
|
||||
self.buttons_hbox = QHBoxLayout()
|
||||
self.buttons_hbox.addWidget(self.transfer_to_code_pushbutton)
|
||||
self.buttons_hbox.addWidget(QLabel("Navigate to"))
|
||||
self.buttons_hbox.addWidget(self.jump_spinbox)
|
||||
self.buttons_hbox.addStretch()
|
||||
# self.buttons_hbox.addWidget(self.search_lineedit)
|
||||
|
||||
self.tape_list_widget = TapeListWidget()
|
||||
|
||||
self.scroll = QScrollArea()
|
||||
self.scroll.setWidget(self.tape_list_widget)
|
||||
self.scroll.setWidgetResizable(True)
|
||||
self.scroll.setFrameStyle(QFrame.Shape.NoFrame)
|
||||
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
|
||||
self.validation_vbox = QVBoxLayout()
|
||||
self.validation_vbox.addLayout(self.buttons_hbox)
|
||||
self.validation_vbox.addWidget(self.scroll)
|
||||
|
||||
self.validation_gbox = QGroupBox("Data validation")
|
||||
self.validation_gbox.setLayout(self.validation_vbox)
|
||||
|
||||
self.jump_spinbox.valueChanged.connect(self.jump_to_tape)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.validation_gbox)
|
||||
self.setMinimumWidth(500)
|
||||
self.setMaximumWidth(500)
|
||||
|
||||
self.tapes = None
|
||||
self.load_tapes_from_file()
|
||||
|
||||
def load_tapes_from_file(self):
|
||||
try:
|
||||
with open("./source/tapes.yaml", encoding="utf-8") as fp:
|
||||
tapes = yaml.safe_load(fp)
|
||||
self.load_tapes(tapes)
|
||||
|
||||
except yaml.scanner.ScannerError as e:
|
||||
logging.error(f"Error applying yaml code:\n\n{str(e)}")
|
||||
|
||||
def load_tapes(self, tapes):
|
||||
self.tapes = tapes
|
||||
|
||||
self.tape_list_widget.clear()
|
||||
for i, tape in enumerate(self.tapes):
|
||||
self.tape_list_widget.append(TapeEditWidget(tape, i))
|
||||
|
||||
self.jump_spinbox.setValue(0)
|
||||
self.jump_spinbox.setMaximum(i)
|
||||
|
||||
def save_tapes(self):
|
||||
yaml_text = yaml.safe_dump(self.tapes, sort_keys=False, allow_unicode=True).replace("\n-", "\n\n-")
|
||||
self.parent.editor_widget.editor.setPlainText(yaml_text)
|
||||
|
||||
def jump_to_tape(self, tape_number):
|
||||
scrollbar = self.scroll.verticalScrollBar()
|
||||
|
||||
division = (scrollbar.maximum() + scrollbar.pageStep()) / len(self.tapes)
|
||||
overflow_ratio = (scrollbar.pageStep() + 5) / division
|
||||
step = scrollbar.maximum() / (len(self.tapes) - overflow_ratio)
|
||||
|
||||
scrollbar.setValue(tape_number * step)
|
||||
|
||||
|
||||
class UpdateWidget(QGroupBox):
|
||||
def __init__(self, parent: QWidget, deployer: Deployer):
|
||||
super().__init__("Update")
|
||||
|
||||
self.parent = parent
|
||||
self.deployer = deployer
|
||||
|
||||
hr_html_string = "<hr style=\"background-color:gray;\">"
|
||||
self.grid = QGridLayout(self)
|
||||
|
||||
self.git_checkbox = QCheckBox()
|
||||
self.git_checkbox.setDisabled(True)
|
||||
self.git_label = QLabel("Download and unpack git")
|
||||
self.git_label_link = LabelWebLink("https://github.com/git-for-windows/git/releases/download/"
|
||||
"v2.46.2.windows.1/PortableGit-2.46.2-64-bit.7z.exe")
|
||||
self.git_label_link.break_at("download")
|
||||
self.git_pushbutton = QPushButton("Download")
|
||||
self.git_pushbutton.clicked.connect(self.download_git_action)
|
||||
self.git_dir = DirWidget("./utils")
|
||||
self.grid.addWidget(self.git_checkbox, 0, 0)
|
||||
self.grid.addWidget(self.git_label, 0, 1)
|
||||
self.grid.addWidget(self.git_pushbutton, 0, 2)
|
||||
self.grid.addWidget(self.git_label_link, 1, 1)
|
||||
self.grid.addWidget(self.git_dir, 2, 1)
|
||||
|
||||
self.git_update_checkbox = QCheckBox()
|
||||
self.git_update_checkbox.setDisabled(True)
|
||||
self.git_update_label = QLabel("Download update from git")
|
||||
self.git_update_label_link = LabelWebLink("https://git.kompot.si/jaka/barotrauma-sunken-tapes")
|
||||
self.git_update_pushbutton = QPushButton("Update")
|
||||
self.git_update_pushbutton.clicked.connect(self.update_action)
|
||||
self.grid.addWidget(self.git_update_checkbox, 10, 0)
|
||||
self.grid.addWidget(self.git_update_label, 10, 1)
|
||||
self.grid.addWidget(self.git_update_label_link, 11, 1)
|
||||
self.grid.addWidget(self.git_update_pushbutton, 10, 2)
|
||||
self.grid.addWidget(QLabel(hr_html_string), 12, 0, 1, 3)
|
||||
|
||||
self.git_dir_watcher = QFileSystemWatcher()
|
||||
self.git_dir_watcher.addPath(Path(self.git_dir.directory).as_posix())
|
||||
self.git_dir_watcher.directoryChanged.connect(self.does_git_exist)
|
||||
|
||||
self.does_git_exist()
|
||||
|
||||
def download_git_action(self):
|
||||
Thread(target=self.deployer.download_git).start()
|
||||
|
||||
def does_git_exist(self):
|
||||
executable_path = Path("git/bin/git.exe")
|
||||
if (self.git_dir.directory / executable_path).exists():
|
||||
exists = True
|
||||
else:
|
||||
exists = False
|
||||
|
||||
self.git_pushbutton.setEnabled(not exists)
|
||||
self.git_checkbox.setChecked(exists)
|
||||
self.git_update_pushbutton.setEnabled(exists)
|
||||
return exists
|
||||
|
||||
def update_action(self):
|
||||
Thread(target=self.deployer.update).start()
|
||||
|
||||
|
||||
class ConsoleWidget(QGroupBox):
|
||||
def __init__(self, parent: QWidget):
|
||||
super().__init__("Info")
|
||||
self.parent = parent
|
||||
|
||||
self.console = QPlainTextEdit(self)
|
||||
self.console.setReadOnly(True)
|
||||
self.console.setFont("Consolas")
|
||||
self.console.setMinimumHeight(100)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.console)
|
||||
|
||||
|
||||
class InstallWidget(QGroupBox):
|
||||
def __init__(self, parent: QWidget, deployer: Deployer):
|
||||
super().__init__("Install steps")
|
||||
|
||||
self.parent = parent
|
||||
self.deployer = deployer
|
||||
self.grid = QGridLayout(self)
|
||||
|
||||
hr_html_string = "<hr style=\"background-color:gray;\">"
|
||||
|
||||
self.ffmpeg_checkbox = QCheckBox()
|
||||
self.ffmpeg_checkbox.setDisabled(True)
|
||||
self.ffmpeg_label = QLabel("Download and unpack ffmpeg")
|
||||
self.ffmpeg_label_link = LabelWebLink("https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z")
|
||||
self.ffmpeg_pushbutton = QPushButton("Download")
|
||||
self.ffmpeg_pushbutton.clicked.connect(self.download_ffmpeg_action)
|
||||
self.ffmpeg_dir = DirWidget(self.deployer.utils_dir)
|
||||
self.grid.addWidget(self.ffmpeg_checkbox, 0, 0)
|
||||
self.grid.addWidget(self.ffmpeg_label, 0, 1)
|
||||
self.grid.addWidget(self.ffmpeg_pushbutton, 0, 2)
|
||||
self.grid.addWidget(self.ffmpeg_label_link, 1, 1)
|
||||
self.grid.addWidget(self.ffmpeg_dir, 2, 1)
|
||||
self.grid.addWidget(QLabel(hr_html_string), 9, 0, 1, 3)
|
||||
|
||||
self.songs_download_checkbox = QCheckBox()
|
||||
self.songs_download_checkbox.setDisabled(True)
|
||||
self.songs_download_label = QLabel("Download songs and process them")
|
||||
self.songs_download_dir = DirWidget(self.deployer.music_dir)
|
||||
self.songs_download_pushbutton = QPushButton("Download")
|
||||
self.songs_download_pushbutton.clicked.connect(self.are_songs_ready)
|
||||
self.grid.addWidget(self.songs_download_checkbox, 20, 0)
|
||||
self.grid.addWidget(self.songs_download_label, 20, 1)
|
||||
self.grid.addWidget(self.songs_download_pushbutton, 20, 2)
|
||||
self.grid.addWidget(self.songs_download_dir, 21, 1)
|
||||
self.grid.addWidget(QLabel(hr_html_string), 22, 0, 1, 3)
|
||||
|
||||
self.compile_checkbox = QCheckBox()
|
||||
self.compile_checkbox.setDisabled(True)
|
||||
self.compile_label = QLabel("Compile mod according to the settings")
|
||||
self.compile_dir = DirWidget(self.deployer.build_dir)
|
||||
self.compile_pushbutton = QPushButton("Compile")
|
||||
self.grid.addWidget(self.compile_checkbox, 30, 0)
|
||||
self.grid.addWidget(self.compile_label, 30, 1)
|
||||
self.grid.addWidget(self.compile_pushbutton, 30, 2)
|
||||
self.grid.addWidget(self.compile_dir, 31, 1)
|
||||
self.grid.addWidget(QLabel(hr_html_string), 32, 0, 1, 3)
|
||||
|
||||
self.install_checkbox = QCheckBox()
|
||||
self.install_checkbox.setDisabled(True)
|
||||
self.install_label = QLabel("Copy the files to the install directory")
|
||||
self.install_dir = DirWidget(self.deployer.install_dir)
|
||||
self.install_pushbutton = QPushButton("Install")
|
||||
self.grid.addWidget(self.install_checkbox, 40, 0)
|
||||
self.grid.addWidget(self.install_label, 40, 1)
|
||||
self.grid.addWidget(self.install_pushbutton, 40, 2)
|
||||
self.grid.addWidget(self.install_dir, 41, 1)
|
||||
self.grid.addWidget(QLabel(hr_html_string), 42, 0, 1, 3)
|
||||
|
||||
self.grid.setRowStretch(50, 20)
|
||||
|
||||
self.utils_dir_watcher = QFileSystemWatcher()
|
||||
self.utils_dir_watcher.addPath(Path(self.ffmpeg_dir.directory).as_posix())
|
||||
self.utils_dir_watcher.directoryChanged.connect(self.does_ffmpeg_exist)
|
||||
|
||||
self.does_ffmpeg_exist()
|
||||
|
||||
def does_ffmpeg_exist(self):
|
||||
executable_path = Path("ffmpeg-" + get_ffmpeg_version() + "-full_build/bin/ffmpeg.exe")
|
||||
if (self.ffmpeg_dir.directory / executable_path).exists():
|
||||
exists = True
|
||||
else:
|
||||
exists = False
|
||||
|
||||
self.ffmpeg_pushbutton.setEnabled(not exists)
|
||||
self.ffmpeg_checkbox.setChecked(exists)
|
||||
self.songs_download_pushbutton.setEnabled(exists)
|
||||
return exists
|
||||
|
||||
def download_ffmpeg_action(self):
|
||||
Thread(target=self.deployer.download_ffmpeg).start()
|
||||
|
||||
def are_songs_ready(self):
|
||||
tapes = self.parent.parent.validation_widget.tapes
|
||||
|
||||
files = Path(self.songs_download_dir.directory).glob("*.ogg")
|
||||
files = [f.stem for f in files]
|
||||
|
||||
for tape in tapes:
|
||||
tape_name = tape["identifier"]
|
||||
if not tape_name in files:
|
||||
return False
|
||||
if not f"{tape_name}-walkman" in files and self.parent.options["walkmans"]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MainWidget(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
|
||||
self.options_widget = OptionsWidget(self)
|
||||
self.editor_widget = CodeEditorWidget(self)
|
||||
self.validation_widget = ValidationWidget(self)
|
||||
layout.addWidget(self.options_widget)
|
||||
layout.addWidget(self.editor_widget)
|
||||
layout.addWidget(self.validation_widget)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle("Sunken Tapes installer")
|
||||
|
||||
self.quit_shortcut = QShortcut(QKeySequence('Ctrl+Q'), self)
|
||||
self.quit_shortcut.activated.connect(QApplication.instance().quit)
|
||||
|
||||
self.setCentralWidget(MainWidget())
|
||||
|
||||
class QSignaler(QObject):
|
||||
log_message = Signal(str)
|
||||
|
||||
|
||||
class SignalHandler(logging.Handler):
|
||||
"""Logging handler to emit QtSignal with log record text."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SignalHandler, self).__init__(*args, **kwargs)
|
||||
self.emitter = QSignaler()
|
||||
|
||||
def emit(self, log_record: logging.LogRecord):
|
||||
self.emitter.log_message.emit(f"{log_record.message}")
|
||||
# When the line below is enabled, logging is immediate/otherwise events
|
||||
# on the queue will be processed when the slot has finished.
|
||||
# QtGui.qApp.processEvents()
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir("..")
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,331 @@
|
|||
from widgets import *
|
||||
|
||||
|
||||
def change_font_size(label: QLabel | QLineEdit, factor: float) -> QLabel:
|
||||
f = label.font()
|
||||
f.setPointSizeF(f.pointSizeF() * factor)
|
||||
label.setFont(f)
|
||||
return label
|
||||
|
||||
|
||||
class IdentifierEditWidget(QLineEdit):
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
self.tape = tape
|
||||
|
||||
self.setFont("Consolas")
|
||||
self.setStyleSheet("font-weight: bold; background-color: #cfd8dc;")
|
||||
self.setTextMargins(2, 0, 2, 0)
|
||||
self.setFrame(QFrame.Shape.NoFrame)
|
||||
self.setText(self.tape["identifier"])
|
||||
change_font_size(self, 1.41)
|
||||
|
||||
self.load()
|
||||
self.textChanged.connect(self.save)
|
||||
|
||||
def load(self):
|
||||
self.setText(self.tape["identifier"])
|
||||
|
||||
def save(self):
|
||||
self.tape["identifier"] = self.text()
|
||||
|
||||
|
||||
class CoverEditWidget(QLabel):
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
self.tape = tape
|
||||
|
||||
pix = QPixmap(f"./source/images/{self.tape['identifier']}.png")
|
||||
self.pixmap = pix.scaled(350, 350,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation)
|
||||
|
||||
self.resize(350, 0)
|
||||
self.setPixmap(self.pixmap)
|
||||
|
||||
|
||||
class NameEditWidget(QLineEdit):
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
self.tape = tape
|
||||
self.load()
|
||||
self.textChanged.connect(self.save)
|
||||
|
||||
def load(self):
|
||||
self.setText(self.tape["name"])
|
||||
|
||||
def save(self):
|
||||
self.tape["name"] = self.text()
|
||||
|
||||
|
||||
class SourceEditWidget(UrlWidget):
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
self.tape = tape
|
||||
self.load()
|
||||
self.url_lineedit.textChanged.connect(self.save)
|
||||
|
||||
def load(self):
|
||||
self.url_lineedit.setText(self.tape["source"])
|
||||
|
||||
def save(self):
|
||||
self.tape["source"] = self.url_lineedit.text()
|
||||
|
||||
|
||||
class ProcessEditWidget(QWidget):
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
|
||||
self.tape = tape
|
||||
|
||||
self.cbox_clip_start = QCheckBox("Clip at start")
|
||||
self.cbox_clip_end = QCheckBox("Clip at end")
|
||||
self.cbox_volume_factor = QCheckBox("Volume factor")
|
||||
|
||||
self.clip_start = TimeEditWithoutWheel()
|
||||
self.clip_start.setDisplayFormat("hh:mm:ss.z")
|
||||
self.clip_end = TimeEditWithoutWheel()
|
||||
self.clip_end.setDisplayFormat("hh:mm:ss.z")
|
||||
self.volume_factor = SpinBoxWithoutWheel()
|
||||
self.volume_factor.setSuffix(" dB")
|
||||
self.volume_factor.setMinimum(-30)
|
||||
self.volume_factor.setMaximum(30)
|
||||
|
||||
layout = QGridLayout(self)
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
layout.setColumnStretch(3, 1)
|
||||
|
||||
layout.addWidget(self.cbox_clip_start, 0, 0)
|
||||
layout.addWidget(self.cbox_clip_end, 1, 0)
|
||||
layout.addWidget(self.cbox_volume_factor, 2, 0)
|
||||
|
||||
layout.addWidget(self.clip_start, 0, 1)
|
||||
layout.addWidget(self.clip_end, 1, 1)
|
||||
layout.addWidget(self.volume_factor, 2, 1)
|
||||
|
||||
self.load()
|
||||
self.clip_start.timeChanged.connect(self.save)
|
||||
self.clip_end.timeChanged.connect(self.save)
|
||||
self.volume_factor.valueChanged.connect(self.save)
|
||||
self.cbox_clip_start.toggled.connect(self.save)
|
||||
self.cbox_clip_end.toggled.connect(self.save)
|
||||
self.cbox_volume_factor.toggled.connect(self.save)
|
||||
self.save()
|
||||
|
||||
def load(self):
|
||||
clip_start, clip_end = self.tape["process"]["start"], self.tape["process"]["end"]
|
||||
start, end = QTime(), QTime()
|
||||
|
||||
if clip_start is not None:
|
||||
start_time = datetime.datetime.strptime(clip_start, "%H:%M:%S.%f")
|
||||
start.setHMS(start_time.hour, start_time.minute, start_time.second, ms=start_time.microsecond * 1e-3)
|
||||
self.cbox_clip_start.setChecked(clip_start != "00:00:00.0")
|
||||
self.clip_start.setTime(start)
|
||||
|
||||
if clip_end is not None:
|
||||
end_time = datetime.datetime.strptime(clip_end, "%H:%M:%S.%f")
|
||||
end.setHMS(end_time.hour, end_time.minute, end_time.second, ms=end_time.microsecond * 1e-3)
|
||||
self.cbox_clip_end.setChecked(clip_end != "00:00:00.0")
|
||||
self.clip_end.setTime(end)
|
||||
|
||||
volume_factor = self.tape["process"]["volume"]
|
||||
|
||||
if volume_factor is not None:
|
||||
self.volume_factor.setValue(volume_factor)
|
||||
self.cbox_volume_factor.setChecked(volume_factor != 0)
|
||||
|
||||
def save(self):
|
||||
start = self.clip_start.time()
|
||||
start = f"{start.hour():02}:{start.minute():02}:{start.second():02}.{start.msec():03}"
|
||||
end = self.clip_end.time()
|
||||
end = f"{end.hour():02}:{end.minute():02}:{end.second():02}.{end.msec():03}"
|
||||
volume = self.volume_factor.value()
|
||||
|
||||
self.tape["process"]["start"] = start if self.cbox_clip_start.isChecked() else None
|
||||
self.tape["process"]["end"] = end if self.cbox_clip_end.isChecked() else None
|
||||
self.tape["process"]["volume"] = volume if self.cbox_volume_factor.isChecked() else None
|
||||
|
||||
|
||||
class EconomyEditWidget(QWidget):
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
|
||||
self.tape = tape
|
||||
|
||||
self.spinbox_price = SpinBoxWithoutWheel()
|
||||
self.spinbox_price.setRange(10, 100000)
|
||||
self.spinbox_price.setSuffix(" mk")
|
||||
self.spinbox_price.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)
|
||||
|
||||
layout = QGridLayout(self)
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
layout.addWidget(self.spinbox_price, 1, 0)
|
||||
layout.setColumnStretch(4, 1)
|
||||
|
||||
for i, text in enumerate(["Price", "Sold in", "Factor", " Local price"]):
|
||||
layout.addWidget(change_font_size(QLabel(text), 0.8), 0, i)
|
||||
|
||||
self.economy = {"outpost": {},
|
||||
"city": {},
|
||||
"research": {},
|
||||
"military": {},
|
||||
"mine": {}}
|
||||
|
||||
for i, (location, editors) in enumerate(self.economy.items()):
|
||||
editors["cbox"] = QCheckBox(location.capitalize())
|
||||
editors["float"] = DoubleSpinBoxWithoutWheel()
|
||||
editors["float"].setSingleStep(0.05)
|
||||
editors["label"] = QLabel()
|
||||
editors["label"].setFont("consolas")
|
||||
layout.addWidget(editors["cbox"], int(i) + 1, 1)
|
||||
layout.addWidget(editors["float"], int(i) + 1, 2)
|
||||
layout.addWidget(editors["label"], int(i) + 1, 3)
|
||||
|
||||
self.load()
|
||||
self.update_labels()
|
||||
|
||||
self.spinbox_price.valueChanged.connect(self.save)
|
||||
for editors in self.economy.values():
|
||||
editors["cbox"].toggled.connect(self.save)
|
||||
editors["float"].valueChanged.connect(self.save)
|
||||
|
||||
def load(self):
|
||||
self.spinbox_price.setValue(self.tape["price"])
|
||||
for locale in self.tape["economy"]:
|
||||
self.economy[locale["location"]]["cbox"].setChecked(locale["sold"])
|
||||
self.economy[locale["location"]]["float"].setValue(locale["factor"])
|
||||
|
||||
def save(self):
|
||||
self.tape["price"] = self.spinbox_price.value()
|
||||
self.tape["economy"] = [{"location": location,
|
||||
"factor": editors["float"].value(),
|
||||
"sold": editors["cbox"].isChecked()} for location, editors in self.economy.items()]
|
||||
self.update_labels()
|
||||
|
||||
def update_labels(self):
|
||||
for editors in self.economy.values():
|
||||
editors["label"].setText(f" {self.tape['price'] * editors['float'].value():4.0f} mk")
|
||||
|
||||
|
||||
class SpawnEditWidget(QWidget):
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
|
||||
self.tape = tape
|
||||
|
||||
layout = QGridLayout(self)
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
layout.setColumnStretch(2, 1)
|
||||
|
||||
for i, text in enumerate(["Place", "Probability"]):
|
||||
layout.addWidget(change_font_size(QLabel(text), 0.8), 0, i)
|
||||
|
||||
self.spawn = {"outpostcrewcabinet": {"label": QLabel("Outpost Crew Cabinet"),
|
||||
"value": ProbabilitySpinBox()},
|
||||
"wreckstorage": {"label": QLabel("Wreck Storage"),
|
||||
"value": ProbabilitySpinBox()},
|
||||
"abandonedcrewcab": {"label": QLabel("Abandoned Crew Cabinet"),
|
||||
"value": ProbabilitySpinBox()},
|
||||
"abandonedstoragecab": {"label": QLabel("Abandoned Storage Cabinet"),
|
||||
"value": ProbabilitySpinBox()}}
|
||||
|
||||
for i, (location, editors) in enumerate(self.spawn.items()):
|
||||
layout.addWidget(editors["label"], int(i) + 1, 0)
|
||||
layout.addWidget(editors["value"], int(i) + 1, 1)
|
||||
|
||||
self.load()
|
||||
|
||||
for i, (location, editors) in enumerate(self.spawn.items()):
|
||||
editors["value"].valueChanged.connect(self.save)
|
||||
|
||||
def load(self):
|
||||
for locale in self.tape["spawn"]:
|
||||
self.spawn[locale["location"]]["value"].setValue(locale["probability"])
|
||||
|
||||
def save(self):
|
||||
self.tape["spawn"] = [{"location": location,
|
||||
"probability": editors["value"].value()} for location, editors in self.spawn.items() if
|
||||
editors["value"].value() > 0]
|
||||
|
||||
|
||||
# TODO: afflictions with shorter range for walkman songs.
|
||||
|
||||
|
||||
class CraftingEditWidget(QWidget):
|
||||
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
|
||||
self.tape = tape
|
||||
|
||||
self.int_uses = SpinBoxWithoutWheel()
|
||||
self.int_uses.setMinimum(1)
|
||||
self.int_uses.setMaximum(1000)
|
||||
|
||||
self.description = QPlainTextEdit("One use is duration of the tape")
|
||||
self.description.setEnabled(False)
|
||||
self.description.setMaximumHeight(QFontMetrics(self.description.font()).height() * 1.7)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.addWidget(QLabel("Number of uses:"))
|
||||
layout.addWidget(self.int_uses)
|
||||
layout.addWidget(self.description)
|
||||
layout.addStretch()
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
|
||||
self.load()
|
||||
self.int_uses.valueChanged.connect(self.save)
|
||||
|
||||
def load(self):
|
||||
self.int_uses.setValue(self.tape["no_of_uses"])
|
||||
|
||||
def save(self):
|
||||
self.tape["no_of_uses"] = self.int_uses.value()
|
||||
|
||||
|
||||
class AfflictionsEditWidget(QWidget):
|
||||
|
||||
def __init__(self, tape: dict):
|
||||
super().__init__()
|
||||
|
||||
self.tape = tape
|
||||
|
||||
self.description = QPlainTextEdit("Factor 1.0 means that the affliction will reach the "
|
||||
"highest level after being exposed to the sound of the "
|
||||
"tape for its play duration.")
|
||||
self.description.setEnabled(False)
|
||||
self.description.setMaximumWidth(212)
|
||||
|
||||
layout = QGridLayout(self)
|
||||
layout.setContentsMargins(5, 0, 5, 0)
|
||||
layout.addWidget(change_font_size(QLabel("Affliction"), 0.8), 0, 0)
|
||||
layout.addWidget(change_font_size(QLabel("Factor"), 0.8), 0, 1)
|
||||
layout.addWidget(self.description, 1, 2, 3, 1)
|
||||
layout.setColumnStretch(3, 1)
|
||||
|
||||
self.afflictions = {"strengthen": {},
|
||||
"haste": {},
|
||||
"psychosis": {}}
|
||||
|
||||
for i, (affliction, editors) in enumerate(self.afflictions.items()):
|
||||
editors["cbox"] = QCheckBox(affliction.capitalize())
|
||||
editors["float"] = DoubleSpinBoxWithoutWheel()
|
||||
editors["float"].setSingleStep(0.01)
|
||||
layout.addWidget(editors["cbox"], int(i) + 1, 0)
|
||||
layout.addWidget(editors["float"], int(i) + 1, 1)
|
||||
|
||||
self.load()
|
||||
|
||||
for editors in self.afflictions.values():
|
||||
editors["cbox"].toggled.connect(self.save)
|
||||
editors["float"].valueChanged.connect(self.save)
|
||||
|
||||
def load(self):
|
||||
if self.tape["afflictions"]:
|
||||
for affliction in self.tape["afflictions"]:
|
||||
self.afflictions[affliction["name"]]["cbox"].setChecked(True)
|
||||
self.afflictions[affliction["name"]]["float"].setValue(affliction["factor"])
|
||||
|
||||
def save(self):
|
||||
self.tape["afflictions"] = [{"name": affliction, "factor": editors["float"].value()} for affliction, editors in
|
||||
self.afflictions.items() if editors["cbox"].isChecked()]
|
|
@ -0,0 +1,260 @@
|
|||
from PySide6.QtCore import QSize, Qt, Slot, QRect, QFileSystemWatcher, Signal, QTime, QObject
|
||||
from PySide6.QtGui import QIcon, QAction, QShortcut, QKeySequence, QPainter, QColor, QTextFormat, QPixmap, QFontMetrics
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QLabel, QVBoxLayout, QHBoxLayout, \
|
||||
QGroupBox, QLineEdit, QCheckBox, QSpinBox, QPlainTextEdit, QSizePolicy, QGridLayout, QTextEdit, QScrollArea, QFrame, \
|
||||
QDoubleSpinBox, QFormLayout, QTimeEdit, QFileDialog, QDialog
|
||||
|
||||
import os
|
||||
import webbrowser
|
||||
import validators
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
class LabelWebLink(QLabel):
|
||||
def __init__(self, text):
|
||||
text = f"<a href=\"{text}\">{text.replace('https://', '')}</a>"
|
||||
super().__init__(text)
|
||||
self.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
self.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse)
|
||||
self.setOpenExternalLinks(True)
|
||||
|
||||
def break_at(self, string):
|
||||
self.setText(self.text().replace(f'{string}/', f'{string}/<br>'))
|
||||
|
||||
|
||||
class LineNumberArea(QWidget):
|
||||
def __init__(self, editor):
|
||||
QWidget.__init__(self, editor)
|
||||
self._code_editor = editor
|
||||
|
||||
def sizeHint(self):
|
||||
return QSize(self._code_editor.line_number_area_width(), 0)
|
||||
|
||||
def paintEvent(self, event):
|
||||
self._code_editor.line_number_area_paint_event(event)
|
||||
|
||||
|
||||
class CodeEditor(QPlainTextEdit):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.line_number_area = LineNumberArea(self)
|
||||
|
||||
self.blockCountChanged[int].connect(self.update_line_number_area_width)
|
||||
self.updateRequest[QRect, int].connect(self.update_line_number_area)
|
||||
self.cursorPositionChanged.connect(self.highlight_current_line)
|
||||
|
||||
def line_number_area_width(self):
|
||||
digits = len(str(self.blockCount()))
|
||||
space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
|
||||
return space
|
||||
|
||||
def resizeEvent(self, e):
|
||||
super().resizeEvent(e)
|
||||
cr = self.contentsRect()
|
||||
self.line_number_area.setGeometry(QRect(cr.left(),
|
||||
cr.top(),
|
||||
self.line_number_area_width(),
|
||||
cr.height()))
|
||||
|
||||
def line_number_area_paint_event(self, event):
|
||||
with QPainter(self.line_number_area) as painter:
|
||||
painter.fillRect(event.rect(), Qt.white)
|
||||
painter.setFont("Consolas")
|
||||
|
||||
block = self.firstVisibleBlock()
|
||||
block_number = block.blockNumber()
|
||||
|
||||
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
|
||||
bottom = top + self.blockBoundingRect(block).height()
|
||||
|
||||
while block.isValid() and top <= event.rect().bottom():
|
||||
if block.isVisible() and bottom >= event.rect().top():
|
||||
painter.setPen(Qt.gray)
|
||||
painter.drawText(-2, top,
|
||||
self.line_number_area.width(),
|
||||
self.fontMetrics().height(),
|
||||
Qt.AlignmentFlag.AlignRight,
|
||||
f"{block_number + 1}")
|
||||
|
||||
block = block.next()
|
||||
top = bottom
|
||||
bottom = top + self.blockBoundingRect(block).height()
|
||||
block_number += 1
|
||||
|
||||
def update_line_number_area_width(self):
|
||||
self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
|
||||
|
||||
def update_line_number_area(self, rect, dy):
|
||||
if dy:
|
||||
self.line_number_area.scroll(0, dy)
|
||||
else:
|
||||
self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
|
||||
|
||||
if rect.contains(self.viewport().rect()):
|
||||
self.update_line_number_area_width()
|
||||
|
||||
def highlight_current_line(self):
|
||||
extra_selections = []
|
||||
|
||||
if not self.isReadOnly():
|
||||
selection = QTextEdit.ExtraSelection()
|
||||
selection.format.setBackground(QColor(Qt.lightGray))
|
||||
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
|
||||
|
||||
selection.cursor = self.textCursor()
|
||||
selection.cursor.clearSelection()
|
||||
|
||||
extra_selections.append(selection)
|
||||
|
||||
self.setExtraSelections(extra_selections)
|
||||
|
||||
|
||||
class SpinBoxWithoutWheel(QSpinBox):
|
||||
def wheelEvent(self, event):
|
||||
event.ignore()
|
||||
|
||||
|
||||
class DoubleSpinBoxWithoutWheel(QDoubleSpinBox):
|
||||
def wheelEvent(self, event):
|
||||
event.ignore()
|
||||
|
||||
|
||||
class ProbabilitySpinBox(DoubleSpinBoxWithoutWheel):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.setSingleStep(0.01)
|
||||
self.valueChanged.connect(self.update_color)
|
||||
self.update_color()
|
||||
self.setMinimumWidth(75)
|
||||
|
||||
def update_color(self):
|
||||
if self.value() > 0:
|
||||
self.setStyleSheet("color: black;")
|
||||
else:
|
||||
self.setStyleSheet("color: gray;")
|
||||
|
||||
|
||||
class TimeEditWithoutWheel(QTimeEdit):
|
||||
def wheelEvent(self, event):
|
||||
event.ignore()
|
||||
|
||||
|
||||
class DirWidget(QWidget):
|
||||
def __init__(self, default_dir: Path | str | None = None):
|
||||
super().__init__()
|
||||
|
||||
self.directory = default_dir
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.setSpacing(2)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.dir_lineedit = QLineEdit()
|
||||
self.dir_lineedit.setFont("Consolas")
|
||||
self.dir_lineedit.setText(self.directory)
|
||||
|
||||
self.dir_set_btn = QPushButton()
|
||||
self.dir_set_btn.setIcon(QIcon("./gui/icons/ic_folder_open_48px.svg"))
|
||||
self.dir_set_btn.setIconSize(QSize(16, 16))
|
||||
self.dir_set_btn.setToolTip("Set directory with selection dialog")
|
||||
self.dir_set_btn.clicked.connect(self.set_dir)
|
||||
|
||||
self.dir_open_btn = QPushButton()
|
||||
self.dir_open_btn.setIcon(QIcon("./gui/icons/ic_open_in_new_24px.svg"))
|
||||
self.dir_open_btn.setIconSize(QSize(16, 16))
|
||||
self.dir_open_btn.setToolTip("Open directory")
|
||||
self.dir_open_btn.clicked.connect(self.open_dir)
|
||||
|
||||
layout.addWidget(self.dir_set_btn)
|
||||
layout.addWidget(self.dir_lineedit)
|
||||
layout.addWidget(self.dir_open_btn)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def open_dir(self):
|
||||
directory = Path(self.directory)
|
||||
os.startfile(directory)
|
||||
|
||||
def set_dir(self):
|
||||
directory = QFileDialog.getExistingDirectory(self, "Select a directory.", dir=self.directory)
|
||||
if directory:
|
||||
directory = Path(directory)
|
||||
|
||||
if Path.cwd() in directory.parents:
|
||||
self.directory = "./" + directory.relative_to(Path.cwd()).as_posix()
|
||||
elif Path.cwd() == directory:
|
||||
self.directory = "."
|
||||
else:
|
||||
self.directory = directory.as_posix()
|
||||
|
||||
self.dir_lineedit.setText(self.directory)
|
||||
|
||||
|
||||
class UrlLineEdit(QLineEdit):
|
||||
|
||||
def __init__(self, initial_text: str | None = None):
|
||||
super().__init__()
|
||||
self.setFont("Consolas")
|
||||
self.textChanged.connect(self.color_text)
|
||||
self.setText(initial_text)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
modifiers = QApplication.keyboardModifiers()
|
||||
if modifiers == Qt.ControlModifier:
|
||||
self.open_url()
|
||||
|
||||
def open_url(self):
|
||||
if validators.url(self.text()):
|
||||
webbrowser.open(self.text())
|
||||
|
||||
def color_text(self):
|
||||
if validators.url(self.text()):
|
||||
self.setStyleSheet("color: blue;")
|
||||
else:
|
||||
self.setStyleSheet("color: black;")
|
||||
|
||||
|
||||
class UrlWidget(QWidget):
|
||||
def __init__(self, initial_url: str | None = None):
|
||||
super().__init__()
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.setSpacing(2)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.url_lineedit = UrlLineEdit(initial_url)
|
||||
self.url_lineedit.textChanged.connect(self.toggle_url_open_btn)
|
||||
|
||||
self.url_open_btn = QPushButton()
|
||||
self.url_open_btn.setIcon(QIcon("./gui/icons/ic_open_in_new_24px.svg"))
|
||||
self.url_open_btn.setIconSize(QSize(16, 16))
|
||||
self.url_open_btn.setToolTip("Open in browser")
|
||||
self.url_open_btn.clicked.connect(self.url_lineedit.open_url)
|
||||
|
||||
self.toggle_url_open_btn()
|
||||
|
||||
layout.addWidget(self.url_lineedit)
|
||||
layout.addWidget(self.url_open_btn)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def toggle_url_open_btn(self):
|
||||
url_valid = bool(validators.url(self.url_lineedit.text()))
|
||||
self.url_open_btn.setEnabled(url_valid)
|
||||
|
||||
|
||||
class TelegraphingCheckbox(QCheckBox):
|
||||
hover = Signal(str)
|
||||
|
||||
def __init__(self, text: str, description_text: str | None = None):
|
||||
super().__init__(text)
|
||||
|
||||
self.description_text = description_text
|
||||
|
||||
def enterEvent(self, event):
|
||||
self.hover.emit(self.description_text)
|
||||
|
||||
def leaveEvent(self, event):
|
||||
self.hover.emit("")
|
2
main.py
|
@ -289,7 +289,7 @@ def create_deploy_frame(container, config):
|
|||
|
||||
frame_3.pack(side="top", fill="x")
|
||||
|
||||
does_ffmpeg_exists()
|
||||
#does_ffmpeg_exists()
|
||||
|
||||
return frame
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
[tool.poetry]
|
||||
name = "barotrauma-sunken-tapes"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Jaka <jaka@kompot.si>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
pyyaml = "^6.0.1"
|
||||
validators = "^0.22.0"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
|
@ -27,7 +27,7 @@
|
|||
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="0,0,64,42" origin="0.5,0.5" />
|
||||
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="0,0,117,76" depth="0.55" origin="0.5,0.5" />
|
||||
<Body width="117" height="76" />
|
||||
<LightComponent LightColor="0.0,0.85,0.0,0.7" range="4" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent>
|
||||
<LightComponent LightColor="0.0,0.85,0.0,0.7" range="4" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false" />
|
||||
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
|
||||
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
|
||||
<TickBox text="Play">
|
||||
|
@ -66,7 +66,7 @@
|
|||
<Price locationtype="military" multiplier="1.1" sold="false" />
|
||||
<Price locationtype="mine" multiplier="1.1" sold="false" />
|
||||
</Price>
|
||||
<Deconstruct time="20"></Deconstruct>
|
||||
<Deconstruct time="20"/>
|
||||
<Fabricate suitablefabricators="fabricator" requiredtime="45">
|
||||
<RequiredSkill identifier="mechanical" level="45" />
|
||||
<RequiredSkill identifier="electrical" level="55" />
|
||||
|
@ -79,7 +79,7 @@
|
|||
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="64,0,64,40" origin="0.5,0.5" />
|
||||
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="117,0,51,33" depth="0.55" origin="0.5,0.5" />
|
||||
<Body width="51" height="33" />
|
||||
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent>
|
||||
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"/>
|
||||
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
|
||||
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
|
||||
<TickBox text="Play">
|
||||
|
@ -118,7 +118,7 @@
|
|||
<Price locationtype="military" multiplier="1.1" sold="false" />
|
||||
<Price locationtype="mine" multiplier="1.1" sold="false" />
|
||||
</Price>
|
||||
<Deconstruct time="20"></Deconstruct>
|
||||
<Deconstruct time="20"/>
|
||||
<Fabricate suitablefabricators="fabricator" requiredtime="45">
|
||||
<RequiredSkill identifier="mechanical" level="45" />
|
||||
<RequiredSkill identifier="electrical" level="55" />
|
||||
|
@ -131,7 +131,7 @@
|
|||
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="128,0,64,40" origin="0.5,0.5" />
|
||||
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="168,0,51,33" depth="0.55" origin="0.5,0.5" />
|
||||
<Body width="51" height="33" />
|
||||
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent>
|
||||
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"/>
|
||||
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
|
||||
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
|
||||
<TickBox text="Play">
|
||||
|
@ -170,7 +170,7 @@
|
|||
<Price locationtype="military" multiplier="1.1" sold="false" />
|
||||
<Price locationtype="mine" multiplier="1.0" minavailable="1" />
|
||||
</Price>
|
||||
<Deconstruct time="20"></Deconstruct>
|
||||
<Deconstruct time="20"/>
|
||||
<Fabricate suitablefabricators="fabricator" requiredtime="45">
|
||||
<RequiredSkill identifier="mechanical" level="45" />
|
||||
<RequiredSkill identifier="electrical" level="55" />
|
||||
|
@ -183,7 +183,7 @@
|
|||
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="192,0,64,40" origin="0.5,0.5" />
|
||||
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="219,0,51,33" depth="0.55" origin="0.5,0.5" />
|
||||
<Body width="51" height="33" />
|
||||
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent>
|
||||
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"/>
|
||||
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
|
||||
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
|
||||
<TickBox text="Play">
|
||||
|
@ -222,7 +222,7 @@
|
|||
<Price locationtype="military" multiplier="1.0" minavailable="1" />
|
||||
<Price locationtype="mine" multiplier="1.1" sold="false" />
|
||||
</Price>
|
||||
<Deconstruct time="20"></Deconstruct>
|
||||
<Deconstruct time="20"/>
|
||||
<Fabricate suitablefabricators="fabricator" requiredtime="45">
|
||||
<RequiredSkill identifier="mechanical" level="45" />
|
||||
<RequiredSkill identifier="electrical" level="55" />
|
||||
|
@ -235,7 +235,7 @@
|
|||
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="256,0,64,40" origin="0.5,0.5" />
|
||||
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="270,0,51,33" depth="0.55" origin="0.5,0.5" />
|
||||
<Body width="51" height="33" />
|
||||
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent>
|
||||
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"/>
|
||||
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
|
||||
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
|
||||
<TickBox text="Play">
|
||||
|
@ -273,7 +273,7 @@
|
|||
<Price baseprice="{{ tape.price }}" soldeverywhere="false">{% for location in ["outpost", "city", "research", "military", "mine"] %}
|
||||
<Price locationtype="{{ location }}" multiplier="{{ tape.multipliers[loop.index0] }}" sold="{{ tape.sold[loop.index0] }}" minavailable="1" />{% endfor %}
|
||||
</Price>
|
||||
<Deconstruct time="10"></Deconstruct>
|
||||
<Deconstruct time="10"/>
|
||||
<Fabricate suitablefabricators="fabricator" displayname="recycleitem" requiredtime="{{ song_lengths[loop.index0]*0.5 }}">
|
||||
<RequiredSkill identifier="mechanical" level="25" />
|
||||
<RequiredSkill identifier="electrical" level="45" />
|
||||
|
|