├── .gitattributes ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── Stardrop ├── App.axaml ├── App.axaml.cs ├── Assets ├── Info.plist ├── Stardrop.icns ├── Stardrop.sh ├── icon.ico └── smapi.png ├── Converters ├── EnumConverter.cs └── EnumEqualsConverter.cs ├── Models ├── Config.cs ├── Data │ ├── ClientData.cs │ ├── Enums │ │ ├── Choice.cs │ │ ├── DisplayFilter.cs │ │ ├── EndorsementResponse.cs │ │ ├── InstallState.cs │ │ ├── ModGrouping.cs │ │ └── NexusServers.cs │ ├── LastSessionData.cs │ ├── ModDownloadEvents.cs │ ├── ModInstallData.cs │ ├── ModKeyInfo.cs │ ├── ModUpdateInfo.cs │ ├── PairedKeys.cs │ └── UpdateCache.cs ├── Mod.cs ├── Nexus │ ├── NexusUser.cs │ └── Web │ │ ├── DownloadLink.cs │ │ ├── Endorsement.cs │ │ ├── EndorsementResult.cs │ │ ├── ModDetails.cs │ │ ├── ModFile.cs │ │ ├── ModFiles.cs │ │ ├── NXM.cs │ │ └── Validate.cs ├── Profile.cs ├── SMAPI │ ├── Converters │ │ ├── BooleanConverter.cs │ │ ├── BooleanConverterAssumeTrue.cs │ │ └── ModKeyConverter.cs │ ├── GameDetails.cs │ ├── Manifest.cs │ ├── ManifestContentPackFor.cs │ ├── ManifestDependency.cs │ └── Web │ │ ├── ModEntry.cs │ │ ├── ModEntryMetadata.cs │ │ ├── ModEntryVersion.cs │ │ ├── ModSearchData.cs │ │ └── ModSearchEntry.cs └── Settings.cs ├── Program.cs ├── Properties └── PublishProfiles │ ├── FolderProfile - Linux.pubxml │ ├── FolderProfile - MacOS.pubxml │ └── FolderProfile - Windows.pubxml ├── Stardrop.csproj ├── Stardrop.sln ├── Themes ├── Dark.xaml ├── Light.xaml ├── Solarized-Lite.xaml └── Stardrop.xaml ├── Utilities ├── Extension │ └── TranslateExtension.cs ├── External │ ├── GitHub.cs │ ├── NexusClient.cs │ ├── NexusDownloadResult.cs │ └── SMAPI.cs ├── Helper.cs ├── Internal │ ├── EnumParser.cs │ └── ManifestParser.cs ├── JsonTools.cs ├── NXMProtocol.cs ├── Pathing.cs ├── SimpleObscure.cs └── Translation.cs ├── ViewLocator.cs ├── ViewModels ├── DownloadPanelViewModel.cs ├── FlexibleOptionWindowViewModel.cs ├── MainWindowViewModel.cs ├── MessageWindowViewModel.cs ├── ModDownloadViewModel.cs ├── ProfileEditorViewModel.cs ├── SettingsWindowViewModel.cs ├── ViewModelBase.cs └── WarningWindowViewModel.cs ├── Views ├── DownloadPanel.axaml ├── DownloadPanel.axaml.cs ├── FlexibleOptionWindow.axaml ├── FlexibleOptionWindow.axaml.cs ├── MainWindow.axaml ├── MainWindow.axaml.cs ├── MessageWindow.axaml ├── MessageWindow.axaml.cs ├── NexusInfo.axaml ├── NexusInfo.axaml.cs ├── NexusLogin.axaml ├── NexusLogin.axaml.cs ├── ProfileEditor.axaml ├── ProfileEditor.axaml.cs ├── ProfileNaming.axaml ├── ProfileNaming.axaml.cs ├── SettingsWindow.axaml ├── SettingsWindow.axaml.cs ├── WarningWindow.axaml └── WarningWindow.axaml.cs └── i18n ├── de.json ├── default.json ├── es.json ├── fr.json ├── hu.json ├── it.json ├── pt.json ├── ru.json ├── th.json ├── tr.json ├── uk.json └── zh.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.sh text eol=lf 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015/2017 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Visual Studio 2017 auto generated files 34 | Generated\ Files/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # Benchmark Results 50 | BenchmarkDotNet.Artifacts/ 51 | 52 | # .NET Core 53 | project.lock.json 54 | project.fragment.lock.json 55 | artifacts/ 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_h.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *_wpftmp.csproj 81 | *.log 82 | *.vspscc 83 | *.vssscc 84 | .builds 85 | *.pidb 86 | *.svclog 87 | *.scc 88 | 89 | # Chutzpah Test files 90 | _Chutzpah* 91 | 92 | # Visual C++ cache files 93 | ipch/ 94 | *.aps 95 | *.ncb 96 | *.opendb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | *.VC.db 101 | *.VC.VC.opendb 102 | 103 | # Visual Studio profiler 104 | *.psess 105 | *.vsp 106 | *.vspx 107 | *.sap 108 | 109 | # Visual Studio Trace Files 110 | *.e2e 111 | 112 | # TFS 2012 Local Workspace 113 | $tf/ 114 | 115 | # Guidance Automation Toolkit 116 | *.gpState 117 | 118 | # ReSharper is a .NET coding add-in 119 | _ReSharper*/ 120 | *.[Rr]e[Ss]harper 121 | *.DotSettings.user 122 | 123 | # JustCode is a .NET coding add-in 124 | .JustCode 125 | 126 | # TeamCity is a build add-in 127 | _TeamCity* 128 | 129 | # DotCover is a Code Coverage Tool 130 | *.dotCover 131 | 132 | # AxoCover is a Code Coverage Tool 133 | .axoCover/* 134 | !.axoCover/settings.json 135 | 136 | # Visual Studio code coverage results 137 | *.coverage 138 | *.coveragexml 139 | 140 | # NCrunch 141 | _NCrunch_* 142 | .*crunch*.local.xml 143 | nCrunchTemp_* 144 | 145 | # MightyMoose 146 | *.mm.* 147 | AutoTest.Net/ 148 | 149 | # Web workbench (sass) 150 | .sass-cache/ 151 | 152 | # Installshield output folder 153 | [Ee]xpress/ 154 | 155 | # DocProject is a documentation generator add-in 156 | DocProject/buildhelp/ 157 | DocProject/Help/*.HxT 158 | DocProject/Help/*.HxC 159 | DocProject/Help/*.hhc 160 | DocProject/Help/*.hhk 161 | DocProject/Help/*.hhp 162 | DocProject/Help/Html2 163 | DocProject/Help/html 164 | 165 | # Click-Once directory 166 | publish/ 167 | 168 | # Publish Web Output 169 | *.[Pp]ublish.xml 170 | *.azurePubxml 171 | # Note: Comment the next line if you want to checkin your web deploy settings, 172 | # but database connection strings (with potential passwords) will be unencrypted 173 | #*.pubxml 174 | *.publishproj 175 | 176 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 177 | # checkin your Azure Web App publish settings, but sensitive information contained 178 | # in these scripts will be unencrypted 179 | PublishScripts/ 180 | 181 | # NuGet Packages 182 | *.nupkg 183 | # The packages folder can be ignored because of Package Restore 184 | **/[Pp]ackages/* 185 | # except build/, which is used as an MSBuild target. 186 | !**/[Pp]ackages/build/ 187 | # Uncomment if necessary however generally it will be regenerated when needed 188 | #!**/[Pp]ackages/repositories.config 189 | # NuGet v3's project.json files produces more ignorable files 190 | *.nuget.props 191 | *.nuget.targets 192 | 193 | # Microsoft Azure Build Output 194 | csx/ 195 | *.build.csdef 196 | 197 | # Microsoft Azure Emulator 198 | ecf/ 199 | rcf/ 200 | 201 | # Windows Store app package directories and files 202 | AppPackages/ 203 | BundleArtifacts/ 204 | Package.StoreAssociation.xml 205 | _pkginfo.txt 206 | *.appx 207 | 208 | # Visual Studio cache files 209 | # files ending in .cache can be ignored 210 | *.[Cc]ache 211 | # but keep track of directories ending in .cache 212 | !*.[Cc]ache/ 213 | 214 | # Others 215 | ClientBin/ 216 | ~$* 217 | *~ 218 | *.dbmdl 219 | *.dbproj.schemaview 220 | *.jfm 221 | *.pfx 222 | *.publishsettings 223 | orleans.codegen.cs 224 | 225 | # Including strong name files can present a security risk 226 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 227 | #*.snk 228 | 229 | # Since there are multiple workflows, uncomment next line to ignore bower_components 230 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 231 | #bower_components/ 232 | 233 | # RIA/Silverlight projects 234 | Generated_Code/ 235 | 236 | # Backup & report files from converting an old project file 237 | # to a newer Visual Studio version. Backup files are not needed, 238 | # because we have git ;-) 239 | _UpgradeReport_Files/ 240 | Backup*/ 241 | UpgradeLog*.XML 242 | UpgradeLog*.htm 243 | ServiceFabricBackup/ 244 | *.rptproj.bak 245 | 246 | # SQL Server files 247 | *.mdf 248 | *.ldf 249 | *.ndf 250 | 251 | # Business Intelligence projects 252 | *.rdl.data 253 | *.bim.layout 254 | *.bim_*.settings 255 | *.rptproj.rsuser 256 | 257 | # Microsoft Fakes 258 | FakesAssemblies/ 259 | 260 | # GhostDoc plugin setting file 261 | *.GhostDoc.xml 262 | 263 | # Node.js Tools for Visual Studio 264 | .ntvs_analysis.dat 265 | node_modules/ 266 | 267 | # Visual Studio 6 build log 268 | *.plg 269 | 270 | # Visual Studio 6 workspace options file 271 | *.opt 272 | 273 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 274 | *.vbw 275 | 276 | # Visual Studio LightSwitch build output 277 | **/*.HTMLClient/GeneratedArtifacts 278 | **/*.DesktopClient/GeneratedArtifacts 279 | **/*.DesktopClient/ModelManifest.xml 280 | **/*.Server/GeneratedArtifacts 281 | **/*.Server/ModelManifest.xml 282 | _Pvt_Extensions 283 | 284 | # Paket dependency manager 285 | .paket/paket.exe 286 | paket-files/ 287 | 288 | # FAKE - F# Make 289 | .fake/ 290 | 291 | # JetBrains Rider 292 | .idea/ 293 | *.sln.iml 294 | 295 | # CodeRush personal settings 296 | .cr/personal 297 | 298 | # Python Tools for Visual Studio (PTVS) 299 | __pycache__/ 300 | *.pyc 301 | 302 | # Cake - Uncomment if you are using it 303 | # tools/** 304 | # !tools/packages.config 305 | 306 | # Tabs Studio 307 | *.tss 308 | 309 | # Telerik's JustMock configuration file 310 | *.jmconfig 311 | 312 | # BizTalk build output 313 | *.btp.cs 314 | *.btm.cs 315 | *.odx.cs 316 | *.xsd.cs 317 | 318 | # OpenCover UI analysis results 319 | OpenCover/ 320 | 321 | # Azure Stream Analytics local run output 322 | ASALocalRun/ 323 | 324 | # MSBuild Binary and Structured Log 325 | *.binlog 326 | 327 | # NVidia Nsight GPU debugger configuration file 328 | *.nvuser 329 | 330 | # MFractors (Xamarin productivity tool) working folder 331 | .mfractor/ 332 | 333 | # Local History for Visual Studio 334 | .localhistory/ 335 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If reporting an issue with Stardrop, please ensure that a copy of Stardrop's log is attached. 2 | 3 | The log file can be found via Stardrop's menu under `View` > `Log File`. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stardrop 2 | 3 | Stardrop is an open-source, cross-platform mod manager for the game [Stardew Valley](https://www.stardewvalley.net/). It is built using the Avalonia UI framework. 4 | 5 | Stadrop utilizes [SMAPI (Stardew Modding API)](https://smapi.io/) to simplify the management and update checking for all applicable Stardew Valley mods. 6 | 7 | Profiles are also supported, allowing users to have multiple mod groups for specific gameplay or multiplayer sessions. 8 | 9 | # Getting Started 10 | 11 | See the [GitBook pages](https://floogen.gitbook.io/stardrop/) for detailed documentation on how to install, update and use Stardrop. 12 | 13 | ## Downloading Stardrop 14 | 15 | See the [release page](https://github.com/Floogen/Stardrop/releases/latest) for the latest builds. 16 | 17 | # Credits 18 | ## Translations 19 | Stardrop has been generously translated into several languages by the following users: 20 | 21 | * **Chinese** - guanyintu, PIut02, Z2549, CuteCat233 22 | * **French** - xynerorias 23 | * **German** - Schn1ek3 24 | * **Hungarian** - martin66789 25 | * **Italian** - S-zombie 26 | * **Portuguese** - aracnus 27 | * **Russian** - Rongarah 28 | * **Spanish** - Evexyron, Gaelhaine 29 | * **Thai** - ellipszist 30 | * **Turkish** - KediDili 31 | * **Ukrainian** - burunduk, ChulkyBow 32 | 33 | # Gallery 34 | 35 | ![](https://imgur.com/WdjwfnG.gif) 36 | 37 | ![](https://imgur.com/kalsOjS.gif) 38 | -------------------------------------------------------------------------------- /Stardrop/App.axaml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Stardrop/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | using Avalonia.Styling; 5 | using Stardrop.Models.Nexus.Web; 6 | using Stardrop.Utilities; 7 | using Stardrop.Utilities.External; 8 | using Stardrop.Views; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Linq; 13 | 14 | namespace Stardrop 15 | { 16 | public class App : Application 17 | { 18 | public override void Initialize() 19 | { 20 | AvaloniaXamlLoader.Load(this); 21 | 22 | // Verify that the helper is instantiated, if it isn't then this code is likely reached by Avalonia's previewer and bypassed Main 23 | if (Program.helper is null) 24 | { 25 | Program.helper = new Helper(); 26 | } 27 | 28 | // Load in translations 29 | Program.translation.LoadTranslations(); 30 | 31 | // Handle adding the themes 32 | Dictionary themes = new Dictionary(); 33 | foreach (string fileFullName in Directory.EnumerateFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Themes"), "*.xaml")) 34 | { 35 | try 36 | { 37 | var themeName = Path.GetFileNameWithoutExtension(fileFullName); 38 | themes[themeName] = AvaloniaRuntimeXamlLoader.Parse(File.ReadAllText(fileFullName)); 39 | Program.helper.Log($"Loaded theme {Path.GetFileNameWithoutExtension(fileFullName)}", Helper.Status.Debug); 40 | } 41 | catch (Exception ex) 42 | { 43 | Program.helper.Log($"Unable to load theme on {Path.GetFileNameWithoutExtension(fileFullName)}: {ex}", Helper.Status.Warning); 44 | } 45 | } 46 | 47 | Current.Styles.Insert(0, !themes.ContainsKey(Program.settings.Theme) ? themes.Values.First() : themes[Program.settings.Theme]); 48 | } 49 | 50 | private async void OnUrlsOpen(object? sender, UrlOpenedEventArgs e, MainWindow mainWindow) 51 | { 52 | foreach (string? url in e.Urls.Where(u => String.IsNullOrEmpty(u) is false)) 53 | { 54 | await mainWindow.ProcessNXMLink(new NXM() { Link = url, Timestamp = DateTime.Now }); 55 | } 56 | } 57 | 58 | public override void OnFrameworkInitializationCompleted() 59 | { 60 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 61 | { 62 | var mainWindow = new MainWindow(); 63 | desktop.MainWindow = mainWindow; 64 | 65 | // Register events 66 | this.UrlsOpened += (sender, e) => OnUrlsOpen(sender, e, mainWindow); 67 | } 68 | 69 | base.OnFrameworkInitializationCompleted(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Stardrop/Assets/Info.plist: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | CFBundleInfoDictionaryVersion 6 | 6.0 7 | CFBundlePackageType 8 | APPL 9 | CFBundleIconFile 10 | Stardrop 11 | 12 | CFBundleName 13 | Stardrop 14 | CFBundleExecutable 15 | Stardrop 16 | CFBundleIdentifier 17 | stardrop 18 | CFBundleVersion 19 | 1.0.0 20 | CFBundleShortVersionString 21 | 1.0 22 | CFBundleURLTypes 23 | 24 | 25 | CFBundleURLName 26 | NXM 27 | CFBundleURLSchemes 28 | 29 | nxm 30 | 31 | 32 | 33 | NSHighResolutionCapable 34 | true 35 | 36 | -------------------------------------------------------------------------------- /Stardrop/Assets/Stardrop.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floogen/Stardrop/60add2192418102381bdaf615be5465c96ebb23a/Stardrop/Assets/Stardrop.icns -------------------------------------------------------------------------------- /Stardrop/Assets/Stardrop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | chmod u+x ./Internal 3 | ./Internal 4 | -------------------------------------------------------------------------------- /Stardrop/Assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floogen/Stardrop/60add2192418102381bdaf615be5465c96ebb23a/Stardrop/Assets/icon.ico -------------------------------------------------------------------------------- /Stardrop/Assets/smapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Floogen/Stardrop/60add2192418102381bdaf615be5465c96ebb23a/Stardrop/Assets/smapi.png -------------------------------------------------------------------------------- /Stardrop/Converters/EnumConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data.Converters; 2 | using System; 3 | using System.Globalization; 4 | 5 | namespace Stardrop.Converters 6 | { 7 | public class EnumConverter : IValueConverter 8 | { 9 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 10 | { 11 | return Enum.GetName((value.GetType()), value); 12 | } 13 | 14 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 15 | { 16 | throw new NotImplementedException(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Stardrop/Converters/EnumEqualsConverter.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data.Converters; 2 | using System; 3 | using System.Globalization; 4 | 5 | namespace Stardrop.Converters 6 | { 7 | public class EnumEqualsConverter : IValueConverter 8 | { 9 | public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) 10 | { 11 | if (value?.GetType()?.IsEnum is not true) 12 | { 13 | throw new ArgumentOutOfRangeException(nameof(value), "Value must be a non-null enum."); 14 | } 15 | if (parameter?.GetType()?.IsEnum is not true) 16 | { 17 | throw new ArgumentOutOfRangeException(nameof(parameter), "Parameter must be a non-null enum."); 18 | } 19 | 20 | return Enum.Equals(value, parameter); 21 | } 22 | 23 | public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) 24 | { 25 | throw new NotImplementedException(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Stardrop/Models/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Stardrop.Models 4 | { 5 | public class Config 6 | { 7 | public string UniqueId { get; set; } 8 | public string FilePath { get; set; } 9 | public DateTime LastWriteTimeUtc { get; set; } 10 | public string Data { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/ClientData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Stardrop.Models.Data 4 | { 5 | public class ClientData 6 | { 7 | public List ModInstallData { get; set; } 8 | public Dictionary ColumnActiveStates { get; set; } = new Dictionary(); 9 | public LastSessionData LastSessionData { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/Enums/Choice.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.Data.Enums 2 | { 3 | public enum Choice 4 | { 5 | First, 6 | Second, 7 | Third 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/Enums/DisplayFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.Data.Enums 2 | { 3 | public enum DisplayFilter 4 | { 5 | None, 6 | ShowEnabled, 7 | ShowDisabled, 8 | RequireConfig 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/Enums/EndorsementResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.Data.Enums 2 | { 3 | public enum InstallState 4 | { 5 | Unknown, 6 | Downloading, 7 | Installing 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/Enums/InstallState.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.Data.Enums 2 | { 3 | public enum EndorsementResponse 4 | { 5 | Unknown, 6 | IsOwnMod, 7 | TooSoonAfterDownload, 8 | NotDownloadedMod, 9 | Abstained, 10 | Endorsed 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/Enums/ModGrouping.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Stardrop.Models.Data.Enums 4 | { 5 | public enum ModGrouping 6 | { 7 | None, 8 | Folder, 9 | [Description("Content Pack")] 10 | ContentPack 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/Enums/NexusServers.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Stardrop.Models.Data.Enums 4 | { 5 | public enum NexusServers 6 | { 7 | [Description("Nexus CDN")] 8 | NexusCDN, 9 | Chicago, 10 | Paris, 11 | Amsterdam, 12 | Prague, 13 | [Description("Los Angeles")] 14 | LosAngeles, 15 | Miami, 16 | Singapore 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/LastSessionData.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using System; 3 | 4 | namespace Stardrop.Models.Data 5 | { 6 | public class LastSessionData 7 | { 8 | public double Height { get; set; } = 800; 9 | public double Width { get; set; } = 1430; 10 | public int PositionX { get; set; } 11 | public int PositionY { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/ModDownloadEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Stardrop.Models.Data 5 | { 6 | internal record ModDownloadStartedEventArgs(Uri Uri, string Name, long? Size, CancellationTokenSource DownloadCancellationSource); 7 | internal record ModDownloadProgressEventArgs(Uri Uri, long TotalBytes); 8 | internal record ModDownloadCompletedEventArgs(Uri Uri); 9 | internal record ModDownloadFailedEventArgs(Uri Uri); 10 | } 11 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/ModInstallData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Stardrop.Models.Data 4 | { 5 | public class ModInstallData 6 | { 7 | public string UniqueId { get; set; } 8 | public DateTime InstallTimestamp { get; set; } 9 | public DateTime? LastUpdateTimestamp { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/ModKeyInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.Data 2 | { 3 | public class ModKeyInfo 4 | { 5 | public string UniqueId { get; set; } 6 | public string Name { get; set; } 7 | public string PageUrl { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/ModUpdateInfo.cs: -------------------------------------------------------------------------------- 1 | using static Stardrop.Models.SMAPI.Web.ModEntryMetadata; 2 | 3 | namespace Stardrop.Models.Data 4 | { 5 | public class ModUpdateInfo 6 | { 7 | public string UniqueId { get; set; } 8 | public string SuggestedVersion { get; set; } 9 | public WikiCompatibilityStatus Status { get; set; } 10 | public string Link { get; set; } 11 | 12 | 13 | public ModUpdateInfo() 14 | { 15 | 16 | } 17 | 18 | public ModUpdateInfo(string uniqueId, string recommendedVersion, WikiCompatibilityStatus status, string link) 19 | { 20 | UniqueId = uniqueId; 21 | SuggestedVersion = recommendedVersion; 22 | Status = status; 23 | Link = link; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/PairedKeys.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.Data 2 | { 3 | public class PairedKeys 4 | { 5 | public byte[] Lock { get; set; } 6 | public byte[] Vector { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Stardrop/Models/Data/UpdateCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Stardrop.Models.Data 5 | { 6 | public class UpdateCache 7 | { 8 | public DateTime LastRuntime { get; set; } 9 | public List Mods { get; set; } 10 | 11 | public UpdateCache(DateTime lastRuntime) 12 | { 13 | LastRuntime = lastRuntime; 14 | Mods = new List(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/NexusUser.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.Nexus 2 | { 3 | public class NexusUser 4 | { 5 | public string Username { get; set; } 6 | public bool IsPremium { get; set; } 7 | 8 | public byte[] Key { get; set; } 9 | 10 | public NexusUser() 11 | { 12 | 13 | } 14 | 15 | public NexusUser(string username, byte[] key) 16 | { 17 | Username = username; 18 | Key = key; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/Web/DownloadLink.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Stardrop.Models.Nexus.Web 4 | { 5 | public class DownloadLink 6 | { 7 | [JsonPropertyName("name")] 8 | public string? Name { get; set; } 9 | 10 | [JsonPropertyName("short_name")] 11 | public string? ShortName { get; set; } 12 | 13 | [JsonPropertyName("URI")] 14 | public string? Uri { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/Web/Endorsement.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Stardrop.Models.Nexus.Web 4 | { 5 | public class EndorsementResult 6 | { 7 | [JsonPropertyName("message")] 8 | public string? Message { get; set; } 9 | 10 | [JsonPropertyName("status")] 11 | public string? Status { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/Web/EndorsementResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Stardrop.Models.Nexus.Web 4 | { 5 | public class Endorsement 6 | { 7 | [JsonPropertyName("mod_id")] 8 | public int Id { get; set; } 9 | 10 | [JsonPropertyName("domain_name")] 11 | public string? DomainName { get; set; } 12 | 13 | [JsonPropertyName("status")] 14 | public string? Status { get; set; } 15 | 16 | public bool IsEndorsed() 17 | { 18 | if (Status?.ToUpper() == "ENDORSED") 19 | { 20 | return true; 21 | } 22 | 23 | return false; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/Web/ModDetails.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Stardrop.Models.Nexus.Web 4 | { 5 | public class ModDetails 6 | { 7 | [JsonPropertyName("name")] 8 | public string? Name { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/Web/ModFile.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Stardrop.Models.Nexus.Web 4 | { 5 | public class ModFile 6 | { 7 | [JsonPropertyName("file_id")] 8 | public int Id { get; set; } 9 | 10 | [JsonPropertyName("file_name")] 11 | public string? Name { get; set; } 12 | 13 | [JsonPropertyName("description")] 14 | public string? Description { get; set; } 15 | 16 | [JsonPropertyName("version")] 17 | public string? Version { get; set; } 18 | 19 | [JsonPropertyName("category_name")] 20 | public string? Category { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/Web/ModFiles.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Stardrop.Models.Nexus.Web 5 | { 6 | public class ModFiles 7 | { 8 | [JsonPropertyName("files")] 9 | public List Files { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/Web/NXM.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Stardrop.Models.Nexus.Web 4 | { 5 | public class NXM 6 | { 7 | public string? Link { get; set; } 8 | public DateTime Timestamp { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Stardrop/Models/Nexus/Web/Validate.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Stardrop.Models.Nexus.Web 4 | { 5 | public class Validate 6 | { 7 | [JsonPropertyName("name")] 8 | public string Name { get; set; } 9 | 10 | [JsonPropertyName("is_premium")] 11 | public bool IsPremium { get; set; } 12 | 13 | [JsonPropertyName("profile_url")] 14 | public string ProfileUrl { get; set; } 15 | 16 | [JsonPropertyName("message")] 17 | public string Message { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Stardrop/Models/Profile.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json; 3 | 4 | namespace Stardrop.Models 5 | { 6 | public class Profile 7 | { 8 | public string Name { get; set; } 9 | public bool IsProtected { get; set; } 10 | public List EnabledModIds { get; set; } 11 | public Dictionary PreservedModConfigs { get; set; } 12 | 13 | public Profile() 14 | { 15 | Name = "Unknown"; 16 | IsProtected = false; 17 | EnabledModIds = new List(); 18 | PreservedModConfigs = new Dictionary(); 19 | } 20 | 21 | public Profile(string name, bool isProtected = false, List? enabledMods = null, Dictionary? preservedModConfigs = null) 22 | { 23 | Name = name; 24 | IsProtected = isProtected; 25 | EnabledModIds = enabledMods is null ? new List() : enabledMods; 26 | PreservedModConfigs = preservedModConfigs is null ? new Dictionary() : preservedModConfigs; 27 | } 28 | 29 | public Profile ShallowCopy() 30 | { 31 | return (Profile)this.MemberwiseClone(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Converters/BooleanConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Stardrop.Models.SMAPI.Converters 6 | { 7 | internal class BooleanConverter : JsonConverter 8 | { 9 | public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False) 12 | { 13 | return reader.GetBoolean(); 14 | } 15 | 16 | string value = reader.GetString(); 17 | if (Boolean.TryParse(value, out bool result)) 18 | { 19 | return result; 20 | } 21 | 22 | return false; 23 | } 24 | 25 | public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) 26 | { 27 | throw new InvalidOperationException("This converter should not be used to write, it is read only."); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Converters/BooleanConverterAssumeTrue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Stardrop.Models.SMAPI.Converters 6 | { 7 | internal class BooleanConverterAssumeTrue : JsonConverter 8 | { 9 | public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False) 12 | { 13 | return reader.GetBoolean(); 14 | } 15 | 16 | string value = reader.GetString(); 17 | if (Boolean.TryParse(value, out bool result)) 18 | { 19 | return result; 20 | } 21 | 22 | return true; 23 | } 24 | 25 | public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) 26 | { 27 | throw new InvalidOperationException("This converter should not be used to write, it is read only."); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Converters/ModKeyConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Stardrop.Models.SMAPI.Converters 7 | { 8 | internal class ModKeyConverter : JsonConverter 9 | { 10 | public override string[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | if (reader.TokenType != JsonTokenType.StartArray) 13 | { 14 | throw new JsonException(); 15 | } 16 | 17 | var modKeys = new List(); 18 | while (reader.Read()) 19 | { 20 | if (reader.TokenType == JsonTokenType.EndArray) 21 | { 22 | return modKeys.ToArray(); 23 | } 24 | 25 | if (reader.TokenType == JsonTokenType.Number) 26 | { 27 | modKeys.Add($"Nexus: {reader.GetInt32()}"); 28 | } 29 | else 30 | { 31 | modKeys.Add(reader.GetString()); 32 | } 33 | } 34 | 35 | // Should not reach here, due to reader.TokenType == JsonTokenType.EndArray 36 | throw new JsonException(); 37 | } 38 | 39 | public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options) 40 | { 41 | throw new InvalidOperationException("This converter should not be used to write, it is read only."); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/GameDetails.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | using System; 3 | 4 | namespace Stardrop.Models.SMAPI 5 | { 6 | public class GameDetails 7 | { 8 | public enum OS 9 | { 10 | Unknown, 11 | Linux, 12 | Mac, 13 | Windows 14 | } 15 | 16 | /// Stardew Valley's game version. 17 | public string GameVersion { get; set; } 18 | 19 | /// SMAPI's version. 20 | public string SmapiVersion { get; set; } 21 | 22 | /// The operating system. 23 | public OS System { get; set; } 24 | 25 | public GameDetails() 26 | { 27 | 28 | } 29 | 30 | public GameDetails(string gameVersion, string smapiVersion, string system) 31 | { 32 | GameVersion = gameVersion; 33 | if (GameVersion.Contains(' ')) 34 | { 35 | GameVersion = GameVersion.Split(' ')[0]; 36 | } 37 | SmapiVersion = smapiVersion; 38 | 39 | if (system.Contains("macOS", StringComparison.OrdinalIgnoreCase)) 40 | { 41 | System = OS.Mac; 42 | } 43 | else if (system.Contains("Windows", StringComparison.OrdinalIgnoreCase)) 44 | { 45 | System = OS.Windows; 46 | } 47 | else 48 | { 49 | System = OS.Linux; 50 | } 51 | } 52 | 53 | public bool HasSMAPIUpdated(string version) 54 | { 55 | if (String.IsNullOrEmpty(version)) 56 | { 57 | return false; 58 | } 59 | 60 | return HasSMAPIUpdated(SemVersion.Parse(version)); 61 | } 62 | 63 | public bool HasSMAPIUpdated(SemVersion version) 64 | { 65 | if (version is null) 66 | { 67 | return false; 68 | } 69 | 70 | return version != SemVersion.Parse(SmapiVersion); 71 | } 72 | 73 | public bool HasBadGameVersion() 74 | { 75 | if (GameVersion.Contains(' ')) 76 | { 77 | return true; 78 | } 79 | 80 | return false; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Manifest.cs: -------------------------------------------------------------------------------- 1 | using Stardrop.Models.SMAPI.Converters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Stardrop.Models.SMAPI 5 | { 6 | public class Manifest 7 | { 8 | // Based on SMAPI's Manfiest.cs: https://github.com/Pathoschild/SMAPI/blob/c10685b03574e967c1bf48aafc814f60196812ec/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs 9 | 10 | /// The mod name. 11 | public string Name { get; set; } 12 | 13 | /// A brief description of the mod. 14 | public string Description { get; set; } 15 | 16 | /// The namespaced mod IDs to query for updates (like Nexus:541). 17 | [JsonConverter(typeof(ModKeyConverter))] 18 | public string[] UpdateKeys { get; set; } 19 | 20 | /// The mod author's name. 21 | public string Author { get; set; } 22 | 23 | /// The mod version. 24 | public string Version { get; set; } 25 | 26 | /// The unique mod ID. 27 | public string UniqueID { get; set; } 28 | 29 | /// The mod which will read this as a content pack. Mutually exclusive with . 30 | //[JsonConverter(typeof(ManifestContentPackForConverter))] 31 | public ManifestContentPackFor ContentPackFor { get; set; } 32 | 33 | /// The other mods that must be loaded before this mod. 34 | //[JsonConverter(typeof(ManifestDependencyArrayConverter))] 35 | public ManifestDependency[] Dependencies { get; set; } 36 | 37 | // Custom property for Stardrop. 38 | public bool DeleteOldVersion { get; set; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/ManifestContentPackFor.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.SMAPI 2 | { 3 | public class ManifestContentPackFor 4 | { 5 | // Based on SMAPI's IManifestContentPackFor.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs 6 | 7 | /// The unique ID of the mod which can read this content pack. 8 | public string UniqueID { get; set; } 9 | 10 | /// The minimum required version (if any). 11 | public string MinimumVersion { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/ManifestDependency.cs: -------------------------------------------------------------------------------- 1 | using Stardrop.Models.SMAPI.Converters; 2 | using System.ComponentModel; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Stardrop.Models.SMAPI 6 | { 7 | public class ManifestDependency : INotifyPropertyChanged 8 | { 9 | // Based on SMAPI's IManifestDependency.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit.CoreInterfaces/IManifestDependency.cs 10 | 11 | /// The unique mod ID to require. 12 | public string UniqueID { get; set; } 13 | 14 | /// The minimum required version (if any). 15 | public string MinimumVersion { get; set; } 16 | 17 | /// Whether the dependency must be installed to use the mod. 18 | [JsonConverter(typeof(BooleanConverterAssumeTrue))] 19 | public bool IsRequired { get; set; } 20 | 21 | // Custom properties for Stardrop. 22 | private string _name { get; set; } 23 | public string Name { get { return _name; } set { _name = value; NotifyPropertyChanged("Name"); NotifyPropertyChanged("GenericLink"); } } 24 | public bool IsMissing { get; set; } 25 | public string GenericLink { get { return $"https://smapi.io/mods#{Name.Replace(" ", "_")}"; } } 26 | 27 | public event PropertyChangedEventHandler? PropertyChanged; 28 | public ManifestDependency(string uniqueId, string minimumVersion, bool isRequired = true) 29 | { 30 | UniqueID = uniqueId; 31 | MinimumVersion = minimumVersion; 32 | IsRequired = isRequired; 33 | } 34 | 35 | private void NotifyPropertyChanged(string propertyName) 36 | { 37 | var handler = PropertyChanged; 38 | if (handler != null) 39 | handler(this, new PropertyChangedEventArgs(propertyName)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Web/ModEntry.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.SMAPI.Web 2 | { 3 | public class ModEntry 4 | { 5 | // Based on SMAPI's ModEntryModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs 6 | 7 | /// The mod's unique ID. 8 | public string Id { get; set; } 9 | 10 | /// The update version recommended by the web API based on its version update and mapping rules. 11 | public ModEntryVersion SuggestedUpdate { get; set; } 12 | 13 | /// Optional extended data which isn't needed for update checks. 14 | public ModEntryMetadata Metadata { get; set; } 15 | 16 | /// The errors that occurred while fetching update data. 17 | public string[] Errors { get; set; } = new string[0]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Web/ModEntryMetadata.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Stardrop.Models.SMAPI.Web 4 | { 5 | public class ModEntryMetadata 6 | { 7 | // Based on SMAPI's WikiCompatibilityStatus.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs 8 | /// The compatibility status for a mod. 9 | public enum WikiCompatibilityStatus 10 | { 11 | /// The status is unknown. 12 | Unknown, 13 | 14 | /// The mod is compatible. 15 | Ok, 16 | 17 | /// The mod is compatible if you use an optional official download. 18 | Optional, 19 | 20 | /// The mod is compatible if you use an unofficial update. 21 | Unofficial, 22 | 23 | /// The mod isn't compatible, but the player can fix it or there's a good alternative. 24 | Workaround, 25 | 26 | /// The mod isn't compatible. 27 | Broken, 28 | 29 | /// The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely. 30 | Abandoned, 31 | 32 | /// The mod is no longer needed and should be removed. 33 | Obsolete 34 | } 35 | 36 | // Based on SMAPI's ModExtendedMetadataModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs 37 | /// The mod's display name. 38 | public string Name { get; set; } 39 | 40 | /// The main version. 41 | public ModEntryVersion Main { get; set; } 42 | 43 | /// The latest unofficial version, if newer than and . 44 | public ModEntryVersion Unofficial { get; set; } 45 | public string CustomUrl { get; set; } 46 | 47 | [JsonConverter(typeof(JsonStringEnumConverter))] 48 | public WikiCompatibilityStatus CompatibilityStatus { get; set; } 49 | 50 | /// The human-readable summary of the compatibility status or workaround, without HTML formatting. 51 | public string CompatibilitySummary { get; set; } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Web/ModEntryVersion.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Models.SMAPI.Web 2 | { 3 | public class ModEntryVersion 4 | { 5 | // Based on SMAPI's ModEntryVersionModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs 6 | 7 | /********* 8 | ** Accessors 9 | *********/ 10 | /// The version number. 11 | public string Version { get; set; } 12 | 13 | /// The mod page URL. 14 | public string Url { get; set; } 15 | 16 | 17 | /********* 18 | ** Public methods 19 | *********/ 20 | /// Construct an instance. 21 | public ModEntryVersion() { } 22 | 23 | /// Construct an instance. 24 | /// The version number. 25 | /// The mod page URL. 26 | public ModEntryVersion(string version, string url) 27 | { 28 | this.Version = version; 29 | this.Url = url; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Web/ModSearchData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Stardrop.Models.SMAPI.Web 4 | { 5 | class ModSearchData 6 | { 7 | // Based on SMAPI's ModSearchModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs 8 | 9 | /********* 10 | ** Accessors 11 | *********/ 12 | /// The mods for which to find data. 13 | public ModSearchEntry[] Mods { get; set; } 14 | 15 | /// Whether to include extended metadata for each mod. 16 | public bool IncludeExtendedMetadata { get; set; } 17 | 18 | /// The SMAPI version installed by the player. This is used for version mapping in some cases. 19 | public string ApiVersion { get; set; } 20 | 21 | /// The Stardew Valley version installed by the player. 22 | public string GameVersion { get; set; } 23 | 24 | /// The OS on which the player plays. 25 | public string Platform { get; set; } 26 | 27 | 28 | /********* 29 | ** Public methods 30 | *********/ 31 | /// Construct an empty instance. 32 | public ModSearchData() 33 | { 34 | // needed for JSON deserializing 35 | } 36 | 37 | /// Construct an instance. 38 | /// The mods to search. 39 | /// The SMAPI version installed by the player. If this is null, the API won't provide a recommended update. 40 | /// The Stardew Valley version installed by the player. 41 | /// The OS on which the player plays. 42 | /// Whether to include extended metadata for each mod. 43 | public ModSearchData(List mods, string apiVersion, string gameVersion, string platform, bool includeExtendedMetadata) 44 | { 45 | this.Mods = mods.ToArray(); 46 | this.ApiVersion = apiVersion.ToString(); 47 | this.GameVersion = gameVersion.ToString(); 48 | this.Platform = platform; 49 | this.IncludeExtendedMetadata = includeExtendedMetadata; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Stardrop/Models/SMAPI/Web/ModSearchEntry.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | 3 | namespace Stardrop.Models.SMAPI.Web 4 | { 5 | public class ModSearchEntry 6 | { 7 | // Based on SMAPI's ModSearchEntryModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs 8 | 9 | /********* 10 | ** Accessors 11 | *********/ 12 | /// The unique mod ID. 13 | public string Id { get; set; } 14 | 15 | /// The namespaced mod update keys (if available). 16 | public string[] UpdateKeys { get; set; } 17 | 18 | /// The mod version installed by the local player. This is used for version mapping in some cases. 19 | public string InstalledVersion { get; set; } 20 | 21 | 22 | /********* 23 | ** Public methods 24 | *********/ 25 | /// Construct an empty instance. 26 | public ModSearchEntry() 27 | { 28 | // needed for JSON deserializing 29 | } 30 | 31 | /// Construct an instance. 32 | /// The unique mod ID. 33 | /// The version installed by the local player. This is used for version mapping in some cases. 34 | /// The namespaced mod update keys (if available). 35 | /// Whether the installed version is broken or could not be loaded. 36 | public ModSearchEntry(string id, SemVersion installedVersion, string[] updateKeys, bool isBroken = false) 37 | { 38 | this.Id = id; 39 | this.InstalledVersion = installedVersion.ToString(); 40 | this.UpdateKeys = updateKeys ?? new string[0]; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Stardrop/Models/Settings.cs: -------------------------------------------------------------------------------- 1 | using Stardrop.Models.Data.Enums; 2 | using Stardrop.Models.Nexus; 3 | using Stardrop.Models.SMAPI; 4 | 5 | namespace Stardrop.Models 6 | { 7 | public class Settings 8 | { 9 | public string Theme { get; set; } = "Stardrop"; 10 | public string Language { get; set; } 11 | public ModGrouping ModGroupingMethod { get; set; } = ModGrouping.None; 12 | public string Version { get; set; } 13 | public string LastSelectedProfileName { get; set; } 14 | public string SMAPIFolderPath { get; set; } 15 | public string ModFolderPath { get; set; } 16 | public string ModInstallPath { get; set; } 17 | public bool IgnoreHiddenFolders { get; set; } = true; 18 | public bool EnableProfileSpecificModConfigs { get; set; } 19 | public bool ShouldWriteToModConfigs { get; set; } 20 | public bool EnableModsOnAdd { get; set; } 21 | /// 22 | /// Whether to always ask before deleting a previous version of a mod when updating the mod. 23 | /// 24 | public bool AlwaysAskToDelete { get; set; } = true; 25 | public bool ShouldAutomaticallySaveProfileChanges { get; set; } = true; 26 | public NexusServers PreferredNexusServer { get; set; } = NexusServers.NexusCDN; 27 | public bool IsAskingBeforeAcceptingNXM { get; set; } = true; 28 | public GameDetails GameDetails { get; set; } 29 | public NexusUser NexusDetails { get; set; } 30 | 31 | public Settings ShallowCopy() 32 | { 33 | return (Settings)this.MemberwiseClone(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Stardrop/Properties/PublishProfiles/FolderProfile - Linux.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | publish\linux 10 | FileSystem 11 | net7.0 12 | linux-x64 13 | true 14 | true 15 | 16 | -------------------------------------------------------------------------------- /Stardrop/Properties/PublishProfiles/FolderProfile - MacOS.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | publish\mac 10 | FileSystem 11 | net7.0 12 | osx-x64 13 | true 14 | true 15 | 16 | -------------------------------------------------------------------------------- /Stardrop/Properties/PublishProfiles/FolderProfile - Windows.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | publish\windows 10 | FileSystem 11 | net7.0 12 | win-x64 13 | true 14 | true 15 | false 16 | 17 | -------------------------------------------------------------------------------- /Stardrop/Stardrop.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | WinExe 4 | net7.0 5 | enable 6 | 1.2.1 7 | false 8 | true 9 | Assets\icon.ico 10 | 11 | 12 | 13 | x64 14 | 15 | 16 | x64 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | FlexibleOptionWindow.axaml 51 | 52 | 53 | NexusInfo.axaml 54 | 55 | 56 | ProfileNaming.axaml 57 | 58 | 59 | ProfileEditor.axaml 60 | 61 | 62 | WarningWindow.axaml 63 | 64 | 65 | DownloadPanel.axaml 66 | 67 | 68 | 69 | 70 | Designer 71 | MSBuild:Compile 72 | 73 | 74 | Designer 75 | MSBuild:Compile 76 | 77 | 78 | Designer 79 | MSBuild:Compile 80 | 81 | 82 | MSBuild:Compile 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /Stardrop/Stardrop.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31729.503 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stardrop", "Stardrop.csproj", "{68543B63-0EB4-43E8-9B7B-7AFA64097CAF}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {68543B63-0EB4-43E8-9B7B-7AFA64097CAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {68543B63-0EB4-43E8-9B7B-7AFA64097CAF}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {68543B63-0EB4-43E8-9B7B-7AFA64097CAF}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {68543B63-0EB4-43E8-9B7B-7AFA64097CAF}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {86B7436F-BA81-4F86-B816-DB4AD0E2C918} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Stardrop/Themes/Dark.xaml: -------------------------------------------------------------------------------- 1 |  5 | 60 | 61 | -------------------------------------------------------------------------------- /Stardrop/Themes/Light.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 62 | 63 | -------------------------------------------------------------------------------- /Stardrop/Themes/Solarized-Lite.xaml: -------------------------------------------------------------------------------- 1 |  5 | 59 | 60 | -------------------------------------------------------------------------------- /Stardrop/Themes/Stardrop.xaml: -------------------------------------------------------------------------------- 1 |  5 | 59 | 60 | -------------------------------------------------------------------------------- /Stardrop/Utilities/Extension/TranslateExtension.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Data; 2 | using Avalonia.Markup.Xaml; 3 | using Avalonia.Markup.Xaml.MarkupExtensions; 4 | using System; 5 | 6 | 7 | namespace Stardrop.Utilities.Extension 8 | { 9 | public class TranslateExtension : MarkupExtension 10 | { 11 | public TranslateExtension(string key) 12 | { 13 | this.Key = key; 14 | } 15 | 16 | public string Key { get; set; } 17 | 18 | public string Context { get; set; } 19 | 20 | public override object ProvideValue(IServiceProvider serviceProvider) 21 | { 22 | var keyToUse = Key; 23 | if (!string.IsNullOrWhiteSpace(Context)) 24 | { 25 | keyToUse = $"{Context}/{Key}"; 26 | } 27 | 28 | var binding = new ReflectionBindingExtension($"[{keyToUse}]") 29 | { 30 | Mode = BindingMode.OneWay, 31 | Source = Program.translation, 32 | }; 33 | 34 | return binding.ProvideValue(serviceProvider); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Stardrop/Utilities/External/GitHub.cs: -------------------------------------------------------------------------------- 1 | using SharpCompress.Archives; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Runtime.InteropServices; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 10 | 11 | namespace Stardrop.Utilities.External 12 | { 13 | static class GitHub 14 | { 15 | public async static Task?> GetLatestSMAPIRelease() 16 | { 17 | KeyValuePair? versionToUri = null; 18 | 19 | // Create a throwaway client 20 | HttpClient client = new HttpClient(); 21 | client.DefaultRequestHeaders.Add("User-Agent", "Stardrop - SDV Mod Manager"); 22 | 23 | try 24 | { 25 | var response = await client.GetAsync("https://api.github.com/repos/Pathoschild/SMAPI/releases/latest"); 26 | 27 | if (response.Content is not null) 28 | { 29 | JsonDocument parsedContent = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); 30 | string tagName = parsedContent.RootElement.GetProperty("tag_name").ToString(); 31 | string downloadUri = parsedContent.RootElement.GetProperty("html_url").ToString(); 32 | downloadUri = String.Concat(downloadUri, "/", $"SMAPI-{tagName}-installer.zip").Replace("releases/tag/", "releases/download/"); 33 | 34 | versionToUri = new KeyValuePair(tagName, downloadUri); 35 | } 36 | } 37 | catch (Exception ex) 38 | { 39 | Program.helper.Log($"Failed to get latest the version of SMAPI: {ex}", Helper.Status.Alert); 40 | } 41 | client.Dispose(); 42 | 43 | return versionToUri; 44 | } 45 | 46 | public async static Task DownloadLatestSMAPIRelease(string uri) 47 | { 48 | // Create a throwaway client 49 | HttpClient client = new HttpClient(); 50 | client.DefaultRequestHeaders.Add("User-Agent", "Stardrop - SDV Mod Manager"); 51 | 52 | string downloadedArchivePath = String.Empty; 53 | try 54 | { 55 | var response = await client.GetAsync(uri); 56 | using (var archive = ArchiveFactory.Open(await response.Content.ReadAsStreamAsync())) 57 | { 58 | downloadedArchivePath = Path.Combine(Pathing.GetSmapiUpgradeFolderPath(), Path.GetDirectoryName(archive.Entries.First().Key)); 59 | foreach (var entry in archive.Entries) 60 | { 61 | entry.WriteToDirectory(Pathing.GetSmapiUpgradeFolderPath(), new SharpCompress.Common.ExtractionOptions() { ExtractFullPath = true, Overwrite = true }); 62 | } 63 | } 64 | } 65 | catch (Exception ex) 66 | { 67 | Program.helper.Log($"Failed to download latest the version of SMAPI: {ex}", Helper.Status.Alert); 68 | } 69 | client.Dispose(); 70 | 71 | return downloadedArchivePath; 72 | } 73 | 74 | public async static Task?> GetLatestStardropRelease() 75 | { 76 | KeyValuePair? versionToUri = null; 77 | 78 | // Create a throwaway client 79 | HttpClient client = new HttpClient(); 80 | client.DefaultRequestHeaders.Add("User-Agent", "Stardrop - SDV Mod Manager"); 81 | 82 | try 83 | { 84 | var response = await client.GetAsync("https://api.github.com/repos/Floogen/Stardrop/releases/latest"); 85 | 86 | if (response.Content is not null) 87 | { 88 | JsonDocument parsedContent = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); 89 | string tagName = parsedContent.RootElement.GetProperty("tag_name").ToString(); 90 | string downloadUri = parsedContent.RootElement.GetProperty("html_url").ToString(); 91 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 92 | { 93 | downloadUri = String.Concat(downloadUri, "/", "Stardrop-osx-x64.zip"); 94 | } 95 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 96 | { 97 | downloadUri = String.Concat(downloadUri, "/", "Stardrop-linux-x64.zip"); 98 | } 99 | else 100 | { 101 | downloadUri = String.Concat(downloadUri, "/", "Stardrop-win-x64.zip"); 102 | } 103 | downloadUri = downloadUri.Replace("releases/tag/", "releases/download/"); 104 | 105 | versionToUri = new KeyValuePair(tagName, downloadUri); 106 | } 107 | } 108 | catch (Exception ex) 109 | { 110 | Program.helper.Log($"Failed to get latest the version of Stardrop: {ex}", Helper.Status.Alert); 111 | } 112 | client.Dispose(); 113 | 114 | return versionToUri; 115 | } 116 | 117 | public async static Task DownloadLatestStardropRelease(string uri) 118 | { 119 | // Create a throwaway client 120 | HttpClient client = new HttpClient(); 121 | client.DefaultRequestHeaders.Add("User-Agent", "Stardrop - SDV Mod Manager"); 122 | 123 | string downloadedArchivePath = String.Empty; 124 | try 125 | { 126 | var response = await client.GetAsync(uri); 127 | using (var archive = ArchiveFactory.Open(await response.Content.ReadAsStreamAsync())) 128 | { 129 | foreach (var entry in archive.Entries) 130 | { 131 | entry.WriteToDirectory(Directory.GetCurrentDirectory(), new SharpCompress.Common.ExtractionOptions() { ExtractFullPath = true, Overwrite = true }); 132 | } 133 | } 134 | 135 | var extractFolderName = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Stardrop.app" : "Stardrop"; 136 | var adjustedExtractFolderName = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "~Stardrop.app" : "~Stardrop"; 137 | if (Directory.Exists(extractFolderName)) 138 | { 139 | if (Directory.Exists(adjustedExtractFolderName)) 140 | { 141 | Directory.Delete(adjustedExtractFolderName, true); 142 | } 143 | Directory.Move(extractFolderName, adjustedExtractFolderName); 144 | } 145 | downloadedArchivePath = Path.Combine(Directory.GetCurrentDirectory(), adjustedExtractFolderName); 146 | } 147 | catch (Exception ex) 148 | { 149 | Program.helper.Log($"Failed to download latest the version of Stardrop: {ex}", Helper.Status.Alert); 150 | } 151 | client.Dispose(); 152 | 153 | return downloadedArchivePath; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Stardrop/Utilities/External/NexusDownloadResult.cs: -------------------------------------------------------------------------------- 1 | namespace Stardrop.Utilities.External 2 | { 3 | public enum DownloadResultKind 4 | { 5 | Failed, 6 | UserCanceled, 7 | Success 8 | } 9 | 10 | public record struct NexusDownloadResult(DownloadResultKind ResultKind, string? DownloadedModFilePath); 11 | } 12 | -------------------------------------------------------------------------------- /Stardrop/Utilities/External/SMAPI.cs: -------------------------------------------------------------------------------- 1 | using Semver; 2 | using Stardrop.Models; 3 | using Stardrop.Models.SMAPI; 4 | using Stardrop.Models.SMAPI.Web; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Net.Http; 11 | using System.Reflection; 12 | using System.Runtime.InteropServices; 13 | using System.Text; 14 | using System.Text.Json; 15 | using System.Threading.Tasks; 16 | 17 | namespace Stardrop.Utilities.External 18 | { 19 | static class SMAPI 20 | { 21 | internal static bool IsRunning = false; 22 | internal static Process Process; 23 | 24 | public static ProcessStartInfo GetPrepareProcess(bool hideConsole) 25 | { 26 | var arguments = String.Empty; 27 | var smapiInfo = new FileInfo(Pathing.GetSmapiPath()); 28 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) is true) 29 | { 30 | arguments = $"-c \"SMAPI_MODS_PATH='{Pathing.GetSelectedModsFolderPath()}' '{Pathing.GetSmapiPath().Replace("StardewModdingAPI.dll", "StardewValley")}'\""; 31 | } 32 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) is true) 33 | { 34 | arguments = $"-c \"'{Pathing.GetSmapiPath().Replace("StardewModdingAPI.dll", "StardewModdingAPI")}' --mods-path '{Pathing.GetSelectedModsFolderPath()}'\""; 35 | } 36 | 37 | Program.helper.Log($"Starting SMAPI with the following arguments: {arguments}"); 38 | var processInfo = new ProcessStartInfo 39 | { 40 | FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? smapiInfo.FullName : "/bin/bash", 41 | Arguments = arguments, 42 | WorkingDirectory = smapiInfo.DirectoryName, 43 | RedirectStandardOutput = false, 44 | RedirectStandardError = false, 45 | CreateNoWindow = hideConsole, 46 | UseShellExecute = false 47 | }; 48 | processInfo.EnvironmentVariables["SMAPI_MODS_PATH"] = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) is false ? $"'{Pathing.GetSelectedModsFolderPath()}'" : Pathing.GetSelectedModsFolderPath(); 49 | 50 | return processInfo; 51 | } 52 | 53 | public static string GetProcessName() 54 | { 55 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 56 | { 57 | return "StardewModdingA"; 58 | } 59 | 60 | return "StardewModdingAPI"; 61 | } 62 | 63 | public async static Task> GetModUpdateData(GameDetails gameDetails, List mods) 64 | { 65 | List searchEntries = new List(); 66 | foreach (var mod in mods.Where(m => m.HasValidVersion() && m.HasUpdateKeys())) 67 | { 68 | searchEntries.Add(new ModSearchEntry(mod.UniqueId, mod.Version, mod.Manifest.UpdateKeys)); 69 | } 70 | foreach (var requirementKey in mods.SelectMany(m => m.Requirements)) 71 | { 72 | if (!searchEntries.Any(e => e.Id.Equals(requirementKey.UniqueID, StringComparison.OrdinalIgnoreCase))) 73 | { 74 | searchEntries.Add(new ModSearchEntry() { Id = requirementKey.UniqueID }); 75 | } 76 | } 77 | 78 | // Create the body to be sent via the POST request 79 | ModSearchData searchData = new ModSearchData(searchEntries, gameDetails.SmapiVersion, gameDetails.GameVersion, gameDetails.System.ToString(), true); 80 | 81 | // Create a throwaway client 82 | HttpClient client = new HttpClient(); 83 | client.DefaultRequestHeaders.Add("Application-Name", "Stardrop"); 84 | client.DefaultRequestHeaders.Add("Application-Version", Program.ApplicationVersion); 85 | client.DefaultRequestHeaders.Add("User-Agent", $"Stardrop/{Program.ApplicationVersion} {Environment.OSVersion}"); 86 | 87 | var parsedRequest = JsonSerializer.Serialize(searchData, new JsonSerializerOptions() { WriteIndented = true, IgnoreNullValues = true }); 88 | var requestPackage = new StringContent(parsedRequest, Encoding.UTF8, "application/json"); 89 | var response = await client.PostAsync("https://smapi.io/api/v3.0/mods", requestPackage); 90 | 91 | List modUpdateData = new List(); 92 | if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null) 93 | { 94 | // In the name of the Nine Divines, why is JsonSerializer.Deserialize case sensitive by default??? 95 | string content = await response.Content.ReadAsStringAsync(); 96 | modUpdateData = JsonSerializer.Deserialize>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); 97 | 98 | if (modUpdateData is null || modUpdateData.Count == 0) 99 | { 100 | Program.helper.Log($"Mod update data was not parsable from smapi.io"); 101 | Program.helper.Log($"Response from smapi.io:\n{content}"); 102 | Program.helper.Log($"Our request to smapi.io:\n{parsedRequest}"); 103 | } 104 | } 105 | else 106 | { 107 | if (response.StatusCode != System.Net.HttpStatusCode.OK) 108 | { 109 | Program.helper.Log($"Bad status given from smapi.io: {response.StatusCode}"); 110 | if (response.Content is not null) 111 | { 112 | Program.helper.Log($"Response from smapi.io:\n{await response.Content.ReadAsStringAsync()}"); 113 | } 114 | } 115 | else if (response.Content is null) 116 | { 117 | Program.helper.Log($"No response from smapi.io!"); 118 | } 119 | else 120 | { 121 | Program.helper.Log($"Error getting mod update data from smapi.io!"); 122 | } 123 | 124 | Program.helper.Log($"Our request to smapi.io:\n{parsedRequest}"); 125 | } 126 | 127 | client.Dispose(); 128 | 129 | return modUpdateData; 130 | } 131 | 132 | internal static SemVersion? GetVersion() 133 | { 134 | AssemblyName smapiAssembly = AssemblyName.GetAssemblyName(Path.Combine(Pathing.defaultGamePath, "StardewModdingAPI.dll")); 135 | 136 | if (smapiAssembly is null || smapiAssembly.Version is null) 137 | { 138 | return null; 139 | } 140 | 141 | return SemVersion.Parse($"{smapiAssembly.Version.Major}.{smapiAssembly.Version.Minor}.{smapiAssembly.Version.Build}", SemVersionStyles.Any); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Stardrop/Utilities/Helper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Stardrop.Utilities 7 | { 8 | internal class Helper 9 | { 10 | // Log file related 11 | private string basePath = AppDomain.CurrentDomain.BaseDirectory; 12 | private string logFileName = "log"; 13 | private string logFileExtension = ".txt"; 14 | 15 | // Listener and stat related 16 | private TraceListener listener; 17 | private int totalDebug = 0, totalAlert = 0, totalWarning = 0, totalInfo = 0; 18 | 19 | // Used for identifying different statuses when logging 20 | public enum Status { Debug, Alert, Warning, Info }; 21 | 22 | public Helper(string fileName = "log", string fileExtension = ".txt", string path = null) 23 | { 24 | // Set the log file name and extension 25 | logFileName = fileName; 26 | logFileExtension = fileExtension; 27 | basePath = String.IsNullOrEmpty(path) ? basePath : path; 28 | 29 | // Delete any previous log file 30 | if (File.Exists(GetLogPath())) 31 | { 32 | File.Delete(GetLogPath()); 33 | } 34 | 35 | // Create and enable the listener 36 | listener = new DelimitedListTraceListener(GetLogPath()); 37 | Trace.Listeners.Add(listener); 38 | 39 | // This makes the Debug.WriteLine() calls always write to the text file 40 | // Rather than waiting for a Debug.Flush() call 41 | Trace.AutoFlush = true; 42 | } 43 | 44 | public string GetLogPath() 45 | { 46 | return Path.Combine(basePath, String.Concat(logFileName, logFileExtension)); 47 | } 48 | 49 | public void DisableTracing() 50 | { 51 | // If listener exists and still is active, remove it 52 | if (!(listener is null) && Trace.Listeners.Contains(listener)) 53 | { 54 | Trace.Listeners.Remove(listener); 55 | listener.Close(); 56 | } 57 | } 58 | 59 | public bool IsActive() 60 | { 61 | return listener != null; 62 | } 63 | 64 | // Handles the Debug.WriteLine calls 65 | // It will grab the calling method and line as well via CompilerServices 66 | public void Log(string message, Status status = Status.Debug, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0, [CallerFilePath] string path = "") 67 | { 68 | // Tracking status info 69 | TrackStatus(status); 70 | 71 | string fileName = Path.GetFileName(path).Split('.')[0]; 72 | Trace.WriteLine(string.Format("[{0}][{1}][{2}.{3}: Line {4}] {5}", DateTime.Now.ToString(), status.ToString(), fileName, caller.ToString(), line, message)); 73 | } 74 | 75 | public void Log(object messageObj, Status status = Status.Debug, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0, [CallerFilePath] string path = "") 76 | { 77 | try 78 | { 79 | Log(messageObj.ToString(), status, caller, line, path); 80 | } 81 | catch 82 | { 83 | Log(String.Format($"Unable to parse {messageObj} to string!"), Status.Warning); 84 | } 85 | } 86 | 87 | #region Status related tracking 88 | public bool HasAlert() 89 | { 90 | return totalAlert > 0 ? true : false; 91 | } 92 | 93 | public bool HasWarning() 94 | { 95 | return totalWarning > 0 ? true : false; 96 | } 97 | 98 | public bool HasInfo() 99 | { 100 | return totalInfo > 0 ? true : false; 101 | } 102 | 103 | public bool HasDebug() 104 | { 105 | return totalDebug > 0 ? true : false; 106 | } 107 | #endregion 108 | 109 | // Handles tracking the count of different Status counts 110 | private void TrackStatus(Status status) 111 | { 112 | switch (status) 113 | { 114 | case Status.Debug: 115 | totalDebug += 1; 116 | break; 117 | case Status.Alert: 118 | totalAlert += 1; 119 | break; 120 | case Status.Warning: 121 | totalWarning += 1; 122 | break; 123 | case Status.Info: 124 | totalInfo += 1; 125 | break; 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Stardrop/Utilities/Internal/EnumParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Reflection; 4 | 5 | namespace Stardrop.Utilities.Internal 6 | { 7 | internal static class EnumParser 8 | { 9 | // Shamelessly copied from: https://stackoverflow.com/questions/1415140/can-my-enums-have-friendly-names 10 | public static string? GetDescription(this Enum? value) 11 | { 12 | if (value is null) 13 | { 14 | return null; 15 | } 16 | 17 | Type type = value.GetType(); 18 | string? name = Enum.GetName(type, value); 19 | if (name is not null) 20 | { 21 | FieldInfo? field = type.GetField(name); 22 | if (field is not null) 23 | { 24 | DescriptionAttribute? attr = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute; 25 | 26 | if (attr is not null) 27 | { 28 | return attr.Description; 29 | } 30 | } 31 | } 32 | 33 | return value.ToString(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Stardrop/Utilities/Internal/ManifestParser.cs: -------------------------------------------------------------------------------- 1 | using SharpCompress.Archives; 2 | using Stardrop.Models.SMAPI; 3 | using System; 4 | using System.IO; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace Stardrop.Utilities.Internal 9 | { 10 | internal static class ManifestParser 11 | { 12 | public static async Task GetDataAsync(IArchiveEntry manifestFile) 13 | { 14 | using (Stream stream = manifestFile.OpenEntryStream()) 15 | { 16 | using (var reader = new StreamReader(stream)) 17 | { 18 | return GetData(await reader.ReadToEndAsync()); 19 | } 20 | } 21 | } 22 | 23 | public static Manifest? GetData(string manifestText) 24 | { 25 | try 26 | { 27 | return JsonSerializer.Deserialize(manifestText, new JsonSerializerOptions() { AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true }); 28 | } 29 | catch (JsonException) 30 | { 31 | // Attempt to parse out illegal JSON characters, as System.Text.Json does not have any native handling (unlike Newtonsoft.Json) 32 | manifestText = manifestText.Replace("\r", String.Empty).Replace("\n", String.Empty); 33 | return JsonSerializer.Deserialize(manifestText, new JsonSerializerOptions() { AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true }); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Stardrop/Utilities/JsonTools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Text; 6 | using System.Text.Json; 7 | 8 | namespace Stardrop.Utilities 9 | { 10 | internal class JsonTools 11 | { 12 | // https://stackoverflow.com/questions/58378409/jsondocument-get-json-string 13 | public static string ParseDocumentToString(JsonDocument jdoc) 14 | { 15 | using (var stream = new MemoryStream()) 16 | { 17 | Utf8JsonWriter writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); 18 | jdoc.WriteTo(writer); 19 | writer.Flush(); 20 | 21 | return Encoding.UTF8.GetString(stream.ToArray()); 22 | } 23 | } 24 | 25 | // https://stackoverflow.com/questions/58694837/system-text-json-merge-two-objects 26 | public static string Merge(string originalJson, string newContent, bool includeMissingProperties) 27 | { 28 | var outputBuffer = new ArrayBufferWriter(); 29 | 30 | using (JsonDocument jDoc1 = JsonDocument.Parse(originalJson)) 31 | using (JsonDocument jDoc2 = JsonDocument.Parse(newContent)) 32 | using (var jsonWriter = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = true })) 33 | { 34 | JsonElement root1 = jDoc1.RootElement; 35 | JsonElement root2 = jDoc2.RootElement; 36 | 37 | if (root1.ValueKind != JsonValueKind.Array && root1.ValueKind != JsonValueKind.Object) 38 | { 39 | throw new InvalidOperationException($"The original JSON document to merge new content into must be a container type. Instead it is {root1.ValueKind}."); 40 | } 41 | 42 | if (root1.ValueKind != root2.ValueKind) 43 | { 44 | return originalJson; 45 | } 46 | 47 | if (root1.ValueKind == JsonValueKind.Array) 48 | { 49 | MergeArrays(jsonWriter, root1, root2); 50 | } 51 | else 52 | { 53 | MergeObjects(jsonWriter, root1, root2, includeMissingProperties); 54 | } 55 | } 56 | 57 | return Encoding.UTF8.GetString(outputBuffer.WrittenSpan); 58 | } 59 | 60 | private static void MergeObjects(Utf8JsonWriter jsonWriter, JsonElement root1, JsonElement root2, bool includeMissingProperties) 61 | { 62 | Debug.Assert(root1.ValueKind == JsonValueKind.Object); 63 | Debug.Assert(root2.ValueKind == JsonValueKind.Object); 64 | 65 | jsonWriter.WriteStartObject(); 66 | 67 | // Write all the properties of the first document. 68 | // If a property exists in both documents, either: 69 | // * Merge them, if the value kinds match (e.g. both are objects or arrays), 70 | // * Completely override the value of the first with the one from the second, if the value kind mismatches (e.g. one is object, while the other is an array or string), 71 | // * Or favor the value of the first (regardless of what it may be), if the second one is null (i.e. don't override the first). 72 | foreach (JsonProperty property in root1.EnumerateObject()) 73 | { 74 | string propertyName = property.Name; 75 | 76 | JsonValueKind newValueKind; 77 | 78 | if (root2.TryGetProperty(propertyName, out JsonElement newValue) && (newValueKind = newValue.ValueKind) != JsonValueKind.Null) 79 | { 80 | jsonWriter.WritePropertyName(propertyName); 81 | 82 | JsonElement originalValue = property.Value; 83 | JsonValueKind originalValueKind = originalValue.ValueKind; 84 | 85 | if (newValueKind == JsonValueKind.Object && originalValueKind == JsonValueKind.Object) 86 | { 87 | MergeObjects(jsonWriter, originalValue, newValue, includeMissingProperties); // Recursive call 88 | } 89 | else if (newValueKind == JsonValueKind.Array && originalValueKind == JsonValueKind.Array) 90 | { 91 | MergeArrays(jsonWriter, originalValue, newValue); 92 | } 93 | else 94 | { 95 | newValue.WriteTo(jsonWriter); 96 | } 97 | } 98 | else 99 | { 100 | property.WriteTo(jsonWriter); 101 | } 102 | } 103 | 104 | // Write all the properties of the second document that are unique to it 105 | if (includeMissingProperties is true) 106 | { 107 | foreach (JsonProperty property in root2.EnumerateObject()) 108 | { 109 | if (!root1.TryGetProperty(property.Name, out _)) 110 | { 111 | property.WriteTo(jsonWriter); 112 | } 113 | } 114 | } 115 | 116 | jsonWriter.WriteEndObject(); 117 | } 118 | 119 | private static void MergeArrays(Utf8JsonWriter jsonWriter, JsonElement root1, JsonElement root2) 120 | { 121 | Debug.Assert(root1.ValueKind == JsonValueKind.Array); 122 | Debug.Assert(root2.ValueKind == JsonValueKind.Array); 123 | 124 | jsonWriter.WriteStartArray(); 125 | 126 | // Write all the elements from the original JSON arrays 127 | foreach (JsonElement element in root2.EnumerateArray()) 128 | { 129 | element.WriteTo(jsonWriter); 130 | } 131 | 132 | jsonWriter.WriteEndArray(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Stardrop/Utilities/NXMProtocol.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Stardrop.Utilities 6 | { 7 | internal static class NXMProtocol 8 | { 9 | public static bool Register(string applicationPath) 10 | { 11 | try 12 | { 13 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) is false) 14 | { 15 | Program.helper.Log($"Attempted to modify registery keys for NXM protocol on a non-Windows system!"); 16 | return false; 17 | } 18 | 19 | var keyTest = Registry.CurrentUser.OpenSubKey("Software", true).OpenSubKey("Classes", true); 20 | RegistryKey key = keyTest.CreateSubKey("nxm"); 21 | key.SetValue("URL Protocol", "nxm"); 22 | key.CreateSubKey(@"shell\open\command").SetValue("", "\"" + applicationPath + "\" --nxm \"%1\""); 23 | } 24 | catch (Exception ex) 25 | { 26 | Program.helper.Log($"Failed to associate Stardrop with the NXM protocol: {ex}"); 27 | return false; 28 | } 29 | 30 | return true; 31 | } 32 | 33 | public static bool Validate(string applicationPath) 34 | { 35 | try 36 | { 37 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) is false) 38 | { 39 | Program.helper.Log($"Attempted to modify registery keys for NXM protocol on a non-Windows system!"); 40 | return false; 41 | } 42 | 43 | var baseKeyTest = Registry.CurrentUser.OpenSubKey("Software", true).OpenSubKey("Classes", true).OpenSubKey("nxm", true); 44 | if (baseKeyTest is null || baseKeyTest.GetValue("URL Protocol").ToString() != "nxm") 45 | { 46 | return false; 47 | } 48 | 49 | var actualKeyTest = Registry.CurrentUser.OpenSubKey("Software", true).OpenSubKey("Classes", true).OpenSubKey(@"nxm\shell\open\command", true); 50 | if (actualKeyTest.GetValue(String.Empty).ToString() != "\"" + applicationPath + "\" --nxm \"%1\"") 51 | { 52 | return false; 53 | } 54 | } 55 | catch (Exception ex) 56 | { 57 | return false; 58 | } 59 | 60 | return true; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Stardrop/Utilities/Pathing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Stardrop.Utilities 6 | { 7 | public static class Pathing 8 | { 9 | internal static string defaultGamePath; 10 | internal static string defaultModPath; 11 | internal static string defaultHomePath; 12 | 13 | internal static void SetHomePath(string homePath) 14 | { 15 | defaultHomePath = Path.Combine(homePath, "Stardrop", "Data"); 16 | } 17 | 18 | internal static void SetSmapiPath(string smapiPath, bool useDefaultModPath = false) 19 | { 20 | if (smapiPath is not null) 21 | { 22 | defaultGamePath = smapiPath; 23 | 24 | if (useDefaultModPath) 25 | { 26 | defaultModPath = Path.Combine(smapiPath, "Mods"); 27 | } 28 | } 29 | } 30 | 31 | internal static void SetModPath(string modPath) 32 | { 33 | if (modPath is not null) 34 | { 35 | defaultModPath = modPath; 36 | } 37 | } 38 | 39 | internal static string GetLogFolderPath() 40 | { 41 | return Path.Combine(defaultHomePath, "Logs"); 42 | } 43 | 44 | internal static string GetSettingsPath() 45 | { 46 | return Path.Combine(defaultHomePath, "Settings.json"); 47 | } 48 | 49 | public static string GetProfilesFolderPath() 50 | { 51 | return Path.Combine(defaultHomePath, "Profiles"); 52 | } 53 | 54 | public static string GetSelectedModsFolderPath() 55 | { 56 | return Path.Combine(defaultHomePath, "Selected Mods"); 57 | } 58 | 59 | public static string GetSmapiPath() 60 | { 61 | return Path.Combine(defaultGamePath, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "StardewModdingAPI.exe" : "StardewModdingAPI.dll"); 62 | } 63 | 64 | internal static string GetSmapiLogFolderPath() 65 | { 66 | return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); 67 | } 68 | 69 | public static string GetCacheFolderPath() 70 | { 71 | return Path.Combine(defaultHomePath, "Cache"); 72 | } 73 | 74 | public static string GetVersionCachePath() 75 | { 76 | return Path.Combine(GetCacheFolderPath(), "Versions.json"); 77 | } 78 | 79 | internal static string GetKeyCachePath() 80 | { 81 | return Path.Combine(GetCacheFolderPath(), "Keys.json"); 82 | } 83 | 84 | internal static string GetDataCachePath() 85 | { 86 | return Path.Combine(GetCacheFolderPath(), "Data.json"); 87 | } 88 | 89 | public static string GetNotionCachePath() 90 | { 91 | return Path.Combine(GetCacheFolderPath(), "Notion.json"); 92 | } 93 | 94 | public static string GetLinksCachePath() 95 | { 96 | return Path.Combine(GetCacheFolderPath(), "Links.json"); 97 | } 98 | 99 | public static string GetNexusPath() 100 | { 101 | return Path.Combine(defaultHomePath, "Nexus"); 102 | } 103 | 104 | public static string GetSmapiUpgradeFolderPath() 105 | { 106 | return Path.Combine(defaultHomePath, "SMAPI"); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /Stardrop/Utilities/SimpleObscure.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Security.Cryptography; 3 | 4 | namespace Stardrop.Utilities 5 | { 6 | internal class SimpleObscure 7 | { 8 | internal byte[] Key { get; set; } 9 | internal byte[] Vector { get; set; } 10 | 11 | public SimpleObscure() 12 | { 13 | using (Aes aes = Aes.Create()) 14 | { 15 | Key = aes.Key; 16 | Vector = aes.IV; 17 | } 18 | } 19 | 20 | internal static byte[] Encrypt(string plainText, byte[] Key, byte[] IV) 21 | { 22 | byte[] encrypted; 23 | 24 | using (Aes aes = Aes.Create()) 25 | { 26 | aes.Key = Key; 27 | aes.IV = IV; 28 | 29 | // Create an encryptor to perform the stream transform. 30 | ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV); 31 | 32 | // Create the streams used for encryption. 33 | using (MemoryStream msEncrypt = new MemoryStream()) 34 | { 35 | using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) 36 | { 37 | using (StreamWriter swEncrypt = new StreamWriter(csEncrypt)) 38 | { 39 | swEncrypt.Write(plainText); 40 | } 41 | encrypted = msEncrypt.ToArray(); 42 | } 43 | } 44 | } 45 | 46 | return encrypted; 47 | } 48 | 49 | internal static string Decrypt(byte[] cipherText, byte[] Key, byte[] IV) 50 | { 51 | string plaintext = null; 52 | 53 | using (Aes aes = Aes.Create()) 54 | { 55 | aes.Key = Key; 56 | aes.IV = IV; 57 | 58 | // Create a decryptor to perform the stream transform. 59 | ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV); 60 | 61 | // Create the streams used for decryption. 62 | using (MemoryStream msDecrypt = new MemoryStream(cipherText)) 63 | { 64 | using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) 65 | { 66 | using (StreamReader srDecrypt = new StreamReader(csDecrypt)) 67 | { 68 | plaintext = srDecrypt.ReadToEnd(); 69 | } 70 | } 71 | } 72 | } 73 | 74 | return plaintext; 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Stardrop/Utilities/Translation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.Json; 7 | 8 | namespace Stardrop.Utilities 9 | { 10 | internal class Translation : INotifyPropertyChanged 11 | { 12 | public enum Language 13 | { 14 | English, 15 | Chinese, 16 | French, 17 | German, 18 | Hungarian, 19 | Italian, 20 | Japanese, 21 | Korean, 22 | Portuguese, 23 | Russian, 24 | Spanish, 25 | Thai, 26 | Turkish, 27 | Ukrainian 28 | } 29 | 30 | public enum LanguageAbbreviation 31 | { 32 | @default, 33 | zh, 34 | fr, 35 | de, 36 | hu, 37 | it, 38 | ja, 39 | ko, 40 | pt, 41 | ru, 42 | es, 43 | th, 44 | tr, 45 | uk 46 | } 47 | public Dictionary LanguageNameToAbbreviations = new(); 48 | public Dictionary AbbreviationsToLanguageName = new(); 49 | 50 | private Language _selectedLanguage = Language.English; 51 | private Dictionary> _languageTranslations = new(); 52 | private const string IndexerName = "Item"; 53 | private const string IndexerArrayName = "Item[]"; 54 | 55 | 56 | public Translation() 57 | { 58 | int index = 0; 59 | foreach (Language language in Enum.GetValues(typeof(Language))) 60 | { 61 | LanguageNameToAbbreviations[language] = (LanguageAbbreviation)index; 62 | AbbreviationsToLanguageName[(LanguageAbbreviation)index] = language; 63 | 64 | index++; 65 | } 66 | } 67 | 68 | public string GetLanguageFromAbbreviation(string abbreviation) 69 | { 70 | if (Enum.TryParse(typeof(LanguageAbbreviation), abbreviation, out var languageAbbreviation)) 71 | { 72 | if (AbbreviationsToLanguageName.ContainsKey((LanguageAbbreviation)languageAbbreviation)) 73 | { 74 | return AbbreviationsToLanguageName[(LanguageAbbreviation)languageAbbreviation].ToString(); 75 | } 76 | } 77 | 78 | return Language.English.ToString(); 79 | } 80 | 81 | public Language GetLanguage(string language) 82 | { 83 | if (Enum.TryParse(typeof(Language), language, out var parsedLanguage)) 84 | { 85 | return (Language)parsedLanguage; 86 | } 87 | 88 | return Language.English; 89 | } 90 | 91 | public void SetLanguage(string language) 92 | { 93 | if (Enum.TryParse(typeof(Language), language, out var parsedLanguage)) 94 | { 95 | SetLanguage((Language)parsedLanguage); 96 | } 97 | } 98 | 99 | public void SetLanguage(Language language) 100 | { 101 | _selectedLanguage = language; 102 | 103 | Invalidate(); 104 | } 105 | 106 | public void LoadTranslations() 107 | { 108 | // Load the languages 109 | foreach (string fileFullName in Directory.EnumerateFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "i18n"), "*.json")) 110 | { 111 | try 112 | { 113 | var fileName = Path.GetFileNameWithoutExtension(fileFullName); 114 | if (Enum.TryParse(typeof(LanguageAbbreviation), fileName, out var language)) 115 | { 116 | _languageTranslations[(LanguageAbbreviation)language] = JsonSerializer.Deserialize>(File.ReadAllText(fileFullName), new JsonSerializerOptions { AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true }); 117 | Program.helper.Log($"Loaded language {Path.GetFileNameWithoutExtension(fileFullName)}", Helper.Status.Debug); 118 | } 119 | } 120 | catch (Exception ex) 121 | { 122 | Program.helper.Log($"Unable to load translation at {Path.GetFileNameWithoutExtension(fileFullName)}: {ex}", Helper.Status.Warning); 123 | } 124 | } 125 | } 126 | 127 | public void LoadTranslations(Language language) 128 | { 129 | // Set the language 130 | SetLanguage(language); 131 | 132 | LoadTranslations(); 133 | } 134 | 135 | public List GetAvailableTranslations() 136 | { 137 | List availableLanguages = new(); 138 | foreach (var abbreviation in _languageTranslations.Keys.Where(l => AbbreviationsToLanguageName.ContainsKey(l))) 139 | { 140 | availableLanguages.Add(AbbreviationsToLanguageName[abbreviation]); 141 | } 142 | 143 | return availableLanguages; 144 | } 145 | 146 | public string Get(string key) 147 | { 148 | var languageAbbreviation = LanguageNameToAbbreviations[_selectedLanguage]; 149 | if (_languageTranslations.ContainsKey(languageAbbreviation) && _languageTranslations[languageAbbreviation].ContainsKey(key)) 150 | { 151 | return _languageTranslations[languageAbbreviation][key]; 152 | } 153 | else if (_languageTranslations.ContainsKey(LanguageAbbreviation.@default) && _languageTranslations[LanguageAbbreviation.@default].ContainsKey(key)) 154 | { 155 | return _languageTranslations[LanguageAbbreviation.@default][key]; 156 | } 157 | 158 | return $"(No translation provided for key {key})"; 159 | } 160 | 161 | public string this[string key] 162 | { 163 | get 164 | { 165 | return Get(key); 166 | } 167 | } 168 | 169 | public event PropertyChangedEventHandler PropertyChanged; 170 | public void Invalidate() 171 | { 172 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerName)); 173 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerArrayName)); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Stardrop/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Controls.Templates; 3 | using Stardrop.ViewModels; 4 | using System; 5 | 6 | namespace Stardrop 7 | { 8 | public class ViewLocator : IDataTemplate 9 | { 10 | public IControl Build(object data) 11 | { 12 | var name = data.GetType().FullName!.Replace("ViewModel", "View"); 13 | var type = Type.GetType(name); 14 | 15 | if (type != null) 16 | { 17 | return (Control)Activator.CreateInstance(type)!; 18 | } 19 | else 20 | { 21 | return new TextBlock { Text = "Not Found: " + name }; 22 | } 23 | } 24 | 25 | public bool Match(object data) 26 | { 27 | return data is ViewModelBase; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Stardrop/ViewModels/DownloadPanelViewModel.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using DynamicData; 3 | using DynamicData.Aggregation; 4 | using DynamicData.Alias; 5 | using DynamicData.Binding; 6 | using ReactiveUI; 7 | using Stardrop.Models.Data; 8 | using Stardrop.Utilities.External; 9 | using System; 10 | using System.Collections.ObjectModel; 11 | using System.Linq; 12 | 13 | namespace Stardrop.ViewModels 14 | { 15 | public class DownloadPanelViewModel : ViewModelBase 16 | { 17 | private ObservableCollection _downloads = new(); 18 | public ObservableCollection Downloads { get => _downloads; set => this.RaiseAndSetIfChanged(ref _downloads, value); } 19 | 20 | public IObservable InProgressDownloads { get; init; } 21 | 22 | public DownloadPanelViewModel(NexusClient? nexusClient) 23 | { 24 | Nexus.ClientChanged += NexusClientChanged; 25 | if (nexusClient is not null) 26 | { 27 | RegisterEventHandlers(nexusClient); 28 | } 29 | 30 | // Count failed and canceled downloads toward this value, because those still need to be 31 | // handled in some way by the user 32 | InProgressDownloads = Downloads 33 | .ToObservableChangeSet(t => t.ModUri) 34 | .AutoRefresh(x => x.DownloadStatus, scheduler: RxApp.MainThreadScheduler) 35 | .Filter(x => x.DownloadStatus != ModDownloadStatus.Successful) 36 | .Count(); 37 | } 38 | 39 | private void NexusClientChanged(NexusClient? oldClient, NexusClient? newClient) 40 | { 41 | if (oldClient is not null) 42 | { 43 | // Cancel all downloads and clear the dictionary, so we don't have zombie downloads from an old client lingering 44 | foreach (var download in Downloads) 45 | { 46 | // Trigger the cancel command, and ignore any return values (as it has none) 47 | download.CancelCommand.Execute().Subscribe(); 48 | } 49 | ClearEventHandlers(oldClient); 50 | Downloads.Clear(); 51 | } 52 | if (newClient is not null) 53 | { 54 | RegisterEventHandlers(newClient); 55 | } 56 | } 57 | 58 | private void RegisterEventHandlers(NexusClient nexusClient) 59 | { 60 | nexusClient.DownloadStarted += DownloadStarted; 61 | nexusClient.DownloadProgressChanged += DownloadProgressChanged; 62 | nexusClient.DownloadCompleted += DownloadCompleted; 63 | nexusClient.DownloadFailed += DownloadFailed; 64 | } 65 | 66 | private void ClearEventHandlers(NexusClient nexusClient) 67 | { 68 | nexusClient.DownloadStarted -= DownloadStarted; 69 | nexusClient.DownloadProgressChanged -= DownloadProgressChanged; 70 | nexusClient.DownloadCompleted -= DownloadCompleted; 71 | nexusClient.DownloadFailed -= DownloadFailed; 72 | } 73 | 74 | private void DownloadStarted(object? sender, ModDownloadStartedEventArgs e) 75 | { 76 | var existingDownload = Downloads.FirstOrDefault(x => x.ModUri == e.Uri); 77 | if (existingDownload is not null) 78 | { 79 | // If the user is trying to download the same file twice, it's *probably* because they 80 | // want to retry a failed download. 81 | // But just in case, check to see if the existing download is still in-progress. If it is, do nothing. 82 | // We don't want to stop a user's 95% download because they accidentally hit the "download again please" button! 83 | if (existingDownload.DownloadStatus == ModDownloadStatus.NotStarted 84 | || existingDownload.DownloadStatus == ModDownloadStatus.InProgress) 85 | { 86 | return; 87 | } 88 | 89 | // If it does exist, and isn't in a progress state, they're probably trying to redownload a failed download. 90 | // Since we use the URI as our unique ID, we shouldn't have two items with the same URI in the list, 91 | // so clear out the old one. 92 | Downloads.Remove(existingDownload); 93 | } 94 | 95 | var downloadVM = new ModDownloadViewModel(e.Uri, e.Name, e.Size, e.DownloadCancellationSource); 96 | downloadVM.RemovalRequested += DownloadRemovalRequested; 97 | Downloads.Add(downloadVM); 98 | } 99 | 100 | private void DownloadProgressChanged(object? sender, ModDownloadProgressEventArgs e) 101 | { 102 | var download = Downloads.SingleOrDefault(x => x.ModUri == e.Uri); 103 | if (download is not null) 104 | { 105 | download.DownloadStatus = ModDownloadStatus.InProgress; 106 | download.DownloadedBytes = e.TotalBytes; 107 | } 108 | } 109 | 110 | private void DownloadCompleted(object? sender, ModDownloadCompletedEventArgs e) 111 | { 112 | var download = Downloads.SingleOrDefault(x => x.ModUri == e.Uri); 113 | if (download is not null) 114 | { 115 | download.DownloadStatus = ModDownloadStatus.Successful; 116 | } 117 | } 118 | 119 | private void DownloadFailed(object? sender, ModDownloadFailedEventArgs e) 120 | { 121 | var download = Downloads.SingleOrDefault(x => x.ModUri == e.Uri); 122 | if (download is not null) 123 | { 124 | download.DownloadStatus = ModDownloadStatus.Failed; 125 | } 126 | } 127 | 128 | private void DownloadRemovalRequested(object? sender, EventArgs _) 129 | { 130 | if (sender is not ModDownloadViewModel downloadVM) 131 | { 132 | return; 133 | } 134 | 135 | downloadVM.RemovalRequested -= DownloadRemovalRequested; 136 | Downloads.Remove(downloadVM); 137 | } 138 | 139 | // Designer-only constructor 140 | public DownloadPanelViewModel() 141 | { 142 | if (!Design.IsDesignMode) 143 | { 144 | throw new Exception("This constructor should only be called in design mode."); 145 | } 146 | 147 | var inProgressDownload = new ModDownloadViewModel( 148 | new Uri("https://www.fakeurl.com/testMod"), 149 | "Fake Test Mod Download", 150 | 1024 * 1024, 151 | new() 152 | ); 153 | inProgressDownload.DownloadStatus = ModDownloadStatus.InProgress; 154 | inProgressDownload.DownloadedBytes = inProgressDownload.SizeBytes!.Value / 2; 155 | Downloads.Add(inProgressDownload); 156 | 157 | var succeededDownload = new ModDownloadViewModel( 158 | new Uri("https://www.fakeSuccess.com"), 159 | "Fake Succeeded Download", 160 | 1234, 161 | new() 162 | ); 163 | succeededDownload.DownloadStatus = ModDownloadStatus.Successful; 164 | succeededDownload.DownloadedBytes = 1234; 165 | Downloads.Add(succeededDownload); 166 | 167 | var failedDownload = new ModDownloadViewModel( 168 | new Uri("https://www.differentFakeUrl.com"), 169 | "Failed Fake Download", 170 | 1024 * 1024 * 1024, 171 | new() 172 | ); 173 | failedDownload.DownloadedBytes = failedDownload.SizeBytes!.Value / 3; 174 | failedDownload.DownloadStatus = ModDownloadStatus.Failed; 175 | Downloads.Add(failedDownload); 176 | 177 | var cancelledDownload = new ModDownloadViewModel( 178 | new Uri("https://www.cancelledFake.com"), 179 | "Cancelled Fake Download", 180 | 1024 * 1024 * 5, 181 | new() 182 | ); 183 | cancelledDownload.DownloadedBytes = cancelledDownload.SizeBytes!.Value / 4; 184 | cancelledDownload.DownloadStatus = ModDownloadStatus.Canceled; 185 | Downloads.Add(cancelledDownload); 186 | 187 | var indeterminateInProgressDownload = new ModDownloadViewModel( 188 | new Uri("https://www.inProgressMystery.com"), 189 | "In Progress Download of Unknown Size", 190 | null, 191 | new() 192 | ); 193 | indeterminateInProgressDownload.DownloadedBytes = 1024 * 1024 * 2; 194 | indeterminateInProgressDownload.DownloadStatus = ModDownloadStatus.InProgress; 195 | Downloads.Add(indeterminateInProgressDownload); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Stardrop/ViewModels/FlexibleOptionWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace Stardrop.ViewModels 4 | { 5 | public class FlexibleOptionWindowViewModel : ViewModelBase 6 | { 7 | private string _messageText; 8 | public string MessageText { get { return _messageText; } set { this.RaiseAndSetIfChanged(ref _messageText, value); } } 9 | private string _firstButtonText; 10 | public string FirstButtonText { get { return _firstButtonText; } set { this.RaiseAndSetIfChanged(ref _firstButtonText, value); } } 11 | private string _secondButtonText; 12 | public string SecondButtonText { get { return _secondButtonText; } set { this.RaiseAndSetIfChanged(ref _secondButtonText, value); } } 13 | private string _thirdButtonText; 14 | public string ThirdButtonText { get { return _thirdButtonText; } set { this.RaiseAndSetIfChanged(ref _thirdButtonText, value); } } 15 | 16 | private bool _isFirstButtonVisible; 17 | public bool IsFirstButtonVisible { get { return _isFirstButtonVisible; } set { this.RaiseAndSetIfChanged(ref _isFirstButtonVisible, value); } } 18 | private bool _isSecondButtonVisible; 19 | public bool IsSecondButtonVisible { get { return _isSecondButtonVisible; } set { this.RaiseAndSetIfChanged(ref _isSecondButtonVisible, value); } } 20 | private bool _isThirdButtonVisible; 21 | public bool IsThirdButtonVisible { get { return _isThirdButtonVisible; } set { this.RaiseAndSetIfChanged(ref _isThirdButtonVisible, value); } } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Stardrop/ViewModels/MessageWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace Stardrop.ViewModels 4 | { 5 | public class MessageWindowViewModel : ViewModelBase 6 | { 7 | private string _messageText; 8 | public string MessageText { get { return _messageText; } set { this.RaiseAndSetIfChanged(ref _messageText, value); } } 9 | private string _positiveButtonText; 10 | public string PositiveButtonText { get { return _positiveButtonText; } set { this.RaiseAndSetIfChanged(ref _positiveButtonText, value); } } 11 | private string _negativeButtonText; 12 | public string NegativeButtonText { get { return _negativeButtonText; } set { this.RaiseAndSetIfChanged(ref _negativeButtonText, value); } } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Stardrop/ViewModels/ModDownloadViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Linq; 4 | using System.Reactive; 5 | using System.Reactive.Linq; 6 | using System.Threading; 7 | 8 | namespace Stardrop.ViewModels 9 | { 10 | public enum ModDownloadStatus 11 | { 12 | NotStarted, 13 | InProgress, 14 | Successful, 15 | Canceled, 16 | Failed 17 | } 18 | 19 | public class ModDownloadViewModel : ViewModelBase 20 | { 21 | private readonly DateTimeOffset _startTime; 22 | private readonly CancellationTokenSource _downloadCancellationSource; 23 | 24 | // Communicates up to the parent panel that the user wants to remove this download from the list. 25 | public event EventHandler? RemovalRequested = null!; 26 | 27 | // --Set-once properties-- 28 | public Uri ModUri { get; init; } 29 | 30 | // --Bindable properties-- 31 | 32 | private string _name; 33 | public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); } 34 | 35 | private long? _sizeBytes; 36 | public long? SizeBytes { get => _sizeBytes; set => this.RaiseAndSetIfChanged(ref _sizeBytes, value); } 37 | 38 | private long _downloadedBytes; 39 | public long DownloadedBytes { get => _downloadedBytes; set => this.RaiseAndSetIfChanged(ref _downloadedBytes, value); } 40 | 41 | private ModDownloadStatus _downloadStatus = ModDownloadStatus.NotStarted; 42 | public ModDownloadStatus DownloadStatus { get => _downloadStatus; set => this.RaiseAndSetIfChanged(ref _downloadStatus, value); } 43 | 44 | // --Composite or dependent properties-- 45 | 46 | private readonly ObservableAsPropertyHelper _completion = null!; 47 | public double Completion => _completion.Value; 48 | 49 | private readonly ObservableAsPropertyHelper _isSizeUnknown = null!; 50 | public bool IsSizeUnknown => _isSizeUnknown.Value; 51 | 52 | private readonly ObservableAsPropertyHelper _downloadSpeedLabel = null!; 53 | public string DownloadSpeedLabel => _downloadSpeedLabel.Value; 54 | 55 | private readonly ObservableAsPropertyHelper _downloadProgressLabel = null!; 56 | public string DownloadProgressLabel => _downloadProgressLabel.Value; 57 | 58 | // --Commands-- 59 | 60 | public ReactiveCommand CancelCommand { get; } 61 | public ReactiveCommand RemoveCommand { get; } 62 | 63 | public ModDownloadViewModel(Uri modUri, string name, long? sizeInBytes, CancellationTokenSource downloadCancellationSource) 64 | { 65 | _startTime = DateTimeOffset.UtcNow; 66 | 67 | ModUri = modUri; 68 | _name = name; 69 | _sizeBytes = sizeInBytes; 70 | _downloadedBytes = 0; 71 | _downloadCancellationSource = downloadCancellationSource; 72 | 73 | CancelCommand = ReactiveCommand.Create(Cancel); 74 | RemoveCommand = ReactiveCommand.Create(Remove); 75 | 76 | // SizeBytes null-ness to IsSizeUnknown converison 77 | this.WhenAnyValue(x => x.SizeBytes) 78 | .Select(x => x.HasValue is false) 79 | .ToProperty(this, x => x.IsSizeUnknown, out _isSizeUnknown); 80 | 81 | // DownloadedBytes to DownloadSpeedLabel conversion 82 | this.WhenAnyValue(x => x.DownloadedBytes) 83 | .Sample(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler) 84 | .Select(bytes => 85 | { 86 | double elapsedSeconds = (DateTimeOffset.UtcNow - _startTime).TotalSeconds; 87 | double bytesPerSecond = bytes / elapsedSeconds; 88 | if (bytesPerSecond > 1024 * 1024) // MB 89 | { 90 | return $"{(bytesPerSecond / (1024 * 1024)):N2} {Program.translation.Get("internal.measurements.megabytes_per_second")}"; 91 | } 92 | else if (bytesPerSecond > 1024) // KB 93 | { 94 | return $"{(bytesPerSecond / 1024):N2} {Program.translation.Get("internal.measurements.kilobytes_per_second")}"; 95 | } 96 | else // Bytes 97 | { 98 | return $"{bytesPerSecond:N0} {Program.translation.Get("internal.measurements.bytes_per_second")}"; 99 | } 100 | }).ToProperty(this, x => x.DownloadSpeedLabel, out _downloadSpeedLabel); 101 | 102 | // DownloadedBytes and SizeBytes to DownloadProgressLabel conversion 103 | this.WhenAnyValue(x => x.DownloadedBytes, x => x.SizeBytes) 104 | .Sample(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler) 105 | .Select(((long Bytes, long? Total) x) => 106 | { 107 | string bytesString = ToHumanReadable(x.Bytes); 108 | if (x.Total is null) 109 | { 110 | return $"{bytesString} / ??? {Program.translation.Get("internal.measurements.megabytes_size")}"; 111 | } 112 | else 113 | { 114 | string totalString = ToHumanReadable(x.Total!.Value); 115 | return $"{bytesString} / {totalString}"; 116 | } 117 | 118 | static string ToHumanReadable(long bytes) 119 | { 120 | if (bytes > 1024 * 1024) // MB 121 | { 122 | return $"{(bytes / (1024.0 * 1024.0)):N2} {Program.translation.Get("internal.measurements.megabytes_size")}"; 123 | } 124 | else if (bytes > 1024) // KB 125 | { 126 | return $"{(bytes / 1024.0):N2} {Program.translation.Get("internal.measurements.kilobytes_size")}"; 127 | } 128 | else 129 | { 130 | return $"{bytes:N0} {Program.translation.Get("internal.measurements.bytes_size")}"; 131 | } 132 | } 133 | }).ToProperty(this, x => x.DownloadProgressLabel, out _downloadProgressLabel); 134 | 135 | if (SizeBytes.HasValue) 136 | { 137 | // DownloadedBytes to Completion conversion 138 | this.WhenAnyValue(x => x.DownloadedBytes) 139 | .Sample(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler) 140 | .Select(x => (DownloadedBytes / (double)SizeBytes) * 100) 141 | .ToProperty(this, x => x.Completion, out _completion); 142 | } 143 | } 144 | 145 | private void Cancel() 146 | { 147 | _downloadCancellationSource.Cancel(); 148 | DownloadStatus = ModDownloadStatus.Canceled; 149 | } 150 | 151 | private void Remove() 152 | { 153 | RemovalRequested?.Invoke(this, EventArgs.Empty); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Stardrop/ViewModels/ProfileEditorViewModel.cs: -------------------------------------------------------------------------------- 1 | using Stardrop.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using System.Text.Json; 9 | 10 | namespace Stardrop.ViewModels 11 | { 12 | public class ProfileEditorViewModel : ViewModelBase 13 | { 14 | public ObservableCollection Profiles { get; set; } 15 | public List OldProfiles { get; set; } 16 | public string ToolTip_Save { get; set; } 17 | public string ToolTip_Cancel { get; set; } 18 | 19 | private readonly string _profileFilePath; 20 | 21 | public ProfileEditorViewModel(string profilesFilePath) 22 | { 23 | OldProfiles = new List(); 24 | Profiles = new ObservableCollection(); 25 | 26 | _profileFilePath = profilesFilePath; 27 | DirectoryInfo profileDirectory = new DirectoryInfo(_profileFilePath); 28 | foreach (var fileInfo in profileDirectory.GetFiles("*.json", SearchOption.AllDirectories)) 29 | { 30 | if (fileInfo.DirectoryName is null) 31 | { 32 | continue; 33 | } 34 | 35 | try 36 | { 37 | var profile = JsonSerializer.Deserialize(File.ReadAllText(fileInfo.FullName), new JsonSerializerOptions { AllowTrailingCommas = true }); 38 | if (profile is null) 39 | { 40 | Program.helper.Log($"The profile file {fileInfo.Name} was empty or not deserializable from {fileInfo.DirectoryName}", Utilities.Helper.Status.Alert); 41 | continue; 42 | } 43 | 44 | Profiles.Add(profile); 45 | } 46 | catch (Exception ex) 47 | { 48 | Program.helper.Log($"Unable to load the profile file {fileInfo.Name} from {fileInfo.DirectoryName}: {ex}", Utilities.Helper.Status.Alert); 49 | } 50 | } 51 | 52 | if (!Profiles.Any(p => p.Name == Program.defaultProfileName)) 53 | { 54 | var defaultProfile = new Profile(Program.defaultProfileName) { IsProtected = true }; 55 | Profiles.Insert(0, defaultProfile); 56 | CreateProfile(defaultProfile); 57 | } 58 | else if (Profiles.IndexOf(Profiles.First(p => p.Name == Program.defaultProfileName)) != 0) 59 | { 60 | // Move the default profile to the top 61 | Profiles.Move(Profiles.IndexOf(Profiles.First(p => p.Name == Program.defaultProfileName)), 0); 62 | } 63 | 64 | OldProfiles = Profiles.ToList(); 65 | 66 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 67 | { 68 | ToolTip_Save = Program.translation.Get("ui.settings_window.tooltips.save_changes"); 69 | ToolTip_Cancel = Program.translation.Get("ui.settings_window.tooltips.cancel_changes"); 70 | } 71 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 72 | { 73 | // TEMPORARY FIX: Due to bug with Avalonia on Linux platforms, tooltips currently cause crashes when they disappear 74 | // To work around this, tooltips are purposely not displayed 75 | } 76 | } 77 | 78 | internal void CreateProfile(Profile profile, bool force = false) 79 | { 80 | string fileFullName = Path.Combine(_profileFilePath, profile.Name + ".json"); 81 | if (File.Exists(fileFullName) && !force) 82 | { 83 | Program.helper.Log($"Attempted to create an already existing profile file ({profile.Name}) at the path {fileFullName}", Utilities.Helper.Status.Warning); 84 | return; 85 | } 86 | 87 | File.WriteAllText(fileFullName, JsonSerializer.Serialize(profile, new JsonSerializerOptions() { WriteIndented = true })); 88 | } 89 | 90 | internal void DeleteProfile(Profile profile) 91 | { 92 | string fileFullName = Path.Combine(_profileFilePath, profile.Name + ".json"); 93 | if (!File.Exists(fileFullName)) 94 | { 95 | Program.helper.Log($"Attempted to delete a non-existent profile file ({profile.Name}) at the path {fileFullName}", Utilities.Helper.Status.Warning); 96 | return; 97 | } 98 | 99 | File.Delete(fileFullName); 100 | } 101 | 102 | internal void UpdateProfile(Profile profile, List enabledModIds) 103 | { 104 | int profileIndex = Profiles.IndexOf(profile); 105 | if (profileIndex == -1) 106 | { 107 | return; 108 | } 109 | 110 | Profiles[profileIndex].EnabledModIds = enabledModIds; 111 | CreateProfile(profile, true); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Stardrop/ViewModels/SettingsWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using Stardrop.Utilities; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Stardrop.ViewModels 6 | { 7 | public class SettingsWindowViewModel : ViewModelBase 8 | { 9 | // Setting bindings 10 | public string SMAPIPath { get { return Program.settings.SMAPIFolderPath; } set { Program.settings.SMAPIFolderPath = value; Pathing.SetSmapiPath(Program.settings.SMAPIFolderPath, String.IsNullOrEmpty(Program.settings.ModFolderPath)); } } 11 | public string ModFolderPath { get { return Program.settings.ModFolderPath; } set { Program.settings.ModFolderPath = value; Pathing.SetModPath(Program.settings.ModFolderPath); } } 12 | public string ModInstallPath { get { return Program.settings.ModInstallPath; } set { Program.settings.ModInstallPath = value; } } 13 | public bool IgnoreHiddenFolders { get { return Program.settings.IgnoreHiddenFolders; } set { Program.settings.IgnoreHiddenFolders = value; } } 14 | public bool IsAskingBeforeAcceptingNXM { get { return Program.settings.IsAskingBeforeAcceptingNXM; } set { Program.settings.IsAskingBeforeAcceptingNXM = value; } } 15 | public bool EnableProfileSpecificModConfigs { get { return Program.settings.EnableProfileSpecificModConfigs; } set { Program.settings.EnableProfileSpecificModConfigs = value; } } 16 | public bool EnableModsOnAdd { get { return Program.settings.EnableModsOnAdd; } set { Program.settings.EnableModsOnAdd = value; } } 17 | public bool AlwaysAskToDelete { get { return Program.settings.AlwaysAskToDelete; } set { Program.settings.AlwaysAskToDelete = value; } } 18 | public bool ShouldAutomaticallySaveProfileChanges { get { return Program.settings.ShouldAutomaticallySaveProfileChanges; } set { Program.settings.ShouldAutomaticallySaveProfileChanges = value; } } 19 | 20 | // Tooltips 21 | public string ToolTip_SMAPI { get; set; } 22 | public string ToolTip_ModFolder { get; set; } 23 | public string ToolTip_ModInstall { get; set; } 24 | public string ToolTip_Theme { get; set; } 25 | public string ToolTip_Language { get; set; } 26 | public string ToolTip_Grouping { get; set; } 27 | public string ToolTip_IgnoreHiddenFolders { get; set; } 28 | public string ToolTip_PreferredServer { get; set; } 29 | public string ToolTip_NXMAssociation { get; set; } 30 | public string ToolTip_AlwaysAskNXMFiles { get; set; } 31 | public string ToolTip_EnableProfileSpecificModConfigs { get; set; } 32 | public string ToolTip_EnableModsOnAdd { get; set; } 33 | public string ToolTip_ShouldAutomaticallySaveProfileChanges { get; set; } 34 | public string ToolTip_Save { get; set; } 35 | public string ToolTip_Cancel { get; set; } 36 | 37 | // Other UI controls 38 | public bool ShowMainMenu { get; set; } 39 | public bool ShowNXMAssociationButton { get; set; } 40 | public bool ShowNexusServers { get; set; } 41 | 42 | public SettingsWindowViewModel() 43 | { 44 | ToolTip_SMAPI = Program.translation.Get("ui.settings_window.tooltips.smapi"); 45 | ToolTip_ModFolder = Program.translation.Get("ui.settings_window.tooltips.mod_folder_path"); 46 | ToolTip_ModInstall = Program.translation.Get("ui.settings_window.tooltips.mod_install_path"); 47 | ToolTip_Theme = Program.translation.Get("ui.settings_window.tooltips.theme"); 48 | ToolTip_Language = Program.translation.Get("ui.settings_window.tooltips.language"); 49 | ToolTip_Grouping = Program.translation.Get("ui.settings_window.tooltips.grouping"); 50 | ToolTip_IgnoreHiddenFolders = Program.translation.Get("ui.settings_window.tooltips.ignore_hidden_folders"); 51 | ToolTip_PreferredServer = Program.translation.Get("ui.settings_window.tooltips.preferred_server"); 52 | ToolTip_NXMAssociation = Program.translation.Get("ui.settings_window.tooltips.nxm_file_association"); 53 | ToolTip_AlwaysAskNXMFiles = Program.translation.Get("ui.settings_window.tooltips.always_ask_nxm_files"); 54 | ToolTip_EnableProfileSpecificModConfigs = Program.translation.Get("ui.settings_window.tooltips.enable_profile_specific_configs"); 55 | ToolTip_EnableModsOnAdd = Program.translation.Get("ui.settings_window.tooltips.enable_mods_on_add"); 56 | ToolTip_Save = Program.translation.Get("ui.settings_window.tooltips.save_changes"); 57 | ToolTip_Cancel = Program.translation.Get("ui.settings_window.tooltips.cancel_changes"); 58 | 59 | ShowMainMenu = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 60 | ShowNXMAssociationButton = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 61 | ShowNexusServers = Program.settings.NexusDetails is not null && Program.settings.NexusDetails.IsPremium; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Stardrop/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace Stardrop.ViewModels 4 | { 5 | public class ViewModelBase : ReactiveObject 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Stardrop/ViewModels/WarningWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace Stardrop.ViewModels 4 | { 5 | public class WarningWindowViewModel : ViewModelBase 6 | { 7 | private string _warningText; 8 | public string WarningText { get { return _warningText; } set { this.RaiseAndSetIfChanged(ref _warningText, value); } } 9 | private string _buttonText; 10 | public string ButtonText { get { return _buttonText; } set { this.RaiseAndSetIfChanged(ref _buttonText, value); } } 11 | private bool _isButtonVisible; 12 | public bool IsButtonVisible { get { return _isButtonVisible; } set { this.RaiseAndSetIfChanged(ref _isButtonVisible, value); } } 13 | private bool _isProgressBarVisible; 14 | public bool IsProgressBarVisible { get { return _isProgressBarVisible; } set { this.RaiseAndSetIfChanged(ref _isProgressBarVisible, value); } } 15 | private double _progressBarValue; 16 | public double ProgressBarValue { get { return _progressBarValue; } set { this.RaiseAndSetIfChanged(ref _progressBarValue, value); } } 17 | 18 | public WarningWindowViewModel() 19 | { 20 | 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Stardrop/Views/DownloadPanel.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Markup.Xaml; 3 | using Avalonia.Media; 4 | using Stardrop.Utilities.External; 5 | using Stardrop.ViewModels; 6 | 7 | namespace Stardrop.Views; 8 | 9 | public partial class DownloadPanel : UserControl 10 | { 11 | private DownloadPanelViewModel _viewModel = null!; 12 | 13 | public DownloadPanel() 14 | { 15 | AvaloniaXamlLoader.Load(this); 16 | if (Design.IsDesignMode) 17 | { 18 | // Normally, the background gets handled by the hosting flyout's FlyoutPresenterClasses 19 | // Since design mode doesn't have a flyout to host this, we do it manually 20 | Background = new SolidColorBrush(new Color(0xFF, 0x03, 0x13, 0x32)); 21 | return; 22 | } 23 | 24 | _viewModel = new DownloadPanelViewModel(Nexus.Client); 25 | DataContext = _viewModel; 26 | } 27 | } -------------------------------------------------------------------------------- /Stardrop/Views/FlexibleOptionWindow.axaml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |