From 054aefa106a271e99e5ae54fac9297a674c8d2bc Mon Sep 17 00:00:00 2001 From: rly0nheart <74001397+rly0nheart@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:05:48 +0200 Subject: [PATCH 1/4] Improved output file naming by adding timestamps. Optimised code in the api module. Fix call functions running twice. --- knewkarma/_cli.py | 4 ++- knewkarma/_coreutils.py | 41 ++++++++++++++++++++++----- knewkarma/_project.py | 2 +- knewkarma/api.py | 61 +++++++++++++++++------------------------ knewkarma/base.py | 12 ++++---- pyproject.toml | 4 +-- 6 files changed, 72 insertions(+), 52 deletions(-) diff --git a/knewkarma/_cli.py b/knewkarma/_cli.py index 48457ac..bca61a3 100644 --- a/knewkarma/_cli.py +++ b/knewkarma/_cli.py @@ -113,6 +113,8 @@ async def setup_cli(arguments: argparse.Namespace): save_csv=arguments.csv, ) + break + # -------------------------------------------------------------------- # if not is_executed: @@ -124,7 +126,7 @@ async def setup_cli(arguments: argparse.Namespace): # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # -def execute_cli(): +def execute(): """Main entrypoint for the Knew Karma command-line interface.""" # -------------------------------------------------------------------- # diff --git a/knewkarma/_coreutils.py b/knewkarma/_coreutils.py index 08bae58..e448eee 100644 --- a/knewkarma/_coreutils.py +++ b/knewkarma/_coreutils.py @@ -4,6 +4,7 @@ import json import logging import os +from datetime import datetime from typing import Union, List from ._parser import create_parser @@ -13,15 +14,13 @@ # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # -def timestamp_to_utc(timestamp: int) -> str: +def unix_timestamp_to_utc(timestamp: int) -> str: """ Converts a UNIX timestamp to a formatted datetime.utc string. :param timestamp: The UNIX timestamp to be converted. :return: A formatted datetime.utc string in the format "dd MMMM yyyy, hh:mm:ssAM/PM" """ - from datetime import datetime - utc_from_timestamp: datetime = datetime.utcfromtimestamp(timestamp) datetime_string: str = utc_from_timestamp.strftime("%d %B %Y, %I:%M:%S%p") @@ -31,6 +30,30 @@ def timestamp_to_utc(timestamp: int) -> str: # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # +def filename_timestamp() -> str: + """ + Generates a timestamp string suitable for file naming, based on the current date and time. + The format of the timestamp is adapted based on the operating system. + + :return: The formatted timestamp as a string. The format is "%d-%B-%Y-%I-%M-%S%p" for Windows + and "%d-%B-%Y-%I:%M:%S%p" for non-Windows systems. + + Example + ------- + - Windows: "20-July-1969-08-17-45PM" + - Non-Windows: "20-July-1969-08:17:45PM" (format may vary based on the current date and time) + """ + now = datetime.now() + return ( + now.strftime("%d-%B-%Y-%I-%M-%S%p") + if os.name == "nt" + else now.strftime("%d-%B-%Y-%I:%M:%S%p") + ) + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # + + def pathfinder(directories: list[str]): """ Creates directories in knewkarma-data directory of the user's home folder. @@ -47,8 +70,8 @@ def pathfinder(directories: list[str]): def save_data( data: Union[User, Subreddit, List[Union[Post, Comment]]], save_to_dir: str, - save_json: bool = False, - save_csv: bool = False, + save_json: Union[bool, str] = False, + save_csv: Union[bool, str] = False, ): """ Save the given (Reddit) data to a JSON/CSV file based on the save_csv and save_json parameters. @@ -74,7 +97,9 @@ def save_data( # -------------------------------------------------------------------- # if save_json: - json_path = os.path.join(save_to_dir, "json", f"{save_json}.json") + json_path = os.path.join( + save_to_dir, "json", f"{save_json.upper()}-{filename_timestamp()}.json" + ) with open(json_path, "w", encoding="utf-8") as json_file: json.dump(function_data, json_file, indent=4) log.info( @@ -84,7 +109,9 @@ def save_data( # -------------------------------------------------------------------- # if save_csv: - csv_path = os.path.join(save_to_dir, "csv", f"{save_csv}.csv") + csv_path = os.path.join( + save_to_dir, "csv", f"{save_csv.upper()}-{filename_timestamp()}.csv" + ) with open(csv_path, "w", newline="", encoding="utf-8") as csv_file: writer = csv.writer(csv_file) if isinstance(function_data, dict): diff --git a/knewkarma/_project.py b/knewkarma/_project.py index d0be520..775e814 100644 --- a/knewkarma/_project.py +++ b/knewkarma/_project.py @@ -6,7 +6,7 @@ author: str = "Richard Mwewa" about_author: str = "https://about.me/rly0nheart" -version: str = "3.2.0.0" +version: str = "3.3.0.0" # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # diff --git a/knewkarma/api.py b/knewkarma/api.py index a833722..888273e 100644 --- a/knewkarma/api.py +++ b/knewkarma/api.py @@ -10,7 +10,9 @@ # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # BASE_REDDIT_ENDPOINT: str = "https://www.reddit.com" -PYPI_PROJECT_ENDPOINT: str = "https://pypi.org/pypi/knewkarma/json" +GITHUB_RELEASE_ENDPOINT: str = ( + "https://api.github.com/repos/rly0nheart/knewkarma/releases/latest" +) # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # @@ -91,13 +93,18 @@ async def get_updates(session: aiohttp.ClientSession): :param session: aiohttp session to use for the request. """ + import rich + from rich.markdown import Markdown # Make a GET request to PyPI to get the project's latest release. - response: dict = await get_data(endpoint=PYPI_PROJECT_ENDPOINT, session=session) - release: dict = process_response(response_data=response.get("info", {})) + response: dict = await get_data(endpoint=GITHUB_RELEASE_ENDPOINT, session=session) + release: dict = process_response(response_data=response, valid_key="tag_name") if release: - remote_version: str = release.get("version") + remote_version: str = release.get("tag_name") + markup_release_notes: str = release.get("body") + markdown_release_notes = Markdown(markup=markup_release_notes) + # Splitting the version strings into components remote_parts: list = remote_version.split(".") local_parts: list = version.split(".") @@ -109,7 +116,7 @@ async def get_updates(session: aiohttp.ClientSession): # Check for differences in version parts if remote_parts[0] != local_parts[0]: update_message = ( - f"MAJOR update ({remote_version}) available:" + f"[bold][red]MAJOR[/][/] update ({remote_version}) available:" f" It might introduce significant changes." ) @@ -117,7 +124,7 @@ async def get_updates(session: aiohttp.ClientSession): elif remote_parts[1] != local_parts[1]: update_message = ( - f"MINOR update ({remote_version}) available:" + f"[bold][blue]MINOR[/][/] update ({remote_version}) available:" f" Includes small feature changes/improvements." ) @@ -125,7 +132,7 @@ async def get_updates(session: aiohttp.ClientSession): elif remote_parts[2] != local_parts[2]: update_message = ( - f"PATCH update ({remote_version}) available:" + f"[bold][green]PATCH[/][/] update ({remote_version}) available:" f" Generally for bug fixes and small tweaks." ) @@ -137,7 +144,7 @@ async def get_updates(session: aiohttp.ClientSession): and remote_parts[3] != local_parts[3] ): update_message = ( - f"BUILD update ({remote_version}) available." + f"[bold][cyan]BUILD[/][/] update ({remote_version}) available." f" Might be for specific builds or special versions." ) @@ -145,6 +152,7 @@ async def get_updates(session: aiohttp.ClientSession): if update_message: log.info(update_message) + rich.print(markdown_release_notes) # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # @@ -227,51 +235,32 @@ async def get_posts( "front_page_posts": f"{BASE_REDDIT_ENDPOINT}/.json?sort={sort}&limit={limit}&t={timeframe}", } + # ---------------------------------------------------------- # + endpoint = source_map.get(posts_type, "") if not endpoint: raise ValueError(f"Invalid profile type in {source_map}: {posts_type}") - all_posts = await paginated_posts( - posts_endpoint=endpoint, limit=limit, session=session - ) - - return all_posts[:limit] - - -# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # - - -async def paginated_posts( - posts_endpoint: str, limit: int, session: aiohttp.ClientSession -) -> list: - """ - Paginates and retrieves posts until the specified limit is reached. - - :param posts_endpoint: API endpoint for retrieving posts. - :param limit: Limit of the number of posts to retrieve. - :param session: aiohttp session to use for the request. - :return: A list of all posts. - """ all_posts: list = [] last_post_id: str = "" # Determine whether to use the 'after' parameter - use_after: bool = limit > 100 + paginate_posts: bool = limit > 100 - while len(all_posts) < limit: - # ---------------------------------------------------------- # + # ---------------------------------------------------------- # + while len(all_posts) < limit: # Make the API request with the 'after' parameter if it's provided and the limit is more than 100 - if use_after and last_post_id: - endpoint_with_after: str = f"{posts_endpoint}&after={last_post_id}" + if paginate_posts and last_post_id: + pagination_endpoint: str = f"{endpoint}&after={last_post_id}" else: - endpoint_with_after: str = posts_endpoint + pagination_endpoint: str = endpoint # ---------------------------------------------------------- # raw_posts_data: dict = await get_data( - endpoint=endpoint_with_after, session=session + endpoint=pagination_endpoint, session=session ) posts_list: list = raw_posts_data.get("data", {}).get("children", []) diff --git a/knewkarma/base.py b/knewkarma/base.py index 22249ef..76f5b14 100644 --- a/knewkarma/base.py +++ b/knewkarma/base.py @@ -4,7 +4,7 @@ import aiohttp -from ._coreutils import timestamp_to_utc +from ._coreutils import unix_timestamp_to_utc from .api import get_profile, get_posts from .data import User, Subreddit, Comment, Post @@ -67,7 +67,7 @@ async def profile(self, session: aiohttp.ClientSession) -> User: awardee_karma=user_profile.get("awardee_karma"), total_karma=user_profile.get("total_karma"), subreddit=user_profile.get("subreddit"), - created_at=timestamp_to_utc(timestamp=user_profile.get("created")), + created_at=unix_timestamp_to_utc(timestamp=user_profile.get("created")), raw_data=user_profile, ) @@ -127,7 +127,7 @@ async def comments(self, session: aiohttp.ClientSession) -> List[Comment]: is_stickied=comment_data.get("stickied"), is_locked=comment_data.get("locked"), is_archived=comment_data.get("archived"), - created_at=timestamp_to_utc(timestamp=comment_data.get("created")), + created_at=unix_timestamp_to_utc(timestamp=comment_data.get("created")), subreddit=comment_data.get("subreddit_name_prefixed"), subreddit_type=comment_data.get("subreddit_type"), post_id=comment_data.get("link_id"), @@ -191,7 +191,9 @@ async def profile(self, session: aiohttp.ClientSession) -> Subreddit: current_active_users=subreddit_profile.get("accounts_active"), is_nsfw=subreddit_profile.get("over18"), language=subreddit_profile.get("lang"), - created_at=timestamp_to_utc(timestamp=subreddit_profile.get("created")), + created_at=unix_timestamp_to_utc( + timestamp=subreddit_profile.get("created") + ), raw_data=subreddit_profile, ) @@ -273,7 +275,7 @@ def process_posts(raw_posts: list) -> List[Post]: permalink=post_data.get("permalink"), is_locked=post_data.get("locked"), is_archived=post_data.get("archived"), - created_at=timestamp_to_utc(timestamp=post_data.get("created")), + created_at=unix_timestamp_to_utc(timestamp=post_data.get("created")), raw_post=post_data, ) posts_list.append(post) diff --git a/pyproject.toml b/pyproject.toml index 801b23d..26ec542 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "knewkarma" -version = "3.2.0.0" +version = "3.3.0.0" description = "A Reddit Data Analysis Toolkit." authors = ["Richard Mwewa "] readme = "README.md" @@ -31,4 +31,4 @@ rich = "*" rich-argparse = "*" [tool.poetry.scripts] -knewkarma = "knewkarma._cli:execute_cli" +knewkarma = "knewkarma._cli:execute" From 529e9c4fccfc4a08112428403b82b84c71643069 Mon Sep 17 00:00:00 2001 From: rly0nheart <74001397+rly0nheart@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:41:00 +0000 Subject: [PATCH 2/4] Optimised code in ApiHandler and CoreUtils --- Knew Karma/KnewKarma/Handlers/ApiHandler.vb | 97 ++-- .../KnewKarma/Handlers/DataGridViewHandler.vb | 2 +- Knew Karma/KnewKarma/KnewKarma.vbproj | 6 +- Knew Karma/KnewKarma/KnewKarma.vbproj.user | 3 + Knew Karma/KnewKarma/Utilities/CoreUtils.vb | 304 ------------ Knew Karma/KnewKarma/Utilities/Settings.vb | 466 ------------------ Knew Karma/KnewKarma/Windows/AboutWindow.vb | 3 +- .../KnewKarma/Windows/MainWindow.Designer.vb | 12 +- .../KnewKarmaSetup/KnewKarmaSetup.vdproj | 8 +- 9 files changed, 71 insertions(+), 830 deletions(-) delete mode 100644 Knew Karma/KnewKarma/Utilities/CoreUtils.vb delete mode 100644 Knew Karma/KnewKarma/Utilities/Settings.vb diff --git a/Knew Karma/KnewKarma/Handlers/ApiHandler.vb b/Knew Karma/KnewKarma/Handlers/ApiHandler.vb index 1996b6a..ca37f3b 100644 --- a/Knew Karma/KnewKarma/Handlers/ApiHandler.vb +++ b/Knew Karma/KnewKarma/Handlers/ApiHandler.vb @@ -62,8 +62,52 @@ Public Class ApiHandler ''' ''' Asynchronously fetches the program's update information from GitHub. ''' - Public Async Function AsyncGetUpdates() As Task(Of JObject) - Return Await AsyncGetData(endpoint:="https://api.github.com/repos/bellingcat/knewkarma/releases/latest") + Public Async Function AsyncGetUpdates() As Task + AboutWindow.Version.Text = "Checking for updates..." + ' Making an asynchronous request to check for updates. + Dim data As JObject = Await AsyncGetData(endpoint:="https://api.github.com/repos/rly0nheart/knewkarma/releases/latest") + + ' Checking if data is not null before proceeding with extracting information from it. + If data IsNot Nothing Then + ' Extracting the tag name, body, and HTML URL from the data. + Dim tagName As String = data("tag_name")?.ToString + + ' Checking if the current version is the latest version. + If tagName = My.Application.Info.Version.ToString Then + AboutWindow.Version.Text = $"Up-to-date: {My.Application.Info.Version}" + Else + AboutWindow.Version.Text = $"Updates found: {tagName}" + AboutWindow.ButtonGetUpdates.Enabled = True + End If + End If + End Function + + + ''' + ''' Asynchronously retrieves profile data from a specified source. + ''' + ''' The type of profile to retrieve. + ''' The source from where the profile should be fetched (e.g., specific user or subreddit). + ''' A Task(Of JObject) representing the asynchronous operation, which upon completion returns a Jobject of profile data. + Public Async Function AsyncGetProfile( + profileType As String, profileSource As String) As Task(Of JObject) + Dim profileTypeMap As New List(Of Tuple(Of String, String)) From { + Tuple.Create("user_profile", $"{BASE_ENDPOINT}/user/{profileSource}/about.json"), + Tuple.Create("subreddit_profile", $"{BASE_ENDPOINT}/r/{profileSource}/about.json") + } + + Dim profileEndpoint As String = Nothing + + For Each Type In profileTypeMap + If Type.Item1 = profileType Then + profileEndpoint = Type.Item2 + Exit For + End If + Next + + Dim profile As JObject = Await AsyncGetData(endpoint:=profileEndpoint) + + Return If(profile IsNot Nothing AndAlso profile?("data") IsNot Nothing, profile?("data"), New JObject()) End Function @@ -95,23 +139,13 @@ Public Class ApiHandler End If Dim postsEndpoint As String = postsTypeMap(postsType) - Return Await PaginatedPosts(postsEndpoint, postsLimit) - End Function - - ''' - ''' Retrieves posts in a paginated manner until the specified limit is reached. - ''' - ''' The API endpoint for retrieving posts. - ''' The limit on the number of posts to retrieve. - ''' A Task(Of JArray) representing the asynchronous operation, which upon completion returns a JArray of posts. - Private Async Function PaginatedPosts(endpoint As String, limit As Integer) As Task(Of JArray) Dim allPosts As New JArray() - Dim lastPostId As String = "" - Dim useAfter As Boolean = limit > 100 + Dim lastPostId As String = String.Empty + Dim paginatePosts As Boolean = postsLimit > 100 - While allPosts.Count < limit - Dim endpointWithAfter As String = If(useAfter And Not String.IsNullOrEmpty(lastPostId), $"{endpoint}&after={lastPostId}", endpoint) - Dim postsData As JObject = Await AsyncGetData(endpoint:=endpointWithAfter) + While allPosts.Count < postsLimit + Dim paginationEndpoint As String = If(paginatePosts And Not String.IsNullOrEmpty(lastPostId), $"{postsEndpoint}&after={lastPostId}", postsEndpoint) + Dim postsData As JObject = Await AsyncGetData(endpoint:=paginationEndpoint) Dim postsChildren As JArray = postsData("data")("children") If postsChildren.Count = 0 Then @@ -125,32 +159,5 @@ Public Class ApiHandler Return allPosts End Function - - - ''' - ''' Asynchronously retrieves profile data from a specified source. - ''' - ''' The type of profile to retrieve. - ''' The source from where the profile should be fetched (e.g., specific user or subreddit). - ''' A Task(Of JObject) representing the asynchronous operation, which upon completion returns a Jobject of profile data. - Public Async Function AsyncGetProfile( - profileType As String, profileSource As String) As Task(Of JObject) - Dim profileTypeMap As New List(Of Tuple(Of String, String)) From { - Tuple.Create("user_profile", $"{BASE_ENDPOINT}/user/{profileSource}/about.json"), - Tuple.Create("subreddit_profile", $"{BASE_ENDPOINT}/r/{profileSource}/about.json") - } - - Dim profileEndpoint As String = Nothing - - For Each Type In profileTypeMap - If Type.Item1 = profileType Then - profileEndpoint = Type.Item2 - Exit For - End If - Next - - Dim profile As JObject = Await AsyncGetData(endpoint:=profileEndpoint) - - Return If(profile IsNot Nothing AndAlso profile?("data") IsNot Nothing, profile?("data"), New JObject()) - End Function End Class + diff --git a/Knew Karma/KnewKarma/Handlers/DataGridViewHandler.vb b/Knew Karma/KnewKarma/Handlers/DataGridViewHandler.vb index 9cd5255..e8049b0 100644 --- a/Knew Karma/KnewKarma/Handlers/DataGridViewHandler.vb +++ b/Knew Karma/KnewKarma/Handlers/DataGridViewHandler.vb @@ -51,7 +51,7 @@ Public Class DataGridViewer ' Special handling for the "created" timestamp If RowKey = "created" Then Dim timestamp As Double = CDbl(data("data")(RowKey)) - value = CoreUtils.ConvertTimestampToDatetime(timestamp) + value = CoreUtils.UnixTimestampToUtc(timestamp:=timestamp) Else value = If(data("data")(RowKey)?.ToString(), "N/A") End If diff --git a/Knew Karma/KnewKarma/KnewKarma.vbproj b/Knew Karma/KnewKarma/KnewKarma.vbproj index ace5c61..0e3fef0 100644 --- a/Knew Karma/KnewKarma/KnewKarma.vbproj +++ b/Knew Karma/KnewKarma/KnewKarma.vbproj @@ -12,11 +12,11 @@ https://github.com/bellingcat/knewkarma/wiki README.md https://github.com/bellingcat/knewkarma - 3.2.0.0 - 3.2.0.0 + 3.3.0.0 + 3.3.0.0 LICENSE True - 3.2.0 + 3.3.0 reddit;scraper;reddit-scraper;osint;reddit-data 6.0-recommended diff --git a/Knew Karma/KnewKarma/KnewKarma.vbproj.user b/Knew Karma/KnewKarma/KnewKarma.vbproj.user index ee29f88..471fe9d 100644 --- a/Knew Karma/KnewKarma/KnewKarma.vbproj.user +++ b/Knew Karma/KnewKarma/KnewKarma.vbproj.user @@ -13,6 +13,9 @@ Form + + Form + Form diff --git a/Knew Karma/KnewKarma/Utilities/CoreUtils.vb b/Knew Karma/KnewKarma/Utilities/CoreUtils.vb deleted file mode 100644 index 8fe7ab1..0000000 --- a/Knew Karma/KnewKarma/Utilities/CoreUtils.vb +++ /dev/null @@ -1,304 +0,0 @@ -Imports System.IO -Imports Microsoft.Win32 -Imports Newtonsoft.Json -Imports Newtonsoft.Json.Linq -Imports System.Globalization - -Public Class CoreUtils - ''' - ''' Handles the enabling and disabling of various controls based on the state of radio buttons on the main form. - ''' - ''' The main form containing the radio buttons and controls to be manipulated. - Public Shared Sub HandleRadioButtonChanges(form As MainWindow) - ' Check the state of user radio buttons and enable/disable relevant controls accordingly - ' Depending on which radio button is selected, different sets of controls will be enabled or disabled - ' to provide a more intuitive user experience and prevent invalid configurations. - - ' Handling User Radio Buttons - If form.RadioButtonUserProfile.Checked Then - ' If the User Profile radio button is checked, disable the user data limit and posts listing controls - form.NumericUpDownUserDataLimit.Enabled = False - form.ComboBoxUserDataListing.Enabled = False - ElseIf form.RadioButtonUserPosts.Checked Then - ' If the User Posts radio button is checked, enable the user data limit and posts listing controls - form.NumericUpDownUserDataLimit.Enabled = True - form.ComboBoxUserDataListing.Enabled = True - ElseIf form.RadioButtonUserComments.Checked Then - ' If the User Comments radio button is checked, enable the user data limit control and disable the posts listing control - form.NumericUpDownUserDataLimit.Enabled = True - form.ComboBoxUserDataListing.Enabled = True - End If - - ' Handling Subreddit Radio Buttons - ' Similar to the user radio buttons above, check the state of subreddit radio buttons - ' and enable/disable the corresponding controls to ensure a coherent set of options is available to the user. - - If form.RadioButtonSubredditProfile.Checked Then - ' If the Subreddit Profile radio button is checked, disable the subreddit data limit and posts listing controls - form.NumericUpDownSubredditPostsLimit.Enabled = False - form.ComboBoxSubredditPostsListing.Enabled = False - ElseIf form.RadioButtonSubredditPosts.Checked Then - ' If the Subreddit Posts radio button is checked, enable the subreddit data limit and posts listing controls - form.NumericUpDownSubredditPostsLimit.Enabled = True - form.ComboBoxSubredditPostsListing.Enabled = True - End If - End Sub - - ''' - ''' Determines if the Windows system theme is in dark mode. - ''' - ''' - ''' True if the dark mode is enabled, otherwise false. - ''' - Public Shared Function IsSystemDarkTheme() As Boolean - Dim registryKey As RegistryKey = Registry.CurrentUser.OpenSubKey( - "Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" - ) - If registryKey IsNot Nothing Then - Dim appsUseLightTheme As Object = registryKey.GetValue("AppsUseLightTheme") - Return appsUseLightTheme IsNot Nothing AndAlso CType(appsUseLightTheme, Integer) = 0 - Else - Return False - End If - End Function - - ''' - ''' Asynchronously checks for available updates and optionally displays a message to the user. - ''' - ''' Indicates whether the update check is triggered automatically. - ''' A task representing the asynchronous operation. - Public Shared Async Function AsyncCheckUpdates() As Task - AboutWindow.Version.Text = "Checking for Updates..." - ' Creating a new instance of the ApiHandler class to interact with the API. - Dim Api As New ApiHandler() - - ' Making an asynchronous request to check for updates. - Dim data As JObject = Await Api.AsyncGetUpdates() - - ' Checking if data is not null before proceeding with extracting information from it. - If data IsNot Nothing Then - ' Extracting the tag name, body, and HTML URL from the data. - Dim tagName As String = data("tag_name")?.ToString - - ' Checking if the current version is the latest version. - If tagName = My.Application.Info.Version.ToString Then - AboutWindow.Version.Text = $"Up-to-date: {My.Application.Info.Version}" - Else - AboutWindow.Version.Text = $"Updates found: {tagName}" - AboutWindow.ButtonGetUpdates.Enabled = True - End If - End If - End Function - - ''' - ''' Checks whether the given JSON data (either JObject or JArray) is null or empty. - ''' - ''' The JToken (JObject/JArray) to be validated. - ''' True if the data is not null and contains an "id" key, otherwise False. - Public Shared Function IsValidData(data As JToken) As Boolean - If data IsNot Nothing AndAlso ( - (TypeOf data Is JArray AndAlso DirectCast(data, JArray).Any()) OrElse - (TypeOf data Is JObject AndAlso (data("id") IsNot Nothing)) - ) Then - Return True - Else - Return False - End If - End Function - - ''' - ''' Converts a Unix timestamp with possible decimal points to a formatted datetime string. - ''' - ''' The Unix timestamp to be converted. - ''' A formatted datetime string in the format "dd MMMM yyyy, hh:mm:ss.fff tt". - Public Shared Function ConvertTimestampToDatetime(ByVal timestamp As Double) As String - Dim utcFromTimestamp As Date = New DateTime( - 1970, - 1, - 1, - 0, - 0, - 0, - DateTimeKind.Utc - ).AddSeconds(timestamp) - Dim datetimeString As String = utcFromTimestamp.ToString( - "dd MMMM yyyy, hh:mm:ss tt", - CultureInfo.InvariantCulture - ) - Return datetimeString - End Function - - ''' - ''' Shows the license notice in a messagebox. - ''' - Public Shared Sub License() - MessageBox.Show( - $"{My.Application.Info.Copyright} - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the ""Software""), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", - "MIT License", - MessageBoxButtons.OK, - MessageBoxIcon.Information - ) - End Sub - - ''' - ''' Checks for the existence of the 'Knew Karma' directory within the user's AppData\Roaming folder. - ''' If the directory does not exist, it creates one. - ''' - Public Shared Sub PathFinder() - Dim directoryPath As String = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Knew Karma" - ) - - If Not Directory.Exists(directoryPath) Then - Directory.CreateDirectory(directoryPath) - End If - End Sub - - ''' - ''' Prompts the user to save the data in either JSON or CSV format based on the settings specified in FormMain. - ''' - ''' The data to save, represented as a JToken, which can accommodate both JArray and JObject. - Public Shared Sub PromptSaveData(data As JToken, title As String) - ' Save profile data to JSON if the JSON toolStripMenuItem is checked. - If MainWindow.settings.SaveToJson Then - SaveDataToJson(data:=data, title:=title) - End If - ' Save profile data to CSV if the CSV toolStripMenuItem is checked. - If MainWindow.settings.SaveToCsv Then - SaveDataToCSV(data:=data, title:=title) - End If - End Sub - - ''' - ''' Saves the provided data to a JSON file. - ''' - ''' The data to save, which can be a JObject or a JArray. - ''' The title to use in the SaveFileDialog and the success message. - Private Shared Sub SaveDataToJson(data As JToken, title As String) - ' Initialize a new SaveFileDialog with the specified filter and title - Dim saveFileDialog As New SaveFileDialog With { - .Filter = "JSON files (*.json)|*.json", - .title = $"Save data to JSON" - } - - ' Open the SaveFileDialog and if the result is OK, proceed to save the data - If saveFileDialog.ShowDialog() = DialogResult.OK Then - Dim fileName As String = saveFileDialog.FileName - - ' Check if the data is not null and has values before proceeding to serialize it - If data Is Nothing OrElse (TypeOf data Is JArray AndAlso Not CType(data, JArray).HasValues) OrElse (TypeOf data Is JObject AndAlso Not CType(data, JObject).HasValues) Then - MessageBox.Show( - "Empty or null data cannot be saved.", - "Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ) - Exit Sub - End If - - ' Define serializer settings to format the JSON string nicely - Dim serializerSettings As New JsonSerializerSettings With { - .Formatting = Formatting.Indented - } - - ' Serialize the JToken (which could be a JArray or a JObject) to a JSON string with the defined settings - Dim json As String = JsonConvert.SerializeObject(data, serializerSettings) - - ' Write the JSON string to a file with the specified file name - File.WriteAllText(fileName, json) - - ' Show a message indicating that the data has been successfully saved - MessageBox.Show( - $"{title} data saved to {fileName}", - "Saved", - MessageBoxButtons.OK, - MessageBoxIcon.Information - ) - End If - End Sub - - ''' - ''' Saves the provided data to a CSV file. - ''' - ''' The data to save, which can be a JObject or a JArray. - ''' The title to use in the SaveFileDialog and the success message. - Private Shared Sub SaveDataToCSV(data As JToken, title As String) - ' Initialize a new SaveFileDialog with the specified filter and title - Dim saveFileDialog As New SaveFileDialog With { - .Filter = "CSV files (*.csv)|*.csv", - .title = $"Save data to CSV" - } - - ' Open the SaveFileDialog and if the result is OK, proceed to save the data - If saveFileDialog.ShowDialog() = DialogResult.OK Then - Dim fileName As String = saveFileDialog.FileName - - ' Create a new StreamWriter to write data to the CSV file - Using csvWriter As New StreamWriter(fileName) - ' Variable to store the headers obtained from the data - Dim headers As String() = Nothing - - ' Check if the data is a JArray and has values, and if true, get the headers from the first JObject in the JArray - If TypeOf data Is JArray AndAlso data.HasValues Then - headers = CType(data(0), JObject).Properties().Select(Function(p) p.Name).ToArray() - ' Check if the data is a JObject and has values, and if true, get the headers from the JObject - ElseIf TypeOf data Is JObject AndAlso data.HasValues Then - headers = CType(data, JObject).Properties().Select(Function(p) p.Name).ToArray() - End If - - ' If headers are not obtained, show an error message and exit the subroutine - If headers Is Nothing OrElse Not headers.Any() Then - MessageBox.Show( - "Unsupported data type or empty data.", - "Error", - MessageBoxButtons.OK, - MessageBoxIcon.Error - ) - Exit Sub - End If - - ' Write the headers to the CSV file - csvWriter.WriteLine(String.Join(",", headers)) - - ' Create an IEnumerable of JObject to handle both JArray and JObject cases - ' If it's a JArray, cast it to IEnumerable of JObject - ' If it's a JObject, create a new array with this single JObject - Dim dataRows As IEnumerable(Of JObject) = If(TypeOf data Is JArray, CType(data, JArray).Cast(Of JObject)(), New JObject() {CType(data, JObject)}) - - ' Loop through each row in dataRows to write the data to the CSV file - For Each row In dataRows - ' For each header, get the corresponding value from the current row (if it exists, otherwise use "N/A") - ' and write this set of values to the CSV file - Dim values = headers.Select(Function(header) If(row(header)?.ToString(), "N/A")).ToArray() - csvWriter.WriteLine(String.Join(",", values)) - Next - End Using - - ' Show a message to indicate that the data has been successfully saved - MessageBox.Show( - $"{title} saved to {fileName}", - "Success", - MessageBoxButtons.OK, - MessageBoxIcon.Information - ) - End If - End Sub -End Class diff --git a/Knew Karma/KnewKarma/Utilities/Settings.vb b/Knew Karma/KnewKarma/Utilities/Settings.vb deleted file mode 100644 index 1ad42a1..0000000 --- a/Knew Karma/KnewKarma/Utilities/Settings.vb +++ /dev/null @@ -1,466 +0,0 @@ -Imports System.Globalization -Imports System.IO -Imports System.Security.Principal -Imports Newtonsoft.Json - -Public Class SettingsManager - ''' - ''' Represents the Dark Mode property. - ''' Indicates whether the dark mode is enabled or disabled. - ''' - Public Property DarkMode As Boolean - - ''' - ''' Represents the SaveToJson property. - ''' Indicates whether the application will save data to JSON files. - ''' - Public Property SaveToJson As Boolean - - ''' - ''' Represents the SaveToCsv property. - ''' Indicates whether the application will save data to CSV files. - ''' - Public Property SaveToCsv As Boolean - - ''' - ''' Contains the color settings for different visual themes (e.g., Dark, Light). - ''' The outer dictionary key represents the theme (Dark, Light), - ''' and the inner dictionary contains the color settings. - ''' - Public Property ColorSettings As Dictionary(Of String, Dictionary(Of String, String)) - - ''' - ''' Represents the path where the settings file is stored. - ''' - Private ReadOnly settingsFilePath As String = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Knew Karma", - "settings.json" - ) - - ''' - ''' Loads application settings from the 'settings.json' file. - ''' If the settings file doesn't exist, it creates a new file with default settings. - ''' - Public Sub LoadSettings() - If File.Exists(settingsFilePath) Then - Dim json As String = File.ReadAllText(settingsFilePath) - Dim settings = JsonConvert.DeserializeObject(Of SettingsManager)(json) - - DarkMode = settings.DarkMode - SaveToJson = settings.SaveToJson - SaveToCsv = settings.SaveToCsv - ColorSettings = settings.ColorSettings - - MainWindow.ToJSONToolStripMenuItem.Checked = settings.SaveToJson - MainWindow.ToCSVToolStripMenuItem.Checked = settings.SaveToCsv - Else - ' Settings file does not exist - Dim defaultSettings = New SettingsManager With { - .DarkMode = False, - .SaveToCsv = False, - .SaveToJson = False, - .ColorSettings = New Dictionary(Of String, Dictionary(Of String, String))() From { - {"Dark", New Dictionary(Of String, String)() From { - {"MainBackgroundColor", "#FF1A1A1A"}, - {"PrimaryTextColor", "#FFFFFFFF"}, - {"SecondaryTextColor", "#FFFF4500"}, - {"InputFieldBackgroundColor", "#FF2E2E2E"}, - {"MenuItemMouseEnterColor", "#FF000000"} - }}, - {"Light", New Dictionary(Of String, String)() From { - {"MainBackgroundColor", "#FFF0F0F0"}, - {"PrimaryTextColor", "#FF121212"}, - {"SecondaryTextColor", "#FF5F99CF"}, - {"InputFieldBackgroundColor", "#FFFFFAFA"}, - {"MenuItemMouseEnterColor", "#FFFFFFFF"} - }} - } - } - - ' Write settings to jsonOutput file - Dim jsonOutput = JsonConvert.SerializeObject(defaultSettings) - File.WriteAllText(settingsFilePath, jsonOutput) - - SaveToJson = False - SaveToCsv = False - ColorSettings = defaultSettings.ColorSettings - MainWindow.ToJSONToolStripMenuItem.Checked = False - MainWindow.ToCSVToolStripMenuItem.Checked = False - - ' Check the system settings to see if dark mode is enabled/disabled - If CoreUtils.IsSystemDarkTheme() Then - ' If dark mode is enabled in the system settings, update the program's dark mode settings: - ' set DarkMode to True, check the DarkModeEnable menu item, and uncheck the DarkModeDisable menu item. - DarkMode = True - MainWindow.DarkModeEnableToolStripMenuItem.Checked = True - MainWindow.DarkModeDisableToolStripMenuItem.Checked = False - Else - ' If dark mode is not enabled in the system settings, update the program's dark mode settings: - ' set DarkMode to False, uncheck the DarkModeEnable menu item, and check the DarkModeDisable menu item. - DarkMode = False - MainWindow.DarkModeEnableToolStripMenuItem.Checked = False - MainWindow.DarkModeDisableToolStripMenuItem.Checked = True - End If - End If - End Sub - - ''' - ''' Retrieves application settings as key-value pairs. - ''' - ''' A Dictionary containing the names and values of all settings. - Private Function GetSettings() As Dictionary(Of String, Object) - Dim settings As New Dictionary(Of String, Object)() - - If File.Exists(settingsFilePath) Then - Dim json As String = File.ReadAllText(settingsFilePath) - settings = JsonConvert.DeserializeObject(Of Dictionary(Of String, Object))(json) - End If - - Return settings - End Function - - ''' - ''' Saves the provided settings to a JSON file with a read-only attribute. - ''' - ''' An instance of the SettingsManager containing the configurations to be saved. - Private Sub SaveSettings(settings) - ' Prepare the file to be written by ensuring it's not read-only - Dim fileInfo As New FileInfo(settingsFilePath) - If fileInfo.Exists AndAlso fileInfo.IsReadOnly Then - fileInfo.IsReadOnly = False - End If - - ' Serialize and write the data - Dim jsonOutput = JsonConvert.SerializeObject(settings) - File.WriteAllText(settingsFilePath, jsonOutput) - - ' Reset the read-only attribute - fileInfo.Refresh() ' This ensures that we're working with the latest file info - fileInfo.IsReadOnly = True - End Sub - - ''' - ''' Applies the current settings to the application's interface. This includes - ''' toggling SaveToJson, SaveToCsv, and applying the visual theme based on the Dark Mode setting. - ''' - Public Sub ApplySettings() - ' Retrieve the current settings - Dim settings As Dictionary(Of String, Object) = GetSettings() - ' Apply the SaveToJson setting to the menu item checkbox - MainWindow.ToJSONToolStripMenuItem.Checked = Me.SaveToJson - - ' Apply the SaveToCsv setting to the menu item checkbox - MainWindow.ToCSVToolStripMenuItem.Checked = Me.SaveToCsv - - ' Use ColorSettings property directly - Dim colorSettings As Dictionary(Of String, Dictionary(Of String, String)) = Me.ColorSettings - - ' Apply the color scheme based on the Dark Mode setting - ApplyColorScheme(isDarkMode:=CBool(settings("DarkMode")), colorSettings:=colorSettings) - End Sub - - ''' - ''' Applies the color scheme based on the given Dark Mode setting. - ''' Colors are defined in a mapping for easier maintenance and flexibility. - ''' - ''' Indicates whether Dark Mode is enabled. - Public Shared Sub ApplyColorScheme( - ByVal isDarkMode As Boolean, - ByVal colorSettings As Dictionary(Of String, Dictionary(Of String, String)) - ) - Dim color As New Dictionary(Of String, Color) - - Dim mode As String = If(isDarkMode, "Dark", "Light") - Dim modeColors As Dictionary(Of String, String) = colorSettings(mode) - - For Each colorName As String In modeColors.Keys - color(colorName) = ColorTranslator.FromHtml(modeColors(colorName)) - Next - - SetUIColors(color:=color) - - If isDarkMode Then - MainWindow.DarkModeEnableToolStripMenuItem.Text = "Enabled" - MainWindow.DarkModeDisableToolStripMenuItem.Text = "Disable" - MainWindow.DarkModeEnableToolStripMenuItem.Checked = True - MainWindow.DarkModeDisableToolStripMenuItem.Checked = False - Else - MainWindow.DarkModeEnableToolStripMenuItem.Text = "Enable" - MainWindow.DarkModeDisableToolStripMenuItem.Text = "Disabled" - MainWindow.DarkModeEnableToolStripMenuItem.Checked = False - MainWindow.DarkModeDisableToolStripMenuItem.Checked = True - End If - End Sub - - ''' - ''' Applies the specified color settings to the UI components. - ''' - ''' A dictionary mapping color names to Color objects. - Private Shared Sub SetUIColors(ByVal color As Dictionary(Of String, Color)) - MainWindow.TreeView1.BackColor = color("InputFieldBackgroundColor") - MainWindow.TreeView1.ForeColor = color("PrimaryTextColor") - MainWindow.TreeView1.LineColor = color("SecondaryTextColor") - - ''' - ''' Apply colors to tab pages - ''' - Dim tabpages As New List(Of TabPage) From { - UserProfileWindow.TabPage1, - UserProfileWindow.TabPage2 - } - For Each tabpage In tabpages - tabpage.BackColor = color("InputFieldBackgroundColor") - tabpage.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to Forms - ''' - Dim forms As New List(Of Form) From { - MainWindow, - AboutWindow, - CommentsWindow, - SubredditProfileWindow, - PostsWindow - } - For Each form In forms - form.BackColor = color("MainBackgroundColor") - form.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to buttons - ''' - Dim buttons As New List(Of Button) From { - MainWindow.ButtonFetchFrontPageData, - MainWindow.ButtonFetchListingPosts, - MainWindow.ButtonFetchSubredditData, - MainWindow.ButtonFetchUserData, - MainWindow.ButtonSearch, - AboutWindow.ButtonViewLicense, - AboutWindow.ButtonGetUpdates - } - ' MainWindow.ButtonFetchPostData, - For Each button In buttons - button.BackColor = color("InputFieldBackgroundColor") - button.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to Label controls - ''' - Dim labels As New List(Of Label) From { - MainWindow.LabelPostListingsLimit, - MainWindow.LabelPostListingsListing, - MainWindow.LabelFrontPageDataLimit, - MainWindow.LabelFrontPageDataListing, - MainWindow.LabelSearchResultsLimit, - MainWindow.LabelSearchResultsListing, - MainWindow.LabelUserPostsListing, - MainWindow.LabelUserDataLimit, - MainWindow.LabelSubredditPostsListing, - MainWindow.LabelSubredditPostsLimit, - AboutWindow.LabelProgramFirstName, - MainWindow.Label1, - MainWindow.Label2, - MainWindow.Label3, - MainWindow.Label4, - MainWindow.Label5 - } - AboutWindow.LabelProgramLastName.ForeColor = color("SecondaryTextColor") - For Each label In labels - label.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to RadioButton controls - ''' - Dim radioButtons As New List(Of RadioButton) From { - MainWindow.RadioButtonBest, - MainWindow.RadioButtonRising, - MainWindow.RadioButtonPopular, - MainWindow.RadioButtonControversial, - MainWindow.RadioButtonUserProfile, - MainWindow.RadioButtonUserPosts, - MainWindow.RadioButtonUserComments, - MainWindow.RadioButtonSubredditProfile, - MainWindow.RadioButtonSubredditPosts - } - ' MainWindow.RadioButtonPostProfile, - ' MainWindow.RadioButtonPostComments, - For Each radioButton In radioButtons - radioButton.BackColor = color("MainBackgroundColor") - radioButton.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to TextBox controls - ''' - Dim textBoxes As New List(Of TextBox) From { - MainWindow.TextBoxUsername, - MainWindow.TextBoxQuery, - MainWindow.TextBoxSubreddit - } - - For Each textBox In textBoxes - textBox.BackColor = color("InputFieldBackgroundColor") - textBox.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to NumericUpDown controls - ''' - Dim numericUpDowns As New List(Of NumericUpDown) From { - MainWindow.NumericUpDownPostListingsLimit, - MainWindow.NumericUpDownFrontPageDataLimit, - MainWindow.NumericUpDownSubredditPostsLimit, - MainWindow.NumericUpDownUserDataLimit, - MainWindow.NumericUpDownSearchResultLimit - } - For Each numericUpDown In numericUpDowns - numericUpDown.BackColor = color("InputFieldBackgroundColor") - numericUpDown.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to ComboBox controls - ''' - Dim comboBoxes As New List(Of ComboBox) From { - MainWindow.ComboBoxPostListingsListing, - MainWindow.ComboBoxFrontPageDataListing, - MainWindow.ComboBoxSubredditPostsListing, - MainWindow.ComboBoxUserDataListing, - MainWindow.ComboBoxSearchResultListing - } - For Each comboBox In comboBoxes - comboBox.BackColor = color("InputFieldBackgroundColor") - comboBox.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to GroupBox controls - ''' - Dim GroupBoxes As New List(Of GroupBox) From { - MainWindow.GroupBoxPostListings, - MainWindow.GroupBoxFrontPageDataFiltering, - MainWindow.GroupBoxSubredditDataFiltering, - MainWindow.GroupBoxUserDataFiltering, - MainWindow.GroupBoxSearchResultsFiltering, - MainWindow.GroupBoxPostListingsFiltering, - MainWindow.GroupBoxUserData, - MainWindow.GroupBoxSubredditData - } - ' MainWindow.GroupBoxPostDataFiltering, - ' MainWindow.GroupBoxPostData, - For Each groupBox In GroupBoxes - groupBox.BackColor = color("MainBackgroundColor") - groupBox.ForeColor = color("SecondaryTextColor") - Next - - ''' - ''' Apply colors to ToolStripMenuItem items - ''' - Dim menuItems As New List(Of ToolStripMenuItem) From { - MainWindow.SettingsToolStripMenuItem, - MainWindow.DarkModeToolStripMenuItem, - MainWindow.DarkModeEnableToolStripMenuItem, - MainWindow.DarkModeDisableToolStripMenuItem, - MainWindow.SaveDataToolStripMenuItem, - MainWindow.ToJSONToolStripMenuItem, - MainWindow.ToCSVToolStripMenuItem, - MainWindow.AboutToolStripMenuItem, - MainWindow.ExitToolStripMenuItem - } - For Each menuItem In menuItems - menuItem.BackColor = color("MainBackgroundColor") - menuItem.ForeColor = color("PrimaryTextColor") - Next - - ''' - ''' Apply colors to data grid view cells - ''' - Dim cellStyles As New List(Of DataGridViewCellStyle) From { - SubredditProfileWindow.DataGridViewProfile.AlternatingRowsDefaultCellStyle, - SubredditProfileWindow.DataGridViewProfile.DefaultCellStyle, - UserProfileWindow.DataGridViewUserProfile.AlternatingRowsDefaultCellStyle, - UserProfileWindow.DataGridViewUserProfile.DefaultCellStyle, - UserProfileWindow.DataGridViewUserSubreddit.AlternatingRowsDefaultCellStyle, - UserProfileWindow.DataGridViewUserSubreddit.DefaultCellStyle, - PostsWindow.DataGridViewPosts.AlternatingRowsDefaultCellStyle, - PostsWindow.DataGridViewPosts.RowHeadersDefaultCellStyle, - PostsWindow.DataGridViewPosts.ColumnHeadersDefaultCellStyle, - PostsWindow.DataGridViewPosts.DefaultCellStyle, - CommentsWindow.DataGridViewComments.AlternatingRowsDefaultCellStyle, - CommentsWindow.DataGridViewComments.RowHeadersDefaultCellStyle, - CommentsWindow.DataGridViewComments.ColumnHeadersDefaultCellStyle, - CommentsWindow.DataGridViewComments.DefaultCellStyle - } - For Each cellStyle In cellStyles - cellStyle.BackColor = color("InputFieldBackgroundColor") - cellStyle.ForeColor = color("PrimaryTextColor") - Next - - - ''' - ''' Apply mouse enter/mouse leave colors to ToolStripMenuItem and ContextMenuStrip controls - ''' - Dim toolStripItems As New List(Of ToolStripMenuItem) From { - MainWindow.SettingsToolStripMenuItem, - MainWindow.DarkModeToolStripMenuItem, - MainWindow.SaveDataToolStripMenuItem - } - ' Iterate over the individual menu items - For Each toolStripItem In toolStripItems - For Each item As ToolStripMenuItem In toolStripItem.DropDownItems - ' Add handlers for MouseEnter and MouseLeave events - AddHandler item.MouseEnter, Sub(sender As Object, e As EventArgs) - MainWindow.OnMenuItemMouseEnter(sender:=sender, e:=e, color:=color) - End Sub - AddHandler item.MouseLeave, Sub(sender As Object, e As EventArgs) - MainWindow.OnMenuItemMouseLeave(sender:=sender, e:=e, color:=color) - End Sub - Next - Next - - ' Iterate over the context menus and their items - For Each item As ToolStripMenuItem In MainWindow.ContextMenuStripRightClick.Items - ' Add handlers for MouseEnter and MouseLeave events - AddHandler item.MouseEnter, Sub(sender As Object, e As EventArgs) - MainWindow.OnMenuItemMouseEnter(sender:=sender, e:=e, color:=color) - End Sub - AddHandler item.MouseLeave, Sub(sender As Object, e As EventArgs) - MainWindow.OnMenuItemMouseLeave(sender:=sender, e:=e, color:=color) - End Sub - Next - End Sub - - - ''' - ''' Toggles specific settings on or off based on the provided parameters. - ''' - ''' A Boolean indicating if the setting option should be enabled or not. - ''' A String specifying the type of setting to toggle ('json', 'csv', or 'darkmode'). - Public Sub ToggleSettings(enabled As Boolean, saveTo As String) - ' Read the existing settings from the settings file - Dim json As String = File.ReadAllText(settingsFilePath) - Dim settings As SettingsManager = JsonConvert.DeserializeObject(Of SettingsManager)(json) - - ' Update the settings based on the specified saveTo parameter - If saveTo.ToLower(CultureInfo.InvariantCulture) = "json" Then - settings.SaveToJson = enabled - Me.SaveToJson = enabled ' Update the current instance property - ElseIf saveTo.ToLower(CultureInfo.InvariantCulture) = "csv" Then - settings.SaveToCsv = enabled - Me.SaveToCsv = enabled ' Update the current instance property - ElseIf saveTo.ToLower(CultureInfo.InvariantCulture) = "darkmode" Then - settings.DarkMode = enabled - Me.DarkMode = enabled ' Update the current instance property - End If - - ' Save the updated settings back to the settings file - SaveSettings(settings:=settings) - ' Apply the updated settings to the application - ApplySettings() - End Sub -End Class \ No newline at end of file diff --git a/Knew Karma/KnewKarma/Windows/AboutWindow.vb b/Knew Karma/KnewKarma/Windows/AboutWindow.vb index b9cc9d0..906c745 100644 --- a/Knew Karma/KnewKarma/Windows/AboutWindow.vb +++ b/Knew Karma/KnewKarma/Windows/AboutWindow.vb @@ -22,7 +22,8 @@ 'Version.Text = $"Version {My.Application.Info.Version}" Copyright.Text = My.Application.Info.Copyright - Await CoreUtils.AsyncCheckUpdates() + Dim ApiHandler As New ApiHandler() + Await ApiHandler.AsyncGetUpdates() End Sub ''' diff --git a/Knew Karma/KnewKarma/Windows/MainWindow.Designer.vb b/Knew Karma/KnewKarma/Windows/MainWindow.Designer.vb index dc7a345..aaa5463 100644 --- a/Knew Karma/KnewKarma/Windows/MainWindow.Designer.vb +++ b/Knew Karma/KnewKarma/Windows/MainWindow.Designer.vb @@ -23,7 +23,7 @@ Partial Class MainWindow Private Sub InitializeComponent() components = New ComponentModel.Container() - Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(MainWindow)) + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(MainWindow)) Dim TreeNode1 As TreeNode = New TreeNode("Subreddit") Dim TreeNode2 As TreeNode = New TreeNode("User") Dim TreeNode3 As TreeNode = New TreeNode("Front Page") @@ -495,7 +495,7 @@ Partial Class MainWindow NumericUpDownSearchResultLimit.Font = New Font("Segoe UI Variable Display Semib", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownSearchResultLimit.Location = New Point(130, 41) NumericUpDownSearchResultLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownSearchResultLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownSearchResultLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownSearchResultLimit.Name = "NumericUpDownSearchResultLimit" NumericUpDownSearchResultLimit.ReadOnly = True NumericUpDownSearchResultLimit.Size = New Size(79, 22) @@ -541,7 +541,7 @@ Partial Class MainWindow NumericUpDownFrontPageDataLimit.Font = New Font("Segoe UI Variable Text Semibold", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownFrontPageDataLimit.Location = New Point(130, 41) NumericUpDownFrontPageDataLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownFrontPageDataLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownFrontPageDataLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownFrontPageDataLimit.Name = "NumericUpDownFrontPageDataLimit" NumericUpDownFrontPageDataLimit.ReadOnly = True NumericUpDownFrontPageDataLimit.Size = New Size(79, 22) @@ -600,7 +600,7 @@ Partial Class MainWindow NumericUpDownSubredditPostsLimit.Font = New Font("Segoe UI Variable Display Semib", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownSubredditPostsLimit.Location = New Point(130, 41) NumericUpDownSubredditPostsLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownSubredditPostsLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownSubredditPostsLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownSubredditPostsLimit.Name = "NumericUpDownSubredditPostsLimit" NumericUpDownSubredditPostsLimit.ReadOnly = True NumericUpDownSubredditPostsLimit.Size = New Size(79, 22) @@ -675,7 +675,7 @@ Partial Class MainWindow NumericUpDownUserDataLimit.Font = New Font("Segoe UI Variable Display Semib", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownUserDataLimit.Location = New Point(130, 41) NumericUpDownUserDataLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownUserDataLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownUserDataLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownUserDataLimit.Name = "NumericUpDownUserDataLimit" NumericUpDownUserDataLimit.ReadOnly = True NumericUpDownUserDataLimit.Size = New Size(79, 22) @@ -927,7 +927,7 @@ Partial Class MainWindow NumericUpDownPostListingsLimit.Font = New Font("Segoe UI Variable Display Semib", 8.25F, FontStyle.Bold, GraphicsUnit.Point) NumericUpDownPostListingsLimit.Location = New Point(130, 41) NumericUpDownPostListingsLimit.Maximum = New Decimal(New Integer() {10000, 0, 0, 0}) - NumericUpDownPostListingsLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) + NumericUpDownPostListingsLimit.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) NumericUpDownPostListingsLimit.Name = "NumericUpDownPostListingsLimit" NumericUpDownPostListingsLimit.ReadOnly = True NumericUpDownPostListingsLimit.Size = New Size(79, 22) diff --git a/Knew Karma/KnewKarmaSetup/KnewKarmaSetup.vdproj b/Knew Karma/KnewKarmaSetup/KnewKarmaSetup.vdproj index c1eeb53..4d62f4c 100644 --- a/Knew Karma/KnewKarmaSetup/KnewKarmaSetup.vdproj +++ b/Knew Karma/KnewKarmaSetup/KnewKarmaSetup.vdproj @@ -229,15 +229,15 @@ { "Name" = "8:Microsoft Visual Studio" "ProductName" = "8:Knew Karma" - "ProductCode" = "8:{73004786-0578-4532-B92C-A9D8A8B5E8B2}" - "PackageCode" = "8:{9ECB32FA-4D0C-4D29-9723-17958851D59F}" + "ProductCode" = "8:{EC317D6E-D0D6-4C8B-9120-59D6F8AE1626}" + "PackageCode" = "8:{5A52E59B-8C23-4400-8D0F-851C28E3DADA}" "UpgradeCode" = "8:{9B03AD0F-0C14-4075-AB75-01CD38A594B4}" "AspNetVersion" = "8:2.0.50727.0" "RestartWWWService" = "11:FALSE" "RemovePreviousVersions" = "11:TRUE" "DetectNewerInstalledVersion" = "11:TRUE" "InstallAllUsers" = "11:FALSE" - "ProductVersion" = "8:3.2.0" + "ProductVersion" = "8:3.3.0" "Manufacturer" = "8:Richard Mwewa" "ARPHELPTELEPHONE" = "8:" "ARPHELPLINK" = "8:https://github.com/bellingcat/knewkarma/wiki" @@ -777,7 +777,7 @@ { "{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_6E15D9F422094BD9809550AF1BA1C161" { - "SourcePath" = "8:..\\KnewKarma\\obj\\Release\\net6.0-windows\\apphost.exe" + "SourcePath" = "8:..\\KnewKarma\\obj\\Debug\\net6.0-windows\\apphost.exe" "TargetName" = "8:" "Tag" = "8:" "Folder" = "8:_C0F76EDD899B4FFF80C2AC1B5526BC22" From b9f47bddab0e0d15581db9006a3ddd8638bf0173 Mon Sep 17 00:00:00 2001 From: Richard Mwewa <74001397+rly0nheart@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:43:00 +0000 Subject: [PATCH 3/4] Add files via upload --- Knew Karma/KnewKarma/Assets/CoreUtils.vb | 277 ++++++++++++++ Knew Karma/KnewKarma/Assets/Settings.vb | 466 +++++++++++++++++++++++ 2 files changed, 743 insertions(+) create mode 100644 Knew Karma/KnewKarma/Assets/CoreUtils.vb create mode 100644 Knew Karma/KnewKarma/Assets/Settings.vb diff --git a/Knew Karma/KnewKarma/Assets/CoreUtils.vb b/Knew Karma/KnewKarma/Assets/CoreUtils.vb new file mode 100644 index 0000000..ede95df --- /dev/null +++ b/Knew Karma/KnewKarma/Assets/CoreUtils.vb @@ -0,0 +1,277 @@ +Imports System.IO +Imports Microsoft.Win32 +Imports Newtonsoft.Json +Imports Newtonsoft.Json.Linq +Imports System.Globalization + +Public Class CoreUtils + ''' + ''' Handles the enabling and disabling of various controls based on the state of radio buttons on the main form. + ''' + ''' The main form containing the radio buttons and controls to be manipulated. + Public Shared Sub HandleRadioButtonChanges(form As MainWindow) + ' Check the state of user radio buttons and enable/disable relevant controls accordingly + ' Depending on which radio button is selected, different sets of controls will be enabled or disabled + ' to provide a more intuitive user experience and prevent invalid configurations. + + ' Handling User Radio Buttons + If form.RadioButtonUserProfile.Checked Then + ' If the User Profile radio button is checked, disable the user data limit and posts listing controls + form.NumericUpDownUserDataLimit.Enabled = False + form.ComboBoxUserDataListing.Enabled = False + ElseIf form.RadioButtonUserPosts.Checked Then + ' If the User Posts radio button is checked, enable the user data limit and posts listing controls + form.NumericUpDownUserDataLimit.Enabled = True + form.ComboBoxUserDataListing.Enabled = True + ElseIf form.RadioButtonUserComments.Checked Then + ' If the User Comments radio button is checked, enable the user data limit control and disable the posts listing control + form.NumericUpDownUserDataLimit.Enabled = True + form.ComboBoxUserDataListing.Enabled = True + End If + + ' Handling Subreddit Radio Buttons + ' Similar to the user radio buttons above, check the state of subreddit radio buttons + ' and enable/disable the corresponding controls to ensure a coherent set of options is available to the user. + + If form.RadioButtonSubredditProfile.Checked Then + ' If the Subreddit Profile radio button is checked, disable the subreddit data limit and posts listing controls + form.NumericUpDownSubredditPostsLimit.Enabled = False + form.ComboBoxSubredditPostsListing.Enabled = False + ElseIf form.RadioButtonSubredditPosts.Checked Then + ' If the Subreddit Posts radio button is checked, enable the subreddit data limit and posts listing controls + form.NumericUpDownSubredditPostsLimit.Enabled = True + form.ComboBoxSubredditPostsListing.Enabled = True + End If + End Sub + + ''' + ''' Determines if the Windows system theme is in dark mode. + ''' + ''' + ''' True if the dark mode is enabled, otherwise false. + ''' + Public Shared Function IsSystemDarkTheme() As Boolean + Dim registryKey As RegistryKey = Registry.CurrentUser.OpenSubKey( + "Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" + ) + If registryKey IsNot Nothing Then + Dim appsUseLightTheme As Object = registryKey.GetValue("AppsUseLightTheme") + Return appsUseLightTheme IsNot Nothing AndAlso CType(appsUseLightTheme, Integer) = 0 + Else + Return False + End If + End Function + + + ''' + ''' Checks whether the given JSON data (either JObject or JArray) is null or empty. + ''' + ''' The JToken (JObject/JArray) to be validated. + ''' True if the data is not null and contains an "id" key, otherwise False. + Public Shared Function IsValidData(data As JToken) As Boolean + If data IsNot Nothing AndAlso ( + (TypeOf data Is JArray AndAlso DirectCast(data, JArray).Any()) OrElse + (TypeOf data Is JObject AndAlso (data("id") IsNot Nothing)) + ) Then + Return True + Else + Return False + End If + End Function + + ''' + ''' Converts a Unix timestamp with possible decimal points to a formatted datetime.utc string. + ''' + ''' The Unix timestamp to be converted. + ''' A formatted datetime string in the format "dd MMMM yyyy, hh:mm:ss.fff tt". + Public Shared Function UnixTimestampToUtc(ByVal timestamp As Double) As String + Dim utcFromTimestamp As Date = New DateTime( + 1970, + 1, + 1, + 0, + 0, + 0, + DateTimeKind.Utc + ).AddSeconds(timestamp) + Dim datetimeString As String = utcFromTimestamp.ToString( + "dd MMMM yyyy, hh:mm:ss tt", + CultureInfo.InvariantCulture + ) + Return datetimeString + End Function + + ''' + ''' Shows the license notice in a messagebox. + ''' + Public Shared Sub License() + MessageBox.Show( + $"{My.Application.Info.Copyright} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the ""Software""), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.", + "MIT License", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ) + End Sub + + ''' + ''' Checks for the existence of the 'Knew Karma' directory within the user's AppData\Roaming folder. + ''' If the directory does not exist, it creates one. + ''' + Public Shared Sub PathFinder() + Dim directoryPath As String = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Knew Karma" + ) + + If Not Directory.Exists(directoryPath) Then + Directory.CreateDirectory(directoryPath) + End If + End Sub + + ''' + ''' Prompts the user to save the data in either JSON or CSV format based on the settings specified in FormMain. + ''' + ''' The data to save, represented as a JToken, which can accommodate both JArray and JObject. + Public Shared Sub PromptSaveData(data As JToken, title As String) + ' Save profile data to JSON if the JSON toolStripMenuItem is checked. + If MainWindow.settings.SaveToJson Then + SaveDataToJson(data:=data, title:=title) + End If + ' Save profile data to CSV if the CSV toolStripMenuItem is checked. + If MainWindow.settings.SaveToCsv Then + SaveDataToCSV(data:=data, title:=title) + End If + End Sub + + ''' + ''' Saves the provided data to a JSON file. + ''' + ''' The data to save, which can be a JObject or a JArray. + ''' The title to use in the SaveFileDialog and the success message. + Private Shared Sub SaveDataToJson(data As JToken, title As String) + ' Initialize a new SaveFileDialog with the specified filter and title + Dim saveFileDialog As New SaveFileDialog With { + .Filter = "JSON files (*.json)|*.json", + .title = $"Save data to JSON" + } + + ' Open the SaveFileDialog and if the result is OK, proceed to save the data + If saveFileDialog.ShowDialog() = DialogResult.OK Then + Dim fileName As String = saveFileDialog.FileName + + ' Check if the data is not null and has values before proceeding to serialize it + If data Is Nothing OrElse (TypeOf data Is JArray AndAlso Not CType(data, JArray).HasValues) OrElse (TypeOf data Is JObject AndAlso Not CType(data, JObject).HasValues) Then + MessageBox.Show( + "Empty or null data cannot be saved.", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ) + Exit Sub + End If + + ' Define serializer settings to format the JSON string nicely + Dim serializerSettings As New JsonSerializerSettings With { + .Formatting = Formatting.Indented + } + + ' Serialize the JToken (which could be a JArray or a JObject) to a JSON string with the defined settings + Dim json As String = JsonConvert.SerializeObject(data, serializerSettings) + + ' Write the JSON string to a file with the specified file name + File.WriteAllText(fileName, json) + + ' Show a message indicating that the data has been successfully saved + MessageBox.Show( + $"{title} data saved to {fileName}", + "Saved", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ) + End If + End Sub + + ''' + ''' Saves the provided data to a CSV file. + ''' + ''' The data to save, which can be a JObject or a JArray. + ''' The title to use in the SaveFileDialog and the success message. + Private Shared Sub SaveDataToCSV(data As JToken, title As String) + ' Initialize a new SaveFileDialog with the specified filter and title + Dim saveFileDialog As New SaveFileDialog With { + .Filter = "CSV files (*.csv)|*.csv", + .title = $"Save data to CSV" + } + + ' Open the SaveFileDialog and if the result is OK, proceed to save the data + If saveFileDialog.ShowDialog() = DialogResult.OK Then + Dim fileName As String = saveFileDialog.FileName + + ' Create a new StreamWriter to write data to the CSV file + Using csvWriter As New StreamWriter(fileName) + ' Variable to store the headers obtained from the data + Dim headers As String() = Nothing + + ' Check if the data is a JArray and has values, and if true, get the headers from the first JObject in the JArray + If TypeOf data Is JArray AndAlso data.HasValues Then + headers = CType(data(0), JObject).Properties().Select(Function(p) p.Name).ToArray() + ' Check if the data is a JObject and has values, and if true, get the headers from the JObject + ElseIf TypeOf data Is JObject AndAlso data.HasValues Then + headers = CType(data, JObject).Properties().Select(Function(p) p.Name).ToArray() + End If + + ' If headers are not obtained, show an error message and exit the subroutine + If headers Is Nothing OrElse Not headers.Any() Then + MessageBox.Show( + "Unsupported data type or empty data.", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ) + Exit Sub + End If + + ' Write the headers to the CSV file + csvWriter.WriteLine(String.Join(",", headers)) + + ' Create an IEnumerable of JObject to handle both JArray and JObject cases + ' If it's a JArray, cast it to IEnumerable of JObject + ' If it's a JObject, create a new array with this single JObject + Dim dataRows As IEnumerable(Of JObject) = If(TypeOf data Is JArray, CType(data, JArray).Cast(Of JObject)(), New JObject() {CType(data, JObject)}) + + ' Loop through each row in dataRows to write the data to the CSV file + For Each row In dataRows + ' For each header, get the corresponding value from the current row (if it exists, otherwise use "N/A") + ' and write this set of values to the CSV file + Dim values = headers.Select(Function(header) If(row(header)?.ToString(), "N/A")).ToArray() + csvWriter.WriteLine(String.Join(",", values)) + Next + End Using + + ' Show a message to indicate that the data has been successfully saved + MessageBox.Show( + $"{title} saved to {fileName}", + "Success", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ) + End If + End Sub +End Class diff --git a/Knew Karma/KnewKarma/Assets/Settings.vb b/Knew Karma/KnewKarma/Assets/Settings.vb new file mode 100644 index 0000000..1ad42a1 --- /dev/null +++ b/Knew Karma/KnewKarma/Assets/Settings.vb @@ -0,0 +1,466 @@ +Imports System.Globalization +Imports System.IO +Imports System.Security.Principal +Imports Newtonsoft.Json + +Public Class SettingsManager + ''' + ''' Represents the Dark Mode property. + ''' Indicates whether the dark mode is enabled or disabled. + ''' + Public Property DarkMode As Boolean + + ''' + ''' Represents the SaveToJson property. + ''' Indicates whether the application will save data to JSON files. + ''' + Public Property SaveToJson As Boolean + + ''' + ''' Represents the SaveToCsv property. + ''' Indicates whether the application will save data to CSV files. + ''' + Public Property SaveToCsv As Boolean + + ''' + ''' Contains the color settings for different visual themes (e.g., Dark, Light). + ''' The outer dictionary key represents the theme (Dark, Light), + ''' and the inner dictionary contains the color settings. + ''' + Public Property ColorSettings As Dictionary(Of String, Dictionary(Of String, String)) + + ''' + ''' Represents the path where the settings file is stored. + ''' + Private ReadOnly settingsFilePath As String = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Knew Karma", + "settings.json" + ) + + ''' + ''' Loads application settings from the 'settings.json' file. + ''' If the settings file doesn't exist, it creates a new file with default settings. + ''' + Public Sub LoadSettings() + If File.Exists(settingsFilePath) Then + Dim json As String = File.ReadAllText(settingsFilePath) + Dim settings = JsonConvert.DeserializeObject(Of SettingsManager)(json) + + DarkMode = settings.DarkMode + SaveToJson = settings.SaveToJson + SaveToCsv = settings.SaveToCsv + ColorSettings = settings.ColorSettings + + MainWindow.ToJSONToolStripMenuItem.Checked = settings.SaveToJson + MainWindow.ToCSVToolStripMenuItem.Checked = settings.SaveToCsv + Else + ' Settings file does not exist + Dim defaultSettings = New SettingsManager With { + .DarkMode = False, + .SaveToCsv = False, + .SaveToJson = False, + .ColorSettings = New Dictionary(Of String, Dictionary(Of String, String))() From { + {"Dark", New Dictionary(Of String, String)() From { + {"MainBackgroundColor", "#FF1A1A1A"}, + {"PrimaryTextColor", "#FFFFFFFF"}, + {"SecondaryTextColor", "#FFFF4500"}, + {"InputFieldBackgroundColor", "#FF2E2E2E"}, + {"MenuItemMouseEnterColor", "#FF000000"} + }}, + {"Light", New Dictionary(Of String, String)() From { + {"MainBackgroundColor", "#FFF0F0F0"}, + {"PrimaryTextColor", "#FF121212"}, + {"SecondaryTextColor", "#FF5F99CF"}, + {"InputFieldBackgroundColor", "#FFFFFAFA"}, + {"MenuItemMouseEnterColor", "#FFFFFFFF"} + }} + } + } + + ' Write settings to jsonOutput file + Dim jsonOutput = JsonConvert.SerializeObject(defaultSettings) + File.WriteAllText(settingsFilePath, jsonOutput) + + SaveToJson = False + SaveToCsv = False + ColorSettings = defaultSettings.ColorSettings + MainWindow.ToJSONToolStripMenuItem.Checked = False + MainWindow.ToCSVToolStripMenuItem.Checked = False + + ' Check the system settings to see if dark mode is enabled/disabled + If CoreUtils.IsSystemDarkTheme() Then + ' If dark mode is enabled in the system settings, update the program's dark mode settings: + ' set DarkMode to True, check the DarkModeEnable menu item, and uncheck the DarkModeDisable menu item. + DarkMode = True + MainWindow.DarkModeEnableToolStripMenuItem.Checked = True + MainWindow.DarkModeDisableToolStripMenuItem.Checked = False + Else + ' If dark mode is not enabled in the system settings, update the program's dark mode settings: + ' set DarkMode to False, uncheck the DarkModeEnable menu item, and check the DarkModeDisable menu item. + DarkMode = False + MainWindow.DarkModeEnableToolStripMenuItem.Checked = False + MainWindow.DarkModeDisableToolStripMenuItem.Checked = True + End If + End If + End Sub + + ''' + ''' Retrieves application settings as key-value pairs. + ''' + ''' A Dictionary containing the names and values of all settings. + Private Function GetSettings() As Dictionary(Of String, Object) + Dim settings As New Dictionary(Of String, Object)() + + If File.Exists(settingsFilePath) Then + Dim json As String = File.ReadAllText(settingsFilePath) + settings = JsonConvert.DeserializeObject(Of Dictionary(Of String, Object))(json) + End If + + Return settings + End Function + + ''' + ''' Saves the provided settings to a JSON file with a read-only attribute. + ''' + ''' An instance of the SettingsManager containing the configurations to be saved. + Private Sub SaveSettings(settings) + ' Prepare the file to be written by ensuring it's not read-only + Dim fileInfo As New FileInfo(settingsFilePath) + If fileInfo.Exists AndAlso fileInfo.IsReadOnly Then + fileInfo.IsReadOnly = False + End If + + ' Serialize and write the data + Dim jsonOutput = JsonConvert.SerializeObject(settings) + File.WriteAllText(settingsFilePath, jsonOutput) + + ' Reset the read-only attribute + fileInfo.Refresh() ' This ensures that we're working with the latest file info + fileInfo.IsReadOnly = True + End Sub + + ''' + ''' Applies the current settings to the application's interface. This includes + ''' toggling SaveToJson, SaveToCsv, and applying the visual theme based on the Dark Mode setting. + ''' + Public Sub ApplySettings() + ' Retrieve the current settings + Dim settings As Dictionary(Of String, Object) = GetSettings() + ' Apply the SaveToJson setting to the menu item checkbox + MainWindow.ToJSONToolStripMenuItem.Checked = Me.SaveToJson + + ' Apply the SaveToCsv setting to the menu item checkbox + MainWindow.ToCSVToolStripMenuItem.Checked = Me.SaveToCsv + + ' Use ColorSettings property directly + Dim colorSettings As Dictionary(Of String, Dictionary(Of String, String)) = Me.ColorSettings + + ' Apply the color scheme based on the Dark Mode setting + ApplyColorScheme(isDarkMode:=CBool(settings("DarkMode")), colorSettings:=colorSettings) + End Sub + + ''' + ''' Applies the color scheme based on the given Dark Mode setting. + ''' Colors are defined in a mapping for easier maintenance and flexibility. + ''' + ''' Indicates whether Dark Mode is enabled. + Public Shared Sub ApplyColorScheme( + ByVal isDarkMode As Boolean, + ByVal colorSettings As Dictionary(Of String, Dictionary(Of String, String)) + ) + Dim color As New Dictionary(Of String, Color) + + Dim mode As String = If(isDarkMode, "Dark", "Light") + Dim modeColors As Dictionary(Of String, String) = colorSettings(mode) + + For Each colorName As String In modeColors.Keys + color(colorName) = ColorTranslator.FromHtml(modeColors(colorName)) + Next + + SetUIColors(color:=color) + + If isDarkMode Then + MainWindow.DarkModeEnableToolStripMenuItem.Text = "Enabled" + MainWindow.DarkModeDisableToolStripMenuItem.Text = "Disable" + MainWindow.DarkModeEnableToolStripMenuItem.Checked = True + MainWindow.DarkModeDisableToolStripMenuItem.Checked = False + Else + MainWindow.DarkModeEnableToolStripMenuItem.Text = "Enable" + MainWindow.DarkModeDisableToolStripMenuItem.Text = "Disabled" + MainWindow.DarkModeEnableToolStripMenuItem.Checked = False + MainWindow.DarkModeDisableToolStripMenuItem.Checked = True + End If + End Sub + + ''' + ''' Applies the specified color settings to the UI components. + ''' + ''' A dictionary mapping color names to Color objects. + Private Shared Sub SetUIColors(ByVal color As Dictionary(Of String, Color)) + MainWindow.TreeView1.BackColor = color("InputFieldBackgroundColor") + MainWindow.TreeView1.ForeColor = color("PrimaryTextColor") + MainWindow.TreeView1.LineColor = color("SecondaryTextColor") + + ''' + ''' Apply colors to tab pages + ''' + Dim tabpages As New List(Of TabPage) From { + UserProfileWindow.TabPage1, + UserProfileWindow.TabPage2 + } + For Each tabpage In tabpages + tabpage.BackColor = color("InputFieldBackgroundColor") + tabpage.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to Forms + ''' + Dim forms As New List(Of Form) From { + MainWindow, + AboutWindow, + CommentsWindow, + SubredditProfileWindow, + PostsWindow + } + For Each form In forms + form.BackColor = color("MainBackgroundColor") + form.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to buttons + ''' + Dim buttons As New List(Of Button) From { + MainWindow.ButtonFetchFrontPageData, + MainWindow.ButtonFetchListingPosts, + MainWindow.ButtonFetchSubredditData, + MainWindow.ButtonFetchUserData, + MainWindow.ButtonSearch, + AboutWindow.ButtonViewLicense, + AboutWindow.ButtonGetUpdates + } + ' MainWindow.ButtonFetchPostData, + For Each button In buttons + button.BackColor = color("InputFieldBackgroundColor") + button.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to Label controls + ''' + Dim labels As New List(Of Label) From { + MainWindow.LabelPostListingsLimit, + MainWindow.LabelPostListingsListing, + MainWindow.LabelFrontPageDataLimit, + MainWindow.LabelFrontPageDataListing, + MainWindow.LabelSearchResultsLimit, + MainWindow.LabelSearchResultsListing, + MainWindow.LabelUserPostsListing, + MainWindow.LabelUserDataLimit, + MainWindow.LabelSubredditPostsListing, + MainWindow.LabelSubredditPostsLimit, + AboutWindow.LabelProgramFirstName, + MainWindow.Label1, + MainWindow.Label2, + MainWindow.Label3, + MainWindow.Label4, + MainWindow.Label5 + } + AboutWindow.LabelProgramLastName.ForeColor = color("SecondaryTextColor") + For Each label In labels + label.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to RadioButton controls + ''' + Dim radioButtons As New List(Of RadioButton) From { + MainWindow.RadioButtonBest, + MainWindow.RadioButtonRising, + MainWindow.RadioButtonPopular, + MainWindow.RadioButtonControversial, + MainWindow.RadioButtonUserProfile, + MainWindow.RadioButtonUserPosts, + MainWindow.RadioButtonUserComments, + MainWindow.RadioButtonSubredditProfile, + MainWindow.RadioButtonSubredditPosts + } + ' MainWindow.RadioButtonPostProfile, + ' MainWindow.RadioButtonPostComments, + For Each radioButton In radioButtons + radioButton.BackColor = color("MainBackgroundColor") + radioButton.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to TextBox controls + ''' + Dim textBoxes As New List(Of TextBox) From { + MainWindow.TextBoxUsername, + MainWindow.TextBoxQuery, + MainWindow.TextBoxSubreddit + } + + For Each textBox In textBoxes + textBox.BackColor = color("InputFieldBackgroundColor") + textBox.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to NumericUpDown controls + ''' + Dim numericUpDowns As New List(Of NumericUpDown) From { + MainWindow.NumericUpDownPostListingsLimit, + MainWindow.NumericUpDownFrontPageDataLimit, + MainWindow.NumericUpDownSubredditPostsLimit, + MainWindow.NumericUpDownUserDataLimit, + MainWindow.NumericUpDownSearchResultLimit + } + For Each numericUpDown In numericUpDowns + numericUpDown.BackColor = color("InputFieldBackgroundColor") + numericUpDown.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to ComboBox controls + ''' + Dim comboBoxes As New List(Of ComboBox) From { + MainWindow.ComboBoxPostListingsListing, + MainWindow.ComboBoxFrontPageDataListing, + MainWindow.ComboBoxSubredditPostsListing, + MainWindow.ComboBoxUserDataListing, + MainWindow.ComboBoxSearchResultListing + } + For Each comboBox In comboBoxes + comboBox.BackColor = color("InputFieldBackgroundColor") + comboBox.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to GroupBox controls + ''' + Dim GroupBoxes As New List(Of GroupBox) From { + MainWindow.GroupBoxPostListings, + MainWindow.GroupBoxFrontPageDataFiltering, + MainWindow.GroupBoxSubredditDataFiltering, + MainWindow.GroupBoxUserDataFiltering, + MainWindow.GroupBoxSearchResultsFiltering, + MainWindow.GroupBoxPostListingsFiltering, + MainWindow.GroupBoxUserData, + MainWindow.GroupBoxSubredditData + } + ' MainWindow.GroupBoxPostDataFiltering, + ' MainWindow.GroupBoxPostData, + For Each groupBox In GroupBoxes + groupBox.BackColor = color("MainBackgroundColor") + groupBox.ForeColor = color("SecondaryTextColor") + Next + + ''' + ''' Apply colors to ToolStripMenuItem items + ''' + Dim menuItems As New List(Of ToolStripMenuItem) From { + MainWindow.SettingsToolStripMenuItem, + MainWindow.DarkModeToolStripMenuItem, + MainWindow.DarkModeEnableToolStripMenuItem, + MainWindow.DarkModeDisableToolStripMenuItem, + MainWindow.SaveDataToolStripMenuItem, + MainWindow.ToJSONToolStripMenuItem, + MainWindow.ToCSVToolStripMenuItem, + MainWindow.AboutToolStripMenuItem, + MainWindow.ExitToolStripMenuItem + } + For Each menuItem In menuItems + menuItem.BackColor = color("MainBackgroundColor") + menuItem.ForeColor = color("PrimaryTextColor") + Next + + ''' + ''' Apply colors to data grid view cells + ''' + Dim cellStyles As New List(Of DataGridViewCellStyle) From { + SubredditProfileWindow.DataGridViewProfile.AlternatingRowsDefaultCellStyle, + SubredditProfileWindow.DataGridViewProfile.DefaultCellStyle, + UserProfileWindow.DataGridViewUserProfile.AlternatingRowsDefaultCellStyle, + UserProfileWindow.DataGridViewUserProfile.DefaultCellStyle, + UserProfileWindow.DataGridViewUserSubreddit.AlternatingRowsDefaultCellStyle, + UserProfileWindow.DataGridViewUserSubreddit.DefaultCellStyle, + PostsWindow.DataGridViewPosts.AlternatingRowsDefaultCellStyle, + PostsWindow.DataGridViewPosts.RowHeadersDefaultCellStyle, + PostsWindow.DataGridViewPosts.ColumnHeadersDefaultCellStyle, + PostsWindow.DataGridViewPosts.DefaultCellStyle, + CommentsWindow.DataGridViewComments.AlternatingRowsDefaultCellStyle, + CommentsWindow.DataGridViewComments.RowHeadersDefaultCellStyle, + CommentsWindow.DataGridViewComments.ColumnHeadersDefaultCellStyle, + CommentsWindow.DataGridViewComments.DefaultCellStyle + } + For Each cellStyle In cellStyles + cellStyle.BackColor = color("InputFieldBackgroundColor") + cellStyle.ForeColor = color("PrimaryTextColor") + Next + + + ''' + ''' Apply mouse enter/mouse leave colors to ToolStripMenuItem and ContextMenuStrip controls + ''' + Dim toolStripItems As New List(Of ToolStripMenuItem) From { + MainWindow.SettingsToolStripMenuItem, + MainWindow.DarkModeToolStripMenuItem, + MainWindow.SaveDataToolStripMenuItem + } + ' Iterate over the individual menu items + For Each toolStripItem In toolStripItems + For Each item As ToolStripMenuItem In toolStripItem.DropDownItems + ' Add handlers for MouseEnter and MouseLeave events + AddHandler item.MouseEnter, Sub(sender As Object, e As EventArgs) + MainWindow.OnMenuItemMouseEnter(sender:=sender, e:=e, color:=color) + End Sub + AddHandler item.MouseLeave, Sub(sender As Object, e As EventArgs) + MainWindow.OnMenuItemMouseLeave(sender:=sender, e:=e, color:=color) + End Sub + Next + Next + + ' Iterate over the context menus and their items + For Each item As ToolStripMenuItem In MainWindow.ContextMenuStripRightClick.Items + ' Add handlers for MouseEnter and MouseLeave events + AddHandler item.MouseEnter, Sub(sender As Object, e As EventArgs) + MainWindow.OnMenuItemMouseEnter(sender:=sender, e:=e, color:=color) + End Sub + AddHandler item.MouseLeave, Sub(sender As Object, e As EventArgs) + MainWindow.OnMenuItemMouseLeave(sender:=sender, e:=e, color:=color) + End Sub + Next + End Sub + + + ''' + ''' Toggles specific settings on or off based on the provided parameters. + ''' + ''' A Boolean indicating if the setting option should be enabled or not. + ''' A String specifying the type of setting to toggle ('json', 'csv', or 'darkmode'). + Public Sub ToggleSettings(enabled As Boolean, saveTo As String) + ' Read the existing settings from the settings file + Dim json As String = File.ReadAllText(settingsFilePath) + Dim settings As SettingsManager = JsonConvert.DeserializeObject(Of SettingsManager)(json) + + ' Update the settings based on the specified saveTo parameter + If saveTo.ToLower(CultureInfo.InvariantCulture) = "json" Then + settings.SaveToJson = enabled + Me.SaveToJson = enabled ' Update the current instance property + ElseIf saveTo.ToLower(CultureInfo.InvariantCulture) = "csv" Then + settings.SaveToCsv = enabled + Me.SaveToCsv = enabled ' Update the current instance property + ElseIf saveTo.ToLower(CultureInfo.InvariantCulture) = "darkmode" Then + settings.DarkMode = enabled + Me.DarkMode = enabled ' Update the current instance property + End If + + ' Save the updated settings back to the settings file + SaveSettings(settings:=settings) + ' Apply the updated settings to the application + ApplySettings() + End Sub +End Class \ No newline at end of file From 3bb00e6b193fc01a2eb82633bb3952d2e4a808b8 Mon Sep 17 00:00:00 2001 From: Richard Mwewa <74001397+rly0nheart@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:45:10 +0000 Subject: [PATCH 4/4] Update README.md --- Knew Karma/KnewKarma/README.md | 124 +++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 6 deletions(-) diff --git a/Knew Karma/KnewKarma/README.md b/Knew Karma/KnewKarma/README.md index e6cf90a..79959a3 100644 --- a/Knew Karma/KnewKarma/README.md +++ b/Knew Karma/KnewKarma/README.md @@ -1,27 +1,139 @@ -![carbon](https://github.com/bellingcat/knewkarma/assets/74001397/a7165335-89d4-4632-b3af-2f3bb03082bb) +![knewkarma](https://github.com/bellingcat/knewkarma/assets/74001397/45262d9d-6633-418d-9ace-7c3c88b5ca36) + A **Reddit** Data Analysis Toolkit. [![.Net](https://img.shields.io/badge/Visual%20Basic%20.NET-5C2D91?style=flat&logo=.net&logoColor=white)](https://github.com/search?q=repo%3Abellingcat%2Fknewkarma++language%3A%22Visual+Basic+.NET%22&type=code) [![Python](https://img.shields.io/badge/Python-3670A0?style=flat&logo=python&logoColor=ffdd54)](https://github.com/search?q=repo%3Abellingcat%2Fknewkarma++language%3APython&type=code) [![Docker](https://img.shields.io/badge/Dockefile-%230db7ed.svg?style=flat&logo=docker&logoColor=white)](https://github.com/search?q=repo%3Abellingcat%2Fknewkarma++language%3ADockerfile&type=code) [![PyPI - Version](https://img.shields.io/pypi/v/knewkarma?style=flat&logo=pypi&logoColor=ffdd54&label=PyPI&labelColor=3670A0&color=3670A0)](https://pypi.org/project/knewkarma) [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=flat&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/_rly0nheart) +*** # Feature Overview -* **Knew Karma can get the following Reddit data from individual targets**: +## Knew Karma CLI/GUI + +- [x] **Knew Karma can get the following Reddit data from individual targets**: * **User**: *Profile*, *Posts*, *Comments* * **Subreddit**: *Profile*, *Posts* - * **Post**: *Data*, *Comments* (available only in the CLI) -* **It can also get posts from various sources, such as**: +- [x] **It can also get posts from various sources, such as**: * **Searching**: Allows getting posts that match the user-provided query from all over Reddit * **Reddit Front-Page**: Allows getting posts from the Reddit Front-Page * **Listing**: Allows getting posts from a user-specified Reddit Listing -* **Bonus Features** - * **CLI/GUI** +- [x] **Bonus Features** + * **Fully Async (both in the CLI and GUI)** * **Dark Mode** (*GUI Automatic/Manual*) * **Write data to files** (*JSON/CSV*) +## Knew Karma Python Library + +
+ Code Examples + +### Get User Data + +```python +import asyncio +import aiohttp +from knewkarma import RedditUser + + +# Define an asynchronous function to fetch User +async def async_user(username: str, data_timeframe: str, data_limit: int, data_sort: str): + # Initialize a RedditUser object with the specified username, data timeframe, limit, and sorting criteria + user = RedditUser(username=username, data_timeframe=data_timeframe, data_limit=data_limit, data_sort=data_sort) + + # Establish an asynchronous HTTP session + async with aiohttp.ClientSession() as session: + # Fetch user's profile + profile = await user.profile(session=session) + + # Fetch user's posts + posts = await user.posts(session=session) + + # Fetch user's comments + comments = await user.comments(session=session) + + print(profile) + print(posts) + print(comments) + + +# Run the asynchronous function with a specified username, data limit, and sorting parameter +# timeframes: ["all", "hour", "day", "month", "year"] +# sorting: ["all", "controversial", "new", "top", "best", "hot", "rising"] +asyncio.run(async_user(username="automoderator", data_timeframe="year", data_limit=100, data_sort="all")) +``` + +### Get Subreddit Data + +````python +import asyncio +import aiohttp +from knewkarma import RedditSub + + +async def async_subreddit(subreddit_name: str, data_timeframe: str, data_limit: int, data_sort: str): + # Initialize a RedditSub object with the specified subreddit, data timeframe, limit, and sorting criteria + subreddit = RedditSub( + subreddit=subreddit_name, data_timeframe=data_timeframe, data_limit=data_limit, data_sort=data_sort + ) + + # Create an asynchronous HTTP session + async with aiohttp.ClientSession() as session: + # Fetch subreddit's profile + profile = await subreddit.profile(session=session) + + # Fetch subreddit's posts + posts = await subreddit.posts(session=session) + + print(profile) + print(posts) + + +# Run the asynchronous function with specified subreddit name, data limit, and sorting criteria +# timeframes: ["all", "hour", "day", "month", "year"] +# sorting: ["all", "controversial", "new", "top", "best", "hot", "rising"] +asyncio.run( + async_subreddit(subreddit_name="MachineLearning", data_timeframe="year", data_limit=100, data_sort="top") +) +```` + +### Get Posts + +```python +import asyncio +import aiohttp +from knewkarma import RedditPosts + + +async def async_posts(timeframe: str, limit: int, sort: str): + # Initialize RedditPosts with the specified timeframe, limit and sorting criteria + posts = RedditPosts(timeframe=timeframe, limit=limit, sort=sort) + + # Create an asynchronous HTTP session + async with aiohttp.ClientSession() as session: + # Fetch front page posts + front_page_posts = await posts.front_page(session=session) + # Fetch posts from a specified listing ('best') + listing_posts = await posts.listing(listings_name="best", session=session) + # Fetch posts that match the specified search query 'covid-19' + search_results = await posts.search(query="covid-19", session=session) + + print(front_page_posts) + print(listing_posts) + print(search_results) + + +# Run the asynchronous function with a specified limit and sorting parameter +# timeframes: ["all", "hour", "day", "month", "year"] +# sorting: ["all", "controversial", "new", "top", "best", "hot", "rising"] +asyncio.run(async_posts(timeframe="year", limit=100, sort="all")) +``` + +
+ # Documentation *[Refer to the Wiki](https://github.com/bellingcat/knewkarma/wiki) for Installation, Usage and Uninstallation instructions.* *** [![me](https://github.com/bellingcat/knewkarma/assets/74001397/efd19c7e-9840-4969-b33c-04087e73e4da)](https://about.me/rly0nheart) +