import kivy from kivy.app import App from kivy.uix.screenmanager import ScreenManager, Screen import os import json from kivy.clock import Clock from kivy.properties import StringProperty, ListProperty from utils import ( generate_key, load_key, encrypt_data, decrypt_data, check_server_settings, save_server_settings, test_connection, get_devices_from_server, save_route_to_file, fetch_positions_for_selected_day ) from datetime import date from kivy.uix.popup import Popup from kivy.uix.gridlayout import GridLayout from kivy.uix.button import Button from kivy.uix.label import Label from kivy.uix.boxlayout import BoxLayout from threading import Thread from kivy.clock import mainthread from kivy.uix.image import Image from kivy.uix.behaviors import ButtonBehavior from kivy.uix.progressbar import ProgressBar import webbrowser from selenium import webdriver from selenium.webdriver.chrome.options import Options from PIL import Image import time kivy.require("2.0.0") from kivy.core.window import Window Window.size = (360, 780) RESOURCES_FOLDER = "resources" CREDENTIALS_FILE = os.path.join(RESOURCES_FOLDER, "credentials.enc") class LoginScreen(Screen): def login(self): username = self.ids.username_input.text.strip() password = self.ids.username_input.text.strip() if not username or not password: self.manager.get_screen("home").ids.result_label.text = "Please fill in all fields." return credentials = {"username": username, "password": password} encrypted_data = encrypt_data(json.dumps(credentials)) with open(CREDENTIALS_FILE, "wb") as file: file.write(encrypted_data) self.manager.current = "home" class SettingsScreen(Screen): server_response = "Waiting to test connection..." def on_pre_enter(self): settings = check_server_settings() if settings: self.ids.server_url_input.text = settings.get("server_url", "") self.ids.username_input.text = settings.get("username", "") self.ids.password_input.text = settings.get("password", "") self.ids.token_input.text = settings.get("token", "") else: self.ids.server_url_input.text = "" self.ids.username_input.text = "" self.ids.password_input.text = "" self.ids.token_input.text = "" def test_connection(self): server_url = self.ids.server_url_input.text.strip() username = self.ids.username_input.text.strip() password = self.ids.password_input.text.strip() token = self.ids.token_input.text.strip() result = test_connection(server_url, username, password, token) self.server_response = result["message"] self.ids.result_label.text = self.server_response def save_settings(self): server_url = self.ids.server_url_input.text.strip() username = self.ids.username_input.text.strip() password = self.ids.password_input.text.strip() token = self.ids.token_input.text.strip() if not server_url or not username or not password or not token: self.ids.result_label.text = "Please fill in all fields." return settings_data = { "server_url": server_url, "username": username, "password": password, "token": token, } try: save_server_settings(settings_data) self.ids.result_label.text = "Settings saved successfully!" self.manager.current = "home" except Exception as e: self.ids.result_label.text = f"Failed to save settings: {str(e)}" # get trip from server screen class GetTripFromServer(Screen): server_info_text = StringProperty("LOADING DATA...") server_box_color = ListProperty([0.984, 0.553, 0.078, 1]) device_mapping = {} def on_pre_enter(self): self.server_box_color = [0.984, 0.553, 0.078, 1] self.server_info_text = "LOADING DATA..." Clock.schedule_once(self.check_server_settings, 1) def check_server_settings(self, dt): settings = check_server_settings() if settings: server_url = settings["server_url"] username = settings["username"] password = settings["password"] token = settings["token"] self.server_info_text = f"CHECKING server: {server_url}" self.server_box_color = [0.984, 0.553, 0.078, 1] self.ids.devices_spinner.text = "Loading devices..." Clock.schedule_once(lambda dt: self.test_connection(server_url, username, password, token), 1) else: self.server_info_text = "Go to settings and set Traccar Server" self.server_box_color = [0.909, 0.031, 0.243, 1] self.ids.devices_spinner.text = "No devices available" def set_result_message(self, message, color=(1, 1, 1, 1)): self.ids.result_label.text = message self.ids.result_label.color = color def update_devices_spinner(self, devices): if devices: self.device_mapping = devices device_names = list(devices.keys()) self.ids.devices_spinner.values = device_names self.ids.devices_spinner.text = "Select a device" else: self.ids.devices_spinner.text = "No devices found" self.ids.devices_spinner.values = [] def test_connection(self, server_url, username, password, token): result = test_connection(server_url, username, password, token) if result["status"]: self.server_info_text = f"Connected to {server_url}" self.server_box_color = [0.008, 0.525, 0.290, 1] devices = get_devices_from_server() self.update_devices_spinner(devices) else: self.server_info_text = result["message"] self.server_box_color = [0.909, 0.031, 0.243, 1] def on_device_selected(self, device_name): if device_name != "Loading devices..." and device_name != "No devices found": self.ids.devices_spinner.background_color = (0.008, 0.525, 0.290, 1) else: self.ids.devices_spinner.background_color = (1, 1, 1, 1) def open_date_picker(self, which): from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.button import Button from kivy.uix.label import Label from kivy.uix.popup import Popup import calendar today = date.today() selected = {"year": today.year, "month": today.month} def update_grid(): grid.clear_widgets() days_in_month = calendar.monthrange(selected["year"], selected["month"])[1] for day in range(1, days_in_month + 1): btn = Button( text=str(day), size_hint=(None, None), size=(38, 38), background_color=(0.341, 0.235, 0.980, 1), color=(1, 1, 1, 1), font_size=15 ) def on_date_selected(instance, day=day): date_str = f"{selected['year']}-{selected['month']:02d}-{day:02d}" if which == 'start': self.ids.start_date_picker_button.text = date_str else: self.ids.end_date_picker_button.text = date_str popup.dismiss() btn.bind(on_press=on_date_selected) grid.add_widget(btn) def prev_month(instance): if selected["month"] > 1: selected["month"] -= 1 else: selected["year"] -= 1 selected["month"] = 12 # Don't allow future months if (selected["year"], selected["month"]) > (today.year, today.month): selected["year"], selected["month"] = today.year, today.month title_label.text = f"Select a Date ({selected['year']}-{selected['month']:02d})" update_grid() def next_month(instance): # Only allow up to current month if (selected["year"], selected["month"]) < (today.year, today.month): if selected["month"] < 12: selected["month"] += 1 else: selected["year"] += 1 selected["month"] = 1 title_label.text = f"Select a Date ({selected['year']}-{selected['month']:02d})" update_grid() def on_cancel(instance): popup.dismiss() # Main vertical layout main_layout = BoxLayout(orientation='vertical', spacing=10, padding=10) # Month navigation row nav_layout = BoxLayout(orientation='horizontal', size_hint_y=None, height=40, spacing=10) prev_btn = Button(text="<", size_hint_x=None, width=40, font_size=18, background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1)) next_btn = Button(text=">", size_hint_x=None, width=40, font_size=18, background_color=(0.341, 0.235, 0.980, 1), color=(1,1,1,1)) title_label = Label( text=f"Select a Date ({selected['year']}-{selected['month']:02d})", size_hint_x=1, font_size=18, color=(1, 1, 1, 1) ) prev_btn.bind(on_press=prev_month) next_btn.bind(on_press=next_month) nav_layout.add_widget(prev_btn) nav_layout.add_widget(title_label) nav_layout.add_widget(next_btn) main_layout.add_widget(nav_layout) # Grid of days: 6 columns grid = GridLayout(cols=6, spacing=6, size_hint_y=None) grid.bind(minimum_height=grid.setter('height')) main_layout.add_widget(grid) # Cancel button cancel_btn = Button( text="Cancel", size_hint_y=None, height=44, background_color=(0.909, 0.031, 0.243, 1), color=(1, 1, 1, 1), font_size=16 ) cancel_btn.bind(on_press=on_cancel) main_layout.add_widget(cancel_btn) popup = Popup( title="", content=main_layout, size_hint=(0.95, 0.8), background_color=(0.11, 0.10, 0.15, 1), separator_height=0, auto_dismiss=False ) update_grid() popup.open() def open_hour_picker(self, which): from kivy.uix.popup import Popup from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.scrollview import ScrollView from kivy.uix.gridlayout import GridLayout popup_layout = BoxLayout(orientation='vertical', spacing=8, padding=8) scroll = ScrollView(size_hint=(1, 1)) grid = GridLayout(cols=1, size_hint_y=None, spacing=4) grid.bind(minimum_height=grid.setter('height')) # Add hour buttons for h in range(24): hour_str = f"{h:02d}" btn = Button( text=hour_str, size_hint_y=None, height=44, font_size=18, background_color=(0.341, 0.235, 0.980, 1), color=(1, 1, 1, 1) ) def set_hour(instance, hour=hour_str): if which == 'start': self.ids.start_hour_button.text = hour else: self.ids.end_hour_button.text = hour popup.dismiss() btn.bind(on_press=set_hour) grid.add_widget(btn) scroll.add_widget(grid) popup_layout.add_widget(scroll) # Cancel button cancel_btn = Button( text="Cancel", size_hint_y=None, height=44, background_color=(0.909, 0.031, 0.243, 1), color=(1, 1, 1, 1), font_size=16 ) cancel_btn.bind(on_press=lambda x: popup.dismiss()) popup_layout.add_widget(cancel_btn) popup = Popup( title="Select Hour", content=popup_layout, size_hint=(0.6, 0.7), auto_dismiss=False ) popup.open() def update_points_count(self, count): """Update the label showing the number of points.""" self.ids.points_count_label.text = f"Points: {count}" def save_route(self): """Save the current list of positions as a route in resources/projects//positions.json.""" route_name = self.ids.route_name_input.text.strip() positions = getattr(self, "last_positions", None) success, message, file_path = save_route_to_file(route_name, positions) self.ids.result_label.text = message if success: # Reset UI fields self.ids.devices_spinner.text = "Select a device" self.ids.start_date_picker_button.text = "Select Start Date" self.ids.end_date_picker_button.text = "Select End Date" self.ids.start_hour_button.text = "00" self.ids.end_hour_button.text = "23" self.ids.points_count_label.text = "Points: 0" self.ids.route_name_input.text = "" # Show popup and schedule reroute to home popup = Popup(title="Success", content=Label(text=f"Route '{route_name}' saved!"), size_hint=(None, None), size=(300, 150), auto_dismiss=False) popup.open() def close_and_go_home(dt): popup.dismiss() self.manager.current = "home" Clock.schedule_once(close_and_go_home, 3) def get_trip_server_data(self): """Handle the Get trip server data button press.""" selected_device = self.ids.devices_spinner.text start_date = self.ids.start_date_picker_button.text end_date = self.ids.end_date_picker_button.text if selected_device == "Loading devices..." or selected_device == "No devices found": print("No valid device selected.") self.ids.result_label.text = "Please select a valid device." return if start_date == "Select Date" or end_date == "Select Date": print("No valid date selected.") self.ids.result_label.text = "Please select valid start and end dates." return # Fetch trip data from the server print(f"Fetching trip data for device: {selected_device} from {start_date} to {end_date}") self.ids.result_label.text = f"Fetching trip data for {selected_device} from {start_date} to {end_date}..." positions = self.fetch_positions_for_selected_day() self.last_positions = positions # Store for saving self.update_points_count(len(positions) if positions else 0) if positions: print("Positions received:") for pos in positions: print(f"{pos['deviceTime']}: {pos['latitude']}, {pos['longitude']}") else: print("No positions found or error occurred.") def fetch_positions_for_selected_day(self): settings = check_server_settings() device_name = self.ids.devices_spinner.text start_date = self.ids.start_date_picker_button.text end_date = self.ids.end_date_picker_button.text start_hour = self.ids.start_hour_button.text end_hour = self.ids.end_hour_button.text positions, error = fetch_positions_for_selected_day( settings, self.device_mapping, device_name, start_date, end_date, start_hour, end_hour ) if error: self.ids.result_label.text = error return [] self.ids.result_label.text = f"Retrieved {len(positions)} positions." return positions def fetch_devices_async(self): Thread(target=self._fetch_devices_worker).start() def _fetch_devices_worker(self): devices = get_devices_from_server() self.update_devices_spinner_mainthread(devices) @mainthread def update_devices_spinner_mainthread(self, devices): self.update_devices_spinner(devices) # trhis screen is used to create a new user # register screen class RegisterScreen(Screen): def create_user(self): """Handle user creation.""" username = self.ids.set_username_input.text.strip() password = self.ids.set_password_input.text.strip() confirm_password = self.ids.confirm_password_input.text.strip() email = self.ids.set_email_input.text.strip() if not username or not password or not confirm_password or not email: self.ids.result_label.text = "Please fill in all fields." return if password != confirm_password: self.ids.result_label.text = "Passwords do not match." return # Check if the username or email already exists if self.user_exists(username, email): self.ids.result_label.text = "User or email already exists." return # Save user data (encrypted) user_data = { "username": username, "password": password, "email": email, } encrypted_data = encrypt_data(json.dumps(user_data)) try: with open(CREDENTIALS_FILE, "ab") as file: # Append encrypted data file.write(encrypted_data + b"\n") # Add a newline for separation except Exception as e: self.ids.result_label.text = f"Failed to save user: {str(e)}" return self.ids.result_label.text = "User created successfully!" # Navigate back to the login screen self.manager.current = "login" def user_exists(self, username, email): """Check if a username or email already exists in the credentials.enc file.""" try: with open(CREDENTIALS_FILE, "rb") as file: for line in file: decrypted_data = decrypt_data(line.strip()) user = json.loads(decrypted_data) if user["username"] == username or user["email"] == email: return True except FileNotFoundError: pass except Exception as e: self.ids.result_label.text = f"Error checking user: {str(e)}" return False class CreateAnimationScreen(Screen): project_name = StringProperty("") preview_html_path = StringProperty("") # Path to the HTML file for preview preview_image_path = StringProperty("") # Add this line def on_pre_enter(self): # Update the route entries label with the actual number of entries project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) positions_path = os.path.join(project_folder, "positions.json") count = 0 if os.path.exists(positions_path): with open(positions_path, "r") as f: try: positions = json.load(f) count = len(positions) except Exception: count = 0 self.ids.route_entries_label.text = f"Your route has {count} entries," def open_rename_popup(self): from kivy.uix.popup import Popup from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.textinput import TextInput from kivy.uix.label import Label layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Enter new project name:") input_field = TextInput(text=self.project_name, multiline=False) btn_save = Button(text="Save", background_color=(0.008, 0.525, 0.290, 1)) btn_cancel = Button(text="Cancel") layout.add_widget(label) layout.add_widget(input_field) layout.add_widget(btn_save) layout.add_widget(btn_cancel) popup = Popup(title="Rename Project", content=layout, size_hint=(None, None), size=(350, 260), auto_dismiss=False) def do_rename(instance): new_name = input_field.text.strip() if new_name and new_name != self.project_name: if self.rename_project_folder(self.project_name, new_name): self.project_name = new_name popup.dismiss() btn_save.bind(on_press=do_rename) btn_cancel.bind(on_press=lambda x: popup.dismiss()) popup.open() def rename_project_folder(self, old_name, new_name): import os old_path = os.path.join(RESOURCES_FOLDER, "projects", old_name) new_path = os.path.join(RESOURCES_FOLDER, "projects", new_name) if os.path.exists(old_path) and not os.path.exists(new_path): os.rename(old_path, new_path) return True return False def optimize_route_entries(self): # Show popup with progress bar layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Processing route entries...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Optimizing Route", content=layout, size_hint=(0.92, None), size=(0, 260), auto_dismiss=False ) popup.open() def process_entries(dt): project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) positions_path = os.path.join(project_folder, "positions.json") if not os.path.exists(positions_path): label.text = "positions.json not found!" progress.value = 100 return with open(positions_path, "r") as f: positions = json.load(f) # Detect duplicate positions at the start start_remove = 0 if positions: first = positions[0] for pos in positions: if pos['latitude'] == first['latitude'] and pos['longitude'] == first['longitude']: start_remove += 1 else: break if start_remove > 0: start_remove -= 1 # Detect duplicate positions at the end end_remove = 0 if positions: last = positions[-1] for pos in reversed(positions): if pos['latitude'] == last['latitude'] and pos['longitude'] == last['longitude']: end_remove += 1 else: break if end_remove > 0: end_remove -= 1 progress.value = 100 label.text = ( f"Entries removable at start: {start_remove}\n" f"Entries removable at end: {end_remove}" ) btn_save = Button(text="Save optimized file", background_color=(0.008, 0.525, 0.290, 1)) btn_cancel = Button(text="Cancel") btn_box = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=None, height=44) btn_box.add_widget(btn_save) btn_box.add_widget(btn_cancel) layout.add_widget(btn_box) def save_optimized(instance): new_positions = positions[start_remove:len(positions)-end_remove if end_remove > 0 else None] with open(positions_path, "w") as f: json.dump(new_positions, f, indent=2) label.text = "File optimized and saved!" btn_save.disabled = True btn_cancel.disabled = True def close_and_refresh(dt): popup.dismiss() self.on_pre_enter() # Refresh the screen Clock.schedule_once(close_and_refresh, 1) btn_save.bind(on_press=save_optimized) btn_cancel.bind(on_press=lambda x: popup.dismiss()) Clock.schedule_once(process_entries, 0.5) def preview_route(self): # Show processing popup layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text="Processing route preview...") progress = ProgressBar(max=100, value=0) layout.add_widget(label) layout.add_widget(progress) popup = Popup( title="Previewing Route", content=layout, size_hint=(0.8, None), size=(0, 180), auto_dismiss=False ) popup.open() def process_preview(dt): try: import folium project_folder = os.path.join(RESOURCES_FOLDER, "projects", self.project_name) positions_path = os.path.join(project_folder, "positions.json") html_path = os.path.join(project_folder, "preview.html") img_path = os.path.join(project_folder, "preview.png") if not os.path.exists(positions_path): label.text = "positions.json not found!" progress.value = 100 return with open(positions_path, "r") as f: positions = json.load(f) if not positions: label.text = "No positions to preview." progress.value = 100 return coords = [(pos['latitude'], pos['longitude']) for pos in positions] m = folium.Map(location=coords[0], zoom_start=14) folium.PolyLine(coords, color="blue", weight=4.5, opacity=1).add_to(m) folium.Marker(coords[0], tooltip="Start", icon=folium.Icon(color="green")).add_to(m) folium.Marker(coords[-1], tooltip="End", icon=folium.Icon(color="red")).add_to(m) m.save(html_path) # Convert HTML to image save_folium_map_as_image(html_path, img_path) # Set the image path for Kivy Image widget self.preview_image_path = img_path label.text = "Preview ready!" progress.value = 100 def close_popup(dt): popup.dismiss() Clock.schedule_once(close_popup, 1) except Exception as e: label.text = f"Error: {e}" progress.value = 100 def close_popup(dt): popup.dismiss() Clock.schedule_once(close_popup, 2) Clock.schedule_once(process_preview, 0.5) def save_folium_map_as_image(html_path, img_path, width=800, height=600, delay=2): chrome_options = Options() chrome_options.add_argument("--headless") chrome_options.add_argument(f"--window-size={width},{height}") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") driver = webdriver.Chrome(options=chrome_options) try: driver.get("file://" + os.path.abspath(html_path)) time.sleep(delay) # Wait for map to render screenshot_path = img_path + ".tmp.png" driver.save_screenshot(screenshot_path) driver.quit() img = Image.open(screenshot_path) img = img.crop((0, 0, width, height)) img.save(img_path) os.remove(screenshot_path) except Exception as e: print(f"Error saving folium map as image: {e}") driver.quit() class HomeScreen(Screen): def on_pre_enter(self): """Load existing projects/trips when the screen is entered.""" self.load_existing_projects() def load_existing_projects(self): projects_folder = os.path.join(RESOURCES_FOLDER, "projects") archive_folder = os.path.join(RESOURCES_FOLDER, "trip_archive") if not os.path.exists(projects_folder): os.makedirs(projects_folder) if not os.path.exists(archive_folder): os.makedirs(archive_folder) self.ids.projects_list.clear_widgets() for project in os.listdir(projects_folder): row = BoxLayout( orientation="horizontal", size_hint_y=None, height=44, spacing=6, padding=(8, 6) ) from kivy.graphics import Color, Line, RoundedRectangle with row.canvas.before: Color(0.11, 0.10, 0.15, 1) row.bg_rect = RoundedRectangle(pos=row.pos, size=row.size, radius=[6]) Color(0.341, 0.235, 0.980, 1) row.border_line = Line(rounded_rectangle=[row.x, row.y, row.width, row.height, 6], width=1) # Use a closure to bind the correct row instance def make_update_bg_rect(r): def update_bg_rect(instance, value): r.bg_rect.pos = r.pos r.bg_rect.size = r.size r.border_line.rounded_rectangle = [r.x, r.y, r.width, r.height, 6] return update_bg_rect row.bind(pos=make_update_bg_rect(row), size=make_update_bg_rect(row)) project_label = Label( text=project, size_hint_x=0.64, color=(1, 1, 1, 1), font_size=15, shorten=True, shorten_from='right' ) edit_button = IconButton( source="resources/images/edit.png", size_hint_x=0.18, allow_stretch=True, keep_ratio=True ) edit_button.bind(on_press=lambda instance, p=project: self.edit_project(p)) delete_button = IconButton( source="resources/images/delete.png", size_hint_x=0.18, allow_stretch=True, keep_ratio=True ) delete_button.bind(on_press=lambda instance, p=project: self.confirm_delete_project(p)) row.add_widget(project_label) row.add_widget(edit_button) row.add_widget(delete_button) self.ids.projects_list.add_widget(row) def open_project(self, project_name): """Handle opening an existing project/trip.""" print(f"Opening project: {project_name}") self.ids.result_label.text = f"Opened project: {project_name}" def create_new_project(self): """Navigate to the GetTripFromServer screen to create a new project/trip.""" self.manager.current = "get_trip_from_server" def edit_project(self, project_name): # Set the project name on the CreateAnimationScreen before switching create_anim_screen = self.manager.get_screen("create_animation") create_anim_screen.project_name = project_name self.manager.current = "create_animation" # delete or archive project def confirm_delete_project(self, project_name): from kivy.uix.popup import Popup from kivy.uix.boxlayout import BoxLayout from kivy.uix.button import Button from kivy.uix.label import Label layout = BoxLayout(orientation='vertical', spacing=10, padding=10) label = Label(text=f"Delete project '{project_name}'?\nChoose an option:") btn_delete = Button(text="Delete Completely", background_color=(1, 0, 0, 1)) btn_archive = Button(text="Archive Trip", background_color=(0.341, 0.235, 0.980, 1)) btn_cancel = Button(text="Cancel") popup = Popup(title="Delete Project", content=layout, size_hint=(None, None), size=(400, 250), auto_dismiss=False) layout.add_widget(label) layout.add_widget(btn_delete) layout.add_widget(btn_archive) layout.add_widget(btn_cancel) def do_delete(instance): self.delete_project(project_name) popup.dismiss() self.load_existing_projects() def do_archive(instance): self.archive_project(project_name) popup.dismiss() self.load_existing_projects() btn_delete.bind(on_press=do_delete) btn_archive.bind(on_press=do_archive) btn_cancel.bind(on_press=lambda x: popup.dismiss()) popup.open() def delete_project(self, project_name): import shutil folder_path = os.path.join(RESOURCES_FOLDER, "projects", project_name) if os.path.exists(folder_path): shutil.rmtree(folder_path) def archive_project(self, project_name): import shutil src_file = os.path.join(RESOURCES_FOLDER, "projects", project_name, "positions.json") archive_folder = os.path.join(RESOURCES_FOLDER, "trip_archive") if not os.path.exists(archive_folder): os.makedirs(archive_folder) dst_file = os.path.join(archive_folder, f"{project_name}.json") if os.path.exists(src_file): shutil.copy2(src_file, dst_file) # Optionally, delete the project folder after archiving self.delete_project(project_name) class IconButton(ButtonBehavior, Image): pass class TraccarApp(App): def build(self): if not os.path.exists(RESOURCES_FOLDER): os.makedirs(RESOURCES_FOLDER) generate_key() sm = ScreenManager() sm.add_widget(LoginScreen(name="login")) sm.add_widget(HomeScreen(name="home")) sm.add_widget(GetTripFromServer(name="get_trip_from_server")) sm.add_widget(SettingsScreen(name="settings")) sm.add_widget(RegisterScreen(name="register")) sm.add_widget(CreateAnimationScreen(name="create_animation")) print("Screens added to ScreenManager:", [screen.name for screen in sm.screens]) return sm if __name__ == "__main__": TraccarApp().run()