[
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: IridiumIO\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: IridiumIO\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\nCompactGUI.TestingGround/\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Mono auto generated files\nmono_crash.*\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Ww][Ii][Nn]32/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Oo]ut/\n[Ll]og/\n[Ll]ogs/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUnit\n*.VisualState.xml\nTestResult.xml\nnunit-*.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# ASP.NET Scaffolding\nScaffoldingReadMe.txt\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Coverlet is a free, cross platform Code Coverage Tool\ncoverage*.json\ncoverage*.xml\ncoverage*.info\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# NuGet Symbol Packages\n*.snupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n*.appxbundle\n*.appxupload\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- [Bb]ackup.rdl\n*- [Bb]ackup ([0-9]).rdl\n*- [Bb]ackup ([0-9][0-9]).rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\n# Backup folder for Package Reference Convert tool in Visual Studio 2017\nMigrationBackup/\n\n# Ionide (cross platform F# VS Code tools) working folder\n.ionide/\n\n# Fody - auto-generated XML schema\nFodyWeavers.xsd\n/CompactGUI/My Project/launchSettings.json\n\n# IntelliJ\n.idea/\n/CompactGUI.WatcherCS\n"
  },
  {
    "path": "CompactGUI/Application.xaml",
    "content": "﻿<Application x:Class=\"Application\"\n             xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:system=\"clr-namespace:System;assembly=mscorlib\"\n             xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n             Startup=\"OnStartup\">\n    <Application.Resources>\n        <ResourceDictionary>\n            <ResourceDictionary.MergedDictionaries>\n                <ResourceDictionary Source=\"Components/Converters/Converters.xaml\" />\n                <ResourceDictionary Source=\"pack://application:,,,/Wpf.Ui;component/Resources/Theme/Dark.xaml\" />\n                <ResourceDictionary Source=\"pack://application:,,,/Wpf.Ui;component/Resources/Wpf.Ui.xaml\" />\n                <ui:ControlsDictionary />\n                <!--  Other merged dictionaries here  -->\n\n                <ResourceDictionary>\n                    <Geometry x:Key=\"SteamLogo\">M110.5,87.3c0,0.2,0,0.4,0,0.6L82,129.3c-4.6-0.2-9.3,0.6-13.6,2.4c-1.9,0.8-3.8,1.8-5.5,2.9L0.3,108.8  c0,0-1.4,23.8,4.6,41.6l44.3,18.3c2.2,9.9,9,18.6,19.1,22.8c16.4,6.9,35.4-1,42.2-17.4c1.8-4.3,2.6-8.8,2.5-13.3l40.8-29.1  c0.3,0,0.7,0,1,0c24.4,0,44.3-19.9,44.3-44.3c0-24.4-19.8-44.3-44.3-44.3C130.4,43,110.5,62.9,110.5,87.3z M103.7,171.2  c-5.3,12.7-19.9,18.7-32.6,13.4c-5.9-2.4-10.3-6.9-12.8-12.2l14.4,6c9.4,3.9,20.1-0.5,24-9.9c3.9-9.4-0.5-20.1-9.9-24l-14.9-6.2  c5.7-2.2,12.3-2.3,18.4,0.3c6.2,2.6,10.9,7.4,13.5,13.5S106.2,165.1,103.7,171.2 M154.8,116.9c-16.3,0-29.5-13.3-29.5-29.5  c0-16.3,13.2-29.5,29.5-29.5c16.3,0,29.5,13.3,29.5,29.5C184.2,103.6,171,116.9,154.8,116.9 M132.7,87.3c0-12.3,9.9-22.2,22.1-22.2  c12.2,0,22.1,9.9,22.1,22.2c0,12.3-9.9,22.2-22.1,22.2C142.6,109.5,132.7,99.5,132.7,87.3z M233,116.5c0,64.3-52.2,116.5-116.5,116.5S0,180.8,0,116.5c0-30.4,11-60.2,30.7-78.8C53.5,16.1,82.5,0,116.5,0  C180.8,0,233,52.2,233,116.5z</Geometry>\n                </ResourceDictionary>\n\n            </ResourceDictionary.MergedDictionaries>\n\n            <Style x:Key=\"RoundedButton\" TargetType=\"{x:Type Button}\">\n                <Style.Setters>\n                    <Setter Property=\"Template\">\n                        <Setter.Value>\n                            <ControlTemplate TargetType=\"{x:Type Button}\">\n                                <Border x:Name=\"button\"\n                                        Background=\"{TemplateBinding Background}\"\n                                        BorderBrush=\"Transparent\" BorderThickness=\"0\" CornerRadius=\"20\">\n                                    <Border.Effect>\n                                        <DropShadowEffect BlurRadius=\"16\" Direction=\"-90\" Opacity=\"0.26\" ShadowDepth=\"6\" />\n                                    </Border.Effect>\n\n                                    <TextBlock Text=\"{TemplateBinding Button.Content}\"\n                                               Margin=\"0,-2,0,0\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                                               FontFamily=\"Segoe UI\" FontSize=\"15\" />\n\n                                    <VisualStateManager.VisualStateGroups>\n                                        <VisualStateGroup x:Name=\"Common\">\n                                            <VisualState x:Name=\"MouseOver\">\n                                                <Storyboard>\n                                                    <ColorAnimation Storyboard.TargetProperty=\"(Border.Background).(SolidColorBrush.Color)\" To=\"#FF597186\"\n                                                                    Duration=\"0:00:0.3\" />\n                                                </Storyboard>\n                                            </VisualState>\n                                            <VisualState x:Name=\"Normal\">\n                                                <Storyboard>\n                                                    <ColorAnimation Storyboard.TargetProperty=\"(Border.Background).(SolidColorBrush.Color)\"\n                                                                    To=\"{TemplateBinding Background}\"\n                                                                    Duration=\"0:00:0.3\" />\n                                                </Storyboard>\n                                            </VisualState>\n\n                                        </VisualStateGroup>\n                                    </VisualStateManager.VisualStateGroups>\n                                </Border>\n\n\n                                <ControlTemplate.Triggers>\n                                    <Trigger Property=\"IsPressed\" Value=\"True\">\n                                        <Setter TargetName=\"button\" Property=\"Background\" Value=\"#FF4B6175\" />\n                                    </Trigger>\n\n                                    <Trigger Property=\"IsEnabled\" Value=\"False\">\n                                        <Setter TargetName=\"button\" Property=\"Opacity\" Value=\"0.7\" />\n                                        <Setter Property=\"Foreground\" Value=\"White\" />\n                                    </Trigger>\n                                </ControlTemplate.Triggers>\n                            </ControlTemplate>\n                        </Setter.Value>\n                    </Setter>\n                </Style.Setters>\n            </Style>\n\n            <Style x:Key=\"RoundedCheckBox\" TargetType=\"CheckBox\">\n                <Setter Property=\"Cursor\" Value=\"Hand\" />\n                <Setter Property=\"Content\" Value=\"\" />\n                <Setter Property=\"Template\">\n                    <Setter.Value>\n                        <ControlTemplate TargetType=\"{x:Type CheckBox}\">\n                            <Grid>\n\n                                <Border x:Name=\"cBox\"\n                                        Width=\"36\" Height=\"36\"\n                                        VerticalAlignment=\"Center\"\n                                        Background=\"Transparent\" BorderBrush=\"#98A9B9\" BorderThickness=\"2\" CornerRadius=\"18\">\n                                    <Grid>\n                                        <Ellipse x:Name=\"outerEllipse\"\n                                                 Grid.Column=\"0\"\n                                                 Margin=\"3\"\n                                                 Fill=\"#FFFFFF\" />\n                                    </Grid>\n                                </Border>\n                                <ContentPresenter x:Name=\"content\"\n                                                  Margin=\"5,0,0,0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\" />\n                            </Grid>\n                            <ControlTemplate.Triggers>\n                                <Trigger Property=\"IsChecked\" Value=\"True\">\n                                    <Trigger.EnterActions>\n                                        <BeginStoryboard>\n                                            <Storyboard>\n                                                <ColorAnimation Storyboard.TargetName=\"outerEllipse\"\n                                                                Storyboard.TargetProperty=\"(Ellipse.Fill).(SolidColorBrush.Color)\" From=\"#FFFFFF\"\n                                                                To=\"#98A9B9\" Duration=\"0:0:0.2\" />\n                                            </Storyboard>\n                                        </BeginStoryboard>\n                                    </Trigger.EnterActions>\n                                    <Trigger.ExitActions>\n                                        <BeginStoryboard>\n                                            <Storyboard>\n                                                <ColorAnimation Storyboard.TargetName=\"outerEllipse\"\n                                                                Storyboard.TargetProperty=\"(Ellipse.Fill).(SolidColorBrush.Color)\" From=\"#98A9B9\"\n                                                                To=\"#FFFFFF\" Duration=\"0:0:0.2\" />\n                                            </Storyboard>\n                                        </BeginStoryboard>\n                                    </Trigger.ExitActions>\n                                </Trigger>\n\n                            </ControlTemplate.Triggers>\n                        </ControlTemplate>\n                    </Setter.Value>\n                </Setter>\n            </Style>\n\n            <Style x:Key=\"{x:Static SystemParameters.FocusVisualStyleKey}\">\n                <Setter Property=\"Control.Template\" Value=\"{x:Null}\" />\n            </Style>\n\n            <Style x:Key=\"CustomCardExpanderStyle\"\n                   BasedOn=\"{StaticResource DefaultUiCardExpanderStyle}\"\n                   TargetType=\"{x:Type ui:CardExpander}\">\n                <Setter Property=\"Template\">\n                    <Setter.Value>\n                        <ControlTemplate TargetType=\"{x:Type ui:CardExpander}\">\n                            <Grid>\n                                <Grid.RowDefinitions>\n                                    <RowDefinition Height=\"Auto\" />\n                                    <RowDefinition Height=\"Auto\" />\n                                </Grid.RowDefinitions>\n\n                                <!--  Top level controls always visible  -->\n                                <Border x:Name=\"ToggleButtonBorder\"\n                                        Grid.Row=\"0\"\n                                        Background=\"{TemplateBinding Background}\"\n                                        BorderBrush=\"{TemplateBinding BorderBrush}\"\n                                        BorderThickness=\"1\"\n                                        CornerRadius=\"{TemplateBinding CornerRadius}\">\n                                    <ToggleButton x:Name=\"ExpanderToggleButton\"\n                                                  Margin=\"0\"\n                                                  Padding=\"{TemplateBinding Padding}\"\n                                                  HorizontalAlignment=\"Stretch\"\n                                                  VerticalAlignment=\"{TemplateBinding VerticalContentAlignment}\"\n                                                  HorizontalContentAlignment=\"Stretch\" VerticalContentAlignment=\"Center\"\n                                                  FontSize=\"{TemplateBinding FontSize}\"\n                                                  Foreground=\"{TemplateBinding Foreground}\"\n                                                  IsChecked=\"{Binding IsExpanded, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}\"\n                                                  IsEnabled=\"{TemplateBinding IsEnabled}\"\n                                                  OverridesDefaultStyle=\"True\"\n                                                  Template=\"{StaticResource DefaultUiCardExpanderToggleButtonStyle}\">\n                                        <ToggleButton.Content>\n                                            <Grid>\n                                                <Grid.ColumnDefinitions>\n                                                    <ColumnDefinition Width=\"Auto\" />\n                                                    <ColumnDefinition Width=\"*\" />\n                                                </Grid.ColumnDefinitions>\n\n                                                <ContentControl x:Name=\"ControlIcon\"\n                                                                Content=\"{TemplateBinding Icon}\"\n                                                                Grid.Column=\"0\"\n                                                                Margin=\"{StaticResource CardExpanderIconMargin}\"\n                                                                VerticalAlignment=\"Center\"\n                                                                Focusable=\"False\"\n                                                                FontSize=\"{StaticResource CardExpanderIconSize}\"\n                                                                Foreground=\"{TemplateBinding Foreground}\"\n                                                                KeyboardNavigation.IsTabStop=\"False\" />\n\n                                                <ContentPresenter x:Name=\"HeaderContentPresenter\"\n                                                                  Content=\"{TemplateBinding Header}\"\n                                                                  Grid.Column=\"1\"\n                                                                  TextElement.Foreground=\"{TemplateBinding Foreground}\" />\n                                            </Grid>\n                                        </ToggleButton.Content>\n                                    </ToggleButton>\n                                </Border>\n\n                                <!--  Collapsed content to expand  -->\n                                <Grid Grid.Row=\"1\" ClipToBounds=\"True\">\n                                    <Border x:Name=\"ContentPresenterBorder\"\n                                            Background=\"Transparent\"\n                                            BorderBrush=\"{TemplateBinding BorderBrush}\"\n                                            BorderThickness=\"1,0,1,1\" CornerRadius=\"0,0,4,4\" Visibility=\"Collapsed\">\n                                        <ContentPresenter x:Name=\"ContentPresenter\"\n                                                          Content=\"{TemplateBinding Content}\"\n                                                          Margin=\"{TemplateBinding ContentPadding}\"\n                                                          HorizontalAlignment=\"{TemplateBinding HorizontalContentAlignment}\"\n                                                          VerticalAlignment=\"{TemplateBinding VerticalContentAlignment}\" />\n                                        <Border.Tag>\n                                            <system:Double>0.0</system:Double>\n                                        </Border.Tag>\n                                        <Border.RenderTransform>\n                                            <TranslateTransform>\n                                                <TranslateTransform.Y>\n                                                    <MultiBinding Converter=\"{StaticResource AnimationFactorToValueConverter}\" ConverterParameter=\"negative\">\n                                                        <Binding ElementName=\"ContentPresenterBorder\" Path=\"ActualHeight\" />\n                                                        <Binding ElementName=\"ContentPresenterBorder\" Path=\"Tag\" />\n                                                    </MultiBinding>\n                                                </TranslateTransform.Y>\n                                            </TranslateTransform>\n                                        </Border.RenderTransform>\n                                    </Border>\n                                </Grid>\n                            </Grid>\n                            <ControlTemplate.Triggers>\n                                <Trigger Property=\"IsExpanded\" Value=\"True\">\n                                    <!--  TODO: Update  -->\n                                    <Setter TargetName=\"ToggleButtonBorder\" Property=\"CornerRadius\" Value=\"4,4,0,0\" />\n                                    <Trigger.EnterActions>\n                                        <BeginStoryboard>\n                                            <Storyboard>\n                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName=\"ContentPresenterBorder\" Storyboard.TargetProperty=\"(Border.Visibility)\">\n                                                    <DiscreteObjectKeyFrame KeyTime=\"0\" Value=\"{x:Static Visibility.Visible}\" />\n                                                </ObjectAnimationUsingKeyFrames>\n                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetName=\"ContentPresenterBorder\" Storyboard.TargetProperty=\"Tag\">\n                                                    <DiscreteDoubleKeyFrame KeyTime=\"0\" Value=\"1.0\" />\n                                                    <SplineDoubleKeyFrame KeySpline=\"0.0, 0.0, 0.0, 1.0\" KeyTime=\"0:0:0.333\"\n                                                                          Value=\"0.0\" />\n                                                </DoubleAnimationUsingKeyFrames>\n                                            </Storyboard>\n                                        </BeginStoryboard>\n                                    </Trigger.EnterActions>\n                                    <Trigger.ExitActions>\n                                        <BeginStoryboard>\n                                            <Storyboard>\n                                                <ObjectAnimationUsingKeyFrames Storyboard.TargetName=\"ContentPresenterBorder\" Storyboard.TargetProperty=\"(Border.Visibility)\">\n                                                    <DiscreteObjectKeyFrame KeyTime=\"0\" Value=\"{x:Static Visibility.Visible}\" />\n                                                    <DiscreteObjectKeyFrame KeyTime=\"0:0:0.2\" Value=\"{x:Static Visibility.Collapsed}\" />\n                                                </ObjectAnimationUsingKeyFrames>\n                                                <DoubleAnimationUsingKeyFrames Storyboard.TargetName=\"ContentPresenterBorder\" Storyboard.TargetProperty=\"Tag\">\n                                                    <DiscreteDoubleKeyFrame KeyTime=\"0\" Value=\"0.0\" />\n                                                    <SplineDoubleKeyFrame KeySpline=\"1.0, 1.0, 0.0, 1.0\" KeyTime=\"0:0:0.167\"\n                                                                          Value=\"1.0\" />\n                                                </DoubleAnimationUsingKeyFrames>\n                                            </Storyboard>\n                                        </BeginStoryboard>\n                                    </Trigger.ExitActions>\n                                </Trigger>\n                                <Trigger Property=\"IsEnabled\" Value=\"False\">\n                                    <Setter Property=\"Background\" Value=\"{DynamicResource CardBackgroundDisabled}\" />\n                                    <Setter Property=\"BorderBrush\" Value=\"{DynamicResource CardBorderBrushDisabled}\" />\n                                    <Setter TargetName=\"ContentPresenter\" Property=\"TextElement.Foreground\" Value=\"{DynamicResource CardForegroundDisabled}\" />\n                                    <Setter TargetName=\"ExpanderToggleButton\" Property=\"Foreground\" Value=\"{DynamicResource CardForegroundDisabled}\" />\n                                </Trigger>\n                                <Trigger Property=\"Icon\" Value=\"{x:Null}\">\n                                    <Setter TargetName=\"ControlIcon\" Property=\"Margin\" Value=\"0\" />\n                                    <Setter TargetName=\"ControlIcon\" Property=\"Visibility\" Value=\"Collapsed\" />\n                                </Trigger>\n                            </ControlTemplate.Triggers>\n                        </ControlTemplate>\n                    </Setter.Value>\n                </Setter>\n            </Style>\n\n\n            <!--  Other app resources here  -->\n\n        </ResourceDictionary>\n\n    </Application.Resources>\n</Application>\n"
  },
  {
    "path": "CompactGUI/Application.xaml.vb",
    "content": "﻿Imports System.IO\nImports System.IO.Pipes\nImports System.Threading\nImports System.Windows.Threading\nImports Wpf.Ui\nImports Wpf.Ui.DependencyInjection\nImports Microsoft.Extensions.Hosting\nImports Microsoft.Extensions.Logging\nImports Microsoft.Extensions.DependencyInjection\nImports Microsoft.Extensions.Configuration\nImports System.Drawing\nImports CompactGUI.Core.Settings\nImports Coravel\nImports CompactGUI.Watcher\nImports Coravel.Scheduling.Schedule.Interfaces\nImports Coravel.Scheduling.Schedule\n\nPartial Public Class Application\n\n    Public Shared ReadOnly AppVersion As New SemVersion(4, 0, 0, \"beta\", 6)\n\n    Private Shared _host As IHost\n\n    Private Shared ReadOnly SettingsService As ISettingsService\n\n    Shared Sub New()\n        SettingsService = New SettingsService()\n        SettingsService.LoadSettings()\n\n        AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf OnDomainUnhandledException\n\n    End Sub\n\n\n    Private Shared Sub InitializeHost()\n\n        _host = Host.CreateDefaultBuilder() _\n        .ConfigureAppConfiguration(Sub(context, configBuilder)\n                                       ' Set base path using IConfigurationBuilder\n                                       configBuilder.SetBasePath(AppContext.BaseDirectory)\n                                   End Sub) _\n        .ConfigureServices(Sub(context, services)\n\n                               services.AddHostedService(Of ApplicationHostService)()\n\n                               'Settings handler\n                               services.AddSingleton(Of ISettingsService)(SettingsService)\n\n                               services.AddLogging(Sub(logging)\n                                                       logging.SetMinimumLevel(SettingsService.AppSettings.LogLevel)\n                                                       logging.AddConsole()\n                                                       logging.AddDebug()\n                                                       logging.AddFile(\n                                                        Path.Combine(SettingsService.DataFolder.FullName, \"log.log\"),\n                                                        SettingsService.AppSettings.LogLevel,\n                                                        retainedFileCountLimit:=2,\n                                                        fileSizeLimitBytes:=1000000,\n                                                        outputTemplate:=\"{Timestamp:o} {RequestId,13} [{Level:u3}] {Message}{NewLine}{Exception}\"\n                                                        )\n                                                   End Sub)\n\n                               ' Theme manipulation\n                               services.AddSingleton(Of IThemeService, ThemeService)()\n\n                               ' TaskBar manipulation\n                               services.AddSingleton(Of ITaskBarService, TaskBarService)()\n                               ' Service containing navigation, same as INavigationWindow... but without window\n                               services.AddNavigationViewPageProvider()\n                               services.AddSingleton(Of INavigationService, NavigationService)()\n                               services.AddSingleton(Of CustomSnackBarService)()\n                               services.AddSingleton(Of IWindowService, WindowService)()\n                               services.AddSingleton(Of IUpdaterService, UpdaterService)()\n                               services.AddSingleton(Of IWikiService, WikiService)()\n\n                               services.AddSingleton(Of INavigationWindow, MainWindow)()\n                               services.AddSingleton(Of MainWindow)()\n                               services.AddSingleton(Of MainWindowViewModel)()\n\n\n                               ' Views and ViewModels\n                               services.AddTransient(Of HomePage)()\n                               services.AddSingleton(Of HomeViewModel)()\n\n                               services.AddTransient(Of WatcherPage)()\n                               services.AddSingleton(Of WatcherViewModel)()\n\n                               services.AddTransient(Of SettingsPage)()\n                               services.AddSingleton(Of SettingsViewModel)()\n\n                               services.AddTransient(Of DatabasePage)()\n                               services.AddTransient(Of DatabaseViewModel)()\n\n                               'Other services\n                               services.AddSingleton(Of TrayNotifierService)(Function(sp)\n                                                                                 Return New TrayNotifierService(sp.GetRequiredService(Of MainWindow)(), Icon.ExtractAssociatedIcon(Environment.ProcessPath), \"CompactGUI\")\n                                                                             End Function)\n\n                               services.AddSingleton(Of CompressableFolderService)\n\n                               services.AddSingleton(Of IdleDetector)(Function()\n                                                                          Dim idleDetector = New IdleDetector(New IdleSettings)\n                                                                          idleDetector.Start()\n                                                                          Return idleDetector\n                                                                      End Function)\n                               services.AddSingleton(Of SchedulerService)()\n                               services.AddSingleton(Of Watcher.Watcher)()\n\n                               services.AddScheduler()\n\n                           End Sub) _\n        .Build()\n\n\n    End Sub\n\n\n    Public Shared Function GetService(Of T As Class)() As T\n        Return TryCast(_host?.Services.GetService(GetType(T)), T)\n    End Function\n\n\n    Public Shared ReadOnly mutex As New Mutex(False, \"Global\\CompactGUI\")\n    Private pipeServerCancellation As New CancellationTokenSource()\n    Private pipeServerTask As Task\n\n\n\n    Private Shadows Async Sub OnStartup(sender As Object, e As StartupEventArgs)\n\n        AddHandler Dispatcher.CurrentDispatcher.UnhandledException, AddressOf OnDispatcherUnhandledException\n        Dim acquiredMutex As Boolean = mutex.WaitOne(0, False)\n\n        If Not acquiredMutex Then\n            If Not SettingsService.AppSettings.AllowMultiInstance Then\n                HandleSecondInstance(e.Args)\n                Return\n            End If\n        Else\n            If Not SettingsService.AppSettings.AllowMultiInstance Then\n                pipeServerTask = ProcessNextInstanceMessage()\n            End If\n        End If\n\n        InitializeHost()\n\n        GetService(Of Watcher.Watcher)()\n\n        Await _host.StartAsync()\n        Await GetService(Of SettingsViewModel).InitializeEnvironment()\n\n\n        GetService(Of SchedulerService).RegenerateSchedule()\n\n\n        Dim UpdateTask = GetService(Of IUpdaterService).CheckForUpdate(SettingsService.AppSettings.EnablePreReleaseUpdates)\n        Dim WikiTask = GetService(Of IWikiService).GetUpdatedJSONAsync()\n        Await Task.WhenAll(UpdateTask, WikiTask)\n\n    End Sub\n\n\n    Private Sub HandleSecondInstance(args As String())\n        If args.Length > 0 AndAlso args(0) <> \"-tray\" Then\n            Using client = New NamedPipeClientStream(\".\", \"CompactGUI\", PipeDirection.Out)\n                client.Connect()\n                Using writer = New StreamWriter(client)\n                    writer.WriteLine(args(0))\n                End Using\n            End Using\n        Else\n            MessageBox.Show(\"An instance of CompactGUI is already running\")\n        End If\n        Current.Shutdown()\n    End Sub\n\n    Private Async Function ProcessNextInstanceMessage() As Task\n        While Not pipeServerCancellation.IsCancellationRequested\n            Using server = New NamedPipeServerStream(\"CompactGUI\", PipeDirection.In, -1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous)\n                Try\n                    Await server.WaitForConnectionAsync(pipeServerCancellation.Token)\n                    Using reader = New StreamReader(server)\n                        Dim message = Await reader.ReadLineAsync()\n                        Await MainWindow.Dispatcher.InvokeAsync(Async Function()\n                                                                    If message IsNot Nothing Then\n                                                                        MainWindow.Show()\n                                                                        MainWindow.WindowState = WindowState.Normal\n                                                                        MainWindow.Activate()\n                                                                        Await GetService(Of HomeViewModel).AddFoldersAsync({message})\n                                                                        'Await MainWindow.ViewModel.SelectFolderAsync(message)\n                                                                    End If\n                                                                End Function).Task\n                    End Using\n                Catch ex As OperationCanceledException\n                    Return\n                Finally\n                    If server.IsConnected Then server.Disconnect()\n                End Try\n            End Using\n        End While\n    End Function\n\n    Public Async Function ShutdownPipeServer() As Task\n        If pipeServerTask IsNot Nothing Then\n            pipeServerCancellation.Cancel()\n            Await pipeServerTask\n        End If\n    End Function\n\n\n    Private Shadows Async Sub OnExit(sender As Object, e As ExitEventArgs)\n        Await _host.StopAsync()\n        _host.Dispose()\n    End Sub\n\n    Private Sub OnDispatcherUnhandledException(sender As Object, e As DispatcherUnhandledExceptionEventArgs)\n        GetService(Of ILogger(Of Application))().LogCritical(e.Exception, \"Unhandled exception in application: {Message}\", e.Exception.Message)\n    End Sub\n\n    Private Shared Sub OnDomainUnhandledException(sender As Object, e As UnhandledExceptionEventArgs)\n        Dim ex = TryCast(e.ExceptionObject, Exception)\n        Dim logger = GetService(Of ILogger(Of Application))()\n        If logger IsNot Nothing AndAlso ex IsNot Nothing Then\n            logger.LogCritical(ex, \"Unhandled domain exception: {Message}\", ex.Message)\n        End If\n    End Sub\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n    'Public Shared ReadOnly mutex As New Mutex(False, \"Global\\CompactGUI\")\n    'Private pipeServerCancellation As New CancellationTokenSource()\n    'Private pipeServerTask As Task\n    'Private Shadows mainWindow As MainWindow\n\n    'Private Async Sub Application_Startup(sender As Object, e As StartupEventArgs)\n    '    SettingsHandler.InitialiseSettings()\n    '    Dim acquiredMutex As Boolean = mutex.WaitOne(0, False)\n\n    '    If Not acquiredMutex Then\n    '        If Not SettingsHandler.AppSettings.AllowMultiInstance Then\n    '            HandleSecondInstance(e.Args)\n    '            Return\n    '        End If\n    '    Else\n    '        If Not SettingsHandler.AppSettings.AllowMultiInstance Then\n    '            pipeServerTask = ProcessNextInstanceMessage()\n    '        End If\n    '    End If\n\n    '    mainWindow = New MainWindow()\n    '    Dim shouldMinimizeToTray As Boolean = (e.Args.Length = 1 AndAlso e.Args(0).ToString = \"-tray\") OrElse\n    '                                      (SettingsHandler.AppSettings.StartInSystemTray AndAlso e.Args.Length = 0)\n\n    '    If shouldMinimizeToTray Then\n    '        mainWindow.Show()\n    '        mainWindow.ViewModel.ClosingCommand.Execute(New ComponentModel.CancelEventArgs(True))\n    '    Else\n    '        If e.Args.Length = 1 Then\n    '            Await mainWindow.ViewModel.SelectFolderAsync(e.Args(0))\n    '        End If\n    '        mainWindow.Show()\n    '    End If\n\n    '    Await SettingsViewModel.InitializeEnvironment()\n    'End Sub\n\n    'Private Sub HandleSecondInstance(args As String())\n    '    If args.Length > 0 AndAlso args(0) <> \"-tray\" Then\n    '        Using client = New NamedPipeClientStream(\".\", \"CompactGUI\", PipeDirection.Out)\n    '            client.Connect()\n    '            Using writer = New StreamWriter(client)\n    '                writer.WriteLine(args(0))\n    '            End Using\n    '        End Using\n    '    Else\n    '        MessageBox.Show(\"An instance of CompactGUI is already running\")\n    '    End If\n    '    Application.Current.Shutdown()\n    'End Sub\n\n    'Private Async Function ProcessNextInstanceMessage() As Task\n    '    While Not pipeServerCancellation.IsCancellationRequested\n    '        Using server = New NamedPipeServerStream(\"CompactGUI\", PipeDirection.In, -1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous)\n    '            Try\n    '                Await server.WaitForConnectionAsync(pipeServerCancellation.Token)\n    '                Using reader = New StreamReader(server)\n    '                    Dim message = Await reader.ReadLineAsync()\n    '                    Await mainWindow.Dispatcher.InvokeAsync(Async Function()\n    '                                                                If message IsNot Nothing Then\n    '                                                                    mainWindow.Show()\n    '                                                                    mainWindow.WindowState = WindowState.Normal\n    '                                                                    mainWindow.Activate()\n    '                                                                    Await mainWindow.ViewModel.SelectFolderAsync(message)\n    '                                                                End If\n    '                                                            End Function).Task\n    '                End Using\n    '            Catch ex As OperationCanceledException\n    '                Return\n    '            Finally\n    '                If server.IsConnected Then server.Disconnect()\n    '            End Try\n    '        End Using\n    '    End While\n    'End Function\n\n    'Public Async Function ShutdownPipeServer() As Task\n    '    If pipeServerTask IsNot Nothing Then\n    '        pipeServerCancellation.Cancel()\n    '        Await pipeServerTask\n    '    End If\n    'End Function\n\nEnd Class"
  },
  {
    "path": "CompactGUI/AssemblyInfo.vb",
    "content": "Imports System.Windows\n\n'The ThemeInfo attribute describes where any theme specific and generic resource dictionaries can be found.\n'1st parameter: where theme specific resource dictionaries are located\n'(used if a resource is not found in the page,\n' or application resource dictionaries)\n\n'2nd parameter: where the generic resource dictionary is located\n'(used if a resource is not found in the page,\n'app, and any theme specific resource dictionaries)\n<Assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)>\n"
  },
  {
    "path": "CompactGUI/CompactGUI.vbproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    \n    <OutputType>WinExe</OutputType>\n    <TargetFramework>net9.0-windows</TargetFramework>\n    <RootNamespace>CompactGUI</RootNamespace>\n    <UseWPF>true</UseWPF>\n    <OptionStrict>Off</OptionStrict>\n    <ApplicationManifest>My Project\\app.manifest</ApplicationManifest>\n    <Version>4.0.0</Version>\n    <Authors>IridiumIO</Authors>\n    <Company>IridiumIO</Company>\n    <Description>GUI for the Windows compact.exe command-line tool.</Description>\n    <Copyright>Copyright © 2025</Copyright>\n    <PackageProjectUrl>https://github.com/IridiumIO/CompactGUI/</PackageProjectUrl>\n    <ApplicationIcon>icon.ico</ApplicationIcon>\n \n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|AnyCPU'\">\n    <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036</NoWarn>\n    <WarningsAsErrors></WarningsAsErrors>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|AnyCPU'\">\n    <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036</NoWarn>\n    <WarningsAsErrors></WarningsAsErrors>    \n    <DebugType>none</DebugType>\n  </PropertyGroup>\n\n\n  <ItemGroup>\n    <Import Include=\"System.Windows\" />\n    <Import Include=\"System.Windows.Controls\" />\n    <Import Include=\"System.Windows.Data\" />\n    <Import Include=\"System.Windows.Documents\" />\n    <Import Include=\"System.Windows.Input\" />\n    <Import Include=\"System.Windows.Media\" />\n    <Import Include=\"System.Windows.Media.Imaging\" />\n    <Import Include=\"System.Windows.Navigation\" />\n    <Import Include=\"System.Windows.Shapes\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"CommunityToolkit.Mvvm\" Version=\"8.4.0\" />\n    <PackageReference Include=\"Coravel\" Version=\"6.0.2\" />\n    <PackageReference Include=\"diskdetector-net\" Version=\"0.3.2\" />\n    <PackageReference Include=\"Gameloop.Vdf\" Version=\"0.6.2\" />\n    <PackageReference Include=\"IridiumIO.MVVM.VBSourceGenerators\" Version=\"0.6.1\" PrivateAssets=\"All\" />\n    <PackageReference Include=\"Microsoft.Extensions.Caching.Memory\" Version=\"9.0.7\" />\n    <PackageReference Include=\"Microsoft.Extensions.DependencyInjection\" Version=\"9.0.7\" />\n    <PackageReference Include=\"Microsoft.Extensions.Hosting\" Version=\"9.0.7\" />\n    <PackageReference Include=\"Microsoft.Xaml.Behaviors.Wpf\" Version=\"1.1.135\" />\n    <PackageReference Include=\"Serilog.Extensions.Logging.File\" Version=\"3.0.0\" />\n    <PackageReference Include=\"ValueConverters\" Version=\"3.1.22\" />\n    <PackageReference Include=\"WPF-UI\" Version=\"4.0.3\" />\n    <PackageReference Include=\"WPF-UI.DependencyInjection\" Version=\"4.0.3\" />\n    <PackageReference Include=\"WPF-UI.Tray\" Version=\"4.0.3\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\CompactGUI.Core\\CompactGUI.Core.csproj\" />\n    <ProjectReference Include=\"..\\CompactGUI.Logging\\CompactGUI.Logging.csproj\" />\n    <ProjectReference Include=\"..\\CompactGUI.Watcher\\CompactGUI.Watcher.vbproj\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <Reference Include=\"FunctionalConverters\">\n      <HintPath>..\\..\\FunctionalConverters\\FunctionalConverters\\bin\\Release\\net6.0-windows\\FunctionalConverters.dll</HintPath>\n    </Reference>\n  </ItemGroup>\n\n  <Target Name=\"RenamePublishedExe\" AfterTargets=\"Publish\" Condition=\"'$(IsMonolithic)' == 'true'\">\n    <Move SourceFiles=\"$(PublishDir)CompactGUI.exe\" DestinationFiles=\"$(PublishDir)CompactGUI.mono.exe\" />\n\n  </Target>\n  \n  <PropertyGroup>\n    <FinalPublishDir>$(ProjectDir)bin\\publish\\FinalOutput\\</FinalPublishDir>\n  </PropertyGroup>\n\n  <Target Name=\"MovePublishedFiles\" AfterTargets=\"Publish\">\n    <Message Text=\"Moving published files to FinalOutput...\" Importance=\"high\" />\n    \n    <ItemGroup>\n        <PublishedFiles Include=\"$(PublishDir)**\\*\" />\n    </ItemGroup>\n\n    <Copy SourceFiles=\"@(PublishedFiles)\" DestinationFolder=\"$(FinalPublishDir)\" SkipUnchangedFiles=\"true\" />\n  </Target>\n\n  \n</Project>\n"
  },
  {
    "path": "CompactGUI/Components/Behaviors/FocusOnMouseOverBehavior.vb",
    "content": "﻿Imports Microsoft.Xaml.Behaviors\n\nPublic Class FocusOnMouseOverBehavior : Inherits Behavior(Of ComboBox)\n\n    Protected Overrides Sub OnAttached()\n        MyBase.OnAttached()\n        AddHandler AssociatedObject.MouseEnter, AddressOf OnMouseEnter\n    End Sub\n    Protected Overrides Sub OnDetaching()\n        MyBase.OnDetaching()\n        RemoveHandler AssociatedObject.MouseEnter, AddressOf OnMouseEnter\n    End Sub\n    Private Sub OnMouseEnter(sender As Object, e As MouseEventArgs)\n        AssociatedObject.Focus()\n    End Sub\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Components/Converters/Converters.xaml",
    "content": "﻿<ResourceDictionary xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n                    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n                    xmlns:converters=\"clr-namespace:ValueConverters;assembly=ValueConverters\"\n                    xmlns:local=\"clr-namespace:CompactGUI\">\n\n    <converters:BoolToVisibilityConverter x:Key=\"BoolToVisConverter\" />\n    <!-- <converters:EnumToBoolConverter x:Key=\"XEnumToBoolConverter\" /> -->\n    <local:EnumToRadioButtonConverter x:Key=\"EnumToRadioButtonConverter\" />\n    <local:DecimalToPercentageConverter x:Key=\"DecimalToPercentageConverter\" />\n    <local:BytesToReadableConverter x:Key=\"BytesToReadableConverter\" />\n    <local:StrippedFolderPathConverter x:Key=\"StrippedFolderPathConverter\" />\n    <local:RelativeDateConverter x:Key=\"RelativeDateConverter\" />\n    <local:CompressionLevelAbbreviatedConverter x:Key=\"CompressionLevelAbbreviatedConverter\" />\n    <!-- <local:ConfidenceIntToColorConverter x:Key=\"ConfidenceIntToColorConverter\" /> -->\n    <!-- <local:ConfidenceIntToStringConverter x:Key=\"ConfidenceIntToStringConverter\" /> -->\n    <!-- <local:WikiCompressionLevelAbbreviatedConverter x:Key=\"WikiCompressionLevelAbbreviatedConverter\" /> -->\n    <!-- <local:RatioConverter x:Key=\"RatioConverter\" /> -->\n    <local:ProgressBarColorConverter x:Key=\"ProgressBarColorConverter\" />\n    <!-- <local:NonZeroToVisConverter x:Key=\"NonZeroToVisConverter\" /> -->\n    <!-- <local:WindowScalingConverter x:Key=\"WindowScalingConverter\" /> -->\n    <local:BooleanToInverseVisibilityConverter x:Key=\"BooleanToInverseVisibilityConverter\" />\n    <local:TokenisedFolderPathConverter x:Key=\"TokenisedFolderPathConverter\" />\n    <local:FolderStatusToColorConverter x:Key=\"FolderStatusToColorConverter\" />\n    <local:FolderStatusToStringConverter x:Key=\"FolderStatusToStringConverter\" />\n    <local:FolderWorkingStateToPauseSymbolConverter x:Key=\"FolderWorkingStateToPauseSymbolConverter\"/>\n    <local:IsSteamFolderAndFreshlyCompressedMultiConverter x:Key=\"IsSteamFolderAndFreshlyCompressedMultiConverter\" />\n    <local:IsSteamFolderConverter x:Key=\"IsSteamFolderConverter\"/>\n    <local:AnimationFactorToValueConverter x:Key=\"AnimationFactorToValueConverter\" />\n    <local:FolderActionStateWorkingToVisibilityConverter x:Key=\"FolderActionStateWorkingToVisibilityConverter\" />\n    <local:ZeroCountToVisibilityConverter x:Key=\"ZeroCountToVisibilityConverter\"/>\n    <local:NumberWithSpacesConverter x:Key=\"NumberWithSpacesConverter\"/>\n    <local:BackgroundModeToVisibilityConverter x:Key=\"BackgroundModeToVisibilityConverter\"/>\n    <local:EnumToIntConverter x:Key=\"EnumToIntConverter\" />\n\n    <converters:ValueConverterGroup x:Key=\"IsSteamFolderToVisibilityConverter\">\n        <local:IsSteamFolderConverter/>\n        <converters:BoolToVisibilityConverter/>\n    </converters:ValueConverterGroup>\n    <converters:IntegerToBoolConverter x:Key=\"IntegerToBoolConverter\" />\n\n</ResourceDictionary>\n"
  },
  {
    "path": "CompactGUI/Components/Converters/IValueConverters.vb",
    "content": "﻿Imports System.Globalization\n\nPublic Class DecimalToPercentageConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        'IF = invert and format, to show the \"percentage smaller\" text\n        If parameter = \"IF\" Then Return CInt(100 - (CType(value, Decimal) * 100)) & \"%\"\n        If parameter = \"I\" Then Return CInt(100 - (CType(value, Decimal) * 100))\n        Return CInt(CType(value, Decimal) * 100)\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class BytesToReadableConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim suf As String() = {\" B\", \" KB\", \" MB\", \" GB\", \" TB\", \" PB\", \" EB\"}\n\n        If value = 1010101010101010 Then Return \"?\"\n\n        If value = 0 Then Return \"0\" & suf(0)\n        Dim bytes As Long = Math.Abs(value)\n        Dim place As Integer = CInt(Math.Floor(Math.Log(bytes, 1024)))\n\n        'Dim roundingPrecision As Integer = 1\n        'If parameter IsNot Nothing AndAlso Integer.TryParse(parameter.ToString(), roundingPrecision) Then\n        '    roundingPrecision = Math.Max(0, roundingPrecision)\n        '    'We want to round to 1 decimal place if the value is in the GB range or higher\n        '    If Array.IndexOf(suf, suf(place)) > 2 AndAlso roundingPrecision = 0 Then\n        '        roundingPrecision = 1\n        '    End If\n        'End If\n\n        'Dim num As Double = Math.Round(bytes / Math.Pow(1024, place), roundingPrecision)\n\n        'Return (Math.Sign(value) * num).ToString() & suf(place)\n\n        Dim roundingSigDigits As Integer = 3 ' Default significant digits\n        If parameter IsNot Nothing AndAlso Integer.TryParse(parameter.ToString(), roundingSigDigits) Then\n            roundingSigDigits = Math.Max(1, roundingSigDigits)\n        End If\n\n        Dim num As Double = bytes / Math.Pow(1024, place)\n        Dim absNum As Double = Math.Abs(num)\n        Dim digitsBeforeDecimal As Integer = If(absNum < 1, 0, Math.Floor(Math.Log10(absNum)) + 1)\n        Dim decimalPlaces As Integer = Math.Max(0, roundingSigDigits - digitsBeforeDecimal)\n        Dim roundedNum As Double = Math.Round(num, decimalPlaces)\n\n        Return (Math.Sign(value) * roundedNum).ToString() & suf(place)\n\n\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class StrippedFolderPathConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        If value Is Nothing Then Return Nothing\n        Dim Str = CType(value, String)\n        Return Str.Substring(Str.LastIndexOf(\"\\\"c) + 1)\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class TokenisedFolderPathConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        If value Is Nothing Then Return Nothing\n        Dim Str = CType(value, String)\n        Dim formattedString = Str.Replace(\"\\\"c, \" 🢒 \")\n        Return formattedString\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class RelativeDateConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim dt = CType(value, DateTime)\n        Dim ts As TimeSpan = DateTime.Now - dt\n\n        If ts > TimeSpan.FromDays(19000) Then\n            Return String.Format(\"Unknown\")\n        End If\n        If ts > TimeSpan.FromDays(2) Then\n            Return String.Format(\"{0:0} days ago\", ts.TotalDays)\n        ElseIf ts > TimeSpan.FromHours(2) Then\n            Return String.Format(\"{0:0} hours ago\", ts.TotalHours)\n        ElseIf ts > TimeSpan.FromMinutes(2) Then\n            Return String.Format(\"{0:0} minutes ago\", ts.TotalMinutes)\n        Else\n            Return \"just now\"\n        End If\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class CompressionLevelAbbreviatedConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim clvl = CType(value, Core.WOFCompressionAlgorithm)\n        Select Case clvl\n            Case Core.WOFCompressionAlgorithm.NO_COMPRESSION : Return \"NIL\"\n            Case Core.WOFCompressionAlgorithm.LZNT1 : Return \"NT\"\n            Case Core.WOFCompressionAlgorithm.XPRESS4K : Return \"X4\"\n            Case Core.WOFCompressionAlgorithm.XPRESS8K : Return \"X8\"\n            Case Core.WOFCompressionAlgorithm.XPRESS16K : Return \"X16\"\n            Case Core.WOFCompressionAlgorithm.LZX : Return \"LZX\"\n            Case Else : Return \"NIL\"\n        End Select\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class ConfidenceIntToStringConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Select Case value\n            Case 0\n                Return \"▬\"\n            Case 1\n                Return \"▬▬\"\n            Case 2\n                Return \"▬▬▬\"\n            Case Else\n                Return \"▭▭▭\"\n        End Select\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class ConfidenceIntToColorConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Select Case value\n            Case 0\n                Return New SolidColorBrush(ColorConverter.ConvertFromString(\"#FF996B6B\"))\n            Case 1\n                Return New SolidColorBrush(ColorConverter.ConvertFromString(\"#F1CE92\"))\n            Case 2\n                Return New SolidColorBrush(ColorConverter.ConvertFromString(\"#92F1AB\"))\n            Case Else\n                Return New SolidColorBrush(ColorConverter.ConvertFromString(\"#BAC2CA\"))\n        End Select\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class WindowScalingConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim dimension = CInt(parameter)\n        Return CInt(value * dimension)\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class WikiCompressionLevelAbbreviatedConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim clvl = CType(value, Integer)\n        Select Case clvl\n            Case 0 : Return \"X4\"\n            Case 1 : Return \"X8\"\n            Case 2 : Return \"X16\"\n            Case 3 : Return \"LZX\"\n            Case Else : Return \"NIL\"\n        End Select\n\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\n\n\n\nPublic Class NonZeroToVisConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim clvl = CType(value, Integer)\n        If clvl = 0 Then Return Visibility.Collapsed\n        Return Visibility.Visible\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class ProgressBarColorConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim progress As Decimal = DirectCast(value, Decimal)\n\n        If progress > 0.6 Then\n            Return New SolidColorBrush(Color.FromRgb(239, 146, 146))\n        ElseIf progress > 0.2 Then\n            Return New SolidColorBrush(Color.FromRgb(239, 239, 146))\n        Else\n            Return New SolidColorBrush(Color.FromRgb(146, 241, 171))\n        End If\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class BooleanToInverseVisibilityConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim b = CType(value, Boolean)\n        If b Then Return Visibility.Collapsed\n        Return Visibility.Visible\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\n\nEnd Class\n\nPublic Class EnumToRadioButtonConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim enumValue = CType(value, [Enum])\n        Dim parameterValue = CType(parameter, [Enum])\n        Return enumValue.Equals(parameterValue)\n    End Function\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        If value Then\n            Return parameter\n        End If\n        Return Binding.DoNothing\n    End Function\nEnd Class\n\nPublic Class FolderStatusToColorConverter : Implements IValueConverter\n\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim status = CType(value, ActionState)\n        Select Case status\n            Case ActionState.Idle\n                Return New SolidColorBrush(ColorConverter.ConvertFromString(\"#92e7f1\"))\n            Case ActionState.Analysing, ActionState.Working, ActionState.Paused\n                Return New SolidColorBrush(ColorConverter.ConvertFromString(\"#F1CE92\"))\n            Case ActionState.Results\n                Return New SolidColorBrush(ColorConverter.ConvertFromString(\"#92F1AB\"))\n            Case Else\n                Return New SolidColorBrush(ColorConverter.ConvertFromString(\"#FFBAC2CA\"))\n        End Select\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\nPublic Class FolderStatusToStringConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim status = CType(value, ActionState)\n        Select Case status\n            Case ActionState.Idle\n                Return \"Awaiting Compression\"\n            Case ActionState.Analysing\n                Return \"Analysing\"\n            Case ActionState.Working, ActionState.Paused\n                Return \"Working\"\n            Case ActionState.Results\n                Return \"Compressed\"\n            Case Else\n                Return \"Unknown\"\n        End Select\n    End Function\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\n\nPublic Class FolderWorkingStateToPauseSymbolConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim status = CType(value, ActionState)\n        Select Case status\n            Case ActionState.Paused\n                Return \"Play12\"\n            Case Else\n                Return \"Pause12\"\n        End Select\n    End Function\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class IsSteamFolderConverter : Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim folder = CType(value, CompressableFolder)\n        If folder Is Nothing Then Return False\n        Return TypeOf (folder) Is SteamFolder\n    End Function\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class IsSteamFolderAndFreshlyCompressedMultiConverter : Implements IMultiValueConverter\n\n    Public Function Convert(values As Object(), targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IMultiValueConverter.Convert\n        ' Ensure both properties are provided\n        If values.Length < 2 OrElse\n            values(0) Is Nothing OrElse values(1) Is Nothing OrElse\n            values(0) Is DependencyProperty.UnsetValue OrElse values(1) Is DependencyProperty.UnsetValue Then\n            Return Visibility.Collapsed\n        End If\n\n        ' Example logic: Both properties must be True for the element to be visible\n        Dim isFreshlyCompressed As Boolean = CType(values(0), Boolean)\n        Dim isSteamFolder As Boolean = CType(values(1), Boolean)\n\n        If isFreshlyCompressed AndAlso isSteamFolder Then\n            Return Visibility.Visible\n        End If\n\n        Return Visibility.Collapsed\n    End Function\n\n    Public Function ConvertBack(value As Object, targetTypes As Type(), parameter As Object, culture As CultureInfo) As Object() Implements IMultiValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\nPublic Class AnimationFactorToValueConverter\n    Implements IMultiValueConverter\n\n    Public Function Convert(values() As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IMultiValueConverter.Convert\n        If TypeOf values(0) IsNot Double Then\n            Return 0.0\n        End If\n\n        Dim completeValue As Double = DirectCast(values(0), Double)\n\n        If TypeOf values(1) IsNot Double Then\n            Return 0.0\n        End If\n\n        Dim factor As Double = DirectCast(values(1), Double)\n\n        If parameter IsNot Nothing AndAlso parameter.ToString() = \"negative\" Then\n            factor = -factor\n        End If\n\n        Return factor * completeValue\n    End Function\n\n    Public Function ConvertBack(value As Object, targetTypes() As Type, parameter As Object, culture As CultureInfo) As Object() Implements IMultiValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class FolderActionStateWorkingToVisibilityConverter\n    Implements IValueConverter\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim status = CType(value, ActionState)\n        Select Case status\n            Case ActionState.Working\n                Return Visibility.Visible\n            Case Else\n                Return Visibility.Collapsed\n        End Select\n    End Function\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\n\nEnd Class\n\nPublic Class ZeroCountToVisibilityConverter\n    Implements IValueConverter\n\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim val = CType(value, Long)\n\n        If parameter IsNot Nothing AndAlso parameter.ToString() = \"invert\" Then\n            If val = 0 Then val = 1 Else val = 0\n        End If\n\n        If val = 0 Then\n            Return Visibility.Collapsed\n        Else\n            Return Visibility.Visible\n        End If\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException()\n    End Function\nEnd Class\n\n\nPublic Class NumberWithSpacesConverter\n    Implements IValueConverter\n\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        If value Is Nothing Then Return String.Empty\n\n        Dim number As Long\n        If Long.TryParse(value.ToString(), number) Then\n            Return number.ToString(\"#,0\", CultureInfo.InvariantCulture).Replace(\",\"c, \" \"c)\n        End If\n\n        Return value.ToString()\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        If value Is Nothing Then Return 0\n        Dim s = value.ToString().Replace(\" \", \"\")\n        Dim result As Long\n        If Long.TryParse(s, result) Then\n            Return result\n        End If\n        Return 0\n    End Function\nEnd Class\n\n\nPublic Class BackgroundModeToVisibilityConverter\n    Implements IValueConverter\n\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        Dim index As Integer = If(value, 0)\n        Return If(index > 1, Visibility.Visible, Visibility.Collapsed)\n\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        Throw New NotImplementedException\n    End Function\nEnd Class\n\nPublic Class EnumToIntConverter\n    Implements IValueConverter\n\n    Public Function Convert(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.Convert\n        If value Is Nothing OrElse Not value.GetType().IsEnum Then Return 0\n        Return CType(value, [Enum]).GetHashCode()\n    End Function\n\n    Public Function ConvertBack(value As Object, targetType As Type, parameter As Object, culture As CultureInfo) As Object Implements IValueConverter.ConvertBack\n        If targetType Is Nothing OrElse Not targetType.IsEnum OrElse value Is Nothing Then Return Binding.DoNothing\n        Return [Enum].ToObject(targetType, value)\n    End Function\nEnd Class"
  },
  {
    "path": "CompactGUI/Components/Converters/MyConverters.vb",
    "content": "﻿Imports FunctionalConverters\n\nPublic Class MyConverters : Inherits ExtensibleConverter\n\n    Public Sub New(ConverterName As String)\n        MyBase.New(ConverterName)\n    End Sub\n\n\n    Public Shared Function BytesToProgressMultiConverter() As MultiConverter(Of Long, Integer)\n\n        Dim convert = Function(folderBytes As Long()) As Integer\n                          Return 25\n                      End Function\n\n        Return CreateMultiConverter(convert)\n\n    End Function\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Components/Custom/ImageControl.vb",
    "content": "﻿Imports System.Windows.Media.Animation\n\nPublic Class ImageControl : Inherits Image\n\n    Public Shared ReadOnly SourceChangingEvent As RoutedEvent =\n        EventManager.RegisterRoutedEvent(\"SourceChanging\", RoutingStrategy.Direct, GetType(RoutedEventHandler), GetType(ImageControl))\n\n    Public Shared ReadOnly SourceChangedEvent As RoutedEvent =\n        EventManager.RegisterRoutedEvent(\"SourceChanged\", RoutingStrategy.Direct, GetType(RoutedEventHandler), GetType(ImageControl))\n\n\n    Public Shared ReadOnly NewSourceProperty As DependencyProperty =\n        DependencyProperty.Register(\"NewSource\", GetType(ImageSource), GetType(ImageControl),\n                                    New PropertyMetadata(Nothing, AddressOf OnNewSourceChanged))\n\n    Public Property NewSource As ImageSource\n        Get\n            Return CType(GetValue(NewSourceProperty), ImageSource)\n        End Get\n        Set(value As ImageSource)\n            SetValue(NewSourceProperty, value)\n        End Set\n    End Property\n\n\n    Private Shared Async Sub OnNewSourceChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)\n        Dim control = TryCast(d, ImageControl)\n        If control Is Nothing Then Return\n\n        control.RaiseEvent(New RoutedEventArgs(SourceChangingEvent))\n\n        ' Animate fade out\n        Dim fadeOut As New DoubleAnimation(0, TimeSpan.FromMilliseconds(300))\n        Await control.BeginAnimationAsync(OpacityProperty, fadeOut)\n\n        ' Now update the actual image source\n        control.Source = CType(e.NewValue, ImageSource)\n\n        control.RaiseEvent(New RoutedEventArgs(SourceChangedEvent))\n    End Sub\n\n    Shared Sub New()\n        SourceProperty.OverrideMetadata(GetType(ImageControl), New FrameworkPropertyMetadata(Nothing, AddressOf OnSourcePropertyChanged, AddressOf OnSourceCoerceValue))\n    End Sub\n\n    Public Custom Event SourceChanging As RoutedEventHandler\n        AddHandler(value As RoutedEventHandler)\n            [AddHandler](SourceChangingEvent, value)\n        End AddHandler\n        RemoveHandler(value As RoutedEventHandler)\n            [RemoveHandler](SourceChangingEvent, value)\n        End RemoveHandler\n        RaiseEvent(sender As Object, e As RoutedEventArgs)\n            [RaiseEvent](e)\n        End RaiseEvent\n    End Event\n\n    Public Custom Event SourceChanged As RoutedEventHandler\n        AddHandler(value As RoutedEventHandler)\n            [AddHandler](SourceChangedEvent, value)\n        End AddHandler\n        RemoveHandler(value As RoutedEventHandler)\n            [RemoveHandler](SourceChangedEvent, value)\n        End RemoveHandler\n        RaiseEvent(sender As Object, e As RoutedEventArgs)\n            [RaiseEvent](e)\n        End RaiseEvent\n    End Event\n\n    Private Shared Function OnSourceCoerceValue(d As DependencyObject, baseValue As Object) As Object\n        Dim image = TryCast(d, ImageControl)\n        If image IsNot Nothing Then\n            image.RaiseEvent(New RoutedEventArgs(SourceChangingEvent))\n        End If\n        Return baseValue\n    End Function\n\n    Private Shared Sub OnSourcePropertyChanged(obj As DependencyObject, e As DependencyPropertyChangedEventArgs)\n        Dim image = TryCast(obj, ImageControl)\n        If image IsNot Nothing Then\n            image.RaiseEvent(New RoutedEventArgs(SourceChangedEvent))\n        End If\n    End Sub\nEnd Class\n\nPublic Module AnimationHelper\n    <Runtime.CompilerServices.Extension()>\n    Public Async Function BeginAnimationAsync(target As UIElement, dp As DependencyProperty, animation As AnimationTimeline) As Task\n        Dim tcs As New TaskCompletionSource(Of Boolean)()\n\n        If animation Is Nothing Then\n            tcs.SetResult(True)\n            Return\n        End If\n\n        animation.FillBehavior = FillBehavior.Stop\n\n        AddHandler animation.Completed, Sub(s, e)\n                                            tcs.TrySetResult(True)\n                                        End Sub\n\n        target.BeginAnimation(dp, animation)\n        Await tcs.Task\n    End Function\nEnd Module"
  },
  {
    "path": "CompactGUI/Components/Custom/TokenizedTextBox.vb",
    "content": "﻿'Credit: https://blog.pixelingene.com/2010/10/tokenizing-control-convert-text-to-tokens\n\nPublic Class TokenizedTextBox : Inherits RichTextBox\n\n    Public Shared ReadOnly TokenTemplateProperty As DependencyProperty = DependencyProperty.Register(\"TokenTemplate\", GetType(DataTemplate), GetType(TokenizedTextBox))\n    Public Property TokenTemplate As DataTemplate\n        Get\n            Return GetValue(TokenTemplateProperty)\n        End Get\n        Set(value As DataTemplate)\n            SetValue(TokenTemplateProperty, value)\n        End Set\n    End Property\n\n    Public Property TokenMatcher As Func(Of String, Object)\n\n    Public Sub New()\n\n        AddHandler TextChanged, AddressOf OnTokenTextChanged\n\n    End Sub\n\n    Private Sub OnTokenTextChanged(sender As Object, e As TextChangedEventArgs)\n        Dim text = CaretPosition.GetTextInRun(LogicalDirection.Backward)\n        If TokenMatcher Is Nothing Then Return\n\n        Dim token = TokenMatcher(text)\n        If token IsNot Nothing Then\n            ReplaceTextWithToken(text, token)\n        End If\n\n    End Sub\n\n    Public Sub InsertText(text As String)\n        text &= \" \"\n        AppendText(text)\n        If TokenMatcher Is Nothing Then Return\n        Dim token = TokenMatcher(text)\n        If token IsNot Nothing Then\n            ReplaceTextWithToken(text, token)\n        End If\n    End Sub\n\n    Private Sub ReplaceTextWithToken(inputText As String, token As Object)\n\n        RemoveHandler TextChanged, AddressOf OnTokenTextChanged\n\n        Dim para = CaretPosition.Paragraph\n\n        Dim matchedRun As Run = para.Inlines.FirstOrDefault(Function(inline)\n                                                                Dim run = TryCast(inline, Run)\n                                                                Return (run IsNot Nothing AndAlso run.Text.EndsWith(inputText))\n                                                            End Function)\n\n        If matchedRun IsNot Nothing Then\n\n            Dim tokenContainer = CreateTokenContainer(inputText, token)\n            para.Inlines.InsertBefore(matchedRun, tokenContainer)\n\n            If matchedRun.Text = inputText Then\n                para.Inlines.Remove(matchedRun)\n            Else\n                Dim index = matchedRun.Text.IndexOf(inputText) + inputText.Length\n                Dim tailEnd = New Run(matchedRun.Text.Substring(index))\n                para.Inlines.InsertAfter(matchedRun, tailEnd)\n                para.Inlines.Remove(matchedRun)\n            End If\n\n        End If\n\n        AddHandler TextChanged, AddressOf OnTokenTextChanged\n\n    End Sub\n\n    Private Function CreateTokenContainer(inputText As String, token As Object) As InlineUIContainer\n        Dim presenter = New ContentPresenter() With {\n            .Content = token,\n            .ContentTemplate = TokenTemplate\n        }\n\n        Return New InlineUIContainer(presenter) With {.BaselineAlignment = BaselineAlignment.Bottom}\n\n    End Function\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Components/FolderActionStateTemplateSelector.vb",
    "content": "﻿Public Class FolderActionStateTemplateSelector\n    Inherits DataTemplateSelector\n\n    Public Property IdleTemplate As DataTemplate\n    Public Property AnalysingTemplate As DataTemplate\n    Public Property CompressingTemplate As DataTemplate\n    Public Property ResultsTemplate As DataTemplate\n\n    Public Overrides Function SelectTemplate(item As Object, container As DependencyObject) As DataTemplate\n        If item Is Nothing Then Return MyBase.SelectTemplate(item, container)\n        Dim action = DirectCast(item, ActionState)\n        ' Dim folderVM = TryCast(item, CompressableFolder)\n        Select Case action\n            Case ActionState.Idle\n                Return IdleTemplate\n            Case ActionState.Analysing\n                Return AnalysingTemplate\n            Case ActionState.Working, ActionState.Paused\n                Return CompressingTemplate\n            Case ActionState.Results\n                Return ResultsTemplate\n            Case Else\n                Return MyBase.SelectTemplate(item, container)\n        End Select\n    End Function\nEnd Class\n\n\nPublic Class HomeViewStateTemplateSelector\n    Inherits DataTemplateSelector\n\n    Public Property IdleTemplate As DataTemplate\n    Public Property AnalysingTemplate As DataTemplate\n    Public Property CompressingTemplate As DataTemplate\n    Public Property ResultsTemplate As DataTemplate\n\n    Public Overrides Function SelectTemplate(item As Object, container As DependencyObject) As DataTemplate\n        If item Is Nothing Then Return MyBase.SelectTemplate(item, container)\n        Dim action = DirectCast(item, ActionState)\n        Select Case action\n            Case ActionState.Idle\n                Return IdleTemplate\n            Case ActionState.Analysing\n                Return AnalysingTemplate\n            Case ActionState.Working, ActionState.Paused\n                Return CompressingTemplate\n            Case ActionState.Results\n                Return ResultsTemplate\n            Case Else\n                Return MyBase.SelectTemplate(item, container)\n        End Select\n    End Function\nEnd Class"
  },
  {
    "path": "CompactGUI/Components/Settings/Settings_skiplistflyout.xaml",
    "content": "﻿<ui:FluentWindow x:Class=\"Settings_skiplistflyout\"\n                 xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n                 xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n                 xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n                 xmlns:local=\"clr-namespace:CompactGUI\"\n                 xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n                 xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n                 Width=\"620\" Height=\"400\"\n                 d:DesignHeight=\"400\" d:DesignWidth=\"500\"\n                 Background=\"{DynamicResource ApplicationBackgroundBrush}\"\n                 ui:Design.Foreground=\"{DynamicResource TextFillColorPrimaryBrush}\"\n                 ExtendsContentIntoTitleBar=\"True\"\n                 Foreground=\"{DynamicResource TextFillColorPrimaryBrush}\"\n                 RenderOptions.BitmapScalingMode=\"HighQuality\" RenderOptions.ClearTypeHint=\"Enabled\"\n                 WindowBackdropType=\"Mica\" WindowStartupLocation=\"CenterScreen\"\n                 mc:Ignorable=\"d\">\n    <ui:FluentWindow.Resources>\n        <DataTemplate x:Key=\"NameTokenTemplate\">\n            <DataTemplate.Resources>\n                <Storyboard x:Key=\"OnLoaded1\">\n                    <DoubleAnimationUsingKeyFrames Storyboard.TargetName=\"border\" Storyboard.TargetProperty=\"(UIElement.Opacity)\">\n                        <SplineDoubleKeyFrame KeyTime=\"0\" Value=\"0\" />\n                        <SplineDoubleKeyFrame KeyTime=\"0:0:0.4\" Value=\"1\" />\n                    </DoubleAnimationUsingKeyFrames>\n                </Storyboard>\n            </DataTemplate.Resources>\n            <Border x:Name=\"border\"\n                    Height=\"28\" MinWidth=\"50\"\n                    Margin=\"5,8,5,0\" Padding=\"10,0\"\n                    Background=\"#7AB3C6D8\" BorderThickness=\"0\" CornerRadius=\"5\">\n\n                <TextBlock Text=\"{Binding}\"\n                           Grid.Column=\"0\"\n                           Margin=\"0,-1,0,0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n                           FontSize=\"13\" Foreground=\"Black\" TextWrapping=\"NoWrap\" />\n\n            </Border>\n            <DataTemplate.Triggers>\n                <EventTrigger RoutedEvent=\"FrameworkElement.Loaded\">\n                    <BeginStoryboard Storyboard=\"{StaticResource OnLoaded1}\" />\n                </EventTrigger>\n            </DataTemplate.Triggers>\n        </DataTemplate>\n    </ui:FluentWindow.Resources>\n    <Grid x:Name=\"MainGrid\">\n        <ui:TitleBar Panel.ZIndex=\"10\"\n                     ShowMaximize=\"False\" ShowMinimize=\"False\" />\n        <TextBlock Text=\"edit skipped filetypes\"\n                   Margin=\"10\"\n                   FontSize=\"22\" FontWeight=\"SemiBold\" />\n        <local:TokenizedTextBox x:Name=\"UiTokenizedText\"\n                                Margin=\"10,50,10,80\"\n                                AcceptsTab=\"False\"\n                                Background=\"{StaticResource CardBackground}\"\n                                CaretBrush=\"{StaticResource CardForeground}\"\n                                Foreground=\"{StaticResource CardForeground}\"\n                                TokenTemplate=\"{DynamicResource NameTokenTemplate}\"\n                                VerticalScrollBarVisibility=\"Visible\">\n            <FlowDocument>\n                <Paragraph>\n                    <Run />\n                </Paragraph>\n            </FlowDocument>\n        </local:TokenizedTextBox>\n\n        <Button x:Name=\"UiSave\"\n                Content=\"Save\"\n                Width=\"100\"\n                Margin=\"0,0,130,20\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Bottom\"\n                Click=\"UISave_Click\" />\n        <Button x:Name=\"UiReset\"\n                Content=\"Reset\"\n                Width=\"100\"\n                Margin=\"0,0,10,20\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Bottom\"\n                Click=\"UIReset_Click\" />\n\n    </Grid>\n</ui:FluentWindow>\n"
  },
  {
    "path": "CompactGUI/Components/Settings/Settings_skiplistflyout.xaml.vb",
    "content": "﻿Imports CompactGUI.Core.Settings\n\nPublic Class Settings_skiplistflyout\n\n    Private _settingsService As ISettingsService\n\n    Sub New()\n\n        InitializeComponent()\n        _settingsService = Application.GetService(Of ISettingsService)()\n        UiTokenizedText.TokenMatcher = Function(text) If(text.EndsWith(\" \"c) OrElse text.EndsWith(\";\"c) OrElse text.EndsWith(\",\"c), text.Substring(0, text.Length - 1).Trim(), Nothing)\n        PopulateTokens()\n    End Sub\n\n    Private Sub PopulateTokens()\n        UiTokenizedText.Document.Blocks.Clear()\n        For Each i In _settingsService.AppSettings.NonCompressableList\n            UiTokenizedText.InsertText(i)\n        Next\n    End Sub\n\n    Private Sub UIReset_Click(sender As Object, e As RoutedEventArgs)\n        _settingsService.AppSettings.NonCompressableList = New Settings().NonCompressableList\n        _settingsService.SaveSettings()\n        PopulateTokens()\n    End Sub\n\n    Private Sub UISave_Click(sender As Object, e As RoutedEventArgs)\n        Dim items = UiTokenizedText.Document.Blocks\n\n        Dim inlineI As Paragraph = items(0)\n        Dim allObj = inlineI.Inlines _\n            .Where(Function(c) c.GetType = GetType(InlineUIContainer)) _\n            .Select(Function(f As InlineUIContainer)\n                        Dim cl As ContentPresenter = f.Child\n                        Return cl.Content.ToString\n                    End Function).ToList\n\n        _settingsService.AppSettings.NonCompressableList = allObj.Where(Function(c) c.StartsWith(\".\"c) AndAlso c.Length > 1).Distinct().ToList\n        _settingsService.SaveSettings()\n\n        PopulateTokens()\n\n    End Sub\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Helper.vb",
    "content": "﻿Imports System.IO\nImports System.Net.NetworkInformation\nImports System.Runtime.InteropServices\nImports System.Text\n\nImports CommunityToolkit.Mvvm.Input\n\nImports CompactGUI.Core.SharedMethods\n\nImports Gameloop.Vdf\n\n\nPublic Module Helper\n    Function GetSteamNameAndIDFromFolder(path As String) As (appID As Integer, gameName As String, installDir As String)\n\n        Dim workingDir = New DirectoryInfo(path)\n        Dim parentfolder = workingDir.Parent.Parent\n\n        If Not parentfolder?.Name = \"steamapps\" Then Return Nothing\n\n        For Each fl In parentfolder.EnumerateFiles(\"*.acf\").Where(Function(f) f.Length > 0)\n            Try\n                Dim vConv = VdfConvert.Deserialize(IO.File.ReadAllText(fl.FullName))\n                If vConv.Value.Item(\"installdir\").ToString = workingDir.Name Then\n                    Dim appID = CInt(vConv.Value.Item(\"appid\").ToString)\n                    Dim sName = vConv.Value.Item(\"name\").ToString\n                    Dim sInstallDir = vConv.Value.Item(\"installdir\").ToString\n                    Return (appID, sName, sInstallDir)\n                    'TODO: Maybe add check to see when game was last updated?\n                End If\n            Catch\n                Debug.WriteLine($\"VDF file unsupported: {fl.FullName}\")\n            End Try\n        Next\n\n        Return Nothing\n    End Function\n\n\n    Function getUID() As String\n        Dim macAddress As String = String.Empty\n\n        For Each nic As NetworkInterface In NetworkInterface.GetAllNetworkInterfaces()\n            If nic.OperationalStatus = OperationalStatus.Up AndAlso\n           nic.NetworkInterfaceType <> NetworkInterfaceType.Loopback AndAlso\n           Not String.IsNullOrEmpty(nic.GetPhysicalAddress().ToString()) Then\n\n                Dim raw = nic.GetPhysicalAddress().ToString()\n                macAddress = String.Join(\":\", Enumerable.Range(0, raw.Length \\ 2).Select(Function(i) raw.Substring(i * 2, 2)))\n                Exit For\n            End If\n        Next\n\n        Return Convert.ToBase64String(Encoding.UTF8.GetBytes(macAddress))\n    End Function\n\n    Function LoadImageFromDisk(imagePath As String) As BitmapImage\n        Dim bImg As New BitmapImage(New Uri(imagePath))\n        Return bImg\n    End Function\n\n    Function LoadImageFromMemoryStream(imageData As Byte()) As BitmapImage\n        Dim bImg As New BitmapImage()\n        Using ms As New MemoryStream(imageData)\n            bImg.BeginInit()\n            bImg.CacheOption = BitmapCacheOption.OnLoad\n            bImg.StreamSource = ms\n            bImg.EndInit()\n        End Using\n        Return bImg\n    End Function\n\n\n    Public Function GetInvalidFolders(folderPaths() As String) As (InvalidFolders As List(Of String), InvalidMessages As List(Of FolderVerificationResult))\n\n        Dim invalidFolders As New List(Of String)\n        Dim invalidMessages As New List(Of FolderVerificationResult)\n\n        For Each folder In folderPaths\n            Dim validation = Core.SharedMethods.VerifyFolder(folder)\n\n            If validation <> FolderVerificationResult.Valid Then\n                invalidFolders.Add(folder)\n                invalidMessages.Add(validation)\n            End If\n        Next\n\n\n        Return (invalidFolders, invalidMessages)\n    End Function\n\n\n\n    Public Sub RunAsAdmin(FolderName As String)\n        Dim myproc As New Process With {\n            .StartInfo = New ProcessStartInfo With {\n                .FileName = Environment.ProcessPath,\n                .UseShellExecute = True,\n                .Arguments = $\"\"\"{FolderName}\"\"\",\n                .Verb = \"runas\"}\n        }\n        Dim app As Application = Application.Current\n\n        app.ShutdownPipeServer().ContinueWith(\n            Sub()\n                app.Dispatcher.Invoke(\n                    Sub()\n                        Application.mutex.ReleaseMutex()\n                        Application.mutex.Dispose()\n                    End Sub\n                )\n                myproc.Start()\n                app.Dispatcher.Invoke(Sub() app.Shutdown())\n            End Sub\n        )\n    End Sub\n\nEnd Module\n\n'https://stackoverflow.com/questions/4897655/create-a-shortcut-on-desktop\nModule ShortcutCreator\n\n    <ComImport>\n    <Guid(\"00021401-0000-0000-C000-000000000046\")>\n    Friend Class ShellLink\n    End Class\n\n    <ComImport>\n    <InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>\n    <Guid(\"000214F9-0000-0000-C000-000000000046\")>\n    Friend Interface IShellLink\n        Sub GetPath(<Out, MarshalAs(UnmanagedType.LPWStr)> pszFile As StringBuilder, cchMaxPath As Integer, ByRef pfd As IntPtr, fFlags As Integer)\n        Sub GetIDList(ByRef ppidl As IntPtr)\n        Sub SetIDList(pidl As IntPtr)\n        Sub GetDescription(<Out, MarshalAs(UnmanagedType.LPWStr)> pszName As StringBuilder, cchMaxName As Integer)\n        Sub SetDescription(<MarshalAs(UnmanagedType.LPWStr)> pszName As String)\n        Sub GetWorkingDirectory(<Out, MarshalAs(UnmanagedType.LPWStr)> pszDir As StringBuilder, cchMaxPath As Integer)\n        Sub SetWorkingDirectory(<MarshalAs(UnmanagedType.LPWStr)> pszDir As String)\n        Sub GetArguments(<Out, MarshalAs(UnmanagedType.LPWStr)> pszArgs As StringBuilder, cchMaxPath As Integer)\n        Sub SetArguments(<MarshalAs(UnmanagedType.LPWStr)> pszArgs As String)\n        Sub GetHotkey(ByRef pwHotkey As Short)\n        Sub SetHotkey(wHotkey As Short)\n        Sub GetShowCmd(ByRef piShowCmd As Integer)\n        Sub SetShowCmd(iShowCmd As Integer)\n        Sub GetIconLocation(<Out, MarshalAs(UnmanagedType.LPWStr)> pszIconPath As StringBuilder, cchIconPath As Integer, ByRef piIcon As Integer)\n        Sub SetIconLocation(<MarshalAs(UnmanagedType.LPWStr)> pszIconPath As String, iIcon As Integer)\n        Sub SetRelativePath(<MarshalAs(UnmanagedType.LPWStr)> pszPathRel As String, dwReserved As Integer)\n        Sub Resolve(hwnd As IntPtr, fFlags As Integer)\n        Sub SetPath(<MarshalAs(UnmanagedType.LPWStr)> pszFile As String)\n    End Interface\n\n    <ComImport>\n    <Guid(\"0000010B-0000-0000-C000-000000000046\")>\n    <InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>\n    Friend Interface IPersistFile\n        Sub GetClassID(ByRef pClassID As Guid)\n        Sub IsDirty()\n        Sub Load(<MarshalAs(UnmanagedType.LPWStr)> pszFileName As String, dwMode As UInteger)\n        Sub Save(<MarshalAs(UnmanagedType.LPWStr)> pszFileName As String, fRemember As Boolean)\n        Sub SaveCompleted(<MarshalAs(UnmanagedType.LPWStr)> pszFileName As String)\n        Sub GetCurFile(<MarshalAs(UnmanagedType.LPWStr)> ByRef ppszFileName As String)\n    End Interface\n\n    Public Sub CreateShortcut(shortcutPath As String, targetPath As String, Optional description As String = \"\", Optional workingDirectory As String = \"\", Optional iconPath As String = \"\")\n        Dim link As IShellLink = CType(New ShellLink(), IShellLink)\n\n        link.SetDescription(description)\n        link.SetPath(targetPath)\n        link.SetWorkingDirectory(If(String.IsNullOrWhiteSpace(workingDirectory), IO.Path.GetDirectoryName(targetPath), workingDirectory))\n\n        If Not String.IsNullOrWhiteSpace(iconPath) Then\n            link.SetIconLocation(iconPath, 0)\n        End If\n\n        Dim file As IPersistFile = CType(link, IPersistFile)\n        file.Save(shortcutPath, True)\n    End Sub\n\nEnd Module"
  },
  {
    "path": "CompactGUI/MainWindow.xaml",
    "content": "﻿<ui:FluentWindow x:Class=\"MainWindow\"\n                 xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n                 xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n                 xmlns:bh=\"http://schemas.microsoft.com/xaml/behaviors\"\n                 xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n                 xmlns:local=\"clr-namespace:CompactGUI\"\n                 xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n                 xmlns:tray=\"http://schemas.lepo.co/wpfui/2022/xaml/tray\"\n                 xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n                 x:Name=\"MainWindow\" Title=\"CompactGUI\"\n                 Width=\"1300\" Height=\"700\" MinWidth=\"768\" MinHeight=\"600\"\n                 d:DataContext=\"{d:DesignInstance Type=local:MainWindowViewModel}\"\n                 ui:Design.Background=\"{DynamicResource ApplicationBackgroundBrush}\"\n                 ui:Design.Foreground=\"{DynamicResource TextFillColorPrimaryBrush}\"\n                 Closing=\"MainWindow_Closing\" ExtendsContentIntoTitleBar=\"True\"\n                 Foreground=\"{DynamicResource TextFillColorPrimaryBrush}\"\n                 RenderOptions.BitmapScalingMode=\"HighQuality\" WindowBackdropType=\"Mica\"\n                 WindowCornerPreference=\"Round\" WindowStartupLocation=\"CenterScreen\"\n                 mc:Ignorable=\"d\">\n    <bh:Interaction.Triggers>\n\n        <bh:EventTrigger EventName=\"Closing\">\n            <bh:InvokeCommandAction Command=\"{Binding ClosingCommand}\" PassEventArgsToCommand=\"True\" />\n        </bh:EventTrigger>\n    </bh:Interaction.Triggers>\n    <Grid>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"auto\" />\n            <RowDefinition Height=\"*\" />\n        </Grid.RowDefinitions>\n\n        <ui:TitleBar Grid.Row=\"0\" Grid.ColumnSpan=\"2\"\n                     IsHitTestVisible=\"True\" />\n\n\n\n        <ui:NavigationView x:Name=\"NavigationView\"\n                           Grid.Row=\"1\"\n                           Margin=\"0,10,0,0\" \n                           IsBackButtonVisible=\"Collapsed\" PaneDisplayMode=\"Top\" Transition=\"FadeIn\">\n            <ui:NavigationView.MenuItems>\n\n\n                <ui:NavigationViewItem Content=\"Home\"\n                                       Margin=\"15,2,15,10\"\n                                       NavigationCacheMode=\"Required\"\n                                       Tag=\"{Binding}\"\n                                       TargetPageType=\"{x:Type local:HomePage}\">\n                    <ui:NavigationViewItem.Icon>\n                        <ui:SymbolIcon Symbol=\"Home32\" />\n                    </ui:NavigationViewItem.Icon>\n                </ui:NavigationViewItem>\n\n\n                <ui:NavigationViewItem Content=\"Watcher\"\n                                       Margin=\"15,2,15,10\"\n                                       NavigationCacheMode=\"Required\"\n                                       Tag=\"{Binding}\"\n                                       TargetPageType=\"{x:Type local:WatcherPage}\">\n                    <ui:NavigationViewItem.Icon>\n                        <ui:SymbolIcon Filled=\"False\" Symbol=\"Eye32\" />\n                    </ui:NavigationViewItem.Icon>\n                </ui:NavigationViewItem>\n\n                <ui:NavigationViewItem Content=\"Compression DB\"\n                                       Margin=\"15,2,15,10\"\n                                       NavigationCacheMode=\"Disabled\"\n                                       Tag=\"{Binding}\"\n                                       TargetPageType=\"{x:Type local:DatabasePage}\">\n                    <ui:NavigationViewItem.Icon>\n                        <ui:SymbolIcon Filled=\"False\" Symbol=\"Database16\" />\n                    </ui:NavigationViewItem.Icon>\n                </ui:NavigationViewItem>\n\n            </ui:NavigationView.MenuItems>\n\n            <ui:NavigationView.FooterMenuItems>\n\n                <ui:NavigationViewItem Margin=\"-5,0,15,1\"\n                                       NavigationCacheMode=\"Required\"\n                                       Tag=\"{Binding}\"\n                                       TargetPageType=\"{x:Type local:SettingsPage}\">\n                    <!--  TargetPageType=\"{x:Type local:SettingsPage}\"  -->\n\n                    <ui:SymbolIcon Margin=\"0,0,-3,0\"\n                                   FontSize=\"16\" Symbol=\"Settings48\" />\n\n                </ui:NavigationViewItem>\n            </ui:NavigationView.FooterMenuItems>\n\n\n            <ui:NavigationView.ContentOverlay>\n                <Grid>\n                    <ui:SnackbarPresenter x:Name=\"SnackbarPresenter\" />\n                </Grid>\n            </ui:NavigationView.ContentOverlay>\n\n\n        </ui:NavigationView>\n\n\n        <Grid x:Name=\"RootContentDialog\"\n              Grid.Row=\"0\" Grid.RowSpan=\"2\"\n              Margin=\"0\" Panel.ZIndex=\"-1\"\n              ClipToBounds=\"True\">\n\n            <Border Background=\"#ffffff\" />\n            <Border>\n                <Border.Background>\n                    <LinearGradientBrush Opacity=\"1\" StartPoint=\"0.5,0.5\" EndPoint=\"0.5,1\">\n                        <GradientStop Offset=\"0\" Color=\"#500D2A41\" />\n                        <GradientStop Offset=\"1\" Color=\"#FF0D2A41\" />\n                    </LinearGradientBrush>\n                </Border.Background>\n            </Border>\n\n            <Border Background=\"#1f3448\" Opacity=\"0.75\" />\n\n            <!--<ui:Image x:Name=\"DefaultBG\" Stretch=\"UniformToFill\" Opacity=\"0.66\" Margin=\"-20\" RenderOptions.BitmapScalingMode=\"HighQuality\">\n                <ui:Image.Effect>\n                    <BlurEffect KernelType=\"Box\" Radius=\"18\" RenderingBias=\"Quality\" />\n                </ui:Image.Effect>\n            </ui:Image>-->\n\n            <local:ImageControl x:Name=\"SteamBg\"\n                                Height=\"{Binding ActualHeight, ElementName=MainWindow}\"\n                                Margin=\"-75\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                                ClipToBounds=\"True\"\n                                NewSource=\"{Binding BackgroundImage}\"\n                                Opacity=\"1\" RenderOptions.BitmapScalingMode=\"HighQuality\" Stretch=\"UniformToFill\">\n                <local:ImageControl.OpacityMask>\n                    <LinearGradientBrush StartPoint=\"0,0.2\" EndPoint=\"0,1\">\n                        <GradientStop Offset=\"0.0\" Color=\"#00FFFFFF\" />\n                        <GradientStop Offset=\"1.0\" Color=\"#FFFFFFFF\" />\n                    </LinearGradientBrush>\n                </local:ImageControl.OpacityMask>\n                <local:ImageControl.RenderTransform>\n                    <TransformGroup>\n                        <ScaleTransform ScaleX=\"1.2\" ScaleY=\"1.2\" />\n                    </TransformGroup>\n\n                </local:ImageControl.RenderTransform>\n                <local:ImageControl.RenderTransformOrigin>\n                    <Point X=\"0.5\" Y=\"0.5\" />\n                </local:ImageControl.RenderTransformOrigin>\n                <local:ImageControl.Triggers>\n                    <EventTrigger RoutedEvent=\"local:ImageControl.SourceChanged\">\n                        <BeginStoryboard>\n                            <Storyboard>\n\n                                <DoubleAnimation Storyboard.TargetProperty=\"(Image.Opacity)\" From=\"0\" To=\"0.8\" Duration=\"0:0:0.5\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <QuadraticEase EasingMode=\"EaseInOut\" />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n                                <DoubleAnimation Storyboard.TargetProperty=\"(Effect).Radius\" From=\"100\" To=\"22\" Duration=\"0:0:2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <ExponentialEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n                                <DoubleAnimation Storyboard.TargetProperty=\"(RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)\"\n                                                 To=\"1.2\" Duration=\"0:0:6\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <ExponentialEase EasingMode=\"EaseIn\" />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n                                <DoubleAnimation Storyboard.TargetProperty=\"(RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)\"\n                                                 To=\"1.2\" Duration=\"0:0:8\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <QuinticEase EasingMode=\"EaseOut\" />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n\n                            </Storyboard>\n                        </BeginStoryboard>\n                    </EventTrigger>\n                    <!--<EventTrigger RoutedEvent=\"local:ImageControl.SourceChanging\">\n                        <BeginStoryboard>\n                            <Storyboard>\n                                <DoubleAnimation Storyboard.TargetProperty=\"(Effect).Radius\" To=\"100\" Duration=\"0:0:0\" BeginTime=\"0:0:0\"/>\n                                <DoubleAnimation Storyboard.TargetProperty=\"(Image.Opacity)\" To=\"0\" Duration=\"0:0:0\"/>\n\n                                <DoubleAnimation Storyboard.TargetProperty=\"(RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)\" To=\"2.0\"  Duration=\"0:0:0\"/>\n                                <DoubleAnimation Storyboard.TargetProperty=\"(RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)\" To=\"1.2\" Duration=\"0:0:0\"/>\n\n                            </Storyboard>\n                        </BeginStoryboard>\n                    </EventTrigger>-->\n                </local:ImageControl.Triggers>\n\n\n                <local:ImageControl.Effect>\n                    <BlurEffect Radius=\"0\" RenderingBias=\"Quality\" />\n                </local:ImageControl.Effect>\n\n\n            </local:ImageControl>\n\n            <Label x:Name=\"ProgTitle\"\n                   Margin=\"20,16\" HorizontalAlignment=\"Left\"\n                   d:Visibility=\"Visible\" FontSize=\"14\" Visibility=\"Collapsed\">\n                <Label.RenderTransform>\n                    <TranslateTransform x:Name=\"TransformL\" Y=\"20\" />\n                </Label.RenderTransform>\n                <Label.Style>\n                    <Style TargetType=\"Label\">\n                        <Setter Property=\"Opacity\" Value=\"0\" />\n                        <Style.Triggers>\n                            <DataTrigger Binding=\"{Binding Visibility, RelativeSource={RelativeSource Self}}\" Value=\"Visible\">\n                                <DataTrigger.EnterActions>\n                                    <BeginStoryboard>\n                                        <Storyboard>\n                                            <DoubleAnimation Storyboard.TargetProperty=\"Opacity\" From=\"0\" To=\"0\" />\n                                            <!--  Fade-in animation  -->\n                                            <DoubleAnimation BeginTime=\"0:0:0.5\"\n                                                             Storyboard.TargetProperty=\"Opacity\" From=\"0\" To=\"1\" Duration=\"0:0:1.5\" />\n\n                                            <!--  Move upwards animation  -->\n                                            <DoubleAnimation BeginTime=\"0:0:0.5\"\n                                                             Storyboard.TargetProperty=\"(RenderTransform).(TranslateTransform.Y)\" From=\"10\" To=\"0\"\n                                                             Duration=\"0:0:0.5\">\n                                                <DoubleAnimation.EasingFunction>\n                                                    <CircleEase />\n                                                </DoubleAnimation.EasingFunction>\n                                            </DoubleAnimation>\n                                        </Storyboard>\n                                    </BeginStoryboard>\n                                </DataTrigger.EnterActions>\n                            </DataTrigger>\n                        </Style.Triggers>\n                    </Style>\n                </Label.Style>\n                <StackPanel VerticalAlignment=\"Center\" Orientation=\"Horizontal\">\n                    <TextBlock Text=\"CompactGUI\"\n                               Margin=\"0,0,20,0\" VerticalAlignment=\"Center\"\n                               Foreground=\"#20FFFFFF\"\n                               Visibility=\"{Binding IsAdmin, Converter={StaticResource BooleanToInverseVisibilityConverter}}\" />\n                    <Border Width=\"80\" Height=\"22\"\n                            Background=\"#20f44336\" CornerRadius=\"10\"\n                            Visibility=\"{Binding IsAdmin, Converter={StaticResource BooleanToVisibilityConverter}}\">\n                        <TextBlock Text=\"Admin\"\n                                   HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                                   Foreground=\"White\" />\n\n                    </Border>\n                </StackPanel>\n            </Label>\n\n\n        </Grid>\n        <ui:SnackbarPresenter x:Name=\"RootSnackbar\"\n                              Grid.Row=\"1\" Grid.Column=\"0\"\n                              MaxWidth=\"600\" />\n        <tray:NotifyIcon x:Name=\"NotifyIconTray\"\n                         Grid.Row=\"0\"\n                         FocusOnLeftClick=\"True\" MenuOnRightClick=\"True\" TooltipText=\"CompactGUI\">\n            <tray:NotifyIcon.Menu>\n                <ContextMenu x:Name=\"NotifyIconTrayMenu\">\n                    <MenuItem Command=\"{Binding NotifyIconOpenCommand}\" Header=\"Open\" />\n                    <MenuItem Command=\"{Binding NotifyIconExitCommand}\" Header=\"Exit\" />\n                </ContextMenu>\n            </tray:NotifyIcon.Menu>\n        </tray:NotifyIcon>\n\n    </Grid>\n</ui:FluentWindow>\n"
  },
  {
    "path": "CompactGUI/MainWindow.xaml.vb",
    "content": "﻿Imports System.ComponentModel\n\nImports CompactGUI.Core.Settings\n\nImports Wpf.Ui\nImports Wpf.Ui.Abstractions\nImports Wpf.Ui.Controls\n\nClass MainWindow : Implements INavigationWindow, INotifyPropertyChanged\n\n    Private ReadOnly _NavigationService As INavigationService\n    Private _MainWindowViewModel As MainWindowViewModel\n    Private _SettingsService As ISettingsService\n    Public Sub New(settingsService As ISettingsService, navigationService As INavigationService, serviceProvider As IServiceProvider, snackbarService As CustomSnackBarService, viewmodel As MainWindowViewModel)\n\n        ' This call is required by the designer.\n        InitializeComponent()\n        ' Add any initialization after the InitializeComponent() call.\n\n        snackbarService.SetSnackbarPresenter(RootSnackbar)\n        navigationService.SetNavigationControl(NavigationView)\n        NavigationView.SetServiceProvider(serviceProvider)\n\n        _NavigationService = navigationService\n        _MainWindowViewModel = viewmodel\n        _SettingsService = settingsService\n        DataContext = viewmodel\n\n        NotifyIconTrayMenu.DataContext = viewmodel\n\n        AddHandler Application.GetService(Of HomeViewModel)().PropertyChanged, AddressOf HVPropertyChanged\n        AddHandler navigationService.GetNavigationControl.Navigated, AddressOf OnNavigated\n\n        If _SettingsService.AppSettings.WindowWidth > 0 Then\n            Width = _SettingsService.AppSettings.WindowWidth\n            Height = _SettingsService.AppSettings.WindowHeight\n            Left = _SettingsService.AppSettings.WindowLeft\n            Top = _SettingsService.AppSettings.WindowTop\n            If _SettingsService.AppSettings.WindowState = Core.Settings.WindowState.Maximized Then\n                WindowState = Core.Settings.WindowState.Maximized\n            Else\n                WindowState = Core.Settings.WindowState.Normal\n            End If\n        End If\n\n\n    End Sub\n\n\n    Private _isOnHomePage As Boolean\n\n    Private Sub OnNavigated(sender As NavigationView, args As NavigatedEventArgs)\n        If args.Page.GetType Is GetType(HomePage) Then\n            _isOnHomePage = True\n            _MainWindowViewModel.IsActive = True\n            HVPropertyChanged(Application.GetService(Of HomeViewModel)(), New PropertyChangedEventArgs(NameOf(HomeViewModel.HomeViewIsFresh)))\n        Else\n            _isOnHomePage = False\n            _MainWindowViewModel.IsActive = False\n            ProgTitle.Visibility = Visibility.Visible\n        End If\n    End Sub\n\n    Private Sub HVPropertyChanged(sender As Object, e As PropertyChangedEventArgs)\n\n        Dim homeVM As HomeViewModel = CType(sender, HomeViewModel)\n\n\n        If _isOnHomePage AndAlso e.PropertyName = NameOf(homeVM.HomeViewIsFresh) Then\n            If homeVM.HomeViewIsFresh Then\n                ProgTitle.Visibility = Visibility.Collapsed\n            Else\n                ProgTitle.Visibility = Visibility.Visible\n            End If\n        End If\n    End Sub\n\n    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged\n\n    Public Sub SetServiceProvider(serviceProvider As IServiceProvider) Implements INavigationWindow.SetServiceProvider\n        Throw New NotImplementedException()\n    End Sub\n\n    Public Sub SetPageService(navigationViewPageProvider As INavigationViewPageProvider) Implements INavigationWindow.SetPageService\n        Throw New NotImplementedException()\n    End Sub\n\n    Public Sub ShowWindow() Implements INavigationWindow.ShowWindow\n        Throw New NotImplementedException()\n    End Sub\n\n    Public Sub CloseWindow() Implements INavigationWindow.CloseWindow\n        Throw New NotImplementedException()\n    End Sub\n\n    Public Function GetNavigation() As INavigationView Implements INavigationWindow.GetNavigation\n        Throw New NotImplementedException()\n    End Function\n\n    Public Function Navigate(pageType As Type) As Boolean Implements INavigationWindow.Navigate\n        Throw New NotImplementedException()\n    End Function\n\n    Private Sub MainWindow_Closing(sender As Object, e As CancelEventArgs)\n        If Not IsVisible Then Return\n        _SettingsService.AppSettings.WindowState = WindowState\n        _SettingsService.AppSettings.WindowWidth = If(Width > 0, Width, 1300)\n        _SettingsService.AppSettings.WindowHeight = If(Height > 0, Height, 700)\n        _SettingsService.AppSettings.WindowLeft = Left\n        _SettingsService.AppSettings.WindowTop = Top\n        _SettingsService.SaveSettings()\n    End Sub\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Messages/WatcherAddedFolderToQueueMessage.vb",
    "content": "﻿Imports CommunityToolkit.Mvvm.Messaging.Messages\n\nPublic Class WatcherAddedFolderToQueueMessage : Inherits ValueChangedMessage(Of String)\n\n    Public Sub New(value As String)\n        MyBase.New(value)\n    End Sub\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Models/CompressableFolders/CompressableFolder.vb",
    "content": "﻿Imports System.Collections.ObjectModel\nImports System.IO\nImports System.Threading\n\nImports CommunityToolkit.Mvvm.ComponentModel\n\nImports CompactGUI.Core\nImports CompactGUI.Core.Settings\nImports CompactGUI.Core.WOFHelper\n\nImports Microsoft.Extensions.Logging\n\nImports PropertyChanged\n\n\n'Need this abstract class so we can use it in XAML\nPublic MustInherit Class CompressableFolder : Inherits ObservableObject : Implements IDisposable\n\n    <ObservableProperty> Private _FolderName As String\n    <ObservableProperty> Private _DisplayName As String\n    <ObservableProperty> Private _CurrentCompression As CompressionMode\n\n    <NotifyPropertyChangedFor(NameOf(BytesSaved), NameOf(CompressionRatio))>\n    <ObservableProperty> Private _FolderActionState As ActionState\n\n    <NotifyPropertyChangedFor(NameOf(BytesSaved), NameOf(CompressionRatio))>\n    <ObservableProperty> Private _UncompressedBytes As Long = 0\n\n    <NotifyPropertyChangedFor(NameOf(BytesSaved), NameOf(CompressionRatio))>\n    <ObservableProperty> Private _CompressedBytes As Long = 0\n\n    <ObservableProperty> Private _AnalysisResults As New ObservableCollection(Of AnalysedFileDetails)\n    <ObservableProperty> Private _PoorlyCompressedFiles As List(Of ExtensionResult)\n    <ObservableProperty> Private _CompressionOptions As New CompressionOptions\n    <ObservableProperty> Private _IsFreshlyCompressed As Boolean\n\n    <ObservableProperty> Private _FolderBGImage As BitmapImage = Nothing\n\n\n    <ObservableProperty> Private _IsGettingEstimate As Boolean = False\n\n    <ObservableProperty> Private _WikiCompressionResults As WikiCompressionResults\n    <ObservableProperty> Private _WikiPoorlyCompressedFiles As New List(Of String)\n\n\n    Public ReadOnly Property BytesSaved As Long\n        Get\n            Return UncompressedBytes - CompressedBytes\n        End Get\n    End Property\n\n\n    Public ReadOnly Property CompressionRatio As Decimal\n        Get\n            If CompressedBytes = 0 Then Return 0\n            Return CompressedBytes / UncompressedBytes\n        End Get\n    End Property\n\n\n    Public ReadOnly Property GlobalPoorlyCompressedFileCount\n        Get\n            If AnalysisResults Is Nothing OrElse Application.GetService(Of ISettingsService).AppSettings.NonCompressableList.Count = 0 Then Return 0\n            Return AnalysisResults.Where(Function(fl) Application.GetService(Of ISettingsService).AppSettings.NonCompressableList.Contains(New IO.FileInfo(fl.FileName).Extension)).Count\n        End Get\n    End Property\n\n    Public ReadOnly Property WikiPoorlyCompressedFilesCount As Integer\n        Get\n            If AnalysisResults Is Nothing OrElse WikiPoorlyCompressedFiles Is Nothing Then Return 0\n            Return WikiPoorlyCompressedFiles.Count\n        End Get\n    End Property\n\n\n    <ObservableProperty> Private _CompressionProgress As CompressionProgress\n\n\n    Public Compressor As ICompressor\n    Public Analyser As Analyser\n\n\n    Public Sub NotifyPropertyChanged(name As String)\n        OnPropertyChanged(name)\n    End Sub\n\n\n    Public Sub Dispose() Implements IDisposable.Dispose\n        Compressor?.Dispose()\n        Analyser?.Dispose()\n\n        AnalysisResults?.Clear()\n        PoorlyCompressedFiles?.Clear()\n        WikiPoorlyCompressedFiles?.Clear()\n\n\n        GC.SuppressFinalize(Me)\n    End Sub\nEnd Class\n\n\n\nPublic Enum ActionState\n    Idle\n    Analysing\n    Working\n    Results\n    Paused\n    Waiting\nEnd Enum"
  },
  {
    "path": "CompactGUI/Models/CompressableFolders/CompressableFolderFactory.vb",
    "content": "﻿Public Class CompressableFolderFactory\n    Public Shared Async Function CreateCompressableFolder(path As String) As Task(Of CompressableFolder)\n        Dim folderInfo = New IO.DirectoryInfo(path)\n\n        If IsSteamFolder(folderInfo) Then\n            Return If(Await CreateSteamFolder(folderInfo), New StandardFolder(path))\n        Else\n            Return New StandardFolder(path)\n        End If\n\n    End Function\n\n\n    Private Shared Function IsSteamFolder(folderPath As IO.DirectoryInfo) As Boolean\n        Return folderPath.Parent?.Parent?.Name.ToLowerInvariant = \"steamapps\"\n    End Function\n\n\n    Private Shared Async Function CreateSteamFolder(folderInfo As IO.DirectoryInfo) As Task(Of CompressableFolder)\n\n        Dim SteamFolderData? = SteamACFParser.GetSteamNameAndIDFromFolder(folderInfo)\n\n        If SteamFolderData Is Nothing Then Return Nothing\n\n        Dim steamFolder As New SteamFolder(folderInfo.FullName, If(SteamFolderData?.GameName, folderInfo.FullName), SteamFolderData?.AppID)\n        Await steamFolder.InitializeAsync()\n\n        Return steamFolder\n    End Function\n\n\n\n\nEnd Class\n\n"
  },
  {
    "path": "CompactGUI/Models/CompressableFolders/StandardFolder.vb",
    "content": "﻿\n\nPublic NotInheritable Class StandardFolder : Inherits CompressableFolder\n\n    Public Sub New(path As String)\n        FolderName = path\n        DisplayName = IO.Path.GetFileName(path.TrimEnd(IO.Path.DirectorySeparatorChar, IO.Path.AltDirectorySeparatorChar))\n\n    End Sub\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Models/CompressableFolders/SteamFolder.vb",
    "content": "﻿\nImports System.IO\nImports System.Net.Http\n\nImports CommunityToolkit.Mvvm.ComponentModel\n\nImports CompactGUI.Core.Settings\n\nPublic Class SteamFolder : Inherits CompressableFolder\n\n\n    'Steam-Specific\n    <ObservableProperty> Private _SteamAppID As Integer\n\n\n    Public Sub New(folderName As String, displayName As String, steamappId As Integer)\n        Me.FolderName = folderName\n        Me.SteamAppID = steamappId\n        Me.DisplayName = displayName\n\n        If Not CompressableFolderService.IsHDD(Me) AndAlso Core.SharedMethods.IsDirectStorageGameFolder(folderName) Then Application.GetService(Of CustomSnackBarService).ShowDirectStorageWarning(displayName)\n\n    End Sub\n\n\n    Public Async Function InitializeAsync() As Task\n        Try\n            Await GetSteamHeaderAsync(Me)\n        Catch ex As Exception\n            Debug.WriteLine($\"Error getting Steam header: {ex.Message}\")\n        End Try\n    End Function\n\n\n    Public Overloads ReadOnly Property WikiPoorlyCompressedFilesCount As Integer\n        Get\n            If AnalysisResults Is Nothing OrElse WikiPoorlyCompressedFiles Is Nothing Then Return 0\n            Return AnalysisResults.Where(Function(fl) WikiPoorlyCompressedFiles.Contains(New FileInfo(fl.FileName).Extension)).Count\n        End Get\n    End Property\n\n\n    Public Async Function GetWikiResults() As Task\n        ' Dim wikihandler = Application.GetService(Of WikiHandler)()\n        Dim res = Await Application.GetService(Of IWikiService).ParseData(SteamAppID)\n\n        WikiPoorlyCompressedFiles = res.poorlyCompressedList?.Where(Function(k) k.Value > 100 AndAlso k.Key <> \"\").Select(Function(k) k.Key).ToList\n\n        WikiCompressionResults = If(res.compressionResults IsNot Nothing, New WikiCompressionResults(res.compressionResults), Nothing)\n        If WikiCompressionResults Is Nothing Then Return\n\n        Dim tempX4KLvl = WikiCompressionResults.XPress4K.CompressionPercent\n        WikiCompressionResults.XPress4K.BeforeBytes = UncompressedBytes\n        WikiCompressionResults.XPress4K.AfterBytes = UncompressedBytes * tempX4KLvl / 100\n\n        Dim tempX8KLvl = WikiCompressionResults.XPress8K.CompressionPercent\n        WikiCompressionResults.XPress8K.BeforeBytes = UncompressedBytes\n        WikiCompressionResults.XPress8K.AfterBytes = UncompressedBytes * tempX8KLvl / 100\n\n        Dim tempX16KLvl = WikiCompressionResults.XPress16K.CompressionPercent\n        WikiCompressionResults.XPress16K.BeforeBytes = UncompressedBytes\n        WikiCompressionResults.XPress16K.AfterBytes = UncompressedBytes * tempX16KLvl / 100\n\n        Dim tempLZXLvl = WikiCompressionResults.LZX.CompressionPercent\n        WikiCompressionResults.LZX.BeforeBytes = UncompressedBytes\n        WikiCompressionResults.LZX.AfterBytes = UncompressedBytes * tempLZXLvl / 100\n\n    End Function\n\n    Public Shared Async Function GetSteamHeaderAsync(folder As SteamFolder) As Task\n\n        If folder.SteamAppID = 0 Then Return\n\n        Dim tempImg As BitmapImage = Nothing\n\n        Dim EnvironmentPath = Environment.GetEnvironmentVariable(\"IridiumIO\", EnvironmentVariableTarget.User)\n        Dim imageDir = Path.Combine(EnvironmentPath, \"CompactGUI\", \"SteamCache\")\n        Dim imagePath = Path.Combine(imageDir, $\"{folder.SteamAppID}.jpg\")\n\n        If Not Directory.Exists(imageDir) Then Directory.CreateDirectory(imageDir)\n\n        If File.Exists(imagePath) Then\n            tempImg = LoadImageFromDisk(imagePath)\n            Debug.WriteLine(\"Loaded Steam header image from disk\")\n        Else\n\n            Dim url As String = $\"https://steamcdn-a.akamaihd.net/steam/apps/{folder.SteamAppID}/page_bg_generated_v6b.jpg\"\n            'If FolderBGImage?.UriSource IsNot Nothing AndAlso FolderBGImage.UriSource.ToString() = url Then Return\n\n            Try\n                Using client As New HttpClient()\n                    Dim imageData As Byte() = Await client.GetByteArrayAsync(url)\n                    tempImg = LoadImageFromMemoryStream(imageData)\n                    Await File.WriteAllBytesAsync(imagePath, imageData)\n                End Using\n            Catch ex As Exception\n                Debug.WriteLine($\"Failed to load Steam header image: {ex.Message}\")\n            End Try\n\n        End If\n\n        Application.Current.Dispatcher.Invoke(Sub()\n                                                  If tempImg IsNot Nothing Then\n                                                      folder.FolderBGImage = tempImg\n                                                  End If\n                                              End Sub)\n\n\n    End Function\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Models/CompressionResult.vb",
    "content": "﻿\nImports CommunityToolkit.Mvvm.ComponentModel\n\nPublic Class CompressionResult : Inherits ObservableObject\n\n    <ObservableProperty> Private _CompType As Core.CompressionMode\n\n    <NotifyPropertyChangedFor(NameOf(CompressionSavings), NameOf(CompressionSavings), NameOf(BytesSaved), NameOf(IsNonExistent))>\n    <ObservableProperty> Private _BeforeBytes As Long = 0\n\n    <NotifyPropertyChangedFor(NameOf(CompressionSavings), NameOf(CompressionSavings), NameOf(BytesSaved), NameOf(IsNonExistent))>\n    <ObservableProperty> Private _AfterBytes As Long = 0\n\n    <ObservableProperty> Private _TotalResults As Integer = 0\n\n\n    Public ReadOnly Property CompressionPercent As Integer\n        Get\n            If BeforeBytes = 0 OrElse AfterBytes = 0 Then Return 0\n            Return Math.Round((AfterBytes / BeforeBytes) * 100, 2)\n        End Get\n    End Property\n\n    Public ReadOnly Property CompressionSavings As Integer\n        Get\n            If BeforeBytes = 0 OrElse AfterBytes = 0 Then Return 0\n            Return Math.Round((BeforeBytes - AfterBytes) / BeforeBytes * 100, 2)\n        End Get\n    End Property\n\n    Public ReadOnly Property BytesSaved As Long\n        Get\n            Return BeforeBytes - AfterBytes\n        End Get\n    End Property\n\n    Private ReadOnly Property IsNonExistent As Boolean\n        Get\n            Return BeforeBytes = 0 OrElse AfterBytes = 0\n        End Get\n    End Property\n\nEnd Class\n\n"
  },
  {
    "path": "CompactGUI/Models/NewModels/CompressionOptions.vb",
    "content": "﻿Imports CommunityToolkit.Mvvm.ComponentModel\n\nPublic Class CompressionOptions : Inherits ObservableObject\n    <ObservableProperty> Private _SelectedCompressionMode As Core.CompressionMode = Core.CompressionMode.XPRESS4K\n    <ObservableProperty> Private _SkipPoorlyCompressedFileTypes As Boolean\n    <ObservableProperty> Private _SkipUserSubmittedFiletypes As Boolean\n    <ObservableProperty> Private _WatchFolderForChanges As Boolean\n\n\n    Public Function Clone() As CompressionOptions\n        Dim copy As New CompressionOptions With {\n            .SelectedCompressionMode = SelectedCompressionMode,\n            .SkipPoorlyCompressedFileTypes = SkipPoorlyCompressedFileTypes,\n            .SkipUserSubmittedFiletypes = SkipUserSubmittedFiletypes,\n            .WatchFolderForChanges = WatchFolderForChanges\n        }\n\n        Return copy\n    End Function\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Models/NewModels/DatabaseCompressionResult.vb",
    "content": "﻿Imports CommunityToolkit.Mvvm.ComponentModel\n\nPublic Class DatabaseCompressionResult : Inherits ObservableObject\n\n    <ObservableProperty> Private _GameName As String\n    <ObservableProperty> Private _SteamID As Integer\n    <ObservableProperty> Private _Confidence As DBResultConfidence\n\n    <ObservableProperty> Private _Result_X4K As CompressionResult\n    <ObservableProperty> Private _Result_X8K As CompressionResult\n    <ObservableProperty> Private _Result_X16K As CompressionResult\n    <ObservableProperty> Private _Result_LZX As CompressionResult\n\n    <ObservableProperty> Private _PoorlyCompressedExtensions As List(Of DBPoorlyCompressedExtension)\n\n    Public ReadOnly Property MaxSavings As Decimal\n        Get\n            Return Math.Max(\n            Math.Max(\n                If(Result_X4K IsNot Nothing, Result_X4K.CompressionSavings, 0D),\n                If(Result_X8K IsNot Nothing, Result_X8K.CompressionSavings, 0D)\n            ),\n            Math.Max(\n                If(Result_X16K IsNot Nothing, Result_X16K.CompressionSavings, 0D),\n                If(Result_LZX IsNot Nothing, Result_LZX.CompressionSavings, 0D)\n            )\n        )\n        End Get\n    End Property\n\n\nEnd Class\n\nPublic Enum DBResultConfidence\n    Low = 0\n    Medium = 1\n    High = 2\nEnd Enum\n\nPublic Structure DBPoorlyCompressedExtension\n    Public Property Extension As String\n    Public Property Count As Integer\nEnd Structure"
  },
  {
    "path": "CompactGUI/Models/NewModels/WikiCompressionResults.vb",
    "content": "﻿Imports CommunityToolkit.Mvvm.ComponentModel\n\nPublic Class WikiCompressionResults : Inherits ObservableObject\n    <ObservableProperty> Private _XPress4K As New CompressionResult With {.CompType = Core.CompressionMode.XPRESS4K}\n    <ObservableProperty> Private _XPress8K As New CompressionResult With {.CompType = Core.CompressionMode.XPRESS8K}\n    <ObservableProperty> Private _XPress16K As New CompressionResult With {.CompType = Core.CompressionMode.XPRESS16K}\n    <ObservableProperty> Private _LZX As New CompressionResult With {.CompType = Core.CompressionMode.LZX}\n\n    Sub New(compressionResults As List(Of CompressionResult))\n        For Each result In compressionResults\n            Select Case result.CompType\n                Case Core.CompressionMode.XPRESS4K\n                    XPress4K = result\n                Case Core.CompressionMode.XPRESS8K\n                    XPress8K = result\n                Case Core.CompressionMode.XPRESS16K\n                    XPress16K = result\n                Case Core.CompressionMode.LZX\n                    LZX = result\n            End Select\n        Next\n\n    End Sub\n\n\nEnd Class\n\n"
  },
  {
    "path": "CompactGUI/Models/SemVersion.vb",
    "content": "﻿Public Class SemVersion : Implements IComparable(Of SemVersion)\n    Property Major As Integer\n    Property Minor As Integer\n    Property Patch As Integer\n    Property PreRelease As String\n    Property PreReleaseMinor As Integer\n\n    Sub New()\n    End Sub\n\n    Sub New(major As Integer, minor As Integer, patch As Integer)\n        Me.Major = major\n        Me.Minor = minor\n        Me.Patch = patch\n    End Sub\n\n    Sub New(major As Integer, minor As Integer, patch As Integer, prerelease As String, prereleaseminor As Integer)\n        Me.Major = major\n        Me.Minor = minor\n        Me.Patch = patch\n        Me.PreRelease = prerelease.ToLower\n        Me.PreReleaseMinor = prereleaseminor\n    End Sub\n\n    Public Function CompareTo(other As SemVersion) As Integer Implements IComparable(Of SemVersion).CompareTo\n        If other.Major - Major <> 0 Then Return other.Major - Major\n        If other.Minor - Minor <> 0 Then Return other.Minor - Minor\n        If other.Patch - Patch <> 0 Then Return other.Patch - Patch\n        If Not String.Equals(PreRelease, other.PreRelease) Then\n            If PreRelease = \"\" Then Return -1\n            If other.PreRelease = \"\" Then Return 1\n            Return String.Compare(other.PreRelease, PreRelease)\n        End If\n        If other.PreReleaseMinor - PreReleaseMinor <> 0 Then Return other.PreReleaseMinor - PreReleaseMinor\n        Return 0\n\n    End Function\n\n    Public Shared Operator <(lhs As SemVersion, rhs As SemVersion) As Boolean\n        Dim comparer = lhs.CompareTo(rhs)\n        If comparer <= 0 Then Return False\n        Return True\n    End Operator\n\n    Public Shared Operator >(lhs As SemVersion, rhs As SemVersion) As Boolean\n        Dim comparer = lhs.CompareTo(rhs)\n        If comparer >= 0 Then Return False\n        Return True\n    End Operator\n\n    Public Overrides Function ToString() As String\n        Return $\"{Major}.{Minor}.{Patch}-{PreRelease}.{PreReleaseMinor}\"\n    End Function\n\n    Public Function IsPreRelease() As Boolean\n        If PreRelease = \"\" OrElse PreRelease = Nothing OrElse PreRelease = \"r\" Then Return False\n        Return True\n    End Function\n\n    Public Function Friendly() As String\n        Return If(PreRelease = \"\" OrElse PreRelease = Nothing OrElse PreRelease = \"r\",\n            $\"{Major}.{Minor}.{Patch}\",\n            $\"{Major}.{Minor}.{Patch} {PreRelease} {PreReleaseMinor}\")\n\n    End Function\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Models/SteamACFResult.vb",
    "content": "﻿Public Structure SteamACFResult\n    Public AppID As Integer\n    Public GameName As String\n    Public InstallDirectory As String\n\n    ' Special placeholder for \"no result\"\n    Public Shared ReadOnly NoResult As New SteamACFResult With {\n        .AppID = -1,\n        .GameName = String.Empty,\n        .InstallDirectory = String.Empty\n    }\n\nEnd Structure"
  },
  {
    "path": "CompactGUI/Models/SteamResultsData.vb",
    "content": "﻿\n' Object to get results from existing wiki file\nPublic Class SteamResultsData\n\n    Public SteamID As Integer\n    Public GameName As String\n    Public FolderName As String\n    Public Confidence As Integer '0=Low, 1=Moderate, 2=High\n    Public CompressionResults As New List(Of CompressionResult)\n    Public PoorlyCompressedExtensions As Dictionary(Of String, Integer)\n\nEnd Class\n\n"
  },
  {
    "path": "CompactGUI/Models/SteamSubmissionData.vb",
    "content": "﻿\n' Object used to build submission data to send online after compression\nPublic Class SteamSubmissionData\n    Public Property UID As String\n    Public Property SteamID As Integer\n    Public Property GameName As String\n    Public Property FolderName As String\n    Public Property CompressionMode As Integer\n    Public Property BeforeBytes As Long\n    Public Property AfterBytes As Long\n    Public Property PoorlyCompressedExt As List(Of Core.ExtensionResult)\n\nEnd Class\n\n"
  },
  {
    "path": "CompactGUI/My Project/Application.myapp",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<MyApplicationData xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n  <MySubMain>true</MySubMain>\n  <MainForm>Form1</MainForm>\n  <SingleInstance>false</SingleInstance>\n  <ShutdownMode>0</ShutdownMode>\n  <EnableVisualStyles>true</EnableVisualStyles>\n  <AuthenticationMode>0</AuthenticationMode>\n  <SaveMySettingsOnExit>true</SaveMySettingsOnExit>\n</MyApplicationData>"
  },
  {
    "path": "CompactGUI/My Project/app.manifest",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\">\n  <assemblyIdentity version=\"1.0.0.0\" name=\"MyApplication.app\"/>\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v2\">\n    <security>\n      <requestedPrivileges xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n        <!-- UAC Manifest Options\n             If you want to change the Windows User Account Control level replace the \n             requestedExecutionLevel node with one of the following.\n\n        <requestedExecutionLevel  level=\"asInvoker\" uiAccess=\"false\" />\n        <requestedExecutionLevel  level=\"requireAdministrator\" uiAccess=\"false\" />\n        <requestedExecutionLevel  level=\"highestAvailable\" uiAccess=\"false\" />\n\n            Specifying requestedExecutionLevel element will disable file and registry virtualization. \n            Remove this element if your application requires this virtualization for backwards\n            compatibility.\n        -->\n        <requestedExecutionLevel level=\"asInvoker\" uiAccess=\"false\" />\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n\n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!-- A list of the Windows versions that this application has been tested on\n           and is designed to work with. Uncomment the appropriate elements\n           and Windows will automatically select the most compatible environment. -->\n\n      <!-- Windows Vista -->\n      <!--<supportedOS Id=\"{e2011457-1546-43c5-a5fe-008deee3d3f0}\" />-->\n\n      <!-- Windows 7 -->\n      <!--<supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\" />-->\n\n      <!-- Windows 8 -->\n      <!--<supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\" />-->\n\n      <!-- Windows 8.1 -->\n      <!--<supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\" />-->\n\n      <!-- Windows 10 -->\n      <!--<supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\" />-->\n\n    </application>\n  </compatibility>\n\n  <!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher\n       DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need \n       to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should \n       also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config. \n       \n       Makes the application long-path aware. See https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->\n  <!--\n  <application xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <windowsSettings>\n      <dpiAware xmlns=\"http://schemas.microsoft.com/SMI/2005/WindowsSettings\">true</dpiAware>\n      <longPathAware xmlns=\"http://schemas.microsoft.com/SMI/2016/WindowsSettings\">true</longPathAware>\n    </windowsSettings>\n  </application>\n  -->\n\n  <!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->\n  <!--\n  <dependency>\n    <dependentAssembly>\n      <assemblyIdentity\n          type=\"win32\"\n          name=\"Microsoft.Windows.Common-Controls\"\n          version=\"6.0.0.0\"\n          processorArchitecture=\"*\"\n          publicKeyToken=\"6595b64144ccf1df\"\n          language=\"*\"\n        />\n    </dependentAssembly>\n  </dependency>\n  -->\n\n</assembly>\n"
  },
  {
    "path": "CompactGUI/Services/ApplicationHostService.vb",
    "content": "﻿Imports CompactGUI.Core.Settings\n\nImports Microsoft.Extensions.DependencyInjection\nImports Microsoft.Extensions.Hosting\nImports Microsoft.Extensions.Logging\n\nImports System.Linq\nImports System.Threading\nImports System.Threading.Tasks\nImports System.Windows\n\nPublic Class ApplicationHostService\n    Implements IHostedService\n\n    Private ReadOnly _serviceProvider As IServiceProvider\n    Private ReadOnly _settingsService As ISettingsService\n\n    Public Sub New(serviceProvider As IServiceProvider, settingsService As ISettingsService)\n        _serviceProvider = serviceProvider\n        _settingsService = settingsService\n    End Sub\n\n    ''' <summary>\n    ''' Triggered when the application host is ready to start the service.\n    ''' </summary>\n    ''' <param name=\"cancellationToken\">Indicates that the start process has been aborted.</param>\n    Public Async Function StartAsync(cancellationToken As CancellationToken) As Task Implements IHostedService.StartAsync\n        Await HandleActivationAsync()\n    End Function\n\n    ''' <summary>\n    ''' Triggered when the application host is performing a graceful shutdown.\n    ''' </summary>\n    ''' <param name=\"cancellationToken\">Indicates that the shutdown process should no longer be graceful.</param>\n    Public Async Function StopAsync(cancellationToken As CancellationToken) As Task Implements IHostedService.StopAsync\n        Await Task.CompletedTask\n    End Function\n\n    ''' <summary>\n    ''' Creates main window during activation.\n    ''' </summary>\n    Private Async Function HandleActivationAsync() As Task\n        Await Task.CompletedTask\n\n        Application.GetService(Of ILogger(Of ApplicationHostService))().LogInformation(\"Logging Level: {LogLevel}\", _settingsService.AppSettings.LogLevel)\n\n\n        Dim args As String() = Environment.GetCommandLineArgs()\n\n        Dim shouldMinimizeToTray As Boolean = (args.Length = 2 AndAlso args(1).ToString = \"-tray\") OrElse\n                                          (_settingsService.AppSettings.StartInSystemTray AndAlso args.Length = 1)\n\n        If Not Application.Current.Windows.OfType(Of MainWindow)().Any() Then\n\n            Dim navigationWindow = _serviceProvider.GetRequiredService(Of MainWindow)()\n            AddHandler navigationWindow.Loaded, AddressOf OnNavigationWindowLoaded\n            navigationWindow.Show()\n\n            If shouldMinimizeToTray Then\n                Application.GetService(Of MainWindowViewModel).ClosingCommand.Execute(New ComponentModel.CancelEventArgs(True))\n            ElseIf args.Length = 2 Then\n                Await Application.GetService(Of HomeViewModel).AddFoldersAsync({args(1)})\n            End If\n\n\n        End If\n\n\n    End Function\n\n    Private Sub OnNavigationWindowLoaded(sender As Object, e As RoutedEventArgs)\n        If TypeOf sender IsNot MainWindow Then\n            Return\n        End If\n\n        Dim navigationWindow = DirectCast(sender, MainWindow)\n        navigationWindow.NavigationView.Navigate(GetType(HomePage))\n    End Sub\nEnd Class"
  },
  {
    "path": "CompactGUI/Services/CompressableFolderService.vb",
    "content": "﻿Imports System.Collections.ObjectModel\nImports System.Threading\n\nImports CompactGUI.Core\nImports CompactGUI.Core.Settings\n\nImports Microsoft.CodeAnalysis.Diagnostics\n\nImports Microsoft.Extensions.Logging\n\nPublic Class CompressableFolderService\n\n\n    Private Shared ReadOnly CompactorLogger As ILogger = Application.GetService(Of ILogger(Of Compactor))()\n    Private Shared ReadOnly UncompactorLogger As ILogger = Application.GetService(Of ILogger(Of Uncompactor))()\n    Private Shared ReadOnly AnalyserLogger As ILogger = Application.GetService(Of ILogger(Of Analyser))()\n\n    Private folderTokens As New Dictionary(Of CompressableFolder, CancellationTokenSource)\n\n\n    Public Async Function CompressFolder(folder As CompressableFolder) As Task(Of Boolean)\n        folder.Compressor = New Compactor(folder.FolderName, WOFHelper.WOFConvertCompressionLevel(folder.CompressionOptions.SelectedCompressionMode), GetSkipList(folder), folder.Analyser, CompactorLogger)\n        Return Await RunCompressionAsync(folder, folder.Compressor, Nothing, True)\n\n\n    End Function\n\n\n    Public Async Function UncompressFolder(folder As CompressableFolder) As Task(Of Boolean)\n\n        folder.Compressor = New Uncompactor(UncompactorLogger)\n        Dim compressedFilesList = folder.AnalysisResults.Where(Function(rs) rs.CompressedSize < rs.UncompressedSize).Select(Of String)(Function(f) f.FileName).ToList\n        Return Await RunCompressionAsync(folder, folder.Compressor, compressedFilesList, isCompressing:=False)\n\n\n    End Function\n\n    Private Async Function RunCompressionAsync(folder As CompressableFolder, compressor As ICompressor, filesList As List(Of String), isCompressing As Boolean) As Task(Of Boolean)\n        folder.FolderActionState = ActionState.Working\n\n        CancelEstimation(folder)\n        Dim cts = New CancellationTokenSource()\n        folderTokens(folder) = cts\n        Dim progress As IProgress(Of CompressionProgress) = New Progress(Of CompressionProgress)(Sub(x) folder.CompressionProgress = x)\n\n        progress.Report(New CompressionProgress(0, \"\"))\n\n        Dim res = Await compressor.RunAsync(filesList, progress, GetThreadCount(folder))\n\n        If isCompressing Then\n            folder.FolderActionState = ActionState.Results\n            folder.IsFreshlyCompressed = res\n        Else\n            folder.FolderActionState = ActionState.Idle\n            folder.IsFreshlyCompressed = False\n            Await AnalyseFolderAsync(folder)\n        End If\n        compressor.Dispose()\n\n\n        folderTokens(folder).Dispose()\n        folderTokens.Remove(folder)\n\n\n        Return res\n    End Function\n\n\n    Public Async Function AnalyseFolderAsync(folder As CompressableFolder) As Task(Of Integer)\n\n        folder.FolderActionState = ActionState.Analysing\n        CancelEstimation(folder)\n\n        Dim cts = New CancellationTokenSource()\n        folderTokens(folder) = cts\n        Dim token = cts.Token\n\n\n        folder.Analyser?.Dispose()\n        folder.Analyser = New Analyser(folder.FolderName, AnalyserLogger)\n\n        If Not Core.SharedMethods.HasDirectoryWritePermission(folder.FolderName) Then\n            folder.FolderActionState = ActionState.Idle\n            Return -1\n        End If\n\n        Dim retAnalysisResults = Await folder.Analyser.GetAnalysedFilesAsync(token)\n        If cts.IsCancellationRequested Then\n            folder.FolderActionState = ActionState.Idle\n            Return 1\n        End If\n\n        folder.AnalysisResults = New ObservableCollection(Of AnalysedFileDetails)(retAnalysisResults)\n        folder.UncompressedBytes = folder.Analyser.UncompressedBytes\n        folder.CompressedBytes = folder.Analyser.CompressedBytes\n\n        If folder.Analyser.ContainsCompressedFiles OrElse folder.IsFreshlyCompressed Then\n            folder.FolderActionState = ActionState.Results\n        Else\n            folder.FolderActionState = ActionState.Idle\n        End If\n        folder.PoorlyCompressedFiles = folder.Analyser.GetPoorlyCompressedExtensions()\n\n        Return 0\n\n    End Function\n\n    Public Overridable Async Function GetEstimatedCompression(folder As CompressableFolder) As Task\n        folder.IsGettingEstimate = True\n\n        CancelEstimation(folder)\n        Dim cts = New CancellationTokenSource()\n        folderTokens(folder) = cts\n\n        Dim estimator As New Estimator\n        Dim estimatedData As List(Of (AnalysedFile As AnalysedFileDetails, CompressionRatio As Single)) = Nothing\n\n        Try\n            estimatedData = Await Task.Run(Function() estimator.EstimateCompression(folder.AnalysisResults.ToList, IsHDD(folder), GetThreadCount(folder), Core.SharedMethods.GetClusterSize(folder.FolderName), cts.Token))\n\n        Catch ex As AggregateException\n            folder.IsGettingEstimate = False\n            Return\n        End Try\n\n        For Each item In estimatedData\n            If item.CompressionRatio >= 0.98 AndAlso item.AnalysedFile.FileName <> \"\" Then\n                folder.WikiPoorlyCompressedFiles.Add(item.AnalysedFile.FileName)\n            End If\n        Next\n\n        Dim estimatedAfterBytes = estimatedData.Sum(Function(x) x.AnalysedFile.UncompressedSize * x.CompressionRatio)\n\n        'This is absolutely stupid\n\n        Dim X4KResult As New CompressionResult\n        X4KResult.CompType = CompressionMode.XPRESS4K\n        X4KResult.BeforeBytes = folder.UncompressedBytes\n        X4KResult.AfterBytes = Math.Min(estimatedAfterBytes * 1.01, folder.UncompressedBytes)\n        X4KResult.TotalResults = 1\n\n        Dim X8KResult As New CompressionResult\n        X8KResult.CompType = CompressionMode.XPRESS8K\n        X8KResult.BeforeBytes = folder.UncompressedBytes\n        X8KResult.AfterBytes = Math.Min(estimatedAfterBytes * 1.0, folder.UncompressedBytes)\n        X8KResult.TotalResults = 1\n\n        Dim X16KResult As New CompressionResult\n        X16KResult.CompType = CompressionMode.XPRESS16K\n        X16KResult.BeforeBytes = folder.UncompressedBytes\n        X16KResult.AfterBytes = Math.Min(estimatedAfterBytes * 0.98, folder.UncompressedBytes)\n        X16KResult.TotalResults = 1\n\n        Dim LZXResult As New CompressionResult\n        LZXResult.CompType = CompressionMode.LZX\n        LZXResult.BeforeBytes = folder.UncompressedBytes\n        LZXResult.AfterBytes = Math.Min(estimatedAfterBytes * 0.95, folder.UncompressedBytes)\n        LZXResult.TotalResults = 1\n\n        folder.WikiCompressionResults = New WikiCompressionResults(New List(Of CompressionResult) From {X4KResult, X8KResult, X16KResult, LZXResult})\n\n        folder.IsGettingEstimate = False\n\n\n        folder.NotifyPropertyChanged(NameOf(folder.WikiCompressionResults))\n        folder.NotifyPropertyChanged(NameOf(folder.WikiPoorlyCompressedFiles))\n        folder.NotifyPropertyChanged(NameOf(folder.WikiPoorlyCompressedFilesCount))\n        folder.NotifyPropertyChanged(NameOf(folder.IsGettingEstimate))\n\n    End Function\n    Public Sub CancelEstimation(folder As CompressableFolder)\n        If folderTokens.ContainsKey(folder) AndAlso Not folderTokens(folder).IsCancellationRequested Then\n            folderTokens(folder).Cancel()\n        End If\n    End Sub\n\n\n    Public Shared Function GetThreadCount(folder As CompressableFolder) As Integer\n        Dim threadCount As Integer = Application.GetService(Of ISettingsService).AppSettings.MaxCompressionThreads\n        If Application.GetService(Of ISettingsService).AppSettings.LockHDDsToOneThread Then\n            Dim HDDType As DiskDetector.Models.HardwareType = GetDiskType(folder)\n            If HDDType = DiskDetector.Models.HardwareType.Hdd Then\n                threadCount = 1\n            End If\n        End If\n        Return threadCount\n    End Function\n\n    Public Shared Function GetDiskType(folder As CompressableFolder) As DiskDetector.Models.HardwareType\n        If folder.FolderName Is Nothing Then Return DiskDetector.Models.HardwareType.Unknown\n        Try\n            Return DiskDetector.Detector.DetectDrive(folder.FolderName.First, DiskDetector.Models.QueryType.RotationRate).HardwareType\n        Catch ex As Exception\n            Return DiskDetector.Models.HardwareType.Unknown\n        End Try\n    End Function\n\n    Public Shared Function IsHDD(folder As CompressableFolder) As Boolean\n        Dim HDDType As DiskDetector.Models.HardwareType = GetDiskType(folder)\n        Return HDDType = DiskDetector.Models.HardwareType.Hdd\n    End Function\n\n    Private Function GetSkipList(folder As CompressableFolder) As String()\n        Dim exclist As String() = Array.Empty(Of String)()\n\n        If folder.CompressionOptions.SkipPoorlyCompressedFileTypes AndAlso Application.GetService(Of ISettingsService).AppSettings.NonCompressableList.Count <> 0 Then\n            'Debug.WriteLine(\"Adding non-compressable list to exclusion list\")\n            exclist = exclist.Union(Application.GetService(Of ISettingsService).AppSettings.NonCompressableList).ToArray\n        End If\n        If folder.CompressionOptions.SkipUserSubmittedFiletypes AndAlso folder.WikiPoorlyCompressedFiles?.Count <> 0 Then\n            'Debug.WriteLine(\"Adding estimator poorly compressed list to exclusion list\")\n            exclist = exclist.Union(folder.WikiPoorlyCompressedFiles).ToArray\n        End If\n\n        Return exclist\n    End Function\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Services/CustomSnackBarService.vb",
    "content": "﻿Imports CommunityToolkit.Mvvm.Input\n\nImports CompactGUI.Core.SharedMethods\nImports CompactGUI.Logging\n\nImports Microsoft.Extensions.Logging\n\nImports Wpf.Ui.Controls\n\nPublic Class CustomSnackBarService\n    Inherits Wpf.Ui.SnackbarService\n\n    Private ReadOnly logger As ILogger(Of CustomSnackBarService)\n    Public _snackbar As Snackbar\n\n    Public Sub New(logger As ILogger(Of CustomSnackBarService))\n        MyBase.New()\n        Me.logger = logger\n    End Sub\n\n    Public Sub ShowCustom(message As UIElement, title As String, appearance As ControlAppearance, Optional icon As IconElement = Nothing, Optional timeout As TimeSpan = Nothing)\n\n        If GetSnackbarPresenter() Is Nothing Then Throw New InvalidOperationException(\"The SnackbarPresenter was never set\")\n        If _snackbar Is Nothing Then _snackbar = New Snackbar(GetSnackbarPresenter())\n\n        _snackbar.SetCurrentValue(Snackbar.TitleProperty, title)\n        _snackbar.SetCurrentValue(ContentControl.ContentProperty, message)\n        _snackbar.SetCurrentValue(Snackbar.AppearanceProperty, appearance)\n        _snackbar.SetCurrentValue(Snackbar.IconProperty, icon)\n        _snackbar.SetCurrentValue(Snackbar.TimeoutProperty, If(timeout = Nothing, DefaultTimeOut, timeout))\n\n        _snackbar.Show(True)\n    End Sub\n\n\n\n    Public Sub ShowInvalidFoldersMessage(InvalidFolders As List(Of String), InvalidMessages As List(Of FolderVerificationResult))\n\n        Dim messageString = \"\"\n        For i = 0 To InvalidFolders.Count - 1\n            SnackbarServiceLog.ShowInvalidFoldersMessage(logger, InvalidFolders(i), GetFolderVerificationMessage(InvalidMessages(i)))\n            If InvalidFolders.Count = 1 AndAlso InvalidMessages(i) = FolderVerificationResult.InsufficientPermission Then\n                ShowInsufficientPermission(InvalidFolders(i))\n                Return\n            End If\n            messageString &= $\"{InvalidFolders(i)}: {GetFolderVerificationMessage(InvalidMessages(i))}\" & vbCrLf\n        Next\n\n        Show(\"Invalid Folders\", messageString, Wpf.Ui.Controls.ControlAppearance.Danger, Nothing, TimeSpan.FromSeconds(10))\n\n    End Sub\n\n    Public Sub ShowInsufficientPermission(folderName As String)\n        Dim button = New Button With {\n            .Content = \"Restart as Admin\",\n            .Command = New RelayCommand(Sub() RunAsAdmin(folderName)),\n            .Margin = New Thickness(-3, 10, 0, 0)\n        }\n        ShowCustom(button, \"Insufficient permission to access this folder.\", ControlAppearance.Danger, timeout:=TimeSpan.FromSeconds(60))\n    End Sub\n\n    Public Sub ShowUpdateAvailable(newVersion As String, isPreRelease As Boolean)\n        Dim textBlock = New TextBlock\n        textBlock.Text = \"Click to download\"\n\n        ' Show the custom snackbar\n        SnackbarServiceLog.ShowUpdateAvailable(logger, newVersion, isPreRelease)\n        ShowCustom(textBlock, $\"Update Available ▸ Version {newVersion}\", If(isPreRelease, ControlAppearance.Info, ControlAppearance.Success), timeout:=TimeSpan.FromSeconds(10))\n\n        Dim handler As MouseButtonEventHandler = Nothing\n        Dim closedHandler As TypedEventHandler(Of Snackbar, RoutedEventArgs) = Nothing\n\n        handler = Sub(sender, e)\n                      Process.Start(New ProcessStartInfo(\"https://github.com/IridiumIO/CompactGUI/releases/\") With {.UseShellExecute = True})\n                      RemoveHandler Me.GetSnackbarPresenter.MouseDown, handler\n                      RemoveHandler Me._snackbar.Closed, closedHandler\n                  End Sub\n\n        closedHandler = Sub(sender, e)\n                            RemoveHandler Me.GetSnackbarPresenter.MouseDown, handler\n                            RemoveHandler Me._snackbar.Closed, closedHandler\n                        End Sub\n\n        AddHandler Me.GetSnackbarPresenter.MouseDown, handler\n        AddHandler Me._snackbar.Closed, closedHandler\n    End Sub\n\n    Public Sub ShowFailedToSubmitToWiki()\n        Show(\"Failed to submit to wiki\", \"Please check your internet connection and try again\", Wpf.Ui.Controls.ControlAppearance.Danger, Nothing, TimeSpan.FromSeconds(5))\n        SnackbarServiceLog.ShowFailedToSubmitToWiki(logger)\n    End Sub\n\n    Public Sub ShowSubmittedToWiki(steamsubmitdata As SteamSubmissionData, compressionMode As Integer)\n        Show(\"Submitted to wiki\", $\"UID: {steamsubmitdata.UID}{vbCrLf}Game: {steamsubmitdata.GameName}{vbCrLf}SteamID: {steamsubmitdata.SteamID}{vbCrLf}Compression: {[Enum].GetName(GetType(Core.WOFCompressionAlgorithm), Core.WOFHelper.WOFConvertCompressionLevel(compressionMode))}\", Wpf.Ui.Controls.ControlAppearance.Success, Nothing, TimeSpan.FromSeconds(10))\n        SnackbarServiceLog.ShowSubmittedToWiki(logger, steamsubmitdata.UID, steamsubmitdata.GameName, steamsubmitdata.SteamID, steamsubmitdata.CompressionMode)\n    End Sub\n\n\n    Public Sub ShowAppliedToAllFolders()\n        Show(\"Applied to all folders\", \"Compression options have been applied to all folders\", Wpf.Ui.Controls.ControlAppearance.Success, Nothing, TimeSpan.FromSeconds(5))\n        SnackbarServiceLog.ShowAppliedToAllFolders(logger)\n    End Sub\n\n    Public Sub ShowCannotRemoveFolder()\n        Show(\"Cannot remove folder\", \"Please wait until the current operation is finished\", Wpf.Ui.Controls.ControlAppearance.Caution, Nothing, TimeSpan.FromSeconds(5))\n        SnackbarServiceLog.ShowCannotRemoveFolder(logger)\n    End Sub\n\n    Public Sub ShowAddedToQueue()\n        Show(\"Success\", \"Added to Queue\", Wpf.Ui.Controls.ControlAppearance.Success, Nothing, TimeSpan.FromSeconds(5))\n        SnackbarServiceLog.ShowAddedToQueue(logger)\n    End Sub\n\n    Public Sub ShowDirectStorageWarning(displayName As String)\n        Show(displayName,\n            \"This game uses DirectStorage technology. If you are using this feature, you should not compress this game.\",\n            Wpf.Ui.Controls.ControlAppearance.Info,\n            Nothing,\n            TimeSpan.FromSeconds(20))\n        SnackbarServiceLog.ShowDirectStorageWarning(logger, displayName)\n    End Sub\nEnd Class"
  },
  {
    "path": "CompactGUI/Services/SchedulerService.vb",
    "content": "﻿Imports CompactGUI.Core.Settings\nImports CompactGUI.Logging\nImports CompactGUI.Watcher\n\nImports Coravel.Scheduling.Schedule\nImports Coravel.Scheduling.Schedule.Interfaces\n\nImports Microsoft.Extensions.Logging\n\nPublic Class SchedulerService\n    Private ReadOnly settings As Settings\n    Private ReadOnly idleDetector As IdleDetector\n    Private ReadOnly logger As ILogger(Of SchedulerService)\n\n    Public Sub New(settingsService As ISettingsService, idleDetector As IdleDetector, logger As ILogger(Of SchedulerService))\n        Me.settings = settingsService.AppSettings\n        Me.idleDetector = idleDetector\n        Me.logger = logger\n\n    End Sub\n\n    Friend Sub RegenerateSchedule()\n        Dim scheduler = CType(Application.GetService(Of IScheduler), Scheduler)\n        If Not scheduler.TryUnschedule(NameOf(Watcher)) Then Return\n\n        scheduler.ScheduleAsync(Async Function() Await RunScheduledTask()).\n                                Cron($\"{settings.ScheduledBackgroundMinute} {settings.ScheduledBackgroundHour} * * *\").Zoned(TimeZoneInfo.Local).\n                                When(Function() Task.FromResult(IsSchedulerRunnable)).\n                                PreventOverlapping(NameOf(Watcher))\n\n    End Sub\n\n\n    Public Async Function RunScheduledTask() As Task(Of Boolean)\n\n        Dim trayService = Application.GetService(Of TrayNotifierService)\n        trayService.Notify_BackgroundSchedulerRunning()\n        Dim task = Await Application.GetService(Of Watcher.Watcher).RunWatcher()\n\n        If task Then\n            trayService.Notify_BackgroundSchedulerCompleted()\n            Return True\n        End If\n        Return False\n\n    End Function\n\n\n\n    Public Function IsSchedulerRunnable() As Boolean\n\n        SchedulerServiceLog.CheckingSchedulerRunnable(logger)\n\n        If Not settings.EnableBackgroundWatcher Then\n            SchedulerServiceLog.SchedulerDisabled(logger)\n            Return False\n        End If\n\n        If settings.NextScheduledBackgroundRun.Date > Date.Now.Date Then\n            SchedulerServiceLog.SchedulerNextRunInFuture(logger, settings.NextScheduledBackgroundRun)\n            Return False\n        End If\n\n        Select Case settings.BackgroundModeSelection\n            Case BackgroundMode.ScheduledAndIdle\n                If idleDetector.State = IdleState.Idle Then\n                    SchedulerServiceLog.SchedulerRunningIdle(logger)\n                    Return True\n                Else\n                    SchedulerServiceLog.SchedulerNotIdle(logger)\n                    Return False\n                End If\n            Case BackgroundMode.Scheduled\n                SchedulerServiceLog.SchedulerRunningScheduled(logger)\n                Return True\n            Case Else\n                SchedulerServiceLog.SchedulerModeDisabled(logger)\n                Return False\n        End Select\n\n    End Function\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Services/SettingsService.vb",
    "content": "﻿Imports System.IO\nImports System.Text.Json\n\nImports CompactGUI.Core.Settings\nImports CompactGUI.Logging\n\nImports Microsoft.Extensions.Logging\n\nPublic Class SettingsService : Implements ISettingsService\n\n    Public ReadOnly Property DataFolder As DirectoryInfo Implements ISettingsService.DataFolder\n    Public ReadOnly Property SettingsJSONFile As FileInfo Implements ISettingsService.SettingsJSONFile\n    Public ReadOnly Property SettingsVersion As Decimal Implements ISettingsService.SettingsVersion\n    Public Property AppSettings As Settings Implements ISettingsService.AppSettings\n\n    Public Sub New()\n\n        DataFolder = New IO.DirectoryInfo(IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"IridiumIO\", \"CompactGUI\"))\n        SettingsJSONFile = New IO.FileInfo(IO.Path.Combine(DataFolder.FullName, \"settings.json\"))\n\n        SettingsVersion = 1.2\n\n    End Sub\n\n    Public Sub LoadSettings() Implements ISettingsService.LoadSettings\n        If Not DataFolder.Exists Then DataFolder.Create()\n        If Not SettingsJSONFile.Exists Then SettingsJSONFile.Create().Dispose()\n\n        AppSettings = DeserializeAndValidateJSON(SettingsJSONFile)\n\n        If AppSettings.SettingsVersion = 0 OrElse SettingsVersion > AppSettings.SettingsVersion Then\n\n            Dim skipList = AppSettings.NonCompressableList\n\n            AppSettings = New Settings With {.SettingsVersion = SettingsVersion, .NonCompressableList = skipList}\n\n            Dim msgError As New Wpf.Ui.Controls.ContentDialog With {.Title = $\"New Settings Version {SettingsVersion} Detected\", .Content = \"Your settings have been reset to their default to accommodate the update\", .CloseButtonText = \"OK\"}\n            msgError.ShowAsync()\n\n        End If\n\n        Dim output = JsonSerializer.Serialize(AppSettings, Jsonoptions)\n        IO.File.WriteAllText(SettingsJSONFile.FullName, output)\n    End Sub\n\n    Public Sub SaveSettings() Implements ISettingsService.SaveSettings\n        ScheduleSettingsSave()\n    End Sub\n\n    Public Sub ScheduleSettingsSave() Implements ISettingsService.ScheduleSettingsSave\n        SyncLock timerLock\n            If debounceTimer Is Nothing Then\n                debounceTimer = New System.Timers.Timer(debounceDelay.TotalMilliseconds)\n                debounceTimer.AutoReset = False\n                AddHandler debounceTimer.Elapsed, Async Sub(__, ___)\n                                                      Await WriteToFileAsync()\n                                                  End Sub\n            Else\n                debounceTimer.Stop()\n            End If\n            debounceTimer.Start()\n        End SyncLock\n    End Sub\n\n\n    Private ReadOnly Jsonoptions As New JsonSerializerOptions With {.IncludeFields = True, .WriteIndented = True}\n\n    Private Function DeserializeAndValidateJSON(inputjsonFile As IO.FileInfo) As Settings\n        Dim SettingsJSON = IO.File.ReadAllText(inputjsonFile.FullName)\n        If SettingsJSON = \"\" Then SettingsJSON = \"{}\"\n\n        Dim validatedSettings As Settings\n\n        Try\n\n            validatedSettings = JsonSerializer.Deserialize(Of Settings)(SettingsJSON, Jsonoptions)\n        Catch ex As Exception\n            validatedSettings = New Settings With {.SettingsVersion = SettingsVersion}\n\n            Dim msgError As New Wpf.Ui.Controls.ContentDialog With {.Title = $\"Corrupted Settings File Detected\", .Content = \"Your settings have been reset to their default.\", .CloseButtonText = \"OK\"}\n            msgError.ShowAsync()\n\n\n        End Try\n\n        Return validatedSettings\n\n    End Function\n\n\n\n    Private ReadOnly debounceDelay As TimeSpan = TimeSpan.FromMilliseconds(1000)\n    Private ReadOnly timerLock As New Object()\n    Private debounceTimer As System.Timers.Timer\n\n\n    Private Async Function WriteToFileAsync() As Task\n        Try\n            Dim output = JsonSerializer.Serialize(AppSettings, Jsonoptions)\n            Await IO.File.WriteAllTextAsync(SettingsJSONFile.FullName, output)\n            SettingsLog.SettingsSaved(Application.GetService(Of ILogger(Of Settings)))\n        Catch ex As Exception\n            ' Log or handle exception\n        End Try\n    End Function\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Services/SteamACFParser.vb",
    "content": "﻿\n\nImports Gameloop.Vdf\nImports Gameloop.Vdf.Linq\n\nImports Microsoft.Extensions.Caching.Memory\n\nPublic Class SteamACFParser\n\n\n    Private Shared ReadOnly SteamLibraryCache As New MemoryCache(New MemoryCacheOptions())\n\n    Public Shared Function GetSteamNameAndIDFromFolder(SteamFolder As IO.DirectoryInfo) As SteamACFResult?\n        Dim steamAppsFolder = SteamFolder.Parent.Parent\n\n        Dim cachedResult = TryGetCachedGame(steamAppsFolder, SteamFolder)\n        If cachedResult IsNot Nothing Then\n            Return If(Not cachedResult.Equals(SteamACFResult.NoResult), cachedResult, Nothing)\n        End If\n\n        Dim allGames = LookupAllSteamGames(steamAppsFolder)\n        CacheLibrary(steamAppsFolder, allGames)\n\n        If allGames.ContainsKey(SteamFolder.Name) Then Return allGames(SteamFolder.Name)\n        Return Nothing\n\n    End Function\n\n    Private Shared Sub CacheLibrary(steamAppsFolder As IO.DirectoryInfo, allGames As Dictionary(Of String, SteamACFResult?))\n        Dim policy As New MemoryCacheEntryOptions With {\n            .AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(2)\n        }\n\n        SteamLibraryCache.Set(steamAppsFolder.FullName, allGames, policy)\n    End Sub\n\n    Private Shared Function TryGetCachedGame(steamAppsFolder As IO.DirectoryInfo, SteamFolder As IO.DirectoryInfo) As SteamACFResult?\n\n        Dim libraryCache = GetCachedLibrary(steamAppsFolder)\n        If libraryCache Is Nothing Then Return Nothing\n\n        If libraryCache.ContainsKey(SteamFolder.Name) Then Return libraryCache(SteamFolder.Name)\n        Return SteamACFResult.NoResult\n\n    End Function\n\n\n    Private Shared Function GetCachedLibrary(steamAppsFolder As IO.DirectoryInfo) As Dictionary(Of String, SteamACFResult?)\n        Return TryCast(SteamLibraryCache.Get(steamAppsFolder.FullName), Dictionary(Of String, SteamACFResult?))\n    End Function\n\n    Private Shared Function LookupAllSteamGames(steamAppsFolder As IO.DirectoryInfo) As Dictionary(Of String, SteamACFResult?)\n        Dim allGames As New Dictionary(Of String, SteamACFResult?)\n\n        For Each fl In steamAppsFolder.EnumerateFiles(\"*.acf\").Where(Function(f) f.Length > 0)\n            Try\n                Dim ACFFile = VdfConvert.Deserialize(IO.File.ReadAllText(fl.FullName))\n                If ACFFile IsNot Nothing Then\n                    Dim game = ParseACFFile(ACFFile)\n                    allGames(game.InstallDirectory) = game\n                End If\n            Catch\n                Debug.WriteLine($\"ACF file unsupported: {fl.FullName}\")\n            End Try\n        Next\n\n        Return allGames\n    End Function\n\n\n    Private Shared Function ParseACFFile(ACFFile As VProperty) As SteamACFResult\n        Dim appID = CInt(ACFFile.Value.Item(\"appid\").ToString)\n        Dim sName = ACFFile.Value.Item(\"name\").ToString\n        Dim sInstallDir = ACFFile.Value.Item(\"installdir\").ToString\n        Return New SteamACFResult With {.AppID = appID, .GameName = sName, .InstallDirectory = sInstallDir}\n    End Function\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Services/TrayNotifierService.vb",
    "content": "﻿Imports System.Drawing\nImports System.Runtime.InteropServices\nImports System.Windows.Interop\n\nImports CompactGUI.Core.Settings\n\nPublic Class TrayNotifierService\n    Private Const NIM_ADD As Integer = &H0\n    Private Const NIM_MODIFY As Integer = &H1\n    Private Const NIM_DELETE As Integer = &H2\n\n    Private Const NIF_MESSAGE As Integer = &H1\n    Private Const NIF_ICON As Integer = &H2\n    Private Const NIF_TIP As Integer = &H4\n    Private Const NIF_INFO As Integer = &H10\n\n    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Unicode)>\n    Private Structure NOTIFYICONDATA\n        Public cbSize As Integer\n        Public hWnd As IntPtr\n        Public uID As Integer\n        Public uFlags As Integer\n        Public uCallbackMessage As Integer\n        Public hIcon As IntPtr\n        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=128)>\n        Public szTip As String\n        Public dwState As Integer\n        Public dwStateMask As Integer\n        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=256)>\n        Public szInfo As String\n        Public uTimeoutOrVersion As Integer\n        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=64)>\n        Public szInfoTitle As String\n        Public dwInfoFlags As Integer\n        Public guidItem As Guid\n        Public hBalloonIcon As IntPtr\n    End Structure\n\n    <DllImport(\"shell32.dll\", CharSet:=CharSet.Unicode)>\n    Private Shared Function Shell_NotifyIcon(dwMessage As Integer, ByRef lpdata As NOTIFYICONDATA) As Boolean\n    End Function\n\n    Private m_data As NOTIFYICONDATA\n    Private m_icon As Icon\n\n    Public Sub New(window As Window, icon As Icon, tooltip As String)\n\n        Application.Current.Dispatcher.Invoke(\n            Sub()\n\n                Dim helper As New WindowInteropHelper(window)\n                m_icon = icon\n                m_data = New NOTIFYICONDATA()\n                m_data.cbSize = Marshal.SizeOf(m_data)\n                m_data.hWnd = helper.Handle\n                m_data.uID = 1\n                m_data.uFlags = NIF_MESSAGE Or NIF_ICON Or NIF_TIP\n                m_data.uCallbackMessage = 0\n                m_data.hIcon = icon.Handle\n                m_data.szTip = tooltip\n                m_data.szInfo = \"\"\n                m_data.szInfoTitle = \"\"\n                m_data.dwInfoFlags = 0\n                m_data.guidItem = Guid.Empty\n            End Sub)\n\n\n    End Sub\n\n    Public Sub ShowBalloon(title As String, message As String, Optional infoFlags As Integer = 0)\n\n        m_data.uFlags = NIF_INFO Or NIF_ICON Or NIF_TIP\n        m_data.szInfoTitle = title\n        m_data.szInfo = message\n        m_data.dwInfoFlags = infoFlags ' 1=Info, 2=Warning, 3=Error\n\n        Shell_NotifyIcon(NIM_ADD, m_data)\n        Shell_NotifyIcon(NIM_DELETE, m_data)\n    End Sub\n\n    Public Sub Dispose()\n        Shell_NotifyIcon(NIM_DELETE, m_data)\n        If m_icon IsNot Nothing Then\n            m_icon.Dispose()\n        End If\n    End Sub\n\n    Public Sub Notify_Compressed(DisplayName As String, BytesSaved As Long, CompressionRatio As Decimal)\n\n        Dim title = $\"{DisplayName}\"\n        Dim readableSaved = $\"{New BytesToReadableConverter().Convert(BytesSaved, GetType(Long), Nothing, Globalization.CultureInfo.CurrentCulture)} saved\"\n        Dim percentCompressed = $\"{100 - CInt(CompressionRatio * 100)}% smaller\"\n        ShowBalloon(title, $\"▸ {readableSaved}{Environment.NewLine}▸ {percentCompressed}\")\n\n    End Sub\n\n    Public Sub Notify_BackgroundSchedulerRunning()\n        Dim title = \"Scheduled Compression Running\"\n        Dim message = \"CompactGUI is running a scheduled task and will compress monitored folders in the background\"\n        ShowBalloon(title, message)\n    End Sub\n\n    Public Sub Notify_BackgroundSchedulerCompleted()\n        Dim title = \"Scheduled Compression Completed\"\n        Dim message = $\"Next scheduled task is on {Application.GetService(Of ISettingsService).AppSettings.NextScheduledBackgroundRun}\"\n        ShowBalloon(title, message)\n    End Sub\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Services/UpdaterService.vb",
    "content": "﻿Imports System.Net.Http\nImports System.Text.Json\n\nImports Wpf.Ui.Controls\n\nPublic Interface IUpdaterService\n    Function CheckForUpdate(includePrerelease As Boolean) As Task\nEnd Interface\n\nPublic Class UpdaterService : Implements IUpdaterService\n\n    Public Shared ReadOnly UpdateURL As String = \"https://raw.githubusercontent.com/IridiumIO/CompactGUI/database/version.json\"\n    Public Shared ReadOnly httpClient As New HttpClient()\n\n\n    Public Async Function CheckForUpdate(includePrerelease As Boolean) As Task Implements IUpdaterService.CheckForUpdate\n        Try\n            Dim ret = Await httpClient.GetStringAsync(UpdateURL)\n            Dim jVer = JsonSerializer.Deserialize(Of Dictionary(Of String, SemVersion))(ret)\n            Dim newVersion As SemVersion = If(includePrerelease, jVer(\"Latest\"), jVer(\"LatestNonPreRelease\"))\n            If newVersion > Application.AppVersion Then\n                Application.GetService(Of CustomSnackBarService).ShowUpdateAvailable(newVersion.Friendly, newVersion.IsPreRelease)\n            End If\n        Catch ex As Exception\n            Debug.WriteLine(ex.Message)\n        End Try\n    End Function\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Services/WikiService.vb",
    "content": "﻿Imports System.Net.Http\nImports System.Text.Json\n\nImports CompactGUI.Core.Settings\n\nPublic Interface IWikiService\n    Function GetUpdatedJSONAsync() As Task\n    Function ParseData(appid As Integer) As Task(Of (estimatedRatio As Decimal, confidence As Integer, poorlyCompressedList As Dictionary(Of String, Integer), compressionResults As List(Of CompressionResult)))\n    Function GetAllDatabaseCompressionResultsAsync() As Task(Of List(Of DatabaseCompressionResult))\n    Function SubmitToWiki(folderpath As String, analysisResults As List(Of Core.AnalysedFileDetails), poorlyCompressedFiles As List(Of Core.ExtensionResult), compressionMode As Integer) As Task(Of Boolean)\n    Function SubmitURLForm(url As String, submissionstring As String) As Task(Of Boolean)\nEnd Interface\n\nPublic Class WikiService : Implements IWikiService\n\n    Private ReadOnly filePath As String\n    Private ReadOnly dlPath As String\n\n    Private ReadOnly _settingsService As ISettingsService\n\n    Public Sub New(settingsService As ISettingsService)\n        _settingsService = settingsService\n        filePath = IO.Path.Combine(_settingsService.DataFolder.FullName, \"databasev2.json\")\n\n        dlPath = \"https://raw.githubusercontent.com/IridiumIO/CompactGUI/database/database.json\"\n\n    End Sub\n\n    Async Function GetUpdatedJSONAsync() As Task Implements IWikiService.GetUpdatedJSONAsync\n        Debug.WriteLine(\"Updating JSON file\")\n        Dim JSONFile As New IO.FileInfo(filePath)\n\n        If JSONFile.Exists AndAlso _settingsService.AppSettings.ResultsDBLastUpdated.AddHours(6) >= DateTime.Now Then Return\n\n        Dim httpClient As New HttpClient\n\n        Try\n\n            Dim res = Await httpClient.GetStreamAsync(dlPath)\n\n            Using fs As New IO.FileStream(JSONFile.FullName, IO.FileMode.Create)\n                Await res.CopyToAsync(fs)\n            End Using\n\n        Catch ex As TaskCanceledException\n            Debug.WriteLine(\"HTTP request timed out.\")\n            Return\n\n        Catch ex As IO.IOException\n            Debug.WriteLine(\"Could not update JSON file: file is in use.\")\n            Return\n        Catch ex As HttpRequestException\n            Debug.WriteLine($\"Unable to reach endpoint. Likely no internet connection\")\n            Return\n        Finally\n            httpClient.Dispose()\n        End Try\n\n\n        _settingsService.AppSettings.ResultsDBLastUpdated = DateTime.Now\n        _settingsService.SaveSettings()\n        Debug.WriteLine(\"Updated JSON file\")\n\n    End Function\n\n    Private ReadOnly JsonDefaultSettings As New JsonSerializerOptions With {.IncludeFields = True}\n\n    Async Function ParseData(appid As Integer) As Task(Of (estimatedRatio As Decimal, confidence As Integer, poorlyCompressedList As Dictionary(Of String, Integer), compressionResults As List(Of CompressionResult))) Implements IWikiService.ParseData\n        Dim JSONFile As New IO.FileInfo(filePath)\n        If Not JSONFile.Exists Then Return Nothing\n\n        Dim jStream As IO.FileStream = JSONFile.OpenRead\n        Dim parsedSteamWikiResults = Await JsonSerializer.DeserializeAsync(Of List(Of SteamResultsData))(jStream, JsonDefaultSettings).ConfigureAwait(False)\n        Dim workingGame = parsedSteamWikiResults.Find(Function(game) game.SteamID = appid)\n\n        If workingGame Is Nothing Then Return Nothing\n        Dim estimatedRatio As Decimal\n        Dim totaldataPoints As Integer = workingGame.CompressionResults.Sum(Function(x) x.TotalResults)\n\n        For Each compressionResult In workingGame.CompressionResults\n            Dim ratio = compressionResult.AfterBytes / compressionResult.BeforeBytes\n            estimatedRatio += ratio * compressionResult.TotalResults\n        Next\n\n        workingGame.CompressionResults.Sort(Function(x, y) x.CompType.CompareTo(y.CompType))\n\n        'TODO: Adjust this return to account for selected level of aggressiveness in settings\n        'Dim poorlyCompressedExt = workingGame.PoorlyCompressedExtensions.Where(Function(k) k.Value > 100).Select(Function(k) k.Key)\n\n        estimatedRatio /= totaldataPoints\n        Return (estimatedRatio, workingGame.Confidence, workingGame.PoorlyCompressedExtensions, workingGame.CompressionResults)\n\n    End Function\n\n\n    Public Async Function GetAllDatabaseCompressionResultsAsync() As Task(Of List(Of DatabaseCompressionResult)) Implements IWikiService.GetAllDatabaseCompressionResultsAsync\n        Dim JSONFile As New IO.FileInfo(filePath)\n        If Not JSONFile.Exists Then Return New List(Of DatabaseCompressionResult)()\n\n        Using jStream As IO.FileStream = JSONFile.OpenRead()\n            ' Deserialize the JSON into a list of SteamResultsData (or your source model)\n            Dim parsedResults = Await JsonSerializer.DeserializeAsync(Of List(Of SteamResultsData))(jStream, JsonDefaultSettings).ConfigureAwait(False)\n            If parsedResults Is Nothing Then Return New List(Of DatabaseCompressionResult)()\n\n            ' Map each SteamResultsData to DatabaseCompressionResult\n            Dim results As New List(Of DatabaseCompressionResult)\n            For Each item In parsedResults\n                Dim dbResult As New DatabaseCompressionResult With {\n                .GameName = item.GameName,\n                .SteamID = item.SteamID,\n                .Confidence = CType(item.Confidence, DBResultConfidence),\n                .Result_X4K = item.CompressionResults.FirstOrDefault(Function(r) r.CompType = 0),\n                .Result_X8K = item.CompressionResults.FirstOrDefault(Function(r) r.CompType = 1),\n                .Result_X16K = item.CompressionResults.FirstOrDefault(Function(r) r.CompType = 2),\n                .Result_LZX = item.CompressionResults.FirstOrDefault(Function(r) r.CompType = 3),\n                .PoorlyCompressedExtensions = item.PoorlyCompressedExtensions?.Select(Function(kvp) New DBPoorlyCompressedExtension With {.Extension = kvp.Key, .Count = kvp.Value}).ToList()\n            }\n                results.Add(dbResult)\n            Next\n\n            Return results\n        End Using\n    End Function\n\n\n\n\n\n    Async Function SubmitToWiki(folderpath As String, analysisResults As List(Of Core.AnalysedFileDetails), poorlyCompressedFiles As List(Of Core.ExtensionResult), compressionMode As Integer) As Task(Of Boolean) Implements IWikiService.SubmitToWiki\n        Dim wikiSubmitURI = \"https://docs.google.com/forms/d/e/1FAIpQLSdQyMwHIfldsuKKdDYBE9DNEyro8bidBDInq8EafGogFu382A/formResponse?entry.1019946248=%3CCompactGUI3%3E\"\n\n        Dim ret = Await Task.Run(Function() GetSteamNameAndIDFromFolder(folderpath))\n\n        Dim before = analysisResults.Sum(Function(res) res.UncompressedSize)\n        Dim after = analysisResults.Sum(Function(res) res.CompressedSize)\n\n        Dim steamsubmitdata As New SteamSubmissionData With {\n            .UID = getUID(),\n            .SteamID = ret.appID,\n            .GameName = ret.gameName,\n            .FolderName = ret.installDir,\n            .BeforeBytes = before,\n            .AfterBytes = after,\n            .CompressionMode = compressionMode,\n            .PoorlyCompressedExt = poorlyCompressedFiles}\n\n        Dim jstring = JsonSerializer.Serialize(steamsubmitdata)\n        Dim response = Await SubmitURLForm(wikiSubmitURI, jstring)\n\n        Dim snackbar = Application.GetService(Of CustomSnackBarService)()\n        If Not response Then\n            snackbar.ShowFailedToSubmitToWiki()\n            Return False\n        End If\n\n        snackbar.ShowSubmittedToWiki(steamsubmitdata, compressionMode)\n        Return True\n\n    End Function\n\n\n    Async Function SubmitURLForm(url As String, submissionstring As String) As Task(Of Boolean) Implements IWikiService.SubmitURLForm\n        Try\n\n            Dim httpC As New HttpClient\n            Dim resp = Await httpC.GetAsync(New Uri(url & submissionstring))\n            Return resp.StatusCode\n        Catch ex As Exception\n            Return 0\n        End Try\n\n    End Function\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Services/WindowService.vb",
    "content": "﻿Public Interface IWindowService\n    Sub ShowMainWindow()\n    Sub MinimizeMainWindow()\n    Sub HideMainWindow()\n    Function ShowMessageBox(title As String, content As String) As Task(Of Boolean)\nEnd Interface\n\n\nPublic Class WindowService\n    Implements IWindowService\n\n    Public Sub ShowMainWindow() Implements IWindowService.ShowMainWindow\n        Dim mainWindow = Application.GetService(Of MainWindow)()\n        mainWindow.Show()\n        mainWindow.WindowState = WindowState.Normal\n        mainWindow.Topmost = True\n        mainWindow.Activate()\n        mainWindow.Topmost = False\n    End Sub\n\n    Public Sub MinimizeMainWindow() Implements IWindowService.MinimizeMainWindow\n        Dim mainWindow = Application.GetService(Of MainWindow)()\n        mainWindow.WindowState = WindowState.Minimized\n    End Sub\n\n    Public Sub HideMainWindow() Implements IWindowService.HideMainWindow\n        Dim mainWindow = Application.GetService(Of MainWindow)()\n        mainWindow.Hide()\n    End Sub\n\n    Public Async Function ShowMessageBox(title As String, content As String) As Task(Of Boolean) Implements IWindowService.ShowMessageBox\n        Dim msgBox = New Wpf.Ui.Controls.MessageBox With {\n               .Title = title,\n               .Content = content,\n               .IsPrimaryButtonEnabled = True,\n               .PrimaryButtonText = \"Yes\",\n               .CloseButtonText = \"Cancel\"\n           }\n        Dim result = Await msgBox.ShowDialogAsync()\n        Return result = Wpf.Ui.Controls.MessageBoxResult.Primary\n    End Function\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Themes/Generic.xaml",
    "content": "<ResourceDictionary\n    xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n    xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n    xmlns:local=\"clr-namespace:CompactGUI\">\n\n\n    <Style TargetType=\"{x:Type local:TokenizedTextBox}\">\n        <Setter Property=\"Template\">\n            <Setter.Value>\n                <ControlTemplate TargetType=\"{x:Type local:TokenizedTextBox}\">\n                    <Border Background=\"{TemplateBinding Background}\"\n                            BorderBrush=\"{TemplateBinding BorderBrush}\"\n                            BorderThickness=\"{TemplateBinding BorderThickness}\">\n                    </Border>\n                </ControlTemplate>\n            </Setter.Value>\n        </Setter>\n    </Style>\n</ResourceDictionary>\n"
  },
  {
    "path": "CompactGUI/ViewModels/DatabaseViewModel.vb",
    "content": "﻿Imports System.Collections.ObjectModel\nImports System.ComponentModel\n\nImports CommunityToolkit.Mvvm.ComponentModel\nImports CommunityToolkit.Mvvm.Input\n\nImports CompactGUI.Core.Settings\n\nPublic Class DatabaseViewModel : Inherits ObservableObject\n\n    <ObservableProperty>\n    Private _DatabaseResults As ObservableCollection(Of DatabaseCompressionResult)\n\n    <ObservableProperty>\n    Private _searchText As String\n\n    Public ReadOnly Property FilteredResults As ICollectionView\n\n    Public ReadOnly Property DatabaseGamesCount As Integer\n        Get\n            Return DatabaseResults.Count\n        End Get\n    End Property\n\n    Public ReadOnly Property DatabaseSubmissionsCount As Integer\n        Get\n            Return DatabaseResults.Sum(Function(result) _\n                    (If(result.Result_X4K?.TotalResults, 0)) +\n                    (If(result.Result_X8K?.TotalResults, 0)) +\n                    (If(result.Result_X16K?.TotalResults, 0)) +\n                    (If(result.Result_LZX?.TotalResults, 0))\n                    )\n        End Get\n    End Property\n\n    Public ReadOnly Property LastUpdatedDatabase As DateTime\n        Get\n            Return _SettingsService.AppSettings.ResultsDBLastUpdated\n        End Get\n    End Property\n\n\n    Private ReadOnly _SettingsService As ISettingsService\n\n    Public Sub New(settingsService As ISettingsService, wikiService As IWikiService)\n\n        _SettingsService = settingsService\n        DatabaseResults = New ObservableCollection(Of DatabaseCompressionResult)(wikiService.GetAllDatabaseCompressionResultsAsync().GetAwaiter.GetResult)\n        FilteredResults = CollectionViewSource.GetDefaultView(DatabaseResults)\n        FilteredResults.Filter = AddressOf FilterResults\n    End Sub\n\n\n    Private Sub OnSearchTextChanged(value As String)\n        FilteredResults.Refresh()\n    End Sub\n\n    Private Function NormalizeString(input As String) As String\n        If String.IsNullOrEmpty(input) Then Return String.Empty\n        Return New String(input.Where(Function(c) Char.IsLetterOrDigit(c) OrElse Char.IsWhiteSpace(c)).ToArray()).ToLowerInvariant()\n    End Function\n\n    Private Function FilterResults(obj As Object) As Boolean\n        If String.IsNullOrWhiteSpace(SearchText) Then Return True\n        Dim item = TryCast(obj, DatabaseCompressionResult)\n        If item Is Nothing OrElse item.GameName Is Nothing Then Return False\n\n        ' Normalize  GameName for punctuation-insensitive search\n        Dim normalizedGameName = NormalizeString(item.GameName)\n\n        Return (item.GameName.IndexOf(SearchText, StringComparison.OrdinalIgnoreCase) >= 0) OrElse\n           (normalizedGameName.Contains(SearchText)) OrElse\n           (item.SteamID.ToString().Contains(SearchText))\n    End Function\n\n\n    <RelayCommand>\n    Private Sub SortResults(param As Object)\n        Dim sortOption = param?.ToString()\n        FilteredResults.SortDescriptions.Clear()\n\n        Select Case sortOption\n            Case \"GameNameAsc\"\n                FilteredResults.SortDescriptions.Add(New SortDescription(\"GameName\", ListSortDirection.Ascending))\n            Case \"GameNameDesc\"\n                FilteredResults.SortDescriptions.Add(New SortDescription(\"GameName\", ListSortDirection.Descending))\n            Case \"SteamIDAsc\"\n                FilteredResults.SortDescriptions.Add(New SortDescription(\"SteamID\", ListSortDirection.Ascending))\n            Case \"SteamIDDesc\"\n                FilteredResults.SortDescriptions.Add(New SortDescription(\"SteamID\", ListSortDirection.Descending))\n            Case \"MaxSavingsAsc\"\n                FilteredResults.SortDescriptions.Add(New SortDescription(\"MaxSavings\", ListSortDirection.Ascending))\n            Case \"MaxSavingsDesc\"\n                FilteredResults.SortDescriptions.Add(New SortDescription(\"MaxSavings\", ListSortDirection.Descending))\n        End Select\n    End Sub\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/ViewModels/FolderViewModel.vb",
    "content": "﻿\nImports System.ComponentModel\n\nImports CommunityToolkit.Mvvm.ComponentModel\nImports CommunityToolkit.Mvvm.Input\n\nImports CompactGUI.Core.Settings\n\nImports Wpf.Ui.Controls\n\n\nPublic NotInheritable Class FolderViewModel : Inherits ObservableObject : Implements IDisposable\n\n    <ObservableProperty>\n    Private _Folder As CompressableFolder\n\n    <ObservableProperty>\n    Private _CompressionProgress As Integer\n\n    <ObservableProperty>\n    Private _CompressionProgressFile As String\n\n    <ObservableProperty>\n    Private _AlwaysShowDetailsCompressionMode As Boolean = False\n\n    Private ReadOnly _watcher As Watcher.Watcher\n    Private ReadOnly _snackbarService As CustomSnackBarService\n    Private ReadOnly _compressableFolderService As CompressableFolderService\n\n    Public Sub New(folder As CompressableFolder, watcher As Watcher.Watcher, snackbarService As CustomSnackBarService, compressableFolderService As CompressableFolderService)\n        Me.Folder = folder\n        _watcher = watcher\n        _snackbarService = snackbarService\n        _compressableFolderService = compressableFolderService\n        AddHandler folder.PropertyChanged, AddressOf OnFolderPropertyChanged\n        AddHandler folder.CompressionOptions.PropertyChanged, AddressOf OnFolderCompressionOptionsPropertyChanged\n        AddHandler Application.GetService(Of Core.Settings.ISettingsService).AppSettings.PropertyChanged, AddressOf OnAppSettingsPropertyChanged\n    End Sub\n\n    Private Sub OnAppSettingsPropertyChanged(sender As Object, e As PropertyChangedEventArgs)\n        If e.PropertyName Is NameOf(Core.Settings.Settings.AlwaysShowDetailedCompressionMode) Then\n            AlwaysShowDetailsCompressionMode = Application.GetService(Of Core.Settings.ISettingsService).AppSettings.AlwaysShowDetailedCompressionMode\n        End If\n    End Sub\n\n    Public ReadOnly Property IsAnalysing As Boolean\n        Get\n            Return Folder?.FolderActionState = ActionState.Analysing\n        End Get\n    End Property\n\n    Public ReadOnly Property IsNotResultsOrAnalysing As Boolean\n        Get\n            Return Folder?.FolderActionState <> ActionState.Results AndAlso Not IsAnalysing\n        End Get\n    End Property\n\n    Public ReadOnly Property CompressionDisplayLevel As String\n        Get\n            If Folder.AnalysisResults Is Nothing OrElse\n                Not Folder.AnalysisResults.Any(Function(x) x.CompressionMode <> Core.WOFCompressionAlgorithm.NO_COMPRESSION) Then\n                Return \"Not Compressed\"\n            End If\n            Return \"Compressed\"\n        End Get\n    End Property\n\n    Public ReadOnly Property TotalCompressedFiles As Integer\n        Get\n            Return Folder.AnalysisResults.Where(Function(x) x.CompressionMode <> Core.WOFCompressionAlgorithm.NO_COMPRESSION).Count()\n        End Get\n    End Property\n\n    Public ReadOnly Property TotalFiles As Integer\n        Get\n            Return Folder.AnalysisResults.Count\n        End Get\n    End Property\n\n    Public ReadOnly Property DominantCompressionMode As Core.WOFCompressionAlgorithm\n        Get\n            Return Folder?.AnalysisResults _\n            .Where(Function(x) x.CompressionMode <> Core.WOFCompressionAlgorithm.NO_COMPRESSION) _\n            .GroupBy(Function(x) x.CompressionMode) _\n            .OrderByDescending(Function(g) g.Count()) _\n            .Select(Function(g) g.Key) _\n            .FirstOrDefault()\n        End Get\n    End Property\n\n    Public ReadOnly Property DisplayedFolderAfterSize As Long\n        Get\n            If TypeOf (Folder) Is SteamFolder AndAlso (Folder.FolderActionState = ActionState.Idle OrElse Folder.FolderActionState = ActionState.Working) Then\n                Dim working = CType(Folder, SteamFolder)\n                If working.WikiCompressionResults Is Nothing Then Return Folder.CompressedBytes\n                Select Case working.CompressionOptions.SelectedCompressionMode\n                    Case Core.CompressionMode.XPRESS4K\n                        Return CLng(working.WikiCompressionResults.XPress4K?.CompressionPercent / 100 * working.UncompressedBytes)\n                    Case Core.CompressionMode.XPRESS8K\n                        Return CLng(working.WikiCompressionResults.XPress8K?.CompressionPercent / 100 * working.UncompressedBytes)\n                    Case Core.CompressionMode.XPRESS16K\n                        Return CLng(working.WikiCompressionResults.XPress16K?.CompressionPercent / 100 * working.UncompressedBytes)\n                    Case Core.CompressionMode.LZX\n                        Return CLng(working.WikiCompressionResults.LZX?.CompressionPercent / 100 * working.UncompressedBytes)\n                End Select\n\n\n            End If\n            Return Folder.CompressedBytes\n        End Get\n    End Property\n\n\n    Public ReadOnly Property IsSteamIDVisible\n        Get\n            Return TypeOf Folder Is SteamFolder\n        End Get\n    End Property\n\n    Private Sub OnFolderCompressionOptionsPropertyChanged(sender As Object, e As PropertyChangedEventArgs)\n        Dim compressionOptions = CType(sender, CompressionOptions)\n        If e.PropertyName = NameOf(compressionOptions.SelectedCompressionMode) Then\n            OnPropertyChanged(NameOf(DisplayedFolderAfterSize))\n        End If\n    End Sub\n\n    Private Sub OnFolderPropertyChanged(sender As Object, e As PropertyChangedEventArgs)\n        If e.PropertyName = NameOf(Folder.FolderActionState) Then\n            OnPropertyChanged(NameOf(IsAnalysing))\n            OnPropertyChanged(NameOf(IsNotResultsOrAnalysing))\n            OnPropertyChanged(NameOf(CompressionDisplayLevel))\n            OnPropertyChanged(NameOf(DisplayedFolderAfterSize))\n            OnPropertyChanged(NameOf(TotalFiles))\n\n        ElseIf e.PropertyName = NameOf(Folder.CompressionProgress) Then\n            CompressionProgress = Folder.CompressionProgress.ProgressPercent\n            CompressionProgressFile = Folder.CompressionProgress.FileName.Replace(Folder.FolderName, \"\")\n\n        End If\n    End Sub\n\n\n    <RelayCommand>\n    Private Sub CompressAgain()\n        Folder.FolderActionState = ActionState.Idle\n    End Sub\n\n    <RelayCommand>\n    Private Async Function Uncompress() As Task\n        Await _compressableFolderService.UncompressFolder(Folder)\n        _watcher.UpdateWatched(Folder.FolderName, Folder.Analyser, False)\n    End Function\n\n    <RelayCommand>\n    Private Sub ApplyToAll()\n        Dim allFolders = Application.GetService(Of HomeViewModel)().Folders\n\n        For Each fl In allFolders.Where(Function(f) f.FolderActionState <> ActionState.Analysing AndAlso f.FolderActionState <> ActionState.Working AndAlso f.FolderActionState <> ActionState.Paused)\n            If fl IsNot Folder Then\n                fl.CompressionOptions = Folder.CompressionOptions.Clone\n                fl.FolderActionState = ActionState.Idle\n            End If\n        Next\n\n        _snackbarService.ShowAppliedToAllFolders()\n    End Sub\n\n    <RelayCommand>\n    Private Sub Pause()\n\n        If Folder.FolderActionState = ActionState.Working Then\n            Folder.Compressor?.Pause()\n            Folder.FolderActionState = ActionState.Paused\n        Else\n            Folder.Compressor?.Resume()\n            Folder.FolderActionState = ActionState.Working\n\n        End If\n    End Sub\n\n    <RelayCommand>\n    Private Sub Cancel()\n        Folder.Compressor?.Cancel()\n    End Sub\n\n    <RelayCommand>\n    Private Async Function SubmitToWiki() As Task\n\n        SubmitToWikiCommand.NotifyCanExecuteChanged()\n\n        Dim result = Await Application.GetService(Of IWikiService).SubmitToWiki(Folder.FolderName, Folder.AnalysisResults.ToList, Folder.PoorlyCompressedFiles, Folder.CompressionOptions.SelectedCompressionMode)\n\n        Folder.IsFreshlyCompressed = False\n        SubmitToWikiCommand.NotifyCanExecuteChanged()\n    End Function\n    Private Function CanSubmitToWiki() As Boolean\n        Return TypeOf (Folder) _\n            Is SteamFolder AndAlso\n            Folder.IsFreshlyCompressed AndAlso\n            Not Folder.CompressionOptions.SkipPoorlyCompressedFileTypes AndAlso\n            Not Folder.CompressionOptions.SkipUserSubmittedFiletypes\n    End Function\n\n\n\n\n\n\n    Public Sub Dispose() Implements IDisposable.Dispose\n        RemoveHandler Folder.PropertyChanged, AddressOf OnFolderPropertyChanged\n        RemoveHandler Folder.CompressionOptions.PropertyChanged, AddressOf OnFolderCompressionOptionsPropertyChanged\n    End Sub\n\n\n\nEnd Class\n\n"
  },
  {
    "path": "CompactGUI/ViewModels/HomeViewModel.vb",
    "content": "﻿Imports System.Collections.ObjectModel\nImports System.Collections.Specialized\nImports System.ComponentModel\n\nImports CommunityToolkit.Mvvm.ComponentModel\nImports CommunityToolkit.Mvvm.Input\nImports CommunityToolkit.Mvvm.Messaging\n\nImports CompactGUI.Core.Settings\n\nImports CompactGUI.Core.SharedMethods\nImports CompactGUI.Logging\n\nImports Microsoft.Extensions.Logging\n\nPartial Public NotInheritable Class HomeViewModel : Inherits ObservableRecipient : Implements IRecipient(Of WatcherAddedFolderToQueueMessage)\n\n    Private ReadOnly _folderViewModels As New Dictionary(Of CompressableFolder, FolderViewModel)\n\n    <ObservableProperty>\n    Private _Folders As ObservableCollection(Of CompressableFolder) = New ObservableCollection(Of CompressableFolder)\n\n    <ObservableProperty>\n    <NotifyPropertyChangedFor(NameOf(SelectedFolderViewModel))>\n    <NotifyPropertyChangedRecipients>\n    Private _SelectedFolder As CompressableFolder\n\n    Public ReadOnly Property SelectedFolderViewModel As FolderViewModel\n        Get\n            If SelectedFolder Is Nothing Then Return Nothing\n\n            Dim value As FolderViewModel = Nothing\n            Return If(_folderViewModels.TryGetValue(SelectedFolder, value), value, Nothing)\n\n        End Get\n    End Property\n\n    Public ReadOnly Property HomeViewIsFresh As Boolean\n        Get\n            Return Not Folders.Any()\n        End Get\n    End Property\n\n    Public ReadOnly Property DisplayVersion As String\n        Get\n            Return Application.AppVersion.Friendly\n        End Get\n    End Property\n\n    Public ReadOnly Property IsAdmin As Boolean\n        Get\n            Dim principal = New Security.Principal.WindowsPrincipal(Security.Principal.WindowsIdentity.GetCurrent())\n            Return principal.IsInRole(Security.Principal.WindowsBuiltInRole.Administrator)\n        End Get\n    End Property\n\n\n\n    Private ReadOnly _watcher As Watcher.Watcher\n    Private ReadOnly _snackbarService As CustomSnackBarService\n    Private ReadOnly _logger As ILogger(Of HomeViewModel)\n    Private ReadOnly _settingsService As ISettingsService\n    Private ReadOnly _compressableFolderService As CompressableFolderService\n\n    Sub New(watcher As Watcher.Watcher, snackbarService As CustomSnackBarService, logger As ILogger(Of HomeViewModel), settingsService As ISettingsService, compressableFolderService As CompressableFolderService)\n        WeakReferenceMessenger.Default.Register(Of WatcherAddedFolderToQueueMessage)(Me)\n        AddHandler Folders.CollectionChanged, AddressOf OnFoldersCollectionChanged\n        _watcher = watcher\n        _snackbarService = snackbarService\n        _logger = logger\n        _settingsService = settingsService\n        _compressableFolderService = compressableFolderService\n    End Sub\n\n\n    'Private Sub OnSelectedFolderChanged(value As CompressableFolder)\n\n    '    WeakReferenceMessenger.Default.Send(New BackgroundImageChangedMessage(value?.FolderBGImage))\n\n    'End Sub\n\n\n\n\n    Private Sub OnAnyFolderPropertyChanged(sender As Object, e As PropertyChangedEventArgs)\n        If e.PropertyName = NameOf(CompressableFolder.FolderActionState) Then\n            OnPropertyChanged(NameOf(HomeViewModelState))\n            Application.Current.Dispatcher.Invoke(Sub() RemoveFolderCommand.NotifyCanExecuteChanged())\n        End If\n    End Sub\n\n    Private Sub OnFoldersCollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)\n        OnPropertyChanged(NameOf(HomeViewModelState))\n        If e.Action = NotifyCollectionChangedAction.Add Then\n            For Each folder As CompressableFolder In e.NewItems\n                AddHandler folder.PropertyChanged, AddressOf OnAnyFolderPropertyChanged\n            Next\n        ElseIf e.Action = NotifyCollectionChangedAction.Remove Then\n            For Each folder As CompressableFolder In e.OldItems\n                RemoveHandler folder.PropertyChanged, AddressOf OnAnyFolderPropertyChanged\n            Next\n        End If\n\n        OnPropertyChanged(NameOf(HomeViewIsFresh))\n    End Sub\n\n\n\n    Public Async Function AddFoldersAsync(folderPaths As IEnumerable(Of String)) As Task\n\n        HomeViewModelLog.AddingFolders(_logger, folderPaths)\n\n        Dim invalidFolders = GetInvalidFolders(folderPaths.ToArray)\n        Dim validFolders = folderPaths.Except(invalidFolders.InvalidFolders)\n\n        If invalidFolders.InvalidFolders.Count > 0 Then\n            'TODO: Move this logger check to the snackbarService class?\n            HomeViewModelLog.InvalidFolders(_logger, invalidFolders.InvalidFolders, invalidFolders.InvalidMessages.Select(Function(x) GetFolderVerificationMessage(x)))\n            _snackbarService.ShowInvalidFoldersMessage(invalidFolders.InvalidFolders, invalidFolders.InvalidMessages)\n        End If\n\n        For Each folderName In validFolders\n\n            Dim newFolder As CompressableFolder = Await CompressableFolderFactory.CreateCompressableFolder(folderName)\n\n            newFolder.CompressionOptions.WatchFolderForChanges = _settingsService.AppSettings.WatchFolderForChanges\n            newFolder.CompressionOptions.SelectedCompressionMode = _settingsService.AppSettings.SelectedCompressionMode\n            newFolder.CompressionOptions.SkipPoorlyCompressedFileTypes = _settingsService.AppSettings.SkipNonCompressable\n            newFolder.CompressionOptions.SkipUserSubmittedFiletypes = _settingsService.AppSettings.SkipUserNonCompressable\n\n            If Not Folders.Any(Function(f) f.FolderName = newFolder.FolderName) Then\n                Folders.Add(newFolder)\n                Dim vm As New FolderViewModel(newFolder, _watcher, _snackbarService, _compressableFolderService)\n                _folderViewModels.Add(newFolder, vm)\n                SelectedFolder = newFolder\n            End If\n\n            Dim res = Await _compressableFolderService.AnalyseFolderAsync(newFolder)\n            If TypeOf (newFolder) Is SteamFolder Then\n                Await CType(newFolder, SteamFolder).GetWikiResults()\n            Else\n                If _settingsService.AppSettings.EstimateCompressionForNonSteamFolders Then\n                    HomeViewModelLog.GettingEstimatedCompression(_logger, newFolder.FolderName, newFolder.UncompressedBytes)\n                    Await _compressableFolderService.GetEstimatedCompression(newFolder)\n                End If\n\n            End If\n\n            If _watcher.WatchedFolders.Any(Function(w) w.Folder = newFolder.FolderName) Then\n                Dim watchedFolder = _watcher.WatchedFolders.First(Function(w) w.Folder = newFolder.FolderName)\n                newFolder.CompressionOptions.WatchFolderForChanges = True\n                If watchedFolder.CompressionLevel <> Core.WOFCompressionAlgorithm.NO_COMPRESSION Then\n                    newFolder.CompressionOptions.SelectedCompressionMode = Core.WOFHelper.CompressionModeFromWOFMode(watchedFolder.CompressionLevel)\n                End If\n\n            End If\n\n\n\n        Next\n\n\n    End Function\n\n\n\n\n    <RelayCommand>\n    Public Sub RemoveFolder(folder As CompressableFolder)\n        If Not CanRemoveFolder() Then\n            Application.GetService(Of CustomSnackBarService)().ShowCannotRemoveFolder()\n            Return\n        End If\n\n        If folder Is Nothing Then Return\n        Dim index = Folders.IndexOf(folder)\n        _compressableFolderService.CancelEstimation(folder)\n        folder.Dispose()\n\n        Dim value As FolderViewModel = Nothing\n\n        If _folderViewModels.TryGetValue(folder, value) Then\n            value.Dispose()\n            _folderViewModels.Remove(folder)\n        End If\n\n        Folders.Remove(folder)\n\n        If SelectedFolder IsNot Nothing OrElse Folders.Count = 0 Then Return\n        SelectedFolder = If(index < Folders.Count, Folders(index), Folders.Last())\n    End Sub\n\n    Public Function CanRemoveFolder() As Boolean\n        Return HomeViewModelState = ActionState.Results OrElse HomeViewModelState = ActionState.Idle\n    End Function\n\n\n    Public Sub NotifyPropertyChanged(propertyName As String)\n        OnPropertyChanged(propertyName)\n    End Sub\n\n    Public ReadOnly Property HomeViewModelState As ActionState\n        Get\n\n            Dim retState As ActionState\n\n            If Compressing OrElse Folders.Any(Function(f) f.FolderActionState = ActionState.Working OrElse f.FolderActionState = ActionState.Paused) Then\n                retState = ActionState.Working\n            ElseIf Folders.Any(Function(f) f.FolderActionState = ActionState.Analysing) Then\n                retState = ActionState.Analysing\n            ElseIf Folders.All(Function(f) f.FolderActionState = ActionState.Results) Then\n                retState = ActionState.Results\n            Else\n                retState = ActionState.Idle\n            End If\n\n            Return retState\n\n        End Get\n\n    End Property\n\n\n    <ObservableProperty>\n    <NotifyPropertyChangedFor(NameOf(HomeViewModelState))>\n    Private _Compressing As Boolean = False\n\n\n\n\n    <RelayCommand>\n    Private Async Function CompressAll() As Task\n\n        Await _watcher.DisableBackgrounding()\n\n        Compressing = True\n        Core.SharedMethods.PreventSleep()\n        Dim tasks As New List(Of Task)()\n        Dim foldersToCompress = Folders.Where(Function(f) f.FolderActionState = ActionState.Idle).ToList\n        HomeViewModelLog.StartingBatchCompression(_logger, foldersToCompress.Count)\n        For Each folder In foldersToCompress\n            If folder.FolderActionState = ActionState.Idle Then\n                Await Task.Run(Async Function()\n                                   HomeViewModelLog.CompressingFolder(_logger, folder.FolderName)\n                                   Dim ret = Await _compressableFolderService.CompressFolder(folder)\n                                   Dim analysis = Await _compressableFolderService.AnalyseFolderAsync(folder)\n\n                                   If _settingsService.AppSettings.ShowNotifications Then\n\n                                       Application.GetService(Of TrayNotifierService).Notify_Compressed(folder.DisplayName, folder.UncompressedBytes - folder.CompressedBytes, folder.CompressionRatio)\n\n                                   End If\n\n                                   _watcher.UpdateWatched(folder.FolderName, folder.Analyser, True)\n\n                                   'For Each poorext In folder.PoorlyCompressedFiles\n                                   '    Debug.WriteLine($\"{poorext.extension} : {poorext.totalFiles} with ratio of {poorext.cRatio}\")\n                                   'Next\n\n                                   Return True\n                               End Function)\n            End If\n        Next\n        Compressing = False\n\n        For Each folder In Folders.Where(Function(f) f.CompressionOptions.WatchFolderForChanges)\n            AddOrUpdateFolderWatcher(folder)\n        Next\n\n        RemoveFolderCommand.NotifyCanExecuteChanged()\n        Core.SharedMethods.RestoreSleep()\n        Await _watcher.EnableBackgrounding()\n    End Function\n\n\n    Private Function CanCompressAll() As Boolean\n        Return HomeViewModelState <> ActionState.Working AndAlso Not Folders.Any(Function(f) f.FolderActionState = ActionState.Analysing)\n    End Function\n\n\n    Public Sub AddOrUpdateFolderWatcher(folder As CompressableFolder)\n        HomeViewModelLog.AddingFolderToWatcher(_logger, folder.FolderName)\n\n        Dim newWatched = New Watcher.WatchedFolder(folder.FolderName, folder.DisplayName)\n        newWatched.IsSteamGame = TypeOf (folder) Is SteamFolder\n        newWatched.LastCompressedSize = folder.CompressedBytes\n        newWatched.LastUncompressedSize = folder.UncompressedBytes\n        newWatched.LastCompressedDate = DateTime.Now\n        newWatched.LastCheckedDate = DateTime.Now\n        newWatched.LastCheckedSize = folder.CompressedBytes\n        newWatched.LastSystemModifiedDate = DateTime.Now\n        newWatched.CompressionLevel = If(folder.AnalysisResults.Any(), folder.AnalysisResults.Max(Function(f) f.CompressionMode), Core.WOFCompressionAlgorithm.NO_COMPRESSION)\n\n        _watcher.AddOrUpdateWatched(newWatched)\n\n    End Sub\n\n\n    Public Async Sub Receive(message As WatcherAddedFolderToQueueMessage) Implements IRecipient(Of WatcherAddedFolderToQueueMessage).Receive\n        Application.GetService(Of CustomSnackBarService).ShowAddedToQueue()\n        Await AddFoldersAsync({message.Value})\n    End Sub\nEnd Class\n"
  },
  {
    "path": "CompactGUI/ViewModels/MainWindowViewModel.vb",
    "content": "﻿\nImports CommunityToolkit.Mvvm.ComponentModel\nImports CommunityToolkit.Mvvm.Input\nImports CommunityToolkit.Mvvm.Messaging\nImports CommunityToolkit.Mvvm.Messaging.Messages\n\nImports CompactGUI.Core.Settings\n\n\nPartial Public Class MainWindowViewModel : Inherits ObservableRecipient : Implements IRecipient(Of PropertyChangedMessage(Of CompressableFolder))\n\n    <ObservableProperty>\n    Private _BackgroundImage As BitmapImage\n\n    Private ReadOnly _watcher As Watcher.Watcher\n    Private ReadOnly _windowService As IWindowService\n    Private ReadOnly _settingsService As ISettingsService\n\n    Public Sub New(windowService As IWindowService, watcher As Watcher.Watcher, settingsService As ISettingsService)\n        _watcher = watcher\n        _windowService = windowService\n        _settingsService = settingsService\n    End Sub\n\n    Public ReadOnly Property IsAdmin As Boolean\n        Get\n            Dim principal = New Security.Principal.WindowsPrincipal(Security.Principal.WindowsIdentity.GetCurrent())\n            Return principal.IsInRole(Security.Principal.WindowsBuiltInRole.Administrator)\n        End Get\n    End Property\n\n\n    <RelayCommand>\n    Private Sub NotifyIconOpen()\n        _windowService.ShowMainWindow()\n    End Sub\n\n\n    <RelayCommand>\n    Private Async Function NotifyIconExit() As Task\n        If _watcher.WatchedFolders.Count = 0 Then Application.Current.Shutdown()\n        Dim confirmed = Await _windowService.ShowMessageBox(\"CompactGUI\", $\"You currently have {_watcher.WatchedFolders.Count} folders being watched. Closing CompactGUI will stop them from being monitored.{Environment.NewLine}{Environment.NewLine}Are you sure you want to exit?\")\n        If Not confirmed Then Return\n        _watcher.WriteToFile()\n        Application.Current.Shutdown()\n    End Function\n\n\n    <RelayCommand>\n    Private Sub Closing(e As ComponentModel.CancelEventArgs)\n        If e Is Nothing Then Return\n\n        If Keyboard.Modifiers = ModifierKeys.Shift Then\n            e.Cancel = False\n            If _watcher.WatchedFolders.Count <> 0 Then _watcher.WriteToFile()\n            _settingsService.SaveSettings()\n            Application.Current.Shutdown()\n            Return\n        End If\n\n        If _watcher.WatchedFolders.Count <> 0 Then\n            e.Cancel = True\n            _windowService.MinimizeMainWindow()\n            _watcher.WriteToFile()\n            _windowService.HideMainWindow()\n        End If\n\n    End Sub\n\n\n    Public Sub Receive(message As PropertyChangedMessage(Of CompressableFolder)) Implements IRecipient(Of PropertyChangedMessage(Of CompressableFolder)).Receive\n\n        If message.Sender.GetType() IsNot GetType(HomeViewModel) Then Return\n        If message.PropertyName <> NameOf(HomeViewModel.SelectedFolder) Then Return\n        BackgroundImage = message.NewValue?.FolderBGImage\n\n    End Sub\nEnd Class\n"
  },
  {
    "path": "CompactGUI/ViewModels/SettingsViewModel.vb",
    "content": "﻿\nImports System.ComponentModel\nImports System.Xml\n\nImports CommunityToolkit.Mvvm.ComponentModel\nImports CommunityToolkit.Mvvm.Input\n\nImports CompactGUI.Core.Settings\nImports CompactGUI.Logging\n\nImports Coravel.Scheduling.Schedule\n\nImports Coravel.Scheduling.Schedule.Interfaces\n\nImports Microsoft.Extensions.Logging\n\nPublic NotInheritable Class SettingsViewModel : Inherits ObservableObject\n\n    Private ReadOnly _logger As ILogger(Of Settings)\n    Private ReadOnly _settingsService As ISettingsService\n\n    Public ReadOnly Property AppSettings As Settings\n\n\n    Public Sub New(settingsService As ISettingsService, logger As ILogger(Of Settings))\n\n        Me._logger = logger\n        _settingsService = settingsService\n        AppSettings = settingsService.AppSettings\n        AddHandler AppSettings.PropertyChanged, AddressOf SettingsPropertyChanged\n    End Sub\n\n    Public Async Function InitializeEnvironment() As Task\n\n        Await SetEnv()\n        Await ApplyContextIntegrationAsync()\n        ApplyStartMenuIntegration()\n\n    End Function\n\n    Private Shared Async Function SetEnv() As Task\n        SettingsLog.SettingEnvironmentVariables(Application.GetService(Of ILogger(Of Settings)))\n        Dim desiredValue = IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), \"IridiumIO\")\n        Dim currentValue = Environment.GetEnvironmentVariable(\"IridiumIO\", EnvironmentVariableTarget.User)\n        If currentValue <> desiredValue Then Await Task.Run(Sub() Environment.SetEnvironmentVariable(\"IridiumIO\", desiredValue, EnvironmentVariableTarget.User))\n\n    End Function\n\n    Private Async Sub SettingsPropertyChanged(sender As Object, e As PropertyChangedEventArgs)\n\n        If e.PropertyName = NameOf(Settings.IsContextIntegrated) Then\n            Await ApplyContextIntegrationAsync()\n        End If\n\n        If e.PropertyName = NameOf(Settings.IsStartMenuEnabled) Then\n            ApplyStartMenuIntegration()\n        End If\n\n        If e.PropertyName = NameOf(Settings.ScheduledBackgroundHour) OrElse e.PropertyName = NameOf(Settings.ScheduledBackgroundMinute) Then\n            Application.GetService(Of SchedulerService).RegenerateSchedule()\n        End If\n\n        Application.GetService(Of ISettingsService).SaveSettings()\n    End Sub\n\n    Public Async Function ApplyContextIntegrationAsync() As Task\n        If _settingsService.AppSettings.IsContextIntegrated Then\n            Await AddContextMenus()\n        Else\n            Await RemoveContextMenus()\n        End If\n    End Function\n\n    Public Sub ApplyStartMenuIntegration()\n        If _settingsService.AppSettings.IsStartMenuEnabled Then\n            CreateStartMenuShortcut()\n        Else\n            DeleteStartMenuShortcut()\n        End If\n    End Sub\n\n\n\n    Public Shared Sub CreateStartMenuShortcut()\n        SettingsLog.AddingStartMenuShortcut(Application.GetService(Of ILogger(Of Settings)))\n        Dim startMenuPath As String = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu)\n        Dim shortcutPath As String = IO.Path.Combine(startMenuPath, \"CompactGUI.lnk\")\n        Dim exePath As String = Environment.ProcessPath\n        CreateShortcut(shortcutPath, exePath, \"CompactGUI\", IO.Path.GetDirectoryName(exePath), exePath)\n\n    End Sub\n\n    Public Shared Sub DeleteStartMenuShortcut()\n        SettingsLog.RemovingStartMenuShortcut(Application.GetService(Of ILogger(Of Settings)))\n        Dim startMenuPath As String = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu)\n        Dim shortcutPath As String = IO.Path.Combine(startMenuPath, \"CompactGUI.lnk\")\n\n        If IO.File.Exists(shortcutPath) Then\n            IO.File.Delete(shortcutPath)\n        End If\n    End Sub\n\n\n\n    Public Shared Async Function AddContextMenus() As Task\n        SettingsLog.AddingToContextMenus(Application.GetService(Of ILogger(Of Settings)))\n        Await Task.Run(Sub()\n                           Try\n                               Microsoft.Win32.Registry.SetValue(\"HKEY_CURRENT_USER\\Software\\Classes\\Directory\\shell\\CompactGUI\", \"\", \"Compress Folder\")\n                               Microsoft.Win32.Registry.SetValue(\"HKEY_CURRENT_USER\\Software\\Classes\\Directory\\shell\\CompactGUI\", \"Icon\", Environment.ProcessPath)\n                               Microsoft.Win32.Registry.SetValue(\"HKEY_CURRENT_USER\\Software\\Classes\\Directory\\shell\\CompactGUI\\command\", \"\", Environment.ProcessPath & \" \" & \"\"\"%1\"\"\")\n                               Microsoft.Win32.Registry.SetValue(\"HKEY_CURRENT_USER\\Software\\Classes\\Directory\\Background\\shell\\CompactGUI\", \"\", \"Compress Folder\")\n                               Microsoft.Win32.Registry.SetValue(\"HKEY_CURRENT_USER\\Software\\Classes\\Directory\\Background\\shell\\CompactGUI\", \"Icon\", Environment.ProcessPath)\n                               Microsoft.Win32.Registry.SetValue(\"HKEY_CURRENT_USER\\Software\\Classes\\Directory\\Background\\shell\\CompactGUI\\command\", \"\", Environment.ProcessPath & \" \" & \"\"\"%V\"\"\")\n                               SettingsLog.AddingToContextMenusSuccess(Application.GetService(Of ILogger(Of Settings)))\n                           Catch ex As Exception\n                               SettingsLog.AddingToContextMenusFailed(Application.GetService(Of ILogger(Of Settings)), ex)\n                           End Try\n                       End Sub)\n    End Function\n\n    Public Shared Async Function RemoveContextMenus() As Task\n        SettingsLog.RemovingFromContextMenus(Application.GetService(Of ILogger(Of Settings)))\n        Await Task.Run(Sub()\n                           Try\n                               Microsoft.Win32.Registry.CurrentUser.DeleteSubKey(\"Software\\\\Classes\\\\Directory\\\\shell\\\\CompactGUI\\command\")\n                               Microsoft.Win32.Registry.CurrentUser.DeleteSubKey(\"Software\\\\Classes\\\\Directory\\\\shell\\\\CompactGUI\")\n                               Microsoft.Win32.Registry.CurrentUser.DeleteSubKey(\"Software\\\\Classes\\\\Directory\\\\Background\\\\shell\\\\CompactGUI\\command\")\n                               Microsoft.Win32.Registry.CurrentUser.DeleteSubKey(\"Software\\\\Classes\\\\Directory\\\\Background\\\\shell\\\\CompactGUI\")\n                               SettingsLog.RemovingFromContextMenusSuccess(Application.GetService(Of ILogger(Of Settings)))\n                           Catch ex As Exception\n                               SettingsLog.RemovingFromContextMenusFailed(Application.GetService(Of ILogger(Of Settings)), ex)\n                           End Try\n                       End Sub)\n    End Function\n\n    Public ReadOnly Property HourOptions As IEnumerable(Of Integer)\n        Get\n            Return Enumerable.Range(0, 24)\n        End Get\n    End Property\n    Public ReadOnly Property MinuteOptions As IEnumerable(Of Integer)\n        Get\n            Return Enumerable.Range(0, 60)\n        End Get\n    End Property\n\n    Public Property EditSkipListCommand As ICommand = New RelayCommand(Function() (New Settings_skiplistflyout).ShowDialog())\n\n\n    Public Property EnableBackgroundWatcherCommand As ICommand = New RelayCommand(Sub() AppSettings.EnableBackgroundWatcher = AppSettings.BackgroundModeSelection <> BackgroundMode.Never)\n    Public Property OpenGitHubCommand As ICommand = New RelayCommand(Sub() Process.Start(New ProcessStartInfo(\"https://github.com/IridiumIO/CompactGUI\") With {.UseShellExecute = True}))\n    Public Property OpenKoFiCommand As ICommand = New RelayCommand(Sub() Process.Start(New ProcessStartInfo(\"https://ko-fi.com/IridiumIO\") With {.UseShellExecute = True}))\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/ViewModels/WatcherViewModel.vb",
    "content": "﻿Imports System.Threading\n\nImports CommunityToolkit.Mvvm.ComponentModel\nImports CommunityToolkit.Mvvm.Input\nImports CommunityToolkit.Mvvm.Messaging\n\nImports CompactGUI.Watcher\n\nImports Wpf.Ui.Controls\n\nPublic NotInheritable Class WatcherViewModel : Inherits ObservableObject\n\n    Private ReadOnly _snackbarService As CustomSnackBarService\n    Public ReadOnly Property Watcher As Watcher.Watcher\n\n    Public Sub New(watcher As Watcher.Watcher, snackbarService As CustomSnackBarService)\n        Me.Watcher = watcher\n        _snackbarService = snackbarService\n    End Sub\n\n\n\n    <RelayCommand>\n    Public Async Function RunWatcher(token As CancellationToken) As Task\n        Await Watcher.RunWatcher(True, token)\n    End Function\n\n    <RelayCommand>\n    Public Sub CancelBackgrounding()\n        RunWatcherCommand.Cancel()\n        Watcher.BGCompactor.CancelCompacting()\n        Application.Current.Dispatcher.Invoke(Sub() CancelBackgroundingCommand.NotifyCanExecuteChanged())\n    End Sub\n\n\n    <RelayCommand>\n    Private Async Function RemoveWatcher(watchedFolder As Watcher.WatchedFolder) As Task\n        If watchedFolder Is Nothing Then Return\n        Await Application.Current.Dispatcher.InvokeAsync(Sub() Watcher.RemoveWatched(watchedFolder))\n    End Function\n\n    <RelayCommand>\n    Private Async Function RefreshWatched() As Task\n        Await Watcher.DeleteWatchersWithNonExistentFolders()\n        Await Task.Run(Function() Watcher.ParseWatchers(True))\n    End Function\n\n    <RelayCommand>\n    Private Async Function ReAnalyseWatched(watchedfolder As Watcher.WatchedFolder) As Task\n        Await Task.Run(Function() Watcher.ParseSingleWatcher(watchedfolder))\n    End Function\n\n\n\n    <RelayCommand>\n    Private Sub AddWatchedFolderToQueue(folder As Watcher.WatchedFolder)\n\n        WeakReferenceMessenger.Default.Send(New WatcherAddedFolderToQueueMessage(folder.Folder))\n    End Sub\n\n    <RelayCommand>\n    Private Async Function ManuallyAddFolderToWatcher() As Task\n\n        Dim folderSelector As New Microsoft.Win32.OpenFolderDialog\n        folderSelector.ShowDialog()\n        If folderSelector.FolderName = \"\" Then Return\n        Dim path As String = folderSelector.FolderName\n        Dim validFolder = Core.SharedMethods.VerifyFolder(path)\n        If validFolder <> Core.SharedMethods.FolderVerificationResult.Valid Then\n\n            _snackbarService.ShowInvalidFoldersMessage(New List(Of String) From {path}, New List(Of Core.SharedMethods.FolderVerificationResult) From {validFolder})\n\n            Return\n        End If\n\n        Dim newFolder = Await AddFolderAsync(path)\n\n        Dim newWatched = New Watcher.WatchedFolder(newFolder.FolderName, newFolder.DisplayName) With {\n           .IsSteamGame = TypeOf (newFolder) Is SteamFolder,\n           .LastCompressedSize = 0,\n           .LastUncompressedSize = 0,\n           .LastCompressedDate = DateTime.UnixEpoch,\n           .LastCheckedDate = DateTime.UnixEpoch,\n           .LastCheckedSize = 0,\n           .LastSystemModifiedDate = DateTime.UnixEpoch,\n           .CompressionLevel = Core.WOFCompressionAlgorithm.NO_COMPRESSION}\n\n        Watcher.AddOrUpdateWatched(newWatched)\n        Await Watcher.Analyse(path, True)\n\n    End Function\n\n\n    Public Async Function AddFolderAsync(folderPath As String) As Task(Of CompressableFolder)\n\n        If GetInvalidFolders({folderPath}).InvalidFolders.Count > 0 Then\n            Dim msgError As New ContentDialog With {.Title = \"Invalid Folder\", .Content = $\"{folderPath}\", .CloseButtonText = \"OK\"}\n            Await msgError.ShowAsync()\n            Return Nothing\n        End If\n\n        Return Await CompressableFolderFactory.CreateCompressableFolder(folderPath)\n\n    End Function\n\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/Components/CompressionMode_Radio.xaml",
    "content": "﻿<RadioButton x:Class=\"CompressionMode_Radio\"\n             xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:i=\"http://schemas.microsoft.com/xaml/behaviors\"\n             xmlns:local=\"clr-namespace:CompactGUI\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n             x:Name=\"Root\"\n             Width=\"185\" Height=\"85\"\n             mc:Ignorable=\"d\">\n\n\n    <RadioButton.Template>\n        <ControlTemplate TargetType=\"RadioButton\">\n            <ui:Card x:Name=\"CheckMark\"\n                     Padding=\"15,10,15,5\" VerticalAlignment=\"Stretch\" VerticalContentAlignment=\"Stretch\"\n                     Background=\"#20FFFFFF\"\n                     BorderBrush=\"{TemplateBinding BorderBrush}\"\n                     BorderThickness=\"1\" Visibility=\"Visible\">\n                <Grid>\n                    <Grid.RowDefinitions>\n                        <RowDefinition Height=\"*\" />\n                        <RowDefinition Height=\"10\" />\n                    </Grid.RowDefinitions>\n\n                    <Grid.ColumnDefinitions>\n                        <ColumnDefinition />\n                        <ColumnDefinition Width=\"65\" />\n                    </Grid.ColumnDefinitions>\n\n\n                    <ui:TextBlock x:Name=\"Mode_Text\"\n                                  Text=\"{Binding CompressionMode, FallbackValue=XPRESS4K, RelativeSource={RelativeSource TemplatedParent}}\"\n                                  HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n                                  FontSize=\"15\" FontWeight=\"SemiBold\">\n                        <ui:TextBlock.RenderTransform>\n                            <TranslateTransform x:Name=\"ModeTextTransform\" Y=\"0\" />\n                        </ui:TextBlock.RenderTransform>\n                    </ui:TextBlock>\n\n\n                    <ui:TextBlock x:Name=\"estimatedSize_Text\"\n                                  Text=\"Estimated size\"\n                                  Grid.Row=\"0\"\n                                  Margin=\"-10,10,0,0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n                                  Foreground=\"{StaticResource AccentFillColorDisabledBrush}\"\n                                  Opacity=\"0\"\n                                  Visibility=\"{Binding EstimatedVisibility, RelativeSource={RelativeSource TemplatedParent}}\">\n                        <ui:TextBlock.RenderTransform>\n                            <TranslateTransform x:Name=\"EstSize_LabelTextTransform\" X=\"10\" Y=\"3\" />\n                        </ui:TextBlock.RenderTransform>\n                    </ui:TextBlock>\n\n                    <ui:TextBlock x:Name=\"savings_Text\"\n                                  Text=\"Savings\"\n                                  Grid.Row=\"0\"\n                                  Margin=\"0,40,0,0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n                                  Foreground=\"{StaticResource AccentFillColorDisabledBrush}\"\n                                  Opacity=\"0\"\n                                  Visibility=\"{Binding EstimatedVisibility, RelativeSource={RelativeSource TemplatedParent}}\">\n                        <ui:TextBlock.RenderTransform>\n                            <TranslateTransform x:Name=\"EstSavings_LabelTextTransform\" Y=\"5\" />\n                        </ui:TextBlock.RenderTransform>\n                    </ui:TextBlock>\n\n\n                    <Grid Grid.Column=\"1\" Visibility=\"{Binding IsEstimating, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToInverseVisibilityConverter}}\">\n                        <ui:TextBlock Text=\"unknown\"\n                                      HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                      FontSize=\"13\" Foreground=\"#30FFFFFF\"\n                                      Visibility=\"{Binding BytesAfter, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource ZeroCountToVisibilityConverter}, ConverterParameter=invert}\" />\n                    </Grid>\n\n                    <Grid Grid.Column=\"1\" Visibility=\"{Binding BytesAfter, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource ZeroCountToVisibilityConverter}}\">\n\n                        <ui:TextBlock x:Name=\"BytesAfter_Text\"\n                                      Grid.Column=\"1\"\n                                      Margin=\"0,-22,0,0\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                      FontSize=\"13\"\n                                      Visibility=\"{Binding IsEstimating, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToInverseVisibilityConverter}}\">\n                            <Run Text=\"{Binding BytesAfter, RelativeSource={RelativeSource TemplatedParent}, FallbackValue=9.4 GB, Converter={StaticResource BytesToReadableConverter}, ConverterParameter=3}\" />\n                            <ui:TextBlock.RenderTransform>\n                                <TranslateTransform x:Name=\"BytesAfterTextTransform\" Y=\"0\" />\n                            </ui:TextBlock.RenderTransform>\n                        </ui:TextBlock>\n                        <ui:TextBlock x:Name=\"BytesSaved_Text\"\n                                      Grid.Column=\"1\"\n                                      Margin=\"0,22,0,0\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                      FontSize=\"13\" Foreground=\"#92F1AB\"\n                                      Visibility=\"{Binding IsEstimating, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToInverseVisibilityConverter}}\">\n                            <Run Text=\"↓\" />\n                            <Run Text=\"{Binding Savings, RelativeSource={RelativeSource TemplatedParent}, FallbackValue=87%, StringFormat={} {0}%}\" />\n\n                            <ui:TextBlock.RenderTransform>\n                                <TranslateTransform x:Name=\"BytesSavedTextTransform\" Y=\"0\" />\n                            </ui:TextBlock.RenderTransform>\n                        </ui:TextBlock>\n\n\n                        <ui:TextBlock x:Name=\"PercentSaved_Text\"\n                                      Grid.Column=\"1\"\n                                      Margin=\"0,22,0,0\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                      FontSize=\"13\" Foreground=\"#92F1AB\" Opacity=\"0\"\n                                      Visibility=\"{Binding IsEstimating, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToInverseVisibilityConverter}}\">\n                            <Run Text=\"↓\" />\n                            <Run Text=\"{Binding BytesSaved, RelativeSource={RelativeSource TemplatedParent}, FallbackValue=1.2 GB, Converter={StaticResource BytesToReadableConverter}, ConverterParameter=3}\" />\n                            <ui:TextBlock.RenderTransform>\n                                <TranslateTransform x:Name=\"PercentSavedTextTransform\" Y=\"0\" />\n                            </ui:TextBlock.RenderTransform>\n                        </ui:TextBlock>\n                    </Grid>\n\n                    <ui:ProgressRing x:Name=\"Loading_ProgressRing\"\n                                     Grid.Column=\"1\"\n                                     Width=\"18\" Height=\"18\"\n                                     Margin=\"0,0,5,0\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                     Foreground=\"#92F1AB\" IsIndeterminate=\"True\"\n                                     Visibility=\"{Binding IsEstimating, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToVisibilityConverter}}\">\n                        <ui:ProgressRing.RenderTransform>\n                            <TranslateTransform x:Name=\"Loading_ProgressRingTransform\" Y=\"0\" />\n                        </ui:ProgressRing.RenderTransform>\n                    </ui:ProgressRing>\n\n                    <Border x:Name=\"Progress_Area\"\n                            Grid.Row=\"1\" Grid.ColumnSpan=\"2\"\n                            Margin=\"-15,-5\" VerticalAlignment=\"Bottom\"\n                            Background=\"Transparent\" ClipToBounds=\"True\" CornerRadius=\"5\"\n                            RenderTransformOrigin=\"0.5 1\">\n                        <Border.RenderTransform>\n                            <ScaleTransform x:Name=\"ProgressAreaTransform\" ScaleY=\"1\" />\n                        </Border.RenderTransform>\n                        <ProgressBar Height=\"12\"\n                                     Margin=\"0\"\n                                     Background=\"Transparent\" BorderThickness=\"0\" Foreground=\"#92F1AB\"\n                                     Visibility=\"{Binding EstimatedVisibility, RelativeSource={RelativeSource TemplatedParent}}\"\n                                     Value=\"{Binding ProgressValue, FallbackValue=82, RelativeSource={RelativeSource TemplatedParent}}\">\n                            <ProgressBar.Template>\n                                <ControlTemplate TargetType=\"ProgressBar\">\n                                    <Grid x:Name=\"GridRoot\" SnapsToDevicePixels=\"true\">\n                                        <Border x:Name=\"PART_Track\"\n                                                Background=\"#30FFFFFF\" CornerRadius=\"0 0 3 3\" />\n                                        <Border x:Name=\"PART_Indicator\"\n                                                Width=\"0\"\n                                                HorizontalAlignment=\"Left\"\n                                                Background=\"{TemplateBinding Foreground}\"\n                                                CornerRadius=\"0 0 0 3\" />\n                                    </Grid>\n\n                                </ControlTemplate>\n                            </ProgressBar.Template>\n                        </ProgressBar>\n                    </Border>\n\n                </Grid>\n                <VisualStateManager.VisualStateGroups>\n                    <VisualStateGroup x:Name=\"CheckStates\">\n                        <VisualState x:Name=\"Checked\">\n                            <Storyboard>\n                                <ColorAnimation Storyboard.TargetName=\"CheckMark\"\n                                                Storyboard.TargetProperty=\"(BorderBrush).(SolidColorBrush.Color)\" To=\"#92F1AB\"\n                                                Duration=\"0:0:0.4\" />\n                            </Storyboard>\n                        </VisualState>\n                        <VisualState x:Name=\"Unchecked\" />\n                        <VisualState x:Name=\"Indeterminate\" />\n                    </VisualStateGroup>\n                    <VisualStateGroup x:Name=\"HoverStates\">\n                        <VisualState x:Name=\"MouseOver\">\n                            <Storyboard>\n                                <DoubleAnimation Storyboard.TargetName=\"Mode_Text\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"-20\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n                                <DoubleAnimation Storyboard.TargetName=\"estimatedSize_Text\" Storyboard.TargetProperty=\"Opacity\" To=\"1\"\n                                                 Duration=\"0:0:0.2\" />\n                                <DoubleAnimation Storyboard.TargetName=\"savings_Text\" Storyboard.TargetProperty=\"Opacity\" To=\"1\"\n                                                 Duration=\"0:0:0.2\" />\n                                <DoubleAnimation Storyboard.TargetName=\"PercentSaved_Text\" Storyboard.TargetProperty=\"Opacity\" To=\"1\"\n                                                 Duration=\"0:0:0.0\" />\n                                <DoubleAnimation Storyboard.TargetName=\"BytesSaved_Text\" Storyboard.TargetProperty=\"Opacity\" To=\"0\"\n                                                 Duration=\"0:0:0.0\" />\n                                <DoubleAnimation Storyboard.TargetName=\"Progress_Area\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(ScaleTransform.ScaleY)\" To=\"0.3\"\n                                                 Duration=\"0:0:0.2\" />\n\n\n\n                                <DoubleAnimation Storyboard.TargetName=\"BytesAfter_Text\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"20\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n\n                                <DoubleAnimation Storyboard.TargetName=\"BytesSaved_Text\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"16\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n                                <DoubleAnimation Storyboard.TargetName=\"PercentSaved_Text\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"16\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n                                <DoubleAnimation Storyboard.TargetName=\"Loading_ProgressRing\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"16\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n\n                            </Storyboard>\n                        </VisualState>\n                        <VisualState x:Name=\"MouseLeave\">\n                            <Storyboard BeginTime=\"0:0:0.1\">\n                                <DoubleAnimation Storyboard.TargetName=\"Mode_Text\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"0\"\n                                                 Duration=\"0:0:0.2\" />\n                                <DoubleAnimation Storyboard.TargetName=\"estimatedSize_Text\" Storyboard.TargetProperty=\"Opacity\" To=\"0\"\n                                                 Duration=\"0:0:0.2\" />\n                                <DoubleAnimation Storyboard.TargetName=\"savings_Text\" Storyboard.TargetProperty=\"Opacity\" To=\"0\"\n                                                 Duration=\"0:0:0.2\" />\n                                <DoubleAnimation Storyboard.TargetName=\"PercentSaved_Text\" Storyboard.TargetProperty=\"Opacity\" To=\"0\"\n                                                 Duration=\"0:0:0.0\" />\n                                <DoubleAnimation Storyboard.TargetName=\"BytesSaved_Text\" Storyboard.TargetProperty=\"Opacity\" To=\"1\"\n                                                 Duration=\"0:0:0.0\" />\n                                <DoubleAnimation Storyboard.TargetName=\"Progress_Area\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(ScaleTransform.ScaleY)\" To=\"1\"\n                                                 Duration=\"0:0:0.2\" />\n\n\n                                <DoubleAnimation Storyboard.TargetName=\"BytesAfter_Text\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"0\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n\n                                <DoubleAnimation Storyboard.TargetName=\"BytesSaved_Text\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"0\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n                                <DoubleAnimation Storyboard.TargetName=\"PercentSaved_Text\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"0\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n                                <DoubleAnimation Storyboard.TargetName=\"Loading_ProgressRing\"\n                                                 Storyboard.TargetProperty=\"(UIElement.RenderTransform).(TranslateTransform.Y)\" To=\"0\"\n                                                 Duration=\"0:0:0.2\">\n                                    <DoubleAnimation.EasingFunction>\n                                        <SineEase />\n                                    </DoubleAnimation.EasingFunction>\n                                </DoubleAnimation>\n\n                            </Storyboard>\n                        </VisualState>\n                    </VisualStateGroup>\n                </VisualStateManager.VisualStateGroups>\n            </ui:Card>\n        </ControlTemplate>\n    </RadioButton.Template>\n</RadioButton>\n"
  },
  {
    "path": "CompactGUI/Views/Components/CompressionMode_Radio.xaml.vb",
    "content": "﻿Public Class CompressionMode_Radio\n    Inherits RadioButton\n\n\n    Public Shared ReadOnly CompressionModeProperty As DependencyProperty = DependencyProperty.RegisterAttached(\n        NameOf(CompressionMode),\n        GetType(String),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.None))\n\n    Public Property CompressionMode As String\n        Get\n            Return CType(GetValue(CompressionModeProperty), String)\n        End Get\n        Set(value As String)\n            SetValue(CompressionModeProperty, value)\n        End Set\n    End Property\n\n    Public Shared ReadOnly SavingsProperty As DependencyProperty = DependencyProperty.RegisterAttached(\n        NameOf(Savings),\n        GetType(Integer),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.None))\n\n    Public Property Savings As Integer\n        Get\n            Return CType(GetValue(SavingsProperty), Integer)\n        End Get\n        Set(value As Integer)\n            SetValue(SavingsProperty, value)\n        End Set\n    End Property\n\n    Public Shared ReadOnly SelectedBrushColorProperty As DependencyProperty = DependencyProperty.RegisterAttached(\n        NameOf(SelectedBrushColor),\n        GetType(Brush),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(Nothing, FrameworkPropertyMetadataOptions.None))\n\n    Public Property SelectedBrushColor As Brush\n        Get\n            Return CType(GetValue(SelectedBrushColorProperty), Brush)\n        End Get\n        Set(value As Brush)\n            SetValue(SelectedBrushColorProperty, value)\n        End Set\n    End Property\n\n\n    Public Shared ReadOnly ProgressValueProperty As DependencyProperty = DependencyProperty.RegisterAttached(\n        NameOf(ProgressValue),\n        GetType(Integer),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.None))\n\n    Public Property ProgressValue As Integer\n        Get\n            Return CType(GetValue(ProgressValueProperty), Integer)\n        End Get\n        Set(value As Integer)\n            SetValue(ProgressValueProperty, value)\n        End Set\n    End Property\n\n\n    Public Shared ReadOnly EstimatedVisibilityProperty As DependencyProperty = DependencyProperty.RegisterAttached(\n        NameOf(EstimatedVisibility),\n        GetType(Visibility),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(Visibility.Visible, FrameworkPropertyMetadataOptions.None))\n\n\n    Public Property EstimatedVisibility As Visibility\n        Get\n            Return CType(GetValue(EstimatedVisibilityProperty), Visibility)\n        End Get\n        Set(value As Visibility)\n            SetValue(EstimatedVisibilityProperty, value)\n        End Set\n    End Property\n\n    Public Shared ReadOnly IsEstimatingProperty As DependencyProperty = DependencyProperty.RegisterAttached(\n        NameOf(IsEstimating),\n        GetType(Boolean),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(True, FrameworkPropertyMetadataOptions.None))\n\n    Public Property IsEstimating As Boolean\n        Get\n            Return CType(GetValue(IsEstimatingProperty), Boolean)\n        End Get\n        Set(value As Boolean)\n            SetValue(IsEstimatingProperty, value)\n        End Set\n    End Property\n\n\n    Public Shared ReadOnly BytesSavedProperty As DependencyProperty = DependencyProperty.RegisterAttached(\n        NameOf(BytesSaved),\n        GetType(Long),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(0L, FrameworkPropertyMetadataOptions.None))\n\n    Public Property BytesSaved As Long\n        Get\n            Return CType(GetValue(BytesSavedProperty), Long)\n        End Get\n        Set(value As Long)\n            SetValue(BytesSavedProperty, value)\n        End Set\n    End Property\n\n\n    Public Shared ReadOnly BytesAfterProperty As DependencyProperty = DependencyProperty.RegisterAttached(\n        NameOf(BytesAfter),\n        GetType(Long),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(0L, FrameworkPropertyMetadataOptions.None))\n\n    Public Property BytesAfter As Long\n        Get\n            Return CType(GetValue(BytesAfterProperty), Long)\n        End Get\n        Set(value As Long)\n            SetValue(BytesAfterProperty, value)\n        End Set\n    End Property\n\n    ' Shared/global hover state\n    Public Shared Event GlobalHoverChanged As EventHandler\n    Private Shared _isAnyHovered As Boolean\n\n    Public Shared Property IsAnyHovered As Boolean\n        Get\n            Return _isAnyHovered\n        End Get\n        Set(value As Boolean)\n            If _isAnyHovered <> value Then\n                _isAnyHovered = value\n                RaiseEvent GlobalHoverChanged(Nothing, EventArgs.Empty)\n            End If\n        End Set\n    End Property\n\n    Public Shared ReadOnly IsForcedDetailedProperty As DependencyProperty =\n    DependencyProperty.RegisterAttached(\n        NameOf(IsForcedDetailed),\n        GetType(Boolean),\n        GetType(CompressionMode_Radio),\n        New FrameworkPropertyMetadata(False, AddressOf OnIsForcedDetailedChanged))\n\n    Private Shared Sub OnIsForcedDetailedChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)\n        Dim ctrl = TryCast(d, CompressionMode_Radio)\n        If ctrl IsNot Nothing AndAlso CBool(e.NewValue) Then\n            VisualStateManager.GoToState(ctrl, \"MouseOver\", False)\n        End If\n    End Sub\n\n    Public Property IsForcedDetailed As Boolean\n        Get\n            Return CType(GetValue(IsForcedDetailedProperty), Boolean)\n        End Get\n        Set(value As Boolean)\n            SetValue(IsForcedDetailedProperty, value)\n        End Set\n    End Property\n\n    Public Sub New()\n        InitializeComponent()\n        AddHandler Me.Loaded, AddressOf OnLoaded\n        AddHandler Me.Unloaded, AddressOf OnUnloaded\n    End Sub\n\n    Private Sub OnLoaded(sender As Object, e As RoutedEventArgs)\n        RemoveHandler GlobalHoverChanged, AddressOf OnGlobalHoverChanged\n        AddHandler GlobalHoverChanged, AddressOf OnGlobalHoverChanged\n    End Sub\n\n    Private Sub OnUnloaded(sender As Object, e As RoutedEventArgs)\n        RemoveHandler GlobalHoverChanged, AddressOf OnGlobalHoverChanged\n    End Sub\n\n    Private Sub OnGlobalHoverChanged(sender As Object, e As EventArgs)\n        ' Update the local dependency property to trigger visual state\n        SetValue(IsGloballyHoveredProperty, IsAnyHovered)\n\n        If IsForcedDetailed Then\n            VisualStateManager.GoToState(Me, \"MouseOver\", False)\n            Return\n        End If\n        VisualStateManager.GoToState(Me, If(IsAnyHovered, \"MouseOver\", \"MouseLeave\"), True)\n\n\n    End Sub\n\n    ' DependencyProperty for binding in XAML\n    Public Shared ReadOnly IsGloballyHoveredProperty As DependencyProperty =\n        DependencyProperty.Register(\"IsGloballyHovered\", GetType(Boolean), GetType(CompressionMode_Radio), New PropertyMetadata(False))\n\n    Public Property IsGloballyHovered As Boolean\n        Get\n            Return CType(GetValue(IsGloballyHoveredProperty), Boolean)\n        End Get\n        Set(value As Boolean)\n            SetValue(IsGloballyHoveredProperty, value)\n        End Set\n    End Property\n\n    Protected Overrides Sub OnMouseEnter(e As MouseEventArgs)\n        MyBase.OnMouseEnter(e)\n        IsAnyHovered = True\n    End Sub\n\n    Protected Overrides Sub OnMouseLeave(e As MouseEventArgs)\n        If IsForcedDetailed Then\n            VisualStateManager.GoToState(Me, \"MouseOver\", False)\n            e.Handled = True\n            Return\n        End If\n\n        MyBase.OnMouseLeave(e)\n\n        IsAnyHovered = False\n    End Sub\n\n    Public Overrides Sub OnApplyTemplate()\n        MyBase.OnApplyTemplate()\n        If IsForcedDetailed Then\n            VisualStateManager.GoToState(Me, \"MouseOver\", False)\n        End If\n    End Sub\n\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/Components/FolderWatcherCard.xaml",
    "content": "﻿<UserControl x:Class=\"FolderWatcherCard\"\n             xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:local=\"clr-namespace:CompactGUI\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:scm=\"clr-namespace:System.ComponentModel;assembly=WindowsBase\"\n             xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n             Margin=\"0\" Padding=\"0\" Panel.ZIndex=\"1010\"\n             d:Background=\"Black\"\n             d:DataContext=\"{d:DesignInstance Type=local:WatcherViewModel}\"\n             d:DesignHeight=\"500\" d:DesignWidth=\"800\"\n             mc:Ignorable=\"d\">\n    <Grid Margin=\"30,10,30,0\" VerticalAlignment=\"Stretch\">\n\n        <Grid.Resources>\n            <CollectionViewSource x:Key=\"Src\" Source=\"{Binding Watcher.WatchedFolders}\">\n                <CollectionViewSource.SortDescriptions>\n                    <scm:SortDescription PropertyName=\"DisplayName\" />\n                </CollectionViewSource.SortDescriptions>\n            </CollectionViewSource>\n            <CollectionViewSource x:Key=\"Dtd\" d:Source=\"{d:SampleData ItemCount=3}\" />\n        </Grid.Resources>\n\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"40\" />\n            <RowDefinition Height=\"40\" />\n            <RowDefinition Height=\"20\" />\n            <RowDefinition />\n        </Grid.RowDefinitions>\n\n        <Grid.ColumnDefinitions>\n            <ColumnDefinition/>\n            <ColumnDefinition/>\n        </Grid.ColumnDefinitions>\n\n\n        <StackPanel Orientation=\"Horizontal\" Grid.Row=\"0\">\n              <TextBlock Text=\"Watched Folders\"\n                   Grid.Row=\"0\"\n                   VerticalAlignment=\"Top\"\n                   FontSize=\"26\"\n                   Foreground=\"{StaticResource CardForeground}\"\n                   Visibility=\"Visible\" />\n\n        <Button\n\n                Height=\"30\" Margin=\"20 0 0 0 \"\n                HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n                Command=\"{Binding ManuallyAddFolderToWatcherCommand}\">\n                <StackPanel Orientation=\"Horizontal\">\n                    <ui:SymbolIcon Symbol=\"FolderAdd24\"/>\n                    <TextBlock Text=\"Add\" Margin=\"8 0 0 0 \" FontSize=\"13\" FontWeight=\"SemiBold\"/>\n                </StackPanel>\n            <Button.ToolTip>\n                <ToolTip ToolTipService.InitialShowDelay=\"100\">\n                    <TextBlock Text=\"Add a custom folder to the watchlist\"\n                               FontSize=\"12\" Foreground=\"#FFBFC7CE\" TextWrapping=\"NoWrap\" />\n                </ToolTip>\n            </Button.ToolTip>\n        </Button>\n        </StackPanel>\n\n      \n\n\n\n        <TextBlock Grid.Row=\"1\"\n                   HorizontalAlignment=\"Left\" VerticalAlignment=\"Top\"\n                   FontSize=\"18\" FontWeight=\"SemiBold\"\n                   Foreground=\"#40FFFFFF\">\n            <Run Text=\"{Binding Watcher.TotalSaved, Mode=OneWay, Converter={StaticResource BytesToReadableConverter}}\" d:Text=\"51.8GB\" />\n            <Run Text=\"saved\" />\n        </TextBlock>\n\n        <StackPanel Grid.Row=\"1\" Grid.RowSpan=\"2\"\n                    Margin=\"0,20,0,0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n                    Orientation=\"Horizontal\">\n\n            <TextBlock Text=\"{Binding Watcher.LastAnalysed, StringFormat=Last analysed {0}, Converter={StaticResource RelativeDateConverter}}\"\n                       Margin=\"0,-2,0,0\" VerticalAlignment=\"Center\"\n                       d:Text=\"Last analysed: just now\" FontSize=\"14\" FontWeight=\"SemiBold\" Foreground=\"#40FFFFFF\" />\n            <Grid Width=\"45\" Height=\"25\"\n                  VerticalAlignment=\"Center\">\n                <ui:Button Background=\"Transparent\" BorderThickness=\"0\" Margin=\"2 0\"\n                           Command=\"{Binding RefreshWatchedCommand}\"\n                           Visibility=\"{Binding RefreshWatchedCommand.IsRunning, Converter={StaticResource BooleanToInverseVisibilityConverter}}\">\n      \n                    <ui:SymbolIcon Symbol=\"ArrowClockwiseDashes24\" FontSize=\"18\"/>\n                    <ui:Button.ToolTip>\n                        <ToolTip ToolTipService.InitialShowDelay=\"100\">\n                            <TextBlock Text=\"Re-analyse all watched folders\"\n                                       FontSize=\"12\" Foreground=\"#FFBFC7CE\" TextWrapping=\"NoWrap\" />\n                        </ToolTip>\n                    </ui:Button.ToolTip>\n                </ui:Button>\n                <ui:ProgressRing Width=\"18\" Height=\"18\"\n                                 Margin=\"0,0,0,0\"\n                                 Foreground=\"#FFBFC7CE\" IsIndeterminate=\"True\"\n                                 Visibility=\"{Binding RefreshWatchedCommand.IsRunning, Converter={StaticResource BoolToVisConverter}}\" />\n            </Grid>\n       \n        </StackPanel>\n\n        <StackPanel Orientation=\"Horizontal\" Grid.Column=\"1\" Grid.Row=\"1\" Grid.RowSpan=\"2\" HorizontalAlignment=\"Right\">\n\n            <Button Content=\"Cancel Background Compressor\"  Command=\"{Binding CancelBackgroundingCommand}\" Visibility=\"{Binding Watcher.IsRunning, Converter={StaticResource BooleanToVisibilityConverter}}\"/>\n            <Button Content=\"Compress All Now\"  Command=\"{Binding RunWatcherCommand}\" Visibility=\"{Binding Watcher.IsRunning, Converter={StaticResource BooleanToInverseVisibilityConverter}}\"/>\n        </StackPanel>\n       \n\n        <Separator Grid.Row=\"2\" Grid.ColumnSpan=\"2\"\n                   Height=\"1\"\n                   VerticalAlignment=\"Bottom\" />\n\n\n        <ListView x:Name=\"UiWatcherListView\"\n                  Grid.Row=\"3\" Grid.ColumnSpan=\"2\"\n                  Margin=\"-10,0,-20,0\" Padding=\"0,0,10,0\" HorizontalAlignment=\"Stretch\"\n                  d:ItemsSource=\"{Binding Source={StaticResource Dtd}}\"\n                  Background=\"Transparent\" BorderThickness=\"0\" VirtualizingPanel.ScrollUnit=\"Pixel\"\n                  ItemsSource=\"{Binding Source={StaticResource Src}}\"\n                  ScrollViewer.VerticalScrollBarVisibility=\"Visible\">\n\n            <ListView.ItemContainerStyle>\n                <Style TargetType=\"ListViewItem\">\n                    <Setter Property=\"Margin\" Value=\"0,0,0,0\" />\n                    <Setter Property=\"Background\" Value=\"Transparent\" />\n                    <Setter Property=\"BorderBrush\" Value=\"Transparent\" />\n                    <Setter Property=\"VerticalContentAlignment\" Value=\"Center\" />\n                    <Setter Property=\"FocusVisualStyle\" Value=\"{x:Null}\" />\n                    <Setter Property=\"Template\">\n                        <Setter.Value>\n                            <ControlTemplate TargetType=\"{x:Type ListViewItem}\">\n                                <Border Name=\"Border\"\n                                        Background=\"{TemplateBinding Background}\"\n                                        BorderBrush=\"{TemplateBinding BorderBrush}\"\n                                        BorderThickness=\"{TemplateBinding BorderThickness}\"\n                                        CornerRadius=\"5\">\n                                    <ContentPresenter Content=\"{TemplateBinding Content}\"\n                                                      Margin=\"{TemplateBinding Padding}\"\n                                                      d:Content=\"{TemplateBinding Content}\"\n                                                      ContentTemplate=\"{TemplateBinding ContentTemplate}\" />\n                                </Border>\n                            </ControlTemplate>\n                        </Setter.Value>\n                    </Setter>\n\n                    <Style.Triggers>\n                        <Trigger Property=\"IsMouseOver\" Value=\"True\">\n                            <Setter Property=\"Background\" Value=\"#20000000\" />\n                            <Setter Property=\"BorderBrush\" Value=\"Transparent\" />\n                        </Trigger>\n                    </Style.Triggers>\n\n                </Style>\n\n            </ListView.ItemContainerStyle>\n            <ListView.ItemTemplate>\n                <DataTemplate>\n                    <Border Height=\"70\"\n                            Margin=\"0,0\" Padding=\"6,16\"\n                            d:Height=\"100\" Background=\"#00FFFFFF\" KeyboardNavigation.TabNavigation=\"None\"\n                            MouseDown=\"ToggleBorderHeight\" MouseEnter=\"ToggleBorderHeight\">\n\n\n                        <Grid>\n\n\n\n                            <Label>\n                                <StackPanel Orientation=\"Horizontal\">\n\n                                    <ui:ProgressRing Width=\"15\" Height=\"15\"\n                                                     Margin=\"0,0,6,0\" Padding=\"7\"\n                                                     Foreground=\"#FFBFC7CE\" IsIndeterminate=\"True\"\n                                                     Visibility=\"{Binding IsWorking, Converter={StaticResource BoolToVisConverter}, Mode=OneWay}\" />\n                                    <Viewbox Grid.Column=\"1\"\n                                             Width=\"12\" Height=\"12\"\n                                             Margin=\"0,0,5,0\" HorizontalAlignment=\"Left\"\n                                             Visibility=\"{Binding IsSteamGame, Converter={StaticResource BoolToVisConverter}}\">\n                                        <Path Data=\"M110.5,87.3c0,0.2,0,0.4,0,0.6L82,129.3c-4.6-0.2-9.3,0.6-13.6,2.4c-1.9,0.8-3.8,1.8-5.5,2.9L0.3,108.8  c0,0-1.4,23.8,4.6,41.6l44.3,18.3c2.2,9.9,9,18.6,19.1,22.8c16.4,6.9,35.4-1,42.2-17.4c1.8-4.3,2.6-8.8,2.5-13.3l40.8-29.1  c0.3,0,0.7,0,1,0c24.4,0,44.3-19.9,44.3-44.3c0-24.4-19.8-44.3-44.3-44.3C130.4,43,110.5,62.9,110.5,87.3z M103.7,171.2  c-5.3,12.7-19.9,18.7-32.6,13.4c-5.9-2.4-10.3-6.9-12.8-12.2l14.4,6c9.4,3.9,20.1-0.5,24-9.9c3.9-9.4-0.5-20.1-9.9-24l-14.9-6.2  c5.7-2.2,12.3-2.3,18.4,0.3c6.2,2.6,10.9,7.4,13.5,13.5S106.2,165.1,103.7,171.2 M154.8,116.9c-16.3,0-29.5-13.3-29.5-29.5  c0-16.3,13.2-29.5,29.5-29.5c16.3,0,29.5,13.3,29.5,29.5C184.2,103.6,171,116.9,154.8,116.9 M132.7,87.3c0-12.3,9.9-22.2,22.1-22.2  c12.2,0,22.1,9.9,22.1,22.2c0,12.3-9.9,22.2-22.1,22.2C142.6,109.5,132.7,99.5,132.7,87.3z M233,116.5c0,64.3-52.2,116.5-116.5,116.5S0,180.8,0,116.5c0-30.4,11-60.2,30.7-78.8C53.5,16.1,82.5,0,116.5,0  C180.8,0,233,52.2,233,116.5z\" Fill=\"#FFFFFF\" />\n                                    </Viewbox>\n                                    <TextBlock Text=\"&#xED42;\"\n                                               Grid.Column=\"1\"\n                                               Width=\"12\" Height=\"12\"\n                                               Margin=\"0,0,5,0\"\n                                               FontFamily=\"Segoe Fluent Icons, Segoe MDL2 Assets\" Foreground=\"White\"\n                                               Visibility=\"{Binding IsSteamGame, Converter={StaticResource BooleanToInverseVisibilityConverter}}\" />\n                                    <Grid>\n                                        <TextBlock Text=\"{Binding DisplayName, Converter={StaticResource StrippedFolderPathConverter}}\"\n                                                   MinWidth=\"100\" MaxWidth=\"280\"\n                                                   Margin=\"0,-2,0,0\" VerticalAlignment=\"Top\"\n                                                   FontSize=\"15\" FontWeight=\"SemiBold\" Foreground=\"White\"\n                                                   MouseLeftButtonDown=\"DisplayNameTextBlock_MouseLeftButtonDown\"\n                                                   TextTrimming=\"CharacterEllipsis\"\n                                                   Visibility=\"{Binding IsEditing, Converter={StaticResource BooleanToInverseVisibilityConverter}}\" />\n\n                                        <TextBox Text=\"{Binding DisplayName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}\"\n                                                 MinWidth=\"100\" MaxWidth=\"280\"\n                                                 Margin=\"-3,-3,0,0\" Padding=\"0\" VerticalAlignment=\"Top\"\n                                                 FontSize=\"15\" FontWeight=\"SemiBold\"\n                                                 Foreground=\"{StaticResource TextControlForeground}\"\n                                                 KeyDown=\"DisplayNameTextBox_KeyDown\" LostFocus=\"DisplayNameTextBox_LostFocus\"\n                                                 Visibility=\"{Binding IsEditing, Converter={StaticResource BoolToVisConverter}}\" />\n                                    </Grid>\n\n                                </StackPanel>\n\n                            </Label>\n                            <Border Width=\"40\" Height=\"15\"\n                                    Margin=\"0,22,0,0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Top\"\n                                    Background=\"#8373808C\" CornerRadius=\"5\">\n                                <TextBlock Text=\"{Binding CompressionLevel, Converter={StaticResource CompressionLevelAbbreviatedConverter}}\"\n                                           HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                                           d:Text=\"X4K\" FontSize=\"11\" FontWeight=\"SemiBold\" Foreground=\"#FFF\" />\n                            </Border>\n\n                            <TextBlock Text=\"{Binding Folder, Converter={StaticResource TokenisedFolderPathConverter}}\"\n                                       Width=\"450\"\n                                       Margin=\"50,20,0,0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Top\"\n                                       FontSize=\"12\" Foreground=\"#80FFFFFF\" TextTrimming=\"CharacterEllipsis\">\n                                <TextBlock.ToolTip>\n                                    <ToolTip MaxWidth=\"500\" Placement=\"RelativePoint\">\n                                        <TextBlock Text=\"{Binding Folder, Converter={StaticResource TokenisedFolderPathConverter}}\"\n                                                   FontSize=\"12\" Foreground=\"#FFBFC7CE\" TextWrapping=\"NoWrap\" />\n                                    </ToolTip>\n                                </TextBlock.ToolTip>\n                            </TextBlock>\n                            <TextBlock x:Name=\"DecayedText\"\n                                       Text=\"{Binding DecayPercentage, StringFormat={}{0}% decayed, Mode=OneWay, Converter={StaticResource DecimalToPercentageConverter}}\"\n                                       Margin=\"0,-2,0,0\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\"\n                                       d:Text=\"7% decayed\" FontSize=\"12\" Foreground=\"#FFBFC7CE\" />\n                            <TextBlock x:Name=\"SavedText\"\n                                       Text=\"{Binding SavedSpace, StringFormat={}{0} saved, Mode=OneWay, Converter={StaticResource BytesToReadableConverter}, ConverterParameter=0}\"\n                                       Margin=\"0,-2,0,0\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\"\n                                       d:Text=\"39GB saved\" FontSize=\"12\" Foreground=\"#FFBFC7CE\" Visibility=\"Collapsed\" />\n                            <ProgressBar Width=\"160\" Height=\"6\"\n                                         Margin=\"0,25,0,0\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\"\n                                         Background=\"#808B9FB3\"\n                                         Foreground=\"{Binding DecayPercentage, Converter={StaticResource ProgressBarColorConverter}}\"\n                                         Value=\"{Binding DecayPercentage, Mode=OneWay, Converter={StaticResource DecimalToPercentageConverter}, ConverterParameter='I'}\" />\n\n                            <Separator Margin=\"0,42,30,0\" VerticalAlignment=\"Top\"\n                                       Background=\"#60FFFFFF\" Visibility=\"Collapsed\" />\n\n                            <Grid Margin=\"0,45,0,0\">\n                                <StackPanel Orientation=\"Horizontal\">\n                                    <TextBlock Text=\"last compressed:\"\n                                               Margin=\"0,0,10,0\"\n                                               FontSize=\"12\" Foreground=\"#FF8B9FB3\" />\n                                    <TextBlock Text=\"{Binding LastCompressedDate, Converter={StaticResource RelativeDateConverter}}\"\n                                               d:Text=\"fds\" FontSize=\"12\" Foreground=\"#FF8B9FB3\" />\n                                </StackPanel>\n                                <StackPanel Margin=\"0,20,0,-3\" Orientation=\"Horizontal\">\n                                    <TextBlock Text=\"last modified:\"\n                                               Margin=\"0,0,27,0\"\n                                               FontSize=\"12\" Foreground=\"#FF8B9FB3\" />\n                                    <TextBlock Text=\"{Binding LastSystemModifiedDate, Converter={StaticResource RelativeDateConverter}}\"\n                                               d:Text=\"fds\" FontSize=\"12\" Foreground=\"#FF8B9FB3\" />\n                                </StackPanel>\n\n\n\n                                <Button HorizontalAlignment=\"Right\"\n                                        Background=\"#DD6B6B\"\n                                        Command=\"{Binding ElementName=UiWatcherListView, Path=DataContext.RemoveWatcherCommand}\"\n                                        CommandParameter=\"{Binding}\"\n                                        Foreground=\"White\">\n                                    <ui:SymbolIcon FontSize=\"14\" Symbol=\"Delete16\" />\n                                    <Button.ToolTip>\n                                        <ToolTip ToolTipService.InitialShowDelay=\"100\">\n                                            <TextBlock Text=\"Remove from Watchlist\"\n                                                       FontSize=\"12\" Foreground=\"#FFBFC7CE\" TextWrapping=\"NoWrap\" />\n                                        </ToolTip>\n                                    </Button.ToolTip>\n                                </Button>\n\n                                <Button Margin=\"0,0,40,0\" HorizontalAlignment=\"Right\"\n                                        Background=\"#6B8399\"\n                                        Command=\"{Binding ElementName=UiWatcherListView, Path=DataContext.AddWatchedFolderToQueueCommand}\"\n                                        CommandParameter=\"{Binding}\"\n                                        Foreground=\"White\">\n                                    <ui:SymbolIcon FontSize=\"14\" RenderTransformOrigin=\"0.5,0.5\" Symbol=\"ArrowMinimize20\">\n                                        <ui:SymbolIcon.RenderTransform>\n                                            <TransformGroup>\n                                                <ScaleTransform />\n                                                <SkewTransform />\n                                                <RotateTransform Angle=\"90\" />\n                                                <TranslateTransform />\n                                            </TransformGroup>\n                                        </ui:SymbolIcon.RenderTransform>\n                                    </ui:SymbolIcon>\n                                    <Button.ToolTip>\n                                        <ToolTip  ToolTipService.InitialShowDelay=\"100\">\n                                            <TextBlock Text=\"Add to compression queue\"\n                                                       FontSize=\"12\" Foreground=\"#FFBFC7CE\" TextWrapping=\"NoWrap\" />\n                                        </ToolTip>\n                                    </Button.ToolTip>\n                                </Button>\n\n                                <Button Margin=\"0,0,80,0\" HorizontalAlignment=\"Right\" Visibility=\"{Binding IsWorking, Converter={StaticResource BooleanToInverseVisibilityConverter}}\"\n                                        Command=\"{Binding ElementName=UiWatcherListView, Path=DataContext.ReAnalyseWatchedCommand}\"\n                                        CommandParameter=\"{Binding}\"\n                                        Foreground=\"White\">\n                                    <ui:FontIcon FontFamily=\"Segoe Fluent Icons, Segoe MDL2 Assets\" FontSize=\"14\"\n                                                 Glyph=\"&#xE9F3;\" />\n                                    <Button.ToolTip>\n                                        <ToolTip ToolTipService.InitialShowDelay=\"100\">\n                                            <TextBlock Text=\"Re-analyse this folder\"\n                                                       FontSize=\"12\" Foreground=\"#FFBFC7CE\" TextWrapping=\"NoWrap\" />\n                                        </ToolTip>\n                                    </Button.ToolTip>\n                                </Button>\n\n                            </Grid>\n\n\n                        </Grid>\n\n                    </Border>\n                </DataTemplate>\n            </ListView.ItemTemplate>\n\n        </ListView>\n\n\n\n    </Grid>\n</UserControl>\n"
  },
  {
    "path": "CompactGUI/Views/Components/FolderWatcherCard.xaml.vb",
    "content": "﻿Imports System.Windows.Media.Animation\n\nPublic Class FolderWatcherCard : Inherits UserControl\n    Private currentlyExpandedBorder As Border = Nothing\n\n\n    Private Sub ToggleBorderHeight(sender As Object, e As RoutedEventArgs)\n        Dim border As Border = DirectCast(sender, Border)\n        Dim newHeight As Double = If(border.Height = 110, 70, 110)\n\n        Dim childSavedText = FindChild(Of TextBlock)(border, \"SavedText\")\n        Dim childDecayedText = FindChild(Of TextBlock)(border, \"DecayedText\")\n\n        Dim previousBorderChildSavedText = FindChild(Of TextBlock)(currentlyExpandedBorder, \"SavedText\")\n        Dim previousBorderChildDecayedText = FindChild(Of TextBlock)(currentlyExpandedBorder, \"DecayedText\")\n\n        If currentlyExpandedBorder Is border AndAlso border.Height = 110 AndAlso TypeOf (e) IsNot MouseButtonEventArgs Then\n            ' Do nothing, keep it expanded\n            Return\n        End If\n\n        If currentlyExpandedBorder IsNot Nothing AndAlso currentlyExpandedBorder IsNot border Then\n            AnimateBorderHeight(currentlyExpandedBorder, 70)\n            previousBorderChildSavedText.Visibility = Visibility.Collapsed\n            previousBorderChildDecayedText.Visibility = Visibility.Visible\n\n        End If\n        AnimateBorderHeight(border, newHeight)\n        childSavedText.Visibility = If(newHeight = 110, Visibility.Visible, Visibility.Collapsed)\n        childDecayedText.Visibility = If(newHeight = 110, Visibility.Collapsed, Visibility.Visible)\n        currentlyExpandedBorder = If(newHeight = 110, border, Nothing)\n    End Sub\n\n    Private Sub AnimateBorderHeight(border As Border, targetHeight As Double)\n        Dim animation As New DoubleAnimation() With {\n        .From = border.ActualHeight,\n        .To = targetHeight,\n        .Duration = TimeSpan.FromSeconds(0.2)\n    }\n        Dim storyboard As New Storyboard()\n        Storyboard.SetTarget(animation, border)\n        Storyboard.SetTargetProperty(animation, New PropertyPath(HeightProperty))\n\n        storyboard.Children.Add(animation)\n        storyboard.Begin()\n    End Sub\n\n    Public Shared Function FindChild(Of T As DependencyObject)(parent As DependencyObject, childName As String) As T\n        If parent Is Nothing Then Return Nothing\n        Dim foundChild As T = Nothing\n        Dim childrenCount As Integer = VisualTreeHelper.GetChildrenCount(parent)\n        For i As Integer = 0 To childrenCount - 1\n            Dim child As DependencyObject = VisualTreeHelper.GetChild(parent, i)\n            Dim childType As T = TryCast(child, T)\n            If childType Is Nothing Then\n                ' The child is not of the request type, so recurse down the tree\n                foundChild = FindChild(Of T)(child, childName)\n                If foundChild IsNot Nothing Then Exit For\n            ElseIf Not String.IsNullOrEmpty(childName) Then\n                Dim frameworkElement As FrameworkElement = TryCast(child, FrameworkElement)\n                ' If the child has the correct name and type\n                If frameworkElement IsNot Nothing AndAlso frameworkElement.Name = childName Then\n                    foundChild = DirectCast(child, T)\n                    Exit For\n                End If\n            Else\n                ' Child is of the requested type but has no name, return it\n                foundChild = DirectCast(child, T)\n                Exit For\n            End If\n        Next\n        Return foundChild\n    End Function\n\n    Private Sub DisplayNameTextBlock_MouseLeftButtonDown(sender As Object, e As MouseButtonEventArgs)\n        If e.ClickCount = 2 Then\n            Dim fe = TryCast(sender, FrameworkElement)\n            If fe IsNot Nothing AndAlso fe.DataContext IsNot Nothing Then\n                Dim vm = TryCast(fe.DataContext, Watcher.WatchedFolder)\n                If vm IsNot Nothing Then\n                    vm.IsEditing = True\n                End If\n            End If\n        End If\n    End Sub\n\n    Private Sub DisplayNameTextBox_LostFocus(sender As Object, e As RoutedEventArgs)\n        Dim fe = TryCast(sender, FrameworkElement)\n        If fe IsNot Nothing AndAlso fe.DataContext IsNot Nothing Then\n            Dim vm = TryCast(fe.DataContext, Watcher.WatchedFolder)\n            If vm IsNot Nothing Then\n                vm.IsEditing = False\n                Application.GetService(Of Watcher.Watcher).WriteToFile()\n            End If\n        End If\n    End Sub\n\n    Private Sub DisplayNameTextBox_KeyDown(sender As Object, e As KeyEventArgs)\n        If e.Key = Key.Enter Then\n            Dim fe = TryCast(sender, FrameworkElement)\n            If fe IsNot Nothing AndAlso fe.DataContext IsNot Nothing Then\n                Dim vm = TryCast(fe.DataContext, Watcher.WatchedFolder)\n                If vm IsNot Nothing Then\n                    vm.IsEditing = False\n                    Application.GetService(Of Watcher.Watcher).WriteToFile()\n                End If\n            End If\n        End If\n    End Sub\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/Pages/DatabasePage.xaml",
    "content": "﻿<UserControl x:Class=\"DatabasePage\"\n             xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" \n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\" \n             xmlns:local=\"clr-namespace:CompactGUI\"\n             mc:Ignorable=\"d\" xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\" d:DataContext=\"{d:DesignInstance Type=local:DatabaseViewModel}\"\n             d:DesignHeight=\"450\" d:DesignWidth=\"800\">\n    <Grid Margin=\"40,20,40,0\" VerticalAlignment=\"Stretch\">\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"40\"/>\n            <RowDefinition Height=\"40\"/>\n            <RowDefinition Height=\"20\"/>\n            <RowDefinition Height=\"*\"/>\n        </Grid.RowDefinitions>\n        \n        \n        \n        \n        <TextBlock Text=\"Database Results\"\n             Grid.Row=\"0\"\n             VerticalAlignment=\"Top\"\n             FontSize=\"26\"\n             Foreground=\"{StaticResource CardForeground}\"\n             Visibility=\"Visible\" />\n\n        <TextBlock Grid.Row=\"2\" Text=\"{Binding LastUpdatedDatabase, StringFormat=Last Fetched: {0:dd MMM yyy HH:mm:ss}}\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\" Foreground=\"#10FFFFFF\" Margin=\"0 0 0 0 \" FontSize=\"12\"/>\n\n        <TextBlock Grid.Row=\"1\"\n           HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n           FontSize=\"20\"\n           Foreground=\"{StaticResource CardForegroundDisabled}\">\n            <!--<Run Text=\"{Binding DatabaseSubmissionsCount, Mode=OneWay}\" />\n            <Run Text=\"Submissions\" />\n            <Run Text=\"across\" />-->\n            <Run Text=\"{Binding DatabaseGamesCount, Mode=OneWay }\" />  \n            <Run Text=\"Games\" />\n        </TextBlock>\n\n        <Grid Grid.Row=\"0\" Margin=\"20 0 0 0\" HorizontalAlignment=\"Right\" Grid.RowSpan=\"1\">\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"*\"/>\n                <ColumnDefinition Width=\"auto\"/>\n            </Grid.ColumnDefinitions>\n\n            \n            <!-- Search Box -->\n            <ui:TextBox VerticalAlignment=\"Center\"\n               FontSize=\"14\" Width=\"250\"\n               PlaceholderText=\"Search by game name or SteamID...\"\n               Text=\"{Binding SearchText, UpdateSourceTrigger=PropertyChanged}\" />\n\n            <!-- Sorting Menu -->\n            <ui:DropDownButton Grid.Column=\"1\" Margin=\"20 0 0 0\" Foreground=\"{StaticResource LabelForeground}\" Height=\"36\" VerticalAlignment=\"Center\" HorizontalAlignment=\"Stretch\">\n                <TextBlock Text=\"Sort By\"/>\n                <ui:DropDownButton.Flyout>\n                    <ContextMenu>\n                        <MenuItem Header=\"Game Name\">\n                            <MenuItem Header=\"Ascending\" Command=\"{Binding SortResultsCommand}\" CommandParameter=\"GameNameAsc\"/>\n                            <MenuItem Header=\"Descending\" Command=\"{Binding SortResultsCommand}\" CommandParameter=\"GameNameDesc\"/>\n                        </MenuItem>\n                        <MenuItem Header=\"SteamID\">\n                            <MenuItem Header=\"Ascending\" Command=\"{Binding SortResultsCommand}\" CommandParameter=\"SteamIDAsc\"/>\n                            <MenuItem Header=\"Descending\" Command=\"{Binding SortResultsCommand}\" CommandParameter=\"SteamIDDesc\"/>\n                        </MenuItem>\n                        <MenuItem Header=\"Max Savings\">\n                            <MenuItem Header=\"Ascending\" Command=\"{Binding SortResultsCommand}\" CommandParameter=\"MaxSavingsAsc\"/>\n                            <MenuItem Header=\"Descending\" Command=\"{Binding SortResultsCommand}\" CommandParameter=\"MaxSavingsDesc\"/>\n                        </MenuItem>\n                    </ContextMenu>\n                </ui:DropDownButton.Flyout>\n            </ui:DropDownButton>\n\n        </Grid>\n        \n        \n        <Separator Grid.Row=\"2\"\n           Height=\"1\"\n           VerticalAlignment=\"Bottom\" />\n       \n        <!-- Results ListView -->\n        <ListView Grid.Row=\"3\" Background=\"Transparent\" VirtualizingPanel.VirtualizationMode=\"Recycling\"\n                  ItemsSource=\"{Binding FilteredResults}\"\n                  Margin=\"-20 10\" BorderBrush=\"Transparent\" VirtualizingPanel.ScrollUnit=\"Pixel\" \n                  VerticalAlignment=\"Stretch\" Foreground=\"{StaticResource CardForeground}\" \n                  HorizontalContentAlignment=\"Stretch\">\n\n            <ListView.ItemContainerStyle>\n                <Style TargetType=\"ListViewItem\">\n                    <Setter Property=\"Margin\" Value=\"0,0,0,0\" />\n                    <Setter Property=\"Background\" Value=\"Transparent\" />\n                    <Setter Property=\"BorderBrush\" Value=\"Transparent\" />\n                    <Setter Property=\"VerticalContentAlignment\" Value=\"Center\" />\n                    <Setter Property=\"FocusVisualStyle\" Value=\"{x:Null}\" />\n                    <Setter Property=\"Template\">\n                        <Setter.Value>\n                            <ControlTemplate TargetType=\"{x:Type ListViewItem}\">\n                                <Border Name=\"Border\"\n                            Background=\"{TemplateBinding Background}\"\n                            BorderBrush=\"{TemplateBinding BorderBrush}\"\n                            BorderThickness=\"{TemplateBinding BorderThickness}\"\n                            CornerRadius=\"5\">\n                                    <ContentPresenter Content=\"{TemplateBinding Content}\"\n                                          Margin=\"{TemplateBinding Padding}\"\n                                          d:Content=\"{TemplateBinding Content}\"\n                                          ContentTemplate=\"{TemplateBinding ContentTemplate}\" />\n                                </Border>\n                            </ControlTemplate>\n                        </Setter.Value>\n                    </Setter>\n\n                    <Style.Triggers>\n                        <Trigger Property=\"IsMouseOver\" Value=\"True\">\n                            <Setter Property=\"Background\" Value=\"#00000000\" />\n                            <Setter Property=\"BorderBrush\" Value=\"Transparent\" />\n                        </Trigger>\n                    </Style.Triggers>\n\n                </Style>\n\n            </ListView.ItemContainerStyle>\n            <ListView.ItemTemplate>\n                <DataTemplate>\n                    <Border BorderBrush=\"#DDD\"  BorderThickness=\"0\" CornerRadius=\"6\" Padding=\"12\" Margin=\"0,0,0,12\" >\n                        <StackPanel>\n                            <!-- Header: Game Info -->\n                            <StackPanel Orientation=\"Horizontal\" VerticalAlignment=\"Center\">\n                                <TextBlock Text=\"{Binding GameName}\" FontWeight=\"SemiBold\" FontSize=\"20\"/>\n                                <Border Margin=\"14,2\" Padding=\"6,2\" Background=\"{StaticResource CardBackgroundPressed}\" CornerRadius=\"4\">\n                                    <StackPanel Orientation=\"Horizontal\">\n                                        \n                                        <TextBlock Text=\"{Binding SteamID, StringFormat={}SteamID: {0}}\" VerticalAlignment=\"Center\"/>\n                                    </StackPanel>\n                                </Border>\n                                \n                            </StackPanel>\n\n                            <!-- Compression Results -->\n\n                            <StackPanel Orientation=\"Vertical\" Margin=\"0,8,0,0\">\n\n                                <ui:Card>\n\n                                <Grid>\n                                    <Grid.RowDefinitions>\n\n                                        <RowDefinition/>\n                                        <RowDefinition/>\n                                        <RowDefinition/>\n                                        <RowDefinition/>\n                                        <RowDefinition/>\n                                    </Grid.RowDefinitions>\n\n                                    <Grid Margin=\"0 0 0 10\">\n                                        <Grid.ColumnDefinitions>\n                                            <ColumnDefinition Width=\"80\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"130\"/>\n                                        </Grid.ColumnDefinitions>\n\n                                        <TextBlock Text=\"MODE\" FontWeight=\"SemiBold\" Foreground=\"{StaticResource CardForegroundDisabled}\"/>\n                                        <TextBlock Grid.Column=\"1\" Text=\"BEFORE\" FontWeight=\"SemiBold\" HorizontalAlignment=\"Right\" Foreground=\"{StaticResource CardForegroundDisabled}\"/>\n                                        <TextBlock Grid.Column=\"2\" Text=\"AFTER\" FontWeight=\"SemiBold\" HorizontalAlignment=\"Right\" Foreground=\"{StaticResource CardForegroundDisabled}\"/>\n                                        <TextBlock Grid.Column=\"3\" Text=\"SAVINGS\" FontWeight=\"SemiBold\" HorizontalAlignment=\"Right\" Foreground=\"{StaticResource CardForegroundDisabled}\"/>\n                                        <TextBlock Grid.Column=\"4\" Text=\"TOTAL RESULTS\" FontWeight=\"SemiBold\" HorizontalAlignment=\"Right\" Foreground=\"{StaticResource CardForegroundDisabled}\"/>\n\n                                    </Grid>\n\n                                    <Grid Grid.Row=\"1\" Visibility=\"{Binding Result_X4K, Converter={StaticResource NullToVisibilityConverter}}\">\n                                        <Grid.ColumnDefinitions>\n                                            <ColumnDefinition Width=\"80\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"130\"/>\n                                        </Grid.ColumnDefinitions>\n\n                                        <Label Content=\"XPRESS4K\"/>\n                                        <TextBlock Grid.Column=\"1\" Text=\"{Binding Result_X4K.BeforeBytes, Converter={StaticResource BytesToReadableConverter}}\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"2\" Text=\"{Binding Result_X4K.AfterBytes, Converter={StaticResource BytesToReadableConverter}}\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"3\" Text=\"{Binding Result_X4K.CompressionSavings, StringFormat={}{0}%}\" Foreground=\"#92f1ab\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"4\" Text=\"{Binding Result_X4K.TotalResults}\" HorizontalAlignment=\"Right\"/>\n                                    </Grid>\n\n                                    <Grid Grid.Row=\"2\" Visibility=\"{Binding Result_X8K, Converter={StaticResource NullToVisibilityConverter}}\">\n                                        <Grid.ColumnDefinitions>\n                                            <ColumnDefinition Width=\"80\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"130\"/>\n                                        </Grid.ColumnDefinitions>\n\n                                        <Label Content=\"XPRESS8K\"/>\n                                        <TextBlock Grid.Column=\"1\" Text=\"{Binding Result_X8K.BeforeBytes, Converter={StaticResource BytesToReadableConverter}}\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"2\" Text=\"{Binding Result_X8K.AfterBytes, Converter={StaticResource BytesToReadableConverter}}\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"3\" Text=\"{Binding Result_X8K.CompressionSavings, StringFormat={}{0}%}\" Foreground=\"#92f1ab\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"4\" Text=\"{Binding Result_X8K.TotalResults}\" HorizontalAlignment=\"Right\"/>\n                                    </Grid>\n                                    <Grid Grid.Row=\"3\" Visibility=\"{Binding Result_X16K, Converter={StaticResource NullToVisibilityConverter}}\">\n                                        <Grid.ColumnDefinitions>\n                                            <ColumnDefinition Width=\"80\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"130\"/>\n                                        </Grid.ColumnDefinitions>\n\n                                        <Label Content=\"XPRESS16K\"/>\n                                        <TextBlock Grid.Column=\"1\" Text=\"{Binding Result_X16K.BeforeBytes, Converter={StaticResource BytesToReadableConverter}}\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"2\" Text=\"{Binding Result_X16K.AfterBytes, Converter={StaticResource BytesToReadableConverter}}\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"3\" Text=\"{Binding Result_X16K.CompressionSavings, StringFormat={}{0}%}\" Foreground=\"#92f1ab\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"4\" Text=\"{Binding Result_X16K.TotalResults}\" HorizontalAlignment=\"Right\"/>\n                                    </Grid>\n                                    <Grid Grid.Row=\"4\" Visibility=\"{Binding Result_LZX, Converter={StaticResource NullToVisibilityConverter}}\">\n                                        <Grid.ColumnDefinitions>\n                                            <ColumnDefinition Width=\"80\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"100\"/>\n                                            <ColumnDefinition Width=\"130\"/>\n                                        </Grid.ColumnDefinitions>\n\n                                        <Label Content=\"LZX\"/>\n                                        <TextBlock Grid.Column=\"1\" Text=\"{Binding Result_LZX.BeforeBytes, Converter={StaticResource BytesToReadableConverter}}\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"2\" Text=\"{Binding Result_LZX.AfterBytes, Converter={StaticResource BytesToReadableConverter}}\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"3\" Text=\"{Binding Result_LZX.CompressionSavings, StringFormat={}{0}%}\" Foreground=\"#92f1ab\" HorizontalAlignment=\"Right\"/>\n                                        <TextBlock Grid.Column=\"4\" Text=\"{Binding Result_LZX.TotalResults}\" HorizontalAlignment=\"Right\"/>\n                                    </Grid>\n                                </Grid>\n                                </ui:Card>\n                                \n                               \n                            </StackPanel>\n\n                            <!-- Poorly Compressed Extensions -->\n                            <StackPanel Orientation=\"Vertical\" Margin=\"0,10,0,0\" \n                                        Visibility=\"Collapsed\">\n                                <TextBlock Text=\"UNCOMPRESSABLE FILETYPES\" FontWeight=\"SemiBold\" Margin=\"0,0,0,4\"/>\n                                <ItemsControl ItemsSource=\"{Binding PoorlyCompressedExtensions}\">\n                                    <ItemsControl.ItemsPanel>\n                                        <ItemsPanelTemplate>\n                                            <WrapPanel Orientation=\"Horizontal\" />\n                                        </ItemsPanelTemplate>\n                                    </ItemsControl.ItemsPanel>\n                                    <ItemsControl.ItemTemplate>\n                                        <DataTemplate>\n                                            <Border Margin=\"4,2\" Padding=\"6,2\" Background=\"{StaticResource CardBackground}\" CornerRadius=\"4\">\n                                                <StackPanel Orientation=\"Horizontal\">\n                                                    <TextBlock Text=\"{Binding Extension}\" FontWeight=\"Bold\"/>\n                                                </StackPanel>\n                                            </Border>\n                                        </DataTemplate>\n                                    </ItemsControl.ItemTemplate>\n                                </ItemsControl>\n                            </StackPanel>\n                        </StackPanel>\n                    </Border>\n                </DataTemplate>\n            </ListView.ItemTemplate>\n        </ListView>\n\n\n    </Grid>\n</UserControl>\n"
  },
  {
    "path": "CompactGUI/Views/Pages/DatabasePage.xaml.vb",
    "content": "﻿Public Class DatabasePage\n    Public Property viewModel As DatabaseViewModel\n    Sub New(VM As DatabaseViewModel)\n\n        InitializeComponent()\n        DataContext = VM\n        viewModel = VM\n\n        ScrollViewer.SetCanContentScroll(Me, False)\n\n    End Sub\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/Pages/FolderView.xaml",
    "content": "﻿<UserControl x:Class=\"FolderView\"\n             xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:local=\"clr-namespace:CompactGUI\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n             d:DataContext=\"{d:DesignInstance Type=local:FolderViewModel}\"\n             d:DesignHeight=\"580\" d:DesignWidth=\"900\" UseLayoutRounding=\"True\"\n             mc:Ignorable=\"d\">\n    <UserControl.Resources>\n\n        <DataTemplate x:Key=\"IdleTemplate\">\n            <local:PendingCompression DataContext=\"{Binding DataContext, RelativeSource={RelativeSource AncestorType=UserControl}}\" />\n        </DataTemplate>\n\n        <DataTemplate x:Key=\"AnalysingTemplate\">\n            <ui:ProgressRing HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                             IsIndeterminate=\"True\" />\n        </DataTemplate>\n\n        <DataTemplate x:Key=\"CompressingTemplate\">\n            <StackPanel Margin=\"0,30\"\n                        d:DataContext=\"{d:DesignInstance Type=local:FolderViewModel}\"\n                        DataContext=\"{Binding DataContext, RelativeSource={RelativeSource AncestorType=UserControl}}\">\n                <TextBlock Text=\"{Binding Folder.FolderActionState}\"   \n                           HorizontalAlignment=\"Left\"\n                            FontSize=\"16\" FontWeight=\"SemiBold\" />\n                <TextBlock Margin=\"0 5 0 0\" Text=\"{Binding CompressionProgressFile, Mode=OneWay, Converter={StaticResource TokenisedFolderPathConverter}}\"\n           HorizontalAlignment=\"Left\" Foreground=\"{StaticResource AccentFillColorDisabledBrush}\" \n           TextTrimming=\"CharacterEllipsis\" />\n                <ProgressBar Height=\"30\"\n                             Margin=\"0 50 0 50\"\n                             Background=\"{StaticResource CardBackground}\"\n                             Foreground=\"#92F1AB\"\n                             Value=\"{Binding CompressionProgress, Mode=OneWay}\" />\n                \n\n                <StackPanel Orientation=\"Horizontal\" HorizontalAlignment=\"Center\" >\n                    <ui:Button Width=\"80\" Height=\"35\" Margin=\"0 0 20 0\" Command=\"{Binding PauseCommand}\">\n                        <ui:SymbolIcon Symbol=\"{Binding Folder.FolderActionState, Converter={StaticResource FolderWorkingStateToPauseSymbolConverter}}\" Filled=\"True\"/>\n                    </ui:Button>\n                    <ui:Button Width=\"80\" Height=\"35\" Command=\"{Binding CancelCommand}\">\n                        <ui:SymbolIcon Symbol=\"Dismiss12\" Foreground=\"{StaticResource PaletteRedBrush}\"/>\n                    </ui:Button>\n                </StackPanel>\n                \n            </StackPanel>\n\n        </DataTemplate>\n\n        <DataTemplate x:Key=\"ResultsTemplate\">\n\n            <local:ResultsTemplate DataContext=\"{Binding DataContext, RelativeSource={RelativeSource AncestorType=UserControl}}\" />\n\n        </DataTemplate>\n\n        <local:FolderActionStateTemplateSelector x:Key=\"FolderActionStateTemplateSelector\"\n                                                 AnalysingTemplate=\"{StaticResource AnalysingTemplate}\"\n                                                 CompressingTemplate=\"{StaticResource CompressingTemplate}\"\n                                                 IdleTemplate=\"{StaticResource IdleTemplate}\"\n                                                 ResultsTemplate=\"{StaticResource ResultsTemplate}\" />\n\n    </UserControl.Resources>\n    <Grid MinWidth=\"20\"\n          Margin=\"5 0 10 0\" VerticalAlignment=\"Stretch\">\n\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"130\" />\n            <RowDefinition Height=\"2\" />\n            <RowDefinition Height=\"*\" />\n        </Grid.RowDefinitions>\n\n        <Grid Grid.Row=\"0\" Height=\"130\" Margin=\"20 0 0 0\">\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"*\" MinWidth=\"350\" />\n                <ColumnDefinition MaxWidth=\"200\"  />\n            </Grid.ColumnDefinitions>\n\n            <Grid.RowDefinitions>\n                <RowDefinition Height=\"*\" />\n                <RowDefinition Height=\"*\" />\n            </Grid.RowDefinitions>\n\n            <StackPanel Orientation=\"Horizontal\" Margin=\"0 12\" Grid.ColumnSpan=\"3\">\n                <ui:TextBlock Text=\"{Binding Folder.DisplayName}\" \n                d:Text=\"Hades\" FontSize=\"30\" FontWeight=\"SemiBold\" />\n                <Border Margin=\"14,6\" Padding=\"7 2 7 2\" Background=\"#20FFFFFF\" CornerRadius=\"4\" Visibility=\"{Binding IsSteamIDVisible, Converter={StaticResource BoolToVisConverter}}\">\n                    <StackPanel Orientation=\"Horizontal\">\n                        <Viewbox \n           Width=\"12\" Height=\"12\"\n           Margin=\"0 1 5 0\"  VerticalAlignment=\"Center\">\n                            <Path Data=\"M110.5,87.3c0,0.2,0,0.4,0,0.6L82,129.3c-4.6-0.2-9.3,0.6-13.6,2.4c-1.9,0.8-3.8,1.8-5.5,2.9L0.3,108.8  c0,0-1.4,23.8,4.6,41.6l44.3,18.3c2.2,9.9,9,18.6,19.1,22.8c16.4,6.9,35.4-1,42.2-17.4c1.8-4.3,2.6-8.8,2.5-13.3l40.8-29.1  c0.3,0,0.7,0,1,0c24.4,0,44.3-19.9,44.3-44.3c0-24.4-19.8-44.3-44.3-44.3C130.4,43,110.5,62.9,110.5,87.3z M103.7,171.2  c-5.3,12.7-19.9,18.7-32.6,13.4c-5.9-2.4-10.3-6.9-12.8-12.2l14.4,6c9.4,3.9,20.1-0.5,24-9.9c3.9-9.4-0.5-20.1-9.9-24l-14.9-6.2  c5.7-2.2,12.3-2.3,18.4,0.3c6.2,2.6,10.9,7.4,13.5,13.5S106.2,165.1,103.7,171.2 M154.8,116.9c-16.3,0-29.5-13.3-29.5-29.5  c0-16.3,13.2-29.5,29.5-29.5c16.3,0,29.5,13.3,29.5,29.5C184.2,103.6,171,116.9,154.8,116.9 M132.7,87.3c0-12.3,9.9-22.2,22.1-22.2  c12.2,0,22.1,9.9,22.1,22.2c0,12.3-9.9,22.2-22.1,22.2C142.6,109.5,132.7,99.5,132.7,87.3z M233,116.5c0,64.3-52.2,116.5-116.5,116.5S0,180.8,0,116.5c0-30.4,11-60.2,30.7-78.8C53.5,16.1,82.5,0,116.5,0  C180.8,0,233,52.2,233,116.5z\" Fill=\"#A0FFFFFF\" />\n                        </Viewbox>\n                        <TextBlock Text=\"{Binding Folder.SteamAppID, StringFormat={} {0}}\" VerticalAlignment=\"Center\" Foreground=\"#60FFFFFF\" FontSize=\"13\" />\n                    </StackPanel>\n                </Border>\n\n            </StackPanel>\n          \n            <ui:TextBlock Text=\"{Binding Folder.FolderName, Converter={StaticResource TokenisedFolderPathConverter}}\"\n                          VerticalAlignment=\"Bottom\" Margin=\"0 0 0 -5\" Grid.ColumnSpan=\"3\"\n                          d:Text=\"F: &gt;SteamGames &gt; common &gt; Hades\"\n                          FontSize=\"15\" FontWeight=\"Regular\" TextTrimming=\"WordEllipsis\"\n                          Foreground=\"#30FFFFFF\" />\n\n            <StackPanel Grid.Row=\"1\" Orientation=\"Horizontal\" Margin=\"0 0 0 5\">\n                <ui:TextBlock Text=\"uncompressed size\" FontSize=\"13\" Foreground=\"#30FFFFFF\" VerticalAlignment=\"Bottom\" Margin=\"0 0 8 -1\"/>\n                <ui:TextBlock Text=\"{Binding Folder.UncompressedBytes, Converter={StaticResource BytesToReadableConverter}}\" FontSize=\"20\" VerticalAlignment=\"Bottom\" Width=\"80\"/>\n                <ui:TextBlock Text=\"contained files\" FontSize=\"13\" Foreground=\"#30FFFFFF\" VerticalAlignment=\"Bottom\" Margin=\"30 0 8 -1\" />\n                <ui:TextBlock Text=\"{Binding TotalFiles, Converter={StaticResource NumberWithSpacesConverter}}\" FontSize=\"20\" VerticalAlignment=\"Bottom\"/>\n            </StackPanel>\n            \n            <StackPanel Grid.Column=\"1\" Grid.ColumnSpan=\"3\" Grid.Row=\"1\" Margin=\"0 0 -10 8\"\n                        VerticalAlignment=\"Bottom\" HorizontalAlignment=\"Right\"\n                        Orientation=\"Horizontal\">\n               \n                <ui:SymbolIcon Filled=\"True\" FontSize=\"20\" VerticalAlignment=\"Center\"\n                               Foreground=\"{Binding Folder.FolderActionState, Converter={StaticResource FolderStatusToColorConverter}}\"\n                               Symbol=\"Circle24\" />\n                <ui:TextBlock Margin=\"10 1 10 0\" Foreground=\"{StaticResource TextFillColorTertiaryBrush}\" VerticalAlignment=\"Center\" FontSize=\"14\">\n                    <!--<Run Text=\"Status: \"/>-->\n                    <Run Text=\"{Binding Folder.FolderActionState, Converter={StaticResource FolderStatusToStringConverter}}\" Foreground=\"#30FFFFFF\" />\n                </ui:TextBlock>\n            </StackPanel>\n\n            <!--<ui:ProgressRing Grid.RowSpan=\"2\" Grid.Column=\"1\"\n                             HorizontalAlignment=\"Center\"\n                             IsIndeterminate=\"True\"\n                             Visibility=\"{Binding IsAnalysing, Converter={StaticResource BooleanToVisibilityConverter}}\" />\n\n            <ProgressBar Grid.Row=\"0\" Grid.Column=\"1\"\n                         Height=\"12\"\n                         Margin=\"0,-10,0,0\"\n                         Foreground=\"#F1CE92\"\n                         Visibility=\"{Binding IsNotResultsOrAnalysing, Converter={StaticResource BooleanToVisibilityConverter}}\"\n                         Value=\"100\" />\n            <ProgressBar Grid.Row=\"1\" Grid.Column=\"1\"\n                         Height=\"12\"\n                         Margin=\"0,-10,0,0\"\n                         Background=\"{StaticResource CardBackground}\"\n                         Foreground=\"#92F1AB\"\n                         Maximum=\"{Binding Folder.UncompressedBytes}\"\n                         Visibility=\"{Binding IsNotResultsOrAnalysing, Converter={StaticResource BooleanToVisibilityConverter}}\"\n                         Value=\"{Binding DisplayedFolderAfterSize, Mode=OneWay}\" />\n\n            <StackPanel Grid.Row=\"0\" Grid.Column=\"2\"\n                        VerticalAlignment=\"Center\"\n                        Visibility=\"{Binding IsNotResultsOrAnalysing, Converter={StaticResource BooleanToVisibilityConverter}}\">\n                <TextBlock Text=\"{Binding Folder.UncompressedBytes, Converter={StaticResource BytesToReadableConverter}}\"\n                           HorizontalAlignment=\"Right\"\n                           FontSize=\"20\" />\n                <TextBlock Text=\"Before\"\n                           HorizontalAlignment=\"Right\"\n                           FontSize=\"14\"\n                           Foreground=\"{StaticResource AccentFillColorDisabledBrush}\" />\n            </StackPanel>\n\n            <StackPanel Grid.Row=\"1\" Grid.Column=\"2\"\n                        VerticalAlignment=\"Center\"\n                        Visibility=\"{Binding IsNotResultsOrAnalysing, Converter={StaticResource BooleanToVisibilityConverter}}\">\n                <TextBlock Text=\"{Binding DisplayedFolderAfterSize, Mode=OneWay, Converter={StaticResource BytesToReadableConverter}}\"\n                           HorizontalAlignment=\"Right\"\n                           FontSize=\"20\" />\n                <TextBlock Text=\"After\"\n                           HorizontalAlignment=\"Right\"\n                           FontSize=\"14\"\n                           Foreground=\"{StaticResource AccentFillColorDisabledBrush}\" />\n            </StackPanel>-->\n\n        </Grid>\n        <Separator Grid.Row=\"1\"\n                   Height=\"2\" Background=\"#30FFFFFF\"\n                   BorderThickness=\"0\"  />\n        <ui:DynamicScrollViewer Grid.Row=\"2\"\n                                CanContentScroll=\"True\" Focusable=\"False\" IsDeferredScrollingEnabled=\"False\">\n\n\n            <ContentControl Content=\"{Binding Folder.FolderActionState}\" Margin=\"20 0\"\n                            VerticalAlignment=\"Stretch\" HorizontalContentAlignment=\"Stretch\"\n                            ContentTemplateSelector=\"{StaticResource FolderActionStateTemplateSelector}\" />\n\n\n\n        </ui:DynamicScrollViewer>\n\n\n        \n\n    </Grid>\n\n</UserControl>\n"
  },
  {
    "path": "CompactGUI/Views/Pages/FolderView.xaml.vb",
    "content": "﻿Public Class FolderView\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/Pages/HomePage.xaml",
    "content": "﻿<Page x:Class=\"HomePage\"\n      xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n      xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n      xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n      xmlns:local=\"clr-namespace:CompactGUI\"\n      xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n      xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\" Background=\"Transparent\"\n      x:Name=\"Root\" Title=\"HomePage\" AllowDrop=\"True\" DragOver=\"Root_DragOver\" Drop=\"Root_Drop\"\n      d:DataContext=\"{d:DesignInstance Type=local:HomeViewModel}\"\n      d:DesignHeight=\"450\" d:DesignWidth=\"800\"\n      mc:Ignorable=\"d\">\n\n    <Page.Resources>\n\n        <DataTemplate x:Key=\"IdleTemplate\">\n\n            <Button Content=\"Compress Selected\"\n                    Width=\"160\" Height=\"40\"\n                    HorizontalAlignment=\"Center\" VerticalAlignment=\"Bottom\"\n                    Background=\"{StaticResource CardBackgroundFillColorSecondaryBrush}\"\n                    Command=\"{Binding CompressAllCommand}\"\n                    DataContext=\"{Binding DataContext, RelativeSource={RelativeSource AncestorType=Page}}\" />\n\n        </DataTemplate>\n\n        <DataTemplate x:Key=\"AnalysingTemplate\">\n            <ui:ProgressRing HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                             IsIndeterminate=\"True\" />\n        </DataTemplate>\n\n        <DataTemplate x:Key=\"CompressingTemplate\">\n            <ui:ProgressRing HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                  IsIndeterminate=\"True\" />\n            <!--<StackPanel Margin=\"0,20\">\n                <TextBlock Text=\"Working\" FontSize=\"18\" />\n                <ProgressBar Margin=\"0,10\" IsIndeterminate=\"True\" />\n            </StackPanel>-->\n        </DataTemplate>\n\n        <DataTemplate x:Key=\"ResultsTemplate\">\n            <!--<TextBlock Text=\"Results State\" />-->\n\n        </DataTemplate>\n\n        <local:HomeViewStateTemplateSelector x:Key=\"HomeViewStateTemplateSelector\"\n                                             AnalysingTemplate=\"{StaticResource AnalysingTemplate}\"\n                                             CompressingTemplate=\"{StaticResource CompressingTemplate}\"\n                                             IdleTemplate=\"{StaticResource IdleTemplate}\"\n                                             ResultsTemplate=\"{StaticResource ResultsTemplate}\" />\n    </Page.Resources>\n\n    <Grid Margin=\"10,0,10,10\" VerticalAlignment=\"Stretch\">\n\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"auto\" />\n            <RowDefinition Height=\"*\" />\n        </Grid.RowDefinitions>\n\n        <StackPanel x:Name=\"FreshLaunchSelector\"\n                    Grid.Row=\"0\" Grid.RowSpan=\"2\" Grid.ColumnSpan=\"3\"\n                    Margin=\"0,-90,0,0\" HorizontalAlignment=\"Center\" VerticalAlignment=\"Center\"\n                    Visibility=\"{Binding HomeViewIsFresh, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}\">\n            <Label HorizontalAlignment=\"Center\">\n                <TextBlock Text=\"CompactGUI\" FontSize=\"32\" />\n            </Label>\n            <Border Background=\"#20f44336\" CornerRadius=\"10\" Width=\"70\" Height=\"20\" Visibility=\"{Binding IsAdmin, Converter={StaticResource BooleanToVisibilityConverter}}\">\n                <TextBlock  Text=\"Admin\" Foreground=\"White\" HorizontalAlignment=\"Center\" FontSize=\"12\" VerticalAlignment=\"Center\"/>\n\n            </Border>\n            <Button x:Name=\"BtnAddFolder1\"\n                    Width=\"300\" Height=\"40\"\n                    Margin=\"0,20,0,0\" HorizontalContentAlignment=\"Stretch\">\n                <Grid>\n                    <ui:SymbolIcon HorizontalAlignment=\"Left\" Symbol=\"Search12\" />\n                    <TextBlock Text=\"select a folder\" HorizontalAlignment=\"Center\" />\n                </Grid>\n            </Button>\n\n            <StackPanel.RenderTransform>\n                <TranslateTransform x:Name=\"Transform\" Y=\"20\" />\n            </StackPanel.RenderTransform>\n            <StackPanel.Style>\n                <Style TargetType=\"StackPanel\">\n                    <Setter Property=\"Opacity\" Value=\"0\" />\n                    <Style.Triggers>\n                        <DataTrigger Binding=\"{Binding Visibility, RelativeSource={RelativeSource Self}}\" Value=\"Visible\">\n                            <DataTrigger.EnterActions>\n                                <BeginStoryboard>\n                                    <Storyboard>\n                                        <!-- Fade-in animation -->\n                                        <DoubleAnimation Storyboard.TargetProperty=\"Opacity\"\n                                             From=\"0\" To=\"1\"\n                                             Duration=\"0:0:0.5\" />\n\n                                        <!-- Move upwards animation -->\n                                        <DoubleAnimation Storyboard.TargetProperty=\"(RenderTransform).(TranslateTransform.Y)\"\n                                             From=\"30\" To=\"0\" \n                                             Duration=\"0:0:0.5\">\n                                            <DoubleAnimation.EasingFunction>\n                                                <CircleEase/>\n                                            </DoubleAnimation.EasingFunction>\n                                        </DoubleAnimation>\n                                    </Storyboard>\n                                </BeginStoryboard>\n                            </DataTrigger.EnterActions>\n                        </DataTrigger>\n                    </Style.Triggers>\n                </Style>\n            </StackPanel.Style>\n\n        </StackPanel>\n        <Label Grid.Row=\"1\" VerticalAlignment=\"Bottom\" Margin=\"0 0 0 20\" Foreground=\"{StaticResource CardForegroundDisabled}\" HorizontalAlignment=\"Center\" Visibility=\"{Binding HomeViewIsFresh, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}\">\n            <TextBlock Text=\"{Binding DisplayVersion, StringFormat={}Version {0}}\"/>\n            <Label.RenderTransform>\n                <TranslateTransform x:Name=\"TransformL\" Y=\"20\" />\n            </Label.RenderTransform>\n            <Label.Style>\n                <Style TargetType=\"Label\">\n                    <Setter Property=\"Opacity\" Value=\"0\" />\n                    <Style.Triggers>\n                        <DataTrigger Binding=\"{Binding Visibility, RelativeSource={RelativeSource Self}}\" Value=\"Visible\">\n                            <DataTrigger.EnterActions>\n                                <BeginStoryboard>\n                                    <Storyboard>\n                                        <DoubleAnimation Storyboard.TargetProperty=\"Opacity\" From=\"0\" To=\"0\"/>\n                                        <!-- Fade-in animation -->\n                                        <DoubleAnimation Storyboard.TargetProperty=\"Opacity\"\n                                 From=\"0\" To=\"1\" BeginTime=\"0:0:0.5\"\n                                 Duration=\"0:0:1.5\" />\n\n                                        <!-- Move upwards animation -->\n                                        <DoubleAnimation Storyboard.TargetProperty=\"(RenderTransform).(TranslateTransform.Y)\"\n                                 From=\"10\" To=\"0\" BeginTime=\"0:0:0.5\"\n                                 Duration=\"0:0:0.5\">\n                                            <DoubleAnimation.EasingFunction>\n                                                <CircleEase/>\n                                            </DoubleAnimation.EasingFunction>\n                                        </DoubleAnimation>\n                                    </Storyboard>\n                                </BeginStoryboard>\n                            </DataTrigger.EnterActions>\n                        </DataTrigger>\n                    </Style.Triggers>\n                </Style>\n            </Label.Style>\n        </Label>\n\n\n        <Grid x:Name=\"BaseHomeView\"\n              Grid.Row=\"1\"\n              HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Stretch\"\n              Visibility=\"{Binding HomeViewIsFresh, Converter={StaticResource BooleanToInverseVisibilityConverter}, Mode=OneWay}\">\n            <Grid.ColumnDefinitions>\n                <ColumnDefinition Width=\"370\" MinWidth=\"370\" MaxWidth=\"700\"/>\n                <ColumnDefinition Width=\"2\" />\n                <ColumnDefinition Width=\"*\" MinWidth=\"200\" />\n            </Grid.ColumnDefinitions>\n\n            <ui:Card VerticalAlignment=\"Stretch\" VerticalContentAlignment=\"Stretch\"\n                     Background=\"Transparent\" BorderThickness=\"0\">\n\n                <Grid Margin=\"10\" VerticalAlignment=\"Stretch\">\n                    <Grid.RowDefinitions>\n                        <RowDefinition Height=\"auto\" />\n                        <RowDefinition Height=\"*\" />\n                        <RowDefinition Height=\"90\" />\n                    </Grid.RowDefinitions>\n\n                    <StackPanel Grid.Row=\"0\"\n                                Margin=\"0,0,0,30\"\n                                Orientation=\"Horizontal\">\n                        <ui:Button x:Name=\"BtnAddFolder2\"\n                                   Width=\"38\" Height=\"38\"\n                                   Background=\"{StaticResource CardBackgroundFillColorSecondaryBrush}\">\n                            <ui:SymbolIcon Symbol=\"Add16\" />\n                        </ui:Button>\n                        <TextBlock Text=\"Add Folder to Queue\"\n                                   Margin=\"15,-1,0,0\" VerticalAlignment=\"Center\" />\n                    </StackPanel>\n\n                    <ui:ListView Grid.Row=\"1\"\n                                 Margin=\"0,0,-20,0\"\n                                 ItemsSource=\"{Binding Folders}\"\n                                 ScrollViewer.HorizontalScrollBarVisibility=\"Disabled\"\n                                 SelectedItem=\"{Binding SelectedFolder, Mode=TwoWay}\">\n                        <ui:ListView.ItemContainerStyle>\n                            <Style TargetType=\"ListViewItem\">\n                                <Setter Property=\"Template\">\n                                    <Setter.Value>\n                                        <ControlTemplate TargetType=\"ListViewItem\">\n                                            <Border Background=\"{TemplateBinding Background}\">\n                                                <ContentPresenter />\n                                            </Border>\n                                        </ControlTemplate>\n                                    </Setter.Value>\n                                </Setter>\n                            </Style>\n                        </ui:ListView.ItemContainerStyle>\n                        <ui:ListView.ItemTemplate>\n                            <DataTemplate>\n                                <Grid Height=\"50\" Margin=\"5,0,30,0\">\n\n                                    <Grid.ColumnDefinitions>\n                                        <ColumnDefinition Width=\"*\" />\n                                        <ColumnDefinition Width=\"30\" />\n                                    </Grid.ColumnDefinitions>\n\n                                    <Border x:Name=\"ContainingGrid\"\n                                            Grid.Column=\"0\" Grid.ColumnSpan=\"2\"\n                                            Margin=\"10,0,-10,0\"\n                                            CornerRadius=\"5\">\n                                        <Border.Background>\n                                            <LinearGradientBrush Opacity=\"0\" StartPoint=\"0,0\" EndPoint=\"1,1\">\n                                                <GradientStop Offset=\"0.4\" Color=\"#FFFFFFFF\" />\n                                                <GradientStop Offset=\"1\" Color=\"#FFFFFFFF\" />\n                                            </LinearGradientBrush>\n                                        </Border.Background>\n\n                                    </Border>\n\n                                    <Border Grid.Column=\"0\"\n                                            Width=\"4\" Height=\"30\"\n                                            Margin=\"0,0,0,0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n                                            Background=\"{StaticResource AccentFillColorDefaultBrush}\"\n                                            CornerRadius=\"2\"\n                                            Visibility=\"{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}\" />\n\n                                    <StackPanel Orientation=\"Horizontal\" Margin=\"10,0,0,0\" >\n                                        <ui:ProgressRing Width=\"20\" Height=\"20\" Margin=\"10 0 0 0\" Background=\"Red\" IsIndeterminate=\"True\" Visibility=\"{Binding FolderActionState, Converter={StaticResource FolderActionStateWorkingToVisibilityConverter}}\"/>\n                                        <StackPanel Margin=\"10 0 0 0\" HorizontalAlignment=\"Left\" VerticalAlignment=\"Center\"\n                                                Orientation=\"Vertical\">\n                                        \n                                        <TextBlock Text=\"{Binding DisplayName}\" FontSize=\"15\" />\n\n                                        <TextBlock Text=\"{Binding FolderName, Converter={StaticResource TokenisedFolderPathConverter}}\"\n                                                   Grid.Column=\"0\"\n                                                   HorizontalAlignment=\"Left\"\n                                                   FontSize=\"12\"\n                                                   Foreground=\"{StaticResource AccentFillColorDisabledBrush}\"\n                                                   TextTrimming=\"CharacterEllipsis\" />\n                                    </StackPanel>\n                                    </StackPanel>\n\n                                  \n\n\n\n\n\n                                    <ui:Button Grid.Column=\"1\"\n                                               Width=\"26\" Height=\"26\"\n                                               Padding=\"0\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                               BorderBrush=\"{StaticResource PaletteRedBrush}\"\n                                               Command=\"{Binding DataContext.RemoveFolderCommand, RelativeSource={RelativeSource AncestorType=Page}}\"\n                                               CommandParameter=\"{Binding}\">\n                                        <ui:SymbolIcon FontSize=\"12\"\n                                                       Foreground=\"{StaticResource PaletteRedBrush}\"\n                                                       Symbol=\"Dismiss12\" />\n                                        <ui:Button.Style>\n                                            <Style BasedOn=\"{StaticResource {x:Type ui:Button}}\" TargetType=\"ui:Button\">\n                                                <Setter Property=\"Opacity\" Value=\"0\" />\n                                                <Style.Triggers>\n                                                    <DataTrigger Binding=\"{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=Grid}}\" Value=\"True\">\n                                                        <DataTrigger.EnterActions>\n                                                            <BeginStoryboard>\n                                                                <Storyboard>\n                                                                    <DoubleAnimation Storyboard.TargetProperty=\"Opacity\" To=\"1\" Duration=\"0:0:0.1\" />\n                                                                </Storyboard>\n                                                            </BeginStoryboard>\n                                                        </DataTrigger.EnterActions>\n                                                        <DataTrigger.ExitActions>\n                                                            <BeginStoryboard>\n                                                                <Storyboard>\n                                                                    <DoubleAnimation Storyboard.TargetProperty=\"Opacity\" To=\"0\" Duration=\"0:0:0.1\" />\n                                                                </Storyboard>\n                                                            </BeginStoryboard>\n                                                        </DataTrigger.ExitActions>\n                                                    </DataTrigger>\n                                                </Style.Triggers>\n                                            </Style>\n                                        </ui:Button.Style>\n                                    </ui:Button>\n\n                                </Grid>\n                                <DataTemplate.Triggers>\n                                    <EventTrigger RoutedEvent=\"UIElement.MouseEnter\">\n                                        <BeginStoryboard>\n                                            <Storyboard>\n                                                <DoubleAnimation Storyboard.TargetName=\"ContainingGrid\"\n                                                                 Storyboard.TargetProperty=\"(Grid.Background).(LinearGradientBrush.Opacity)\" To=\"0.11\"\n                                                                 Duration=\"0:0:0.2\" />\n                                            </Storyboard>\n                                        </BeginStoryboard>\n                                    </EventTrigger>\n                                    <EventTrigger RoutedEvent=\"UIElement.MouseLeave\">\n                                        <BeginStoryboard>\n                                            <Storyboard>\n                                                <DoubleAnimation Storyboard.TargetName=\"ContainingGrid\"\n                                                                 Storyboard.TargetProperty=\"(Grid.Background).(LinearGradientBrush.Opacity)\" To=\"0\"\n                                                                 Duration=\"0:0:0.05\" />\n                                            </Storyboard>\n                                        </BeginStoryboard>\n                                    </EventTrigger>\n                                </DataTemplate.Triggers>\n                            </DataTemplate>\n                        </ui:ListView.ItemTemplate>\n\n                    </ui:ListView>\n\n                    <ContentControl Content=\"{Binding HomeViewModelState}\"\n                                    Grid.Row=\"2\"\n                                    VerticalAlignment=\"Stretch\" HorizontalContentAlignment=\"Stretch\"\n                                    ContentTemplateSelector=\"{StaticResource HomeViewStateTemplateSelector}\" />\n\n\n\n                </Grid>\n\n            </ui:Card>\n\n            <GridSplitter Width=\"2\" Margin=\"0 20\"\n                          Background=\"{StaticResource SeparatorBorderBrush}\"\n                          ResizeBehavior=\"BasedOnAlignment\"  />\n\n\n\n            <ui:Card Grid.Column=\"2\"\n                     VerticalAlignment=\"Stretch\" VerticalContentAlignment=\"Stretch\"\n                     Background=\"Transparent\" BorderThickness=\"0\">\n\n                <local:FolderView VerticalContentAlignment=\"Stretch\" DataContext=\"{Binding SelectedFolderViewModel}\" />\n\n            </ui:Card>\n        </Grid>\n\n    </Grid>\n</Page>\n"
  },
  {
    "path": "CompactGUI/Views/Pages/HomePage.xaml.vb",
    "content": "﻿Class HomePage\n\n    Private _viewModel As HomeViewModel\n\n\n    Sub New(viewmodel As HomeViewModel)\n\n        ' This call is required by the designer.\n        InitializeComponent()\n        _viewModel = viewmodel\n        DataContext = viewmodel\n\n        ScrollViewer.SetCanContentScroll(Me, False)\n\n        ' Add any initialization after the InitializeComponent() call.\n\n    End Sub\n\n    Private Async Sub AddFolderButton_Click(sender As Object, e As RoutedEventArgs) Handles BtnAddFolder1.Click, BtnAddFolder2.Click\n        Dim folderBrowser As New Microsoft.Win32.OpenFolderDialog With {\n            .Title = \"Select a folder to compress\",\n            .Multiselect = True,\n            .ValidateNames = True\n        }\n        folderBrowser.ShowDialog()\n\n        If folderBrowser.FolderNames.Length > 0 Then\n            Await _viewModel.AddFoldersAsync(folderBrowser.FolderNames)\n        End If\n    End Sub\n\n    Private Sub Root_DragOver(sender As Object, e As DragEventArgs)\n\n        If e.Data.GetDataPresent(DataFormats.FileDrop) Then\n            Dim paths As String() = e.Data.GetData(DataFormats.FileDrop)\n\n            If paths.All(Function(path) IO.Directory.Exists(path)) Then\n                e.Effects = DragDropEffects.Copy\n            Else\n                e.Effects = DragDropEffects.None\n            End If\n\n\n        Else\n            e.Effects = DragDropEffects.None\n        End If\n\n        e.Handled = True\n\n    End Sub\n\n    Private Sub Root_Drop(sender As Object, e As DragEventArgs)\n\n        If e.Data.GetDataPresent(DataFormats.FileDrop) Then\n            Dim paths As String() = e.Data.GetData(DataFormats.FileDrop)\n            If paths.All(Function(path) IO.Directory.Exists(path)) Then\n                _viewModel.AddFoldersAsync(paths).ConfigureAwait(False)\n            End If\n        End If\n\n    End Sub\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/Pages/PendingCompression.xaml",
    "content": "﻿<UserControl x:Class=\"PendingCompression\"\n             xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:core=\"clr-namespace:CompactGUI.Core;assembly=CompactGUI.Core\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:local=\"clr-namespace:CompactGUI\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n             \n             d:DataContext=\"{d:DesignInstance Type=local:FolderViewModel}\"\n             d:DesignHeight=\"450\" d:DesignWidth=\"900\" d:Foreground=\"White\"\n             RenderOptions.ClearTypeHint=\"Enabled\"\n             mc:Ignorable=\"d\">\n\n    <Grid VerticalAlignment=\"Stretch\">\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"auto\" />\n            <RowDefinition Height=\"auto\" />\n            <RowDefinition Height=\"auto\" />\n            <RowDefinition Height=\"*\" />\n        </Grid.RowDefinitions>\n\n        <ui:TextBlock Text=\"Compression Mode\"\n                      Margin=\"0,30,0,20\" VerticalAlignment=\"Center\"\n                      FontSize=\"16\" FontWeight=\"SemiBold\" />\n\n        <WrapPanel Grid.Row=\"1\"\n                   Margin=\"0,0,0,0\" ToolTipService.HasDropShadow=\"True\" ToolTipService.Placement=\"Bottom\"\n                   Orientation=\"Horizontal\">\n\n            <local:CompressionMode_Radio Width=\"185\" Height=\"85\" IsForcedDetailed=\"{Binding AlwaysShowDetailsCompressionMode}\"\n                                         Margin=\"0,0,20,10\" Checked=\"CompressionMode_Radio_Checked\"\n                                         BorderBrush=\"Transparent\" CompressionMode=\"XPRESS 4K\" EstimatedVisibility=\"Visible\"\n                                         IsChecked=\"{Binding Folder.CompressionOptions.SelectedCompressionMode, Converter={StaticResource EnumToRadioButtonConverter}, ConverterParameter={x:Static core:CompressionMode.XPRESS4K}, Mode=TwoWay}\"\n                                         IsEstimating=\"{Binding Folder.IsGettingEstimate}\"\n                                         ProgressValue=\"{Binding Folder.WikiCompressionResults.XPress4K.CompressionSavings}\"\n                                         Savings=\"{Binding Folder.WikiCompressionResults.XPress4K.CompressionSavings}\"\n                                         BytesAfter=\"{Binding Folder.WikiCompressionResults.XPress4K.AfterBytes}\"\n                                         BytesSaved=\"{Binding Folder.WikiCompressionResults.XPress4K.BytesSaved}\"   \n                                         />\n            <local:CompressionMode_Radio Width=\"185\" Height=\"85\" IsForcedDetailed=\"{Binding AlwaysShowDetailsCompressionMode}\"\n                                         Margin=\"0,0,20,10\" Checked=\"CompressionMode_Radio_Checked\"\n                                         BorderBrush=\"Transparent\" CompressionMode=\"XPRESS 8K\" EstimatedVisibility=\"Visible\"\n                                         IsChecked=\"{Binding Folder.CompressionOptions.SelectedCompressionMode, Converter={StaticResource EnumToRadioButtonConverter}, ConverterParameter={x:Static core:CompressionMode.XPRESS8K}, Mode=TwoWay}\"\n                                         IsEstimating=\"{Binding Folder.IsGettingEstimate}\"\n                                         ProgressValue=\"{Binding Folder.WikiCompressionResults.XPress8K.CompressionSavings}\"\n                                         Savings=\"{Binding Folder.WikiCompressionResults.XPress8K.CompressionSavings}\"\n                                         BytesAfter=\"{Binding Folder.WikiCompressionResults.XPress8K.AfterBytes}\"\n                                         BytesSaved=\"{Binding Folder.WikiCompressionResults.XPress8K.BytesSaved}\"   \n                                         />\n            <local:CompressionMode_Radio Width=\"185\" Height=\"85\" IsForcedDetailed=\"{Binding AlwaysShowDetailsCompressionMode}\"\n                                         Margin=\"0,0,20,10\" Checked=\"CompressionMode_Radio_Checked\"\n                                         BorderBrush=\"Transparent\" CompressionMode=\"XPRESS 16K\" EstimatedVisibility=\"Visible\"\n                                         IsChecked=\"{Binding Folder.CompressionOptions.SelectedCompressionMode, Converter={StaticResource EnumToRadioButtonConverter}, ConverterParameter={x:Static core:CompressionMode.XPRESS16K}, Mode=TwoWay}\"\n                                         IsEstimating=\"{Binding Folder.IsGettingEstimate}\"\n                                         ProgressValue=\"{Binding Folder.WikiCompressionResults.XPress16K.CompressionSavings}\"\n                                         Savings=\"{Binding Folder.WikiCompressionResults.XPress16K.CompressionSavings}\"\n                                         BytesAfter=\"{Binding Folder.WikiCompressionResults.XPress16K.AfterBytes}\"\n                                         BytesSaved=\"{Binding Folder.WikiCompressionResults.XPress16K.BytesSaved}\"   \n                                         />\n            <local:CompressionMode_Radio Width=\"185\" Height=\"85\" IsForcedDetailed=\"{Binding AlwaysShowDetailsCompressionMode}\"\n                                         Margin=\"0,0,20,10\" Checked=\"CompressionMode_Radio_Checked\"\n                                         BorderBrush=\"Transparent\" CompressionMode=\"LZX\" EstimatedVisibility=\"Visible\"\n                                         IsChecked=\"{Binding Folder.CompressionOptions.SelectedCompressionMode, Converter={StaticResource EnumToRadioButtonConverter}, ConverterParameter={x:Static core:CompressionMode.LZX}, Mode=TwoWay}\"\n                                         IsEstimating=\"{Binding Folder.IsGettingEstimate}\"\n                                         ProgressValue=\"{Binding Folder.WikiCompressionResults.LZX.CompressionSavings}\"\n                                         Savings=\"{Binding Folder.WikiCompressionResults.LZX.CompressionSavings}\" \n                                         BytesAfter=\"{Binding Folder.WikiCompressionResults.LZX.AfterBytes}\"\n                                         BytesSaved=\"{Binding Folder.WikiCompressionResults.LZX.BytesSaved}\"   \n                                         />\n\n            <WrapPanel.ToolTip>\n                <ToolTip Visibility=\"Collapsed\" MaxWidth=\"500\" Background=\"Transparent\" ClipToBounds=\"False\" BorderThickness=\"0\" BorderBrush=\"Transparent\">\n\n                    <Border Margin=\"5\" Background=\"#4e6379\" Padding=\"10\" CornerRadius=\"5\">\n                        <Border.Effect>\n                            <DropShadowEffect BlurRadius=\"10\" ShadowDepth=\"0\" Opacity=\"0.4\" Direction=\"0\"/>\n                        </Border.Effect>\n                        <TextBlock TextAlignment=\"Left\">\n    <Bold>For Steam Games:</Bold> estimate is based on database results\n    <LineBreak />\n    <LineBreak />\n    <Bold>For Non-Steam Folders:</Bold> estimate is calculated by block analysis.<LineBreak />\n    If estimation is disabled, this will always show 0%</TextBlock>\n                    </Border>\n                    \n                </ToolTip>\n            </WrapPanel.ToolTip>\n\n        </WrapPanel>\n\n\n        <StackPanel Grid.Row=\"2\" Margin=\"0,20,0,0\">\n            <!--<StackPanel.Resources>\n                <SolidColorBrush x:Key=\"CheckBoxCheckBackgroundFillChecked\" Color=\"#4ca5ff\"/>\n                <SolidColorBrush x:Key=\"CheckBoxCheckBackgroundFillCheckedPointerOver\" Color=\"#6fb6ff\"/>\n            </StackPanel.Resources>-->\n            <ui:TextBlock Text=\"Configuration\"\n                          Margin=\"0,0,0,20\" VerticalAlignment=\"Center\"\n                           FontSize=\"16\" FontWeight=\"SemiBold\" />\n            <WrapPanel Margin=\"0,0,0,15\" Orientation=\"Horizontal\">\n                <CheckBox x:Name=\"UiChkSkipPoorlyCompressed\" Checked=\"UiChkSkipPoorlyCompressed_Checked\" Unchecked=\"UiChkSkipPoorlyCompressed_Unchecked\"\n                          Content=\"Skip file types specified in settings\"\n                          Width=\"330\"\n                          FontSize=\"16\" \n                          IsChecked=\"{Binding Folder.CompressionOptions.SkipPoorlyCompressedFileTypes, Mode=TwoWay}\" >\n                   \n                </CheckBox>\n                <TextBlock Margin=\"10,0,0,0\" VerticalAlignment=\"Center\"\n                           FontSize=\"14\" Foreground=\"#FF98A9B9\">\n                    <TextBlock.Style>\n                        <Style TargetType=\"TextBlock\">\n                            <Setter Property=\"Text\" Value=\"\" />\n                            <Style.Triggers>\n                                <DataTrigger Binding=\"{Binding IsChecked, ElementName=UiChkSkipPoorlyCompressed}\" Value=\"True\">\n                                    <Setter Property=\"Text\" Value=\"{Binding Folder.GlobalPoorlyCompressedFileCount, StringFormat={}{0} files will be skipped}\" />\n                                </DataTrigger>\n                            </Style.Triggers>\n                        </Style>\n                    </TextBlock.Style>\n                </TextBlock>\n            </WrapPanel>\n            <WrapPanel Margin=\"0,0,0,15\" Orientation=\"Horizontal\">\n                <CheckBox x:Name=\"UiChkSkipUserPoorlyCompressed\" Checked=\"UiChkSkipUserPoorlyCompressed_Checked\" Unchecked=\"UiChkSkipUserPoorlyCompressed_Unchecked\"\n                          Width=\"330\"\n                          FontSize=\"16\"\n                          IsChecked=\"{Binding Folder.CompressionOptions.SkipUserSubmittedFiletypes, Mode=TwoWay}\">\n                    <TextBlock>\n                        Skip file types likely to compress poorly<Run Text=\" \" />\n                        <InlineUIContainer>\n                            <TextBlock Text=\"(?)\"\n                                       Cursor=\"Hand\" Foreground=\"#FF98A9B9\" TextDecorations=\"Underline\">\n                                <TextBlock.ToolTip>\n                                    <ToolTip MaxWidth=\"450\">\n                                        <TextBlock TextAlignment=\"Left\">\n                                            <Bold>For Steam Games:</Bold>\n                                            skips files based on database results<LineBreak />\n                                            <LineBreak />\n                                            <Bold>For Non-Steam Folders:</Bold>\n                                            skips files based on compression estimate</TextBlock>\n                                    </ToolTip>\n                                </TextBlock.ToolTip>\n                            </TextBlock>\n                        </InlineUIContainer>\n                    </TextBlock>\n                </CheckBox>\n                <TextBlock Margin=\"10,0,0,0\" VerticalAlignment=\"Center\"\n                           FontSize=\"14\" Foreground=\"#FF98A9B9\">\n                    <TextBlock.Style>\n                        <Style TargetType=\"TextBlock\">\n                            <Setter Property=\"Text\" Value=\"\" />\n                            <Style.Triggers>\n                                <DataTrigger Binding=\"{Binding IsChecked, ElementName=UiChkSkipUserPoorlyCompressed}\" Value=\"True\">\n                                    <Setter Property=\"Text\" Value=\"{Binding Folder.WikiPoorlyCompressedFilesCount, StringFormat={}{0} files will be skipped}\" />\n                                </DataTrigger>\n                            </Style.Triggers>\n                        </Style>\n                    </TextBlock.Style>\n                </TextBlock>\n            </WrapPanel>\n\n\n            <CheckBox x:Name=\"uiChkWatchFolderForChanges\" Content=\"Watch folder for changes\" Checked=\"uiChkWatchFolderForChanges_Checked\" Unchecked=\"uiChkWatchFolderForChanges_Unchecked\"\n                      Margin=\"0,0,0,15\"\n                      FontSize=\"16\"\n                      IsChecked=\"{Binding Folder.CompressionOptions.WatchFolderForChanges, Mode=TwoWay}\" />\n\n        </StackPanel>\n\n\n        <ui:Button Content=\"Apply to all\"\n                   Grid.Row=\"3\"\n                   Width=\"140\" Height=\"40\"\n                   Margin=\"0,0,0,10\" HorizontalAlignment=\"Right\" VerticalAlignment=\"Bottom\"\n                   Background=\"{StaticResource CardBackgroundFillColorSecondaryBrush}\"\n                   Command=\"{Binding ApplyToAllCommand}\"\n                   FontSize=\"16\" />\n\n    </Grid>\n</UserControl>\n"
  },
  {
    "path": "CompactGUI/Views/Pages/PendingCompression.xaml.vb",
    "content": "﻿Imports CompactGUI.Core.Settings\n\nPublic Class PendingCompression\n\n    Private ReadOnly _settingsService As ISettingsService\n\n    Sub New()\n        InitializeComponent()\n        _settingsService = Application.GetService(Of ISettingsService)\n    End Sub\n\n    Private Sub CompressionMode_Radio_Checked(sender As Object, e As RoutedEventArgs)\n        Dim radio As RadioButton = CType(sender, RadioButton)\n\n        Dim ret As FolderViewModel = CType(radio.DataContext, FolderViewModel)\n\n        _settingsService.AppSettings.SelectedCompressionMode = ret.Folder.CompressionOptions.SelectedCompressionMode\n        _settingsService.SaveSettings()\n\n    End Sub\n\n    Private Sub UiChkSkipPoorlyCompressed_Checked(sender As Object, e As RoutedEventArgs)\n        _settingsService.AppSettings.SkipNonCompressable = True\n        _settingsService.SaveSettings()\n\n    End Sub\n\n    Private Sub UiChkSkipPoorlyCompressed_Unchecked(sender As Object, e As RoutedEventArgs)\n        If Not IsVisible Then Return ' Prevents issues when the page is not fully loaded\n        _settingsService.AppSettings.SkipNonCompressable = False\n        _settingsService.SaveSettings()\n    End Sub\n\n    Private Sub UiChkSkipUserPoorlyCompressed_Checked(sender As Object, e As RoutedEventArgs)\n        _settingsService.AppSettings.SkipUserNonCompressable = True\n        _settingsService.SaveSettings()\n    End Sub\n\n    Private Sub UiChkSkipUserPoorlyCompressed_Unchecked(sender As Object, e As RoutedEventArgs)\n        If Not IsVisible Then Return\n        _settingsService.AppSettings.SkipUserNonCompressable = False\n        _settingsService.SaveSettings()\n    End Sub\n\n    Private Sub uiChkWatchFolderForChanges_Checked(sender As Object, e As RoutedEventArgs)\n        _settingsService.AppSettings.WatchFolderForChanges = True\n        _settingsService.SaveSettings()\n    End Sub\n\n    Private Sub uiChkWatchFolderForChanges_Unchecked(sender As Object, e As RoutedEventArgs)\n        If Not IsVisible Then Return\n        _settingsService.AppSettings.WatchFolderForChanges = False\n        _settingsService.SaveSettings()\n    End Sub\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/Pages/ResultsTemplate.xaml",
    "content": "﻿<UserControl x:Class=\"ResultsTemplate\"\n             xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n             xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n             xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n             xmlns:local=\"clr-namespace:CompactGUI\"\n             xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n             xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n             d:DesignHeight=\"450\" d:DesignWidth=\"800\"\n             mc:Ignorable=\"d\">\n    <Grid Margin=\"0,30,0,0\" HorizontalAlignment=\"Stretch\" VerticalAlignment=\"Stretch\"\n          d:DataContext=\"{d:DesignInstance Type=local:FolderViewModel}\">\n\n        <Grid.ColumnDefinitions>\n            <ColumnDefinition MinWidth=\"10\" />\n            <ColumnDefinition MaxWidth=\"90\" />\n        </Grid.ColumnDefinitions>\n        <Grid.RowDefinitions>\n            <RowDefinition Height=\"auto\" />\n            <RowDefinition Height=\"60\" />\n            <RowDefinition Height=\"60\" />\n            <RowDefinition Height=\"auto\" />\n            <RowDefinition />\n        </Grid.RowDefinitions>\n\n        <TextBlock Text=\"Compression Summary\"\n                   HorizontalAlignment=\"Left\"\n                   FontSize=\"16\" FontWeight=\"SemiBold\" />\n\n\n        <ProgressBar Grid.Row=\"1\" Grid.Column=\"0\"\n                     Height=\"24\"\n                     Foreground=\"#F1CE92\"\n                     Value=\"100\" />\n        <ProgressBar Grid.Row=\"2\" Grid.Column=\"0\"\n                     Height=\"24\"\n                     Background=\"{StaticResource CardBackground}\"\n                     Foreground=\"#92F1AB\"\n                     Maximum=\"{Binding Folder.UncompressedBytes}\"\n                     Value=\"{Binding Folder.CompressedBytes}\" />\n\n        <StackPanel Grid.Row=\"1\" Grid.Column=\"1\"\n                    VerticalAlignment=\"Center\">\n            <TextBlock Text=\"{Binding Folder.UncompressedBytes, Converter={StaticResource BytesToReadableConverter}}\"\n                       HorizontalAlignment=\"Right\"\n                       FontSize=\"18\" FontWeight=\"SemiBold\" />\n            <TextBlock Text=\"Before\"\n                       Margin=\"0,-2\" HorizontalAlignment=\"Right\"\n                       FontSize=\"14\"\n                       Foreground=\"{StaticResource AccentFillColorDisabledBrush}\" />\n        </StackPanel>\n\n        <StackPanel Grid.Row=\"2\" Grid.Column=\"1\"\n                    VerticalAlignment=\"Center\">\n            <TextBlock Text=\"{Binding Folder.CompressedBytes, Converter={StaticResource BytesToReadableConverter}}\"\n                       HorizontalAlignment=\"Right\"\n                       FontSize=\"18\" FontWeight=\"SemiBold\" />\n            <TextBlock Text=\"After\"\n                       Margin=\"0,-2\" HorizontalAlignment=\"Right\"\n                       FontSize=\"14\"\n                       Foreground=\"{StaticResource AccentFillColorDisabledBrush}\" />\n        </StackPanel>\n\n\n\n\n        <WrapPanel Grid.Row=\"3\" Grid.ColumnSpan=\"2\"\n                   Margin=\"0,20\" \n                   Orientation=\"Horizontal\">\n            <ui:Card Width=\"200\"\n                     Margin=\"0,0,20,20\" Padding=\"10,20,10,20\"\n                     Background=\"#30FFFFFF\" BorderThickness=\"0\">\n                <Grid>\n                    <ui:TextBlock Text=\"Space Saved\"\n                                  Margin=\"0,0,10,-4\" VerticalAlignment=\"Center\"\n                                  FontSize=\"13\" Foreground=\"#80FFFFFF\" />\n                    <ui:TextBlock Text=\"{Binding Folder.CompressionRatio, Mode=OneWay, Converter={StaticResource DecimalToPercentageConverter}, ConverterParameter='IF'}\"\n                                  HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                  FontSize=\"18\" FontWeight=\"SemiBold\" Foreground=\"#92F1AB\" />\n                </Grid>\n            </ui:Card>\n            <ui:Card Width=\"200\"\n                     Margin=\"0,0,20,20\" Padding=\"10,20,10,20\"\n                     Background=\"#30FFFFFF\" BorderThickness=\"0\">\n                <Grid>\n                    <ui:TextBlock Text=\"Files Compressed\"\n                                  Margin=\"0,0,10,-4\" VerticalAlignment=\"Center\"\n                                  FontSize=\"13\" Foreground=\"#80FFFFFF\" />\n                    <StackPanel HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                Orientation=\"Horizontal\">\n                        <ui:TextBlock Text=\"{Binding TotalCompressedFiles}\"\n                                      FontSize=\"18\" FontWeight=\"SemiBold\" Foreground=\"#92F1AB\" />\n                    </StackPanel>\n\n\n                </Grid>\n            </ui:Card>\n            <ui:Card Width=\"200\"\n                     Margin=\"0,0,20,20\" Padding=\"10,20,10,20\"\n                     Background=\"#30FFFFFF\" BorderThickness=\"0\">\n                <Grid>\n                    <ui:TextBlock Text=\"Compression Mode\"\n                                  Margin=\"0,0,10,-4\" VerticalAlignment=\"Center\"\n                                  FontSize=\"13\" Foreground=\"#80FFFFFF\" />\n                    <ui:TextBlock Text=\"{Binding DominantCompressionMode, Converter={StaticResource CompressionLevelAbbreviatedConverter}}\"\n                                  HorizontalAlignment=\"Right\" VerticalAlignment=\"Center\"\n                                  FontSize=\"18\" FontWeight=\"SemiBold\" Foreground=\"#92F1AB\" />\n                </Grid>\n            </ui:Card>\n\n\n        </WrapPanel>\n\n\n        <StackPanel Grid.Row=\"5\" Grid.ColumnSpan=\"2\"\n                    HorizontalAlignment=\"Right\" VerticalAlignment=\"Bottom\"\n                    Orientation=\"Horizontal\">\n\n            <ui:Button Content=\"Uncompress\"\n                       Width=\"140\" Height=\"40\"\n                       Margin=\"0,0,0,10\"\n                       Background=\"{StaticResource CardBackgroundFillColorSecondaryBrush}\"\n                       Command=\"{Binding UncompressCommand}\"\n                       FontSize=\"16\" />\n            <ui:Button Content=\"Compress Again\"\n                       Width=\"140\" Height=\"40\"\n                       Margin=\"40,0,0,10\"\n                       Background=\"{StaticResource CardBackgroundFillColorSecondaryBrush}\"\n                       Command=\"{Binding CompressAgainCommand}\"\n                       FontSize=\"16\"\n                       Visibility=\"{Binding Folder.IsFreshlyCompressed, Converter={StaticResource BooleanToInverseVisibilityConverter}}\" />\n            <ui:Button Content=\"Submit Results\"\n                       Width=\"140\" Height=\"40\"\n                       Margin=\"40,0,0,10\"\n                       Background=\"{StaticResource CardBackgroundFillColorSecondaryBrush}\"\n                       Command=\"{Binding SubmitToWikiCommand}\"\n                       FontSize=\"16\">\n                <ui:Button.Visibility>\n                    <MultiBinding Converter=\"{StaticResource IsSteamFolderAndFreshlyCompressedMultiConverter}\">\n                        <Binding Path=\"Folder.IsFreshlyCompressed\" />\n                        <Binding Converter=\"{StaticResource IsSteamFolderConverter}\" Path=\"Folder\" />\n                    </MultiBinding>\n                </ui:Button.Visibility>\n            </ui:Button>\n\n        </StackPanel>\n\n\n\n\n    </Grid>\n\n</UserControl>\n"
  },
  {
    "path": "CompactGUI/Views/Pages/ResultsTemplate.xaml.vb",
    "content": "﻿Public Class ResultsTemplate\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/Pages/WatcherPage.xaml",
    "content": "﻿<Page x:Class=\"WatcherPage\"\n      xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n      xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n      xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" \n      xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\" \n      xmlns:local=\"clr-namespace:CompactGUI\"\n      xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n      mc:Ignorable=\"d\" \n      d:DesignHeight=\"450\" d:DesignWidth=\"800\"\n      Title=\"WatcherPage\">\n    <Grid Margin=\"10,10,10,10\">\n        <local:FolderWatcherCard />\n    </Grid>\n</Page>\n"
  },
  {
    "path": "CompactGUI/Views/Pages/WatcherPage.xaml.vb",
    "content": "﻿Class WatcherPage\n\n    Public Property viewModel As WatcherViewModel\n    Sub New(VM As WatcherViewModel)\n\n        InitializeComponent()\n        DataContext = VM\n        viewModel = VM\n\n        ScrollViewer.SetCanContentScroll(Me, False)\n\n    End Sub\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI/Views/SettingsPage.xaml",
    "content": "﻿<Page x:Class=\"SettingsPage\"\n      xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"\n      xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\"\n      xmlns:d=\"http://schemas.microsoft.com/expression/blend/2008\"\n      xmlns:i=\"http://schemas.microsoft.com/xaml/behaviors\"\n      xmlns:local=\"clr-namespace:CompactGUI\"\n      xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\"\n      xmlns:ui=\"http://schemas.lepo.co/wpfui/2022/xaml\"\n      d:DataContext=\"{d:DesignInstance Type=local:SettingsViewModel}\"\n      d:Height=\"1400\" d:Width=\"800\"\n      mc:Ignorable=\"d\">\n    <Grid Margin=\"40,20\" ScrollViewer.CanContentScroll=\"False\">\n        <Grid>\n            <TextBlock Text=\"Settings\"\n                       FontSize=\"26\"\n                       Foreground=\"{StaticResource CardForeground}\" />\n\n\n\n            <Grid HorizontalAlignment=\"Right\" VerticalAlignment=\"Top\">\n                <Grid.ColumnDefinitions>\n                    <ColumnDefinition Width=\"*\" />\n                    <ColumnDefinition Width=\"*\" />\n                </Grid.ColumnDefinitions>\n\n                <!--  There are less stupid ways of adding customised SVG to XAML than this...  -->\n\n                <Button Grid.Column=\"1\"\n                        Width=\"137\" Height=\"37\"\n                        Margin=\"10\" Padding=\"0,6\" HorizontalContentAlignment=\"Stretch\"\n                        VerticalContentAlignment=\"Stretch\"\n                        Command=\"{Binding OpenKoFiCommand}\">\n                    <Canvas Opacity=\"1\">\n\n                        <Canvas.RenderTransform>\n                            <ScaleTransform CenterX=\"0\" CenterY=\"0\" ScaleX=\"0.09\" ScaleY=\"0.09\" />\n                        </Canvas.RenderTransform>\n\n                        <Path Data=\"M448.18652,174.15791V102.29854a6.67935,6.67935,0,0,1,6.69922-6.69922h17.29492a23.14135,23.14135,0,0,1,18.5127,37.02636,26.08472,26.08472,0,0,1-13.7627,48.23047h-21.5581a.11874.11874,0,0,1-.12158-.12109c-.24366,0-.24366.12109-.36524.12109A6.75826,6.75826,0,0,1,448.18652,174.15791Zm13.51953-45.55176h10.47461a9.774,9.774,0,0,0,9.62159-9.86523,9.56462,9.56462,0,0,0-9.62159-9.62207H461.70605Zm0,38.73047h15.22461a12.60562,12.60562,0,0,0,0-25.21094H461.70605Z\" Fill=\"#fff\" />\n                        <Path Data=\"M543.30518,180.85615a25.47074,25.47074,0,0,1-25.3335-25.333V126.41377a6.57929,6.57929,0,0,1,6.45508-6.45508,6.49719,6.49719,0,0,1,6.45556,6.45508v29.10938A12.32007,12.32007,0,0,0,543.30518,167.946a12.63347,12.63347,0,0,0,12.78857-12.42285V126.41377a6.39243,6.39243,0,0,1,6.3335-6.45508,6.77437,6.77437,0,0,1,6.57714,6.45508v29.10938C569.00439,169.40791,557.312,180.85615,543.30518,180.85615Z\" Fill=\"#fff\" />\n                        <Path Data=\"M639.7627,126.41377v46.03906a28.77024,28.77024,0,0,1-41.65381,26.06348,6.372,6.372,0,0,1-3.16651-8.64746,6.69181,6.69181,0,0,1,8.64746-2.67871,16.29746,16.29746,0,0,0,7.06446,1.70508,16.09832,16.09832,0,0,0,15.22363-11.084,23.66274,23.66274,0,0,1-11.57031,2.92383,25.62727,25.62727,0,0,1-25.45508-25.57715V126.41377a6.47315,6.47315,0,0,1,6.333-6.45508,6.27215,6.27215,0,0,1,6.45507,6.45508v28.74414a12.60731,12.60731,0,0,0,25.21192,0V126.41377a6.2442,6.2442,0,0,1,6.333-6.45508A6.52308,6.52308,0,0,1,639.7627,126.41377Z\" Fill=\"#fff\" />\n                        <Path Data=\"M752.90332,145.17061v29.23047a6.49756,6.49756,0,0,1-6.45508,6.45507,6.68655,6.68655,0,0,1-6.57715-6.45507V145.17061a12.42434,12.42434,0,0,0-24.8457.36523v28.86524c0,.24414-.12207.60937-.12207.85253v.36524c-.12207.24414-.12207.4873-.36523.73144v.6084a7.10907,7.10907,0,0,1-6.08985,3.89746c-.4873,0-.85254-.12109-1.21777-.12109a6.63054,6.63054,0,0,1-5.23731-6.334V126.53584a6.44715,6.44715,0,0,1,6.45508-6.57715A6.17885,6.17885,0,0,1,714.294,123.612a25.63672,25.63672,0,0,1,13.03222-3.65332,25.275,25.275,0,0,1,19.12207,8.40332,25.08719,25.08719,0,0,1,19.12207-8.40332,25.44472,25.44472,0,0,1,25.334,25.57715v28.86524a6.4984,6.4984,0,0,1-6.45606,6.45507,6.60365,6.60365,0,0,1-6.57617-6.45507V145.53584a12.47748,12.47748,0,0,0-12.30176-12.54492A12.6429,12.6429,0,0,0,752.90332,145.17061Z\" Fill=\"#fff\" />\n                        <Path Data=\"M813.55176,150.40791c.12109-16.56445,13.03222-30.44922,29.71777-30.44922,15.834,0,28.37891,11.81446,29.35254,28.25684v.6084a3.3938,3.3938,0,0,1-.12109,1.09668c-.36621,2.92285-2.92383,4.87109-6.334,4.87109H827.43652a15.96343,15.96343,0,0,0,4.14063,8.03906c2.55762,2.92285,7.42969,5.11524,11.69238,5.48047,4.38477.36524,9.62207-.73047,12.667-3.04492,2.55762-2.67969,7.55078-2.31348,9.13477-.36523,1.583,1.70507,2.80078,5.35937,0,7.917-5.96777,5.48047-13.1543,8.03808-21.80176,8.03808C826.584,180.73506,813.67285,166.97139,813.55176,150.40791Zm13.27539-5.96875h34.46777c-1.21777-4.87109-7.42871-12.05762-18.02539-12.91015C832.917,132.01631,827.80176,139.446,826.82715,144.43916Z\" Fill=\"#fff\" />\n                        <Path Data=\"M980.03906,150.529v23.87207a6.49756,6.49756,0,0,1-6.45508,6.45507,6.63053,6.63053,0,0,1-6.334-5.2373,27.65366,27.65366,0,0,1-16.19824,5.2373,28.29337,28.29337,0,0,1-20.70508-8.89062,31.17257,31.17257,0,0,1-8.28223-21.43652,30.73316,30.73316,0,0,1,8.28223-21.43555,28.04751,28.04751,0,0,1,20.70508-9.13477,27.7626,27.7626,0,0,1,16.19824,5.3584,6.61794,6.61794,0,0,1,6.334-5.3584,6.52411,6.52411,0,0,1,6.45508,6.57715Zm-12.91015,0a17.91142,17.91142,0,0,0-4.75-12.54493,15.05824,15.05824,0,0,0-11.32715-4.99316,14.6864,14.6864,0,0,0-11.32715,4.99316,17.83594,17.83594,0,0,0-4.62793,12.54493,18.26612,18.26612,0,0,0,4.62793,12.54492,15.33922,15.33922,0,0,0,11.32715,4.75,15.74436,15.74436,0,0,0,11.32715-4.75A18.35145,18.35145,0,0,0,967.12891,150.529Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1036.17871,150.529c0-16.92969,14.12793-30.44825,30.93555-30.57032a32.10452,32.10452,0,0,1,19.36621,6.69824,6.69586,6.69586,0,0,1,1.33887,9.01368,6.48661,6.48661,0,0,1-9.13379,1.21777,19.43225,19.43225,0,0,0-11.57129-3.89746c-9.86524,0-18.02539,8.16016-18.02539,17.53809,0,9.3789,8.16015,17.417,18.02539,17.417a19.7183,19.7183,0,0,0,11.57129-3.89746,6.63587,6.63587,0,0,1,9.13379,1.33984,6.727,6.727,0,0,1-1.33887,9.0127,33.55855,33.55855,0,0,1-19.36621,6.45507C1050.30664,180.85615,1036.17871,167.21553,1036.17871,150.529Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1113.75879,171.96553a30.3973,30.3973,0,0,1-8.4043-21.43652,31.42616,31.42616,0,0,1,8.4043-21.31446,28.44706,28.44706,0,0,1,20.94922-9.25586,26.93551,26.93551,0,0,1,20.33984,9.25586,30.43342,30.43342,0,0,1,8.52539,21.31446c0,8.52636-3.04492,16.07714-8.52539,21.43652a26.93214,26.93214,0,0,1-20.33984,9.25586A28.44343,28.44343,0,0,1,1113.75879,171.96553Zm4.87207-21.43652a19.69256,19.69256,0,0,0,4.38476,12.91015A16.48,16.48,0,0,0,1134.708,167.946a15.94613,15.94613,0,0,0,11.20508-4.50684,18.7136,18.7136,0,0,0,4.75-12.91015,18.31431,18.31431,0,0,0-4.75-12.666,17.20616,17.20616,0,0,0-11.20508-4.62891,17.70765,17.70765,0,0,0-11.69239,4.62891A19.25709,19.25709,0,0,0,1118.63086,150.529Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1199.98633,115.93916v1.70508h3.65332a6.67934,6.67934,0,0,1,6.69922,6.69922,6.75825,6.75825,0,0,1-6.69922,6.69824h-3.65332v42.99414a6.69873,6.69873,0,1,1-13.39746,0V131.0417h-3.167a6.78428,6.78428,0,0,1-6.82032-6.69824,6.7066,6.7066,0,0,1,6.82032-6.69922h3.167v-1.70508c0-4.50586.4873-8.40332,2.31347-11.93555a14.50793,14.50793,0,0,1,8.76954-7.55175,24.61459,24.61459,0,0,1,7.30761-.97461,6.76,6.76,0,0,1,0,13.51953,8.732,8.732,0,0,0-3.167.36523,1.28516,1.28516,0,0,0-.73047.36524C1200.96,109.72725,1199.98633,111.4333,1199.98633,115.93916Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1244.07324,115.93916v1.70508h3.65332a6.67934,6.67934,0,0,1,6.69922,6.69922,6.75825,6.75825,0,0,1-6.69922,6.69824h-3.65332v42.99414a6.69873,6.69873,0,1,1-13.39746,0V131.0417h-3.167a6.78428,6.78428,0,0,1-6.82031-6.69824,6.7066,6.7066,0,0,1,6.82031-6.69922h3.167v-1.70508c0-4.50586.48731-8.40332,2.31348-11.93555a14.51055,14.51055,0,0,1,8.76953-7.55175,24.61465,24.61465,0,0,1,7.30762-.97461,6.76,6.76,0,0,1,0,13.51953,8.72192,8.72192,0,0,0-3.166.36523,1.28543,1.28543,0,0,0-.73144.36524C1245.04688,109.72725,1244.07324,111.4333,1244.07324,115.93916Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1262.70605,150.40791c.1211-16.56445,13.03223-30.44922,29.71778-30.44922,15.834,0,28.3789,11.81446,29.35254,28.25684v.6084a3.39354,3.39354,0,0,1-.1211,1.09668c-.36621,2.92285-2.92382,4.87109-6.334,4.87109h-38.73047a15.96335,15.96335,0,0,0,4.14063,8.03906c2.55761,2.92285,7.42968,5.11524,11.69238,5.48047,4.38476.36524,9.62207-.73047,12.667-3.04492,2.55762-2.67969,7.55078-2.31348,9.13477-.36523,1.583,1.70507,2.80078,5.35937,0,7.917-5.96778,5.48047-13.1543,8.03808-21.80176,8.03808C1275.73828,180.73506,1262.82715,166.97139,1262.70605,150.40791Zm13.2754-5.96875h34.46777c-1.21777-4.87109-7.42871-12.05762-18.02539-12.91015C1282.07129,132.01631,1276.95605,139.446,1275.98145,144.43916Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1336.876,150.40791c.12109-16.56445,13.03222-30.44922,29.71777-30.44922,15.834,0,28.37891,11.81446,29.35254,28.25684v.6084a3.3938,3.3938,0,0,1-.12109,1.09668c-.36622,2.92285-2.92383,4.87109-6.334,4.87109h-38.73047a15.96335,15.96335,0,0,0,4.14063,8.03906c2.55761,2.92285,7.42968,5.11524,11.69238,5.48047,4.38477.36524,9.62207-.73047,12.667-3.04492,2.55762-2.67969,7.55078-2.31348,9.13477-.36523,1.583,1.70507,2.80078,5.35937,0,7.917-5.96778,5.48047-13.1543,8.03808-21.80176,8.03808C1349.9082,180.73506,1336.99707,166.97139,1336.876,150.40791Zm13.27539-5.96875h34.46777c-1.21777-4.87109-7.42871-12.05762-18.02539-12.91015C1356.24121,132.01631,1351.126,139.446,1350.15137,144.43916Z\" Fill=\"#fff\" />\n                        <Path Data=\"M301.10726,155.20527a72.64418,72.64418,0,0,1-15.58258.2681V102.82942h10.57885a23.75507,23.75507,0,0,1,24.079,24.44178c0,17.727-9.1342,24.71074-19.07531,27.93407m45.28651-36.95706c-3.08594-16.29847-11.85981-26.45154-20.8516-32.72972a55.46669,55.46669,0,0,0-31.77857-9.82873H132.80546c-5.59922,0-7.744,5.46684-7.76521,8.205-.00276.35708.01011,1.7861.01011,1.7861s-.26406,71.20994.236,109.24372c1.519,22.45424,24.01634,22.44638,24.01634,22.44638s73.4518-.21494,108.67788-.43393a20.73464,20.73464,0,0,0,4.90529-.5894c20.05633-5.01772,22.13125-23.65022,21.91686-34.03715,40.314,2.23969,68.75758-26.20732,61.59109-64.06224\" Fill=\"#fff\" />\n                        <Path Data=\"M203.88276,187.84042a2.48038,2.48038,0,0,0,2.86921-.21386s25.61811-23.38182,37.15916-36.84762c10.265-12.04636,10.93412-32.34669-6.69431-39.93188-17.628-7.58481-32.132,8.9235-32.132,8.9235-12.57767-13.83327-31.61372-13.133-40.41829-3.77113-8.80417,9.36187-5.72976,25.43024.83851,34.373,6.16576,8.39456,33.26672,32.54951,37.37421,36.63455a8.5541,8.5541,0,0,0,1.00353.83339\" Fill=\"#ff5e5b\" />\n\n                    </Canvas>\n\n                </Button>\n\n                <Button Grid.Column=\"0\"\n                        Width=\"137\" Height=\"37\"\n                        Margin=\"10\" Padding=\"0,6\" HorizontalContentAlignment=\"Stretch\"\n                        VerticalContentAlignment=\"Stretch\"\n                        Command=\"{Binding OpenGitHubCommand}\">\n                    <Canvas Opacity=\"1\">\n\n                        <Canvas.RenderTransform>\n                            <ScaleTransform CenterX=\"0\" CenterY=\"0\" ScaleX=\"0.09\" ScaleY=\"0.09\" />\n                        </Canvas.RenderTransform>\n\n                        <Path Data=\"M475.95605,179.63838a5.22327,5.22327,0,0,0-.85253-.60937c0-.1211-.12159-.1211-.12159-.36524a3.22007,3.22007,0,0,1-.60888-.73047.11955.11955,0,0,1-.12207-.12207,4.25937,4.25937,0,0,0-.60889-.97363l-29.83984-71.12891a6.48633,6.48633,0,0,1,3.6538-8.64746,6.56583,6.56583,0,0,1,8.64747,3.53223l23.75,56.39062L503.604,100.59346a6.71834,6.71834,0,0,1,12.42286,5.11523l-30.0835,71.12891a3.63493,3.63493,0,0,1-.48731.97363c0,.12207,0,.12207-.12158.12207-.12158.24317-.4873.48731-.60888.73047a.931.931,0,0,1-.24366.36524c-.24365.24414-.4873.36621-.73095.60937l-.24366.12207a6.80758,6.80758,0,0,1-.97412.6084c-.12207,0-.12207,0-.24365.12207a2.85993,2.85993,0,0,0-.97412.24414h-.24365a4.596,4.596,0,0,1-1.21826.12109,4.1654,4.1654,0,0,1-1.21778-.12109h-.24365a2.86272,2.86272,0,0,0-.97412-.24414.11955.11955,0,0,0-.12207-.12207h-.12158c-.24366-.12109-.731-.48731-1.0962-.73047Z\" Fill=\"#fff\" />\n                        <Path Data=\"M549.02832,104.73408a6.57638,6.57638,0,0,1-6.57666,6.69922,6.65438,6.65438,0,0,1-6.57715-6.69922v-2.55761a6.5497,6.5497,0,0,1,6.57715-6.57715,6.4739,6.4739,0,0,1,6.57666,6.57715Zm0,22.04493v47.5a6.4739,6.4739,0,0,1-6.57666,6.57714,6.5497,6.5497,0,0,1-6.57715-6.57714v-47.5a6.50291,6.50291,0,0,1,6.57715-6.69825A6.43152,6.43152,0,0,1,549.02832,126.779Z\" Fill=\"#fff\" />\n                        <Path Data=\"M565.7124,150.40791c.12207-16.56445,13.03223-30.44922,29.71826-30.44922,15.833,0,28.37891,11.81446,29.35254,28.25684v.6084a3.3938,3.3938,0,0,1-.12109,1.09668c-.36621,2.92285-2.92383,4.87109-6.334,4.87109h-38.731a15.968,15.968,0,0,0,4.14111,8.03906c2.55762,2.92285,7.42969,5.11524,11.69238,5.48047,4.38477.36524,9.62207-.73047,12.667-3.04492,2.55761-2.67969,7.55078-2.31348,9.13476-.36523,1.583,1.70507,2.80078,5.35937,0,7.917-5.96875,5.48047-13.1543,8.03808-21.80176,8.03808C578.74463,180.73506,565.83447,166.97139,565.7124,150.40791Zm13.27588-5.96875h34.46777c-1.21777-4.87109-7.42871-12.05762-18.02539-12.91015C585.07812,132.01631,579.96289,139.446,578.98828,144.43916Z\" Fill=\"#fff\" />\n                        <Path Data=\"M719.415,129.70186l-20.09668,47.25683a7.08863,7.08863,0,0,1-.48633.97461.11955.11955,0,0,1-.12207.12207,3.22323,3.22323,0,0,1-.60937.73047.37678.37678,0,0,1-.24317.12207,3.22025,3.22025,0,0,1-.73144.6084c0,.12207-.1211.12207-.1211.24414a3.18759,3.18759,0,0,1-1.09668.6084.1189.1189,0,0,0-.12109.12207,3.7528,3.7528,0,0,0-.97461.24414h-.24414a3.77366,3.77366,0,0,1-1.21777.12109,3.388,3.388,0,0,1-1.09571-.12109h-.24414a1.75636,1.75636,0,0,0-.85254-.24414l-.12109-.12207H690.915a2.86993,2.86993,0,0,1-1.09668-.73047v-.12207c-.36524-.12109-.48731-.36523-.73047-.6084,0,0-.24316,0-.24316-.12207a3.22329,3.22329,0,0,1-.60938-.73047.11955.11955,0,0,1-.12207-.12207,7.22739,7.22739,0,0,1-.4873-.97461l-8.89063-21.07031-8.64746,21.07031a11.07767,11.07767,0,0,1-.60937.97461v.12207l-.73047.73047a.11955.11955,0,0,1-.12207.12207l-.73047.73047a4.20337,4.20337,0,0,1-1.09668.73047h-.24317a.11955.11955,0,0,0-.12207.12207,1.30483,1.30483,0,0,0-.73047.24414H665.46a4.59759,4.59759,0,0,1-1.21875.12109,3.77662,3.77662,0,0,1-1.21777-.12109h-.36524a1.72719,1.72719,0,0,0-.73047-.24414c-.12207-.12207-.12207-.12207-.24414-.12207a2.18424,2.18424,0,0,1-1.0957-.73047c-.12207,0-.12207,0-.12207-.12207a4.67267,4.67267,0,0,0-.73047-.6084l-.24414-.12207c-.12109-.24316-.36523-.4873-.4873-.73047-.1211,0-.1211,0-.1211-.12207a6.84161,6.84161,0,0,0-.60937-.97461l-19.97461-47.25683a6.30163,6.30163,0,0,1,3.53222-8.40332,6.052,6.052,0,0,1,8.03907,3.65332l14.37109,32.76367,8.52637-20.33985a6.496,6.496,0,0,1,5.96777-3.89746,6.25429,6.25429,0,0,1,5.96777,3.89746l8.64747,20.33985,14.37207-32.76367a6.10175,6.10175,0,0,1,8.16015-3.65332A6.39722,6.39722,0,0,1,719.415,129.70186Z\" Fill=\"#fff\" />\n                        <Path Data=\"M788.09863,171.96553a30.39734,30.39734,0,0,1-8.40429-21.43652,31.4262,31.4262,0,0,1,8.40429-21.31446,28.44706,28.44706,0,0,1,20.94922-9.25586,26.93553,26.93553,0,0,1,20.33985,9.25586,30.43346,30.43346,0,0,1,8.52539,21.31446c0,8.52636-3.04493,16.07714-8.52539,21.43652a26.93216,26.93216,0,0,1-20.33985,9.25586A28.44343,28.44343,0,0,1,788.09863,171.96553ZM792.9707,150.529a19.69251,19.69251,0,0,0,4.38477,12.91015,16.48,16.48,0,0,0,11.69238,4.50684,15.94609,15.94609,0,0,0,11.20508-4.50684,18.7136,18.7136,0,0,0,4.75-12.91015,18.31431,18.31431,0,0,0-4.75-12.666,17.20611,17.20611,0,0,0-11.20508-4.62891,17.70763,17.70763,0,0,0-11.69238,4.62891A19.257,19.257,0,0,0,792.9707,150.529Z\" Fill=\"#fff\" />\n                        <Path Data=\"M909.40332,145.2917v29.10938a6.39436,6.39436,0,0,1-6.334,6.45507,6.579,6.579,0,0,1-6.45508-6.45507V145.2917a12.50466,12.50466,0,0,0-12.667-12.30078,12.16428,12.16428,0,0,0-12.17969,12.30078v29.10938a2.41236,2.41236,0,0,1-.24317,1.21777,6.6603,6.6603,0,0,1-6.45507,5.2373,6.41909,6.41909,0,0,1-6.45508-6.45507V126.41377a6.41909,6.41909,0,0,1,6.45508-6.45508,6.58549,6.58549,0,0,1,5.96777,3.77539,24.30992,24.30992,0,0,1,12.91016-3.77539A25.49677,25.49677,0,0,1,909.40332,145.2917Z\" Fill=\"#fff\" />\n                        <Path Data=\"M967.36914,137.13154c0-24.48046,20.21875-43.96777,44.94336-43.96777a45.03123,45.03123,0,0,1,27.76953,9.5,7.01213,7.01213,0,0,1,1.33887,9.86524,7.08685,7.08685,0,0,1-9.86524,1.0957,31.64508,31.64508,0,0,0-19.24316-6.57617,30.20738,30.20738,0,0,0-21.92383,8.89062,28.86843,28.86843,0,0,0,0,42.1416,30.20739,30.20739,0,0,0,21.92383,8.89063,33.59166,33.59166,0,0,0,16.68555-4.62793V145.65791h-17.417a7.01905,7.01905,0,0,1-6.94238-6.94238,6.83658,6.83658,0,0,1,6.94238-6.82129h24.23731a6.86606,6.86606,0,0,1,7.06445,6.82129v26.916a10.45344,10.45344,0,0,1-.36523,2.07129c-.12207.24317-.12207.36524-.24414.6084v.12207a5.78149,5.78149,0,0,1-2.31348,2.92285,44.94033,44.94033,0,0,1-27.64746,9.5C987.58789,180.85615,967.36914,161.49092,967.36914,137.13154Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1082.2168,104.73408a6.57618,6.57618,0,0,1-6.57715,6.69922,6.65387,6.65387,0,0,1-6.57617-6.69922v-2.55761a6.54918,6.54918,0,0,1,6.57617-6.57715,6.47371,6.47371,0,0,1,6.57715,6.57715Zm0,22.04493v47.5a6.47371,6.47371,0,0,1-6.57715,6.57714,6.54917,6.54917,0,0,1-6.57617-6.57714v-47.5a6.50241,6.50241,0,0,1,6.57617-6.69825A6.43135,6.43135,0,0,1,1082.2168,126.779Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1130.56836,126.65693a6.86505,6.86505,0,0,1-6.82129,6.69922h-3.04492v33.98047a6.70683,6.70683,0,0,1,6.69922,6.82129,6.60346,6.60346,0,0,1-6.69922,6.69824,13.56691,13.56691,0,0,1-13.51856-13.51953V133.35615h-3.53222a6.67934,6.67934,0,0,1-6.69922-6.69922,6.60346,6.60346,0,0,1,6.69922-6.69824h3.53222V102.29854a6.7066,6.7066,0,0,1,6.82032-6.69922,6.60346,6.60346,0,0,1,6.69824,6.69922v17.66015h3.04492A6.78348,6.78348,0,0,1,1130.56836,126.65693Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1153.21973,174.15791V102.29854a6.60346,6.60346,0,0,1,6.69824-6.69922,6.70683,6.70683,0,0,1,6.82129,6.69922V131.529h31.54492V102.29854a6.60346,6.60346,0,0,1,6.69824-6.69922,6.7066,6.7066,0,0,1,6.82031,6.69922v71.85937a6.70639,6.70639,0,0,1-6.82031,6.69824,6.60323,6.60323,0,0,1-6.69824-6.69824V144.92647h-31.54492v29.23144a6.70662,6.70662,0,0,1-6.82129,6.69824A6.60323,6.60323,0,0,1,1153.21973,174.15791Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1262.34375,180.85615a25.47032,25.47032,0,0,1-25.333-25.333V126.41377a6.579,6.579,0,0,1,6.45508-6.45508,6.49741,6.49741,0,0,1,6.45508,6.45508v29.10938a12.32007,12.32007,0,0,0,12.42285,12.42285,12.63421,12.63421,0,0,0,12.78906-12.42285V126.41377a6.3917,6.3917,0,0,1,6.333-6.45508,6.77437,6.77437,0,0,1,6.57715,6.45508v29.10938C1288.043,169.40791,1276.35059,180.85615,1262.34375,180.85615Z\" Fill=\"#fff\" />\n                        <Path Data=\"M1312.88477,150.529V102.17647a6.638,6.638,0,0,1,13.27539,0v22.89746a28.82748,28.82748,0,0,1,16.32031-5.11524c16.44336,0,29.59668,13.88477,29.59668,30.57032,0,16.44238-13.15332,30.32714-29.59668,30.32714a29.71116,29.71116,0,0,1-16.56348-5.2373,6.559,6.559,0,0,1-6.45508,5.2373,6.62779,6.62779,0,0,1-6.57714-6.57714Zm13.27539,0a17.13533,17.13533,0,0,0,4.87207,12.05761,15.5666,15.5666,0,0,0,11.44824,4.99414,15.76989,15.76989,0,0,0,11.44922-4.99414,17.65772,17.65772,0,0,0,0-24.3584,15.76989,15.76989,0,0,0-11.44922-4.99414,15.5666,15.5666,0,0,0-11.44824,4.99414A17.2952,17.2952,0,0,0,1326.16016,150.529Z\" Fill=\"#fff\" />\n                        <Path Data=\"M205.26968,50.16688c-54.23433,0-98.07751,44.1664-98.07751,98.80625a98.72522,98.72522,0,0,0,67.06266,93.73314c4.87236.9837,6.65708-2.126,6.65708-4.74186,0-2.29063-.16061-10.14222-.16061-18.323-27.28279,5.89018-32.96419-11.77837-32.96419-11.77837-4.38452-11.45115-10.881-14.39423-10.881-14.39423-8.92964-6.0528.65046-6.0528.65046-6.0528,9.90531.65446,15.10289,10.14221,15.10289,10.14221,8.767,15.0487,22.89426,10.79668,28.57767,8.17881.81106-6.38,3.41086-10.79667,6.17126-13.24991-21.76-2.29063-44.65424-10.79668-44.65424-48.74967a38.68168,38.68168,0,0,1,10.06592-26.49984c-.97367-2.45324-4.38452-12.59746.97568-26.17461,0,0,8.2812-2.61787,26.95354,10.14221a93.49732,93.49732,0,0,1,49.03876,0C248.46241,88.44511,256.74361,91.063,256.74361,91.063c5.36019,13.57715,1.94734,23.72137.97367,26.17461a37.93985,37.93985,0,0,1,10.06793,26.49984c0,37.953-22.89425,46.29442-44.81684,48.74967,3.57346,3.1077,6.65708,8.99589,6.65708,18.321,0,13.24992-.16061,23.884-.16061,27.15431,0,2.61786,1.78673,5.72757,6.65709,4.74587a98.7316,98.7316,0,0,0,67.06265-93.73515C303.34518,94.33328,259.3414,50.16688,205.26968,50.16688Z\" Fill=\"#fff\" />\n\n                    </Canvas>\n                </Button>\n\n            </Grid>\n\n        </Grid>\n\n        <ScrollViewer Margin=\"0,50,0,0\">\n            <StackPanel ScrollViewer.CanContentScroll=\"True\">\n\n                <ui:CardExpander Margin=\"-15,0\"\n                                 Background=\"Transparent\" BorderBrush=\"Transparent\" ContentPadding=\"0\"\n                                 FlowDirection=\"RightToLeft\" IsExpanded=\"True\"\n                                 Style=\"{StaticResource CustomCardExpanderStyle}\">\n                    <ui:CardExpander.Header>\n                        <Label Content=\"Filetype Management\"\n                               Margin=\"10,0\"\n                               FlowDirection=\"LeftToRight\" />\n\n                    </ui:CardExpander.Header>\n\n                    <StackPanel Margin=\"15,-10,15,10\" FlowDirection=\"LeftToRight\">\n\n                        <Grid Margin=\"0,10,0,0\">\n                            <Grid.ColumnDefinitions>\n                                <ColumnDefinition Width=\"250\" />\n                                <ColumnDefinition Width=\"100\" />\n                            </Grid.ColumnDefinitions>\n                            <Grid.RowDefinitions>\n                                <RowDefinition />\n                                <RowDefinition Height=\"5\" />\n                                <RowDefinition />\n                            </Grid.RowDefinitions>\n\n                            <Label Content=\"Manage local skipped filetypes\" VerticalAlignment=\"Center\" />\n                            <Button x:Name=\"UiEditSkipListBtn\"\n                                    Content=\"edit\"\n                                    Grid.Column=\"1\"\n                                    Width=\"100\" Height=\"37\"\n                                    BorderThickness=\"0\"\n                                    Command=\"{Binding EditSkipListCommand}\"\n                                    IsEnabled=\"True\" />\n\n                            <Label Content=\"Agression of online skiplist\"\n                                   Grid.Row=\"2\"\n                                   VerticalAlignment=\"Center\" />\n                            <TextBlock x:Name=\"SkipHelpIcon\"\n                                       Text=\"(?)\"\n                                       Grid.Row=\"2\"\n                                       Margin=\"175,0,0,0\" VerticalAlignment=\"Center\"\n                                       Cursor=\"Hand\" FontSize=\"14\" Foreground=\"#FF98A9B9\" TextDecorations=\"Underline\">\n\n                                <ToolTipService.ToolTip>\n                                    <ToolTip Placement=\"Top\">\n                                        <TextBlock Text=\"For Steam games only.&#x0a;When choosing to skip user-submitted filetypes, this setting determines how many submissions are required for each filetype to consider skipping it. 'low' is generally best, as higher options run the risk of skipping files that would otherwise compress well.\"\n                                                   Width=\"200\"\n                                                   TextAlignment=\"Left\" TextWrapping=\"Wrap\" />\n                                    </ToolTip>\n                                </ToolTipService.ToolTip>\n                            </TextBlock>\n\n                            <ComboBox x:Name=\"ComboBoxSkipUserResultsAggression\"\n                                      Grid.Row=\"2\" Grid.Column=\"1\"\n                                      Foreground=\"#98A9B9\" IsEnabled=\"False\" SelectedIndex=\"0\">\n                                <ComboBoxItem>\n                                    <TextBlock Text=\"low\"\n                                               FontSize=\"14\" FontWeight=\"SemiBold\" Foreground=\"#98A9B9\" />\n                                </ComboBoxItem>\n                                <ComboBoxItem>\n                                    <TextBlock Text=\"medium\"\n                                               FontSize=\"14\" FontWeight=\"SemiBold\" Foreground=\"#98A9B9\" />\n                                </ComboBoxItem>\n                                <ComboBoxItem>\n                                    <TextBlock Text=\"high\"\n                                               FontSize=\"14\" FontWeight=\"SemiBold\" Foreground=\"#98A9B9\" />\n                                </ComboBoxItem>\n                            </ComboBox>\n\n                        </Grid>\n\n                    </StackPanel>\n\n                </ui:CardExpander>\n\n                <Separator Height=\"1\" />\n\n                <ui:CardExpander Margin=\"-15,0\"\n                                 Background=\"Transparent\" BorderBrush=\"Transparent\" ContentPadding=\"0\"\n                                 FlowDirection=\"RightToLeft\" IsExpanded=\"True\"\n                                 Style=\"{StaticResource CustomCardExpanderStyle}\">\n                    <ui:CardExpander.Header>\n                        <Label Content=\"System Integration\"\n                               Margin=\"10,0\"\n                               FlowDirection=\"LeftToRight\" />\n\n                    </ui:CardExpander.Header>\n\n                    <StackPanel Margin=\"15,-10,15,10\" FlowDirection=\"LeftToRight\">\n                        <StackPanel Margin=\"-10,10,0,0\">\n                            <CheckBox x:Name=\"UiIsContextEnabled\"\n                                      Content=\"Add to right-click context menu\"\n                                      Margin=\"0,-3\"\n                                      IsChecked=\"{Binding AppSettings.IsContextIntegrated, Mode=TwoWay}\" />\n                            <CheckBox x:Name=\"UiIsStartMenuEnabled\"\n                                      Content=\"Add to start menu\"\n                                      Margin=\"0,-3\"\n                                      IsChecked=\"{Binding AppSettings.IsStartMenuEnabled, Mode=TwoWay}\" />\n                            <CheckBox x:Name=\"UiShowNotifications\"\n                                      Content=\"Show notification on completion\"\n                                      Margin=\"0,-3\"\n                                      IsChecked=\"{Binding AppSettings.ShowNotifications, Mode=TwoWay}\" />\n                            <CheckBox x:Name=\"UiAlwaysStartInTray\"\n                                      Content=\"Start CompactGUI in system tray\"\n                                      Margin=\"0,-3\"\n                                      IsChecked=\"{Binding AppSettings.StartInSystemTray}\" />\n\n                        </StackPanel>\n                    </StackPanel>\n\n\n                </ui:CardExpander>\n\n                <Separator Height=\"1\" />\n\n\n                <ui:CardExpander Margin=\"-15,0\"\n                                 Background=\"Transparent\" BorderBrush=\"Transparent\" ContentPadding=\"0\"\n                                 FlowDirection=\"RightToLeft\" IsExpanded=\"True\"\n                                 Style=\"{StaticResource CustomCardExpanderStyle}\">\n                    <ui:CardExpander.Header>\n                        <Label Content=\"Compression Settings\"\n                               Margin=\"10,0\"\n                               FlowDirection=\"LeftToRight\" />\n\n                    </ui:CardExpander.Header>\n\n\n                    <StackPanel Margin=\"15,-10,15,10\" FlowDirection=\"LeftToRight\">\n                        <Grid Margin=\"0,10,0,0\">\n\n                            <Grid.ColumnDefinitions>\n                                <ColumnDefinition Width=\"250\" />\n                                <ColumnDefinition Width=\"200\" />\n\n                            </Grid.ColumnDefinitions>\n\n                            <Grid.RowDefinitions>\n                                <RowDefinition />\n                                <RowDefinition Height=\"5\" />\n                                <RowDefinition />\n                                <RowDefinition Height=\"5\" />\n                                <RowDefinition />\n                            </Grid.RowDefinitions>\n\n                            <Label Content=\"Maximum Compression Threads\" />\n                            <StackPanel Grid.Column=\"1\"\n                                        Margin=\"15,0,15,0\"\n                                        Orientation=\"Horizontal\">\n\n                                <Slider x:Name=\"CompressionParallelism\"\n                                        Width=\"130\"\n                                        IsSnapToTickEnabled=\"True\" LargeChange=\"1\" Maximum=\"16\" Minimum=\"0\"\n                                        Orientation=\"Horizontal\" SmallChange=\"1\" TickFrequency=\"1\" TickPlacement=\"BottomRight\"\n                                        Value=\"{Binding AppSettings.MaxCompressionThreads, Mode=TwoWay}\" />\n\n                                <Label Content=\"{Binding AppSettings.MaxCompressionThreads, FallbackValue=0}\"\n                                       Margin=\"15,0\"\n                                       FontWeight=\"SemiBold\" />\n                            </StackPanel>\n\n                            <CheckBox x:Name=\"UiLockHddToOneThread\"\n                                      Content=\"HDDs only use 1 thread\"\n                                      Grid.Row=\"2\"\n                                      Margin=\"-10,0\"\n                                      IsChecked=\"{Binding AppSettings.LockHDDsToOneThread}\" />\n\n                            <CheckBox x:Name=\"UiUseEstimator\"\n                                      Content=\"Estimate Compression for non-Steam Folders (beta)\"\n                                      Grid.Row=\"4\" Grid.ColumnSpan=\"2\"\n                                      Margin=\"-10,0\"\n                                      IsChecked=\"{Binding AppSettings.EstimateCompressionForNonSteamFolders}\">\n                                <CheckBox.ToolTip>\n                                    <ToolTip Placement=\"Top\">\n                                        <TextBlock Text=\"Quickly parses over files and estimates their compression ratio without writing to disk.                                                    This will likely be super laggy on HDDs!                                                    \"\n                                                   Width=\"200\"\n                                                   TextAlignment=\"Left\" TextWrapping=\"Wrap\" />\n                                    </ToolTip>\n                                </CheckBox.ToolTip>\n                            </CheckBox>\n                        </Grid>\n                    </StackPanel>\n\n                </ui:CardExpander>\n\n                <Separator Height=\"1\" />\n\n\n                <ui:CardExpander Margin=\"-15,0\"\n                                 Background=\"Transparent\" BorderBrush=\"Transparent\" ContentPadding=\"0\"\n                                 FlowDirection=\"RightToLeft\" IsExpanded=\"True\"\n                                 Style=\"{StaticResource CustomCardExpanderStyle}\">\n                    <ui:CardExpander.Header>\n                        <Label Content=\"Background Watcher Settings\"\n                               Margin=\"10,0\"\n                               FlowDirection=\"LeftToRight\" />\n\n                    </ui:CardExpander.Header>\n\n\n                    <StackPanel Margin=\"15,-10,15,10\" FlowDirection=\"LeftToRight\">\n                        <StackPanel Margin=\"-10,10,0,0\">\n                            <CheckBox x:Name=\"UiEnableBackgroundWatcher\"\n                                      Content=\"Monitor compressed folders for changes\"\n                                      Margin=\"0,-3\"\n                                      IsChecked=\"{Binding AppSettings.EnableBackgroundWatcher, Mode=TwoWay}\">\n\n                                <i:Interaction.Triggers>\n                                    <i:EventTrigger EventName=\"Unchecked\">\n                                        <i:InvokeCommandAction Command=\"{Binding DisableAutoCompressionCommand}\" />\n                                    </i:EventTrigger>\n                                </i:Interaction.Triggers>\n                            </CheckBox>\n\n                            <WrapPanel Margin=\"45,10,0,0\"\n                                       IsEnabled=\"{Binding AppSettings.EnableBackgroundWatcher}\"\n                                       Orientation=\"Horizontal\">\n                                <TextBlock Text=\"Compress folders: \" VerticalAlignment=\"Center\" />\n\n                                <ComboBox x:Name=\"BackgroundModeSelector\"\n                                          Width=\"290\" Height=\"35\"\n                                          Margin=\"10,0,0,0\" HorizontalAlignment=\"Left\"\n                                          SelectedIndex=\"{Binding AppSettings.BackgroundModeSelection, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}\">\n                                    <ComboBoxItem Content=\"Never\" />\n                                    <ComboBoxItem Content=\"When System is Idle\" />\n                                    <ComboBoxItem Content=\"On Schedule\" />\n                                    <ComboBoxItem Content=\"On Schedule if system is also idle\" />\n\n                                </ComboBox>\n\n                                <StackPanel Margin=\"0,10\"\n                                            Orientation=\"Horizontal\"\n                                            Visibility=\"{Binding AppSettings.BackgroundModeSelection, Mode=OneWay, Converter={StaticResource BackgroundModeToVisibilityConverter}}\">\n                                    <TextBlock Text=\"every\"\n                                               Margin=\"10,0,10,0\" VerticalAlignment=\"Center\" />\n                                    <ui:NumberBox Width=\"150\"\n                                                  Margin=\"5,0,0,0\"\n                                                  ClearButtonEnabled=\"False\" Maximum=\"28\" Minimum=\"1\"\n                                                  Value=\"{Binding AppSettings.ScheduledBackgroundInterval, UpdateSourceTrigger=PropertyChanged}\" />\n                                    <TextBlock Text=\"day(s)\"\n                                               Margin=\"-110,0,0,0\" VerticalAlignment=\"Center\"\n                                               Foreground=\"#80FFFFFF\" IsHitTestVisible=\"False\" />\n                                    <TextBlock Text=\"at\"\n                                               Margin=\"20,0,20,0\" VerticalAlignment=\"Center\" />\n                                    <ComboBox Width=\"60\"\n                                              Padding=\"8,8,0,8\"\n                                              ItemsSource=\"{Binding HourOptions}\"\n                                              SelectedIndex=\"{Binding AppSettings.ScheduledBackgroundHour}\">\n                                        <i:Interaction.Behaviors>\n                                            <local:FocusOnMouseOverBehavior />\n                                        </i:Interaction.Behaviors>\n                                        <ComboBox.ItemTemplate>\n                                            <DataTemplate>\n                                                <TextBlock Text=\"{Binding StringFormat={}{0:D2}}\" />\n                                            </DataTemplate>\n                                        </ComboBox.ItemTemplate>\n                                    </ComboBox>\n                                    <TextBlock Text=\":\"\n                                               Margin=\"5,0,5,0\" VerticalAlignment=\"Center\" />\n                                    <ComboBox Width=\"60\"\n                                              Padding=\"8,8,0,8\"\n                                              ItemsSource=\"{Binding MinuteOptions}\"\n                                              SelectedIndex=\"{Binding AppSettings.ScheduledBackgroundMinute}\">\n                                        <i:Interaction.Behaviors>\n                                            <local:FocusOnMouseOverBehavior />\n                                        </i:Interaction.Behaviors>\n                                        <ComboBox.ItemTemplate>\n                                            <DataTemplate>\n                                                <TextBlock Text=\"{Binding StringFormat={}{0:D2}}\" />\n                                            </DataTemplate>\n                                        </ComboBox.ItemTemplate>\n                                    </ComboBox>\n                                 \n                                </StackPanel>\n\n                            </WrapPanel>\n                            <TextBlock Text=\"{Binding AppSettings.ScheduledBackgroundLastRan, StringFormat='{}Last ran: {0:dd MMM yyyy \\\\a\\\\t HH:mm:ss }', Mode=OneWay}\"\n                                       Margin=\"45,10,0,0\"\n                                       Foreground=\"#30FFFFFF\" />\n\n                            <TextBlock Text=\"{Binding AppSettings.NextScheduledBackgroundRun, StringFormat='{}Next scheduled: {0:dd MMM yyyy \\\\a\\\\t HH:mm:ss }', Mode=OneWay}\"\n                                       Margin=\"45,10,0,0\"\n                                       Foreground=\"#30FFFFFF\"\n                                       Visibility=\"{Binding AppSettings.BackgroundModeSelection, Mode=OneWay, Converter={StaticResource BackgroundModeToVisibilityConverter}}\" />\n\n\n                        </StackPanel>\n                    </StackPanel>\n\n                </ui:CardExpander>\n\n\n                <Separator Height=\"1\" />\n\n                <ui:CardExpander Margin=\"-15,0\"\n                                 Background=\"Transparent\" BorderBrush=\"Transparent\" ContentPadding=\"0\"\n                                 FlowDirection=\"RightToLeft\" IsExpanded=\"True\"\n                                 Style=\"{StaticResource CustomCardExpanderStyle}\">\n                    <ui:CardExpander.Header>\n                        <Label Content=\"Update Settings\"\n                               Margin=\"10,0\"\n                               FlowDirection=\"LeftToRight\" />\n\n                    </ui:CardExpander.Header>\n\n\n                    <StackPanel Margin=\"15,-10,15,10\" FlowDirection=\"LeftToRight\">\n                        <StackPanel Margin=\"-10,10,0,0\">\n                            <CheckBox x:Name=\"UiEnablePreReleaseUpdates\"\n                                      Content=\"Check for pre-release updates\"\n                                      Margin=\"0,-3\"\n                                      IsChecked=\"{Binding AppSettings.EnablePreReleaseUpdates}\" />\n\n                        </StackPanel>\n                    </StackPanel>\n\n                </ui:CardExpander>\n\n                <Separator Height=\"1\" />\n\n                <ui:CardExpander Margin=\"-15,0\"\n                                 Background=\"Transparent\" BorderBrush=\"Transparent\" ContentPadding=\"0\"\n                                 FlowDirection=\"RightToLeft\" IsExpanded=\"True\"\n                                 Style=\"{StaticResource CustomCardExpanderStyle}\">\n                    <ui:CardExpander.Header>\n                        <Label Content=\"UI Settings\"\n                               Margin=\"10,0\"\n                               FlowDirection=\"LeftToRight\" />\n\n                    </ui:CardExpander.Header>\n\n\n                    <StackPanel Margin=\"15,-10,15,10\" FlowDirection=\"LeftToRight\">\n                        <StackPanel Margin=\"-10,10,0,0\">\n                            <CheckBox x:Name=\"UiAlwaysShowDetailedCompressionMode\"\n                                      Content=\"Always show details on Compression Mode buttons\"\n                                      Margin=\"0,-3\"\n                                      IsChecked=\"{Binding AppSettings.AlwaysShowDetailedCompressionMode}\" />\n\n                        </StackPanel>\n                    </StackPanel>\n\n                </ui:CardExpander>\n\n\n\n            </StackPanel>\n\n\n\n\n        </ScrollViewer>\n\n\n\n    </Grid>\n\n</Page>\n"
  },
  {
    "path": "CompactGUI/Views/SettingsPage.xaml.vb",
    "content": "﻿Public Class SettingsPage\n\n    Sub New(settingsviewmodel As SettingsViewModel)\n\n        InitializeComponent()\n\n\n        DataContext = settingsviewmodel\n\n\n        ScrollViewer.SetCanContentScroll(Me, False)\n\n    End Sub\n\n\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI.Core/Analyser.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing System.Collections.Concurrent;\nusing System.Diagnostics;\nusing CompactGUI.Logging.Core;\n\nnamespace CompactGUI.Core;\n\npublic sealed class Analyser : IDisposable\n{\n\n    public string FolderName { get; set; }\n    private ILogger<Analyser> _logger;\n    private readonly FolderChangeMonitor _folderMonitor;\n    public bool HasFolderChanged => _folderMonitor.HasChanged;\n    public DateTime LastFolderChanged => _folderMonitor.LastChanged;\n\n\n    public Analyser(string folder, ILogger<Analyser> logger)\n    {\n        FolderName = folder;\n        _logger = logger;\n\n        _folderMonitor = new FolderChangeMonitor(folder);\n        _folderMonitor.Changed += (s, e) =>\n            _logger.LogInformation(\"Folder change detected by FolderChangeMonitor for {FolderName}\", FolderName);\n    }\n\n\n    public long CompressedBytes;\n    public long UncompressedBytes;\n    public bool ContainsCompressedFiles;\n\n    private static long GetTotalCompressedBytes(List<AnalysedFileDetails> fileCompressionDetailsList)\n    {\n        return fileCompressionDetailsList.Sum(f => f.CompressedSize);\n    }\n    private static long GetTotalUncompressedBytes(List<AnalysedFileDetails> fileCompressionDetailsList)\n    {\n        return fileCompressionDetailsList.Sum(f => f.UncompressedSize);\n    }\n    private static bool GetContainsCompressedFiles(List<AnalysedFileDetails> fileCompressionDetailsList)\n    {\n        return fileCompressionDetailsList.Any(f => f.CompressionMode != WOFCompressionAlgorithm.NO_COMPRESSION);\n    }\n\n\n    private List<AnalysedFileDetails>? _analysedFileDetails;\n\n    public async ValueTask<List<AnalysedFileDetails>?> GetAnalysedFilesAsync(CancellationToken token)\n    {\n        if (_analysedFileDetails != null && !HasFolderChanged)\n        {\n            _logger.LogInformation(\"Returning cached analysed files for folder {FolderName}\", FolderName);\n            return _analysedFileDetails;\n        }\n\n        _logger.LogInformation(\"Analysing folder {FolderName} for the first time or after a change\", FolderName);\n        _analysedFileDetails = await AnalyseFolder(token).ConfigureAwait(false);\n        return _analysedFileDetails;\n    }\n\n\n    private async Task<List<AnalysedFileDetails>?> AnalyseFolder(CancellationToken cancellationToken)\n    {\n\n        List<AnalysedFileDetails>? AnalysedFileDetails;\n\n        AnalyserLog.StartingAnalysis(_logger, FolderName);\n        Stopwatch sw = Stopwatch.StartNew();\n        try\n        {\n            var allFiles = await Task.Run(() => Directory.EnumerateFiles(FolderName, \"*\", new EnumerationOptions { RecurseSubdirectories = true, IgnoreInaccessible = true, AttributesToSkip = FileAttributes.ReparsePoint }).AsShortPathNames(), cancellationToken).ConfigureAwait(false);\n            var fileDetails = allFiles\n                .AsParallel()\n                .WithCancellation(cancellationToken)\n                .Select(AnalyseFile)\n                .OfType<AnalysedFileDetails>()\n                .ToList();\n\n            AnalysedFileDetails = fileDetails;\n        }\n        catch (Exception ex)\n        {\n            AnalyserLog.AnalysisFailed(_logger, FolderName, ex.Message);\n            return null;\n        }\n        finally { sw.Stop(); }\n        \n        _folderMonitor.Reset();\n\n        CompressedBytes = GetTotalCompressedBytes(AnalysedFileDetails);\n        UncompressedBytes = GetTotalUncompressedBytes(AnalysedFileDetails);\n        ContainsCompressedFiles = GetContainsCompressedFiles(AnalysedFileDetails);\n        AnalyserLog.AnalysisCompleted(_logger, FolderName, Math.Round(sw.Elapsed.TotalSeconds, 3), CompressedBytes, UncompressedBytes, ContainsCompressedFiles);\n\n        return AnalysedFileDetails;\n\n\n    }\n\n\n    private AnalysedFileDetails? AnalyseFile(string file)\n    {\n        AnalyserLog.ProcessingFile(_logger, file);\n        try\n        {\n            FileInfo fileInfo = new FileInfo(file);\n            long uncompressedSize = fileInfo.Length;\n            long compressedSize = SharedMethods.GetFileSizeOnDisk(file);\n            compressedSize = compressedSize < 0 ? 0 : compressedSize;\n            WOFCompressionAlgorithm compressionMode = (compressedSize == uncompressedSize)\n                ? WOFCompressionAlgorithm.NO_COMPRESSION\n                : WOFHelper.DetectCompression(fileInfo);\n\n            return new AnalysedFileDetails { FileName = file, CompressedSize = compressedSize, UncompressedSize = uncompressedSize, CompressionMode = compressionMode, FileInfo = fileInfo };\n        }\n        catch (IOException ex)\n        {\n            AnalyserLog.ProcessingFileFailed(_logger, file, ex.Message);\n            return null;\n        }\n    }\n\n\n    public List<ExtensionResult> GetPoorlyCompressedExtensions()\n    {\n        // Only use PLINQ if the list is large enough to benefit from parallel processing\n        IEnumerable<AnalysedFileDetails> query = _analysedFileDetails?.Count <= 10000\n            ? _analysedFileDetails\n            : _analysedFileDetails.AsParallel();\n\n        return query\n                .Where(fl => fl.UncompressedSize > 0)\n                .GroupBy(fl => Path.GetExtension(fl.FileName), StringComparer.OrdinalIgnoreCase)\n                .Select(g => new ExtensionResult\n                {\n                    Extension = g.Key,\n                    TotalFiles = g.Count(),\n                    CompressedBytes = g.Sum(fl => fl.CompressedSize),\n                    UncompressedBytes = g.Sum(fl => fl.UncompressedSize)\n                })\n                .Where(r => r.CRatio > 0.95)\n                .ToList();\n\n    }\n\n    public void Dispose()\n    {\n        _folderMonitor.Dispose();\n        _analysedFileDetails?.Clear();\n    }\n}\n\n\n"
  },
  {
    "path": "CompactGUI.Core/CompactGUI.Core.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0-windows</TargetFramework>\n    <Nullable>enable</Nullable>\n    <ImplicitUsings>enable</ImplicitUsings>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|AnyCPU'\"> \n    <DebugType>none</DebugType>\n  </PropertyGroup>\n\n  \n  <ItemGroup>\n    <PackageReference Include=\"CommunityToolkit.Mvvm\" Version=\"8.4.0\" />\n    <PackageReference Include=\"K4os.Compression.LZ4.Streams\" Version=\"1.3.8\" />\n    <PackageReference Include=\"Microsoft.Windows.CsWin32\" Version=\"0.3.183\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n  </ItemGroup>\n\n  \n  <ItemGroup>\n    <ProjectReference Include=\"..\\CompactGUI.Logging\\CompactGUI.Logging.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "CompactGUI.Core/Compactor.cs",
    "content": "﻿\nusing CompactGUI.Logging.Core;\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Microsoft.Win32.SafeHandles;\nusing System.Collections.Concurrent;\nusing System.Diagnostics;\nusing System.IO.Enumeration;\nusing System.Runtime.InteropServices;\nusing Windows.Win32;\n\nnamespace CompactGUI.Core;\n\npublic sealed class Compactor : ICompressor, IDisposable\n{\n\n    private readonly string workingDirectory;\n    private readonly HashSet<string> excludedFileExtensions;\n    private readonly WOFCompressionAlgorithm wofCompressionAlgorithm;\n\n\n    private IntPtr compressionInfoPtr;\n    private UInt32 compressionInfoSize;\n\n    private long totalProcessedBytes = 0;\n    private readonly SemaphoreSlim pauseSemaphore = new SemaphoreSlim(1, 2);\n    private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();\n\n    private ILogger<Compactor> _logger;\n\n    private Analyser _analyser;\n\n    public Compactor(string folderPath, WOFCompressionAlgorithm compressionLevel, string[] excludedFileTypes, Analyser analyser, ILogger<Compactor>? logger = null)\n    {\n        workingDirectory = folderPath;\n        excludedFileExtensions = new HashSet<string>(excludedFileTypes);\n        wofCompressionAlgorithm = compressionLevel;\n        _logger = logger ?? NullLogger<Compactor>.Instance;\n        _analyser = analyser;\n        InitializeCompressionInfoPointer();\n    }\n\n\n    private void InitializeCompressionInfoPointer()\n    {\n        var _EFInfo = new WOFHelper.WOF_FILE_COMPRESSION_INFO_V1 { Algorithm = (UInt32)wofCompressionAlgorithm, Flags = 0 };\n        compressionInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_EFInfo));\n        compressionInfoSize = (UInt32)Marshal.SizeOf(_EFInfo);\n        Marshal.StructureToPtr(_EFInfo, compressionInfoPtr, true);\n\n    }\n\n    public async Task<bool> RunAsync(List<string> filesList, IProgress<CompressionProgress> progressMonitor = null, int maxParallelism = 1)\n    {\n        if(cancellationTokenSource.IsCancellationRequested) { return false; }\n\n        CompactorLog.BuildingWorkingFilesList(_logger, workingDirectory);\n        var workingFiles = await BuildWorkingFilesList().ConfigureAwait(false);\n        long totalFilesSize = workingFiles.Sum((f) => f.UncompressedSize);\n\n        totalProcessedBytes = 0;\n\n        var sw = Stopwatch.StartNew();\n\n        if (maxParallelism <= 0) maxParallelism = Environment.ProcessorCount;\n        ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxParallelism, CancellationToken = cancellationTokenSource.Token };\n\n        CompactorLog.StartingCompression(_logger, workingDirectory, wofCompressionAlgorithm.ToString(), maxParallelism);\n        try\n        {\n           await Parallel.ForEachAsync(workingFiles, parallelOptions,\n                (file, ctx) =>\n                {\n                    ctx.ThrowIfCancellationRequested();\n\n                    return new ValueTask(PauseAndProcessFile(file, totalFilesSize, cancellationTokenSource.Token, progressMonitor));\n                }).ConfigureAwait(false);\n        }\n        catch (OperationCanceledException){\n            CompactorLog.CompressionCanceled(_logger);\n            return false; \n        }\n        catch (Exception ex){ \n            CompactorLog.CompressionFailed(_logger, ex.Message);\n            return false; \n        }\n        finally { sw.Stop();}\n\n\n        \n        CompactorLog.CompressionCompleted(_logger, Math.Round(sw.Elapsed.TotalSeconds, 3));\n        return true;\n    }\n\n    private async Task PauseAndProcessFile(FileDetails file, long totalFilesSize, CancellationToken token, IProgress<CompressionProgress> progressMonitor)\n    {\n        CompactorLog.ProcessingFile(_logger, file.FileName, file.UncompressedSize);\n\n        await pauseSemaphore.WaitAsync(token).ConfigureAwait(false);\n        pauseSemaphore.Release();\n\n        var res = WOFCompressFile(file.FileName);\n        Interlocked.Add(ref totalProcessedBytes, file.UncompressedSize);\n        progressMonitor?.Report(new CompressionProgress((int)((double)totalProcessedBytes / totalFilesSize * 100.0), file.FileName));\n\n    }\n\n    private unsafe int? WOFCompressFile(string filePath)\n    {\n        try\n        {\n            using (SafeFileHandle fs = File.OpenHandle(filePath))\n            {\n                return PInvoke.WofSetFileDataLocation(fs, (uint)WOFHelper.WOF_PROVIDER_FILE, compressionInfoPtr.ToPointer(), compressionInfoSize);\n            }\n        }\n        catch (Exception ex)\n        {\n            CompactorLog.FileCompressionFailed(_logger, filePath, ex.Message);\n            return null;\n        }\n    }\n\n    public async Task<IEnumerable<FileDetails>> BuildWorkingFilesList()\n    {\n        uint clusterSize = SharedMethods.GetClusterSize(workingDirectory);\n\n        \n        var analysedFiles = await _analyser.GetAnalysedFilesAsync(cancellationTokenSource.Token);\n\n        var filesList = analysedFiles?\n            .Where(fl =>\n                fl.CompressionMode != wofCompressionAlgorithm\n                && fl.UncompressedSize > clusterSize\n                && ((fl.FileInfo != null && !excludedFileExtensions.Contains(fl.FileInfo.Extension)) || excludedFileExtensions.Contains(fl.FileName))\n            )\n            .Select(fl => new FileDetails(fl.FileName, fl.UncompressedSize))\n            .ToList();\n\n        return filesList;\n    }\n\n\n\n\n    public void Pause()\n    {\n        CompactorLog.CompressionPaused(_logger);\n        pauseSemaphore.Wait(cancellationTokenSource.Token);\n    }\n\n\n    public void Resume()\n    {\n        if (pauseSemaphore.CurrentCount == 0) pauseSemaphore.Release();  \n        CompactorLog.CompressionResumed(_logger);\n    }\n\n\n    public void Cancel()\n    {\n        Resume();\n        cancellationTokenSource.Cancel();\n    }\n\n\n    public void Dispose()\n    {\n        cancellationTokenSource?.Dispose();\n        pauseSemaphore?.Dispose();\n        if (compressionInfoPtr != IntPtr.Zero)\n        {\n            Marshal.FreeHGlobal(compressionInfoPtr);\n            compressionInfoPtr = IntPtr.Zero;\n        }\n    }\n\n\n    public readonly record struct FileDetails(string FileName, long UncompressedSize);\n\n\n}\n"
  },
  {
    "path": "CompactGUI.Core/Estimator.cs",
    "content": "﻿using K4os.Compression.LZ4;\nusing K4os.Compression.LZ4.Streams;\nusing Microsoft.Win32.SafeHandles;\nusing System.Collections.Concurrent;\nusing System.Runtime.InteropServices;\nusing System.Security.AccessControl;\nusing Windows.Win32;\n\nnamespace CompactGUI.Core;\n\npublic class Estimator\n{\n\n    private const int BlockSize = 8192;\n    private const int SampleSize = 100;\n\n    private int DiskClusterSize = 4096;\n    private bool isHDD = false;\n\n    public delegate Stream CompressionStreamFactory(Stream output);\n\n    private record FileDetails(AnalysedFileDetails AnalysedFile, float CompressionRatio);\n\n    public List<(AnalysedFileDetails AnalysedFile, float CompressionRatio)> EstimateCompression(\n        List<AnalysedFileDetails> analysedFileDetails,\n        bool isHdd,\n        int maxParallelism = 1,\n        int clusterSize = 4096,\n        CancellationToken? cToken = null)\n    {\n\n        DiskClusterSize = clusterSize;\n        if (maxParallelism <= 0) maxParallelism = Environment.ProcessorCount;\n        isHDD = isHdd;\n\n        ConcurrentBag<FileDetails> filesList = new();\n\n        var filteredList = analysedFileDetails.Where(f => f.UncompressedSize > clusterSize);\n        if (isHdd) filteredList = filteredList.OrderBy(f => GetFirstLcn(f.FileName));\n        var finalList = filteredList.ToList();\n\n        ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxParallelism };\n\n        Parallel.ForEach(finalList, parallelOptions, file =>\n        {\n            cToken?.ThrowIfCancellationRequested();\n            float estimatedRatio = EstimateCompressabilityLZ4(file.FileName, file.UncompressedSize, cToken ?? CancellationToken.None);\n            filesList.Add(new FileDetails(file, (float)estimatedRatio));\n\n        });\n\n\n        return filesList.Select(f => (f.AnalysedFile, f.CompressionRatio)).ToList();\n\n    }\n\n    private float EstimateCompressabilityLZ4(string fileName, long uncompressedSize, CancellationToken cancellationToken)\n    {\n        try\n        {\n            using (FileStream fs = File.OpenRead(fileName))\n            {\n                if (isHDD) return EstimateCompressabilityHDD(fs, uncompressedSize,\n                    output => LZ4Stream.Encode(output, LZ4Level.L00_FAST, 0,true), cancellationToken);\n\n                return EstimateCompressability(fs, uncompressedSize,\n                    output => LZ4Stream.Encode(output, LZ4Level.L00_FAST, 0, true), cancellationToken);\n            }\n\n\n        }\n        catch (Exception)\n        {\n\n            return 1.0f;\n        }\n    }\n\n    private float EstimateCompressability(FileStream input, long fileSize, CompressionStreamFactory compressionStreamFactory, CancellationToken cancellationToken)\n    {\n        long totalBlocks = fileSize / BlockSize;\n        int sampleSize = (int)Math.Ceiling((totalBlocks * SampleSize) / (SampleSize + totalBlocks -1.0f));\n        long totalWritten = 0;\n\n        MemoryStream compressed = new();\n\n        using (Stream compressionStream = compressionStreamFactory(compressed))\n        {\n            if (sampleSize == 0 || fileSize < sampleSize * BlockSize * 2)\n            {\n                Byte[] buffer = new byte[BlockSize];\n                input.Position = 0;\n\n                int bytesRead = input.Read(buffer, 0, BlockSize);\n\n                while (bytesRead > 0)\n                {\n                    cancellationToken.ThrowIfCancellationRequested();\n                    compressionStream.Write(buffer, 0, bytesRead);\n                    totalWritten += bytesRead;\n                    bytesRead = input.Read(buffer, 0, BlockSize);\n                }\n\n            }\n            else\n            {\n                long stepSize = BlockSize * (totalBlocks / sampleSize);\n\n                Byte[] buffer = new byte[BlockSize];\n                for (int i = 0; i < sampleSize; i++)\n                {\n                    cancellationToken.ThrowIfCancellationRequested();\n                    input.Position = i * stepSize;\n                    int bytesRead = input.Read(buffer, 0, BlockSize);\n                    compressionStream.Write(buffer, 0, bytesRead);\n                    totalWritten += bytesRead;\n                }\n\n            }\n        }\n\n        return Math.Min(((float)compressed.Length) / Math.Max(totalWritten, 1), 1.0f);\n\n\n    }\n\n    private float EstimateCompressabilityHDD(FileStream input, long fileSize, CompressionStreamFactory compressionStreamFactory, CancellationToken cancellationToken)\n    {\n        int numClusters = SampleSize; // or any small number to sample;\n        int clusterSize = DiskClusterSize;\n\n        long middleCluster = fileSize / (2 * clusterSize);\n        long alignedStart = middleCluster * clusterSize;\n        int chunkSize = (int)Math.Min(clusterSize * numClusters, fileSize - alignedStart);\n\n        long totalWritten = 0;\n\n        MemoryStream compressed = new();\n\n        using (Stream compressionStream = compressionStreamFactory(compressed))\n        {\n            input.Position = alignedStart;\n            Byte[] buffer = new byte[chunkSize];\n            int bytesRead = input.Read(buffer, 0, chunkSize);\n            \n            cancellationToken.ThrowIfCancellationRequested();\n\n            if (bytesRead > 0)\n            {\n                compressionStream.Write(buffer, 0, bytesRead);\n                totalWritten += bytesRead;\n            }\n\n        }\n\n        return Math.Min(((float)compressed.Length) / Math.Max(totalWritten, 1), 1.0f);\n\n    }\n\n    unsafe long GetFirstLcn(string fileName)\n    {\n        SafeFileHandle handle = File.OpenHandle(fileName);\n\n        if (handle.IsInvalid) throw new IOException(\"Failed to open file handle for \" + fileName);\n\n        NTFSInterop.STARTING_VCN_INPUT_BUFFER inBuffer = new() { StartingVcn = 0 };\n        uint inBufferSize = (uint)sizeof(NTFSInterop.STARTING_VCN_INPUT_BUFFER);\n\n        int outBufferSize = 4096;\n        byte* outBuffer = stackalloc byte[outBufferSize];\n\n        uint bytesReturned = 0;\n\n        var result = PInvoke.DeviceIoControl(\n            handle,\n            NTFSInterop.FSCTL_GET_RETRIEVAL_POINTERS,\n            &inBuffer,\n            inBufferSize,\n            outBuffer,\n            (uint)outBufferSize,\n            &bytesReturned,\n            null);\n\n        if (!result) return long.MaxValue;\n\n        int extentOffset = Marshal.OffsetOf<NTFSInterop.RETRIEVAL_POINTERS_BUFFER>(\"Extents\").ToInt32();\n        long lcn = Marshal.ReadInt64((IntPtr)outBuffer + extentOffset + 8);\n\n        return lcn;\n\n    }\n\n\n\n\n}\n"
  },
  {
    "path": "CompactGUI.Core/FolderChangeMonitor.cs",
    "content": "﻿using System;\nusing System.IO;\nusing System.Threading;\n\nnamespace CompactGUI.Core;\n\n/// <summary>\n/// Monitors a folder (and subfolders) for changes, with debouncing and proper disposal.\n/// </summary>\npublic sealed class FolderChangeMonitor : IDisposable\n{\n    private readonly FileSystemWatcher _watcher;\n    private readonly Timer _debounceTimer;\n    private readonly int _debounceMilliseconds;\n    private bool _hasChanged;\n    private DateTime _lastChanged;\n    private bool _disposed;\n\n    /// <summary>\n    /// Raised when the folder has changed (debounced).\n    /// </summary>\n    public event EventHandler? Changed;\n\n    /// <summary>\n    /// True if a change has been detected since the last reset.\n    /// </summary>\n    public bool HasChanged => _hasChanged;\n\n    /// <summary>\n    /// The last time a change was detected.\n    /// </summary>\n    public DateTime LastChanged => _lastChanged;\n\n    /// <summary>\n    /// Create a new FolderChangeMonitor.\n    /// </summary>\n    /// <param name=\"folderPath\">The folder to monitor.</param>\n    /// <param name=\"debounceMilliseconds\">Debounce interval in ms (default: 1000).</param>\n    public FolderChangeMonitor(string folderPath, int debounceMilliseconds = 1000)\n    {\n        _debounceMilliseconds = debounceMilliseconds;\n        _watcher = new FileSystemWatcher(folderPath)\n        {\n            NotifyFilter = NotifyFilters.Size | NotifyFilters.CreationTime | NotifyFilters.LastWrite | NotifyFilters.FileName,\n            IncludeSubdirectories = true,\n            EnableRaisingEvents = true\n        };\n        _watcher.Changed += OnChanged;\n        _watcher.Created += OnChanged;\n        _watcher.Deleted += OnChanged;\n        _watcher.Renamed += OnChanged;\n        _watcher.Error += OnError;\n\n        _debounceTimer = new Timer(DebounceCallback, null, Timeout.Infinite, Timeout.Infinite);\n    }\n\n    private void OnChanged(object sender, FileSystemEventArgs e)\n    {\n        _debounceTimer.Change(_debounceMilliseconds, Timeout.Infinite);\n    }\n\n    private void DebounceCallback(object? state)\n    {\n        _hasChanged = true;\n        _lastChanged = DateTime.Now;\n        Changed?.Invoke(this, EventArgs.Empty);\n    }\n\n    private void OnError(object sender, ErrorEventArgs e)\n    {\n        // Optionally log or handle errors here\n    }\n\n    /// <summary>\n    /// Reset the change flag after handling.\n    /// </summary>\n    public void Reset()\n    {\n        _hasChanged = false;\n    }\n\n    public void Dispose()\n    {\n        if (_disposed) return;\n        _watcher.Dispose();\n        _debounceTimer.Dispose();\n        _disposed = true;\n        GC.SuppressFinalize(this);\n    }\n}"
  },
  {
    "path": "CompactGUI.Core/ICompressor.cs",
    "content": "﻿namespace CompactGUI.Core;\n\npublic interface ICompressor : IDisposable\n{\n    Task<bool> RunAsync(List<String> filesList,\n        IProgress<CompressionProgress> progressMonitor = null,\n        int maxParallelism = 1);\n\n    void Pause();\n    void Resume();\n    void Cancel();\n\n\n}"
  },
  {
    "path": "CompactGUI.Core/NTFSInterop.cs",
    "content": "﻿using System.Runtime.InteropServices;\n\nnamespace CompactGUI.Core;\n\ninternal static class NTFSInterop\n{\n\n    internal const uint FSCTL_GET_RETRIEVAL_POINTERS = 0x90073;\n    internal const int OPEN_EXISTING = 3;\n    internal const int FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;\n    internal const int FILE_SHARE_READ = 1;\n    internal const int FILE_SHARE_WRITE = 2;\n    internal const uint GENERIC_READ = 0x80000000;\n\n    [StructLayout(LayoutKind.Sequential)]\n    internal struct STARTING_VCN_INPUT_BUFFER\n    {\n        public long StartingVcn;\n    }\n\n\n    [StructLayout(LayoutKind.Sequential)]\n    internal struct RETRIEVAL_POINTERS_BUFFER\n    {\n        public int ExtentCount;\n        public long StartingVcn;\n        public LCN_EXTENT Extents; // This is actually an array, but we only need the first. ?Perhaps I should get the LCN of the middle cluster in future?\n    }\n\n\n    [StructLayout(LayoutKind.Sequential)]\n    internal struct LCN_EXTENT\n    {\n        public long NextVcn;\n        public long Lcn;\n    }\n\n}\n"
  },
  {
    "path": "CompactGUI.Core/NativeMethods.txt",
    "content": "﻿WofIsExternalFile\nWofSetFileDataLocation\nDeviceIoControl\nGetDiskFreeSpace\nSetThreadExecutionState\nGetShortPathName\nGetCompressedFileSizeW"
  },
  {
    "path": "CompactGUI.Core/Settings/ISettingsService.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace CompactGUI.Core.Settings\n{\n    public interface ISettingsService\n    {\n\n        DirectoryInfo DataFolder { get; }\n        FileInfo SettingsJSONFile { get; }\n\n        Settings AppSettings { get; set; }\n\n        decimal SettingsVersion { get; }\n\n        void LoadSettings();\n        void SaveSettings();\n        void ScheduleSettingsSave();\n\n\n    }\n\n\n}\n"
  },
  {
    "path": "CompactGUI.Core/Settings/Settings.cs",
    "content": "﻿using CommunityToolkit.Mvvm.ComponentModel;\nusing Microsoft.Extensions.Logging;\n\nnamespace CompactGUI.Core.Settings;\n\n[NotifyPropertyChangedRecipients]\npublic partial class Settings : ObservableRecipient\n{\n    // TODO: Add local saving of per-folder skip list\n\n    [ObservableProperty] private decimal settingsVersion = 1.2m; // You may want to inject this value\n    [ObservableProperty] private LogLevel logLevel = LogLevel.Information;\n    [ObservableProperty] private DateTime resultsDBLastUpdated = DateTime.UnixEpoch;\n    [ObservableProperty] private int selectedCompressionMode = 0;\n    [ObservableProperty] private bool skipNonCompressable = false;\n    [ObservableProperty] private bool skipUserNonCompressable = false;\n    [ObservableProperty] private bool watchFolderForChanges = false;\n    [ObservableProperty] private List<string> nonCompressableList = new List<string> { \".dl_\", \".gif\", \".jpg\", \".jpeg\", \".png\", \".wmf\", \".mkv\", \".mp4\", \".wmv\", \".avi\", \".bik\", \".bk2\", \".flv\", \".ogg\", \".mpg\", \".m2v\", \".m4v\", \".vob\", \".mp3\", \".aac\", \".wma\", \".flac\", \".zip\", \".xap\", \".rar\", \".7z\", \".cab\", \".lzx\", \".docx\", \".xlsx\", \".pptx\", \".vssx\", \".vstx\", \".onepkg\", \".tar\", \".gz\", \".dmg\", \".bz2\", \".tgz\", \".lz\", \".xz\", \".txz\"};\n\n    [ObservableProperty] private int skipUserFileTypesLevel = 0;\n    [ObservableProperty] private bool showNotifications = false;\n    [ObservableProperty] private bool startInSystemTray = false;\n\n    [ObservableProperty] private int maxCompressionThreads = 0;\n    [ObservableProperty] private bool lockHDDsToOneThread = true;\n    [ObservableProperty] private bool estimateCompressionForNonSteamFolders = false;\n\n    [ObservableProperty] private bool enableBackgroundWatcher = true;\n    [ObservableProperty] private BackgroundMode backgroundModeSelection = BackgroundMode.IdleOnly;\n    [ObservableProperty][NotifyPropertyChangedFor(nameof(NextScheduledBackgroundRun))] private int scheduledBackgroundInterval = 1; // in days\n    [ObservableProperty][NotifyPropertyChangedFor(nameof(NextScheduledBackgroundRun))] private int scheduledBackgroundHour = 0; // 0-23\n    [ObservableProperty][NotifyPropertyChangedFor(nameof(NextScheduledBackgroundRun))] private int scheduledBackgroundMinute = 0; // 0-59\n    [ObservableProperty][NotifyPropertyChangedFor(nameof(NextScheduledBackgroundRun))] private DateTime scheduledBackgroundLastRan = DateTime.MinValue;\n    [ObservableProperty] private DateTime nextScheduledBackgroundRun = DateTime.MaxValue;\n\n    [ObservableProperty] private bool allowMultiInstance = false;\n    [ObservableProperty] private bool enablePreReleaseUpdates = true;\n\n    [ObservableProperty] private bool isContextIntegrated = true;\n    [ObservableProperty] private bool isStartMenuEnabled = true;\n\n    [ObservableProperty] private double windowTop = 0;\n    [ObservableProperty] private double windowLeft = 0;\n    [ObservableProperty] private double windowWidth = 1300;\n    [ObservableProperty] private double windowHeight = 700;\n    [ObservableProperty] private WindowState windowState = WindowState.Normal;\n    [ObservableProperty] private bool alwaysShowDetailedCompressionMode = false;\n\n\n    partial void OnScheduledBackgroundIntervalChanged(int value) => UpdateNextScheduledBackgroundRun();\n\n    partial void OnScheduledBackgroundHourChanged(int value) => UpdateNextScheduledBackgroundRun();\n\n    partial void OnScheduledBackgroundMinuteChanged(int value) => UpdateNextScheduledBackgroundRun();\n\n    partial void OnScheduledBackgroundLastRanChanged(DateTime value) => UpdateNextScheduledBackgroundRun();\n\n    private void UpdateNextScheduledBackgroundRun()\n    {\n        DateTime nextRun = scheduledBackgroundLastRan.Date\n                 .AddDays(scheduledBackgroundInterval)\n                 .AddHours(scheduledBackgroundHour)\n                 .AddMinutes(scheduledBackgroundMinute);\n        DateTime now = DateTime.Now;\n        while (nextRun <= now)\n        {\n            nextRun = nextRun.AddDays(scheduledBackgroundInterval == 0 ? 1 : scheduledBackgroundInterval);\n        }\n\n        NextScheduledBackgroundRun = nextRun;\n    }\n\n\n\n\n}\n\npublic enum WindowState\n{\n    Normal,\n    Minimized,\n    Maximized\n}\n\npublic enum BackgroundMode\n{\n    Never,\n    IdleOnly,\n    Scheduled,\n    ScheduledAndIdle\n}\n"
  },
  {
    "path": "CompactGUI.Core/SharedMethods.cs",
    "content": "﻿using System.Runtime.InteropServices;\nusing System.Security.AccessControl;\nusing System.Security.Principal;\nusing Windows.Win32;\n\nusing Windows.Win32.System.Power;\n\nnamespace CompactGUI.Core;\n\npublic static class SharedMethods\n{\n\n\n    public enum FolderVerificationResult\n    {\n        Valid = 0,\n        DirectoryDoesNotExist,\n        SystemDirectory,\n        RootDirectory,\n        DirectoryEmptyOrUnauthorized,\n        OneDriveFolder,\n        NonNTFSDrive,\n        InsufficientPermission,\n        LZNT1Compressed\n    }\n\n    public static FolderVerificationResult VerifyFolder(string folder)\n    {\n        if (!Directory.Exists(folder))\n            return FolderVerificationResult.DirectoryDoesNotExist;\n        else if (folder.ToLowerInvariant().Contains(Environment.GetFolderPath(Environment.SpecialFolder.Windows).ToLowerInvariant()))\n            return FolderVerificationResult.SystemDirectory;\n        else if (folder.EndsWith(\":\\\\\"))\n            return FolderVerificationResult.RootDirectory;\n        else if (IsDirectoryEmptySafe(folder))\n            return FolderVerificationResult.DirectoryEmptyOrUnauthorized;\n        else if (IsOneDriveFolder(folder))\n            return FolderVerificationResult.OneDriveFolder;\n        else if (DriveInfo.GetDrives().First(f => folder.StartsWith(f.Name)).DriveFormat != \"NTFS\")\n            return FolderVerificationResult.NonNTFSDrive;\n        else if (!HasDirectoryWritePermission(folder))\n            return FolderVerificationResult.InsufficientPermission;\n        else if (IsFolderLZNT1Compressed(folder))\n            return FolderVerificationResult.LZNT1Compressed;\n\n        return FolderVerificationResult.Valid;\n    }\n\n    public static string GetFolderVerificationMessage(FolderVerificationResult result)\n    {\n        return result switch\n        {\n            FolderVerificationResult.Valid => \"\",\n            FolderVerificationResult.DirectoryDoesNotExist => \"Directory does not exist\",\n            FolderVerificationResult.SystemDirectory => \"Cannot compress system directory\",\n            FolderVerificationResult.RootDirectory => \"Cannot compress root directory\",\n            FolderVerificationResult.DirectoryEmptyOrUnauthorized => \"This directory is either empty or you are not authorized to access its files.\",\n            FolderVerificationResult.OneDriveFolder => \"Files synced with OneDrive cannot be compressed as they use a different storage structure\",\n            FolderVerificationResult.NonNTFSDrive => \"Cannot compress a directory on a non-NTFS drive\",\n            FolderVerificationResult.InsufficientPermission => \"Insufficient permission to access this folder.\",\n            FolderVerificationResult.LZNT1Compressed => \"Folders using Windows' compression are not supported. Disable Windows compression on this folder first.\",\n            _ => \"Unknown error\"\n        };\n    }\n\n\n    static bool IsDirectoryEmptySafe(string folderPath)\n    {\n        try\n        {\n            return !Directory.EnumerateFileSystemEntries(folderPath).Any();\n        }\n        catch (Exception)\n        {\n            return false; // Assume empty for any other exception\n        }\n    }\n\n\n    static bool IsOneDriveFolder(string folderPath)\n    {\n        string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);\n        List<string> oneDrivePaths = new List<string>\n        {\n            Path.Combine(userProfile, \"OneDrive\"), // Personal OneDrive\n            Path.Combine(userProfile, \"OneDrive - Personal\"), // Alternative Personal OneDrive\n            Path.Combine(userProfile, \"OneDrive for Business\"), // OneDrive for Business\n            Path.Combine(userProfile, \"OneDrive - Business\") // Alternative OneDrive for Business\n        };\n\n        string normalizedFolderPath = Path.GetFullPath(folderPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).ToLowerInvariant();\n\n        return oneDrivePaths.Any(oneDrivePath =>\n                    normalizedFolderPath.StartsWith(Path.GetFullPath(oneDrivePath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).ToLowerInvariant()));\n\n    }\n\n    public static void PreventSleep()\n    {\n        PInvoke.SetThreadExecutionState(\n            EXECUTION_STATE.ES_CONTINUOUS |\n            EXECUTION_STATE.ES_SYSTEM_REQUIRED |\n            EXECUTION_STATE.ES_DISPLAY_REQUIRED);\n    }\n\n\n    public static void RestoreSleep()\n    {\n        PInvoke.SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);\n    }\n\n\n\n\n\n\n\n\n\n\n\n\n    public static IEnumerable<string> AsShortPathNames(this IEnumerable<string> filesList)\n    {\n        return filesList.Select(file => (file.Length >= 255 ? GetShortPath(file) ?? file : file));\n    }\n\n\n    private static string GetShortPath(string filePath)\n    {\n        const string LongPathPrefix = @\"\\\\?\\\";\n        ReadOnlySpan<char> longPathPrefixSpan = LongPathPrefix;\n\n        if (string.IsNullOrWhiteSpace(filePath)) return filePath;\n        ReadOnlySpan<char> filePathSpan = filePath;\n\n        bool addPrefix = filePathSpan.Length >= 255 && !filePathSpan.StartsWith(longPathPrefixSpan, StringComparison.Ordinal);\n        string pathToUse = addPrefix ? LongPathPrefix + filePath : filePath;\n\n        Span<char> shortPath = stackalloc char[1024];\n        uint res = PInvoke.GetShortPathName(pathToUse, shortPath);\n        if (res == 0) return filePath;\n\n        ReadOnlySpan<char> resultSpan = shortPath[..(int)res];\n        return addPrefix && resultSpan.StartsWith(longPathPrefixSpan, StringComparison.Ordinal)\n            ? resultSpan[longPathPrefixSpan.Length..].ToString()\n            : resultSpan.ToString();\n    }\n\n\n\n    public static unsafe uint GetClusterSize(string folderPath)\n    {\n        UInt32 lpSectorsPerCluster;\n        UInt32 lpBytesPerSector;\n\n        PInvoke.GetDiskFreeSpace(\n            new DirectoryInfo(folderPath).Root.ToString(),\n            &lpSectorsPerCluster,\n            &lpBytesPerSector,\n            null,\n            null\n        );\n\n        return lpSectorsPerCluster * lpBytesPerSector;\n\n    }\n\n\n    public static unsafe long GetFileSizeOnDisk(string file)\n    {\n        uint highOrder;\n        uint lowOrder = PInvoke.GetCompressedFileSize(file, &highOrder);\n        if (lowOrder == 0xFFFFFFFF && (Marshal.GetLastWin32Error() != 0)) return -1;\n        return ((long)highOrder << 32) | lowOrder;\n    }\n\n\n    public static bool HasDirectoryWritePermission(string folderName)\n    {\n        try\n        {\n            DirectoryInfo dirInfo = new DirectoryInfo(folderName);\n            DirectorySecurity dirSecurity = dirInfo.GetAccessControl();\n\n            var user = WindowsIdentity.GetCurrent();\n            var userSID = user.User;\n            var userGroupSIDs = user.Groups;\n\n            var rules = dirSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier));\n\n            bool writeAllowed = false;\n            bool writeDenied = false;\n\n            foreach (FileSystemAccessRule rule in rules)\n            {\n                var fileSystemRights = rule.FileSystemRights;\n\n                if (!rule.FileSystemRights.HasFlag(FileSystemRights.Write)) continue;\n                if (rule.IdentityReference is not SecurityIdentifier ruleSID) continue;\n                if (!ruleSID.Equals(userSID) && !userGroupSIDs.Contains(ruleSID)) continue;\n\n                if (rule.AccessControlType == AccessControlType.Deny)\n                {\n                    writeDenied = true;\n                    break;\n                }\n\n                if (rule.AccessControlType == AccessControlType.Allow) writeAllowed = true;\n            }\n\n            return writeAllowed && !writeDenied;\n\n        }\n        catch (UnauthorizedAccessException) { return false; }\n    }\n\n    public static bool IsFolderLZNT1Compressed(string folderPath)\n    {\n        var attributes = File.GetAttributes(folderPath);\n        return attributes.HasFlag(FileAttributes.Compressed);\n    }\n\n    public static bool IsDirectStorageGameFolder(string folderPath)\n    {\n        if (string.IsNullOrWhiteSpace(folderPath) || !Directory.Exists(folderPath)) return false;\n\n        // List of possible DirectStorage DLL relative paths from https://github.com/SteamDatabase/FileDetectionRuleSets/blob/main/tests/types/SDK.DirectStorage.txt\n        string[] directStoragePaths = new[]\n        {\n            \"dstorage.dll\",\n            Path.Combine(\"Bin64\", \"dstorage.dll\"),\n            Path.Combine(\"Engine\", \"Binaries\", \"ThirdParty\", \"Windows\", \"DirectStorage\", \"x64\", \"dstorage.dll\")\n        };\n\n        foreach (var relativePath in directStoragePaths)\n        {\n            string fullPath = Path.Combine(folderPath, relativePath);\n            if (File.Exists(fullPath)) return true;\n        }\n\n        return false;\n    }\n\n\n}\n"
  },
  {
    "path": "CompactGUI.Core/SharedObjects.cs",
    "content": "﻿namespace CompactGUI.Core;\n\n\npublic sealed class AnalysedFileDetails\n{\n    public required string FileName { get; set; }\n    public long UncompressedSize { get; set; }\n    public long CompressedSize { get; set; }\n    public WOFCompressionAlgorithm CompressionMode { get; set; }\n    public FileInfo? FileInfo { get; set; }\n}\n\n\npublic sealed class ExtensionResult\n{\n    public required string Extension { get; set; }\n    public long UncompressedBytes { get; set; }\n    public long CompressedBytes { get; set; }\n    public int TotalFiles { get; set; }\n    public double CRatio => CompressedBytes == 0 ? 0 : Math.Round((double)CompressedBytes / UncompressedBytes, 2);\n\n}\n\n\n\n\npublic struct CompressionProgress\n{\n    public int ProgressPercent;\n    public string FileName;\n\n    public CompressionProgress(int progressPercent, string fileName)\n    {\n        ProgressPercent = progressPercent;\n        FileName = fileName;\n    }\n}\n\n\npublic enum CompressionMode: int\n{\n    XPRESS4K,\n    XPRESS8K,\n    XPRESS16K,\n    LZX,\n    None\n}\n\n\npublic enum WOFCompressionAlgorithm: int\n{\n    NO_COMPRESSION = -2,\n    LZNT1 = -1,\n    XPRESS4K = 0,\n    LZX = 1,\n    XPRESS8K = 2,\n    XPRESS16K = 3\n}"
  },
  {
    "path": "CompactGUI.Core/Uncompactor.cs",
    "content": "﻿\nusing Microsoft.Extensions.Logging;\nusing Microsoft.Extensions.Logging.Abstractions;\nusing Microsoft.Win32.SafeHandles;\nusing System.Collections.Concurrent;\nusing Windows.Win32;\nusing CompactGUI.Logging.Core;\nusing System.Diagnostics;\n\nnamespace CompactGUI.Core;\n\npublic sealed class Uncompactor : ICompressor, IDisposable\n{\n\n    private SemaphoreSlim pauseSemaphore = new SemaphoreSlim(1, 2);\n    private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();\n    private ConcurrentDictionary<string, int> processedFileCount = new ConcurrentDictionary<string, int>();\n\n    private readonly ILogger<Uncompactor> _logger;\n\n    public Uncompactor(ILogger<Uncompactor>? logger = null)\n    {\n        _logger = logger ?? NullLogger<Uncompactor>.Instance;\n    }\n\n    public async Task<bool> RunAsync(List<string> filesList, IProgress<CompressionProgress>? progressMonitor = null, int maxParallelism = 1)\n    {\n        int totalFiles = filesList.Count;\n        if (maxParallelism <= 0) maxParallelism = Environment.ProcessorCount;\n        ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = maxParallelism };\n        processedFileCount.Clear();\n\n        UncompactorLog.StartingDecompression(_logger, totalFiles, maxParallelism);\n        Stopwatch sw = Stopwatch.StartNew();\n        try\n        {\n            await Parallel.ForEachAsync(filesList, parallelOptions,\n                (file, ctx) =>\n                {\n                    ctx.ThrowIfCancellationRequested();\n                    return new ValueTask(PauseAndProcessFile(file, totalFiles, progressMonitor, cancellationTokenSource.Token));\n                });\n        }\n        catch (OperationCanceledException) {\n            UncompactorLog.DecompressionCanceled(_logger);\n            return false; \n        }\n        finally { sw.Stop(); }\n\n        UncompactorLog.DecompressionCompleted(_logger, Math.Round(sw.Elapsed.TotalSeconds, 3));\n        return true;\n\n    }\n\n    private async Task PauseAndProcessFile(string file, int totalFiles, IProgress<CompressionProgress>? progressMonitor, CancellationToken ctx)\n    {\n        UncompactorLog.ProcessingFile(_logger, file);\n        try\n        {\n            await pauseSemaphore.WaitAsync(ctx).ConfigureAwait(false);\n            pauseSemaphore.Release();\n        }\n        catch (OperationCanceledException) { throw; }\n        ctx.ThrowIfCancellationRequested();\n\n        var _ = WOFDecompressFile(file);\n        processedFileCount.TryAdd(file, 1);\n        progressMonitor?.Report(new CompressionProgress(\n                (int)(processedFileCount.Count / (float)totalFiles * 100),\n                file)\n        );\n\n    }\n\n    private unsafe bool? WOFDecompressFile(string file)\n    {\n        try\n        {\n            using (SafeFileHandle fs = File.OpenHandle(file))\n            {\n                var res = PInvoke.DeviceIoControl(fs, WOFHelper.FSCTL_DELETE_EXTERNAL_BACKING, null, 0, null, 0, null, null);\n                return res;\n            }  \n        }\n        catch (Exception ex) { \n            UncompactorLog.FileDecompressionFailed(_logger, file, ex.Message);\n            return null; \n        }\n    }\n\n    public void Pause()\n    {\n        UncompactorLog.DecompressionPaused(_logger);\n        pauseSemaphore.Wait();\n    }\n\n\n    public void Resume()\n    {\n        if (pauseSemaphore.CurrentCount == 0) pauseSemaphore.Release();\n        UncompactorLog.DecompressionResumed(_logger);\n    }\n\n\n    public void Cancel()\n    {\n        Resume();\n        cancellationTokenSource.Cancel();\n    }\n\n\n    public void Dispose()\n    {\n        pauseSemaphore.Dispose();\n        cancellationTokenSource.Dispose();\n    }\n\n\n\n\n\n\n\n}\n"
  },
  {
    "path": "CompactGUI.Core/WOFHelper.cs",
    "content": "﻿using System.Runtime.InteropServices;\nusing Windows.Win32;\n\nnamespace CompactGUI.Core;\n\npublic static class WOFHelper\n{\n\n    public const UInt64 WOF_PROVIDER_FILE = 2;\n    public const UInt32 FSCTL_DELETE_EXTERNAL_BACKING = 0x90314;\n\n\n    public static WOFCompressionAlgorithm WOFConvertCompressionLevel(int level)\n    {\n        return level switch\n        {\n            0 => WOFCompressionAlgorithm.XPRESS4K,\n            1 => WOFCompressionAlgorithm.XPRESS8K,\n            2 => WOFCompressionAlgorithm.XPRESS16K,\n            3 => WOFCompressionAlgorithm.LZX,\n            _ => WOFCompressionAlgorithm.XPRESS4K\n        };\n    }\n\n    public static CompressionMode CompressionModeFromWOFMode(WOFCompressionAlgorithm mode)\n    {\n        return mode switch\n        {\n            WOFCompressionAlgorithm.XPRESS4K => CompressionMode.XPRESS4K,\n            WOFCompressionAlgorithm.XPRESS8K => CompressionMode.XPRESS8K,\n            WOFCompressionAlgorithm.XPRESS16K => CompressionMode.XPRESS16K,\n            WOFCompressionAlgorithm.LZX => CompressionMode.LZX,\n            _ => CompressionMode.None\n        };\n    }\n\n\n    public static WOFCompressionAlgorithm WOFConvertCompressionLevel(CompressionMode mode)\n    {\n        return mode switch\n        {\n            CompressionMode.XPRESS4K => WOFCompressionAlgorithm.XPRESS4K,\n            CompressionMode.XPRESS8K => WOFCompressionAlgorithm.XPRESS8K,\n            CompressionMode.XPRESS16K => WOFCompressionAlgorithm.XPRESS16K,\n            CompressionMode.LZX => WOFCompressionAlgorithm.LZX,\n            _ => WOFCompressionAlgorithm.XPRESS4K\n        };\n    }\n\n    [StructLayout(LayoutKind.Sequential)]\n    public struct WOF_FILE_COMPRESSION_INFO_V1\n    {\n        public UInt32 Algorithm;\n        public UInt32 Flags;\n    }\n\n\n    public static unsafe WOFCompressionAlgorithm DetectCompression(FileInfo fileInfo)\n    {\n        Windows.Win32.Foundation.BOOL isExternalFile;\n        UInt32 provider;\n        WOFHelper.WOF_FILE_COMPRESSION_INFO_V1 info;\n        uint buffer = 8;\n\n        var ret = PInvoke.WofIsExternalFile(fileInfo.FullName, &isExternalFile, &provider, &info, &buffer);\n\n        WOFCompressionAlgorithm algorithm = (WOFCompressionAlgorithm)info.Algorithm;\n\n        if (!isExternalFile) algorithm = WOFCompressionAlgorithm.NO_COMPRESSION;\n        if ((fileInfo.Attributes & FileAttributes.Compressed) != 0) algorithm = WOFCompressionAlgorithm.LZNT1;\n\n        return algorithm;\n\n    }\n}\n"
  },
  {
    "path": "CompactGUI.CoreVB/Analyser.vb",
    "content": "Imports System.IO\nImports System.Security.AccessControl\nImports System.Security.Principal\nImports System.Threading\n\nPublic Class Analyser\n\n    Public Property FolderName As String\n    Public Property UncompressedBytes As Long\n    Public Property CompressedBytes As Long\n    Public Property ContainsCompressedFiles As Boolean\n    Public Property FileCompressionDetailsList As List(Of AnalysedFileDetails)\n\n    Public Sub New(folder As String)\n        FolderName = folder\n    End Sub\n\n\n    Public Async Function AnalyseFolder(cancellationToken As CancellationToken) As Task(Of Boolean)\n        Dim allFiles = Await Task.Run(Function() Directory.EnumerateFiles(FolderName, \"*\", New EnumerationOptions() With {.RecurseSubdirectories = True, .IgnoreInaccessible = True}).AsShortPathNames, cancellationToken).ConfigureAwait(False)\n        Dim fileDetails As New List(Of AnalysedFileDetails)\n        Dim compressedFilesCount As Integer = 0\n\n        ' Use local variables to reduce contention\n        Dim localCompressedBytes As Long = 0\n        Dim localUncompressedBytes As Long = 0\n\n        Try\n            Parallel.ForEach(allFiles, New ParallelOptions With {.CancellationToken = cancellationToken},\n                            Sub(file)\n                                Dim details = AnalyseFile(file)\n                                If details IsNot Nothing Then\n                                    SyncLock fileDetails\n                                        fileDetails.Add(details)\n                                    End SyncLock\n                                    If details.CompressionMode <> WOFCompressionAlgorithm.NO_COMPRESSION Then\n                                        Interlocked.Increment(compressedFilesCount)\n                                    End If\n                                    Interlocked.Add(localCompressedBytes, details.CompressedSize)\n                                    Interlocked.Add(localUncompressedBytes, details.UncompressedSize)\n                                End If\n                            End Sub)\n\n            ' Update the shared state after the parallel loop to minimize contention\n            CompressedBytes = localCompressedBytes\n            UncompressedBytes = localUncompressedBytes\n        Catch ex As OperationCanceledException\n            Debug.WriteLine(ex.Message)\n            Return Nothing\n        End Try\n\n        ContainsCompressedFiles = compressedFilesCount > 0\n        FileCompressionDetailsList = fileDetails\n        Return ContainsCompressedFiles\n    End Function\n\n\n    Private Function AnalyseFile(file As String) As AnalysedFileDetails\n        Try\n            Dim fInfo As New FileInfo(file)\n            Dim unCompSize = fInfo.Length\n            Dim compSize = GetFileSizeOnDisk(file)\n            If compSize < 0 Then\n                compSize = unCompSize ' GetFileSizeOnDisk failed, fall back to unCompSize\n            End If\n            Dim cLevel As WOFCompressionAlgorithm = If(compSize = unCompSize, WOFCompressionAlgorithm.NO_COMPRESSION, DetectCompression(fInfo))\n\n            Return New AnalysedFileDetails With {.FileName = file, .CompressedSize = compSize, .UncompressedSize = unCompSize, .CompressionMode = cLevel, .FileInfo = fInfo}\n        Catch ex As IOException\n            Debug.WriteLine($\"Error analysing file {file}: {ex.Message}\")\n            Return Nothing\n        End Try\n    End Function\n\n\n    Public Async Function GetPoorlyCompressedExtensions() As Task(Of List(Of ExtensionResult))\n        Dim extClassResults As New List(Of ExtensionResult)\n        Await Task.Run(\n        Sub()\n            Dim extRes As New Concurrent.ConcurrentDictionary(Of String, ExtensionResult)\n            Parallel.ForEach(FileCompressionDetailsList,\n                               Sub(fl)\n                                   Dim xt = New FileInfo(fl.FileName).Extension\n                                   If fl.UncompressedSize = 0 Then Return\n\n                                   extRes.AddOrUpdate(xt,\n                                        Function(addKey) ' Add value factory\n                                            Return New ExtensionResult With {\n                                            .extension = xt,\n                                            .totalFiles = 1,\n                                            .uncompressedBytes = fl.UncompressedSize,\n                                            .compressedBytes = fl.CompressedSize\n                                        }\n                                        End Function,\n                                        Function(updateKey, oldValue) ' Update value factory\n                                            Return New ExtensionResult With {\n                                            .extension = xt,\n                                            .totalFiles = oldValue.totalFiles + 1,\n                                            .uncompressedBytes = oldValue.uncompressedBytes + fl.UncompressedSize,\n                                            .compressedBytes = oldValue.compressedBytes + fl.CompressedSize\n                                        }\n                                        End Function)\n                               End Sub)\n\n            ' Filter and convert to list after aggregation\n            extClassResults = extRes.Values.Where(Function(f) f.cRatio > 0.95).ToList()\n        End Sub)\n\n        Return extClassResults\n    End Function\n\n\n    Private Function DetectCompression(fInfo As FileInfo) As WOFCompressionAlgorithm\n\n        Dim isextFile As Integer\n        Dim prov As ULong\n        Dim info As WOF_FILE_COMPRESSION_INFO_V1\n        Dim buf As UInt16 = 8\n\n        Dim ret = WofIsExternalFile(fInfo.FullName, isextFile, prov, info, buf)\n\n        Dim algorithm As WOFCompressionAlgorithm = info.Algorithm\n\n        If isextFile = 0 Then algorithm = WOFCompressionAlgorithm.NO_COMPRESSION\n        If (fInfo.Attributes And 2048) <> 0 Then algorithm = WOFCompressionAlgorithm.LZNT1\n        Return algorithm\n\n    End Function\n\n\n    Public Shared Function HasDirectoryWritePermission(FolderName As String) As Boolean\n        Try\n            Dim directoryInfo = New DirectoryInfo(FolderName)\n            Dim directorySecurity = directoryInfo.GetAccessControl()\n\n            Dim user = WindowsIdentity.GetCurrent()\n            Dim userSID = user.User\n            Dim userGroupSIDs = user.Groups\n\n            Dim accessRules = directorySecurity.GetAccessRules(True, True, GetType(SecurityIdentifier))\n\n            Dim writeAllowed = False\n            Dim writeDenied = False\n\n            For Each rule As FileSystemAccessRule In accessRules\n                Dim fileSystemRights = rule.FileSystemRights\n                If (fileSystemRights And FileSystemRights.Write) > 0 Then\n                    Dim ruleSID = DirectCast(rule.IdentityReference, SecurityIdentifier)\n\n                    ' Check if the rule applies to the user or any of the user's groups\n                    If ruleSID.Equals(userSID) OrElse userGroupSIDs.Contains(ruleSID) Then\n                        If rule.AccessControlType = AccessControlType.Allow Then\n                            writeAllowed = True\n                        ElseIf rule.AccessControlType = AccessControlType.Deny Then\n                            writeDenied = True\n                            Exit For\n                        End If\n                    End If\n                End If\n            Next\n\n            ' Write permission is considered available if it's explicitly allowed and not explicitly denied\n            Return writeAllowed And Not writeDenied\n        Catch ex As UnauthorizedAccessException\n\n            Return False\n        End Try\n    End Function\n\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI.CoreVB/CompactGUI.CoreVB.vbproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <RootNamespace>CompactGUI.CoreVB</RootNamespace>\n    <TargetFramework>net9.0-windows</TargetFramework>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|AnyCPU'\">\n    <DebugType>none</DebugType>\n  </PropertyGroup>\n  \n  <ItemGroup>\n    <PackageReference Include=\"EasyCompressor.LZ4\" Version=\"2.1.0\" />\n    <PackageReference Include=\"EasyCompressor.Snappier\" Version=\"2.1.0\" />\n    <PackageReference Include=\"Fody\" Version=\"6.9.2\">\n      <PrivateAssets>all</PrivateAssets>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n    </PackageReference>\n    <PackageReference Include=\"MeasurePerformance.Fody\" Version=\"1.3.1\" />\n    <PackageReference Include=\"System.Management\" Version=\"9.0.5\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "CompactGUI.CoreVB/Compactor.vb",
    "content": "﻿Imports System.IO\nImports System.Runtime.InteropServices\nImports System.Threading\n\nImports Microsoft.Win32.SafeHandles\n\n\nPublic Class Compactor : Implements IDisposable, ICompressor\n\n    Private ReadOnly workingDirectory As String\n    Private ReadOnly excludedFileExtensions() As String\n    Private ReadOnly wofCompressionAlgorithm As WOFCompressionAlgorithm\n\n    Private compressionInfoPtr As IntPtr\n    Private compressionInfoSize As UInteger\n\n    Private totalProcessedBytes As Long = 0\n    Private ReadOnly pauseSemaphore As New SemaphoreSlim(1, 2)\n    Private ReadOnly cancellationTokenSource As New CancellationTokenSource\n\n\n    Public Sub New(folder As String, compressionLevel As WOFCompressionAlgorithm, excludedFilesTypes As String())\n\n        workingDirectory = folder\n        wofCompressionAlgorithm = compressionLevel\n        excludedFileExtensions = excludedFilesTypes\n\n        InitializeCompressionInfoPointer()\n\n    End Sub\n\n\n    Private Sub InitializeCompressionInfoPointer()\n        Dim _EFInfo As New WOF_FILE_COMPRESSION_INFO_V1 With {.Algorithm = wofCompressionAlgorithm, .Flags = 0}\n        compressionInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_EFInfo))\n        compressionInfoSize = CUInt(Marshal.SizeOf(_EFInfo))\n        Marshal.StructureToPtr(_EFInfo, compressionInfoPtr, True)\n    End Sub\n\n    <MeasurePerformance.IL.Weaver.MeasurePerformance>\n    Public Async Function RunAsync(filesList As List(Of String), Optional progressMonitor As IProgress(Of CompressionProgress) = Nothing, Optional MaxParallelism As Integer = 1) As Task(Of Boolean) Implements ICompressor.RunAsync\n\n        If cancellationTokenSource.IsCancellationRequested Then Return False\n\n        Dim workingFiles = Await BuildWorkingFilesList().ConfigureAwait(False)\n        Dim totalFilesSize As Long = workingFiles.Sum(Function(f) f.UncompressedSize)\n        totalProcessedBytes = 0\n\n        If MaxParallelism <= 0 Then MaxParallelism = Environment.ProcessorCount\n\n        Dim paraOptions As New ParallelOptions With {.MaxDegreeOfParallelism = MaxParallelism}\n\n        Try\n            Await Parallel.ForEachAsync(workingFiles, paraOptions,\n            Function(file, _ctx) As ValueTask\n                _ctx.ThrowIfCancellationRequested()\n                Return New ValueTask(PauseAndProcessFile(file, totalFilesSize, cancellationTokenSource.Token, progressMonitor))\n            End Function).ConfigureAwait(False)\n        Catch ex As OperationCanceledException\n            ' Swallow cancellation, return false\n            Return False\n        End Try\n\n        Return Not cancellationTokenSource.IsCancellationRequested\n\n    End Function\n\n    Private Async Function PauseAndProcessFile(details As FileDetails, totalFilesSize As Long, _ctx As CancellationToken, Optional progressMonitor As IProgress(Of CompressionProgress) = Nothing) As Task\n\n        Try\n            Await pauseSemaphore.WaitAsync(_ctx).ConfigureAwait(False)\n            pauseSemaphore.Release()\n        Catch ex As OperationCanceledException\n            Throw\n            Return\n        End Try\n\n        _ctx.ThrowIfCancellationRequested()\n\n        Dim res = WOFCompressFile(details.FileName)\n        Interlocked.Add(totalProcessedBytes, details.UncompressedSize)\n        progressMonitor?.Report(New CompressionProgress(totalProcessedBytes / totalFilesSize * 100, details.FileName))\n\n\n    End Function\n\n\n\n    Public Sub Pause() Implements ICompressor.Pause\n        pauseSemaphore.Wait()\n    End Sub\n\n\n    Public Sub [Resume]() Implements ICompressor.Resume\n        If pauseSemaphore.CurrentCount = 0 Then pauseSemaphore.Release()\n    End Sub\n\n    Public Sub Cancel() Implements ICompressor.Cancel\n        [Resume]()\n        cancellationTokenSource.Cancel()\n    End Sub\n\n\n\n    Private Function WOFCompressFile(path As String) As Integer\n\n        Try\n            Using fs As SafeFileHandle = File.OpenHandle(path)\n                Return WofSetFileDataLocation(fs, WOF_PROVIDER_FILE, compressionInfoPtr, compressionInfoSize)\n            End Using\n        Catch ex As Exception\n            Debug.WriteLine(ex.Message)\n            Return Nothing\n        End Try\n\n    End Function\n\n    Private Async Function BuildWorkingFilesList() As Task(Of IEnumerable(Of FileDetails))\n\n        Dim clusterSize As Integer = GetClusterSize(workingDirectory)\n\n        Dim _filesList As New Concurrent.ConcurrentBag(Of FileDetails)\n        'TODO: if the user has already analysed within the last minute, then skip creating a new one and use the old one\n        Dim ax As New Analyser(workingDirectory)\n        Dim ret = Await ax.AnalyseFolder(Nothing)\n\n        Parallel.ForEach(ax.FileCompressionDetailsList, Sub(fl)\n                                                            Dim ft = fl.FileInfo\n                                                            If Not (excludedFileExtensions.Contains(ft.Extension) OrElse excludedFileExtensions.Contains(fl.FileName)) AndAlso\n                                                                    ft.Length > clusterSize AndAlso\n                                                                    fl.CompressionMode <> wofCompressionAlgorithm Then\n                                                                _filesList.Add((New FileDetails With {.FileName = fl.FileName, .UncompressedSize = fl.UncompressedSize}))\n                                                            End If\n                                                        End Sub)\n\n\n        Return _filesList.ToList\n    End Function\n\n\n    Public Sub Dispose() Implements IDisposable.Dispose\n        cancellationTokenSource?.Dispose()\n        pauseSemaphore?.Dispose()\n        If Not compressionInfoPtr.Equals(IntPtr.Zero) Then\n            Marshal.FreeHGlobal(compressionInfoPtr)\n            compressionInfoPtr = IntPtr.Zero\n        End If\n    End Sub\n\n\n\n    Private Structure FileDetails\n        Public Property FileName As String\n        Public Property UncompressedSize As Long\n\n    End Structure\n\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI.CoreVB/Estimator.vb",
    "content": "﻿Imports System.IO\nImports System.IO.Compression\nImports System.Runtime.InteropServices\n\nImports K4os.Compression.LZ4\n\nImports K4os.Compression.LZ4.Streams\n\nImports Microsoft.Win32.SafeHandles\n\n\n'https://unix.stackexchange.com/questions/155901/estimate-compressibility-of-file\n\nPublic Class Estimator\n    Private Const BlockSize As Integer = 8192 '16K? Should I get it from the ClusterSize?\n    Private Const ZScore As Single = 1.96\n    Private Const ErrorMargin As Single = 0.1\n    Private Const SampleSize As Single = 100 '0.25 * (ZScore / ErrorMargin) ^ 2\n\n    Private DiskClusterSize As Integer = 4096\n    Private IsHDD As Boolean = False\n\n    Public Sub New()\n    End Sub\n\n\n    Public Delegate Function CompressionStreamFactory(output As Stream) As Stream\n\n\n    Public Class FileDetails\n        Public AnalysedFile As AnalysedFileDetails\n        Public CompressionRatio As Single\n    End Class\n\n    Public Shared IsAlternate As Boolean = False\n\n    Public Function EstimateCompressability(analysisResult As List(Of AnalysedFileDetails), ishdd As Boolean, Optional MaxParallelism As Integer = 1, Optional clusterSize As Integer = 4096, Optional cancellationToken As Threading.CancellationToken = Nothing) As List(Of (AnalysedFile As AnalysedFileDetails, CompressionRatio As Single))\n\n        DiskClusterSize = clusterSize\n        Dim _filesList As New Concurrent.ConcurrentBag(Of FileDetails)\n        If MaxParallelism <= 0 Then MaxParallelism = Environment.ProcessorCount\n\n        Me.IsHDD = ishdd\n\n\n        'Filter the files based on the cluster size and sort them by cluster location if it's an HDD to minimize seek time\n        Dim filteredList = analysisResult.Where(Function(f) f.UncompressedSize > clusterSize)\n\n        Dim sw = Stopwatch.StartNew()\n\n        If ishdd Then filteredList = filteredList.OrderBy(Function(f) GetFirstLcn(f.FileName))\n        Dim finalList = filteredList.ToList\n        sw.Stop()\n        Debug.WriteLine($\"Filtered and sorted {finalList.Count} files in {sw.ElapsedMilliseconds} ms\")\n        If IsAlternate Then Debug.WriteLine(\"Alternate mode enabled, using different handle method.\")\n\n\n        Dim paraOptions As New ParallelOptions With {.MaxDegreeOfParallelism = MaxParallelism}\n\n        Parallel.ForEach(finalList, parallelOptions:=paraOptions, Sub(fl)\n                                                                      cancellationToken.ThrowIfCancellationRequested()\n\n                                                                      Dim estimatedRatio = EstimateCompressabilityLZ4(fl.FileName, fl.UncompressedSize, cancellationToken)\n\n                                                                      _filesList.Add((New FileDetails With {.AnalysedFile = fl, .CompressionRatio = estimatedRatio}))\n                                                                  End Sub)\n\n        Dim retList As New List(Of (AnalysedFile As AnalysedFileDetails, CompressionRatio As Single))\n\n        For Each item In _filesList\n            retList.Add((item.AnalysedFile, item.CompressionRatio))\n        Next\n\n        IsAlternate = Not IsAlternate ' Toggle the alternate mode for next run\n\n        Return retList\n\n    End Function\n\n\n    Public Function EstimateCompressabilityGZip(path As String, filesize As Long) As Double\n\n        Using fs As FileStream = File.OpenRead(path)\n            Return EstimateCompressability(fs, filesize, Function(output) New GZipStream(output, CompressionLevel.Fastest, True))\n        End Using\n\n    End Function\n\n\n    Public Function EstimateCompressabilitySnappy(path As String, filesize As Long) As Double\n\n        Using fs As FileStream = File.OpenRead(path)\n            Return EstimateCompressability(fs, filesize, Function(output) New Snappier.SnappyStream(output, Compression.CompressionMode.Compress, True))\n        End Using\n\n    End Function\n\n    Public Function EstimateCompressabilityLZ4(path As String, filesize As Long, Optional cancellationToken As Threading.CancellationToken = Nothing) As Double\n        Try\n            Using fs As FileStream = File.OpenRead(path)\n                If IsHDD Then\n                    Return EstimateCompressabilityHDD(fs, filesize, Function(output) LZ4Stream.Encode(output, LZ4Level.L00_FAST, 0, True), cancellationToken)\n                End If\n                Return EstimateCompressability(fs, filesize, Function(output) LZ4Stream.Encode(output, LZ4Level.L00_FAST, 0, True), cancellationToken)\n            End Using\n        Catch cancelledEx As OperationCanceledException\n            Throw\n        Catch ex As Exception\n            Return 1.0 ' If there's an error, assume it's incompressible\n        End Try\n\n    End Function\n\n\n    Private Function EstimateCompressability(input As FileStream, fileSize As Long, compressionFactory As CompressionStreamFactory, Optional cancellationToken As Threading.CancellationToken = Nothing) As Double\n\n        Dim totalBlocks As Long = fileSize \\ BlockSize\n        Dim samplesNeeded As Integer = Math.Ceiling((totalBlocks * SampleSize) / (SampleSize + totalBlocks - 1))\n        Dim totalWritten As Long = 0\n\n        Dim compressed = New MemoryStream()\n        Using compressionStream As Stream = compressionFactory(compressed)\n\n            ' If file is small or sample count is zero, compress the whole file\n            If samplesNeeded = 0 OrElse fileSize < samplesNeeded * BlockSize * 2 Then\n\n                Dim buffer(BlockSize - 1) As Byte\n                input.Position = 0\n                Dim bytesRead As Integer = input.Read(buffer, 0, BlockSize)\n\n                While bytesRead > 0\n                    cancellationToken.ThrowIfCancellationRequested()\n                    compressionStream.Write(buffer, 0, bytesRead)\n                    totalWritten += bytesRead\n                    bytesRead = input.Read(buffer, 0, BlockSize)\n                End While\n\n            Else\n                ' Sample evenly spaced blocks\n                Dim stepSize As Long = BlockSize * (totalBlocks \\ samplesNeeded)\n                Dim buffer(BlockSize - 1) As Byte\n                For i As Integer = 0 To samplesNeeded - 1\n                    cancellationToken.ThrowIfCancellationRequested()\n                    input.Position = stepSize * i\n                    Dim bytesRead As Integer = input.Read(buffer, 0, BlockSize)\n                    compressionStream.Write(buffer, 0, bytesRead)\n                    totalWritten += bytesRead\n                Next\n            End If\n        End Using\n\n        Return Math.Min(compressed.Length / Math.Max(totalWritten, 1), 1.0)\n    End Function\n\n\n    Private Function EstimateCompressabilityHDD(input As FileStream, fileSize As Long, compressionFactory As CompressionStreamFactory, Optional cancellationToken As Threading.CancellationToken = Nothing) As Double\n        Dim NumClusters As Integer = SampleSize ' or any small number you want to sample\n        Dim clusterSize As Integer = DiskClusterSize\n\n        Dim middleCluster As Long = (fileSize \\ 2) \\ clusterSize\n        Dim alignedStart As Long = middleCluster * clusterSize\n        Dim chunkSize As Integer = CInt(Math.Min(clusterSize * NumClusters, fileSize - alignedStart))\n\n        Dim totalWritten As Long = 0\n        Dim compressed = New MemoryStream()\n\n        Using compressionStream As Stream = compressionFactory(compressed)\n            Dim buffer(chunkSize - 1) As Byte\n            input.Position = alignedStart\n            Dim bytesRead As Integer = input.Read(buffer, 0, chunkSize)\n\n            If cancellationToken <> Nothing AndAlso cancellationToken.IsCancellationRequested Then\n                Throw New OperationCanceledException(cancellationToken)\n            End If\n\n            If bytesRead > 0 Then\n                compressionStream.Write(buffer, 0, bytesRead)\n                totalWritten += bytesRead\n            End If\n        End Using\n\n        Return Math.Min(compressed.Length / Math.Max(totalWritten, 1), 1.0)\n    End Function\n\n    Public Function GetFirstLcn(filePath As String) As Long\n\n        Dim handle As SafeFileHandle = File.OpenHandle(filePath)\n\n        If handle.IsInvalid Then Throw New IOException(\"Failed to open file handle.\")\n\n\n        Dim inBuffer As NtfsInterop.STARTING_VCN_INPUT_BUFFER\n        inBuffer.StartingVcn = 0\n        Dim inBufferSize = Marshal.SizeOf(inBuffer)\n        Dim inBufferPtr = Marshal.AllocHGlobal(inBufferSize)\n        Marshal.StructureToPtr(inBuffer, inBufferPtr, False)\n\n        Dim outBufferSize = 4096\n        Dim outBufferPtr = Marshal.AllocHGlobal(outBufferSize)\n        Dim bytesReturned As Integer = 0\n\n        Try\n            Dim result = WOFHelper.DeviceIoControl(\n                handle,\n                NtfsInterop.FSCTL_GET_RETRIEVAL_POINTERS,\n                inBufferPtr,\n                inBufferSize,\n                outBufferPtr,\n                outBufferSize,\n                bytesReturned,\n                IntPtr.Zero\n            )\n\n\n\n            'Probably errors because the file is empty\n            If Not result Then Return Long.MaxValue\n\n\n            ' Marshal the output buffer to get the first LCN\n            Dim extentOffset As Integer = Marshal.OffsetOf(Of RETRIEVAL_POINTERS_BUFFER)(\"Extents\").ToInt32()\n            Dim lcn As Long = Marshal.ReadInt64(outBufferPtr, extentOffset + 8)\n            Return lcn\n\n        Finally\n            Marshal.FreeHGlobal(inBufferPtr)\n            Marshal.FreeHGlobal(outBufferPtr)\n            handle.Close()\n        End Try\n    End Function\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI.CoreVB/FodyWeavers.xml",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Weavers xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"FodyWeavers.xsd\">\n  <MeasurePerformance />\n</Weavers>"
  },
  {
    "path": "CompactGUI.CoreVB/ICompressor.vb",
    "content": "﻿Public Interface ICompressor : Inherits IDisposable\n    Function RunAsync(filesList As List(Of String), Optional progressMonitor As IProgress(Of CompressionProgress) = Nothing, Optional maxParallelism As Integer = 1) As Task(Of Boolean)\n    Sub Pause()\n    Sub [Resume]()\n    Sub Cancel()\n\nEnd Interface\n"
  },
  {
    "path": "CompactGUI.CoreVB/NtfsInterop.vb",
    "content": "﻿Imports System.Runtime.InteropServices\n\nImports Microsoft.Win32.SafeHandles\n\nFriend Module NtfsInterop\n    Public Const FSCTL_GET_RETRIEVAL_POINTERS As UInteger = &H90073\n    Public Const OPEN_EXISTING As Integer = 3\n    Public Const FILE_FLAG_BACKUP_SEMANTICS As Integer = &H2000000\n    Public Const FILE_SHARE_READ As Integer = 1\n    Public Const FILE_SHARE_WRITE As Integer = 2\n    Public Const GENERIC_READ As Integer = &H80000000\n\n    <DllImport(\"kernel32.dll\", SetLastError:=True, CharSet:=CharSet.Unicode)>\n    Public Function CreateFile(\n        lpFileName As String,\n        dwDesiredAccess As Integer,\n        dwShareMode As Integer,\n        lpSecurityAttributes As IntPtr,\n        dwCreationDisposition As Integer,\n        dwFlagsAndAttributes As Integer,\n        hTemplateFile As IntPtr\n    ) As SafeFileHandle\n    End Function\n\n    <StructLayout(LayoutKind.Sequential)>\n    Public Structure STARTING_VCN_INPUT_BUFFER\n        Public StartingVcn As Long\n    End Structure\n\n    <StructLayout(LayoutKind.Sequential)>\n    Public Structure RETRIEVAL_POINTERS_BUFFER\n        Public ExtentCount As Integer\n        Public StartingVcn As Long\n        Public Extents As LCN_EXTENT ' This is actually an array, but we only need the first. ?Perhaps I should get the LCN of the middle cluster in future?\n    End Structure\n\n    <StructLayout(LayoutKind.Sequential)>\n    Public Structure LCN_EXTENT\n        Public NextVcn As Long\n        Public Lcn As Long\n    End Structure\nEnd Module"
  },
  {
    "path": "CompactGUI.CoreVB/SharedMethods.vb",
    "content": "﻿Imports System.IO\nImports System.Runtime.CompilerServices\nImports System.Runtime.InteropServices\nImports System.Text\nPublic Module SharedMethods\n    Public Enum FolderVerificationResult\n        Valid = 0\n        DirectoryDoesNotExist\n        SystemDirectory\n        RootDirectory\n        DirectoryEmptyOrUnauthorized\n        OneDriveFolder\n        NonNTFSDrive\n        InsufficientPermission\n    End Enum\n\n    Function verifyFolder(folder As String) As FolderVerificationResult\n\n        If Not Directory.Exists(folder) Then : Return FolderVerificationResult.DirectoryDoesNotExist\n        ElseIf folder.ToLowerInvariant.Contains((Environment.GetFolderPath(Environment.SpecialFolder.Windows)).ToLowerInvariant) Then : Return FolderVerificationResult.SystemDirectory\n        ElseIf folder.EndsWith(\":\\\") Then : Return FolderVerificationResult.RootDirectory\n        ElseIf IsDirectoryEmptySafe(folder) Then : Return FolderVerificationResult.DirectoryEmptyOrUnauthorized\n        ElseIf IsOneDriveFolder(folder) Then : Return FolderVerificationResult.OneDriveFolder\n        ElseIf DriveInfo.GetDrives().First(Function(f) folder.StartsWith(f.Name)).DriveFormat <> \"NTFS\" Then : Return FolderVerificationResult.NonNTFSDrive\n        ElseIf Not Analyser.HasDirectoryWritePermission(folder) Then : Return FolderVerificationResult.InsufficientPermission\n        End If\n\n        Return FolderVerificationResult.Valid\n\n    End Function\n\n\n    Function GetFolderVerificationMessage(result As FolderVerificationResult) As String\n        Select Case result\n            Case FolderVerificationResult.Valid\n                Return \"\"\n            Case FolderVerificationResult.DirectoryDoesNotExist\n                Return \"Directory does not exist\"\n            Case FolderVerificationResult.SystemDirectory\n                Return \"Cannot compress system directory\"\n            Case FolderVerificationResult.RootDirectory\n                Return \"Cannot compress root directory\"\n            Case FolderVerificationResult.DirectoryEmptyOrUnauthorized\n                Return \"This directory is either empty or you are not authorized to access its files.\"\n            Case FolderVerificationResult.OneDriveFolder\n                Return \"Files synced with OneDrive cannot be compressed as they use a different storage structure\"\n            Case FolderVerificationResult.NonNTFSDrive\n                Return \"Cannot compress a directory on a non-NTFS drive\"\n            Case FolderVerificationResult.InsufficientPermission\n                Return \"Insufficient permission to access this folder.\"\n            Case Else\n                Return \"Unknown error\"\n        End Select\n    End Function\n\n\n\n    Function IsDirectoryEmptySafe(folder As String)\n\n        Try\n            Return Not Directory.EnumerateFileSystemEntries(folder).Any()\n\n\n        Catch ex As UnauthorizedAccessException\n            Return False\n\n        Catch ex As Exception\n            Return False\n        End Try\n\n    End Function\n\n    Function GetFileSizeOnDisk(file As String) As Long\n        Dim hosize As UInteger\n        Dim losize As UInteger = GetCompressedFileSizeW(file, hosize)\n        'INVALID_FILE_SIZE (0xFFFFFFFF)\n        If losize = 4294967295 Then\n            Dim errCode As Integer\n            errCode = Marshal.GetLastPInvokeError()\n            If errCode <> 0 Then\n                Return -1\n            End If\n        End If\n        Return CLng(hosize) << 32 Or losize\n    End Function\n\n\n    <Extension()>\n    Function AsShortPathNames(filesList As IEnumerable(Of String)) As List(Of String)\n\n        Return filesList.Select(Of String) _\n            (Function(fl)\n                 If fl.Length >= 255 Then\n                     Dim sfp = GetShortPath(fl)\n                     If sfp IsNot Nothing Then Return sfp\n                 End If\n                 Return fl\n             End Function).ToList\n\n    End Function\n\n\n    Function GetShortPath(filePath As String) As String\n\n        If String.IsNullOrWhiteSpace(filePath) Then Return Nothing\n        Dim hasPrefix As Boolean = False\n\n        If filePath.Length >= 255 AndAlso Not filePath.StartsWith(\"\\\\?\\\") Then\n            filePath = \"\\\\?\\\" & filePath\n            hasPrefix = True\n        End If\n\n        Dim shortPath = New StringBuilder(1024)\n        Dim res As Integer = GetShortPathName(filePath, shortPath, 1024)\n        If res = 0 Then Return Nothing\n        filePath = shortPath.ToString()\n        If hasPrefix Then filePath = filePath.Substring(4)\n        Return filePath\n\n    End Function\n\n\n    Function GetClusterSize(folderPath As String)\n\n        Dim lpSectorsPerCluster As UInteger\n        Dim lpBytesPerSector As UInteger\n        Dim res As Integer = GetDiskFreeSpace(New DirectoryInfo(folderPath).Root.ToString, lpSectorsPerCluster, lpBytesPerSector, Nothing, Nothing)\n        Return lpSectorsPerCluster * lpBytesPerSector\n\n    End Function\n\n    Function IsOneDriveFolder(folderPath As String) As Boolean\n        Dim userProfile As String = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)\n        Dim oneDrivePaths As New List(Of String) From {\n            Path.Combine(userProfile, \"OneDrive\"), ' Personal OneDrive\n            Path.Combine(userProfile, \"OneDrive - Personal\"), ' Alternative Personal OneDrive\n            Path.Combine(userProfile, \"OneDrive for Business\"), ' OneDrive for Business\n            Path.Combine(userProfile, \"OneDrive - Business\") ' Alternative OneDrive for Business\n        }\n\n        ' Normalize the folder path to compare\n        Dim normalizedFolderPath As String = Path.GetFullPath(folderPath).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()\n\n        ' Check if the folder path starts with any of the known OneDrive paths\n        Return oneDrivePaths.Any(Function(odPath) normalizedFolderPath.StartsWith(Path.GetFullPath(odPath).TrimEnd(Path.DirectorySeparatorChar).ToLowerInvariant()))\n    End Function\n\n    Public Sub PreventSleep()\n        SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS Or EXECUTION_STATE.ES_SYSTEM_REQUIRED Or EXECUTION_STATE.ES_DISPLAY_REQUIRED)\n    End Sub\n    Public Sub RestoreSleep()\n        SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS)\n    End Sub\n\n\n#Region \"DLL Imports\"\n\n    <DllImport(\"kernel32.dll\", SetLastError:=True)>\n    Private Function GetCompressedFileSizeW(\n    <[In](), MarshalAs(UnmanagedType.LPWStr)> lpFileName As String,\n    <Out(), MarshalAs(UnmanagedType.U4)> ByRef lpFileSizeHigh As UInteger) _\n    As UInteger\n    End Function\n\n    <DllImport(\"kernel32.dll\", CharSet:=CharSet.Auto)>\n    Private Function GetShortPathName(\n        <MarshalAs(UnmanagedType.LPTStr)> path As String,\n        <MarshalAs(UnmanagedType.LPTStr)> shortPath As StringBuilder, shortPathLength As Integer) As Integer\n\n    End Function\n\n\n\n    <DllImport(\"kernel32.dll\", SetLastError:=True, CharSet:=CharSet.Auto)>\n    Private Function GetDiskFreeSpace(\n        lpRootPathName As String,\n        <Out> ByRef lpSectorsPerCluster As UInteger,\n        <Out> ByRef lpBytesPerSector As UInteger,\n        <Out> ByRef lpNumberOfFreeClusters As UInteger,\n        <Out> ByRef lpTotalNumberOfClusters As UInteger) As Boolean\n    End Function\n\n\n    <DllImport(\"kernel32.dll\", SetLastError:=True)>\n    Private Function SetThreadExecutionState(esFlags As EXECUTION_STATE) As EXECUTION_STATE\n    End Function\n\n    <Flags()>\n    Private Enum EXECUTION_STATE As UInteger\n        ES_AWAYMODE_REQUIRED = &H40\n        ES_CONTINUOUS = &H80000000UI\n        ES_DISPLAY_REQUIRED = &H2\n        ES_SYSTEM_REQUIRED = &H1\n    End Enum\n\n#End Region\n\n\nEnd Module\n"
  },
  {
    "path": "CompactGUI.CoreVB/SharedObjects.vb",
    "content": "﻿Public Class AnalysedFileDetails\n\n    Public Property FileName As String\n    Public Property UncompressedSize As Long\n    Public Property CompressedSize As Long\n    Public Property CompressionMode As WOFCompressionAlgorithm\n    Public Property FileInfo As IO.FileInfo\nEnd Class\n\n\n'Used to track efficiency of compression and built results for submission to wiki\nPublic Class ExtensionResult\n\n    Public Property extension As String\n    Public Property uncompressedBytes As Long\n    Public Property compressedBytes As Long\n    Public Property totalFiles As Integer\n    ReadOnly Property cRatio As Decimal\n        Get\n            Return Math.Round(compressedBytes / uncompressedBytes, 2)\n        End Get\n    End Property\n\nEnd Class\n\nPublic Structure CompressionProgress\n    Public ProgressPercent As Integer\n    Public FileName As String\n\n    Public Sub New(_progressPercent As Integer, _fileName As String)\n        ProgressPercent = _progressPercent\n        FileName = _fileName\n    End Sub\n\nEnd Structure\n\n\n\nPublic Enum CompressionMode\n    XPRESS4K\n    XPRESS8K\n    XPRESS16K\n    LZX\n    None\nEnd Enum\n\nPublic Enum WOFCompressionAlgorithm\n    NO_COMPRESSION = -2\n    LZNT1 = -1\n    XPRESS4K = 0\n    LZX = 1\n    XPRESS8K = 2\n    XPRESS16K = 3\nEnd Enum"
  },
  {
    "path": "CompactGUI.CoreVB/Uncompactor.vb",
    "content": "﻿Imports System.IO\nImports System.Threading\n\nImports Microsoft.Win32.SafeHandles\n\nPublic Class Uncompactor : Implements ICompressor, IDisposable\n\n\n    Private _pauseSemaphore As New SemaphoreSlim(1, 2)\n    Private _processedFileCount As New Concurrent.ConcurrentDictionary(Of String, Integer)\n    Private _cancellationTokenSource As New CancellationTokenSource\n\n\n    Public Async Function RunAsync(filesList As List(Of String), Optional progressMonitor As IProgress(Of CompressionProgress) = Nothing, Optional MaxParallelism As Integer = 1) As Task(Of Boolean) Implements ICompressor.RunAsync\n\n        Dim totalFiles As Integer = filesList.Count\n        If MaxParallelism <= 0 Then MaxParallelism = Environment.ProcessorCount\n        Dim paraOptions As New ParallelOptions With {.MaxDegreeOfParallelism = MaxParallelism}\n\n        _processedFileCount.Clear()\n        Try\n            Await Parallel.ForEachAsync(filesList, paraOptions,\n                                  Function(file, _ctx) As ValueTask\n                                      _ctx.ThrowIfCancellationRequested()\n                                      Return New ValueTask(PauseAndProcessFile(file, totalFiles, _cancellationTokenSource.Token, progressMonitor))\n                                  End Function).ConfigureAwait(False)\n        Catch ex As OperationCanceledException\n            Return False\n        End Try\n\n        Return True\n\n    End Function\n\n    Private Async Function PauseAndProcessFile(file As String, totalFiles As Integer, _ctx As CancellationToken, progressMonitor As IProgress(Of CompressionProgress)) As Task\n\n        Try\n            Await _pauseSemaphore.WaitAsync(_ctx).ConfigureAwait(False)\n            _pauseSemaphore.Release()\n        Catch ex As OperationCanceledException\n            Throw\n            Return\n        End Try\n\n        _ctx.ThrowIfCancellationRequested()\n\n        Dim res = WOFDecompressFile(file)\n        _processedFileCount.TryAdd(file, 1)\n\n        progressMonitor?.Report(New CompressionProgress((CInt(((_processedFileCount.Count / totalFiles) * 100))), file))\n\n    End Function\n\n    Private Function WOFDecompressFile(path As String)\n\n        Try\n            Using fs As SafeFileHandle = File.OpenHandle(path)\n                Dim res = WOFHelper.DeviceIoControl(fs, FSCTL_DELETE_EXTERNAL_BACKING, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero, IntPtr.Zero)\n                Return res\n            End Using\n        Catch ex As Exception\n            Debug.WriteLine(ex.Message)\n            Return Nothing\n        End Try\n\n    End Function\n\n\n    Public Sub Pause() Implements ICompressor.Pause\n        _pauseSemaphore.Wait()\n    End Sub\n\n    Public Sub [Resume]() Implements ICompressor.Resume\n        If _pauseSemaphore.CurrentCount = 0 Then _pauseSemaphore.Release()\n    End Sub\n    Public Sub Cancel() Implements ICompressor.Cancel\n        [Resume]()\n        _cancellationTokenSource.Cancel()\n    End Sub\n\n    Public Sub Dispose() Implements IDisposable.Dispose\n        _pauseSemaphore?.Dispose()\n        _cancellationTokenSource?.Dispose()\n    End Sub\nEnd Class\n"
  },
  {
    "path": "CompactGUI.CoreVB/WOFHelper.vb",
    "content": "﻿Imports System.Runtime.InteropServices\n\nImports Microsoft.Win32.SafeHandles\n\nPublic Module WOFHelper\n\n    Public Const WOF_PROVIDER_FILE As ULong = 2\n    Public Const FSCTL_DELETE_EXTERNAL_BACKING As UInteger = &H90314\n\n    Public Function WOFConvertCompressionLevel(compressionlevel As Integer) As Integer\n\n        Select Case compressionlevel\n            Case 0 : Return WOFCompressionAlgorithm.XPRESS4K\n            Case 1 : Return WOFCompressionAlgorithm.XPRESS8K\n            Case 2 : Return WOFCompressionAlgorithm.XPRESS16K\n            Case 3 : Return WOFCompressionAlgorithm.LZX\n            Case Else : Return WOFCompressionAlgorithm.XPRESS4K\n        End Select\n\n    End Function\n\n    Public Function WOFConvertCompressionLevel(compressionlevel As CompressionMode) As Integer\n        Select Case compressionlevel\n            Case CompressionMode.XPRESS4K : Return WOFCompressionAlgorithm.XPRESS4K\n            Case CompressionMode.XPRESS8K : Return WOFCompressionAlgorithm.XPRESS8K\n            Case CompressionMode.XPRESS16K : Return WOFCompressionAlgorithm.XPRESS16K\n            Case CompressionMode.LZX : Return WOFCompressionAlgorithm.LZX\n            Case Else : Return WOFCompressionAlgorithm.XPRESS4K\n        End Select\n    End Function\n\n\n    Public Function WOFConvertBackCompressionLevel(WOFCompressionLevel As WOFCompressionAlgorithm) As Integer\n\n        Select Case WOFCompressionLevel\n            Case WOFCompressionAlgorithm.XPRESS4K : Return 0\n            Case WOFCompressionAlgorithm.XPRESS8K : Return 1\n            Case WOFCompressionAlgorithm.XPRESS16K : Return 2\n            Case WOFCompressionAlgorithm.LZX : Return 3\n            Case Else : Return 0\n        End Select\n\n    End Function\n\n    <StructLayout(LayoutKind.Sequential)>\n    Public Structure WOF_FILE_COMPRESSION_INFO_V1\n        Public Algorithm As UInteger\n        Public Flags As UInteger\n    End Structure\n\n\n    <DllImport(\"WofUtil.dll\")>\n    Friend Function WofIsExternalFile(\n    <MarshalAs(UnmanagedType.LPWStr)> Filepath As String,\n    <Out> ByRef IsExternalFile As Integer,\n    <Out> ByRef Provider As UInteger,\n    <Out> ByRef Info As WOF_FILE_COMPRESSION_INFO_V1,\n    ByRef BufferLength As UInteger) As Integer\n    End Function\n\n    <DllImport(\"WofUtil.dll\")>\n    Friend Function WofSetFileDataLocation(\n        FileHandle As SafeFileHandle,\n        Provider As ULong,\n        ExternalFileInfo As IntPtr,\n        Length As ULong) As Integer\n    End Function\n\n    'Most of these should be optional if MS Docs are to be believed -.-\n    <DllImport(\"kernel32.dll\")>\n    Friend Function DeviceIoControl(\n        hDevice As SafeFileHandle,\n        dwIoControlCode As UInteger,\n        lpInBuffer As IntPtr,\n        nInBufferSize As UInteger,\n        lpOutBuffer As IntPtr,\n        nOutBufferSize As UInteger,\n        <Out> ByRef lpBytesReturned As IntPtr,\n        <Out> lpOverlapped As IntPtr) As Boolean\n\n    End Function\n\nEnd Module\n"
  },
  {
    "path": "CompactGUI.Logging/CompactGUI.Logging.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <TargetFramework>net9.0</TargetFramework>\n    <ImplicitUsings>enable</ImplicitUsings>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n\n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|AnyCPU'\"> \n    <DebugType>none</DebugType>\n  </PropertyGroup>\n\n  <ItemGroup>\n    <PackageReference Include=\"Microsoft.Extensions.Logging\" Version=\"9.0.5\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "CompactGUI.Logging/Core/AnalyserLog.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace CompactGUI.Logging.Core;\n\npublic static partial class AnalyserLog\n{\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Starting analysis of directory: {Directory}\")]\n    public static partial void StartingAnalysis(ILogger logger, string directory);\n\n    [LoggerMessage(Level = LogLevel.Trace, Message = \"Processing file: {FileName}\")]\n    public static partial void ProcessingFile(ILogger logger, string fileName);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Processing file failed: {FileName} with message: {Message}\")]\n    public static partial void ProcessingFileFailed(ILogger logger, string fileName, string message);\n\n\n    [LoggerMessage(Level = LogLevel.Trace, Message = \"Processing folder: {FolderName}\")]\n    public static partial void ProcessingFolder(ILogger logger, string folderName);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Analysis failed for: {Path} with error: {ErrorMessage}\")]\n    public static partial void AnalysisFailed(ILogger logger, string path, string errorMessage);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Analysis completed for directory: {Directory} in {TimeTaken}s. Uncompressed Size: {UncompressedBytes}b, CompressedSize: {CompressedBytes}b, ContainsCompressedFiles: {ContainsCompressedFiles}\")]\n    public static partial void AnalysisCompleted(ILogger logger, string directory, double timeTaken, long compressedBytes, long uncompressedBytes, bool containsCompressedFiles);\n}\n"
  },
  {
    "path": "CompactGUI.Logging/Core/CompactorLog.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace CompactGUI.Logging.Core;\n\npublic static partial class CompactorLog\n{\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Starting compression in directory: {Directory} with algorithm: {Algorithm} and using {MaxParallelism} threads\")]\n    public static partial void StartingCompression(ILogger logger, string directory, string algorithm, int maxParallelism);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Building working files list for directory: {Directory}\")]\n    public static partial void BuildingWorkingFilesList(ILogger logger, string directory);\n\n    [LoggerMessage(Level = LogLevel.Trace, Message = \"Processing file: {FileName} ({UncompressedSize} bytes)\")]\n    public static partial void ProcessingFile(ILogger logger, string fileName, long uncompressedSize);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"File compression failed for: {FileName} with error: {ErrorMessage}\")]\n    public static partial void FileCompressionFailed(ILogger logger, string fileName, string errorMessage);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Compression paused.\")]\n    public static partial void CompressionPaused(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Compression resumed.\")]\n    public static partial void CompressionResumed(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Compression canceled.\")]\n    public static partial void CompressionCanceled(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Compression completed successfully in {TimeTaken}s.\")]\n    public static partial void CompressionCompleted(ILogger logger, double timeTaken);\n\n    [LoggerMessage(Level = LogLevel.Error, Message = \"Compression failed with error: {ErrorMessage}\")]\n    public static partial void CompressionFailed(ILogger logger, string errorMessage);\n\n}\n"
  },
  {
    "path": "CompactGUI.Logging/Core/UncompactorLog.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace CompactGUI.Logging.Core;\n\npublic static partial class UncompactorLog\n{\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Starting decompression of {FileCount} files with {MaxParallelism} threads\")]\n    public static partial void StartingDecompression(ILogger logger, int fileCount, int maxParallelism);\n\n    [LoggerMessage(Level = LogLevel.Trace, Message = \"Processing file: {FileName}\")]\n    public static partial void ProcessingFile(ILogger logger, string fileName);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"File decompression failed for: {FileName} with error: {ErrorMessage}\")]\n    public static partial void FileDecompressionFailed(ILogger logger, string fileName, string errorMessage);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Decompression paused.\")]\n    public static partial void DecompressionPaused(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Decompression resumed.\")]\n    public static partial void DecompressionResumed(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Decompression canceled.\")]\n    public static partial void DecompressionCanceled(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Decompression completed successfully in {TimeTaken}s.\")]\n    public static partial void DecompressionCompleted(ILogger logger, double timeTaken);\n}\n"
  },
  {
    "path": "CompactGUI.Logging/HomeViewModelLog.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\n\nnamespace CompactGUI.Logging;\n\npublic static partial class HomeViewModelLog\n{\n\n    [LoggerMessage(Level = LogLevel.Information,Message = \"Loading folders from {FolderPaths}\")]\n    public static partial void AddingFolders(ILogger logger, IEnumerable<string> folderPaths);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Invalid folders found: {InvalidFolders} with messages {messages}\")]\n    public static partial void InvalidFolders(ILogger logger, List<string> invalidFolders, IEnumerable<String> messages);\n\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Getting estimated compression for {FolderPath} with uncompressed size {uncompressedSize}.\")]\n    public static partial void GettingEstimatedCompression(ILogger logger, string folderPath, long uncompressedSize);\n\n\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Compressing folder {folderName}.\")]\n    public static partial void CompressingFolder(ILogger logger, string folderName);\n\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Adding folder {folderName} to watcher\")]\n    public static partial void AddingFolderToWatcher(ILogger logger, string folderName);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Starting batch compression of {count} folders\")]\n    public static partial void StartingBatchCompression(ILogger logger, int count);\n\n\n}\n"
  },
  {
    "path": "CompactGUI.Logging/SchedulerServiceLog.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace CompactGUI.Logging\n{\n    public static partial class SchedulerServiceLog\n    {\n\n        [LoggerMessage(Level = LogLevel.Debug, Message = \"Checking if scheduler should run...\")]\n        public static partial void CheckingSchedulerRunnable(ILogger logger);\n\n        [LoggerMessage(Level = LogLevel.Information, Message = \"Scheduler not running: background watcher is disabled.\")]\n        public static partial void SchedulerDisabled(ILogger logger);\n\n        [LoggerMessage(Level = LogLevel.Information, Message = \"Scheduler not running: next scheduled run is in the future. Next run: {NextRun}\")]\n        public static partial void SchedulerNextRunInFuture(ILogger logger, DateTime nextRun);\n\n        [LoggerMessage(Level = LogLevel.Information, Message = \"Scheduler running: system is idle.\")]\n        public static partial void SchedulerRunningIdle(ILogger logger);\n\n        [LoggerMessage(Level = LogLevel.Information, Message = \"Scheduler not running: system is not idle.\")]\n        public static partial void SchedulerNotIdle(ILogger logger);\n\n        [LoggerMessage(Level = LogLevel.Information, Message = \"Scheduler running: scheduled mode, idle not required.\")]\n        public static partial void SchedulerRunningScheduled(ILogger logger);\n\n        [LoggerMessage(Level = LogLevel.Information, Message = \"Scheduler not running: scheduler is disabled.\")]\n        public static partial void SchedulerModeDisabled(ILogger logger);\n\n        [LoggerMessage(Level = LogLevel.Error, Message = \"Scheduler error: {ExceptionMessage}\")]\n        public static partial void SchedulerError(ILogger logger, string exceptionMessage, Exception ex);\n\n    }\n}\n"
  },
  {
    "path": "CompactGUI.Logging/SettingsLog.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\nusing Microsoft.Extensions.Logging;\n\nnamespace CompactGUI.Logging;\n\npublic static partial class SettingsLog\n{\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Adding to Context Menus\")]\n    public static partial void AddingToContextMenus(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Adding to Context Menus: Success\")]\n    public static partial void AddingToContextMenusSuccess(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Adding to Context Menus: Failed\")]\n    public static partial void AddingToContextMenusFailed(ILogger logger, Exception ex);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Removing from Context Menus\")]\n    public static partial void RemovingFromContextMenus(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Removing from Context Menus: Success\")]\n    public static partial void RemovingFromContextMenusSuccess(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Removing from Context Menus: Failed\")]\n    public static partial void RemovingFromContextMenusFailed(ILogger logger, Exception ex);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Adding Start Menu Shortcut\")]\n    public static partial void AddingStartMenuShortcut(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Removing Start Menu Shortcut\")]\n    public static partial void RemovingStartMenuShortcut(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Settings Saved\")]\n    public static partial void SettingsSaved(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Setting Environment Variables\")]\n    public static partial void SettingEnvironmentVariables(ILogger logger);\n\n\n}\n\n"
  },
  {
    "path": "CompactGUI.Logging/SnackbarServiceLog.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\n\nnamespace CompactGUI.Logging;\n\npublic static partial class SnackbarServiceLog\n{\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Invalid Folder {FolderName}: {message}\")]\n    public static partial void ShowInvalidFoldersMessage(ILogger logger, string FolderName, string message);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Showing insufficient permission snackbar for folder: {FolderName}\")]\n    public static partial void ShowInsufficientPermission(ILogger logger, string folderName);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Showing update available snackbar for version: {NewVersion}, pre-release: {IsPreRelease}\")]\n    public static partial void ShowUpdateAvailable(ILogger logger, string newVersion, bool isPreRelease);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Showing failed to submit to wiki snackbar.\")]\n    public static partial void ShowFailedToSubmitToWiki(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Showing submitted to wiki snackbar for UID: {UID}, Game: {GameName}, SteamID: {SteamID}, Compression: {CompressionMode}\")]\n    public static partial void ShowSubmittedToWiki(ILogger logger, string UID, string GameName, string SteamID, string CompressionMode);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Showing applied to all folders snackbar.\")]\n    public static partial void ShowAppliedToAllFolders(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Showing cannot remove folder snackbar.\")]\n    public static partial void ShowCannotRemoveFolder(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Showing added to queue snackbar.\")]\n    public static partial void ShowAddedToQueue(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Showing DirectStorage warning for: {DisplayName}\")]\n    public static partial void ShowDirectStorageWarning(ILogger logger, string displayName);\n\n}\n"
  },
  {
    "path": "CompactGUI.Logging/Watcher/WatcherLog.cs",
    "content": "﻿using Microsoft.Extensions.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nnamespace CompactGUI.Logging.Watcher;\n\npublic static partial class WatcherLog\n{\n    // Watcher events\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher started.\")]\n    public static partial void WatcherStarted(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Backgrounding disabled.\")]\n    public static partial void BackgroundingDisabled(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Backgrounding enabled.\")]\n    public static partial void BackgroundingEnabled(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Initializing watched folders.\")]\n    public static partial void InitializingWatchedFolders(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Parsing watchers (ParseAll={ParseAll}).\")]\n    public static partial void ParsingWatchers(ILogger logger, bool parseAll);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Parsing single watcher: {Folder}.\")]\n    public static partial void ParsingSingleWatcher(ILogger logger, string folder);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Watcher - Removing {Count} folders that do not exist from watcher list.\")]\n    public static partial void RemovingNonexistentFolders(ILogger logger, int count);\n\n    [LoggerMessage(Level = LogLevel.Warning, Message = \"Watcher - Failed to deserialize watcher JSON: {ErrorMessage}\")]\n    public static partial void DeserializeWatcherJsonFailed(ILogger logger, string errorMessage);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - System idle detected.\")]\n    public static partial void SystemIdleDetected(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - System not idle.\")]\n    public static partial void SystemNotIdle(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Debug, Message = \"Watcher - Folder {Folder} changed and will be compressed\")]\n    public static partial void FolderChanged(ILogger logger, string folder);\n\n\n\n\n\n\n\n\n\n    // BackgroundCompactor events\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Background compacting started.\")]\n    public static partial void BackgroundCompactingStarted(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Background compacting finished.\")]\n    public static partial void BackgroundCompactingFinished(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Watcher - Compacting folder: {FolderName}.\")]\n    public static partial void CompactingFolder(ILogger logger, string folderName);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Skipping folder (recently modified): {FolderName}.\")]\n    public static partial void SkippingRecentlyModifiedFolder(ILogger logger, string folderName);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Finished compacting folder: {FolderName}.\")]\n    public static partial void FinishedCompactingFolder(ILogger logger, string folderName);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Watcher - Pausing background compactor.\")]\n    public static partial void PausingBackgroundCompactor(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Information, Message = \"Watcher - Resuming background compactor.\")]\n    public static partial void ResumingBackgroundCompactor(ILogger logger);\n\n    [LoggerMessage(Level = LogLevel.Error, Message = \"Watcher - Error during background compacting: {ErrorMessage}\")]\n    public static partial void BackgroundCompactingError(ILogger logger, string errorMessage);\n}\n"
  },
  {
    "path": "CompactGUI.Watcher/BackgroundCompactor.vb",
    "content": "﻿Imports System.Collections.ObjectModel\nImports System.Threading\n\nImports CompactGUI.Logging.Watcher\n\nImports Microsoft.Extensions.Logging\n\nImports Microsoft.Extensions.Logging.Abstractions\n\nPublic Class BackgroundCompactor\n\n    Private _IsCompactorActive As Boolean = False\n    Public Property IsCompactorActive As Boolean\n        Get\n            Return _IsCompactorActive\n        End Get\n        Set(value As Boolean)\n            If _IsCompactorActive = value Then Return\n            _IsCompactorActive = value\n            RaiseEvent IsCompactingEvent(Me, value)\n        End Set\n    End Property\n\n    Private cancellationTokenSource As New CancellationTokenSource()\n    Private isCompacting As Boolean = False\n    Private isCompactingPaused As Boolean = False ' Track if compacting is paused\n\n    Private _compactor As Core.Compactor\n\n    Private _excludedFileTypes As String()\n\n\n    Private ReadOnly _logger As ILogger(Of Watcher)\n\n\n    Public Event IsCompactingEvent As EventHandler(Of Boolean)\n\n    Public Sub New(excludedFileTypes As String(), logger As ILogger(Of Watcher))\n\n        _excludedFileTypes = excludedFileTypes\n        _logger = logger\n    End Sub\n\n\n    Public Function BeginCompacting(folder As String, compressionLevel As Core.WOFCompressionAlgorithm) As Task(Of Boolean)\n\n        If compressionLevel = Core.WOFCompressionAlgorithm.NO_COMPRESSION Then Return Task.FromResult(False)\n\n        _compactor = New Core.Compactor(folder, compressionLevel, _excludedFileTypes, New Core.Analyser(folder, NullLogger(Of Core.Analyser).Instance))\n\n        Return _compactor.RunAsync(Nothing)\n\n    End Function\n\n    Public Async Function StartCompactingAsync(folders As IEnumerable(Of WatchedFolder)) As Task(Of Boolean)\n        WatcherLog.BackgroundCompactingStarted(_logger)\n        cancellationTokenSource = New CancellationTokenSource()\n\n        IsCompactorActive = True\n\n        Dim currentProcess As Process = Process.GetCurrentProcess()\n        currentProcess.PriorityClass = ProcessPriorityClass.Idle\n\n        isCompacting = True\n\n        For Each folder In folders.ToList\n            folder.IsWorking = True\n\n            WatcherLog.CompactingFolder(_logger, folder.DisplayName)\n            Dim compactingTask = BeginCompacting(folder.Folder, folder.CompressionLevel)\n\n\n            If cancellationTokenSource.IsCancellationRequested Then\n                Trace.WriteLine(\"Compacting cancelled by user.\")\n                folder.IsWorking = False\n                IsCompactorActive = False\n                isCompacting = False ' Ensure compacting status is reset after operation\n                _compactor.Dispose()\n                Return False\n            End If\n\n            Dim result = Await compactingTask\n            If result AndAlso folders.Contains(folder) Then\n                ' Ensure the folder is still in the original collection before updating\n\n                Dim analyser As New Core.Analyser(folder.Folder, NullLogger(Of Core.Analyser).Instance)\n\n                Dim analysed = Await analyser.GetAnalysedFilesAsync(Nothing)\n\n                folder.LastCheckedDate = DateTime.Now\n                folder.LastCheckedSize = analyser.CompressedBytes\n                folder.LastCompressedSize = analyser.CompressedBytes\n                folder.LastSystemModifiedDate = DateTime.Now\n                Dim mainCompressionLVL = analysed.Select(Function(f) f.CompressionMode).Max\n                folder.CompressionLevel = mainCompressionLVL\n\n                folder.LastCompressedDate = DateTime.Now\n\n                folder.HasTargetChanged = False\n\n            End If\n            folder.IsWorking = False\n            folder.RefreshProperties()\n            _compactor.Dispose()\n            WatcherLog.FinishedCompactingFolder(_logger, folder.DisplayName)\n        Next\n\n        IsCompactorActive = False\n        isCompacting = False ' Ensure compacting status is reset after operation\n        WatcherLog.BackgroundCompactingFinished(_logger)\n        currentProcess.PriorityClass = ProcessPriorityClass.Normal\n        Return True\n    End Function\n\n    Public Sub PauseCompacting()\n        If Not isCompacting OrElse isCompactingPaused Then\n            Return\n        End If\n\n        WatcherLog.PausingBackgroundCompactor(_logger)\n        isCompactingPaused = True ' Indicate compacting is paused\n        _compactor?.Pause()\n    End Sub\n\n    Public Sub ResumeCompacting()\n        If Not isCompactingPaused OrElse Not isCompacting Then\n            Return\n        End If\n\n        WatcherLog.ResumingBackgroundCompactor(_logger)\n        isCompactingPaused = False ' Indicate compacting is no longer paused\n        _compactor?.Resume()\n    End Sub\n\n    Public Sub CancelCompacting()\n        If Not isCompacting Then\n            Return\n        End If\n        Debug.WriteLine(\"Cancelling background compactor...\")\n        cancellationTokenSource.Cancel()\n        cancellationTokenSource.Dispose()\n        _compactor?.Cancel()\n        _compactor?.Dispose()\n        isCompacting = False\n        isCompactingPaused = False ' Reset pause state on cancellation\n    End Sub\n\nEnd Class\n"
  },
  {
    "path": "CompactGUI.Watcher/CompactGUI.Watcher.vbproj",
    "content": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <RootNamespace>CompactGUI.Watcher</RootNamespace>\n    <TargetFramework>net9.0-windows</TargetFramework>\n  </PropertyGroup>\n  \n  <PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|AnyCPU'\">\n    <DebugType>none</DebugType>\n  </PropertyGroup>\n  \n  <ItemGroup>\n    <PackageReference Include=\"CommunityToolkit.Mvvm\" Version=\"8.4.0\" />\n    <PackageReference Include=\"IridiumIO.MVVM.VBSourceGenerators\" Version=\"0.6.1\" />\n    <PackageReference Include=\"Microsoft.Extensions.Logging\" Version=\"9.0.5\" />\n  </ItemGroup>\n\n  <ItemGroup>\n    <ProjectReference Include=\"..\\CompactGUI.Core\\CompactGUI.Core.csproj\" />\n    <ProjectReference Include=\"..\\CompactGUI.Logging\\CompactGUI.Logging.csproj\" />\n  </ItemGroup>\n\n</Project>\n"
  },
  {
    "path": "CompactGUI.Watcher/IdleDetector.vb",
    "content": "﻿Imports System.Runtime.InteropServices\nImports System.Threading\n\nPublic Enum IdleState\n    Idle\n    NotIdle\nEnd Enum\n\nPublic Class IdleDetector\n\n    Public Event IsIdle As EventHandler\n    Public Event IsNotIdle As EventHandler\n\n    Private ReadOnly _settings As IdleSettings\n\n    Private  _timerTask As Task\n    Private _idletimer As PeriodicTimer\n    Private _cts As CancellationTokenSource\n\n    Public Property State As IdleState\n    Public Property LastIdleTime As DateTime = DateTime.MinValue\n\n\n    Public Sub New(settings As IdleSettings)\n        _settings = settings\n    End Sub\n\n\n    Public Async Sub Start()\n        Await StopAsync()\n        _cts = New CancellationTokenSource()\n        _idletimer = New PeriodicTimer(TimeSpan.FromSeconds(_settings.IdleCheckIntervalSeconds))\n        _timerTask = IdleTimerDoWorkAsync()\n        State = IdleState.NotIdle\n    End Sub\n\n    Public Async Function StopAsync() As Task\n        _cts?.Cancel()\n        _idletimer?.Dispose()\n        _idletimer = Nothing\n        If _timerTask IsNot Nothing Then Await _timerTask\n        _cts?.Dispose()\n        _cts = Nothing\n    End Function\n\n    Private Async Function IdleTimerDoWorkAsync() As Task\n        Try\n            While Await _idletimer.WaitForNextTickAsync(_cts.Token) AndAlso Not _cts.Token.IsCancellationRequested\n\n                If GetIdleTime() > _settings.IdleThresholdSeconds Then\n                    If State <> IdleState.Idle OrElse DateTime.Now.AddSeconds(-_settings.IdleRepeatTimeSeconds) > LastIdleTime Then\n                        State = IdleState.Idle\n                        LastIdleTime = DateTime.Now\n                        RaiseEvent IsIdle(Nothing, EventArgs.Empty)\n                    End If\n\n                Else\n                    If State = IdleState.Idle Then\n                        State = IdleState.NotIdle\n                        RaiseEvent IsNotIdle(Nothing, EventArgs.Empty)\n                    End If\n\n                End If\n\n            End While\n        Catch ex As OperationCanceledException\n            Return\n        End Try\n\n    End Function\n\n    Public Shared Function GetIdleTime() As Double\n        Dim lastInputInfo As New LASTINPUTINFO() With {.cbSize = CType(Marshal.SizeOf(Of LASTINPUTINFO)(), UInteger)}\n        If Not GetLastInputInfo(lastInputInfo) Then Return 0\n\n        Dim idleTicks = Environment.TickCount64 - CLng(lastInputInfo.dwTime)\n        Return TimeSpan.FromMilliseconds(idleTicks).TotalSeconds\n\n    End Function\n\n\n    <DllImport(\"user32.dll\")>\n    Private Shared Function GetLastInputInfo(ByRef plii As LASTINPUTINFO) As Boolean\n    End Function\n\n\n    Public Structure LASTINPUTINFO\n        Public cbSize As UInteger\n        Public dwTime As UInteger\n    End Structure\n\nEnd Class"
  },
  {
    "path": "CompactGUI.Watcher/IdleSettings.vb",
    "content": "﻿Public Class IdleSettings\n    Public Property IdleCheckIntervalSeconds As Integer = 5 ' How often to check for idle state\n    Public Property IdleThresholdSeconds As Integer = 120 ' Minimum seconds of inactivity to be considered idle\n    Public Property IdleRepeatTimeSeconds As Integer = 60 ' How often to repeat firing the idle event after the initial idle state is detected\n    Public Property LastSystemModifiedTimeThresholdSeconds As Integer = 300 ' How long to wait after the last system modification before considering a folder for analysis / compaction\nEnd Class\n"
  },
  {
    "path": "CompactGUI.Watcher/WatchedFolder.vb",
    "content": "﻿Imports System.IO\nImports System.Text.Json.Serialization\nImports System.Threading\n\nImports CommunityToolkit.Mvvm.ComponentModel\n\nPublic Class WatchedFolder\n    Inherits ObservableObject\n    Implements IDisposable\n\n    ' --- Folder Metadata ---\n    <ObservableProperty> Private _Folder As String\n    <ObservableProperty> Private _DisplayName As String\n    <ObservableProperty> Private _IsSteamGame As Boolean\n    <ObservableProperty> Private _LastCompressedDate As DateTime\n    <ObservableProperty> Private _LastCompressedSize As Long\n    <ObservableProperty> Private _LastUncompressedSize As Long\n    <ObservableProperty> Private _LastSystemModifiedDate As DateTime\n    <NotifyPropertyChangedFor(NameOf(DecayPercentage), NameOf(SavedSpace))>\n    <ObservableProperty> Private _LastCheckedDate As DateTime\n    <ObservableProperty> Private _LastCheckedSize As Long\n    <ObservableProperty> Private _CompressionLevel As Core.WOFCompressionAlgorithm\n\n    <AttachAttribute(GetType(JsonIgnoreAttribute))>\n    <ObservableProperty> Private _IsWorking As Boolean\n\n    <AttachAttribute(GetType(JsonIgnoreAttribute))>\n    <ObservableProperty> Private _IsEditing As Boolean = False\n\n    ' --- Monitoring State ---\n    <AttachAttribute(GetType(JsonIgnoreAttribute))>\n    <ObservableProperty> Private _HasTargetChanged As Boolean = False\n\n    <AttachAttribute(GetType(JsonIgnoreAttribute))>\n    <ObservableProperty> Private _LastChangedDate As DateTime\n\n    ' --- FileSystemWatcher ---\n\n    Private WithEvents FSWatcher As FileSystemWatcher\n\n    Private debounceTimer As Timer\n\n    Private disposedValue As Boolean\n\n    Public Sub New(_folder As String, _displayName As String)\n        Folder = _folder\n        DisplayName = _displayName\n\n        InitializeMonitoring()\n    End Sub\n\n    Public Sub New()\n    End Sub\n\n    Public Sub InitializeMonitoring()\n        If FSWatcher Is Nothing Then\n            FSWatcher = New FileSystemWatcher(Folder) With {\n            .NotifyFilter = NotifyFilters.Size Or NotifyFilters.CreationTime Or NotifyFilters.LastWrite Or NotifyFilters.FileName,\n            .IncludeSubdirectories = True,\n            .Filter = \"\",\n            .EnableRaisingEvents = True\n        }\n            debounceTimer = New Timer(AddressOf DebounceTimerCallback, Nothing, Timeout.Infinite, Timeout.Infinite)\n        End If\n    End Sub\n\n    Public Sub PauseMonitoring()\n        If FSWatcher IsNot Nothing Then\n            FSWatcher.EnableRaisingEvents = False\n        End If\n    End Sub\n\n    Public Sub ResumeMonitoring()\n        If FSWatcher IsNot Nothing Then\n            FSWatcher.EnableRaisingEvents = True\n        End If\n    End Sub\n\n    ' --- Monitoring Events ---\n    Private Sub WatcherErrorEvent(sender As Object, e As ErrorEventArgs) Handles FSWatcher.Error\n        Debug.WriteLine(e.GetException.Message)\n    End Sub\n\n    Private Sub WatcherModifiedEvent(sender As Object, e As FileSystemEventArgs) Handles FSWatcher.Created, FSWatcher.Changed, FSWatcher.Renamed, FSWatcher.Deleted\n        debounceTimer.Change(1000, Timeout.Infinite)\n    End Sub\n\n    Private Sub DebounceTimerCallback(state As Object)\n        HasTargetChanged = True\n        LastChangedDate = DateTime.Now\n        OnPropertyChanged(NameOf(HasTargetChanged))\n        OnPropertyChanged(NameOf(LastChangedDate))\n        OnPropertyChanged(NameOf(LastSystemModifiedDate))\n        Debug.WriteLine(\"Folder \" & Folder & \" has changed!\")\n    End Sub\n\n    ' --- Calculated Properties ---\n    Public ReadOnly Property DecayPercentage As Decimal\n        Get\n            If LastCompressedSize = 0 Then Return 1\n            Return If(LastUncompressedSize = LastCompressedSize OrElse LastCompressedSize > LastUncompressedSize, 1D, Math.Clamp((LastCheckedSize - LastCompressedSize) / (LastUncompressedSize - LastCompressedSize), 0, 1))\n        End Get\n    End Property\n\n    <JsonIgnore>\n    Public ReadOnly Property SavedSpace As Long\n        Get\n            Return LastUncompressedSize - LastCheckedSize\n        End Get\n    End Property\n\n    Public Sub RefreshProperties()\n        For Each prop In Me.GetType.GetProperties\n            OnPropertyChanged(prop.Name)\n        Next\n    End Sub\n\n    ' --- IDisposable ---\n    Protected Overridable Sub Dispose(disposing As Boolean)\n        If Not disposedValue Then\n            If disposing Then\n                FSWatcher?.Dispose()\n                debounceTimer?.Dispose()\n            End If\n            disposedValue = True\n        End If\n    End Sub\n\n    Public Sub Dispose() Implements IDisposable.Dispose\n        Dispose(disposing:=True)\n        GC.SuppressFinalize(Me)\n    End Sub\nEnd Class"
  },
  {
    "path": "CompactGUI.Watcher/Watcher.vb",
    "content": "Imports System.Collections.ObjectModel\nImports System.Collections.Specialized\nImports System.Runtime\nImports System.Text.Json\nImports System.Threading\n\nImports CommunityToolkit.Mvvm.ComponentModel\nImports CommunityToolkit.Mvvm.Input\nImports CommunityToolkit.Mvvm.Messaging\nImports CommunityToolkit.Mvvm.Messaging.Messages\n\nImports CompactGUI.Core\nImports CompactGUI.Core.Settings\nImports CompactGUI.Logging.Watcher\n\nImports Microsoft.Extensions.Logging\n\nImports Microsoft.Extensions.Logging.Abstractions\nImports Microsoft.Win32\nImports Microsoft.Win32.Registry\n\n\nPartial Public Class Watcher : Inherits ObservableRecipient : Implements IRecipient(Of PropertyChangedMessage(Of Boolean))\n\n    Private ReadOnly _DataFolder As IO.DirectoryInfo\n    Private ReadOnly _parseWatchersSemaphore As New SemaphoreSlim(1, 1)\n\n    Private ReadOnly _logger As ILogger(Of Watcher)\n    Private ReadOnly _settingsService As ISettingsService\n    Private ReadOnly _idleDetector As IdleDetector\n\n    <NotifyPropertyChangedFor(NameOf(TotalSaved))>\n    <ObservableProperty> Private _LastAnalysed As DateTime\n    <ObservableProperty> Private _WatchedFolders As New ObservableCollection(Of WatchedFolder)\n    <ObservableProperty> Private _IsWatchingEnabled As Boolean = True\n    <ObservableProperty> Private _IsBackgroundCompactingEnabled As Boolean = True\n    <ObservableProperty> Private _BGCompactor As BackgroundCompactor\n\n    Private ReadOnly Property WatcherJSONFile As IO.FileInfo\n    Private ReadOnly IdleSettings As IdleSettings\n\n    Public ReadOnly Property TotalSaved As Long\n        Get\n            Return WatchedFolders.Sum(Function(f) f.LastUncompressedSize - f.LastCheckedSize)\n        End Get\n    End Property\n\n\n    Sub New(logger As ILogger(Of Watcher), settingsService As ISettingsService, idleDetector As IdleDetector)\n        _logger = logger\n        _settingsService = settingsService\n        _DataFolder = settingsService.DataFolder\n\n        WatcherJSONFile = New IO.FileInfo(IO.Path.Combine(_DataFolder.FullName, \"watcher.json\"))\n\n        IdleSettings = New IdleSettings\n        _idleDetector = idleDetector\n        WatcherLog.WatcherStarted(logger)\n        IsActive = True\n\n        AddHandler _idleDetector.IsIdle, _idleHandler\n        AddHandler _idleDetector.IsNotIdle, AddressOf OnSystemNotIdle\n        AddHandler WatchedFolders.CollectionChanged, AddressOf WatchedFolders_CollectionChanged\n\n\n        BGCompactor = New BackgroundCompactor(Array.Empty(Of String), _logger)\n\n\n        InitializeWatchedFoldersAsync()\n\n\n    End Sub\n\n    Private _idleHandler As EventHandler = AddressOf OnSystemIdle\n    Private _isSystemIdle As Boolean = False\n\n    Private Async Sub OnSystemIdle()\n        If Not _isSystemIdle Then WatcherLog.SystemIdleDetected(_logger)\n        _isSystemIdle = True\n\n        'Skip idle analysis if the background mode is not set to IdleOnly\n        Dim bgMode = _settingsService.AppSettings.BackgroundModeSelection\n        If bgMode <> BackgroundMode.IdleOnly Then Return\n\n        BGCompactor.ResumeCompacting()\n\n        Await RunWatcher(False)\n\n    End Sub\n\n\n\n    <ObservableProperty> Private _isRunning As Boolean = False\n\n    Public Async Function RunWatcher(Optional runAll As Boolean = True, Optional cToken As CancellationToken = Nothing) As Task(Of Boolean)\n        RemoveHandler _idleDetector.IsIdle, _idleHandler\n\n        IsRunning = True\n\n        For Each watcher In WatchedFolders\n            watcher.PauseMonitoring()\n        Next\n\n        Try\n\n            _settingsService.AppSettings.ScheduledBackgroundLastRan = DateTime.Now\n            If Not IsWatchingEnabled Then Return False\n            Dim recentThresholdDate As DateTime = DateTime.Now.AddSeconds(-IdleSettings.LastSystemModifiedTimeThresholdSeconds)\n            If Not runAll AndAlso WatchedFolders.Any(Function(x) x.LastChangedDate > recentThresholdDate) Then Return False\n\n            If _parseWatchersSemaphore.CurrentCount <> 0 Then\n                Await ParseWatchers(runAll, cToken)\n            End If\n            If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then\n                _logger.LogInformation(\"Watcher run cancelled by user.\")\n                Return False\n            End If\n            If _parseWatchersSemaphore.CurrentCount <> 0 AndAlso (IsBackgroundCompactingEnabled OrElse runAll) Then\n                Await BackgroundCompact(runAll) 'Don't need to pass the cancellation token here, as the background compactor handles it internally.\n            End If\n            If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then\n                _logger.LogInformation(\"Watcher run cancelled by user.\")\n                Return False\n            End If\n            Return True\n\n        Catch ex As TaskCanceledException\n            Return False\n        Finally\n\n            AddHandler _idleDetector.IsIdle, _idleHandler\n            For Each watcher In WatchedFolders\n                watcher.ResumeMonitoring()\n            Next\n            IsRunning = False\n        End Try\n        Return False\n    End Function\n\n\n\n    Private Sub OnSystemNotIdle(sender As Object, e As EventArgs)\n        _isSystemIdle = False\n        WatcherLog.SystemNotIdle(_logger)\n\n        'Skip idle analysis if the background mode is not set to IdleOnly\n        Dim bgMode = _settingsService.AppSettings.BackgroundModeSelection\n        If bgMode <> BackgroundMode.IdleOnly Then Return\n\n        BGCompactor.PauseCompacting()\n    End Sub\n\n\n    Private _disableCounter As Integer = 0\n    Private _counterLock As New SemaphoreSlim(1, 1)\n\n    Public Async Function DisableBackgrounding() As Task\n        Await _counterLock.WaitAsync()\n        Try\n            _disableCounter += 1\n            If _disableCounter = 1 Then\n                WatcherLog.BackgroundingDisabled(_logger)\n                Await _idleDetector.StopAsync()\n                BGCompactor.CancelCompacting()\n                Await _parseWatchersSemaphore.WaitAsync()\n            End If\n        Finally\n            _counterLock.Release()\n        End Try\n    End Function\n\n    Public Async Function EnableBackgrounding() As Task\n        Await _counterLock.WaitAsync()\n        Try\n            If _disableCounter > 0 Then\n                _disableCounter -= 1\n                If _disableCounter = 0 Then\n                    _parseWatchersSemaphore.Release()\n                    _idleDetector.Start()\n                    WatcherLog.BackgroundingEnabled(_logger)\n                End If\n            End If\n        Finally\n            _counterLock.Release()\n        End Try\n    End Function\n\n\n\n    Private Sub WatchedFolders_CollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)\n        OnPropertyChanged(NameOf(TotalSaved))\n    End Sub\n\n    Private Async Function InitializeWatchedFoldersAsync() As Task\n        Dim initialWatchedFolders = Await GetWatchedFoldersFromJson()\n\n        If initialWatchedFolders Is Nothing Then Return\n\n        WatchedFolders.Clear()\n\n        For Each folder In initialWatchedFolders.Where(Function(f) IO.Directory.Exists(f.Folder))\n            WatchedFolders.Add(folder)\n            folder.LastChangedDate = folder.LastSystemModifiedDate\n        Next\n\n        UpdateRegistryBasedOnWatchedFolders()\n    End Function\n\n    Private Sub UpdateRegistryBasedOnWatchedFolders()\n        Dim registryKey As RegistryKey = Registry.CurrentUser.OpenSubKey(\"Software\\Microsoft\\Windows\\CurrentVersion\\Run\", True)\n\n        If WatchedFolders.Count > 0 Then\n            registryKey.SetValue(\"CompactGUI\", Environment.ProcessPath & \" -tray\")\n        Else\n            registryKey.DeleteValue(\"CompactGUI\", False)\n        End If\n    End Sub\n\n\n    Public Sub AddOrUpdateWatched(item As WatchedFolder, Optional immediateFlushToDisk As Boolean = True)\n\n        Dim existingItem = WatchedFolders.FirstOrDefault(Function(f) f.Folder = item.Folder)\n        If existingItem Is Nothing Then\n            WatchedFolders.Add(item)\n            item.LastChangedDate = item.LastSystemModifiedDate\n        Else\n            UpdateFolderProperties(existingItem, item)\n        End If\n        OnPropertyChanged(NameOf(TotalSaved))\n        If immediateFlushToDisk Then WriteToFile()\n\n    End Sub\n\n    Public Async Sub UpdateWatched(folder As String, analyser As Analyser, isFreshlyCompressed As Boolean, Optional immediateFlushToDisk As Boolean = True)\n\n        Dim existingItem = WatchedFolders.FirstOrDefault(Function(f) f.Folder = folder)\n\n        If existingItem IsNot Nothing Then\n\n            Dim analysedFiles = Await analyser.GetAnalysedFilesAsync(CancellationToken.None)\n\n            existingItem.LastCheckedDate = DateTime.Now\n            existingItem.LastCheckedSize = analyser.CompressedBytes\n            existingItem.LastUncompressedSize = analyser.UncompressedBytes\n            existingItem.LastSystemModifiedDate = DateTime.Now\n            If analysedFiles?.Count <> 0 Then\n                existingItem.CompressionLevel = analysedFiles.Select(Function(f) f.CompressionMode).Max\n            End If\n\n            If isFreshlyCompressed Then\n                existingItem.LastCompressedDate = DateTime.Now\n            End If\n\n            If isFreshlyCompressed OrElse existingItem.CompressionLevel = WOFCompressionAlgorithm.NO_COMPRESSION Then\n                existingItem.LastCompressedSize = analyser.CompressedBytes\n            End If\n\n            existingItem.HasTargetChanged = False\n            OnPropertyChanged(NameOf(TotalSaved))\n            If immediateFlushToDisk Then WriteToFile()\n        End If\n    End Sub\n\n    Private Sub UpdateFolderProperties(existingItem As WatchedFolder, newItem As WatchedFolder)\n        With existingItem\n            .Folder = newItem.Folder\n            .DisplayName = newItem.DisplayName\n            .IsSteamGame = newItem.IsSteamGame\n            .LastCompressedSize = newItem.LastCompressedSize\n            .LastUncompressedSize = newItem.LastUncompressedSize\n            .LastCompressedDate = DateTime.Now\n            .LastCheckedDate = DateTime.Now\n            .LastCheckedSize = newItem.LastCheckedSize\n            .LastSystemModifiedDate = DateTime.Now\n            .CompressionLevel = If(newItem.CompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION, newItem.CompressionLevel, existingItem.CompressionLevel)\n        End With\n        existingItem.HasTargetChanged = False\n    End Sub\n\n    Public Async Function RemoveWatched(item As WatchedFolder, Optional writeToFile As Boolean = True) As Task\n\n        item.Dispose()\n        WatchedFolders.Remove(item)\n        If writeToFile Then Await WriteToFileAsync()\n\n    End Function\n\n\n    Public Async Function DeleteWatchersWithNonExistentFolders() As Task\n\n        For i As Integer = WatchedFolders.Count - 1 To 0 Step -1\n            If Not IO.Directory.Exists(WatchedFolders(i).Folder) Then\n                WatcherLog.RemovingNonexistentFolders(_logger, 1)\n                Await RemoveWatched(WatchedFolders(i), False)\n            End If\n        Next\n\n        Await WriteToFileAsync()\n\n    End Function\n\n\n    Private Async Function GetWatchedFoldersFromJson() As Task(Of ObservableCollection(Of WatchedFolder))\n\n        If Not _DataFolder.Exists Then _DataFolder.Create()\n        If Not WatcherJSONFile.Exists Then Await WatcherJSONFile.Create().DisposeAsync()\n\n        Dim ret = DeserializeAndValidateJSON(WatcherJSONFile)\n        LastAnalysed = ret.Item1\n        Dim retWatchedFolders = ret.Item2\n\n\n        Return retWatchedFolders\n    End Function\n\n\n    Private Shared ReadOnly DeserializeOptions As New JsonSerializerOptions With {.IncludeFields = True}\n    Private Shared ReadOnly SerializeOptions As New JsonSerializerOptions With {.IncludeFields = True, .WriteIndented = True}\n\n    Private Function DeserializeAndValidateJSON(inputjsonFile As IO.FileInfo) As (DateTime, ObservableCollection(Of WatchedFolder))\n        Dim WatcherJSON = IO.File.ReadAllText(inputjsonFile.FullName)\n        If WatcherJSON = \"\" Then WatcherJSON = \"{}\"\n\n        Dim validatedResult As (DateTime, ObservableCollection(Of WatchedFolder))\n        Try\n            validatedResult = JsonSerializer.Deserialize(Of (DateTime, ObservableCollection(Of WatchedFolder)))(WatcherJSON, DeserializeOptions)\n\n            If validatedResult.Item2 IsNot Nothing Then\n                For Each folder In validatedResult.Item2.Where(Function(f) IO.Directory.Exists(f.Folder))\n                    folder.InitializeMonitoring()\n                Next\n            End If\n\n        Catch ex As Exception\n            validatedResult = (DateTime.Now, Nothing)\n            WatcherLog.DeserializeWatcherJsonFailed(_logger, ex.Message)\n        End Try\n\n        Return validatedResult\n\n    End Function\n    Public Sub WriteToFile()\n\n        Dim output = JsonSerializer.Serialize((LastAnalysed, WatchedFolders), SerializeOptions)\n        IO.File.WriteAllText(WatcherJSONFile.FullName, output)\n\n    End Sub\n\n    Public Async Function WriteToFileAsync() As Task\n        Using stream = IO.File.Open(WatcherJSONFile.FullName, IO.FileMode.Create, IO.FileAccess.Write, IO.FileShare.None)\n            Await JsonSerializer.SerializeAsync(stream, (LastAnalysed, WatchedFolders), SerializeOptions)\n        End Using\n    End Function\n\n\n\n\n    Public Async Function ParseWatchers(Optional ParseAll As Boolean = False, Optional cToken As CancellationToken = Nothing) As Task\n        Dim acquired = Await _parseWatchersSemaphore.WaitAsync(0)\n        If Not acquired Then Return\n\n        Try\n            WatcherLog.ParsingWatchers(_logger, ParseAll)\n            Await DeleteWatchersWithNonExistentFolders()\n\n            Dim WatchersQuery = If(ParseAll,\n                    WatchedFolders,\n                    WatchedFolders.Where(Function(w) w.HasTargetChanged)\n                    ).OrderBy(Function(f) f.DisplayName)\n\n            If Not WatchersQuery.Any() Then Return\n\n            For Each fsWatcher In WatchersQuery\n                WatcherLog.FolderChanged(_logger, fsWatcher.DisplayName)\n                If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then Return\n                Await Analyse(fsWatcher.Folder, ParseAll, cToken)\n            Next\n\n            If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then Return\n            Await WriteToFileAsync()\n            LastAnalysed = DateTime.Now\n        Finally\n            _parseWatchersSemaphore.Release()\n        End Try\n\n\n\n    End Function\n\n    Public Async Function ParseSingleWatcher(watchedFolder As WatchedFolder) As Task\n\n        Dim acquired = Await _parseWatchersSemaphore.WaitAsync(0)\n        If Not acquired Then Return\n\n        Try\n            If watchedFolder Is Nothing Then Return\n            If Not IO.Directory.Exists(watchedFolder.Folder) Then\n                Await RemoveWatched(watchedFolder)\n                Return\n            End If\n\n            Await Analyse(watchedFolder.Folder, False)\n            LastAnalysed = DateTime.Now\n            Await WriteToFileAsync()\n        Finally\n            _parseWatchersSemaphore.Release()\n        End Try\n\n\n    End Function\n\n    Public Async Function BackgroundCompact(Optional runAll As Boolean = False) As Task\n\n        Dim acquired = Await _parseWatchersSemaphore.WaitAsync(0)\n        If Not acquired Then Return\n\n        Try\n\n            If BGCompactor.IsCompactorActive Then Return\n\n            Dim recentThresholdDate As DateTime = DateTime.Now.AddSeconds(-IdleSettings.LastSystemModifiedTimeThresholdSeconds)\n\n            Dim foldersToCompress = WatchedFolders.\n                Where(Function(folder)\n                          Dim eligible = folder.DecayPercentage <> 0 AndAlso folder.CompressionLevel <> WOFCompressionAlgorithm.NO_COMPRESSION\n                          Dim recentlyModified = folder.LastSystemModifiedDate > recentThresholdDate AndAlso Not runAll\n                          If eligible AndAlso recentlyModified Then\n                              WatcherLog.SkippingRecentlyModifiedFolder(_logger, folder.DisplayName)\n                          End If\n                          Return eligible AndAlso Not recentlyModified\n                      End Function)\n\n            If foldersToCompress.Any = 0 Then Return\n\n            Await BGCompactor.StartCompactingAsync(foldersToCompress)\n\n            OnPropertyChanged(NameOf(TotalSaved))\n        Finally\n            _parseWatchersSemaphore.Release()\n\n        End Try\n\n    End Function\n\n\n    Public Async Function Analyse(folder As String, checkDiskModified As Boolean, Optional cToken As CancellationToken = Nothing) As Task(Of Boolean)\n\n        Using analyser As New Analyser(folder, NullLogger(Of Analyser).Instance)\n            Dim watched = WatchedFolders.First(Function(f) f.Folder = folder)\n            watched.IsWorking = True\n            Try\n                Dim analysedFiles = Await analyser.GetAnalysedFilesAsync(cToken)\n                If cToken <> Nothing AndAlso cToken.IsCancellationRequested Then Return False\n\n                watched.LastCheckedDate = DateTime.Now\n                watched.LastCheckedSize = analyser.CompressedBytes\n                watched.LastUncompressedSize = analyser.UncompressedBytes\n\n                watched.LastSystemModifiedDate = watched.LastChangedDate\n\n                If analysedFiles.Count <> 0 Then\n                    Dim mainCompressionLVL = analysedFiles?.Select(Function(f) f.CompressionMode).Max\n                    watched.CompressionLevel = If(mainCompressionLVL <> WOFCompressionAlgorithm.NO_COMPRESSION, mainCompressionLVL, watched.CompressionLevel)\n\n                    If checkDiskModified Then\n                        Dim lastDiskWriteTime = analysedFiles.Select(Function(fl)\n                                                                         Dim finfo As New IO.FileInfo(fl.FileName)\n                                                                         Return finfo.LastWriteTime\n                                                                     End Function).OrderByDescending(Function(f) f).First\n\n                        watched.LastSystemModifiedDate = If(watched.LastSystemModifiedDate < lastDiskWriteTime, lastDiskWriteTime, watched.LastSystemModifiedDate)\n\n                    End If\n                End If\n\n                watched.HasTargetChanged = False\n            Catch ex As OperationCanceledException\n                Return False\n            Finally\n\n                watched.IsWorking = False\n            End Try\n\n            Return True\n\n        End Using\n\n    End Function\n\n    Public Sub Receive(message As PropertyChangedMessage(Of Boolean)) Implements IRecipient(Of PropertyChangedMessage(Of Boolean)).Receive\n        If (message.Sender.GetType() IsNot GetType(Settings)) Then Return\n\n        If message.PropertyName = NameOf(Settings.EnableBackgroundWatcher) Then : IsWatchingEnabled = message.NewValue\n        ElseIf message.PropertyName = NameOf(Settings.BackgroundModeSelection) Then : IsBackgroundCompactingEnabled = (CType(message.NewValue, BackgroundMode) = BackgroundMode.IdleOnly)\n        End If\n\n\n    End Sub\n\n\n\n\n\nEnd Class\n\n\n"
  },
  {
    "path": "CompactGUI.slnx",
    "content": "<Solution>\n  <Configurations>\n    <Platform Name=\"Any CPU\" />\n    <Platform Name=\"ARM\" />\n    <Platform Name=\"ARM64\" />\n    <Platform Name=\"x64\" />\n    <Platform Name=\"x86\" />\n  </Configurations>\n  <Folder Name=\"/Solution Items/\">\n    <File Path=\"README.md\" />\n  </Folder>\n  <Project Path=\"CompactGUI.Core/CompactGUI.Core.csproj\" />\n  <Project Path=\"CompactGUI.CoreVB/CompactGUI.CoreVB.vbproj\" />\n  <Project Path=\"CompactGUI.Logging/CompactGUI.Logging.csproj\" Id=\"d21e36f8-54bb-401e-9621-b87d03488806\" />\n  <Project Path=\"CompactGUI.TestingGround/CompactGUI.TestingGround.csproj\" Id=\"279814db-5112-4c5f-b013-df80d73bcf37\" />\n  <Project Path=\"CompactGUI.Watcher/CompactGUI.Watcher.vbproj\">\n    <BuildDependency Project=\"CompactGUI.CoreVB/CompactGUI.CoreVB.vbproj\" />\n  </Project>\n  <Project Path=\"CompactGUI.WatcherCS/CompactGUI.WatcherCS.csproj\" Id=\"2f8f22c2-66d8-4ac7-ab11-bc1e1086d6c4\" />\n  <Project Path=\"CompactGUI/CompactGUI.vbproj\" />\n</Solution>\n"
  },
  {
    "path": "LICENSE",
    "content": "(c) Iridium IO 2024\n\n                     GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n\n\n\n==========================================================================================\n\nFor Component Ookii.Dialogs.WPF\n\nBSD 3-Clause License\n\nCopyright (c) C. Augusto Proiete 2018-2021\nCopyright (c) Sven Groot         2009-2018\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\"><img src=\"https://github.com/IridiumIO/CompactGUI/assets/1491536/64f66b5d-0710-4f66-8b88-6a69f7eb9b63\" width=\"500\"></p>\n\n\n&nbsp;\n\n<p align=\"center\"><b>CompactGUI transparently compresses your games and programs reducing the space they use without affecting their functionality. It works directly with the Win32 API to achieve the same thing as the native <code>compact.exe</code> command-line tool available from Windows 10 onwards.</b></p> \n\n&nbsp;\n&nbsp;\n\n<p align=\"center\"><img src=\"https://user-images.githubusercontent.com/1491536/172040389-62932137-11ae-49c8-8749-95c0b67f3aab.png\" width=\"250\"/><img src=\"https://user-images.githubusercontent.com/1491536/172040455-6cd06756-6323-44da-b350-daa47f31c5e3.png\" width=\"250\"/><img src=\"https://user-images.githubusercontent.com/1491536/172040456-09c069e3-093a-4c5e-8d69-f52d4dc2f982.png\" width=\"250\"/></>\n\n------\n&nbsp;\n\n**What is `compact.exe`?**\nIt's a commandlet with a collection of new algorithms introduced in Windows 10 that allow you to transparently compress games, programs and other folders with virtually no performance loss.\n\n**Transparently? What does that mean?**\nTransparent compression means that files can still be used normally on the computer as if nothing had happened - they don't get repackaged like Zip and Rar files do. You can still browse, launch games and programs exactly as you did before. \n\n**How is this different from the built-in compression in older versions of Windows?**\nThis is similar to the NTFS-LZNT1 compression built-in to Windows (Right click > Properties > Compress to save space) however the newer algorithms introduced in Windows 10+ are far superior, resulting in greater compression ratios with almost no performance impact. Those with older HDDs may even see a performance gain in the form of reduced loading times - smaller files can be read into RAM faster, and the CPU can decompress them on the fly much faster than a typical HDD can supply them. [More information can be found here](https://msdn.microsoft.com/en-us/library/windows/desktop/hh920921(v=vs.85).aspx) \n\n\n\n<h2>Installation  </h> <a href=\"https://github.com/ImminentFate/CompactGUI/releases\"><img src=\"https://img.shields.io/github/release/ImminentFate/compactgui/all.svg\"\"></a>   <a href=\"https://github.com/ImminentFate/CompactGUI/releases\"><img src=\"https://img.shields.io/github/downloads/ImminentFate/CompactGUI/total.svg\"\"></a>\n\n####\n \n- <p>Download from <a href=\"https://github.com/IridiumIO/CompactGUI/releases\"><b>GitHub Releases</b></a></p>\n- Install using Winget `winget install CompactGUI`\n\n\n## Uses\nUse this tool to compress folders while still being able to use/run them normally: \n- Reduce the size of games (e.g. ARK-Survival Evolved: 169 GB > 91.2 GB)\n- Reduce the size of programs (e.g. Adobe Photoshop: 1.71 GB > 886 MB)\n- Compress any other folder on your computer\n  \n## Extra Features\n - Visual feedback on compression progress and statistics\n - Configurable list of poorly-compressed filetypes that can be skipped.\n - Online integration with community-sourced [database](https://github.com/ImminentFate/CompactGUI/wiki/Community-Compression-Results) to get compression estimates\n      - Steam game results can be submitted to the online database from within CompactGUI \n - Integration into Windows Explorer context menus for easier use.\n - Analyze the status of existing folders\n - Background Watcher - keeps track of folders and monitors them for changes (e.g. Steam game updates) and automatically keeps them compressed in the background.\n \n<h4 align=\"center\"><b>See the <a href=\"https://github.com/ImminentFate/CompactGUI/wiki/Community-Compression-Results\">Wiki</a> for a list of <a href=\"https://github.com/ImminentFate/CompactGUI/wiki/Community-Compression-Results\"><img src=\"https://img.shields.io/badge/9779-Games-blue.svg\"></a> that have been tested from <a href=\"https://github.com/ImminentFate/CompactGUI/wiki/Community-Compression-Results\"><img src=\"https://img.shields.io/badge/-82436-lightgrey.svg\"></a> submissions</b></h3>\n<p>&nbsp;</p>\n\n<p align=\"center\"><img src=\"https://github.com/IridiumIO/CompactGUI/assets/1491536/514bb3eb-55c4-488c-8731-61fcfa878dbd\" width=\"250\"/></p>\n\n## Caveat\n**This tool should not be used on games that utilise DirectStorage on Windows 11.** \n\nDirectStorage is a new API that allows games to load assets directly from the SSD, bypassing the CPU. Compressed files will need to be decompressed before being sent to the GPU, which will negate any performance gains.\n\n\n\n## Background\n\nWindows 10 introduced a little-known but very useful tool called `compact.exe` that allows one to compress folders and files on disk, decompressing them at runtime. With any modern CPU (I have tested as old as an i3-370M from 2010 with negligible impact), this added load is hardly noticed, and the space savings are of most use on those with smaller SSDs. \n\nAs program folders and games can be shrunk by up to 60%, this has the added bonus of potentially reducing load times - especially on slower HDDs. \n\nMore information on the inbuilt Windows function can be found [here](https://technet.microsoft.com/en-au/library/bb490884.aspx) and [here](https://msdn.microsoft.com/en-us/library/windows/desktop/hh920921(v=vs.85).aspx) or by typing `compact /q` into the commandline\n\nThis tool is intentionally designed to only compress folders and files. Whole drives and entire Windows installations cannot be modified from within CompactGUI - users seeking that functionality should use `compact /compactOS` from the commandline. \n\nThe compression is fully transparent - programs, games and files can still be accessed as normal, and show up in Explorer as they normally would — they'll just be decompressed into RAM at runtime, staying compressed on disk.\n\n## Options\nBy default, the program runs Compact with the `XPRESS8K` algorithm active. This provides a good balance between compression speed and size reduction. The default that Windows uses is `XPRESS4K` which is faster but compresses less. \nThe options available are: \n- XPRESS4K: Fastest, but weakest\n- XPRESS8K: Reasonable balance between speed and compression\n- XPRESS16K: Slower, but stronger\n- LZX: Slowest, but strongest - note it has a higher overhead, so use it on programs/games only if your CPU is reasonably strong or the program/game is older. \n\n \n -----\n ### Like this project?\n Please consider leaving a tip on Ko-Fi :) \n \n <p align=\"center\"><a href='https://ko-fi.com/iridiumio' target='_blank'><img height='42' style='border:0px;height:42px;' src='https://cdn.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a></p>\n  \n"
  },
  {
    "path": "Version.xml",
    "content": "<Info>\n\t<VersionMajor>3.0</VersionMajor>\n\t<VersionMinor>0</VersionMinor>\n\t<IsPrerelease>True</IsPrerelease>\n\t<VersionStr>V3.0.0</VersionStr>\n\t<ChocolateyVStr>2.6.2</ChocolateyVStr>\n\t<Changes>Complete application rewrite using WPF and .NET 6|Smoother, simpler UI.|Major speed improvements with asynchronous and parallel processing of files </Changes>\n\t<Fixes></Fixes>\n</Info>\n"
  }
]