Skip to content

Commit

Permalink
Merge pull request #13 from Isvvc/python-ui
Browse files Browse the repository at this point in the history
Python UI
  • Loading branch information
skjiisa authored Dec 29, 2020
2 parents 7c8ebc1 + 181c847 commit 6791613
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.DS_Store
qrcode.png
188 changes: 188 additions & 0 deletions Armor Export/Armor_Export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import PySimpleGUI as sg
import requests
import re
import json
from PIL import Image, ImageTk
from io import BytesIO
# Must be the MyQR fork at https://github.com/Isvvc/qrcode/
from MyQR import myqr

# Support HiDPI
import ctypes
import platform
if int(platform.release()) >= 8:
ctypes.windll.shcore.SetProcessDpiAwareness(True)

# Load the json file
with open('Armor Export\\ingredients.json', 'r') as json_file:
json_data = json_file.read()
ingredients = json.loads(json_data)
print(ingredients['modules'][0]['name'])

images = []
image_cache = dict()

nexus_images = []
nexus_images_loaded = 0

def image_data(url: str) -> bytes:
if url in image_cache:
return image_cache[url]
data = BytesIO(requests.get(url).content)
img = Image.open(data)

cur_width, cur_height = img.size
# TODO: only scale if the image is larger than this
# Nexusmods thumbnails are slightly below 400 pixels, so don't need to be resized.
if cur_width > 400 or cur_height > 400:
scale = min(400/cur_height, 400/cur_width)
img = img.resize((int(cur_width*scale), int(cur_height*scale)), Image.ANTIALIAS)

bio = BytesIO()
img.save(bio, format='PNG')
del img
image_cache[url] = bio.getvalue()
return image_cache[url]

def make_images_window(current_images):
global nexus_images_loaded
image_links = []
if current_images:
image_links = images
else:
image_links = [image[1] for image in nexus_images[:5]]
nexus_images_loaded = 5

col = sg.Column([[
sg.Checkbox('', key=f'Checkbox{index}'),
sg.Image(data=image_data(image), enable_events=True, key=f'Image{index}')
] for index, image in enumerate(image_links)], size=(500,1000), scrollable=True)

images_layout = [[
col,
sg.Column([
[sg.Button('Remove', key='RemoveCurrent') if current_images else sg.Button('Add', key='AddNexus')],
[sg.Button('Done')]
], element_justification='center')
]]
new_window = sg.Window('Images', images_layout, finalize=True)
return new_window

images_col = [
[sg.Text('Images')],
[sg.Listbox(images, size=(40,10), key='-LIST-')],
[
sg.Button('Remove selected', key='Remove'),
sg.Button('Preview selected', key='Preview'),
sg.Button('Preview all', key='PreviewAll')
]
]

preview_col = [
[sg.Image(key='-PREVIEW-')]
]

layout = [
[sg.Text("Nexusmods URL")],
[
sg.Input(key='-NEXUS_INPUT-'),
sg.Button('Open')
],
[sg.Text("Image URL")],
[
sg.Input(key='-URL_INPUT-'),
sg.Button('Add'),
sg.Button('Preview', key='PreviewInput')
],
[sg.Column(images_col), sg.Column(preview_col)],
[
sg.Button('Save images to module', key='SaveModule'),
sg.Button('Save images to mod', key='SaveMod'),
sg.Button('Save images to both', key='SaveBoth'),
sg.Checkbox('Generate QR code', default=True, key='GenerateQR')
],
[sg.Button('Quit')]
]

window = sg.Window('Armor Export', layout, finalize=True)
images_window = None

def save_ingredients():
global ingredients, window
with open('Armor Export\\ingredients.json', 'w') as json_file:
json_file.write(json.dumps(ingredients))
if window['GenerateQR'].get():
myqr.run(json.dumps(ingredients), level = 'L', save_dir='Armor Export')

while True:
active_window, event, values = sg.read_all_windows()
print(event)
if (event == sg.WINDOW_CLOSED and active_window == window) or event == 'Quit':
break

