From e2058fb2513d136aa79064fc119e9a9be7fc9472 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sat, 28 Jan 2023 20:51:45 +0100 Subject: [PATCH 01/21] ui overhaul and implementation of custom widget avatarbox --- src/gui.py | 55 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/gui.py b/src/gui.py index 32f764d..a2b79c4 100644 --- a/src/gui.py +++ b/src/gui.py @@ -2,6 +2,7 @@ from functools import partial import os import lib +from accountbox import AccountBox from lib import config ck.set_appearance_mode("dark") @@ -14,6 +15,11 @@ def close_on_switch(self): config.set_close_on_switch(not config.get_close_on_switch()) config.save() + def clear_accounts_button_press(self): + config.clear_accounts() + config.save() + self.reload_app() + def account_button_click(self, account_index): lib.open_steam_in_account(account_index) @@ -23,20 +29,21 @@ def account_button_right_click(self, account_index, event): self.reload_app() def create_accounts_frame(self): - self.accounts_frame = ck.CTkFrame(self.bg) - self.accounts_frame.pack(padx=5, pady=5) + self.accounts_frame = ck.CTkFrame(self) + self.accounts_frame.pack(padx=10, pady=10) + if len(config.get_accounts()) == 0: + self.accounts_frame.pack_forget() for (account_index, account_title) in enumerate(config.get_account_titles()): callback = partial(self.account_button_click, account_index) - self.button = ck.CTkButton( - self.accounts_frame, text=account_title, command=callback) - - self.button.account_index = account_index + self.account_box = AccountBox( + self.accounts_frame, width=250, height=75, title=account_title, avatar_size=34, command=callback) + self.account_box.account_index = account_index right_click_callback = partial( self.account_button_right_click, account_index) - self.button.bind("", - command=right_click_callback) - self.button.pack(padx=5, pady=5) + self.account_box.bind("", + command=right_click_callback) + self.account_box.pack(padx=5, pady=5) return self.accounts_frame '''Prompts the user with a dialog asking Username and Title, returns a tuple containg (title, username)''' @@ -83,29 +90,31 @@ def __init__(self): super().__init__() self.wm_iconbitmap('Assets/Icon.ico') self.title("Steam Account Switcher") - self.geometry("600x500") + self.geometry("300x400") self.resizable(0, 0) - self.bg = ck.CTkFrame(self) - self.bg.pack(padx=15, pady=15, fill="both", expand=True) + self.topbar = ck.CTkFrame(self, width=450, height=100) + self.topbar.pack(side="top", fill="x", expand=False) - self.label = ck.CTkLabel( - master=self.bg, text='Steam Account Switcher', font=('Arialbd', 40)) - self.label.pack(padx=10, pady=10) + self.add_account_button = ck.CTkButton( + self.topbar, text="+", command=self.add_account_button_press, width=50) + self.add_account_button.grid(padx=10, pady=10, row=0, column=0) + + self.clear_account_button = ck.CTkButton( + self.topbar, text="clear", width=80, command=self.clear_accounts_button_press) + self.clear_account_button.grid(padx=10, pady=10, row=0, column=1) - self.frame = ck.CTkFrame(self.bg) - self.frame.pack(padx=5, pady=15) + self.import_button = ck.CTkButton( + self.topbar, text="import", width=100) + self.import_button.grid(padx=10, pady=10, row=0, column=2) self.close_on_switch = ck.CTkSwitch( - self.frame, text='Close on switch', progress_color=("#326da8", "#326da8"), command=self.close_on_switch) - self.close_on_switch.pack(padx=5, pady=5) + self, text='Close on switch', progress_color=("#326da8", "#326da8"), command=self.close_on_switch) + self.close_on_switch.pack( + side="bottom", padx=10, pady=10) if config.get_close_on_switch(): self.close_on_switch.select() - self.add_account_button = ck.CTkButton( - self.frame, text="+", command=self.add_account_button_press) - self.add_account_button.pack(padx=5, pady=5) - self.accounts_frame = self.create_accounts_frame() self.bind("", lambda _: lib.keylistener.stop()) From 401602b14595e9671a7050b6b5da98aa31f1110b Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sat, 28 Jan 2023 20:52:21 +0100 Subject: [PATCH 02/21] created customwidget accountbox replaces old button and adds profile image --- src/accountbox.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/accountbox.py diff --git a/src/accountbox.py b/src/accountbox.py new file mode 100644 index 0000000..a424dc5 --- /dev/null +++ b/src/accountbox.py @@ -0,0 +1,48 @@ +import customtkinter as ck +from typing import Callable +from PIL import Image +from PIL import PngImagePlugin + + +def get_placeholder_image(size): + return ck.CTkImage(Image.open('Logo.png'), size=(size, size)) + + +class AccountBox(ck.CTkFrame): + def bind(self, sequence=None, command=None, add=True): + """ called on the tkinter.Canvas """ + if not (add == "+" or add is True): + raise ValueError( + "'add' argument can only be '+' or True to preserve internal callbacks") + self.button.bind(sequence, command, add=True) + + def unbind(self, sequence=None, funcid=None): + """ called on the tkinter.Canvas """ + if funcid is not None: + raise ValueError("'funcid' argument can only be None, because there is a bug in" + + " tkinter and its not clear whether the internal callbacks will be unbinded or not") + self.self.button.unbind(sequence, None) + + def __init__(self, *args, + image: ck.CTkImage = None, + title: str, + height: int, + width: int, + command: Callable = None, + avatar_size: int, + ** kwargs): + super().__init__(*args, width=width, height=height, **kwargs) + if image == None: + self.avatar_image = get_placeholder_image(avatar_size) + else: + self.avatar_image = image.configure( + size=(avatar_size, avatar_size)) + + self.avatar = ck.CTkLabel(self, image=self.avatar_image, text="") + self.avatar.grid(padx=5, pady=5, row=0, column=0) + + button_width = (width - avatar_size) - 15 + + self.button = ck.CTkButton( + self, text=title, height=avatar_size, width=button_width, command=command) + self.button.grid(padx=5, pady=5, row=0, column=1) From 3c5c15eba26eee2688565ed2ad1b6a575f936bc8 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sat, 28 Jan 2023 20:52:59 +0100 Subject: [PATCH 03/21] added lib function to clear config accounts --- src/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.py b/src/lib.py index 84e8d2d..bad3e30 100644 --- a/src/lib.py +++ b/src/lib.py @@ -156,6 +156,9 @@ def add_account(self, title, username): def remove_account(self, account_index): del self.data[self.accounts_key][account_index] + def clear_accounts(self): + self.data[self.accounts_key].clear() + config = Config() keylistener = KeyListener() From f5d301265ba9a9473c078edff14b0b7acb9e6780 Mon Sep 17 00:00:00 2001 From: kr1s7ian <57260349+kr1s7ian@users.noreply.github.com> Date: Sat, 28 Jan 2023 20:54:01 +0100 Subject: [PATCH 04/21] Update README.md --- README.md | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/README.md b/README.md index b6d40c6..00d7bdd 100644 --- a/README.md +++ b/README.md @@ -1,18 +1 @@ -# steam-account-switcher -Gui account switcher made with customtkinter py. -Idea of the tool, and functionality of the account switching mechanics by pog -#5249, python code and implementation of UI made by me. - -How To Use: -- Pressing the plus button prompts the user for a new username along with a new title, the title is the text shown on the buttons, and username is the username of the steam account the button will log into. -- Right clicking an account button will delete that entry. -- You can use keyboard keys 1 to 9 to log in in the respective accounts. - -![Program Demo Image](https://i.imgur.com/Xtvrhsx.png) - -Ignore if using releases: -- If you are cloning and running with python, customtkinter has a known bug with the CTkInputDialog, it shold return None when pressing cancel but the command parameter is set to the same one for the ok button, for more details on how to the issue go here https://github.com/TomSchimansky/CustomTkinter/pull/853/commits/d43cc3d6112c77db7742156198b2014ce5f56058 - -Other: -- Feel free to submit feedback in the issues tab, even if you feel its a minor detail or if you have any problems: - +WIP From 91ee0952bdff09a1efbcda51c970f4cf214c9f1a Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sat, 28 Jan 2023 22:49:20 +0100 Subject: [PATCH 05/21] added lib class Steam to encapsulate all Steam logic, added lib functions to fetch and detect steam usernames and steamids on the machine --- src/lib.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/lib.py b/src/lib.py index bad3e30..1077dae 100644 --- a/src/lib.py +++ b/src/lib.py @@ -3,6 +3,8 @@ import toml import subprocess import threading +import vdf +import winreg from keylistener import KeyListener '''Closes the steam process using taskkill''' @@ -42,7 +44,7 @@ def terminate_app(): os._exit(0) -'''Opens steam in the steam account specified by account index, quit_on_switch is +'''Opens steam in the steam account specified by account index, quit_on_switch is a bool that decides if the app should be closed after the account switch''' @@ -160,5 +162,44 @@ def clear_accounts(self): self.data[self.accounts_key].clear() +class Steam: + def get_steam_path(self): + steam_path = '' + try: + hkey = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, "Software\Valve\Steam") + except: + print("cannot locate steam reg files") + + try: + steam_path = winreg.QueryValueEx(hkey, "SteamPath") + except: + print("Unable to locate steam path") + return steam_path[0] + + def load_login_users_vdf_file(self): + steam_path = self.get_steam_path().replace('/', '\\') + vdf_path = os.path.join(steam_path, 'config', 'loginusers.vdf') + try: + with open(vdf_path, 'r', encoding='utf-8') as vdf_file: + return vdf.load(vdf_file) + except: + print('unable to locate loginusers.vdf in ' + vdf_path) + + def get_login_users_names(self): + loginusers_vdf = self.load_login_users_vdf_file() + account_names = [] + for user in loginusers_vdf['users'].values(): + account_names.append(user['AccountName']) + return account_names + + def get_login_users_steamids(self): + loginusers_vdf = self.load_login_users_vdf_file() + account_steamids = [] + for steamid in loginusers_vdf['users'].keys(): + account_steamids.append(steamid) + return account_steamids + + config = Config() keylistener = KeyListener() From ecc60095b994b4a04b314007a6dd578ad6549a1f Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sat, 28 Jan 2023 22:50:05 +0100 Subject: [PATCH 06/21] implemented account import functionality --- src/gui.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/gui.py b/src/gui.py index a2b79c4..e106e7b 100644 --- a/src/gui.py +++ b/src/gui.py @@ -15,6 +15,14 @@ def close_on_switch(self): config.set_close_on_switch(not config.get_close_on_switch()) config.save() + def import_accounts_button_press(self): + lib.config.clear_accounts() + account_usernames = lib.Steam().get_login_users_names() + for name in account_usernames: + config.add_account(name, name) + config.save() + self.reload_app() + def clear_accounts_button_press(self): config.clear_accounts() config.save() @@ -105,7 +113,7 @@ def __init__(self): self.clear_account_button.grid(padx=10, pady=10, row=0, column=1) self.import_button = ck.CTkButton( - self.topbar, text="import", width=100) + self.topbar, text="import", width=100, command=self.import_accounts_button_press) self.import_button.grid(padx=10, pady=10, row=0, column=2) self.close_on_switch = ck.CTkSwitch( From 15bbbec932c9e6797cf6a1f48ffe50102d231e55 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:01:48 +0100 Subject: [PATCH 07/21] fixed image param --- src/accountbox.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/accountbox.py b/src/accountbox.py index a424dc5..b12d530 100644 --- a/src/accountbox.py +++ b/src/accountbox.py @@ -24,7 +24,7 @@ def unbind(self, sequence=None, funcid=None): self.self.button.unbind(sequence, None) def __init__(self, *args, - image: ck.CTkImage = None, + image_path: str = None, title: str, height: int, width: int, @@ -32,11 +32,11 @@ def __init__(self, *args, avatar_size: int, ** kwargs): super().__init__(*args, width=width, height=height, **kwargs) - if image == None: + if image_path == None: self.avatar_image = get_placeholder_image(avatar_size) else: - self.avatar_image = image.configure( - size=(avatar_size, avatar_size)) + self.avatar_image = ck.CTkImage( + Image.open(image_path), size=(avatar_size, avatar_size)) self.avatar = ck.CTkLabel(self, image=self.avatar_image, text="") self.avatar.grid(padx=5, pady=5, row=0, column=0) From d73b5b91deb8fd2f8b8fe5843d3e1b1d9bddc149 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:02:12 +0100 Subject: [PATCH 08/21] added lib function to handle getting user avatars --- src/lib.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/lib.py b/src/lib.py index 1077dae..a452eff 100644 --- a/src/lib.py +++ b/src/lib.py @@ -3,6 +3,8 @@ import toml import subprocess import threading +import requests +from bs4 import BeautifulSoup import vdf import winreg from keylistener import KeyListener @@ -136,6 +138,11 @@ def get_account_title(self, index): def get_account_username(self, index): return self.data[self.accounts_key][index][1] + '''Returns account steamid specified by the account_index from the config file''' + + def get_account_steamid(self, index): + return self.data[self.accounts_key][index][2] + '''Sets account title specified by the account_index to new_title''' def set_account_title(self, index, new_title): @@ -149,8 +156,8 @@ def set_account_username(self, index, new_username): '''Appends a new account object to the config with title and username as its values. Returns index of newly added account''' - def add_account(self, title, username): - self.data[self.accounts_key].append([title, username]) + def add_account(self, title, username, steamid): + self.data[self.accounts_key].append([title, username, steamid]) return len(self.get_accounts()) - 1 '''Removes account specified by account_index from the config''' @@ -163,6 +170,9 @@ def clear_accounts(self): class Steam: + def __init__(self): + self.account_avatars_path = 'Assets/account_avatars' + def get_steam_path(self): steam_path = '' try: @@ -200,6 +210,31 @@ def get_login_users_steamids(self): account_steamids.append(steamid) return account_steamids + def get_user_avatar_url(self, steamid): + with requests.get('https://steamcommunity.com/profiles/{}'.format(steamid)) as r: + steam_page = BeautifulSoup(r.content, features="html.parser") + images = steam_page.find( + 'div', attrs={"class": 'playerAvatarAutoSizeInner'}) + profile_picture = images.findAll('img')[1]['src'] + return profile_picture + + def download_user_avatar(self, steamid, output): + avatar_url = self.get_user_avatar_url(steamid) + avatar_data = requests.get(avatar_url).content + + with open(output, 'wb') as handler: + handler.write(avatar_data) + + def get_user_avatar_path(self, steamid): + user_avatar_path = os.path.join( + self.account_avatars_path, steamid,).replace('/', '\\') + ".jpg" + if os.path.exists(user_avatar_path): + return user_avatar_path + else: + self.download_user_avatar(steamid, user_avatar_path) + return user_avatar_path + config = Config() +steam = Steam() keylistener = KeyListener() From f23f8d15ebdc4bb266d12fddeb19604dfb2513e1 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:02:32 +0100 Subject: [PATCH 09/21] implemented user avatars --- src/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.py b/src/main.py index 90dfecb..e0a8913 100644 --- a/src/main.py +++ b/src/main.py @@ -2,5 +2,6 @@ from lib import config from lib import keylistener from gui import Gui +from lib import steam Gui() From cb26a7376dd3f286ad21a0a539fde6b6502c593d Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:07:53 +0100 Subject: [PATCH 10/21] added requirements.txt --- src/requirements.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/requirements.txt diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..12f7a6b --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,15 @@ +beautifulsoup4==4.11.1 +bs4==0.0.1 +certifi==2022.12.7 +charset-normalizer==3.0.1 +customtkinter==5.0.5 +darkdetect==0.8.0 +idna==3.4 +Pillow==9.4.0 +pynput==1.7.6 +requests==2.28.2 +six==1.16.0 +soupsieve==2.3.2.post1 +toml==0.10.2 +urllib3==1.26.14 +vdf==3.4 From b86f5c4b4d902fc7c222444524a3e5eda12ce6d3 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:12:42 +0100 Subject: [PATCH 11/21] required folder for correct program behaviour --- Assets/account_avatars/76561198180506939.jpg | Bin 0 -> 5493 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Assets/account_avatars/76561198180506939.jpg diff --git a/Assets/account_avatars/76561198180506939.jpg b/Assets/account_avatars/76561198180506939.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bba1b4b05dc6f3c7501e2f9f14fdd3cd61044a28 GIT binary patch literal 5493 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 literal 0 HcmV?d00001 From d394ee9d0d5fde26369b25d5e0e74800f94cadad Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:22:19 +0100 Subject: [PATCH 12/21] implemented user avatar logic --- src/gui.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/gui.py b/src/gui.py index e106e7b..905f3cb 100644 --- a/src/gui.py +++ b/src/gui.py @@ -18,8 +18,10 @@ def close_on_switch(self): def import_accounts_button_press(self): lib.config.clear_accounts() account_usernames = lib.Steam().get_login_users_names() - for name in account_usernames: - config.add_account(name, name) + account_steamids = lib.Steam().get_login_users_steamids() + for (i, name) in enumerate(account_usernames): + steamid = account_steamids[i] + config.add_account(name, name, steamid) config.save() self.reload_app() @@ -44,8 +46,10 @@ def create_accounts_frame(self): for (account_index, account_title) in enumerate(config.get_account_titles()): callback = partial(self.account_button_click, account_index) + user_avatar_path = lib.steam.get_user_avatar_path( + lib.config.get_account_steamid(account_index)) self.account_box = AccountBox( - self.accounts_frame, width=250, height=75, title=account_title, avatar_size=34, command=callback) + self.accounts_frame, width=250, height=75, title=account_title, image_path=user_avatar_path, avatar_size=34, command=callback) self.account_box.account_index = account_index right_click_callback = partial( self.account_button_right_click, account_index) @@ -85,7 +89,7 @@ def add_account_button_press(self): title = account_data[0] username = account_data[1] - account_index = lib.config.add_account(title, username) + account_index = lib.config.add_account(title, username, "") lib.config.save() callback = partial(self.account_button_click, account_index) button = ck.CTkButton(self.accounts_frame, From 433ddaeece10342938b87aa98326e6895e79e22d Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:43:33 +0100 Subject: [PATCH 13/21] if steamid is None or "" appropriate functions return None --- src/lib.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/lib.py b/src/lib.py index a452eff..639fe5b 100644 --- a/src/lib.py +++ b/src/lib.py @@ -79,7 +79,7 @@ def save(self): def create_config_file(self): with open(self.config_path, 'w') as f: empty_config = self.accounts_key + \ - '=[["title1", "username1"]] \ncloseOnSwitch=true' + '=[["title1", "username1", ""]] \ncloseOnSwitch=true' f.write(empty_config) f.close() @@ -141,7 +141,10 @@ def get_account_username(self, index): '''Returns account steamid specified by the account_index from the config file''' def get_account_steamid(self, index): - return self.data[self.accounts_key][index][2] + if len(self.data[self.accounts_key]) > 0: + return self.data[self.accounts_key][index][2] + else: + return None '''Sets account title specified by the account_index to new_title''' @@ -211,6 +214,8 @@ def get_login_users_steamids(self): return account_steamids def get_user_avatar_url(self, steamid): + if steamid == None or steamid == "": + return None with requests.get('https://steamcommunity.com/profiles/{}'.format(steamid)) as r: steam_page = BeautifulSoup(r.content, features="html.parser") images = steam_page.find( @@ -219,6 +224,8 @@ def get_user_avatar_url(self, steamid): return profile_picture def download_user_avatar(self, steamid, output): + if steamid == None: + return None avatar_url = self.get_user_avatar_url(steamid) avatar_data = requests.get(avatar_url).content @@ -226,6 +233,8 @@ def download_user_avatar(self, steamid, output): handler.write(avatar_data) def get_user_avatar_path(self, steamid): + if steamid == None or steamid == "": + return None user_avatar_path = os.path.join( self.account_avatars_path, steamid,).replace('/', '\\') + ".jpg" if os.path.exists(user_avatar_path): From d6635b8f04e1288fd7cbcc2a9b452a9049cb80d9 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:44:16 +0100 Subject: [PATCH 14/21] fixed manual account entry with no steamids --- src/gui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui.py b/src/gui.py index 905f3cb..157f041 100644 --- a/src/gui.py +++ b/src/gui.py @@ -46,8 +46,9 @@ def create_accounts_frame(self): for (account_index, account_title) in enumerate(config.get_account_titles()): callback = partial(self.account_button_click, account_index) + steamid = lib.config.get_account_steamid(account_index) user_avatar_path = lib.steam.get_user_avatar_path( - lib.config.get_account_steamid(account_index)) + steamid=steamid) self.account_box = AccountBox( self.accounts_frame, width=250, height=75, title=account_title, image_path=user_avatar_path, avatar_size=34, command=callback) self.account_box.account_index = account_index From ebba3753b51d055c8a49d5e6af6358d365502fc5 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 00:45:16 +0100 Subject: [PATCH 15/21] small fix, check if the string is empty --- src/accountbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accountbox.py b/src/accountbox.py index b12d530..865b781 100644 --- a/src/accountbox.py +++ b/src/accountbox.py @@ -32,7 +32,7 @@ def __init__(self, *args, avatar_size: int, ** kwargs): super().__init__(*args, width=width, height=height, **kwargs) - if image_path == None: + if image_path == None or image_path == "": self.avatar_image = get_placeholder_image(avatar_size) else: self.avatar_image = ck.CTkImage( From ad1ff20a1dacd80531d964e9644ef4d7aa280491 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 01:08:35 +0100 Subject: [PATCH 16/21] small fix, account_avatars folder is created on demand if not detected --- src/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.py b/src/lib.py index 639fe5b..438de9d 100644 --- a/src/lib.py +++ b/src/lib.py @@ -226,6 +226,8 @@ def get_user_avatar_url(self, steamid): def download_user_avatar(self, steamid, output): if steamid == None: return None + if not os.path.exists(self.account_avatars_path): + os.makedirs(self.account_avatars_path) avatar_url = self.get_user_avatar_url(steamid) avatar_data = requests.get(avatar_url).content From 6f25edfe5bd9cb7e2ba8b26a0d318d803660a997 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 04:23:43 +0100 Subject: [PATCH 17/21] now works even if user has no banner border --- src/lib.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/lib.py b/src/lib.py index 438de9d..b7e6256 100644 --- a/src/lib.py +++ b/src/lib.py @@ -141,10 +141,7 @@ def get_account_username(self, index): '''Returns account steamid specified by the account_index from the config file''' def get_account_steamid(self, index): - if len(self.data[self.accounts_key]) > 0: - return self.data[self.accounts_key][index][2] - else: - return None + return self.data[self.accounts_key][index][2] '''Sets account title specified by the account_index to new_title''' @@ -220,7 +217,13 @@ def get_user_avatar_url(self, steamid): steam_page = BeautifulSoup(r.content, features="html.parser") images = steam_page.find( 'div', attrs={"class": 'playerAvatarAutoSizeInner'}) - profile_picture = images.findAll('img')[1]['src'] + try: + profile_picture = images.findAll('img')[1]['src'] + except: + try: + profile_picture = images.findAll('img')[0]['src'] + except: + profile_picture = None return profile_picture def download_user_avatar(self, steamid, output): From 32e70cebd040247b30216cb14f81d4a4898a569b Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 22:32:20 +0100 Subject: [PATCH 18/21] Commented lib functions and moved appropriate steam functions in steam class --- src/lib.py | 114 +++++++++++++++++++++++++++++------------------------ 1 file changed, 63 insertions(+), 51 deletions(-) diff --git a/src/lib.py b/src/lib.py index b7e6256..562b104 100644 --- a/src/lib.py +++ b/src/lib.py @@ -7,58 +7,14 @@ from bs4 import BeautifulSoup import vdf import winreg -from keylistener import KeyListener -'''Closes the steam process using taskkill''' - - -def kill_steam(): - subprocess.run(["taskkill.exe", "/F", "/IM", "steam.exe"], - capture_output=False, shell=True) - - -'''Open steam process using start''' - - -def open_steam(): - subprocess.call("start steam://open/main", - creationflags=subprocess.DETACHED_PROCESS, shell=True) - - -'''Login in steam account with username parameter using registers red add''' - - -def login_steam(account_index): - global config - username = config.get_account_usernames()[account_index] - print("logging in " + username + " steam account") - subprocess.call('reg add "HKCU\Software\Valve\Steam" /v AutoLoginUser /t REG_SZ /d ' + - username + ' /f', creationflags=subprocess.CREATE_NO_WINDOW, shell=True) - subprocess.call('reg add "HKCU\Software\Valve\Steam" /v RememberPassword /t REG_DWORD /d 1 /f', - creationflags=subprocess.CREATE_NO_WINDOW, shell=True) - - -'''Closes all processes related to the app''' +'''Closes the program''' def terminate_app(): - keylistener.kill() os._exit(0) -'''Opens steam in the steam account specified by account index, quit_on_switch is -a bool that decides if the app should be closed after the account switch''' - - -def open_steam_in_account(account_index): - kill_steam() - login_steam(account_index) - open_steam() - - if config.get_close_on_switch(): - terminate_app() - - '''Config class that contains all config related logic and variables''' @@ -83,6 +39,8 @@ def create_config_file(self): f.write(empty_config) f.close() + '''Setup config class''' + def __init__(self): self.accounts_key = 'accounts' self.config_path = "config.toml" @@ -92,16 +50,17 @@ def __init__(self): self.create_config_file() self.data = self.load() + '''Returns close_on_switch bool from config''' + def get_close_on_switch(self): return self.data["closeOnSwitch"] - def toggle_close_on_switch(self): - self.set_close_on_switch(not self.get_close_on_switch()) + '''Sets close_on_switch bool to memory config''' def set_close_on_switch(self, new_value): self.data["closeOnSwitch"] = new_value - '''Returns all accounts (an array of a list which contains account usernames and titles)''' + '''Returns all the account entries in the config''' def get_accounts(self): return self.data[self.accounts_key] @@ -123,6 +82,7 @@ def get_account_titles(self): # index 0 is titles, index 1 is usernames titles.append(account[0]) return titles + '''Returns account object that contains account title and username specified by the account_index from the config file''' def get_account(self, index): @@ -169,10 +129,17 @@ def clear_accounts(self): self.data[self.accounts_key].clear() +'''Steam class containg all steam lib functions''' + + class Steam: + '''Setup Steam class''' + def __init__(self): self.account_avatars_path = 'Assets/account_avatars' + '''Returns steam path on the current machine, if not found returns None''' + def get_steam_path(self): steam_path = '' try: @@ -182,10 +149,45 @@ def get_steam_path(self): print("cannot locate steam reg files") try: - steam_path = winreg.QueryValueEx(hkey, "SteamPath") + steam_path = winreg.QueryValueEx(hkey, "SteamPath")[0] except: + steam_path = None print("Unable to locate steam path") - return steam_path[0] + return steam_path + + '''Kills the steam process on the machine''' + + def kill_steam(self): + subprocess.run(["taskkill.exe", "/F", "/IM", "steam.exe"], + capture_output=False, shell=True) + '''Launches the steam process on the machine''' + + def open_steam(self): + subprocess.call("start steam://open/main", + creationflags=subprocess.DETACHED_PROCESS, shell=True) + + '''Logins in the user specified by account index of config file''' + + def login_steam(self, account_index): + global config + username = config.get_account_usernames()[account_index] + print("logging in " + username + " steam account") + subprocess.call('reg add "HKCU\Software\Valve\Steam" /v AutoLoginUser /t REG_SZ /d ' + + username + ' /f', creationflags=subprocess.CREATE_NO_WINDOW, shell=True) + subprocess.call('reg add "HKCU\Software\Valve\Steam" /v RememberPassword /t REG_DWORD /d 1 /f', + creationflags=subprocess.CREATE_NO_WINDOW, shell=True) + + '''Kills Steam process and logs into the account specified by account_index before launching steam. + closes application based on close_on_switch bool''' + + def open_steam_in_account(self, account_index, close_on_switch: bool): + self.kill_steam() + self.login_steam(account_index) + self.open_steam() + if close_on_switch: + terminate_app() + + '''Returns object containing all the steam logins information by parsing loginusers.vdf''' def load_login_users_vdf_file(self): steam_path = self.get_steam_path().replace('/', '\\') @@ -196,6 +198,8 @@ def load_login_users_vdf_file(self): except: print('unable to locate loginusers.vdf in ' + vdf_path) + '''Returns login usernames from loginusers.vdf file''' + def get_login_users_names(self): loginusers_vdf = self.load_login_users_vdf_file() account_names = [] @@ -203,6 +207,8 @@ def get_login_users_names(self): account_names.append(user['AccountName']) return account_names + '''Returns login steamids from loginusers.vdf file''' + def get_login_users_steamids(self): loginusers_vdf = self.load_login_users_vdf_file() account_steamids = [] @@ -210,6 +216,8 @@ def get_login_users_steamids(self): account_steamids.append(steamid) return account_steamids + '''Returns url of steam account avatar by steamid''' + def get_user_avatar_url(self, steamid): if steamid == None or steamid == "": return None @@ -226,6 +234,8 @@ def get_user_avatar_url(self, steamid): profile_picture = None return profile_picture + '''Downloads steam account avatar by steamid, and saves it to output path''' + def download_user_avatar(self, steamid, output): if steamid == None: return None @@ -237,6 +247,9 @@ def download_user_avatar(self, steamid, output): with open(output, 'wb') as handler: handler.write(avatar_data) + '''Returns steam account avatar path by steamid if found, + if not found it downloads the avatar image and returns it's path''' + def get_user_avatar_path(self, steamid): if steamid == None or steamid == "": return None @@ -251,4 +264,3 @@ def get_user_avatar_path(self, steamid): config = Config() steam = Steam() -keylistener = KeyListener() From 0784479f052fc3890ac98032c3db924b585fae72 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 22:33:53 +0100 Subject: [PATCH 19/21] replaced button keybinds logic in a more correct way, adapted name and lib.py structure changes --- src/gui.py | 53 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/src/gui.py b/src/gui.py index 157f041..478cd09 100644 --- a/src/gui.py +++ b/src/gui.py @@ -8,35 +8,53 @@ ck.set_appearance_mode("dark") ck.set_default_color_theme('Assets/Theme.json') +'''Custom tkinter main window class''' + class Gui(ck.CTk): + '''Callback for close on switch widget''' + def close_on_switch(self): config.set_close_on_switch(not config.get_close_on_switch()) config.save() + '''Callback for import button widget''' + def import_accounts_button_press(self): lib.config.clear_accounts() - account_usernames = lib.Steam().get_login_users_names() - account_steamids = lib.Steam().get_login_users_steamids() + account_usernames = lib.steam.get_login_users_names() + account_steamids = lib.steam.get_login_users_steamids() for (i, name) in enumerate(account_usernames): steamid = account_steamids[i] config.add_account(name, name, steamid) config.save() - self.reload_app() + self.reload_accounts_frame() + + '''Callback for clear button widget''' def clear_accounts_button_press(self): + # unbinding account buttons + for i in range(10): + self.unbind(str(i)) config.clear_accounts() config.save() - self.reload_app() + self.reload_accounts_frame() - def account_button_click(self, account_index): - lib.open_steam_in_account(account_index) + '''Callback for accountbox button widget''' + + def account_button_click(self, account_index=None, event=None): + lib.steam.open_steam_in_account( + account_index, lib.config.get_close_on_switch()) + + '''Callback for accountbox button right click''' def account_button_right_click(self, account_index, event): lib.config.remove_account(account_index) lib.config.save() - self.reload_app() + self.reload_accounts_frame() + + '''Method that regenerates accounts_frame with all accountboxes present in the config''' def create_accounts_frame(self): self.accounts_frame = ck.CTkFrame(self) @@ -57,13 +75,15 @@ def create_accounts_frame(self): self.account_box.bind("", command=right_click_callback) self.account_box.pack(padx=5, pady=5) + + key_number = account_index + 1 + if key_number < 10: + self.bind(str(key_number), callback) return self.accounts_frame '''Prompts the user with a dialog asking Username and Title, returns a tuple containg (title, username)''' def new_account_dialog(self): - lib.keylistener.stop() - username_dialog = ck.CTkInputDialog( text="Insert Account Username", title='Add Account') username = username_dialog.get_input() @@ -75,12 +95,13 @@ def new_account_dialog(self): if title == None: return None - lib.keylistener.start() return (title, username) - def reload_app(self): + '''Reloads the accounts_frame''' + + def reload_accounts_frame(self): self.accounts_frame.destroy() - self.acoounts_frame = self.create_accounts_frame() + self.accounts_frame = self.create_accounts_frame() def add_account_button_press(self): print("adding new account") @@ -97,7 +118,9 @@ def add_account_button_press(self): text=title, command=callback) button.pack(padx=5, pady=5) button.account_index = account_index - self.reload_app() + self.reload_accounts_frame() + + '''Main window setup''' def __init__(self): super().__init__() @@ -129,9 +152,5 @@ def __init__(self): self.close_on_switch.select() self.accounts_frame = self.create_accounts_frame() - - self.bind("", lambda _: lib.keylistener.stop()) - self.bind("", lambda _: lib.keylistener.start()) - self.mainloop() lib.terminate_app() From 51628263f9872746324f3a1ac446894f6ad4f521 Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 22:34:41 +0100 Subject: [PATCH 20/21] removed keylistener.py --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index e0a8913..8ae6308 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,7 @@ import lib from lib import config -from lib import keylistener from gui import Gui from lib import steam + Gui() From 701592c6db5cc98afcc229578b2ca836bd16a41c Mon Sep 17 00:00:00 2001 From: kr1s7ian Date: Sun, 29 Jan 2023 22:35:12 +0100 Subject: [PATCH 21/21] removed module completely due to better keybinds implementation in gui,py --- src/keylistener.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 src/keylistener.py diff --git a/src/keylistener.py b/src/keylistener.py deleted file mode 100644 index 44ad5c6..0000000 --- a/src/keylistener.py +++ /dev/null @@ -1,39 +0,0 @@ -from pynput import keyboard -import lib -import threading - - -class KeyListener: - def kill(self): - self.thread.join(0) - - def on_press(self, key): - if not self.enabled: - return - - try: - pressed_index = int(key.char) - except: - return - if pressed_index == 0: - return - - if pressed_index > len(lib.config.get_accounts()): - return - - lib.open_steam_in_account(pressed_index-1) - - def listen(self): - with keyboard.Listener(on_press=self.on_press) as listener: - listener.join() - - def __init__(self): - self.enabled = True - self.thread = threading.Thread(target=self.listen) - self.thread.start() - - def stop(self): - self.enabled = False - - def start(self): - self.enabled = True