ARTICLE AD BOX
Working on the front end for a beeware briefcase app for android with a couiple of friends style engine is toga. I am trying to get some images to be clickable. Button icons arent big enough and I havent found a way to make them bigger. In addition, I havent found a way to layer the buttons and images which is a way we also explored which i now believe to be impossible, causing me to turn to my last resort: webview (which is something we want as little of as possible). So far it works (havent got the styling fully sorted but that isnt that big of an issue at the moment. Currently, when I press the image in the webview, it sends everything fine but it comes up with webpage not available for a fraction of a second. To get the button to work ive had to change the url in the javascript to a url that doesnt exist and then reset it back to the correct one.
Python:
assert 1 + 1 == 2 # --- IMPORTS --- from typing import Callable import httpx # type:ignore import toga # type:ignore from toga.style import Pack # type:ignore from toga.style.pack import COLUMN, ROW # type:ignore from toga.widgets import webview # type: ignore import asyncio import pathlib import threading import os from socketserver import TCPServer import json from http.server import SimpleHTTPRequestHandler, HTTPServer from datetime import datetime import time try: import toga_android.libs.location # type:ignore print("Force location sucessful") except Exception as e: # grr print("Force location failed, ", e) # --- CLASSES --- class jsApp(toga.App): def __init__(self, *args, **kwargs): self.permission_granted = False super().__init__(*args, **kwargs) self.locationList:dict = {} # used to store location data if player is a hunter self.gui = GUI(self) # -- ONSTART -- # gets perms on startup async def on_start(self): print("Location backend:", type(self.location)) print("Onstart:") granted = await self.location.request_permission() if not granted: print("You denied location. Please enable in settings") self.permission_granted = False return if granted: print("Permission granted!, getting current location") self.permission_granted = True # -- STARTUP -- def startup(self): self.main_window = toga.MainWindow(title=self.formal_name) self.main_window.content = self.gui.loadingScreen() asyncio.create_task(self.on_start()) # gets perms for location date async def on_running(self): # wait for permission_granted (avoid race) timeout = 10.0 waited = 0.0 while not getattr(self, "permission_granted", False) and waited < timeout: await asyncio.sleep(0.1) waited += 0.1 if not self.permission_granted: print("Permission not granted before on_running finished waiting; skipping location calls") else: print("Before location grab") asyncio.create_task(self.getCurrentLocation()) print("After location grab") if localPlayerId[1] == "i": # i for hider asyncio.create_task(self.sleepyTime(10, self.serverUpdateLocation, True)) elif localPlayerId[1] == "u": # u for hunter asyncio.create_task(self.sleepyTime(recieveLocationEvery, self.serverSendLocations, True)) else: print("Error: Something updated the ID") # begins getting location every few seconds asyncio.create_task(self.sleepyTime(getLocationEvery, self.getCurrentLocation, True)) self.main_window.content = self.gui.startupScreen() self.main_window.show() super().on_running() # -- BUTTONS -- def submitButton(self, button): global localPlayerId, currentIp localPlayerId = self.gui.idInput.value asyncio.create_task(self.endTimeGrab()) currentIp = self.gui.serverInput.value currentIp = "https://" + currentIp + ".ngrok-free.app" print(localPlayerId) print(currentIp) self.main_window.content = self.gui.waitingRoomScreen() self.main_window.show() asyncio.get_running_loop().create_task(self.gameLoop()) # self.gameLoop() # waiting room bypass def dieButtomn (self, button): self.main_window.content = self.gui.checkScreen() self.main_window.show() def hiderKilled(self, button): asyncio.create_task(self.become_hunter()) def hiderSafe(self, button): self.main_window.content = self.gui.hiderScreen() self.main_window.show() # -- SERVER INTERFACE -- # POST update-location async def serverUpdateLocation(self): print(localLat) print(localLong) print("Post request attempted") # remove this for final build try: async with httpx.AsyncClient(verify = False) as client: postAttempt = await client.post(currentIp + "/update-location", json={ "playerId": localPlayerId, "lat": localLat, "long": localLong }) self.updateStatusLabel(f"Server responds with: {postAttempt.json()}") except Exception as e: self.updateStatusLabel(f"Error within serverUpdateLocation: {e}") # POST caught async def serverCaught(self, id): print("caught") try: async with httpx.AsyncClient(verify = False) as client: postAttempt = await client.post(currentIp + "/caught", json={ "playerID": id }) print(f"caught: server responds with {postAttempt.json()}") except Exception as e: print("error within serverCaught:", e) # GET send-locations async def serverSendLocations(self): print("Get request attempted") # remove this for final build try: async with httpx.AsyncClient(verify = False) as client: getAttempt = await client.get(currentIp + "/send-locations") self.locationList = getAttempt.json() try: # self.rebuild_hunter_markers() # self.refreshMarkers() pass except Exception: pass print(getAttempt.json()) # remove this for final build self.updateStatusLabel(f"Location: {getAttempt.json()}") except Exception as e: self.updateStatusLabel(f"Error: {e}") print(f"Error within updateStatusLabel: {e}") # here # GET waiting async def waitingRoom(self): print("waiting room") try: async with httpx.AsyncClient(verify = False) as client: while True: print("waiting attempt") getAttempt = await client.get(currentIp + "/waiting") print(getAttempt) if getAttempt.json().get("status") == "started": print("game started") await self.gameLoop() return await asyncio.sleep(1) except Exception as e: print("failed to connect to waiting room:", e) # -- ASYNC -- async def sleepyTime(self, zzz : float, function : Callable = None, repeat : bool = False): if function is None: # ??? None -> not await asyncio.sleep(zzz) if repeat: while True: await asyncio.sleep(zzz) await function() else: if function is None: await asyncio.sleep(zzz) else: await asyncio.sleep(zzz) await function(zzz) def updateStatusLabel(self, text:str): if hasattr(self.main_window, "statusLabel"): self.gui.statusLabel.text = text print(text) async def endTimeGrab(self): """ Assumes time is in the format HHMM """ global endTime endTime = self.gui.timeInput.value print("end time grabbed:", endTime) def hiderTimer(self): global endTime now = datetime.now() currentTime = now.strftime("%H:%M:%S") currentTime = currentTime.split(":") hoursDifference = int(str(endTime)[0:2]) - int(currentTime[0]) minutesDifference = int(str(endTime)[2:]) - int(currentTime[1]) minutesDifference += hoursDifference * 60 minutesDifference -= 1 if int(currentTime[2]) != 0: secondsDifference = 60 - int(currentTime[2]) else: secondsDifference = 0 secondsDifference = str(secondsDifference) if len(secondsDifference) == 1: secondsDifference = "0" + secondsDifference timer = str(minutesDifference) + ":" + secondsDifference print(timer) global localPlayerId if hasattr(self, "timerLabel"): self.timerLabel.text = timer if hasattr(self.gui, "timerLabel"): self.gui.timerLabel.text = timer # sends a ping and checks perms # bug in here where an error is thrown if location is missed async def getCurrentLocation(self,*args, **kwargs):# try: print("Get current location ran") # remove for final build service = self.location loc = await service.current_location() if loc is not None: print("current location might be:", loc) print(loc.lat, loc.lng) global localLat global localLong localLat = loc.lat localLong = loc.lng else: print("Failed to get location") except Exception as e: print("Error within getCurrentLocation: ", e) # -- GAME LOGIC -- async def become_hunter(self): print("caught") global localPlayerId oldId = localPlayerId localPlayerId = "uu" + localPlayerId while True: try: await self.serverCaught(oldId) print("server connection sucessful") break except Exception as e: print("failed to connect to server:", e) asyncio.sleep(1) print("gameloop 2") await self.gameLoop() async def waitingRoomBackend(self): print("waiting room logic worked") # basically this needs to run the waiting room logic await self.waitingRoom() # this did have some more stuff in it async def gameLoop(self): # phew this is a long time in the making # logic for the game print("gameloop ran") async def hiderTimerLoop(): while True: self.hiderTimer() await asyncio.sleep(1) def hider(): self.main_window.content = self.gui.hiderScreen() self.main_window.show() asyncio.create_task(hiderTimerLoop()) async def hunter(): print("hunterscreen") # this never runs # self.main_window.content = self.gui.hunterScreen() # self.main_window.show() # self.show_hunter_map() # self.refreshMarkers() global recieveLocationEvery self.init_hunter_map() await hiderTimerLoop() await self.sleepyTime(recieveLocationEvery, self.refresh_hunter_markers, True) global localPlayerId if localPlayerId[1] == "i": # i for hider hider() elif localPlayerId[1] == "u": # u for hunter print("hunter") await hunter() else: print("?????") # -- AI CODE -- def init_hunter_map(self): import toga #type:ignore print("Initializing hunter map...") # Step 1: Start local server if not already running if not hasattr(self, "local_port"): self.start_local_server() # Step 2: Create WebView self.webview = toga.WebView(style = Pack( flex = 1 )) print("WebView created") # Step 3: Load map.html via http:// url = f"http://127.0.0.1:{self.local_port}/map.html" print("Loading URL:", url) self.webview.url = url # Step 4: Show it in the window box = toga.Box(style = Pack( flex = 1, direction = COLUMN, )) self.timerLabel = toga.Label("XX:XX", style = Pack( background_color = "#282828", color = "#9B9B9B", text_align = "center", flex = 1 )) box.add(self.timerLabel) box.add(self.webview) self.main_window.content = box self.main_window.show() print("Hunter map loaded.") async def refresh_hunter_markers(self): global currentIp """Fetch latest locations and update markers on the map.""" try: async with httpx.AsyncClient() as client: response = await client.get(f"{currentIp}/send-locations") if response.status_code == 200: locations = response.json() js = f"refreshMarkers({json.dumps(locations)});" self.webview.evaluate_javascript(js) else: print("Failed to fetch locations:", response.status_code) except Exception as e: print("Error refreshing hunter markers:", e) def start_local_server(self): def run_server(): # Try a list of candidate directories candidates = [] try: candidates.append(str(self.paths.app / "resources")) except Exception: pass # add working dir fallback candidates.append(os.getcwd()) chosen = None for c in candidates: try: if os.path.isdir(c): os.chdir(c) chosen = c break except Exception: pass if chosen is None: print("No valid resources directory found; local server won't run.") return with TCPServer(("127.0.0.1", 0), SimpleHTTPRequestHandler) as httpd: self.local_port = httpd.server_address[1] print(f"Local server running on port {self.local_port}, serving {chosen}") httpd.serve_forever() server_thread = threading.Thread(target=run_server, daemon=True) server_thread.start() class GUI: def __init__(self, app): self.app = app self.hunter = toga.Icon("icons/hunter-128") self.hider = toga.Icon("icons/hider-128") image_path = self.app.paths.app / "resources" / "helloworld.png" hiderPath = self.app.paths.app / "resources" / "hider.png" hunterPath = self.app.paths.app / "resources" / "hunter.png" self.image = toga.Image(image_path) self.hiderImage = toga.Image(hiderPath) self.hunterImage = toga.Image(hunterPath) self.imgLabel = toga.ImageView(self.image, style= Pack( width=78, height=78, justify_content="end" )) self.hiderLabel = toga.ImageView(self.hiderImage, style= Pack( width=150, height=150, )) self.hunterLabel = toga.ImageView(self.hunterImage, style= Pack( width=150, height=150, )) async def hunterMessageHandler (self, webview, **kwargs): global hunterID lastUrl = None defaultUrl = f"http://127.0.0.1:{self.app.local_port}/hunterButton.html" while True: await asyncio.sleep(0.01) if not hasattr(self, "hunterView"): continue url = self.hunterView.url if url != lastUrl: lastUrl = url if url == "toga://hunt": self.hunterView.evaluate_javascript(f'window.location = "http://127.0.0.1:{self.app.local_port}/hunterButton.html"') print("button clicked") jsApp.become_hunter print(f"url: {url}") hunterID = "hunter" + self.idInput.value print(f"id: {hunterID}") url = defaultUrl print(f"url: {url}") lastUrl = defaultUrl def loadingScreen(self): waitingLabel = toga.Label("Loading...", style = Pack( padding = 10, background_color = "#282828", color = "#9B9B9B", text_align = "center", )) self.versionLabel = toga.Label("Version 4", style = Pack( padding = 10, background_color = "#141414", color = "#9B9B9B", text_align = "center", )) self.versionBox = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414", height=50, justify_content="end" )) self.versionBox.add(self.versionLabel) loadingScreen = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414" )) loadingScreen.add(waitingLabel) loadingScreen.add(self.versionBox) return loadingScreen def startupScreen(self): print("startup screen ran") self.hunterView = toga.WebView(style = Pack( width=150, height=150, justify_content="center")) print("WebView created") if not hasattr(self.app, "local_port"): self.app.start_local_server() time.sleep(0.2) hunterOpen = self.app.paths.app / "resources" / "hunterButton.html" self.hunterView.url = f"http://127.0.0.1:{self.app.local_port}/hunterButton.html" asyncio.create_task(self.hunterMessageHandler(webview)) #server input field self.serverLabel = toga.Label("Server IP: ", style = Pack( padding = (0, 5), color = "#9B9B9B" )) self.serverInput = toga.TextInput(style=Pack( flex=1, color="#9B9B9B", background_color="#282828", )) self.serverBox = toga.Box(style=Pack( direction=ROW, padding=5, color="#9B9B9B", background_color="#282828" )) self.serverBox.add(self.serverLabel) self.serverBox.add(self.serverInput) # ID input field self.idLabel = toga.Label("Player ID: ", style=Pack( padding=(0, 5), color="#9B9B9B" )) self.idInput = toga.TextInput(style=Pack( flex=1, color="#9B9B9B", background_color="#282828", )) self.idBox = toga.Box(style=Pack( direction=ROW, padding=5, color="#9B9B9B", background_color="#282828" )) self.idBox.add(self.idLabel) self.idBox.add(self.idInput) #Game length input field self.timeLabel = toga.Label("Game time ends: ", style=Pack( padding=(0, 5), color="#9B9B9B", background_color="#282828", )) self.timeInput = toga.TextInput(style=Pack( flex=1, color="#9B9B9B", background_color="#282828", )) self.timeBox = toga.Box(style=Pack( direction=ROW, padding=5, background_color="#282828" )) self.timeBox.add(self.timeLabel) self.timeBox.add(self.timeInput) self.imageBox = toga.Box(style=Pack( direction=ROW, padding=5, background_color="#282828", flex=1, visibility="visible" )) self.imageBox.add(self.hunterLabel) self.imageBox.add(self.hiderLabel) self.idImageHide = toga.Button(icon=self.hider, style=Pack(width=150, height=150)) self.buttonbox = toga.Box(style=Pack( direction=ROW, padding=5, background_color="#282828", ), ) self.buttonbox.add(self.hunterView) self.buttonbox.add(self.idImageHide) self.typeSelectBox = toga.Box(style=Pack( direction=COLUMN, padding=5, background_color="#282828", justify_content="center"), children=[self.buttonbox, self.imageBox] ) # submit button self.submitButton = toga.Button("Submit", on_press = self.app.submitButton, style = Pack( padding = 5, background_color = "#282828", color = "#9B9B9B" )) self.versionLabel = toga.Label("Version 4", style = Pack( padding = 10, background_color = "#141414", color = "#9B9B9B", text_align = "center", )) self.versionBox = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414", height=50, justify_content="end" )) self.versionBox.add(self.versionLabel) self.startUpScreen = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414" )) self.startUpScreen.add(self.serverBox) self.startUpScreen.add(self.idBox) self.startUpScreen.add(self.timeBox) self.startUpScreen.add(self.typeSelectBox) self.startUpScreen.add(self.submitButton) self.startUpScreen.add(self.versionBox) return self.startUpScreen def waitingRoomScreen(self): self.waitingRoomLabel = toga.Label("Waiting for game start", style = Pack( padding = 10, background_color = "#282828", color = "#9B9B9B", text_align = "center" )) self.waitingRoomScreen = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414" )) self.versionLabel = toga.Label("Version 4", style = Pack( padding = 10, background_color = "#141414", color = "#9B9B9B", text_align = "center", )) self.versionBox = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414", height=50, justify_content="end" )) self.versionBox.add(self.versionLabel) self.waitingRoomScreen.add(self.waitingRoomLabel) self.waitingRoomScreen.add(self.versionBox) return self.waitingRoomScreen def hiderScreen(self): self.timerLabel = toga.Label("00:00", style = Pack( padding = 10, background_color = "#282828", color = "#9B9B9B", text_align = "center", height=175, font_size=76, horizontal_align_content="center" )) self.killButton = toga.Button("Caught?", on_press = self.app.dieButtomn, style = Pack( padding = 5, background_color = "#282828", color = "#9B9B9B" )) self.hiderLabel = toga.Label("HIDER", style = Pack( padding = 10, color = "#9B9B9B", text_align = "center", font_size=36, flex=1 )) self.topBox = toga.Box(style = Pack( direction = ROW, background_color = "#141414", height=150, width=400, justify_content="center" )) self.topBox.add(self.hiderLabel) self.versionLabel = toga.Label("Version 4", style = Pack( padding = 10, background_color = "#141414", color = "#9B9B9B", text_align = "center", )) self.versionBox = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414", height=50, justify_content="end" )) self.versionBox.add(self.versionLabel) self.fillerBoxillerBox = toga.Box(style = Pack(flex=1)) self.hiderScreenView = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414" )) self.hiderScreenView.add(self.topBox) self.hiderScreenView.add(self.timerLabel) self.hiderScreenView.add(self.killButton) self.hiderScreenView.add(self.fillerBoxillerBox) self.hiderScreenView.add(self.versionBox) return self.hiderScreenView def checkScreen(self): self.aliveQuestion = toga.Label("Are you sure?", style = Pack( flex = 1, padding = 10, background_color = "#282828", color = "#9B9B9B", text_align = "center" )) self.deadButton = toga.Button("Yes", on_press = self.app.hiderKilled, style = Pack( flex = 1, padding = 5, background_color = "#282828", color = "#9B9B9B" )) self.aliveButton = toga.Button("No", on_press = self.app.hiderSafe, style = Pack( flex = 1, padding = 5, background_color = "#282828", color = "#9B9B9B" )) self.checkTextBox = toga.Box(style = Pack( direction = ROW )) self.checkTextBox.add(self.aliveQuestion) self.checkButtonBox = toga.Box(style = Pack( direction = ROW, flex = 1, justify_content = "center", padding = 10, )) self.checkButtonBox.add(self.deadButton) self.checkButtonBox.add(self.aliveButton) self.versionLabel = toga.Label("Version 4", style = Pack( padding = 10, background_color = "#141414", color = "#9B9B9B", text_align = "center", )) self.versionBox = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414", height=50, justify_content="end" )) self.versionBox.add(self.versionLabel) self.checkScreenView = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414" )) self.checkScreenView.add(self.checkTextBox) self.checkScreenView.add(self.checkButtonBox) self.checkScreenView.add(self.versionBox) return self.checkScreenView def hunterScreen(self): self.tempLabel = toga.Label("Hunter screen, temp", style = Pack( padding = 10, background_color = "grey", color = "black", text_align = "center" )) self.timerLabel = toga.Label("XX:XX", style = Pack( padding = 10, background_color = "#282828", color = "#9B9B9B", text_align = "center" )) self.hunterScreenView = toga.Box(style = Pack( direction = COLUMN, background_color = "#141414" )) self.hunterScreenView.add(self.tempLabel) return self.hunterScreenView # --- FUNCTIONS --- def main(): return jsApp()html relevant:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hunter Button</title> </head> <body style="margin: 0; width: 200px; height: 200px;"> <img src="hunter.png" alt="" id="hunterButton" style="width: 100%; height: 100%; position: center; float: left;" onclick="huntFunction()"> </body> <script> const msg = "uu"; function huntFunction (Toga) { console.log("clicked"); window.location = "toga://hunt" } </script> </html>everything in the class jsApp can be ignored unless necessary. Any tips on how i could:
1.get full size icons on the buttons
2.layer images over buttons
3.get the android webpage not available thing to not appear/ reload the correct url faster.
final aim for the buttons is to store the name of the user with either hunter or hider before it and keep the image highlighted until either the game starts (not in here as not my problem) or the other button has been clicked (im gonna wait until ive got one button fully working first before sorting this out.