elif event == 'Add':
input = values['-URL_INPUT-']
if not input in images:
images.append(input)
window['-LIST-'].update(images)
window['-URL_INPUT-'].update('')

elif event == 'Remove':
[images.remove(x) for x in values['-LIST-']]
window['-LIST-'].update(images)

elif event == 'Preview':
selection = values['-LIST-']
if len(selection) > 0:
window['-PREVIEW-'].update(data=image_data(selection[0]))

elif event == 'PreviewInput':
input = values['-URL_INPUT-']
window['-PREVIEW-'].update(data=image_data(input))

elif event == 'PreviewAll':
if images_window is None:
images_window = make_images_window(current_images=True)

elif (event == sg.WINDOW_CLOSED and active_window == images_window) or event == 'Done':
images_window.close()
images_window = None

elif event == 'Open':
if images_window is None:
url = values['-NEXUS_INPUT-']
html = requests.get(url).text
nexus_images = re.findall('data-src="([^"]*)" data-sub-html="" data-exthumbimage="([^"]*)"', html)
images_window = make_images_window(current_images=False)

elif 'Image' in event:
# 'Image' has 5 characters, so remove the first 5 characters to get the index
index = event[5:]
images_window[f'Checkbox{index}'].update(not images_window[f'Checkbox{index}'].get())

elif event == 'AddNexus':
[images.append(nexus_images[i][0]) for i in range(min(len(nexus_images), nexus_images_loaded)) if images_window[f'Checkbox{i}'].get() and not nexus_images[i][0] in images]
window['-LIST-'].update(images)
images_window.close()
images_window = None

elif event == 'RemoveCurrent':
[images.pop(i) for i in reversed(range(len(images))) if images_window[f'Checkbox{i}'].get()]
window['-LIST-'].update(images)
images_window.close()
images_window = make_images_window(current_images=True)

elif event == 'SaveModule':
ingredients['modules'][0]['images'] = images
save_ingredients()

elif event == 'SaveMod':
ingredients['mods'][0]['images'] = images
save_ingredients()

elif event == 'SaveBoth':
ingredients['modules'][0]['images'] = images
ingredients['mods'][0]['images'] = images
save_ingredients()

window.close()
4 changes: 4 additions & 0 deletions Armor Export/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
PySimpleGUI
requests
pillow
git+https://github.com/Isvvc/qrcode.git
3 changes: 2 additions & 1 deletion Edit Scripts/Armor Stats and Requirements.pas
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ function Finalize: integer;

// Requires my modified myqr.exe compiled from https://github.com/Isvvc/qrcode
// This should have already come with this script if you downloaded it from Releases.
ShellExecute(0, nil, '"Armor Export\myqr.exe"', '-l L -d "Armor Export" "Armor Export\Ingredients.json"', nil, 0);
//ShellExecute(0, nil, '"Armor Export\myqr.exe"', '-l L -d "Armor Export" "Armor Export\Ingredients.json"', nil, 0);
ShellExecute(0, nil, '"Armor Export\Python38\python.exe"', '"Armor Export\Armor_Export.py"', nil, 0);

slIngredients.Free;
slIngredientNames.Free;
Expand Down
55 changes: 41 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ It is still totally usable without it, though.

### Dependencies

This script depends on `mteFunctions.pas` (included in release zips)
[Microsoft Visual C++ Redistributable for Visual Studio](https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads)

### Installation

