From 5cc2a527fc62d7ae99f7c30d48bce755b16b5fd5 Mon Sep 17 00:00:00 2001 From: Gleb Ischenko <33964247+DilerFeed@users.noreply.github.com> Date: Fri, 14 Jun 2024 14:07:55 +0300 Subject: [PATCH] Add files via upload --- stormworks_connect_v1.2.py | 1019 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1019 insertions(+) create mode 100644 stormworks_connect_v1.2.py diff --git a/stormworks_connect_v1.2.py b/stormworks_connect_v1.2.py new file mode 100644 index 0000000..aff3c82 --- /dev/null +++ b/stormworks_connect_v1.2.py @@ -0,0 +1,1019 @@ +import tkinter as tk +from tkinter import filedialog, StringVar, BooleanVar, DoubleVar, IntVar +from tkinter import ttk +from PIL import Image, ImageTk, ImageSequence +from flask import Flask, request, jsonify +import threading +import logging +import webbrowser +import sys +import os +import pygame +import requests +from packaging import version +import TKinterModernThemes as TKMT +import ctypes +import darkdetect +import imageio +import time +import configparser +from bs4 import BeautifulSoup, SoupStrainer +import textwrap +from urllib.request import urlopen +from io import BytesIO + +# Enable High DPI support +try: + ctypes.windll.shcore.SetProcessDpiAwareness(1) +except Exception as e: + print(e) + +def resource_path(relative_path): + try: + base_path = sys._MEIPASS + except Exception: + base_path = os.path.abspath(".") + return os.path.join(base_path, relative_path) + +def open_download_link(url): + webbrowser.open_new(url) + +class App(TKMT.ThemedTKinterFrame): + def __init__(self): + # Define current version + self.current_version = "1.2.0.0" + self.suppress_update_notification = 'False' + self.config_file = "config.ini" + + # Detect system theme + system_theme = self.get_system_theme() + + # Initialize with detected theme + super().__init__("Stormworks Connect", "sun-valley", system_theme) + + # Flask app initialization + self.app = Flask(__name__) + self.current_image = None + self.original_image = None + self.original_gif = None + self.current_gif = None + self.selected_gif_size = (32, 32) + self.current_gif_animation_id = None + self.gif_frames_data = [] # Add this line to store GIF frames data + self.current_result_index = 0 # index to track the current result + self.results_per_page = 3 # number of results on one page + self.results_list = [] # a list to store all results + self.current_page = 0 # Initializing the current_page variable + self.result_text = [] # Initializing a variable to store search results + self.page_content = [] + self.page_line_count = 10 # number of lines per page + self.search_complete = False + self.init_flask() + + # Setting up logging + logging.basicConfig(level=logging.DEBUG) + + # Initialize Pygame + pygame.init() + pygame.joystick.init() + self.joystick = None + + # Variables for joystick control + self.joystick_name = tk.StringVar() + self.steering_angle = DoubleVar() + self.gas_pedal = DoubleVar() + self.brake_pedal = DoubleVar() + self.deadzone = DoubleVar(value=0.05) + self.swap_pedals = BooleanVar(value=False) + self.combined_pedals = BooleanVar(value=False) + self.steering_axis = tk.IntVar(value=0) + self.gas_axis = tk.IntVar(value=2) + self.brake_axis = tk.IntVar(value=1) + self.shift_up_button = tk.IntVar(value=1) + self.shift_down_button = tk.IntVar(value=0) + self.custom_button_1 = tk.IntVar(value=9) + self.custom_button_2 = tk.IntVar(value=8) + self.custom_button_3 = tk.IntVar(value=6) + self.custom_button_4 = tk.IntVar(value=7) + + self.shift_up_status = tk.StringVar() + self.shift_down_status = tk.StringVar() + self.custom_button_1_status = tk.StringVar() + self.custom_button_2_status = tk.StringVar() + self.custom_button_3_status = tk.StringVar() + self.custom_button_4_status = tk.StringVar() + + # Monitor sizes dictionary + self.monitor_sizes = { + "1x1": (32, 32), + "1x2": (64, 32), + "1x3": (96, 32), + "2x2": (64, 64), + "2x3": (96, 64), + "3x3": (96, 96), + "5x3": (160, 96), + "9x5": (288, 160) + } + + self.selected_size = (288, 160) + self.fill_image = False + + # Load images for tabs + self.image_transmit_icon = ImageTk.PhotoImage(Image.open(resource_path("picture.png"))) + self.steering_wheel_icon = ImageTk.PhotoImage(Image.open(resource_path("steering-wheel.png"))) + self.info_icon = ImageTk.PhotoImage(Image.open(resource_path("info.png"))) + self.support_icon = ImageTk.PhotoImage(Image.open(resource_path("money.png"))) + + self.create_widgets() + self.poll_joystick() + self.load_config() + self.check_for_updates() + + self.root.geometry("800x600") + self.root.update_idletasks() + scaleFactor = ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100 + self.root.minsize(int(self.root.winfo_width() * scaleFactor), int(self.root.winfo_height() * scaleFactor)) + self.root.resizable(True, True) + + self.root.iconphoto(False, tk.PhotoImage(file=resource_path('SC_cover.png'))) + + self.run() + + def load_config(self): + self.config = configparser.ConfigParser() + self.config.read(self.config_file) + if 'Settings' not in self.config.sections(): + self.config.add_section('Settings') + if 'SteeringWheel' not in self.config.sections(): + self.config.add_section('SteeringWheel') + + # Load settings + self.suppress_update_notification = self.config.getboolean('Settings', 'suppress_update_notification', fallback=False) + self.deadzone.set(self.config.getfloat('SteeringWheel', 'deadzone', fallback=0.05)) + self.swap_pedals.set(self.config.getboolean('SteeringWheel', 'swap_pedals', fallback=False)) + self.combined_pedals.set(self.config.getboolean('SteeringWheel', 'combined_pedals', fallback=False)) + self.steering_axis.set(self.config.getint('SteeringWheel', 'steering_axis', fallback=0)) + self.gas_axis.set(self.config.getint('SteeringWheel', 'gas_axis', fallback=2)) + self.brake_axis.set(self.config.getint('SteeringWheel', 'brake_axis', fallback=1)) + self.shift_up_button.set(self.config.getint('SteeringWheel', 'shift_up_button', fallback=1)) + self.shift_down_button.set(self.config.getint('SteeringWheel', 'shift_down_button', fallback=0)) + self.custom_button_1.set(self.config.getint('SteeringWheel', 'custom_button_1', fallback=9)) + self.custom_button_2.set(self.config.getint('SteeringWheel', 'custom_button_2', fallback=8)) + self.custom_button_3.set(self.config.getint('SteeringWheel', 'custom_button_3', fallback=6)) + self.custom_button_4.set(self.config.getint('SteeringWheel', 'custom_button_4', fallback=7)) + self.update_swap_pedals_state() + + def save_config(self): + self.config['Settings']['suppress_update_notification'] = str(self.suppress_update_notification) + self.config['SteeringWheel']['deadzone'] = str(self.deadzone.get()) + self.config['SteeringWheel']['swap_pedals'] = str(self.swap_pedals.get()) + self.config['SteeringWheel']['combined_pedals'] = str(self.combined_pedals.get()) + self.config['SteeringWheel']['steering_axis'] = str(self.steering_axis.get()) + self.config['SteeringWheel']['gas_axis'] = str(self.gas_axis.get()) + self.config['SteeringWheel']['brake_axis'] = str(self.brake_axis.get()) + self.config['SteeringWheel']['shift_up_button'] = str(self.shift_up_button.get()) + self.config['SteeringWheel']['shift_down_button'] = str(self.shift_down_button.get()) + self.config['SteeringWheel']['custom_button_1'] = str(self.custom_button_1.get()) + self.config['SteeringWheel']['custom_button_2'] = str(self.custom_button_2.get()) + self.config['SteeringWheel']['custom_button_3'] = str(self.custom_button_3.get()) + self.config['SteeringWheel']['custom_button_4'] = str(self.custom_button_4.get()) + + with open(self.config_file, 'w') as configfile: + self.config.write(configfile) + + def get_system_theme(self): + try: + if darkdetect.isDark(): + return 'dark' + else: + return 'light' + except Exception as e: + print(e) + return 'light' + + def switch_to_dark_theme(self): + self.root.tk.call("set_theme", "dark") + self.mode = "dark" + + def switch_to_light_theme(self): + self.root.tk.call("set_theme", "light") + self.mode = "light" + + def check_for_updates(self): + if self.suppress_update_notification: + logging.debug("Update check is suppressed by user settings.") + return + + try: + logging.debug("Checking for updates...") + response = requests.get("https://dilerfeed.github.io/Stormworks-Connect/version.json") + logging.debug(f"Response status code: {response.status_code}") + + if response.status_code == 200: + version_info = response.json() + latest_version = version_info["version"] + download_url = version_info["download_url"] + + logging.debug(f"Latest version available: {latest_version}") + logging.debug(f"Current version: {self.current_version}") + + if version.parse(latest_version) > version.parse(self.current_version): + logging.debug("A new version is available. Showing update notification.") + self.show_update_notification(download_url) + else: + logging.debug("No new version available.") + else: + logging.error(f"Failed to fetch update information. Status code: {response.status_code}") + except Exception as e: + logging.error(f"Error checking for updates: {e}") + + def show_update_notification(self, download_url): + update_window = tk.Toplevel(self.root) + update_window.title("Update Available") + update_window.geometry("350x250") + + label = ttk.Label(update_window, text="A new version of the program is available!") + label.pack(pady=10) + + download_button = ttk.Button(update_window, text="Download", command=lambda: self.open_download_link(download_url)) + download_button.pack(pady=5) + + def close_and_remember(): + self.config['Settings']['suppress_update_notification'] = 'True' + self.suppress_update_notification = True + self.save_config() + update_window.destroy() + + close_button = ttk.Button(update_window, text="Close", command=update_window.destroy) + close_button.pack(pady=5) + + suppress_button = ttk.Button(update_window, text="Don't remind me again", command=close_and_remember) + suppress_button.pack(pady=5) + + def create_custom_button(self, parent, text, command): + # Creating a custom button using a regular tkinter button + button = tk.Button(parent, text=text, command=command, bg='gold', fg='black', font=('Arial', 16, 'bold'), + activebackground='black', activeforeground='gold', relief='flat', borderwidth=2) + button.pack(pady=20, padx=20) + + def on_enter(event): + button.config(bg='black', fg='gold') + + def on_leave(event): + button.config(bg='gold', fg='black') + + button.bind("", on_enter) + button.bind("", on_leave) + + return button + + def create_widgets(self): + # Frames for different tabs + self.tab_control = ttk.Notebook(self.root) + self.image_transmit_frame = ttk.Frame(self.tab_control) + self.steering_wheel_frame = ttk.Frame(self.tab_control) + self.info_frame = ttk.Frame(self.tab_control) + self.support_frame = ttk.Frame(self.tab_control) # New support frame + + self.tab_control.add(self.image_transmit_frame, text=" Image transmit", image=self.image_transmit_icon, compound='left') + self.tab_control.add(self.steering_wheel_frame, text=" Steering wheel", image=self.steering_wheel_icon, compound='left') + self.tab_control.add(self.info_frame, text=" Info", image=self.info_icon, compound='left') + self.tab_control.add(self.support_frame, text=" Support", image=self.support_icon, compound='left') # Add support tab + self.tab_control.pack(expand=1, fill="both") + + # Frame for image and gif transmit sections + image_gif_frame = ttk.Frame(self.image_transmit_frame) + image_gif_frame.pack(expand=1, fill="both") + + # Image transmit section + image_frame = ttk.Frame(image_gif_frame) + image_frame.pack(side="left", fill="both", expand=True, padx=(10, 5)) + + ttk.Label(image_frame, text="Image Transmit", font=("Arial", 14)).pack(pady=10) + + self.monitor_size_var = tk.StringVar(value="9x5") + monitor_size_menu = ttk.Combobox(image_frame, textvariable=self.monitor_size_var, values=list(self.monitor_sizes.keys())) + monitor_size_menu.bind("<>", self.on_monitor_size_change) + monitor_size_menu.pack(pady=10) + + self.fill_var = tk.BooleanVar(value=False) + fill_checkbutton = ttk.Checkbutton(image_frame, text="Fill", variable=self.fill_var, command=self.on_fill_option_change) + fill_checkbutton.pack(pady=5) + + upload_button = ttk.Button(image_frame, text="Upload image", command=self.open_file) + upload_button.pack(pady=10) + + ttk.Label(image_frame, text="or upload image from URL:", font=("Arial", 10)).pack(pady=5) + self.image_url_var = tk.StringVar() + image_url_entry = ttk.Entry(image_frame, textvariable=self.image_url_var, width=50) + image_url_entry.pack(pady=5) + load_image_button = ttk.Button(image_frame, text="Upload image from URL", command=self.load_image_from_url_click) + load_image_button.pack(pady=5) + + self.image_label = ttk.Label(image_frame) + self.image_label.pack(pady=10) + + self.status_label = ttk.Label(image_frame, text="", font=("Arial", 12)) + self.status_label.pack(pady=5) + + # Separator (vertical) + ttk.Separator(image_gif_frame, orient='vertical').pack(side="left", fill='y', padx=5) + + # GIF transmit section + gif_frame = ttk.Frame(image_gif_frame) + gif_frame.pack(side="left", fill="both", expand=True, padx=(5, 10)) + + ttk.Label(gif_frame, text="GIF Transmit", font=("Arial", 14)).pack(pady=10) + + self.gif_monitor_size_var = tk.StringVar(value="1x1") + gif_monitor_size_menu = ttk.Combobox(gif_frame, textvariable=self.gif_monitor_size_var, values=list(self.monitor_sizes.keys())) + gif_monitor_size_menu.bind("<>", self.on_gif_monitor_size_change) + gif_monitor_size_menu.pack(pady=10) + + self.gif_fill_var = tk.BooleanVar(value=False) + gif_fill_checkbutton = ttk.Checkbutton(gif_frame, text="Fill GIF", variable=self.gif_fill_var, command=self.on_gif_fill_option_change) + gif_fill_checkbutton.pack(pady=5) + + upload_gif_button = ttk.Button(gif_frame, text="Upload GIF", command=self.open_gif_file) + upload_gif_button.pack(pady=10) + + ttk.Label(gif_frame, text="or upload GIF from URL:", font=("Arial", 10)).pack(pady=5) + self.gif_url_var = tk.StringVar() + gif_url_entry = ttk.Entry(gif_frame, textvariable=self.gif_url_var, width=50) + gif_url_entry.pack(pady=5) + load_gif_button = ttk.Button(gif_frame, text="Upload GIF from URL", command=self.load_gif_from_url_click) + load_gif_button.pack(pady=5) + + self.gif_label = ttk.Label(gif_frame) + self.gif_label.pack(pady=10) + + self.gif_status_label = ttk.Label(gif_frame, text="", font=("Arial", 12)) + self.gif_status_label.pack(pady=5) + + # Info tab with two sections: Info and Features + info_features_frame = ttk.Frame(self.info_frame) + info_features_frame.pack(expand=1, fill="both") + + # Info section + info_frame = ttk.Frame(info_features_frame) + info_frame.pack(side="left", fill="both", expand=True, padx=(10, 5)) + + ttk.Label(info_frame, text="Info", font=("Arial", 14)).pack(pady=10) + + server_status = ttk.Label(info_frame, text="Server is running on port 5000", foreground="green", font=("Arial", 12)) + server_status.pack(pady=5) + + program_title = ttk.Label(info_frame, text="Stormworks Connect v1.2.0.0", font=("Arial", 16)) + program_title.pack(pady=5) + + author_info = ttk.Label(info_frame, text="© Hlib Ishchenko 2024", font=("Arial", 10)) + author_info.pack(pady=5) + + steam_profile = ttk.Label(info_frame, text="Steam Profile", foreground="blue", cursor="hand2", font=("Arial", 12)) + steam_profile.pack(pady=5) + steam_profile.bind("", self.open_steam_profile) + + github_profile = ttk.Label(info_frame, text="GitHub Profile", foreground="blue", cursor="hand2", font=("Arial", 12)) + github_profile.pack(pady=5) + github_profile.bind("", self.open_github_profile) + + ttk.Button(info_frame, text="Switch to Dark Theme", command=self.switch_to_dark_theme).pack(pady=10) + ttk.Button(info_frame, text="Switch to Light Theme", command=self.switch_to_light_theme).pack(pady=10) + + # Separator (vertical) + ttk.Separator(info_features_frame, orient='vertical').pack(side="left", fill='y', padx=5) + + # Features section + features_frame = ttk.Frame(info_features_frame) + features_frame.pack(side="left", fill="both", expand=True, padx=(5, 10)) + + ttk.Label(features_frame, text="Features", font=("Arial", 14)).pack(pady=10) + + features = [ + ("Image Transmit", "https://steamcommunity.com/sharedfiles/filedetails/?id=3256896125"), + ("GIF Transmit", "https://steamcommunity.com/sharedfiles/filedetails/?id=3262978714"), + ("Steering Wheel Support", "https://steamcommunity.com/sharedfiles/filedetails/?id=3261140680"), + ("In-game text web browser", "https://steamcommunity.com/sharedfiles/filedetails/?id=3267509700"), + ] + + for feature, link in features: + feature_label = ttk.Label(features_frame, text=f"• {feature}", foreground="blue", cursor="hand2", font=("Arial", 12)) + feature_label.pack(pady=2, anchor='w') + feature_label.bind("", lambda e, url=link: webbrowser.open_new(url)) + + small_notice = ttk.Label(features_frame, text="Clicking on a feature will open the relevant controller page in the browser.", font=("Arial", 8)) + small_notice.pack(pady=5, anchor='w') + + # Support tab + ttk.Label(self.support_frame, text="Support the Development", font=("Arial", 16)).pack(pady=20) + ttk.Label(self.support_frame, text="If you like this software, consider supporting its developer:", font=("Arial", 12)).pack(pady=10) + + # Creating support button inside a separate frame within the support tab + support_button_frame = ttk.Frame(self.support_frame) + support_button_frame.pack(fill="both", expand=True) + + self.create_custom_button(support_button_frame, "Buy me a coffee", self.open_support_page) + + # Adding GIF below the support button + self.support_gif_label = ttk.Label(self.support_frame) + self.support_gif_label.pack(pady=10) + + self.support_load_and_play_gif(resource_path('thank-you-grateful.gif')) + + # Steering wheel tab + ttk.Label(self.steering_wheel_frame, text="Select Joystick:").grid(row=0, column=0, padx=10, pady=5, sticky="w") + self.joystick_menu = ttk.Combobox(self.steering_wheel_frame, textvariable=self.joystick_name) + self.joystick_menu.grid(row=0, column=1, padx=10, pady=5, sticky="w") + + ttk.Label(self.steering_wheel_frame, text="Current Steering Angle:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + ttk.Label(self.steering_wheel_frame, textvariable=self.steering_angle).grid(row=1, column=1, padx=10, pady=5, sticky="w") + + ttk.Label(self.steering_wheel_frame, text="Current Gas Pedal:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + ttk.Label(self.steering_wheel_frame, textvariable=self.gas_pedal).grid(row=2, column=1, padx=10, pady=5, sticky="w") + + ttk.Label(self.steering_wheel_frame, text="Current Brake Pedal:").grid(row=3, column=0, padx=10, pady=5, sticky="w") + ttk.Label(self.steering_wheel_frame, textvariable=self.brake_pedal).grid(row=3, column=1, padx=10, pady=5, sticky="w") + + ttk.Label(self.steering_wheel_frame, text="Deadzone:").grid(row=4, column=0, padx=10, pady=5, sticky="w") + deadzone_scale = ttk.Scale(self.steering_wheel_frame, from_=0, to=0.2, orient=tk.HORIZONTAL, variable=self.deadzone, command=lambda x: self.save_config()) + deadzone_scale.grid(row=4, column=1, padx=10, pady=5, sticky="w") + + deadzone_entry = ttk.Entry(self.steering_wheel_frame, textvariable=self.deadzone) + deadzone_entry.grid(row=4, column=2, padx=10, pady=5, sticky="w") + + self.swap_pedals_checkbutton = ttk.Checkbutton(self.steering_wheel_frame, text="Swap Gas and Brake Pedals", variable=self.swap_pedals, command=self.save_config) + self.swap_pedals_checkbutton.grid(row=5, column=0, padx=10, pady=5, sticky="w") + + ttk.Checkbutton(self.steering_wheel_frame, text="Combined Pedals", variable=self.combined_pedals, command=lambda: [self.update_swap_pedals_state(), self.save_config()]).grid(row=5, column=1, padx=10, pady=5, sticky="w") + + self.create_joystick_buttons() + + # Add labels for shift and custom button statuses + self.create_joystick_button("Shift Up Button", 6, self.shift_up_button, self.shift_up_status) + self.create_joystick_button("Shift Down Button", 7, self.shift_down_button, self.shift_down_status) + self.create_joystick_button("Custom Button 1", 8, self.custom_button_1, self.custom_button_1_status) + self.create_joystick_button("Custom Button 2", 9, self.custom_button_2, self.custom_button_2_status) + self.create_joystick_button("Custom Button 3", 10, self.custom_button_3, self.custom_button_3_status) + self.create_joystick_button("Custom Button 4", 11, self.custom_button_4, self.custom_button_4_status) + + def support_load_and_play_gif(self, gif_path): + gif_image = Image.open(gif_path) + frames = [ImageTk.PhotoImage(img) for img in ImageSequence.Iterator(gif_image)] + + def update_frame(index): + frame = frames[index] + self.support_gif_label.config(image=frame) + self.root.after(400, update_frame, (index + 1) % len(frames)) + + self.root.after(0, update_frame, 0) + + def update_swap_pedals_state(self): + if self.combined_pedals.get(): + self.swap_pedals.set(False) + self.swap_pedals_checkbutton.config(state="disabled") + else: + self.swap_pedals_checkbutton.config(state="normal") + self.save_config() + + def create_joystick_buttons(self): + ttk.Button(self.steering_wheel_frame, text="Set Steering Axis", command=lambda: self.open_axis_window(self.steering_axis, "Steering")).grid(row=1, column=2, padx=10, pady=5) + ttk.Button(self.steering_wheel_frame, text="Set Gas Axis", command=lambda: self.open_axis_window(self.gas_axis, "Gas Pedal")).grid(row=2, column=2, padx=10, pady=5) + ttk.Button(self.steering_wheel_frame, text="Set Brake Axis", command=lambda: self.open_axis_window(self.brake_axis, "Brake Pedal")).grid(row=3, column=2, padx=10, pady=5) + ttk.Button(self.steering_wheel_frame, text="Set Shift Up Button", command=lambda: self.open_button_window(self.shift_up_button, "Shift Up Button")).grid(row=6, column=3, padx=10, pady=5) + ttk.Button(self.steering_wheel_frame, text="Set Shift Down Button", command=lambda: self.open_button_window(self.shift_down_button, "Shift Down Button")).grid(row=7, column=3, padx=10, pady=5) + ttk.Button(self.steering_wheel_frame, text="Set Custom Button 1", command=lambda: self.open_button_window(self.custom_button_1, "Custom Button 1")).grid(row=8, column=3, padx=10, pady=5) + ttk.Button(self.steering_wheel_frame, text="Set Custom Button 2", command=lambda: self.open_button_window(self.custom_button_2, "Custom Button 2")).grid(row=9, column=3, padx=10, pady=5) + ttk.Button(self.steering_wheel_frame, text="Set Custom Button 3", command=lambda: self.open_button_window(self.custom_button_3, "Custom Button 3")).grid(row=10, column=3, padx=10, pady=5) + ttk.Button(self.steering_wheel_frame, text="Set Custom Button 4", command=lambda: self.open_button_window(self.custom_button_4, "Custom Button 4")).grid(row=11, column=3, padx=10, pady=5) + + def create_joystick_button(self, text, row, button_var, status_label): + ttk.Label(self.steering_wheel_frame, text=text).grid(row=row, column=0, padx=10, pady=5, sticky="w") + ttk.Label(self.steering_wheel_frame, textvariable=button_var).grid(row=row, column=1, padx=10, pady=5, sticky="w") + status_label_label = ttk.Label(self.steering_wheel_frame, textvariable=status_label) + status_label_label.grid(row=row, column=2, padx=10, pady=5, sticky="w") + ttk.Button(self.steering_wheel_frame, text=f"Set {text}", command=lambda: self.open_button_window(button_var, text)).grid(row=row, column=3, padx=10, pady=5) + + def open_file(self): + file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.png;*.jpg;*.jpeg")]) + if file_path: + image = Image.open(file_path) + self.original_image = image.copy() + self.current_image = self.process_static_image(image, self.selected_size, self.fill_image) + self.update_display_image() + + def process_static_image(self, image, size, fill): + if fill: + image = image.resize(size, Image.Resampling.LANCZOS) + else: + image.thumbnail(size, Image.Resampling.LANCZOS) + return image + + def update_display_image(self): + if self.original_image: + self.current_image = self.process_static_image(self.original_image.copy(), self.selected_size, self.fill_image) + display_image = self.current_image.copy() + display_image.thumbnail((200, 200), Image.Resampling.LANCZOS) + img_tk = ImageTk.PhotoImage(display_image) + self.image_label.config(image=img_tk) + self.image_label.image = img_tk + self.status_label.config(text="Image loaded") + + def on_monitor_size_change(self, event): + self.selected_size = self.monitor_sizes[self.monitor_size_var.get()] + self.update_display_image() + + def on_fill_option_change(self): + self.fill_image = self.fill_var.get() + self.update_display_image() + + def open_steam_profile(self, event): + webbrowser.open_new("https://steamcommunity.com/id/inspirers/") + + def open_github_profile(self, event): + webbrowser.open_new("https://github.com/DilerFeed") + + def open_support_page(self): + webbrowser.open_new("https://buymeacoffee.com/hlib_ishchenko") + + def open_download_link(self, link): + webbrowser.open_new(link) + + def show_frame(self, frame): + frame.tkraise() + + def open_gif_file(self): + file_path = filedialog.askopenfilename(filetypes=[("GIF files", "*.gif")]) + if file_path: + gif = imageio.mimread(file_path) + self.original_gif = gif + self.update_gif_display_and_data() + + def update_gif_display_and_data(self): + if self.original_gif: + self.current_gif = [self.process_gif_frame(frame, self.selected_gif_size, self.gif_fill_var.get()) for frame in self.original_gif] + self.gif_frames_data = self.prepare_gif_data(self.current_gif) # Prepare GIF data for transmission + self.update_display_gif() + + def process_gif_frame(self, frame, size, fill): + image = Image.fromarray(frame) + if fill: + image = image.resize(size, Image.Resampling.LANCZOS) + else: + image.thumbnail(size, Image.Resampling.LANCZOS) + return image + + def prepare_gif_data(self, frames): + gif_data = [] + for frame in frames: + pixels = frame.load() + width, height = frame.size + frame_data = [] + for y in range(height): + row = [] + for x in range(width): + r, g, b = pixels[x, y][:3] + pixel_str = f"{r} {g} {b}" + row.append(pixel_str) + frame_data.append(",".join(row)) + gif_data.append("\n".join(frame_data)) + return gif_data + + def update_display_gif(self): + if self.original_gif: + self.stop_current_gif_animation() # Stop the previous animation + gif_frames = [ImageTk.PhotoImage(frame) for frame in self.current_gif] + self.gif_label.config(image=gif_frames[0]) + self.gif_label.image = gif_frames + self.animate_gif(gif_frames) + self.gif_status_label.config(text="GIF loaded") + + def animate_gif(self, frames): + def update(index): + frame = frames[index] + self.gif_label.config(image=frame) + self.current_gif_animation_id = self.root.after(100, update, (index + 1) % len(frames)) + self.current_gif_animation_id = self.root.after(0, update, 0) + + def stop_current_gif_animation(self): + if self.current_gif_animation_id is not None: + self.root.after_cancel(self.current_gif_animation_id) + self.current_gif_animation_id = None + + def on_gif_monitor_size_change(self, event): + self.selected_gif_size = self.monitor_sizes[self.gif_monitor_size_var.get()] + self.update_gif_display_and_data() + + def on_gif_fill_option_change(self): + self.update_gif_display_and_data() + + def load_image_from_url(self, url): + try: + with urlopen(url) as response: + image_data = response.read() + image = Image.open(BytesIO(image_data)) + if image.format.lower() not in ["png", "jpeg", "jpg"]: + self.status_label.config(text="Invalid image format") + return None + return image + except Exception as e: + self.status_label.config(text=f"Error loading image: {e}") + return None + + def load_gif_from_url(self, url): + try: + with urlopen(url) as response: + gif_data = response.read() + gif = imageio.mimread(BytesIO(gif_data)) + return gif + except Exception as e: + self.gif_status_label.config(text=f"Error loading GIF: {e}") + return None + + def load_image_from_url_click(self): + url = self.image_url_var.get() + image = self.load_image_from_url(url) + if image: + self.original_image = image.copy() + self.current_image = self.process_static_image(image, self.selected_size, self.fill_image) + self.update_display_image() + + def load_gif_from_url_click(self): + url = self.gif_url_var.get() + gif = self.load_gif_from_url(url) + if gif: + self.original_gif = gif + self.update_gif_display_and_data() + + def open_axis_window(self, axis_var, axis_name): + def detect_axis(): + nonlocal axis_detected, previous_values + pygame.event.pump() + significant_change_detected = False + + for i in range(self.joystick.get_numaxes()): + value = self.joystick.get_axis(i) + if abs(value - previous_values[i]) > 0.2: # Compare the current value with the previous one + axis_detected = i + axis_label.config(text=f"Detected Axis: {axis_detected}") + significant_change_detected = True + previous_values[i] = value + + if not significant_change_detected: + assign_window.after(50, detect_axis) # We continue to check + + def apply_axis(): + axis_var.set(axis_detected) + self.save_config() + assign_window.destroy() + + def cancel(): + assign_window.destroy() + + axis_detected = axis_var.get() + previous_values = [self.joystick.get_axis(i) for i in range(self.joystick.get_numaxes())] + assign_window = tk.Toplevel(self.root) + assign_window.title(f"Assign {axis_name} Axis") + ttk.Label(assign_window, text=f"Move the {axis_name} or press a pedal.").pack(pady=10) + axis_label = ttk.Label(assign_window, text=f"Detected Axis: {axis_detected}") + axis_label.pack(pady=5) + apply_button = ttk.Button(assign_window, text="Apply", command=apply_axis) + apply_button.pack(side="left", padx=10, pady=10) + cancel_button = ttk.Button(assign_window, text="Cancel", command=cancel) + cancel_button.pack(side="right", padx=10, pady=10) + + detect_axis() # We start checking immediately after opening the window + + def open_button_window(self, button_var, description): + def on_detect_button(): + pygame.event.pump() + detected_button = -1 + for i in range(self.joystick.get_numbuttons()): + if self.joystick.get_button(i): + detected_button = i + break + button_var.set(detected_button) + self.save_config() + detected_label.config(text=f"Detected Button: {detected_button}") + def on_apply(): + button_var.set(button_var.get()) + self.save_config() + button_window.destroy() + button_window = tk.Toplevel(self.root) + button_window.title(f"Detect {description}") + ttk.Label(button_window, text=f"Press the {description}").pack(pady=5) + detected_label = ttk.Label(button_window, text="Detected Button: -1") + detected_label.pack(pady=5) + ttk.Button(button_window, text="Detect Button", command=on_detect_button).pack(pady=5) + ttk.Button(button_window, text="Apply", command=on_apply).pack(pady=5) + ttk.Button(button_window, text="Cancel", command=button_window.destroy).pack(pady=5) + + def poll_joystick(self): + self.update_joystick_list() + if self.joystick: + pygame.event.pump() + raw_angle = self.joystick.get_axis(self.steering_axis.get()) + deadzone_value = self.deadzone.get() + + if abs(raw_angle) < deadzone_value: + raw_angle = 0 + adjusted_angle = max(min(raw_angle, 1), -1) + self.steering_angle.set(adjusted_angle) + + if self.combined_pedals.get(): + combined_value = self.joystick.get_axis(1) + gas_value = -min(0, combined_value) + brake_value = max(0, combined_value) + self.gas_pedal.set(self.apply_deadzone(gas_value, deadzone_value)) + self.brake_pedal.set(self.apply_deadzone(brake_value, deadzone_value)) + else: + if self.swap_pedals.get(): + self.gas_pedal.set(self.apply_deadzone((1 - (self.joystick.get_axis(self.brake_axis.get()) + 1) / 2), deadzone_value)) + self.brake_pedal.set(self.apply_deadzone((1 - (self.joystick.get_axis(self.gas_axis.get()) + 1) / 2), deadzone_value)) + else: + self.gas_pedal.set(self.apply_deadzone((1 - (self.joystick.get_axis(self.gas_axis.get()) + 1) / 2), deadzone_value)) + self.brake_pedal.set(self.apply_deadzone((1 - (self.joystick.get_axis(self.brake_axis.get()) + 1) / 2), deadzone_value)) + + if self.shift_up_button.get() != -1 and self.joystick.get_button(self.shift_up_button.get()): + self.update_button_status(self.shift_up_status, "Shift Up Pressed") + if self.shift_down_button.get() != -1 and self.joystick.get_button(self.shift_down_button.get()): + self.update_button_status(self.shift_down_status, "Shift Down Pressed") + if self.custom_button_1.get() != -1 and self.joystick.get_button(self.custom_button_1.get()): + self.update_button_status(self.custom_button_1_status, "Custom Button 1 Pressed") + if self.custom_button_2.get() != -1 and self.joystick.get_button(self.custom_button_2.get()): + self.update_button_status(self.custom_button_2_status, "Custom Button 2 Pressed") + if self.custom_button_3.get() != -1 and self.joystick.get_button(self.custom_button_3.get()): + self.update_button_status(self.custom_button_3_status, "Custom Button 3 Pressed") + if self.custom_button_4.get() != -1 and self.joystick.get_button(self.custom_button_4.get()): + self.update_button_status(self.custom_button_4_status, "Custom Button 4 Pressed") + + self.root.after(50, self.poll_joystick) + + def update_joystick_list(self): + joystick_names = [pygame.joystick.Joystick(i).get_name() for i in range(pygame.joystick.get_count())] + self.joystick_menu['values'] = joystick_names + if joystick_names: + self.joystick_name.set(joystick_names[0]) + self.select_joystick(0) + else: + self.joystick_name.set("") + + def select_joystick(self, index): + if index < pygame.joystick.get_count(): + self.joystick = pygame.joystick.Joystick(index) + self.joystick.init() + else: + self.joystick = None + + def apply_deadzone(self, value, deadzone_value): + return 0 if abs(value) < deadzone_value else value + + def update_button_status(self, status_var, text): + status_var.set(text) + self.root.after(1000, lambda: status_var.set("")) + + def update_display_results(self): + start_index = self.current_result_index + end_index = min(start_index + self.results_per_page, len(self.results_list)) + display_results = self.results_list[start_index:end_index] + self.result_text = "\n\n".join(display_results) + + # Limit search to English language pages + def perform_search(self, query): + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" + } + search_results = [] + + # We go through several pages of results + for start in range(0, 50, 10): + response = requests.get(f"https://www.google.com/search?q={query}&start={start}&lr=lang_en", headers=headers) + response.encoding = 'utf-8' + soup = BeautifulSoup(response.text, 'html.parser') + + for g in soup.find_all('div', class_='tF2Cxc'): + title_elem = g.find('h3') + link_elem = g.find('a') + if title_elem and link_elem: + title = title_elem.text + link = link_elem['href'] + search_results.append((title, link)) + + self.result_text = search_results[:50] # Saving the first 50 results + self.current_page = 0 # Reset to first page + self.search_complete = True # Set the search completion flag + + def get_current_results(self): + start = self.current_page * 3 + end = start + 3 + return self.result_text[start:end] + + def load_page_content(self, url): + try: + response = requests.get(url, timeout=10) # Set timeout to 10 seconds + response.encoding = 'utf-8' + soup = BeautifulSoup(response.text, 'html.parser') + paragraphs = soup.find_all('p') + page_content = "\n\n".join([p.text for p in paragraphs]) + + # Split text into 500-character chunks, avoiding word cutting + self.page_content = [] + current_chunk = "" + for paragraph in paragraphs: + wrapped_lines = textwrap.wrap(paragraph.text, width=80, break_long_words=False) + for line in wrapped_lines: + if len(current_chunk) + len(line) + 1 > 500: # +1 for adding a space or newline + self.page_content.append(current_chunk.strip()) + current_chunk = line + else: + if current_chunk: + current_chunk += " " + line + else: + current_chunk = line + if current_chunk: # Add last chunk + self.page_content.append(current_chunk.strip()) + + self.is_page_loaded = True + self.is_loading = False # Resetting the loading flag after completion + except Exception as e: + self.page_content = ["Failed to load page content"] + logging.error(f"Failed to load page content: {e}") + self.is_page_loaded = True + self.is_loading = False # Resetting the loading flag after completion + + def get_page_content(self, index): + if index < 0 or index >= len(self.page_content): + return "No more content", 200 + return self.page_content[index], 200 + + # Улучшение разбивки текста на строки + def split_text_into_lines(self, text, max_line_length): + wrapped_lines = [] + for line in text.split('\n'): + wrapped_lines.extend(textwrap.wrap(line, width=max_line_length, break_long_words=False)) + return wrapped_lines + + def init_flask(self): + @self.app.before_request + def log_request_info(): + self.app.logger.debug('Headers: %s', request.headers) + self.app.logger.debug('Body: %s', request.get_data()) + + @self.app.after_request + def log_response_info(response): + response.headers.add('Access-Control-Allow-Origin', '*') + response.headers.add('Access-Control-Allow-Methods', '*') + response.headers.add('Access-Control-Allow-Headers', '*') + self.app.logger.debug('Response: %s', response.get_data()) + return response + + @self.app.route('/upload', methods=['POST']) + def upload_image(): + file = request.files['file'] + image = Image.open(file.stream) + self.original_image = image.copy() + self.current_image = self.process_static_image(image, self.selected_size, self.fill_image) + return "Image uploaded and processed", 200 + + @self.app.route('/image', methods=['GET']) + def get_image(): + if self.current_image is None: + return "No image uploaded", 404 + + pixels = self.current_image.load() + width, height = self.current_image.size + image_data = [] + for y in range(height): + row = [] + for x in range(width): + r, g, b = pixels[x, y][:3] + pixel_str = f"{r} {g} {b}" + row.append(pixel_str) + image_data.append(",".join(row)) + + image_string = "\n".join(image_data) + image_string_with_size = f"{width} {height}\n{image_string}" + return image_string_with_size + + @self.app.route('/column', methods=['GET']) + def get_column(): + if self.current_image is None: + return "No image uploaded", 404 + + pixels = self.current_image.load() + width, height = self.current_image.size + x = int(request.args.get('x')) + if x < 0 or x >= width: + return "Column coordinates out of bounds", 400 + + column_data = [] + for y in range(height): + r, g, b = pixels[x, y][:3] + pixel_str = f"{r} {g} {b}" + column_data.append(pixel_str) + + column_string = "\n".join(column_data) + return column_string, 200 + + @self.app.route('/controller_data', methods=['GET']) + def controller_data(): + data = { + "steering_angle": self.steering_angle.get(), + "gas_pedal": self.gas_pedal.get(), + "brake_pedal": self.brake_pedal.get(), + "shift_up": 1 if self.joystick.get_button(self.shift_up_button.get()) else 0, + "shift_down": 1 if self.joystick.get_button(self.shift_down_button.get()) else 0, + "custom_button_1": 1 if self.joystick.get_button(self.custom_button_1.get()) else 0, + "custom_button_2": 1 if self.joystick.get_button(self.custom_button_2.get()) else 0, + "custom_button_3": 1 if self.joystick.get_button(self.custom_button_3.get()) else 0, + "custom_button_4": 1 if self.joystick.get_button(self.custom_button_4.get()) else 0 + } + return '\n'.join([f'{key}={value}' for key, value in data.items()]), 200 + + @self.app.route('/gif_frame/', methods=['GET']) + def get_gif_frame(frame_index): + start_time = time.time() # Start of countdown + if frame_index < 0 or frame_index >= len(self.gif_frames_data): + return "Frame index out of range", 400 + end_time = time.time() # End of countdown + print(f"Time taken for frame {frame_index}: {end_time - start_time} seconds") # Print countdown + return self.gif_frames_data[frame_index], 200 + + @self.app.route('/gif_frame_count', methods=['GET']) + def get_gif_frame_count(): + return str(len(self.gif_frames_data)), 200 + + @self.app.route('/search', methods=['GET']) + def search(): + query = request.args.get('query') + if query: + decoded_query = "".join([chr(int(x)) for x in query.split()]) + self.search_complete = False # Set the search start flag + threading.Thread(target=self.perform_search, args=(decoded_query,)).start() + print(f"Received query: {decoded_query}") + return jsonify({'status': 'searching'}) + return 'Invalid query', 400 + + @self.app.route('/search_status', methods=['GET']) + def search_status(): + if self.search_complete: + return jsonify({'status': 'complete'}), 200 + return jsonify({'status': 'searching'}), 200 + + @self.app.route('/result', methods=['GET']) + def result(): + if not self.search_complete: + return "Search in progress", 202 + results = self.get_current_results() + formatted_results = [] + for title, link in results: + formatted_results.append(f"{title}\n{link}") + return "\n\n".join(formatted_results).encode('utf-8') + + @self.app.route('/scroll', methods=['GET']) + def scroll(): + direction = request.args.get('direction') + results_count = len(self.result_text) + if direction == 'up' and self.current_page > 0: + self.current_page -= 1 + elif direction == 'down' and (self.current_page + 1) * 3 < results_count: + self.current_page += 1 + results = self.get_current_results() + formatted_results = [] + for title, link in results: + formatted_results.append(f"{title}\n{link}") + return "\n\n".join(formatted_results).encode('utf-8') + + @self.app.route('/page_content', methods=['GET']) + def page_content(): + url = request.args.get('url') + if url: + self.is_page_loaded = False + self.is_loading = True # Setting the loading flag + threading.Thread(target=self.load_page_content, args=(url,)).start() + return jsonify({'status': 'loading'}), 200 + return 'Invalid URL', 400 + + @self.app.route('/page_status', methods=['GET']) + def page_status(): + if self.is_page_loaded: + return jsonify({'status': 'loaded'}), 200 + return jsonify({'status': 'loading'}), 200 + + @self.app.route('/page_part', methods=['GET']) + def page_part(): + index = int(request.args.get('index', 0)) + max_line_length = 40 # Set max line length based on screen width + if index < 0 or index >= len(self.page_content): + return "No more content", 200 + lines = self.split_text_into_lines(self.page_content[index], max_line_length) + return "\n".join(lines), 200 + + thread = threading.Thread(target=self.start_flask) + thread.daemon = True + thread.start() + + def start_flask(self): + self.app.run(debug=True, use_reloader=False) + +if __name__ == "__main__": + App()