diff --git a/api.py b/api.py new file mode 100644 index 0000000..882a650 --- /dev/null +++ b/api.py @@ -0,0 +1,31 @@ +import requests + +BASE_URL = "https://api.spacetraders.io/v2" + +def handleResponse(res:requests.Response): + code = res.status_code + try: + data = res.json() + except: + data = None + return code, data + +def newAgent(accToken, callsign, faction='COSMIC'): + header = { + "Authorization": f"Bearer {accToken}", + "Content-Type" : "application/json" + } + params = { + "symbol" : callsign, + "faction" : faction + } + url = f"{BASE_URL}/register" + return handleResponse(requests.post(url, json=params, headers=header)) + +def viewAgent(agentToken): + header = { + "Authorization": f"Bearer {agentToken}", + "Content-Type" : "application/json" + } + url = f"{BASE_URL}/my/agent" + return handleResponse(requests.get(url, headers=header)) \ No newline at end of file diff --git a/mainwindow.py b/mainwindow.py new file mode 100644 index 0000000..f9b20e7 --- /dev/null +++ b/mainwindow.py @@ -0,0 +1,127 @@ +#builtins +import tkinter as Tk +from tkinter import ttk as Ttk +from tkinter import font +from os import path + +#packages +from PIL import ImageTk, Image + +#custom stuff +from storage import readSave, save +import api, ui + +#load icon +icoPng = Image.open(path.join("assets","babynet.png")) + +#initialize root +root = Tk.Tk() +root.title("SPASESHUPS") +ico = ImageTk.PhotoImage(icoPng) +root.wm_iconphoto(False, ico) + +#add the three main sections +#header contains auth keys and such +headerFrame = ui.Container(root, freezeCols=1) +headerFrame.widget.grid(row=0, column=0, sticky='new') + +#requestFrame is the main body where the request buttons will be +requestFrame = ui.Container(root) +requestFrame.widget.grid(row=1, column=0, sticky='nesw') + +#response frame is where the output lives +responseFrame = ui.Container(root) +responseFrame.widget.grid(row=2, column=0, sticky="snew") + +#populate the header +accToken = Tk.StringVar() +accAuthBox = ui.SavedEntryBox(headerFrame.widget, 'acc_token', accToken) +Ttk.Label(headerFrame.widget, text="ACCOUNT TOKEN:").grid(row=0, column=0, sticky='e') +accAuthBox.widget.grid(row=0, column=1, columnspan=5, sticky='ew') + +agentToken = Tk.StringVar() +agentAuthBox = ui.SavedEntryBox(headerFrame.widget, 'active_agent_token', agentToken) +Ttk.Label(headerFrame.widget, text="AGENT TOKEN:").grid(row=1, column=0, sticky='e') +agentAuthBox.widget.grid(row=1, column=1, columnspan=5, sticky='ew') +#freeze the label column +headerFrame.widget.columnconfigure(0, weight=0) + +#populate and style the response frame +resLabel = Tk.Text(responseFrame.widget, background='grey17', foreground='white') +resLabel.grid(row=0, column=0, sticky='news') +resLabel.insert('1.0', "Awaiting call...") +resLabel['state'] = 'disabled' +resScrollbarV = Ttk.Scrollbar(responseFrame.widget, orient='vertical', command=resLabel.yview) +resLabel.configure(yscrollcommand=resScrollbarV.set) +resScrollbarV.grid(row=0, column=1, rowspan=2, sticky='ns') + +def updateResponse(newText): + resLabel['state'] = 'normal' + resLabel.delete('1.0', 'end') + resLabel.insert('1.0', newText) + resLabel['state'] = 'disabled' + +Ttk.Style().configure('Shaded.TFrame', background='grey17') +responseFrame.widget.configure(borderwidth=2, relief='groove', style='Shaded.TFrame') + +#populate the request frame +makeFrame = ui.RequesterFrame(requestFrame.widget, 'CREATE') +makeFrame.widget.grid(row=0, column=0, sticky='nsew') + +#new agent +callsignVar = Tk.StringVar() +factionVar = Tk.StringVar() +ui.FieldLabel(makeFrame.widget, 'Callsign:', 1) +ui.EntryBox(makeFrame.widget, callsignVar).widget.grid(row=1, column=2, sticky='ew') +ui.FieldLabel(makeFrame.widget, 'Faction Name:', 2) +ui.EntryBox(makeFrame.widget, factionVar).widget.grid(row=2, column=2, sticky='ew') +def newAgent(args): + resCode, resData = api.newAgent(*args) + updateResponse(ui.formatResponse(resCode, resData)) + try: + agentToken.set(resData['token']) + if int(resCode) < 300: + agentAuthBox.saveValue() + except: + pass + return resCode, resData +newAgentButton = ui.ApiButton(makeFrame.widget, 'NEW AGENT', 3, newAgent, (accToken, callsignVar, factionVar)) + +viewFrame = ui.RequesterFrame(requestFrame.widget, 'VIEW') +viewFrame.widget.grid(row=0, column=1, sticky='nesw') + +#view agent +def viewAgent(args): + resCode, resData = api.viewAgent(*args) + updateResponse(ui.formatResponse(resCode, resData)) + return resCode, resData +viewAgentButton = ui.ApiButton(viewFrame.widget, 'VIEW AGENT', 2, viewAgent, (agentToken)) + +useFrame = ui.RequesterFrame(requestFrame.widget, 'USE') +useFrame.widget.grid(row=0, column=2, sticky='nsew') + + +#finalize the layout +makeFrame.finalize() +useFrame.finalize() +viewFrame.finalize() + +headerFrame.finalize() +requestFrame.finalize() + +#response frame finalize doesnt help much so do it manually +responseFrame.widget.grid_rowconfigure(0, weight=1) +responseFrame.widget.grid_columnconfigure(0, weight=1) +responseFrame.widget.grid_columnconfigure(1, weight=0) + +for child in root.winfo_children(): + child.grid_configure(padx = 5, pady = 5) + +#sort out scaling for the top 3 frames +root.columnconfigure(0, weight=1) +root.rowconfigure(0, weight=0) +root.rowconfigure(1, weight=1) +root.rowconfigure(2, weight=1, minsize=300) + +#aaaand go +root.mainloop() \ No newline at end of file diff --git a/storage.py b/storage.py new file mode 100644 index 0000000..05a8013 --- /dev/null +++ b/storage.py @@ -0,0 +1,35 @@ +from os import path +import json + +SAVE_FILE = path.join("data", "save.dat") + +def readSave(*fields): + try: + with open(SAVE_FILE, 'r') as f: + out = {} + obj = json.load(f) + for field in fields: + try: + out[field] = obj[field] + except KeyError: + out[field] = None + return out + except FileNotFoundError: + with open(SAVE_FILE, 'w') as f: + json.dump({'acc_token': 0}, f, indent=4) + return readSave(*fields) + +def save(fieldsDic): + try: + obj = None + with open(SAVE_FILE, 'r') as f: + out = {} + obj = json.load(f) + obj.update(fieldsDic) + with open(SAVE_FILE, 'w') as f: + json.dump(obj, f, indent=4) + return True + except FileNotFoundError: + with open(SAVE_FILE, 'w') as f: + json.dump(fieldsDic, f, indent=4) + return True \ No newline at end of file diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..e62e3d8 --- /dev/null +++ b/ui.py @@ -0,0 +1,128 @@ +import tkinter as Tk +from tkinter import ttk as Ttk +from tkinter import font +from pprint import pformat +import threading + +from storage import readSave, save + +class FieldLabel(): + """ + Label to set alongside a text box. + """ + def __init__(self, parent, text, rowNum): + self.widget = Ttk.Label(parent, text=text) + self.widget.grid(row=rowNum, column=1, sticky='e') + +class EntryBox(): + """ + Basic entry box class. + """ + def __init__(self, parent, textVar=None): + if textVar is None: + self.textVar = Tk.StringVar() + else: + self.textVar = textVar + self.widget = Ttk.Entry(parent, width=12, textvariable=self.textVar) + +class SavedEntryBox(EntryBox): + """ + Entry box that saves values to file with a given field name. Reads from that field on launch. + """ + def __init__(self, parent, fieldName, textVar, saveOnEnter=True, saveOnChange=False): + self.textVar = textVar + super().__init__(parent, self.textVar) + self.fieldName = fieldName + + value = readSave(fieldName)[fieldName] + if value is not None: + self.textVar.set(value) + else: + self.textVar.set(fieldName) + + if saveOnEnter: + self.widget.bind("", self.saveValue) + + if saveOnChange: + self.textVar.trace_add("write", self.saveValue) + + def saveValue(self, *args): + save({self.fieldName: self.textVar.get()}) + + +class Container(): + def __init__(self, parent, freezeCols=0): + self.widget = Ttk.Frame(parent, padding=(3)) + self.freezeCols = freezeCols + + def finalize(self): + cols, rows = self.widget.grid_size() + if self.freezeCols > 0: + for i in range(self.freezeCols): + self.widget.grid_columnconfigure(i, weight=0) + for i in range(self.freezeCols, cols): + self.widget.grid_columnconfigure(i, weight=1, uniform='column') + + for i in range(rows): + self.widget.grid_rowconfigure(i, weight=0) + + +class RequesterFrame(Container): + """ + Top-level frame for classes of manipulations. + """ + def __init__(self, parent, label): + super().__init__(parent) + self.widget.configure(height=480, width=330, borderwidth=3, relief="raised") + self.widget.bind('', lambda e: self.widget.configure(relief="sunken")) + self.widget.bind('', lambda e: self.widget.configure(relief="raised")) + + self.label = Ttk.Label(self.widget, text=label, font=font.nametofont('TkHeadingFont')) + self.label.grid(row=0, column=0, sticky='nw') + + def finalize(self): + cols, rows = self.widget.grid_size() + self.widget.grid_columnconfigure(0, weight=0) + self.widget.grid_columnconfigure(1, weight=0) + self.widget.grid_columnconfigure(2, weight=1, minsize=150) + + for i in range(rows): + self.widget.grid_rowconfigure(i, weight=1, minsize=5) + + +class ApiButton(): + """ + Button which triggers an API call when clicked. + """ + def __init__(self, parent, label, rowNum, apiFunc, args): + self.call = apiFunc + self.args = args + self.row = rowNum + self.parent = parent + self.widget = Ttk.Button(parent, command=self.pressed, text=label) + self.widget.grid(row=rowNum, column=1, columnspan=2, sticky='nsew') + + def pressed(self, *args): + progbar = Ttk.Progressbar(self.parent, orient='horizontal', mode='indeterminate', length=10) + progbar.grid(row=self.row, column=0) + threading.Thread(target=self.thread, args=(progbar,)).start() + + def thread(self, progbar): + toPass = [] + if isinstance(self.args, (list, tuple)): + for arg in self.args: + if type(arg) is Tk.StringVar: + toPass.append(arg.get()) + else: + toPass.append(arg) + else: + if type(self.args) is Tk.StringVar: + toPass.append(self.args.get()) + else: + toPass.append(self.args) + self.resCode, self.resData = self.call(toPass) + progbar.grid_forget() + +def formatResponse(resCode, resData): + return f"""Response {resCode}: +{pformat(resData)}"""