1. Download `xEdit-Armor-Export.zip` from the [Releases tab](https://github.com/Isvvc/xEdit-Armor-Export/releases).
1. Extract to your xEdit (TES5Edit, TES5VREdit, etc.) folder, merging any contents.
If it asks to replace `mteFunctions.pas`, it doesn't matter either way.
+ Extracting `Armor Export\myqr.exe` is optional.
It is used for creating QR codes for Character Tracker.
You are welcome to remove it if you are not using the app.
+ If it asks to replace `mteFunctions.pas`, it shouldn't matter either way.
+ Extracting `Python38` and `Armor_Export.py` is optional.
These are used for creating QR codes for Character Tracker.
You are welcome to remove them if you are not using the app.

### Running

Expand All @@ -28,30 +28,57 @@ If it asks to replace `mteFunctions.pas`, it doesn't matter either way.
1. **Optional**: Enter a name for the mod if you want it shown in Character Tracker.
+ If you leave this empty, no mod entry will be created in the JSON.
+ This has no affect on the simple printout.
1. This will create an Ingredients.txt file in the `Armor Export` folder in your xEdit directory with the following information:
1. This will create an `Ingredients.txt` file in the `Armor Export` folder in your xEdit directory with the following information:
+ CSV list of crafting ingredients required
+ quantity, plugin name and FixedFormID, DisplayName
+ Combined armor rating
+ The armor type (of the first armor selected)
+ Calculated level based on type and rating
+ JSON that can be imported into Character Tracker
+ A QR code that can be scanned into Character Tracker if `myqr.exe` is present.
+ This can take some time to generate and may not complete until after the xEdit script has finished running.
+ Generating the QR code may fail if the JSON output is too large.
1. A GUI will launch for adding images and generating QR codes.
+ Manually add images with the **Image URL** text box
+ Enter a Nexusmods mod page in the **Nexusmods URL** text box to select images from the mod page to load.
+ Currently only the first 5 images will load.
+ Adult-only mod pages will not load as they require a user to be signed in, and this does not currently support signing in to an account.
+ When you have the images inputted that you'd like, click any of the **Save images to** buttons to save the images to the `Ingredients.json` file.
+ Check **Generate QR code** to generate a QR code that can be scanned into Character Tracker.
+ Note that this GUI is in early stages and can crash if text fields are left blank.
+ Generating QR codes with no images, however, should work just fine.

## Build

(You can ignore this section if you just download a release zip)

xEdit scripts are compiled at runtime, so no build is required for the Pascal script.

### MyQR
This script depends on [`mteFunctions.pas`](https://github.com/matortheeternal/TES5EditScripts/blob/master/Edit%20Scripts/mteFunctions.pas) (included in releases).

The `myqr.exe` included in Releases comes from [Isvvc/qrcode](https://github.com/Isvvc/qrcode/),
my slightly modified version of [sylnsfar/qrcode](https://github.com/sylnsfar/qrcode).
### Python

The Python UI is built using [PySimpleGUI](https://github.com/PySimpleGUI/PySimpleGUI).

#### WinPython

The image and QR code UI is built using Python.
To use a standalone Python distribution like the ones included in releases, you can download a Python 3.8dot release from [WinPython](https://winpython.github.io/).
Copy the Python folder (will look something like `python-3.8.6.amd64`) to `Armor Export` and rename it to `Python38`.

To install dependencies, navigate to the xEdit directory in Command Prompt and run the following command:

"Armor Export\Python\python.exe" -m pip install -r requirements.txt

You should now be able to access the UI from the xEdit script or by running:

"Armor Export\Python\python.exe" "Armor Export\Armor_Export.py"

Note that you must run this from the xEdit directory, not the `Armor Export` directory, or the relative file paths will be wrong.

#### MyQR

[Isvvc/qrcode](https://github.com/Isvvc/qrcode/) is required to generate QR codes.
This is a slightly modified version of [sylnsfar/qrcode](https://github.com/sylnsfar/qrcode).
The changes I made were simply adding `"` to the supported characters list so it could encode JSON
and allowing it to read input from a file so long JSON could be passed in without having to try to pass it as an argument.

Once Python dependencies are installed (presumably using `pip`), it can be built using [PyInstaller](https://www.pyinstaller.org/) for Windows.

pyinstaller -F myqr.py
Unlike previous releases, there is no `exe` file to generate. The included `python.exe` is run directly.

0 comments on commit 6791613

Please sign in to comment.