├── .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 | 
36 |
37 | 
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 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Stardrop/Views/FlexibleOptionWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Markup.Xaml;
4 | using Stardrop.Models.Data.Enums;
5 | using Stardrop.ViewModels;
6 | using System;
7 |
8 | namespace Stardrop.Views
9 | {
10 | public partial class FlexibleOptionWindow : Window
11 | {
12 | private readonly FlexibleOptionWindowViewModel _viewModel;
13 |
14 | public FlexibleOptionWindow()
15 | {
16 | InitializeComponent();
17 |
18 | // Set the main window view
19 | _viewModel = new FlexibleOptionWindowViewModel();
20 | DataContext = _viewModel;
21 |
22 | #if DEBUG
23 | this.AttachDevTools();
24 | #endif
25 | }
26 |
27 | public FlexibleOptionWindow(string messageText, string? firstButtonText = null, string? secondButtonText = null, string? thirdButtonText = null) : this()
28 | {
29 | _viewModel.MessageText = messageText;
30 |
31 | if (String.IsNullOrEmpty(firstButtonText) is false)
32 | {
33 | _viewModel.FirstButtonText = firstButtonText;
34 | _viewModel.IsFirstButtonVisible = true;
35 | }
36 | if (String.IsNullOrEmpty(secondButtonText) is false)
37 | {
38 | _viewModel.SecondButtonText = secondButtonText;
39 | _viewModel.IsSecondButtonVisible = true;
40 | }
41 | if (String.IsNullOrEmpty(thirdButtonText) is false)
42 | {
43 | _viewModel.ThirdButtonText = thirdButtonText;
44 | _viewModel.IsThirdButtonVisible = true;
45 | }
46 |
47 | this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
48 | this.SizeToContent = SizeToContent.Height;
49 | }
50 |
51 | private void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
52 | {
53 | Button? button = sender as Button;
54 | if (button is null)
55 | {
56 | return;
57 | }
58 |
59 | if (button.Content.Equals(_viewModel.FirstButtonText))
60 | {
61 | this.Close(Choice.First);
62 | }
63 | else if (button.Content.Equals(_viewModel.SecondButtonText))
64 | {
65 | this.Close(Choice.Second);
66 | }
67 | else if (button.Content.Equals(_viewModel.ThirdButtonText))
68 | {
69 | this.Close(Choice.Third);
70 | }
71 | }
72 |
73 | private void InitializeComponent()
74 | {
75 | AvaloniaXamlLoader.Load(this);
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Stardrop/Views/MessageWindow.axaml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Stardrop/Views/MessageWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Markup.Xaml;
4 | using Stardrop.ViewModels;
5 | using System;
6 |
7 | namespace Stardrop.Views
8 | {
9 | public partial class MessageWindow : Window
10 | {
11 | private readonly MessageWindowViewModel _viewModel;
12 |
13 | public MessageWindow()
14 | {
15 | InitializeComponent();
16 |
17 | // Set the main window view
18 | _viewModel = new MessageWindowViewModel();
19 | DataContext = _viewModel;
20 |
21 | #if DEBUG
22 | this.AttachDevTools();
23 | #endif
24 | }
25 |
26 | public MessageWindow(string messageText, string? positiveButtonText = null, string? negativeButtonText = null) : this()
27 | {
28 | Program.helper.Log($"Created a message window with the following text: [{positiveButtonText} | {negativeButtonText}] {messageText}");
29 |
30 | _viewModel.MessageText = messageText;
31 | _viewModel.PositiveButtonText = String.IsNullOrEmpty(positiveButtonText) ? Program.translation.Get("internal.yes") : positiveButtonText;
32 | _viewModel.NegativeButtonText = String.IsNullOrEmpty(negativeButtonText) ? Program.translation.Get("internal.no") : negativeButtonText;
33 |
34 | this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
35 | this.SizeToContent = SizeToContent.Height;
36 | }
37 |
38 | private void PositiveButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
39 | {
40 | this.Close(true);
41 | }
42 |
43 | private void NegativeButton_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
44 | {
45 | this.Close(false);
46 | }
47 |
48 | private void InitializeComponent()
49 | {
50 | AvaloniaXamlLoader.Load(this);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Stardrop/Views/NexusInfo.axaml:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
26 |
29 |
32 |
35 |
39 |
43 |
47 |
50 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
72 |
73 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/Stardrop/Views/NexusInfo.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls;
3 | using Avalonia.Markup.Xaml;
4 | using Stardrop.Models.Data;
5 | using Stardrop.Models.Nexus;
6 | using Stardrop.Utilities;
7 | using System;
8 | using System.IO;
9 | using System.Text.Json;
10 |
11 | namespace Stardrop.Views
12 | {
13 | public partial class NexusInfo : Window
14 | {
15 | public NexusInfo()
16 | {
17 | InitializeComponent();
18 | #if DEBUG
19 | this.AttachDevTools();
20 | #endif
21 | }
22 |
23 |
24 | public NexusInfo(NexusUser nexusUser) : this()
25 | {
26 | // Handle buttons
27 | this.FindControl