Repository: Floogen/Stardrop
Branch: development
Commit: c1c3c243e033
Files: 139
Total size: 804.5 KB
Directory structure:
gitextract_noi0fdfa/
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── build.yml
│ ├── nightly-build.yml
│ ├── package-and-release.yml
│ ├── package.yml
│ ├── pr-target-action.yml
│ └── version-build.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
└── Stardrop/
├── App.axaml
├── App.axaml.cs
├── Assets/
│ ├── Info.plist
│ ├── Stardrop.icns
│ └── Stardrop.sh
├── 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
│ │ ├── NexusConnectionResult.cs
│ │ ├── Validate.cs
│ │ ├── WebsocketResponse.cs
│ │ └── WebsocketResponseData.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
│ └── Theme.cs
├── Program.cs
├── Properties/
│ └── PublishProfiles/
│ ├── FolderProfile - Linux.pubxml
│ ├── FolderProfile - MacOS.pubxml
│ └── FolderProfile - Windows.pubxml
├── Stardrop.csproj
├── Stardrop.sln
├── Themes/
│ ├── Contributors/
│ │ └── hotcereal/
│ │ ├── CASH MONEY.xaml
│ │ ├── Cerene Dark.xaml
│ │ ├── Cerene Light.xaml
│ │ ├── Dark Cherry Chocolate.xaml
│ │ ├── Fairy.xaml
│ │ ├── Forest.xaml
│ │ ├── Light (Pink).xaml
│ │ └── Light Cherry Chocolate.xaml
│ ├── 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
│ ├── NexusWebsocket.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
├── ja.json
├── ko.json
├── pl.json
├── pt.json
├── ru.json
├── th.json
├── tr.json
├── uk.json
└── zh.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto
*.sh text eol=lf
================================================
FILE: .github/workflows/build.yml
================================================
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
name: Build
on:
push:
branches: [ "stable" ]
pull_request:
types:
- opened
- edited
- reopened
- synchronize
workflow_call:
workflow_dispatch:
env:
WORKING_DIRECTORY: "Stardrop"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore "${{ env.WORKING_DIRECTORY }}"
- name: Build
run: dotnet build "${{ env.WORKING_DIRECTORY }}" --no-restore
================================================
FILE: .github/workflows/nightly-build.yml
================================================
name: Package Nightly Build
on:
schedule:
- cron: '0 10 * * *'
workflow_dispatch:
permissions:
contents: write
jobs:
package-and-release:
uses: ./.github/workflows/package-and-release.yml
with:
tag: nightly
is-prerelease: 'true'
generate-release-notes: false
================================================
FILE: .github/workflows/package-and-release.yml
================================================
name: Package and Release
on:
workflow_call:
inputs:
tag:
required: false
type: string
default: ''
is-prerelease:
required: true
type: string
generate-release-notes:
required: true
type: boolean
default: true
workflow_dispatch:
inputs:
tag:
required: true
type: string
is-prerelease:
required: true
type: choice
options:
- 'true'
- 'false'
generate-release-notes:
required: true
type: boolean
default: true
env:
TAG: ${{ inputs.tag == '' && github.ref_name || inputs.tag }}
permissions:
contents: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "releasing"
cancel-in-progress: false
run-name: "[${{ inputs.is-prerelease == 'true' && 'Pre-release' || 'Release' }}] Package and Release (${{ inputs.tag == '' && github.ref_name || inputs.tag }})"
jobs:
verify-build:
uses: ./.github/workflows/build.yml
package:
needs: [ verify-build ]
uses: ./.github/workflows/package.yml
with:
branch-name: '' # Leaving blank to use default branch
release:
runs-on: ubuntu-latest
needs: [ verify-build, package ]
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
merge-multiple: true
- name: Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.TAG }}
prerelease: ${{ inputs.is-prerelease }}
name: "Stardrop ${{ env.TAG }}"
append_body: false
body: |
  
Documentation can be [found here](https://floogen.gitbook.io/stardrop/).
**User Note:**
Download the respective ZIP file relating to the host's operating system (Stardrop-win-x64 if using a Window x64 OS and so on).
generate_release_notes: ${{ inputs.generate-release-notes }}
make_latest: true
fail_on_unmatched_files: true
files: |
Stardrop-win-x64.zip
Stardrop-linux-x64.zip
Stardrop-osx-x64.zip
================================================
FILE: .github/workflows/package.yml
================================================
name: Package
on:
pull_request:
types:
- labeled
- unlabeled
- opened
- edited
- reopened
- synchronize
workflow_call:
inputs:
branch-name:
required: true
type: string
workflow_dispatch:
inputs:
branch-name:
required: true
type: string
env:
OUTPUT_PATH_WINDOWS: published/windows
OUTPUT_PATH_LINUX: published/linux
OUTPUT_PATH_MAC: published/mac
CONFIGURATION: Release
WORKING_DIRECTORY: "Stardrop"
BRANCH: ${{ github.event_name != 'pull_request' && inputs.branch-name || github.event.pull_request.head.ref }}
run-name: "[${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || 'Default Branch' }}] Packaging"
jobs:
package-macos:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'generate packages')
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.BRANCH }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
- run: mkdir -p path/to/release-artifacts
- name: Publish (MacOS - Creating)
run: |
mkdir -p "${{ env.OUTPUT_PATH_MAC }}/Stardrop.app/Contents/MacOS"
mkdir -p "${{ env.OUTPUT_PATH_MAC }}/Stardrop.app/Contents/Resources"
dotnet publish "${{ env.WORKING_DIRECTORY }}" --output "${{ env.OUTPUT_PATH_MAC }}/Stardrop.app/Contents/MacOS" --configuration "${{ env.CONFIGURATION }}" --runtime "osx-x64" --framework "net8.0" --self-contained
- name: Publish (MacOS - Packaging)
working-directory: "${{ env.OUTPUT_PATH_MAC }}/Stardrop.app"
run: |
cp ${GITHUB_WORKSPACE}/Stardrop/Assets/Info.plist "Contents/Info.plist"
cp ${GITHUB_WORKSPACE}/Stardrop/Assets/Stardrop.icns "Contents/Resources/Stardrop.icns"
chmod +x "Contents/MacOS/Stardrop"
(cd ../ && codesign --force --deep -s - Stardrop.app)
(cd ../ && zip -r ${GITHUB_WORKSPACE}/Stardrop-osx-x64.zip "Stardrop.app")
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts-macos
path: |
Stardrop-osx-x64.zip
package-linux:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'generate packages')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.BRANCH }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
- run: mkdir -p path/to/release-artifacts
- name: Publish (Linux - Creating)
run: |
dotnet publish "${{ env.WORKING_DIRECTORY }}" --output "${{ env.OUTPUT_PATH_LINUX }}/Stardrop" --configuration "${{ env.CONFIGURATION }}" --runtime "linux-x64" --framework "net8.0" --self-contained
- name: Publish (Linux - Packaging)
working-directory: "${{ env.OUTPUT_PATH_LINUX }}/Stardrop"
run: |
mv Stardrop Internal
mv Stardrop.pdb Internal.pdb
cp ${GITHUB_WORKSPACE}/Stardrop/Assets/Stardrop.sh Stardrop.sh
chmod 755 ../Stardrop
chmod +x Stardrop.sh
chmod 644 Internal
chmod 644 Internal.pdb
(cd ../ && zip -r ${GITHUB_WORKSPACE}/Stardrop-linux-x64.zip "Stardrop")
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts-linux
path: |
Stardrop-linux-x64.zip
package-windows:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'generate packages')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.BRANCH }}
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
- run: mkdir -p path/to/release-artifacts
- name: Publish (Windows - Creating & Packaging)
run: |
dotnet publish "${{ env.WORKING_DIRECTORY }}" --output "${{ env.OUTPUT_PATH_WINDOWS }}/Stardrop" --configuration "${{ env.CONFIGURATION }}" --runtime "win-x64" --framework "net8.0" --self-contained
(cd "${{ env.OUTPUT_PATH_WINDOWS }}" && zip -r ${GITHUB_WORKSPACE}/Stardrop-win-x64.zip "Stardrop")
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-artifacts-windows
path: |
Stardrop-win-x64.zip
================================================
FILE: .github/workflows/pr-target-action.yml
================================================
name: PR Target Check
on:
pull_request:
types:
- opened
- edited
- reopened
- synchronize
jobs:
is-using-correct-target:
runs-on: ubuntu-latest
steps:
- name: Check branches
run: |
if [ ${{ github.base_ref }} == "stable" ] && [ ${{ github.head_ref }} != "development" ]; then
echo "Merge requests to the stable branch are only allowed from the development branch."
exit 1
fi
================================================
FILE: .github/workflows/version-build.yml
================================================
name: Package Version Release
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
jobs:
package-and-release:
uses: ./.github/workflows/package-and-release.yml
with:
tag: ${{ github.ref_name }}
is-prerelease: ${{ contains(github.ref_name, '-beta') && 'true' || 'false' }}
generate-release-notes: true
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: CONTRIBUTING.md
================================================
If reporting an issue with Stardrop, please ensure that a copy of Stardrop's log is attached.
The log file can be found via Stardrop's menu under `View` > `Log File`.
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: README.md
================================================
# Stardrop
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.
Stadrop utilizes [SMAPI (Stardew Modding API)](https://smapi.io/) to simplify the management and update checking for all applicable Stardew Valley mods.
Profiles are also supported, allowing users to have multiple mod groups for specific gameplay or multiplayer sessions.
# Getting Started
See the [GitBook pages](https://floogen.gitbook.io/stardrop/) for detailed documentation on how to install, update and use Stardrop.
## Downloading Stardrop
See the [release page](https://github.com/Floogen/Stardrop/releases/latest) for the latest builds.
# Credits
## Translations
Stardrop has been generously translated into several languages by the following users:
* **Chinese** - guanyintu, PIut02, Z2549, CuteCat233
* **French** - xynerorias
* **German** - Schn1ek3
* **Hungarian** - martin66789
* **Italian** - S-zombie
* **Japanese** - OishiiUnichan
* **Korean** - buriburishoebill
* **Polish** - Naciux
* **Portuguese** - aracnus
* **Russian** - Rongarah
* **Spanish** - Evexyron, Gaelhaine
* **Thai** - ellipszist
* **Turkish** - KediDili
* **Ukrainian** - burunduk, ChulkyBow, DanielleTlumach
# Gallery


================================================
FILE: Stardrop/App.axaml
================================================
================================================
FILE: Stardrop/App.axaml.cs
================================================
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using Stardrop.Models.Nexus.Web;
using Stardrop.Utilities;
using Stardrop.Utilities.External;
using Stardrop.Views;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Stardrop
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
// Verify that the helper is instantiated, if it isn't then this code is likely reached by Avalonia's previewer and bypassed Main
if (Program.helper is null)
{
Program.helper = new Helper();
}
// Load in translations
Program.translation.LoadTranslations();
// Handle adding the themes
Dictionary themes = new Dictionary();
foreach (string fileFullName in Directory.EnumerateFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Themes"), "*.xaml", SearchOption.AllDirectories))
{
try
{
var themeName = Path.GetFileNameWithoutExtension(fileFullName);
themes[themeName] = AvaloniaRuntimeXamlLoader.Parse(File.ReadAllText(fileFullName));
Program.helper.Log($"Loaded theme {Path.GetFileNameWithoutExtension(fileFullName)}", Helper.Status.Debug);
}
catch (Exception ex)
{
Program.helper.Log($"Unable to load theme on {Path.GetFileNameWithoutExtension(fileFullName)}: {ex}", Helper.Status.Warning);
}
}
Current.Styles.Insert(0, !themes.ContainsKey(Program.settings.Theme) ? themes.Values.First() : themes[Program.settings.Theme]);
}
private async void OnUrlsOpen(object? sender, UrlOpenedEventArgs e, MainWindow mainWindow)
{
foreach (string? url in e.Urls.Where(u => String.IsNullOrEmpty(u) is false))
{
await mainWindow.ProcessNXMLink(new NXM() { Link = url, Timestamp = DateTime.Now });
}
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var mainWindow = new MainWindow();
desktop.MainWindow = mainWindow;
// Register events
this.UrlsOpened += (sender, e) => OnUrlsOpen(sender, e, mainWindow);
}
base.OnFrameworkInitializationCompleted();
}
}
}
================================================
FILE: Stardrop/Assets/Info.plist
================================================
CFBundleInfoDictionaryVersion6.0CFBundlePackageTypeAPPLCFBundleIconFileStardropCFBundleSignaturecom.Floogen.StardropCFBundleNameStardropCFBundleExecutableStardropCFBundleIdentifierstardropCFBundleVersion1.0.0CFBundleShortVersionString1.0CFBundleURLTypesCFBundleURLNameNXMCFBundleURLSchemesnxmNSHighResolutionCapabletrue
================================================
FILE: Stardrop/Assets/Stardrop.sh
================================================
#!/usr/bin/env bash
chmod u+x ./Internal
./Internal
================================================
FILE: Stardrop/Converters/EnumConverter.cs
================================================
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace Stardrop.Converters
{
public class EnumConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Enum.GetName((value.GetType()), value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
================================================
FILE: Stardrop/Converters/EnumEqualsConverter.cs
================================================
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace Stardrop.Converters
{
public class EnumEqualsConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value?.GetType()?.IsEnum is not true)
{
throw new ArgumentOutOfRangeException(nameof(value), "Value must be a non-null enum.");
}
if (parameter?.GetType()?.IsEnum is not true)
{
throw new ArgumentOutOfRangeException(nameof(parameter), "Parameter must be a non-null enum.");
}
return Enum.Equals(value, parameter);
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
================================================
FILE: Stardrop/Models/Config.cs
================================================
using System;
namespace Stardrop.Models
{
public class Config
{
public string UniqueId { get; set; }
public string FilePath { get; set; }
public DateTime LastWriteTimeUtc { get; set; }
public string Data { get; set; }
}
}
================================================
FILE: Stardrop/Models/Data/ClientData.cs
================================================
using System.Collections.Generic;
namespace Stardrop.Models.Data
{
public class ClientData
{
public List ModInstallData { get; set; }
public Dictionary ColumnActiveStates { get; set; } = new Dictionary();
public LastSessionData LastSessionData { get; set; }
}
}
================================================
FILE: Stardrop/Models/Data/Enums/Choice.cs
================================================
namespace Stardrop.Models.Data.Enums
{
public enum Choice
{
First,
Second,
Third
}
}
================================================
FILE: Stardrop/Models/Data/Enums/DisplayFilter.cs
================================================
namespace Stardrop.Models.Data.Enums
{
public enum DisplayFilter
{
None,
ShowEnabled,
ShowDisabled,
RequireConfig
}
}
================================================
FILE: Stardrop/Models/Data/Enums/EndorsementResponse.cs
================================================
namespace Stardrop.Models.Data.Enums
{
public enum InstallState
{
Unknown,
Downloading,
Installing
}
}
================================================
FILE: Stardrop/Models/Data/Enums/InstallState.cs
================================================
namespace Stardrop.Models.Data.Enums
{
public enum EndorsementResponse
{
Unknown,
IsOwnMod,
TooSoonAfterDownload,
NotDownloadedMod,
Abstained,
Endorsed
}
}
================================================
FILE: Stardrop/Models/Data/Enums/ModGrouping.cs
================================================
using System.ComponentModel;
namespace Stardrop.Models.Data.Enums
{
public enum ModGrouping
{
None,
Folder,
[Description("Content Pack")]
ContentPack,
[Description("Folder (Condensed)")]
FolderCondensed
}
}
================================================
FILE: Stardrop/Models/Data/Enums/NexusServers.cs
================================================
using System.ComponentModel;
namespace Stardrop.Models.Data.Enums
{
public enum NexusServers
{
[Description("Nexus CDN")]
NexusCDN,
Chicago,
Paris,
Amsterdam,
Prague,
[Description("Los Angeles")]
LosAngeles,
Miami,
Singapore
}
}
================================================
FILE: Stardrop/Models/Data/LastSessionData.cs
================================================
using Avalonia;
using System;
namespace Stardrop.Models.Data
{
public class LastSessionData
{
public double Height { get; set; } = 800;
public double Width { get; set; } = 1430;
public int PositionX { get; set; }
public int PositionY { get; set; }
}
}
================================================
FILE: Stardrop/Models/Data/ModDownloadEvents.cs
================================================
using System;
using System.Threading;
namespace Stardrop.Models.Data
{
internal record ModDownloadStartedEventArgs(Uri Uri, string Name, long? Size, CancellationTokenSource DownloadCancellationSource);
internal record ModDownloadProgressEventArgs(Uri Uri, long TotalBytes);
internal record ModDownloadCompletedEventArgs(Uri Uri);
internal record ModDownloadFailedEventArgs(Uri Uri);
}
================================================
FILE: Stardrop/Models/Data/ModInstallData.cs
================================================
using System;
namespace Stardrop.Models.Data
{
public class ModInstallData
{
public string UniqueId { get; set; }
public DateTime InstallTimestamp { get; set; }
public DateTime? LastUpdateTimestamp { get; set; }
}
}
================================================
FILE: Stardrop/Models/Data/ModKeyInfo.cs
================================================
namespace Stardrop.Models.Data
{
public class ModKeyInfo
{
public string UniqueId { get; set; }
public string Name { get; set; }
public string PageUrl { get; set; }
}
}
================================================
FILE: Stardrop/Models/Data/ModUpdateInfo.cs
================================================
using static Stardrop.Models.SMAPI.Web.ModEntryMetadata;
namespace Stardrop.Models.Data
{
public class ModUpdateInfo
{
public string UniqueId { get; set; }
public string SuggestedVersion { get; set; }
public WikiCompatibilityStatus Status { get; set; }
public string Link { get; set; }
public ModUpdateInfo()
{
}
public ModUpdateInfo(string uniqueId, string recommendedVersion, WikiCompatibilityStatus status, string link)
{
UniqueId = uniqueId;
SuggestedVersion = recommendedVersion;
Status = status;
Link = link;
}
}
}
================================================
FILE: Stardrop/Models/Data/PairedKeys.cs
================================================
namespace Stardrop.Models.Data
{
public class PairedKeys
{
public byte[] Lock { get; set; }
public byte[] Vector { get; set; }
}
}
================================================
FILE: Stardrop/Models/Data/UpdateCache.cs
================================================
using System;
using System.Collections.Generic;
namespace Stardrop.Models.Data
{
public class UpdateCache
{
public DateTime LastRuntime { get; set; }
public List Mods { get; set; }
public UpdateCache(DateTime lastRuntime)
{
LastRuntime = lastRuntime;
Mods = new List();
}
}
}
================================================
FILE: Stardrop/Models/Mod.cs
================================================
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Shared.PlatformSupport;
using Semver;
using Stardrop.Models.Data.Enums;
using Stardrop.Models.SMAPI;
using Stardrop.Utilities;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using static Stardrop.Models.SMAPI.Web.ModEntryMetadata;
namespace Stardrop.Models
{
public class Mod : INotifyPropertyChanged
{
internal readonly FileInfo ModFileInfo;
internal readonly Manifest Manifest;
public string UniqueId { get; set; }
public SemVersion Version { get; set; }
public string ParsedVersion { get { return Version.ToString(); } }
public string SuggestedVersion { get; set; }
public string Name { get; set; }
public string Path { get { return _path; } set { _path = value; RootPath = GetRootPath(value); } } // Whole mod path inside installed mods path for grouping mod components in the same mod
private string _path { get; set; }
public string RootPath { get; private set; } // Root mod path inside installed mods path for grouping mod components in the same mod
public string ManifestFilePath { get { return ModFileInfo.FullName; } }
public string Description { get; set; }
public string Summary { get { return $"Author: {Author}\nVersion: {ParsedVersion}\nHas Config: {HasConfig}\n\n{Description}"; } }
public string Author { get; set; }
public DateTime? InstallTimestamp { get; set; }
public DateTime? LastUpdateTimestamp { get; set; }
public Config? _config { get; set; }
public Config? Config { get { return _config; } set { _config = value; NotifyPropertyChanged("Config"); NotifyPropertyChanged("HasConfig"); } }
public bool HasConfig { get { return Config is not null; } }
public string FrameworkID { get; set; } = string.Empty;
private List _requirements { get; set; }
public List Requirements { get { return _requirements; } set { _requirements = value; NotifyPropertyChanged("Requirements"); NotifyPropertyChanged("MissingRequirements"); NotifyPropertyChanged("HardRequirements"); } }
public List MissingRequirements { get { return _requirements is null ? null : _requirements.Where(r => !String.IsNullOrEmpty(r.Name) && r.IsMissing && r.IsRequired).ToList(); } }
public List HardRequirements { get { return _requirements is null ? null : _requirements.Where(r => !String.IsNullOrEmpty(r.Name) && !r.IsMissing && r.IsRequired).ToList(); } }
private string _updateUri { get; set; }
public string UpdateUri { get { return _updateUri; } set { _updateUri = value; NotifyPropertyChanged("UpdateUri"); } }
private string _modPageUri { get; set; }
public string ModPageUri { get { return _modPageUri; } set { _modPageUri = value; NotifyPropertyChanged("ModPageUri"); } }
public int? NexusModId { get { return GetNexusId(); } }
private string? _nexusModThumbnailPath { get; set; }
public string? NexusModThumbnailPath { get { return _nexusModThumbnailPath; } set { _nexusModThumbnailPath = value; NexusModThumbnailFile = TryLoadThumbnail(value); NotifyPropertyChanged("NexusModThumbnailFile"); } }
public Bitmap? NexusModThumbnailFile { get; set; }
private bool _isEnabled { get; set; }
public bool IsEnabled
{
get { return _isEnabled; }
set
{
_isEnabled = value;
NotifyPropertyChanged("IsEnabled");
NotifyPropertyChanged("ChangeStateText");
NotifyPropertyChanged("ChangeWholeModGroupStateText");
}
}
private bool _isHidden { get; set; }
public bool IsHidden { get { return _isHidden; } set { _isHidden = value; NotifyPropertyChanged("IsHidden"); } }
private bool _isEndorsement { get; set; }
public bool IsEndorsed { get { return _isEndorsement; } set { _isEndorsement = value; NotifyPropertyChanged("IsEndorsed"); } }
public string ChangeStateText { get { return IsEnabled ? Program.translation.Get("internal.disable") : Program.translation.Get("internal.enable"); } }
public string ChangeWholeModGroupStateText { get { return IsEnabled ? Program.translation.Get("internal.disable_whole_mod") : Program.translation.Get("internal.enable_whole_mod"); } }
private WikiCompatibilityStatus _status { get; set; }
public WikiCompatibilityStatus Status { get { return _status; } set { _status = value; NotifyPropertyChanged("Status"); NotifyPropertyChanged("ParsedStatus"); NotifyPropertyChanged("InstallStatus"); } }
public string ParsedStatus
{
get
{
if (!String.IsNullOrEmpty(SuggestedVersion) && IsModOutdated(SuggestedVersion))
{
if (_status == WikiCompatibilityStatus.Unofficial)
{
return String.Format(Program.translation.Get("ui.main_window.hyperlinks.unofficial_update_available"), SuggestedVersion);
}
return String.Format(Program.translation.Get("ui.main_window.hyperlinks.update_available"), SuggestedVersion);
}
else if (_status == WikiCompatibilityStatus.Broken)
{
return Program.translation.Get("ui.main_window.hyperlinks.broken_compatibility_issue");
}
return String.Empty;
}
}
private InstallState _installState { get; set; }
public InstallState InstallState { get { return _installState; } set { _installState = value; NotifyPropertyChanged("InstallState"); NotifyPropertyChanged("InstallStatus"); } }
public string InstallStatus
{
get
{
if (!String.IsNullOrEmpty(SuggestedVersion) && IsModOutdated(SuggestedVersion))
{
var nexusModId = GetNexusId();
if (_status == WikiCompatibilityStatus.Unofficial || nexusModId is null)
{
return String.Empty;
}
else if (InstallState == InstallState.Unknown)
{
return Program.translation.Get("ui.main_window.hyperlinks.install_update");
}
return InstallState == InstallState.Downloading ? Program.translation.Get("ui.main_window.hyperlinks.downloading") : Program.translation.Get("ui.main_window.hyperlinks.installing");
}
return String.Empty;
}
}
public event PropertyChangedEventHandler? PropertyChanged;
public Mod(Manifest manifest, FileInfo modFileInfo, string uniqueId, string version, string? name = null, string? description = null, string? author = null)
{
Manifest = manifest;
ModFileInfo = modFileInfo;
UniqueId = uniqueId;
Version = SemVersion.TryParse(version, SemVersionStyles.Any, out var parsedVersion) ? parsedVersion : SemVersion.ParsedFrom(0, 0, 0, "bad-version");
Name = String.IsNullOrEmpty(name) ? uniqueId : name;
Path = ComputeModPath(modFileInfo);
Description = String.IsNullOrEmpty(description) ? String.Empty : description;
Author = String.IsNullOrEmpty(author) ? Program.translation.Get("internal.unknown") : author;
Requirements = new List();
}
///
/// Compute relative path to a mod from the installed mods path or default Stardew Valley mods path.
///
private string ComputeModPath(FileInfo modFileInfo)
{
// Set whole mod path for grouping with other mods from the same mod.
var commonNameInstalledFolder = Program.settings.ModInstallPath;
var commonNameModsFolder = Program.settings.ModFolderPath;
string modNamePath;
if (System.IO.Path.EndsInDirectorySeparator(commonNameInstalledFolder))
{
commonNameInstalledFolder += System.IO.Path.DirectorySeparatorChar;
}
if (modFileInfo.DirectoryName.Contains(commonNameModsFolder))
{
// Mod inside default Stardew Valley mods folder.
modNamePath = modFileInfo.DirectoryName.Substring(commonNameModsFolder.Length + 1);
}
else
{
throw new Exception($"Invalid mod folder path: {modFileInfo.DirectoryName}");
}
// TODO: Add program config option to switch between both approaches? And to disable grouping entirely?
// For top-level folder grouping.
// Producing group "automation" as a single group for both "automation/Automate" and
// "automation/Producer Framework Mod".
// var foundIndex = modNamePath.IndexOf(System.IO.Path.DirectorySeparatorChar);
// For subfolders-specific grouping.
// Producing groups "automation/Automate" (with mods `[CP] Automate/manifest.json`, `[JA] Automate/manifest.json`)
// and "automation/Producer Framework Mod" (with mods `[CP] PFM` and `[JA] PFM`) folders as separate groups.
var foundIndex = modNamePath.LastIndexOf(System.IO.Path.DirectorySeparatorChar);
var nameLength = foundIndex == -1 ? modNamePath.Length : foundIndex;
var finalPath = modNamePath.Substring(0, nameLength);
return String.IsNullOrEmpty(finalPath) ? Program.translation.Get("internal.unknown") : finalPath;
}
private string GetRootPath(string path)
{
var foundIndex = path.IndexOf(System.IO.Path.DirectorySeparatorChar);
if (foundIndex == -1)
{
return path;
}
return path.Substring(0, foundIndex);
}
public bool IsModOutdated(string version)
{
if (String.IsNullOrEmpty(version) || !HasValidVersion())
{
return false;
}
return SemVersion.Parse(version, SemVersionStyles.Any).CompareSortOrderTo(Version) > 0;
}
public bool HasValidVersion()
{
if (Version.Prerelease.Equals("bad-version", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
public bool HasUpdateKeys()
{
if (Manifest is not null && Manifest.UpdateKeys is not null && !Manifest.UpdateKeys.Any(k => String.IsNullOrEmpty(k)))
{
return true;
}
return false;
}
public int? GetNexusId()
{
if (HasUpdateKeys() is false)
{
return null;
}
foreach (string key in Manifest.UpdateKeys)
{
string cleanedKey = String.Concat(key.Where(c => !Char.IsWhiteSpace(c)));
var match = Regex.Match(key, @"Nexus:[^0-9-]*(?-?\d+)(?\@.*)?.*");
if (match.Success)
{
if (Int32.TryParse(match.Groups["modId"].ToString(), out int modId) && modId > 0)
{
return modId;
}
}
}
return null;
}
public string? GetNexusFlag()
{
if (HasUpdateKeys() is false)
{
return null;
}
foreach (string key in Manifest.UpdateKeys)
{
string cleanedKey = String.Concat(key.Where(c => !Char.IsWhiteSpace(c)));
var match = Regex.Match(key, @"Nexus:[^0-9-]*(?-?\d+)(?\@.*)?.*");
if (match.Success)
{
if (match.Groups.ContainsKey("flag"))
{
return match.Groups["flag"].ToString();
}
}
}
return null;
}
private Bitmap? TryLoadThumbnail(string? filePath)
{
if (string.IsNullOrEmpty(filePath))
{
return null;
}
try
{
var thumbnail = new Bitmap(filePath);
return thumbnail;
}
catch (Exception ex)
{
Program.helper.Log($"Failed to load thumbnail for mod {UniqueId} using following path {filePath}");
return null;
}
}
internal void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
================================================
FILE: Stardrop/Models/Nexus/NexusUser.cs
================================================
namespace Stardrop.Models.Nexus
{
public class NexusUser
{
public string Username { get; set; }
public bool IsPremium { get; set; }
public byte[] Key { get; set; }
public NexusUser()
{
}
public NexusUser(string username, byte[] key)
{
Username = username;
Key = key;
}
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/DownloadLink.cs
================================================
using System.Text.Json.Serialization;
namespace Stardrop.Models.Nexus.Web
{
public class DownloadLink
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("short_name")]
public string? ShortName { get; set; }
[JsonPropertyName("URI")]
public string? Uri { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/Endorsement.cs
================================================
using System.Text.Json.Serialization;
namespace Stardrop.Models.Nexus.Web
{
public class EndorsementResult
{
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("status")]
public string? Status { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/EndorsementResult.cs
================================================
using System.Text.Json.Serialization;
namespace Stardrop.Models.Nexus.Web
{
public class Endorsement
{
[JsonPropertyName("mod_id")]
public int Id { get; set; }
[JsonPropertyName("domain_name")]
public string? DomainName { get; set; }
[JsonPropertyName("status")]
public string? Status { get; set; }
public bool IsEndorsed()
{
if (Status?.ToUpper() == "ENDORSED")
{
return true;
}
return false;
}
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/ModDetails.cs
================================================
using System.Text.Json.Serialization;
namespace Stardrop.Models.Nexus.Web
{
public class ModDetails
{
[JsonPropertyName("name")]
public string? Name { get; set; }
[JsonPropertyName("picture_url")]
public string? ThumbnailUrl { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/ModFile.cs
================================================
using System.Text.Json.Serialization;
namespace Stardrop.Models.Nexus.Web
{
public class ModFile
{
[JsonPropertyName("file_id")]
public int Id { get; set; }
[JsonPropertyName("file_name")]
public string? Name { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("version")]
public string? Version { get; set; }
[JsonPropertyName("category_name")]
public string? Category { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/ModFiles.cs
================================================
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Stardrop.Models.Nexus.Web
{
public class ModFiles
{
[JsonPropertyName("files")]
public List Files { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/NXM.cs
================================================
using System;
namespace Stardrop.Models.Nexus.Web
{
public class NXM
{
public string? Link { get; set; }
public DateTime Timestamp { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/NexusConnectionResult.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Stardrop.Models.Nexus.Web
{
public class NexusConnectionResult
{
public string? Error { get; set; }
public string? Message { get; set; }
public string? ApiKey { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/Validate.cs
================================================
using System.Text.Json.Serialization;
namespace Stardrop.Models.Nexus.Web
{
public class Validate
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("is_premium")]
public bool IsPremium { get; set; }
[JsonPropertyName("profile_url")]
public string ProfileUrl { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/WebsocketResponse.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Stardrop.Models.Nexus.Web
{
public class WebsocketResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("data")]
public WebsocketResponseData? Data { get; set; }
}
}
================================================
FILE: Stardrop/Models/Nexus/Web/WebsocketResponseData.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Stardrop.Models.Nexus.Web
{
public class WebsocketResponseData
{
[JsonPropertyName("connection_token")]
public string? ConnectionToken { get; set; }
[JsonPropertyName("api_key")]
public string? ApiKey { get; set; }
}
}
================================================
FILE: Stardrop/Models/Profile.cs
================================================
using System.Collections.Generic;
using System.Text.Json;
namespace Stardrop.Models
{
public class Profile
{
public string Name { get; set; }
public bool IsProtected { get; set; }
public List EnabledModIds { get; set; }
public Dictionary PreservedModConfigs { get; set; }
public Profile()
{
Name = "Unknown";
IsProtected = false;
EnabledModIds = new List();
PreservedModConfigs = new Dictionary();
}
public Profile(string name, bool isProtected = false, List? enabledMods = null, Dictionary? preservedModConfigs = null)
{
Name = name;
IsProtected = isProtected;
EnabledModIds = enabledMods is null ? new List() : enabledMods;
PreservedModConfigs = preservedModConfigs is null ? new Dictionary() : preservedModConfigs;
}
public Profile ShallowCopy()
{
return (Profile)this.MemberwiseClone();
}
}
}
================================================
FILE: Stardrop/Models/SMAPI/Converters/BooleanConverter.cs
================================================
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Stardrop.Models.SMAPI.Converters
{
internal class BooleanConverter : JsonConverter
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False)
{
return reader.GetBoolean();
}
string value = reader.GetString();
if (Boolean.TryParse(value, out bool result))
{
return result;
}
return false;
}
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
throw new InvalidOperationException("This converter should not be used to write, it is read only.");
}
}
}
================================================
FILE: Stardrop/Models/SMAPI/Converters/BooleanConverterAssumeTrue.cs
================================================
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Stardrop.Models.SMAPI.Converters
{
internal class BooleanConverterAssumeTrue : JsonConverter
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False)
{
return reader.GetBoolean();
}
string value = reader.GetString();
if (Boolean.TryParse(value, out bool result))
{
return result;
}
return true;
}
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
throw new InvalidOperationException("This converter should not be used to write, it is read only.");
}
}
}
================================================
FILE: Stardrop/Models/SMAPI/Converters/ModKeyConverter.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Stardrop.Models.SMAPI.Converters
{
internal class ModKeyConverter : JsonConverter
{
public override string[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
var modKeys = new List();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
return modKeys.ToArray();
}
if (reader.TokenType == JsonTokenType.Number)
{
modKeys.Add($"Nexus: {reader.GetInt32()}");
}
else
{
modKeys.Add(reader.GetString());
}
}
// Should not reach here, due to reader.TokenType == JsonTokenType.EndArray
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
{
throw new InvalidOperationException("This converter should not be used to write, it is read only.");
}
}
}
================================================
FILE: Stardrop/Models/SMAPI/GameDetails.cs
================================================
using Semver;
using System;
namespace Stardrop.Models.SMAPI
{
public class GameDetails
{
public enum OS
{
Unknown,
Linux,
Mac,
Windows
}
/// Stardew Valley's game version.
public string GameVersion { get; set; }
/// SMAPI's version.
public string SmapiVersion { get; set; }
/// The operating system.
public OS System { get; set; }
public GameDetails()
{
}
public GameDetails(string gameVersion, string smapiVersion, string system)
{
GameVersion = gameVersion;
if (GameVersion.Contains(' '))
{
GameVersion = GameVersion.Split(' ')[0];
}
SmapiVersion = smapiVersion;
if (system.Contains("macOS", StringComparison.OrdinalIgnoreCase))
{
System = OS.Mac;
}
else if (system.Contains("Windows", StringComparison.OrdinalIgnoreCase))
{
System = OS.Windows;
}
else
{
System = OS.Linux;
}
}
public bool HasSMAPIUpdated(string version)
{
if (String.IsNullOrEmpty(version))
{
return false;
}
return HasSMAPIUpdated(SemVersion.Parse(version));
}
public bool HasSMAPIUpdated(SemVersion version)
{
if (version is null)
{
return false;
}
return version != SemVersion.Parse(SmapiVersion);
}
public bool HasBadGameVersion()
{
if (GameVersion.Contains(' '))
{
return true;
}
return false;
}
}
}
================================================
FILE: Stardrop/Models/SMAPI/Manifest.cs
================================================
using Stardrop.Models.SMAPI.Converters;
using System.Text.Json.Serialization;
namespace Stardrop.Models.SMAPI
{
public class Manifest
{
// Based on SMAPI's Manfiest.cs: https://github.com/Pathoschild/SMAPI/blob/c10685b03574e967c1bf48aafc814f60196812ec/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
/// The mod name.
public string Name { get; set; }
/// A brief description of the mod.
public string Description { get; set; }
/// The namespaced mod IDs to query for updates (like Nexus:541).
[JsonConverter(typeof(ModKeyConverter))]
public string[] UpdateKeys { get; set; }
/// The mod author's name.
public string Author { get; set; }
/// The mod version.
public string Version { get; set; }
/// The unique mod ID.
public string UniqueID { get; set; }
/// The mod which will read this as a content pack. Mutually exclusive with .
//[JsonConverter(typeof(ManifestContentPackForConverter))]
public ManifestContentPackFor ContentPackFor { get; set; }
/// The other mods that must be loaded before this mod.
//[JsonConverter(typeof(ManifestDependencyArrayConverter))]
public ManifestDependency[] Dependencies { get; set; }
/// Custom property for Stardrop.
public bool DeleteOldVersion { get; set; }
/// Custom property for Stardrop.
public string? UpdateCautionMessage { get; set; }
}
}
================================================
FILE: Stardrop/Models/SMAPI/ManifestContentPackFor.cs
================================================
namespace Stardrop.Models.SMAPI
{
public class ManifestContentPackFor
{
// Based on SMAPI's IManifestContentPackFor.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs
/// The unique ID of the mod which can read this content pack.
public string UniqueID { get; set; }
/// The minimum required version (if any).
public string MinimumVersion { get; set; }
}
}
================================================
FILE: Stardrop/Models/SMAPI/ManifestDependency.cs
================================================
using Stardrop.Models.SMAPI.Converters;
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace Stardrop.Models.SMAPI
{
public class ManifestDependency : INotifyPropertyChanged
{
// Based on SMAPI's IManifestDependency.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit.CoreInterfaces/IManifestDependency.cs
/// The unique mod ID to require.
public string UniqueID { get; set; }
/// The minimum required version (if any).
public string MinimumVersion { get; set; }
/// Whether the dependency must be installed to use the mod.
[JsonConverter(typeof(BooleanConverterAssumeTrue))]
public bool IsRequired { get; set; }
// Custom properties for Stardrop.
private string _name { get; set; }
public string Name { get { return _name; } set { _name = value; NotifyPropertyChanged("Name"); NotifyPropertyChanged("GenericLink"); } }
public bool IsMissing { get; set; }
public string GenericLink { get { return $"https://smapi.io/mods#{Name.Replace(" ", "_")}"; } }
public event PropertyChangedEventHandler? PropertyChanged;
public ManifestDependency(string uniqueId, string minimumVersion, bool isRequired = true)
{
UniqueID = uniqueId;
MinimumVersion = minimumVersion;
IsRequired = isRequired;
}
private void NotifyPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
================================================
FILE: Stardrop/Models/SMAPI/Web/ModEntry.cs
================================================
namespace Stardrop.Models.SMAPI.Web
{
public class ModEntry
{
// Based on SMAPI's ModEntryModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
/// The mod's unique ID.
public string Id { get; set; }
/// The update version recommended by the web API based on its version update and mapping rules.
public ModEntryVersion SuggestedUpdate { get; set; }
/// Optional extended data which isn't needed for update checks.
public ModEntryMetadata Metadata { get; set; }
/// The errors that occurred while fetching update data.
public string[] Errors { get; set; } = new string[0];
}
}
================================================
FILE: Stardrop/Models/SMAPI/Web/ModEntryMetadata.cs
================================================
using System.Text.Json.Serialization;
namespace Stardrop.Models.SMAPI.Web
{
public class ModEntryMetadata
{
// Based on SMAPI's WikiCompatibilityStatus.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs
/// The compatibility status for a mod.
public enum WikiCompatibilityStatus
{
/// The status is unknown.
Unknown,
/// The mod is compatible.
Ok,
/// The mod is compatible if you use an optional official download.
Optional,
/// The mod is compatible if you use an unofficial update.
Unofficial,
/// The mod isn't compatible, but the player can fix it or there's a good alternative.
Workaround,
/// The mod isn't compatible.
Broken,
/// The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely.
Abandoned,
/// The mod is no longer needed and should be removed.
Obsolete
}
// Based on SMAPI's ModExtendedMetadataModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
/// The mod's display name.
public string Name { get; set; }
/// The main version.
public ModEntryVersion Main { get; set; }
/// The latest unofficial version, if newer than and .
public ModEntryVersion Unofficial { get; set; }
public string CustomUrl { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public WikiCompatibilityStatus CompatibilityStatus { get; set; }
/// The human-readable summary of the compatibility status or workaround, without HTML formatting.
public string CompatibilitySummary { get; set; }
}
}
================================================
FILE: Stardrop/Models/SMAPI/Web/ModEntryVersion.cs
================================================
namespace Stardrop.Models.SMAPI.Web
{
public class ModEntryVersion
{
// Based on SMAPI's ModEntryVersionModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
/*********
** Accessors
*********/
/// The version number.
public string Version { get; set; }
/// The mod page URL.
public string Url { get; set; }
/*********
** Public methods
*********/
/// Construct an instance.
public ModEntryVersion() { }
/// Construct an instance.
/// The version number.
/// The mod page URL.
public ModEntryVersion(string version, string url)
{
this.Version = version;
this.Url = url;
}
}
}
================================================
FILE: Stardrop/Models/SMAPI/Web/ModSearchData.cs
================================================
using System.Collections.Generic;
namespace Stardrop.Models.SMAPI.Web
{
class ModSearchData
{
// Based on SMAPI's ModSearchModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
/*********
** Accessors
*********/
/// The mods for which to find data.
public ModSearchEntry[] Mods { get; set; }
/// Whether to include extended metadata for each mod.
public bool IncludeExtendedMetadata { get; set; }
/// The SMAPI version installed by the player. This is used for version mapping in some cases.
public string ApiVersion { get; set; }
/// The Stardew Valley version installed by the player.
public string GameVersion { get; set; }
/// The OS on which the player plays.
public string Platform { get; set; }
/*********
** Public methods
*********/
/// Construct an empty instance.
public ModSearchData()
{
// needed for JSON deserializing
}
/// Construct an instance.
/// The mods to search.
/// The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.
/// The Stardew Valley version installed by the player.
/// The OS on which the player plays.
/// Whether to include extended metadata for each mod.
public ModSearchData(List mods, string apiVersion, string gameVersion, string platform, bool includeExtendedMetadata)
{
this.Mods = mods.ToArray();
this.ApiVersion = apiVersion.ToString();
this.GameVersion = gameVersion.ToString();
this.Platform = platform;
this.IncludeExtendedMetadata = includeExtendedMetadata;
}
}
}
================================================
FILE: Stardrop/Models/SMAPI/Web/ModSearchEntry.cs
================================================
using Semver;
namespace Stardrop.Models.SMAPI.Web
{
public class ModSearchEntry
{
// Based on SMAPI's ModSearchEntryModel.cs: https://github.com/Pathoschild/SMAPI/blob/develop/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
/*********
** Accessors
*********/
/// The unique mod ID.
public string Id { get; set; }
/// The namespaced mod update keys (if available).
public string[] UpdateKeys { get; set; }
/// The mod version installed by the local player. This is used for version mapping in some cases.
public string InstalledVersion { get; set; }
/*********
** Public methods
*********/
/// Construct an empty instance.
public ModSearchEntry()
{
// needed for JSON deserializing
}
/// Construct an instance.
/// The unique mod ID.
/// The version installed by the local player. This is used for version mapping in some cases.
/// The namespaced mod update keys (if available).
/// Whether the installed version is broken or could not be loaded.
public ModSearchEntry(string id, SemVersion installedVersion, string[] updateKeys, bool isBroken = false)
{
this.Id = id;
this.InstalledVersion = installedVersion.ToString();
this.UpdateKeys = updateKeys ?? new string[0];
}
}
}
================================================
FILE: Stardrop/Models/Settings.cs
================================================
using Stardrop.Models.Data.Enums;
using Stardrop.Models.Nexus;
using Stardrop.Models.SMAPI;
namespace Stardrop.Models
{
public class Settings
{
public string Theme { get; set; } = "Stardrop";
public string Language { get; set; }
public ModGrouping ModGroupingMethod { get; set; } = ModGrouping.None;
public string Version { get; set; }
public string LastSelectedProfileName { get; set; }
public string SMAPIFolderPath { get; set; }
public string ModFolderPath { get; set; }
public string ModInstallPath { get; set; }
public bool IgnoreHiddenFolders { get; set; } = true;
public bool EnableProfileSpecificModConfigs { get; set; }
public bool ShouldWriteToModConfigs { get; set; }
public bool EnableModsOnAdd { get; set; }
///
/// Whether to always ask before deleting a previous version of a mod when updating the mod.
///
public bool AlwaysAskToDelete { get; set; } = true;
public bool ShouldAutomaticallySaveProfileChanges { get; set; } = true;
public bool ShowModThumbnails { get; set; }
public NexusServers PreferredNexusServer { get; set; } = NexusServers.NexusCDN;
public bool IsAskingBeforeAcceptingNXM { get; set; } = true;
public GameDetails GameDetails { get; set; }
public NexusUser NexusDetails { get; set; }
public Settings ShallowCopy()
{
return (Settings)this.MemberwiseClone();
}
}
}
================================================
FILE: Stardrop/Models/Theme.cs
================================================
using Avalonia.Styling;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Stardrop.Models
{
public class Theme
{
public required string Name { get; set; }
public string? Author { get; set; }
public bool IsEnabled { get; set; }
public IStyle? Style { get; set; }
}
}
================================================
FILE: Stardrop/Program.cs
================================================
using Avalonia;
using Avalonia.ReactiveUI;
using Avalonia.Shared.PlatformSupport;
using CommandLine;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.MaterialDesign;
using Semver;
using Stardrop.Models;
using Stardrop.Models.Nexus;
using Stardrop.Models.Nexus.Web;
using Stardrop.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
namespace Stardrop
{
class Program
{
internal static Helper helper;
internal static Settings settings = new Settings();
internal static Translation translation = new Translation();
internal static AssetLoader assetLoader = new AssetLoader();
internal static bool onBootStartSMAPI = false;
internal static string? nxmLink = null;
internal static readonly string defaultProfileName = "Default";
internal static readonly string executablePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Stardrop.exe");
internal static readonly Regex gameDetailsPattern = new Regex(@"SMAPI (?.+) with Stardew Valley (?.+) on (?.+)");
public static string ApplicationVersion { get { return $"{_applicationVersion.WithoutMetadata()}"; } }
private static readonly SemVersion _applicationVersion = SemVersion.Parse(typeof(Program).Assembly.GetCustomAttribute().InformationalVersion, SemVersionStyles.Any);
public class Options
{
[Option("start-smapi", Required = false, HelpText = "Automatically starts SMAPI based on the last selected mod profile.")]
public bool StartSmapi { get; set; }
[Option("nxm", Required = false, HelpText = "Downloads the given NXM file from Nexus Mods.")]
public string? NXMLink { get; set; }
}
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
// Enforce the directory's path
Directory.SetCurrentDirectory(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
// Establish file and folders paths
Pathing.SetHomePath(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
// Verify if another instance is already running
if (Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).Count() > 1 && RuntimeInformation.IsOSPlatform(OSPlatform.OSX) is false)
{
helper = new Helper($"nxm", ".txt", Pathing.GetLogFolderPath());
HandleSecondaryInstance(args);
return;
}
// Set up our logger
helper = new Helper("log", ".txt", Pathing.GetLogFolderPath());
try
{
var operatingSystem = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Windows" : RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "Unix" : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "macOS" : "Unknown";
helper.Log($"{Environment.NewLine}-- Startup Data --{Environment.NewLine}Time: {DateTime.Now}{Environment.NewLine}OS: [{operatingSystem}] {RuntimeInformation.OSDescription}{Environment.NewLine}Settings Directory: {Pathing.defaultHomePath}{Environment.NewLine}Active Directory: {Directory.GetCurrentDirectory()}{Environment.NewLine}Version: v{ApplicationVersion}{Environment.NewLine}----------------------{Environment.NewLine}");
helper.Log($"Started with the following arguments: {String.Join('|', args)}");
// Set the argument values
Parser.Default.ParseArguments(args).WithParsed(o =>
{
onBootStartSMAPI = o.StartSmapi;
nxmLink = o.NXMLink;
});
// Verify the folder paths are created
Directory.CreateDirectory(Pathing.GetCacheFolderPath());
Directory.CreateDirectory(Pathing.GetLogFolderPath());
Directory.CreateDirectory(Pathing.GetProfilesFolderPath());
Directory.CreateDirectory(Pathing.GetSelectedModsFolderPath());
Directory.CreateDirectory(Pathing.GetNexusPath());
Directory.CreateDirectory(Pathing.GetThumbnailsPath());
Directory.CreateDirectory(Pathing.GetSmapiUpgradeFolderPath());
// Verify the settings folder path is created
if (File.Exists(Pathing.GetSettingsPath()))
{
try
{
settings = JsonSerializer.Deserialize(File.ReadAllText(Pathing.GetSettingsPath()), new JsonSerializerOptions { AllowTrailingCommas = true });
}
catch (JsonException ex)
{
settings = new Settings();
helper.Log($"Reset the settings.json file as it was unreadable: {ex}", Helper.Status.Alert);
}
}
// Set the default paths
if (!String.IsNullOrEmpty(settings.ModFolderPath))
{
Pathing.SetSmapiPath(settings.SMAPIFolderPath);
Pathing.SetModPath(settings.ModFolderPath);
}
else
{
Pathing.SetSmapiPath(settings.SMAPIFolderPath, true);
settings.ModFolderPath = Pathing.defaultModPath;
}
// Set the default mod install path (for mods that are installed by Stardrop)
if (!String.IsNullOrEmpty(Pathing.defaultModPath) && String.IsNullOrEmpty(settings.ModInstallPath))
{
settings.ModInstallPath = Path.Combine(Pathing.defaultModPath, "Stardrop Installed Mods");
}
// Set the default Nexus Mods information
if (settings.NexusDetails is null)
{
settings.NexusDetails = new NexusUser();
}
// Delete any files underneath the Nexus folder
var nexusDirectory = new DirectoryInfo(Pathing.GetNexusPath());
foreach (FileInfo file in nexusDirectory.GetFiles())
{
file.Delete();
}
foreach (DirectoryInfo dir in nexusDirectory.GetDirectories())
{
dir.Delete(true);
}
// Load the translations
if (String.IsNullOrEmpty(settings.Language))
{
settings.Language = translation.GetLanguageFromAbbreviation(CultureInfo.CurrentCulture.TwoLetterISOLanguageName);
}
translation.LoadTranslations(translation.GetLanguage(settings.Language));
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
catch (Exception ex)
{
helper.Log(ex, Helper.Status.Alert);
}
}
private static void HandleSecondaryInstance(string[] args)
{
Parser.Default.ParseArguments(args).WithParsed(o =>
{
nxmLink = o.NXMLink;
});
// Verify NXM link is valid
if (String.IsNullOrEmpty(nxmLink))
{
Program.helper.Log("Given empty NXM link");
return;
}
// Write to bridge file
int attempts = 0;
Program.helper.Log("STARTING");
while (true)
{
try
{
using (FileStream stream = new FileStream(Pathing.GetLinksCachePath(), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
{
List links;
try
{
links = JsonSerializer.DeserializeAsync>(stream, new JsonSerializerOptions { AllowTrailingCommas = true }).Result;
if (links is null)
{
links = new List();
}
}
catch (JsonException ex)
{
links = new List();
}
try
{
links.Add(new NXM() { Link = nxmLink, Timestamp = DateTime.Now });
Program.helper.Log(links.Count());
stream.SetLength(0);
JsonSerializer.SerializeAsync(stream, links, new JsonSerializerOptions() { WriteIndented = true });
break;
}
catch (Exception ex)
{
Program.helper.Log(ex);
}
}
}
catch (Exception ex)
{
Program.helper.Log(ex);
}
if (attempts >= 3)
{
return;
}
else
{
attempts += 1;
Thread.Sleep(500);
}
}
Program.helper.Log("DONE");
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure()
.UseReactiveUI()
.UsePlatformDetect()
.LogToTrace()
.WithIcons(container => container
.Register());
}
}
}
================================================
FILE: Stardrop/Properties/PublishProfiles/FolderProfile - Linux.pubxml
================================================
ReleaseAny CPUpublish\linuxFileSystemnet7.0linux-x64truetrue
================================================
FILE: Stardrop/Properties/PublishProfiles/FolderProfile - MacOS.pubxml
================================================
ReleaseAny CPUpublish\macFileSystemnet7.0osx-x64truetrue
================================================
FILE: Stardrop/Properties/PublishProfiles/FolderProfile - Windows.pubxml
================================================
ReleaseAny CPUpublish\windowsFileSystemnet7.0win-x64truetruefalse
================================================
FILE: Stardrop/Stardrop.csproj
================================================
WinExenet8.0enable1.8.4-beta.2falsetrueAssets\icon.icotruetruex64x64FlexibleOptionWindow.axamlNexusInfo.axamlProfileNaming.axamlProfileEditor.axamlWarningWindow.axamlDownloadPanel.axamlDesignerMSBuild:CompileDesignerMSBuild:CompileDesignerMSBuild:CompileMSBuild:Compile
================================================
FILE: Stardrop/Stardrop.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31729.503
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stardrop", "Stardrop.csproj", "{68543B63-0EB4-43E8-9B7B-7AFA64097CAF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{68543B63-0EB4-43E8-9B7B-7AFA64097CAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68543B63-0EB4-43E8-9B7B-7AFA64097CAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68543B63-0EB4-43E8-9B7B-7AFA64097CAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68543B63-0EB4-43E8-9B7B-7AFA64097CAF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {86B7436F-BA81-4F86-B816-DB4AD0E2C918}
EndGlobalSection
EndGlobal
================================================
FILE: Stardrop/Themes/Contributors/hotcereal/CASH MONEY.xaml
================================================
#4FA57E#6AB892#74C29B#0F241C#C6E0D3#8AD3AF#3E7F62#2E5F4B#6AB892
================================================
FILE: Stardrop/Themes/Contributors/hotcereal/Cerene Dark.xaml
================================================
================================================
FILE: Stardrop/Themes/Contributors/hotcereal/Cerene Light.xaml
================================================
#F6FAFF#EEF3FF#E6EDFF#1E2433#FF5F5F#FF9F43#FFD93D#4EDEA3#3FD9FF#5C7CFA#B197FC
================================================
FILE: Stardrop/Themes/Contributors/hotcereal/Dark Cherry Chocolate.xaml
================================================
================================================
FILE: Stardrop/Themes/Contributors/hotcereal/Fairy.xaml
================================================
================================================
FILE: Stardrop/Themes/Contributors/hotcereal/Forest.xaml
================================================
================================================
FILE: Stardrop/Themes/Contributors/hotcereal/Light (Pink).xaml
================================================
================================================
FILE: Stardrop/Themes/Contributors/hotcereal/Light Cherry Chocolate.xaml
================================================
================================================
FILE: Stardrop/Themes/Dark.xaml
================================================
================================================
FILE: Stardrop/Themes/Light.xaml
================================================
================================================
FILE: Stardrop/Themes/Solarized-Lite.xaml
================================================
================================================
FILE: Stardrop/Themes/Stardrop.xaml
================================================
================================================
FILE: Stardrop/Utilities/Extension/TranslateExtension.cs
================================================
using Avalonia.Data;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.MarkupExtensions;
using System;
namespace Stardrop.Utilities.Extension
{
public class TranslateExtension : MarkupExtension
{
public TranslateExtension(string key)
{
this.Key = key;
}
public string Key { get; set; }
public string Context { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
{
var keyToUse = Key;
if (!string.IsNullOrWhiteSpace(Context))
{
keyToUse = $"{Context}/{Key}";
}
var binding = new ReflectionBindingExtension($"[{keyToUse}]")
{
Mode = BindingMode.OneWay,
Source = Program.translation,
};
return binding.ProvideValue(serviceProvider);
}
}
}
================================================
FILE: Stardrop/Utilities/External/GitHub.cs
================================================
using SharpCompress.Archives;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks;
namespace Stardrop.Utilities.External
{
static class GitHub
{
public async static Task?> GetLatestSMAPIRelease()
{
KeyValuePair? versionToUri = null;
// Create a throwaway client
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "Stardrop - SDV Mod Manager");
try
{
var response = await client.GetAsync("https://api.github.com/repos/Pathoschild/SMAPI/releases/latest");
if (response.Content is not null)
{
JsonDocument parsedContent = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
string tagName = parsedContent.RootElement.GetProperty("tag_name").ToString();
string downloadUri = parsedContent.RootElement.GetProperty("html_url").ToString();
downloadUri = String.Concat(downloadUri, "/", $"SMAPI-{tagName}-installer.zip").Replace("releases/tag/", "releases/download/");
versionToUri = new KeyValuePair(tagName, downloadUri);
}
}
catch (Exception ex)
{
Program.helper.Log($"Failed to get latest the version of SMAPI: {ex}", Helper.Status.Alert);
}
client.Dispose();
return versionToUri;
}
public async static Task DownloadLatestSMAPIRelease(string uri)
{
// Create a throwaway client
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "Stardrop - SDV Mod Manager");
string downloadedArchivePath = String.Empty;
try
{
var response = await client.GetAsync(uri);
using (var archive = ArchiveFactory.Open(await response.Content.ReadAsStreamAsync()))
{
downloadedArchivePath = Path.Combine(Pathing.GetSmapiUpgradeFolderPath(), Path.GetDirectoryName(archive.Entries.First().Key));
foreach (var entry in archive.Entries)
{
entry.WriteToDirectory(Pathing.GetSmapiUpgradeFolderPath(), new SharpCompress.Common.ExtractionOptions() { ExtractFullPath = true, Overwrite = true });
}
}
}
catch (Exception ex)
{
Program.helper.Log($"Failed to download latest the version of SMAPI: {ex}", Helper.Status.Alert);
}
client.Dispose();
return downloadedArchivePath;
}
public async static Task?> GetLatestStardropRelease()
{
KeyValuePair? versionToUri = null;
// Create a throwaway client
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "Stardrop - SDV Mod Manager");
try
{
var response = await client.GetAsync("https://api.github.com/repos/Floogen/Stardrop/releases/latest");
if (response.Content is not null)
{
JsonDocument parsedContent = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
string tagName = parsedContent.RootElement.GetProperty("tag_name").ToString();
string downloadUri = parsedContent.RootElement.GetProperty("html_url").ToString();
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
downloadUri = String.Concat(downloadUri, "/", "Stardrop-osx-x64.zip");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
downloadUri = String.Concat(downloadUri, "/", "Stardrop-linux-x64.zip");
}
else
{
downloadUri = String.Concat(downloadUri, "/", "Stardrop-win-x64.zip");
}
downloadUri = downloadUri.Replace("releases/tag/", "releases/download/");
versionToUri = new KeyValuePair(tagName, downloadUri);
}
}
catch (Exception ex)
{
Program.helper.Log($"Failed to get latest the version of Stardrop: {ex}", Helper.Status.Alert);
}
client.Dispose();
return versionToUri;
}
public async static Task DownloadLatestStardropRelease(string uri)
{
// Create a throwaway client
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "Stardrop - SDV Mod Manager");
string downloadedArchivePath = String.Empty;
try
{
var response = await client.GetAsync(uri);
using (var archive = ArchiveFactory.Open(await response.Content.ReadAsStreamAsync()))
{
foreach (var entry in archive.Entries)
{
entry.WriteToDirectory(Directory.GetCurrentDirectory(), new SharpCompress.Common.ExtractionOptions() { ExtractFullPath = true, Overwrite = true });
}
}
var extractFolderName = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Stardrop.app" : "Stardrop";
var adjustedExtractFolderName = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "~Stardrop.app" : "~Stardrop";
if (Directory.Exists(extractFolderName))
{
if (Directory.Exists(adjustedExtractFolderName))
{
Directory.Delete(adjustedExtractFolderName, true);
}
Directory.Move(extractFolderName, adjustedExtractFolderName);
}
downloadedArchivePath = Path.Combine(Directory.GetCurrentDirectory(), adjustedExtractFolderName);
}
catch (Exception ex)
{
Program.helper.Log($"Failed to download latest the version of Stardrop: {ex}", Helper.Status.Alert);
}
client.Dispose();
return downloadedArchivePath;
}
}
}
================================================
FILE: Stardrop/Utilities/External/NexusClient.cs
================================================
using Semver;
using Stardrop.Models.Data;
using Stardrop.Models.Data.Enums;
using Stardrop.Models.Nexus;
using Stardrop.Models.Nexus.Web;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace Stardrop.Utilities.External
{
public static class Nexus
{
private static readonly Uri _baseUrl = new Uri("https://api.nexusmods.com/v1/");
public static NexusClient? Client { get; private set; }
public delegate void NexusClientChangedHandler(NexusClient? oldClient, NexusClient? newClient);
public static event NexusClientChangedHandler? ClientChanged = null;
///
/// If the user has entered their Nexus API key in a previous session, this will attempt to retreive
/// it.
///
/// The key, if it exists. Null otherwise.
public static string? GetCachedKey()
{
if (Program.settings.NexusDetails?.Key is null || File.Exists(Pathing.GetNotionCachePath()) is false)
{
return null;
}
var pairedKeys = JsonSerializer.Deserialize(File.ReadAllText(Pathing.GetNotionCachePath()), new JsonSerializerOptions { AllowTrailingCommas = true });
if (pairedKeys?.Vector is null || pairedKeys?.Lock is null)
{
return null;
}
try
{
return SimpleObscure.Decrypt(Program.settings.NexusDetails.Key, pairedKeys.Lock, pairedKeys.Vector);
}
catch (Exception ex)
{
Program.helper.Log($"Failed to parse API key when requested: {ex}");
}
return null;
}
///
/// Creates a new HttpClient configured with a Nexus API key, and validates it against the Nexus API.
/// If successfully validated, sets , as well as returning a reference to it.
/// If called when a is already set, the Client will be replaced.
/// On success, fires , with the previous client (if any), and the new client.
///
/// The API key from Nexus mods that will be included in the 'apiKey' header when making calls.
/// The created client, if successful. Null otherwise.
public static async Task CreateClient(string apiKey)
{
HttpClient client = new HttpClient();
client.BaseAddress = _baseUrl;
client.DefaultRequestHeaders.Add("apiKey", apiKey);
client.DefaultRequestHeaders.Add("Application-Name", "Stardrop");
client.DefaultRequestHeaders.Add("Application-Version", Program.ApplicationVersion);
client.DefaultRequestHeaders.Add("User-Agent", $"Stardrop/{Program.ApplicationVersion} {Environment.OSVersion}");
var nexusClient = new NexusClient(client);
bool isKeyValid = await nexusClient.ValidateKey();
if (isKeyValid is false)
{
return null;
}
ClientChanged?.Invoke(oldClient: Client, newClient: nexusClient);
Client = nexusClient;
return Client;
}
///
/// Nulls out the , and fires a event
/// to give consumers a chance to clean up their event handlers.
///
public static void ClearClient()
{
ClientChanged?.Invoke(oldClient: Client, newClient: null);
Client = null;
}
}
public class NexusClient
{
private const string _nxmPattern = @"nxm:\/\/(?stardewvalley)\/mods\/(?[0-9]+)\/files\/(?[0-9]+)\?key=(?.*)&expires=(?[0-9]+)&user_id=(?[0-9]+)";
private readonly HttpClient _client;
private NexusUser _settings = null!;
internal int DailyRequestsLimit { get; private set; }
internal int DailyRequestsRemaining { get; private set; }
internal event EventHandler? DailyRequestLimitsChanged = null;
internal event EventHandler? DownloadStarted = null;
internal event EventHandler? DownloadProgressChanged = null;
internal event EventHandler? DownloadCompleted = null;
internal event EventHandler? DownloadFailed = null;
public NexusClient(HttpClient client)
{
_client = client;
}
///
/// Calls the /validate endpoint on Nexus Mods' API using the API key stored in this client upon creation.
/// If the key is successfully validated, its information is cached in .
/// Returns if validation fails.
///
public async Task ValidateKey()
{
try
{
var response = await _client.GetAsync("users/validate");
if (!response.IsSuccessStatusCode || response.Content == null)
{
Program.helper.Log($"Call to Nexus Mods failed. HTTP status code: {response.StatusCode}, {response.ReasonPhrase}");
if (response.Content == null)
{
Program.helper.Log($"No response from Nexus Mods!");
}
else
{
Program.helper.Log($"Response from Nexus Mods:\n{await response.Content.ReadAsStringAsync()}");
}
return false;
}
string content = await response.Content.ReadAsStringAsync();
Validate validationModel = JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
if (validationModel is null || String.IsNullOrEmpty(validationModel.Message) is false)
{
Program.helper.Log($"Unable to validate given API key for Nexus Mods");
Program.helper.Log($"Response from Nexus Mods:\n{content}");
return false;
}
if (Program.settings.NexusDetails is null)
{
return false;
}
Program.settings.NexusDetails.Username = validationModel.Name;
Program.settings.NexusDetails.IsPremium = validationModel.IsPremium;
_settings = Program.settings.NexusDetails;
UpdateRequestCounts(response.Headers);
return true;
}
catch (Exception ex)
{
Program.helper.Log($"Failed to validate user's API key for Nexus Mods: {ex}", Helper.Status.Alert);
return false;
}
}
public async Task GetModDetailsViaNXM(NXM nxmData)
{
if (nxmData.Link is null)
{
return null;
}
var match = Regex.Match(Regex.Unescape(nxmData.Link), _nxmPattern);
if (match.Success is false || match.Groups["domain"].ToString().ToLower() != "stardewvalley" || Int32.TryParse(match.Groups["mod"].ToString(), out int modId) is false)
{
return null;
}
try
{
var response = await _client.GetAsync($"games/stardewvalley/mods/{modId}.json");
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null)
{
string content = await response.Content.ReadAsStringAsync();
ModDetails modDetails = JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (modDetails is null)
{
Program.helper.Log($"Unable to get mod details for the mod {modId} on Nexus Mods");
Program.helper.Log($"Response from Nexus Mods:\n{content}");
return null;
}
UpdateRequestCounts(response.Headers);
return modDetails;
}
else
{
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
Program.helper.Log($"Bad status given from Nexus Mods: {response.StatusCode}");
if (response.Content is not null)
{
Program.helper.Log($"Response from Nexus Mods:\n{await response.Content.ReadAsStringAsync()}");
}
}
else if (response.Content is null)
{
Program.helper.Log($"No response from Nexus Mods!");
}
}
}
catch (Exception ex)
{
Program.helper.Log($"Unable to get mod details for the mod {modId} on Nexus Mods: {ex}", Helper.Status.Alert);
}
return null;
}
public async Task GetFileByVersion(int modId, string version, string? modFlag = null)
{
if (SemVersion.TryParse(version.Replace("v", String.Empty), SemVersionStyles.Any, out var targetVersion) is false)
{
Program.helper.Log($"Unable to parse given target version {version}");
return null;
}
Program.helper.Log($"Requesting version {version} of mod {modId}{(String.IsNullOrEmpty(modFlag) is false ? $" with flag {modFlag}" : String.Empty)}");
try
{
var response = await _client.GetAsync($"games/stardewvalley/mods/{modId}/files.json");
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null)
{
string content = await response.Content.ReadAsStringAsync();
ModFiles modFiles = JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (modFiles is null || modFiles.Files is null || modFiles.Files.Count == 0)
{
Program.helper.Log($"Unable to get the mod file for Nexus Mods");
Program.helper.Log($"Response from Nexus Mods:\n{content}");
}
else
{
ModFile? selectedFile = null;
foreach (var file in modFiles.Files.Where(x => String.IsNullOrEmpty(x.Version) is false && SemVersion.TryParse(x.Version.Replace("v", String.Empty), SemVersionStyles.Any, out var modVersion) && modVersion == targetVersion))
{
if (String.IsNullOrEmpty(modFlag) is false && ((String.IsNullOrEmpty(file.Name) is false && file.Name.Contains(modFlag, StringComparison.OrdinalIgnoreCase)) || (String.IsNullOrEmpty(file.Description) is false && file.Description.Contains(modFlag, StringComparison.OrdinalIgnoreCase))))
{
selectedFile = file;
}
else if (String.IsNullOrEmpty(modFlag) is true && String.IsNullOrEmpty(file.Category) is false && file.Category.Equals("MAIN", StringComparison.OrdinalIgnoreCase))
{
selectedFile = file;
}
}
if (selectedFile is null)
{
Program.helper.Log($"Unable to get a matching file for the mod {modId} with version {version}{(String.IsNullOrEmpty(modFlag) is false ? $" and with the flag {modFlag}" : String.Empty)} via Nexus Mods: \n{String.Join("\n", modFiles.Files.Select(m => $"{m.Name} | {m.Version}"))}");
}
UpdateRequestCounts(response.Headers);
return selectedFile;
}
}
else
{
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
Program.helper.Log($"Bad status given from Nexus Mods: {response.StatusCode}");
if (response.Content is not null)
{
Program.helper.Log($"Response from Nexus Mods:\n{await response.Content.ReadAsStringAsync()}");
}
}
else if (response.Content is null)
{
Program.helper.Log($"No response from Nexus Mods!");
}
}
}
catch (Exception ex)
{
Program.helper.Log($"Failed to get the mod file for Nexus Mods: {ex}", Helper.Status.Alert);
}
return null;
}
public async Task GetFileDownloadLink(NXM nxmData, string? serverName = null)
{
if (nxmData.Link is null)
{
return null;
}
var match = Regex.Match(Regex.Unescape(nxmData.Link), _nxmPattern);
if (match.Success is false || match.Groups["domain"].ToString().ToLower() != "stardewvalley" || Int32.TryParse(match.Groups["mod"].ToString(), out int modId) is false || Int32.TryParse(match.Groups["file"].ToString(), out int fileId) is false)
{
return null;
}
return await GetFileDownloadLink(modId, fileId, match.Groups["key"].ToString(), match.Groups["expiry"].ToString(), serverName);
}
public async Task GetFileDownloadLink(int modId, int fileId, string? nxmKey = null, string? nxmExpiry = null, string? serverName = null)
{
if (String.IsNullOrEmpty(serverName) || _settings.IsPremium is false)
{
serverName = "Nexus CDN";
}
try
{
string url = $"games/stardewvalley/mods/{modId}/files/{fileId}/download_link.json";
if (String.IsNullOrEmpty(nxmKey) is false && String.IsNullOrEmpty(nxmExpiry) is false)
{
url = $"{url}?key={nxmKey}&expires={nxmExpiry}";
}
var response = await _client.GetAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null)
{
string content = await response.Content.ReadAsStringAsync();
List downloadLinks = JsonSerializer.Deserialize>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (downloadLinks is null || downloadLinks.Count == 0)
{
Program.helper.Log($"Unable to get the download link for Nexus Mods");
Program.helper.Log($"Response from Nexus Mods:\n{content}");
}
else
{
UpdateRequestCounts(response.Headers);
var selectedFile = downloadLinks.FirstOrDefault(x => x.ShortName?.ToLower() == serverName.ToLower());
if (selectedFile is not null)
{
Program.helper.Log($"Requested download link from Nexus Mods using their {serverName} server");
return selectedFile.Uri;
}
}
}
else
{
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
Program.helper.Log($"Bad status given from Nexus Mods: {response.StatusCode}");
if (response.Content is not null)
{
Program.helper.Log($"Response from Nexus Mods:\n{await response.Content.ReadAsStringAsync()}");
}
}
else if (response.Content is null)
{
Program.helper.Log($"No response from Nexus Mods!");
}
}
}
catch (Exception ex)
{
Program.helper.Log($"Failed to get the download link for Nexus Mods: {ex}", Helper.Status.Alert);
}
return null;
}
public async Task DownloadFileAndGetPath(string uri, string fileName)
{
var requestUri = new Uri(uri);
var downloadCancellationSource = new CancellationTokenSource();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri);
try
{
var response = await _client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, downloadCancellationSource.Token);
if (response.IsSuccessStatusCode is false)
{
Program.helper.Log($"Failed to download mod file for Nexus Mods: HTTP {response.StatusCode}, {response.ReasonPhrase}", Helper.Status.Alert);
return new(DownloadResultKind.Failed, null);
}
using var fileStream = new FileStream(Path.Combine(Pathing.GetNexusPath(), fileName), FileMode.CreateNew);
using var downloadStream = await response.Content.ReadAsStreamAsync();
long? contentLength = response.Content.Headers.ContentLength;
DownloadStarted?.Invoke(this, new ModDownloadStartedEventArgs(requestUri, fileName, contentLength, downloadCancellationSource));
var buffer = new byte[81920];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await downloadStream.ReadAsync(buffer, 0, buffer.Length, downloadCancellationSource.Token)) != 0)
{
await fileStream.WriteAsync(buffer, 0, bytesRead, downloadCancellationSource.Token);
totalBytesRead += bytesRead;
DownloadProgressChanged?.Invoke(this, new ModDownloadProgressEventArgs(requestUri, totalBytesRead));
}
DownloadCompleted?.Invoke(this, new ModDownloadCompletedEventArgs(requestUri));
return new(DownloadResultKind.Success, Path.Combine(Pathing.GetNexusPath(), fileName));
}
catch (Exception ex)
{
// Delete partially downloaded file, if any.
File.Delete(Path.Combine(Pathing.GetNexusPath(), fileName));
if (ex is TaskCanceledException)
{
Program.helper.Log($"The user canceled the download from Nexus from URL {uri}", Helper.Status.Info);
return new(DownloadResultKind.UserCanceled, null);
}
else
{
Program.helper.Log($"Failed to download mod file for Nexus Mods: {ex}", Helper.Status.Alert);
DownloadFailed?.Invoke(this, new ModDownloadFailedEventArgs(requestUri));
return new(DownloadResultKind.Failed, null);
}
}
}
public async Task> GetEndorsements()
{
try
{
var response = await _client.GetAsync($"user/endorsements");
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null)
{
string content = await response.Content.ReadAsStringAsync();
List endorsements = JsonSerializer.Deserialize>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (endorsements is null)
{
Program.helper.Log($"Unable to get endorsements for Nexus Mods");
Program.helper.Log($"Response from Nexus Mods:\n{content}");
}
else
{
endorsements = endorsements.Where(e => e.DomainName?.ToLower() == "stardewvalley").ToList();
UpdateRequestCounts(response.Headers);
return endorsements;
}
}
else
{
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
Program.helper.Log($"Bad status given from Nexus Mods: {response.StatusCode}");
if (response.Content is not null)
{
Program.helper.Log($"Response from Nexus Mods:\n{await response.Content.ReadAsStringAsync()}");
}
}
else if (response.Content is null)
{
Program.helper.Log($"No response from Nexus Mods!");
}
}
}
catch (Exception ex)
{
Program.helper.Log($"Failed to get endorsements for Nexus Mods: {ex}", Helper.Status.Alert);
}
return new List();
}
public async Task SetModEndorsement(int modId, bool isEndorsed)
{
try
{
var requestPackage = new StringContent("{\"Version\":\"1.0.0\"}", Encoding.UTF8, "application/json");
var response = await _client.PostAsync($"games/stardewvalley/mods/{modId}/{(isEndorsed is true ? "endorse.json" : "abstain.json")}", requestPackage);
if (response.Content is not null)
{
string content = await response.Content.ReadAsStringAsync();
EndorsementResult endorsementResult = JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (endorsementResult is null)
{
Program.helper.Log($"Unable to set endorsement for Nexus Mods");
Program.helper.Log($"Response from Nexus Mods:\n{content}");
return EndorsementResponse.Unknown;
}
UpdateRequestCounts(response.Headers);
switch (endorsementResult.Status?.ToUpper())
{
case "ENDORSED":
return EndorsementResponse.Endorsed;
case "ABSTAINED":
return EndorsementResponse.Abstained;
case "ERROR":
var parsedMessage = endorsementResult.Message?.ToUpper();
if (parsedMessage == "IS_OWN_MOD")
{
return EndorsementResponse.IsOwnMod;
}
else if (parsedMessage == "TOO_SOON_AFTER_DOWNLOAD")
{
return EndorsementResponse.TooSoonAfterDownload;
}
else if (parsedMessage == "NOT_DOWNLOADED_MOD")
{
return EndorsementResponse.NotDownloadedMod;
}
Program.helper.Log(parsedMessage);
break;
default:
Program.helper.Log($"Unhandled status for endorsement: {endorsementResult.Status} | {endorsementResult.Message}");
break;
}
}
else
{
Program.helper.Log($"No response from Nexus Mods! Status code: {response.StatusCode}");
}
}
catch (Exception ex)
{
Program.helper.Log($"Failed to set endorsement for Nexus Mods: {ex}", Helper.Status.Alert);
}
return EndorsementResponse.Unknown;
}
public async Task DownloadThumbnail(int modId)
{
try
{
var response = await _client.GetAsync($"games/stardewvalley/mods/{modId}.json");
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null)
{
string content = await response.Content.ReadAsStringAsync();
ModDetails modDetails = JsonSerializer.Deserialize(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (modDetails is null)
{
Program.helper.Log($"Unable to get mod thumbnail for the mod {modId} on Nexus Mods");
Program.helper.Log($"Response from Nexus Mods:\n{content}");
return null;
}
UpdateRequestCounts(response.Headers);
if (string.IsNullOrEmpty(modDetails.ThumbnailUrl))
{
Program.helper.Log($"The mod {modId} does not have a valid thumbnail image available on Nexus Mods");
Program.helper.Log($"Response from Nexus Mods:\n{content}");
return null;
}
// Download the thumbnail
var thumbnailPath = Path.Combine(Pathing.GetThumbnailsPath(), $"{modId}{Path.GetExtension(modDetails.ThumbnailUrl)}");
using var fileStream = new FileStream(thumbnailPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 8192, useAsync: true);
using var downloadStream = await _client.GetStreamAsync(modDetails.ThumbnailUrl);
await downloadStream.CopyToAsync(fileStream);
await fileStream.FlushAsync();
return thumbnailPath;
}
else
{
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
Program.helper.Log($"Bad status given from Nexus Mods: {response.StatusCode}");
if (response.Content is not null)
{
Program.helper.Log($"Response from Nexus Mods:\n{await response.Content.ReadAsStringAsync()}");
}
}
else if (response.Content is null)
{
Program.helper.Log($"No response from Nexus Mods!");
}
}
}
catch (Exception ex)
{
Program.helper.Log($"Unable to get mod thumbnail for the mod {modId} on Nexus Mods: {ex}", Helper.Status.Alert);
}
return null;
}
private void UpdateRequestCounts(HttpResponseHeaders headers)
{
if (headers.TryGetValues("x-rl-daily-limit", out var limitValues) && Int32.TryParse(limitValues.First(), out int dailyLimit))
{
DailyRequestsLimit = dailyLimit;
}
if (headers.TryGetValues("x-rl-daily-remaining", out var remainingValues) && Int32.TryParse(remainingValues.First(), out int dailyRemaining))
{
DailyRequestsRemaining = dailyRemaining;
}
DailyRequestLimitsChanged?.Invoke(this, EventArgs.Empty);
}
}
}
================================================
FILE: Stardrop/Utilities/External/NexusDownloadResult.cs
================================================
namespace Stardrop.Utilities.External
{
public enum DownloadResultKind
{
Failed,
UserCanceled,
Success
}
public record struct NexusDownloadResult(DownloadResultKind ResultKind, string? DownloadedModFilePath);
}
================================================
FILE: Stardrop/Utilities/External/SMAPI.cs
================================================
using Semver;
using Stardrop.Models;
using Stardrop.Models.SMAPI;
using Stardrop.Models.SMAPI.Web;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Stardrop.Utilities.External
{
static class SMAPI
{
internal static bool IsRunning = false;
internal static Process Process;
public static ProcessStartInfo GetPrepareProcess(bool hideConsole)
{
var smapiInfo = new FileInfo(Pathing.GetSmapiPath());
var fileName = smapiInfo.FullName;
var arguments = string.Empty;
var parsedModPath = string.Empty;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) is true)
{
fileName = "/usr/bin/env";
arguments = $"bash -c \"SMAPI_MODS_PATH='{Pathing.GetSelectedModsFolderPath()}' '{Pathing.GetSmapiPath().Replace("StardewModdingAPI.dll", "StardewValley")}'\"";
parsedModPath = $"'{Pathing.GetSelectedModsFolderPath()}'";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) is true)
{
fileName = "/usr/bin/open";
arguments = $"-a \"Terminal\" \"{Pathing.GetSmapiPath().Replace("StardewModdingAPI.dll", "StardewModdingAPI")}\" --args --mods-path \"{Pathing.GetSelectedModsFolderPath()}\"";
parsedModPath = $"{Pathing.GetSelectedModsFolderPath()}";
/* Alternative route (using AppleScript) of activating Terminal + SMAPI
fileName = "/usr/bin/env";
arguments = $@"osascript -e ""tell application \""Terminal\""
set smapi_path to \""{Pathing.GetSmapiPath().Replace("StardewModdingAPI.dll", "StardewModdingAPI")}\""
set mods_path to \""{Pathing.GetSelectedModsFolderPath()}\""
activate
do script \""\"" & quoted form of the POSIX path of smapi_path & \"" --mods-path \"" & quoted form of the POSIX path of mods_path
end tell""";
*/
}
else
{
parsedModPath = Pathing.GetSelectedModsFolderPath();
}
Program.helper.Log($"Starting SMAPI with the following arguments: {arguments}");
var processInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = smapiInfo.DirectoryName,
RedirectStandardOutput = false,
RedirectStandardError = false,
CreateNoWindow = hideConsole,
UseShellExecute = false
};
// Set SMAPI_MODS_PATH EnvironmentVariable if required
if (string.IsNullOrEmpty(parsedModPath) is false)
{
Program.helper.Log($"Setting SMAPI_MODS_PATH to: {parsedModPath}");
processInfo.EnvironmentVariables["SMAPI_MODS_PATH"] = parsedModPath;
Program.helper.Log($"Process SMAPI_MODS_PATH: {processInfo.EnvironmentVariables["SMAPI_MODS_PATH"]}");
Program.helper.Log($"System SMAPI_MODS_PATH: {Environment.GetEnvironmentVariable("SMAPI_MODS_PATH")}");
}
return processInfo;
}
public static string GetProcessName()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return "StardewModdingA";
}
return "StardewModdingAPI";
}
public async static Task> GetModUpdateData(GameDetails gameDetails, List mods)
{
List searchEntries = new List();
foreach (var mod in mods.Where(m => m.HasValidVersion() && m.HasUpdateKeys()))
{
searchEntries.Add(new ModSearchEntry(mod.UniqueId, mod.Version, mod.Manifest.UpdateKeys));
}
foreach (var requirementKey in mods.SelectMany(m => m.Requirements))
{
if (!searchEntries.Any(e => e.Id.Equals(requirementKey.UniqueID, StringComparison.OrdinalIgnoreCase)))
{
searchEntries.Add(new ModSearchEntry() { Id = requirementKey.UniqueID });
}
}
// Create the body to be sent via the POST request
ModSearchData searchData = new ModSearchData(searchEntries, gameDetails.SmapiVersion, gameDetails.GameVersion, gameDetails.System.ToString(), true);
// Create a throwaway client
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Add("Application-Name", "Stardrop");
client.DefaultRequestHeaders.Add("Application-Version", Program.ApplicationVersion);
client.DefaultRequestHeaders.Add("User-Agent", $"Stardrop/{Program.ApplicationVersion} {Environment.OSVersion}");
var parsedRequest = JsonSerializer.Serialize(searchData, new JsonSerializerOptions() { WriteIndented = true, IgnoreNullValues = true });
var requestPackage = new StringContent(parsedRequest, Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://smapi.io/api/v3.0/mods", requestPackage);
List modUpdateData = new List();
if (response.StatusCode == System.Net.HttpStatusCode.OK && response.Content is not null)
{
// In the name of the Nine Divines, why is JsonSerializer.Deserialize case sensitive by default???
string content = await response.Content.ReadAsStringAsync();
modUpdateData = JsonSerializer.Deserialize>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (modUpdateData is null || modUpdateData.Count == 0)
{
Program.helper.Log($"Mod update data was not parsable from smapi.io");
Program.helper.Log($"Response from smapi.io:\n{content}");
Program.helper.Log($"Our request to smapi.io:\n{parsedRequest}");
}
}
else
{
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
Program.helper.Log($"Bad status given from smapi.io: {response.StatusCode}");
if (response.Content is not null)
{
Program.helper.Log($"Response from smapi.io:\n{await response.Content.ReadAsStringAsync()}");
}
}
else if (response.Content is null)
{
Program.helper.Log($"No response from smapi.io!");
}
else
{
Program.helper.Log($"Error getting mod update data from smapi.io!");
}
Program.helper.Log($"Our request to smapi.io:\n{parsedRequest}");
}
client.Dispose();
return modUpdateData;
}
internal static SemVersion? GetVersion()
{
AssemblyName smapiAssembly = AssemblyName.GetAssemblyName(Path.Combine(Pathing.defaultGamePath, "StardewModdingAPI.dll"));
if (smapiAssembly is null || smapiAssembly.Version is null)
{
return null;
}
return SemVersion.Parse($"{smapiAssembly.Version.Major}.{smapiAssembly.Version.Minor}.{smapiAssembly.Version.Build}", SemVersionStyles.Any);
}
}
}
================================================
FILE: Stardrop/Utilities/Helper.cs
================================================
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
namespace Stardrop.Utilities
{
internal class Helper
{
// Log file related
private string basePath = AppDomain.CurrentDomain.BaseDirectory;
private string logFileName = "log";
private string logFileExtension = ".txt";
// Listener and stat related
private TraceListener listener;
private int totalDebug = 0, totalAlert = 0, totalWarning = 0, totalInfo = 0;
// Used for identifying different statuses when logging
public enum Status { Debug, Alert, Warning, Info };
public Helper(string fileName = "log", string fileExtension = ".txt", string path = null)
{
// Set the log file name and extension
logFileName = fileName;
logFileExtension = fileExtension;
basePath = String.IsNullOrEmpty(path) ? basePath : path;
// Delete any previous log file
if (File.Exists(GetLogPath()))
{
File.Delete(GetLogPath());
}
// Create and enable the listener
listener = new DelimitedListTraceListener(GetLogPath());
Trace.Listeners.Add(listener);
// This makes the Debug.WriteLine() calls always write to the text file
// Rather than waiting for a Debug.Flush() call
Trace.AutoFlush = true;
}
public string GetLogPath()
{
return Path.Combine(basePath, String.Concat(logFileName, logFileExtension));
}
public void DisableTracing()
{
// If listener exists and still is active, remove it
if (!(listener is null) && Trace.Listeners.Contains(listener))
{
Trace.Listeners.Remove(listener);
listener.Close();
}
}
public bool IsActive()
{
return listener != null;
}
// Handles the Debug.WriteLine calls
// It will grab the calling method and line as well via CompilerServices
public void Log(string message, Status status = Status.Debug, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0, [CallerFilePath] string path = "")
{
// Tracking status info
TrackStatus(status);
string fileName = Path.GetFileName(path).Split('.')[0];
Trace.WriteLine(string.Format("[{0}][{1}][{2}.{3}: Line {4}] {5}", DateTime.Now.ToString(), status.ToString(), fileName, caller.ToString(), line, message));
}
public void Log(object messageObj, Status status = Status.Debug, [CallerMemberName] string caller = "", [CallerLineNumber] int line = 0, [CallerFilePath] string path = "")
{
try
{
Log(messageObj.ToString(), status, caller, line, path);
}
catch
{
Log(String.Format($"Unable to parse {messageObj} to string!"), Status.Warning);
}
}
#region Status related tracking
public bool HasAlert()
{
return totalAlert > 0 ? true : false;
}
public bool HasWarning()
{
return totalWarning > 0 ? true : false;
}
public bool HasInfo()
{
return totalInfo > 0 ? true : false;
}
public bool HasDebug()
{
return totalDebug > 0 ? true : false;
}
#endregion
// Handles tracking the count of different Status counts
private void TrackStatus(Status status)
{
switch (status)
{
case Status.Debug:
totalDebug += 1;
break;
case Status.Alert:
totalAlert += 1;
break;
case Status.Warning:
totalWarning += 1;
break;
case Status.Info:
totalInfo += 1;
break;
}
}
}
}
================================================
FILE: Stardrop/Utilities/Internal/EnumParser.cs
================================================
using System;
using System.ComponentModel;
using System.Reflection;
namespace Stardrop.Utilities.Internal
{
internal static class EnumParser
{
// Shamelessly copied from: https://stackoverflow.com/questions/1415140/can-my-enums-have-friendly-names
public static string? GetDescription(this Enum? value)
{
if (value is null)
{
return null;
}
Type type = value.GetType();
string? name = Enum.GetName(type, value);
if (name is not null)
{
FieldInfo? field = type.GetField(name);
if (field is not null)
{
DescriptionAttribute? attr = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
if (attr is not null)
{
return attr.Description;
}
}
}
return value.ToString();
}
}
}
================================================
FILE: Stardrop/Utilities/Internal/ManifestParser.cs
================================================
using SharpCompress.Archives;
using Stardrop.Models.SMAPI;
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
namespace Stardrop.Utilities.Internal
{
internal static class ManifestParser
{
public static async Task GetDataAsync(IArchiveEntry manifestFile)
{
using (Stream stream = manifestFile.OpenEntryStream())
{
using (var reader = new StreamReader(stream))
{
return GetData(await reader.ReadToEndAsync());
}
}
}
public static Manifest? GetData(string manifestText)
{
try
{
return JsonSerializer.Deserialize(manifestText, new JsonSerializerOptions() { AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true });
}
catch (JsonException)
{
// Attempt to parse out illegal JSON characters, as System.Text.Json does not have any native handling (unlike Newtonsoft.Json)
manifestText = manifestText.Replace("\r", String.Empty).Replace("\n", String.Empty);
return JsonSerializer.Deserialize(manifestText, new JsonSerializerOptions() { AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true });
}
}
}
}
================================================
FILE: Stardrop/Utilities/JsonTools.cs
================================================
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Json;
namespace Stardrop.Utilities
{
internal class JsonTools
{
// https://stackoverflow.com/questions/58378409/jsondocument-get-json-string
public static string ParseDocumentToString(JsonDocument jdoc)
{
using (var stream = new MemoryStream())
{
Utf8JsonWriter writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
jdoc.WriteTo(writer);
writer.Flush();
return Encoding.UTF8.GetString(stream.ToArray());
}
}
// https://stackoverflow.com/questions/58694837/system-text-json-merge-two-objects
public static string Merge(string originalJson, string newContent, bool includeMissingProperties)
{
var outputBuffer = new ArrayBufferWriter();
using (JsonDocument jDoc1 = JsonDocument.Parse(originalJson))
using (JsonDocument jDoc2 = JsonDocument.Parse(newContent))
using (var jsonWriter = new Utf8JsonWriter(outputBuffer, new JsonWriterOptions { Indented = true }))
{
JsonElement root1 = jDoc1.RootElement;
JsonElement root2 = jDoc2.RootElement;
if (root1.ValueKind != JsonValueKind.Array && root1.ValueKind != JsonValueKind.Object)
{
throw new InvalidOperationException($"The original JSON document to merge new content into must be a container type. Instead it is {root1.ValueKind}.");
}
if (root1.ValueKind != root2.ValueKind)
{
return originalJson;
}
if (root1.ValueKind == JsonValueKind.Array)
{
MergeArrays(jsonWriter, root1, root2);
}
else
{
MergeObjects(jsonWriter, root1, root2, includeMissingProperties);
}
}
return Encoding.UTF8.GetString(outputBuffer.WrittenSpan);
}
private static void MergeObjects(Utf8JsonWriter jsonWriter, JsonElement root1, JsonElement root2, bool includeMissingProperties)
{
Debug.Assert(root1.ValueKind == JsonValueKind.Object);
Debug.Assert(root2.ValueKind == JsonValueKind.Object);
jsonWriter.WriteStartObject();
// Write all the properties of the first document.
// If a property exists in both documents, either:
// * Merge them, if the value kinds match (e.g. both are objects or arrays),
// * 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),
// * 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).
foreach (JsonProperty property in root1.EnumerateObject())
{
string propertyName = property.Name;
JsonValueKind newValueKind;
if (root2.TryGetProperty(propertyName, out JsonElement newValue) && (newValueKind = newValue.ValueKind) != JsonValueKind.Null)
{
jsonWriter.WritePropertyName(propertyName);
JsonElement originalValue = property.Value;
JsonValueKind originalValueKind = originalValue.ValueKind;
if (newValueKind == JsonValueKind.Object && originalValueKind == JsonValueKind.Object)
{
MergeObjects(jsonWriter, originalValue, newValue, includeMissingProperties); // Recursive call
}
else if (newValueKind == JsonValueKind.Array && originalValueKind == JsonValueKind.Array)
{
MergeArrays(jsonWriter, originalValue, newValue);
}
else
{
newValue.WriteTo(jsonWriter);
}
}
else
{
property.WriteTo(jsonWriter);
}
}
// Write all the properties of the second document that are unique to it
if (includeMissingProperties is true)
{
foreach (JsonProperty property in root2.EnumerateObject())
{
if (!root1.TryGetProperty(property.Name, out _))
{
property.WriteTo(jsonWriter);
}
}
}
jsonWriter.WriteEndObject();
}
private static void MergeArrays(Utf8JsonWriter jsonWriter, JsonElement root1, JsonElement root2)
{
Debug.Assert(root1.ValueKind == JsonValueKind.Array);
Debug.Assert(root2.ValueKind == JsonValueKind.Array);
jsonWriter.WriteStartArray();
// Write all the elements from the original JSON arrays
foreach (JsonElement element in root2.EnumerateArray())
{
element.WriteTo(jsonWriter);
}
jsonWriter.WriteEndArray();
}
}
}
================================================
FILE: Stardrop/Utilities/NXMProtocol.cs
================================================
using Microsoft.Win32;
using System;
using System.Runtime.InteropServices;
namespace Stardrop.Utilities
{
internal static class NXMProtocol
{
public static bool Register(string applicationPath)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) is false)
{
Program.helper.Log($"Attempted to modify registery keys for NXM protocol on a non-Windows system!");
return false;
}
var keyTest = Registry.CurrentUser.OpenSubKey("Software", true).OpenSubKey("Classes", true);
RegistryKey key = keyTest.CreateSubKey("nxm");
key.SetValue("URL Protocol", "nxm");
key.CreateSubKey(@"shell\open\command").SetValue("", "\"" + applicationPath + "\" --nxm \"%1\"");
}
catch (Exception ex)
{
Program.helper.Log($"Failed to associate Stardrop with the NXM protocol: {ex}");
return false;
}
return true;
}
public static bool Validate(string applicationPath)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) is false)
{
Program.helper.Log($"Attempted to modify registery keys for NXM protocol on a non-Windows system!");
return false;
}
var baseKeyTest = Registry.CurrentUser.OpenSubKey("Software", true).OpenSubKey("Classes", true).OpenSubKey("nxm", true);
if (baseKeyTest is null || baseKeyTest.GetValue("URL Protocol").ToString() != "nxm")
{
return false;
}
var actualKeyTest = Registry.CurrentUser.OpenSubKey("Software", true).OpenSubKey("Classes", true).OpenSubKey(@"nxm\shell\open\command", true);
if (actualKeyTest.GetValue(String.Empty).ToString() != "\"" + applicationPath + "\" --nxm \"%1\"")
{
return false;
}
}
catch (Exception ex)
{
return false;
}
return true;
}
}
}
================================================
FILE: Stardrop/Utilities/NexusWebsocket.cs
================================================
using Stardrop.Models.Nexus.Web;
using Stardrop.ViewModels;
using System;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
namespace Stardrop.Utilities
{
internal class NexusWebsocket
{
private readonly Uri ssoWebsocketURI = new("wss://sso.nexusmods.com");
private readonly string connectionUUID = Guid.NewGuid().ToString();
private readonly string connectionSlug = "stardrop";
internal readonly string ssoUrl;
private ClientWebSocket? _socket;
private System.Timers.Timer? _pingTimer;
private bool _hasResolved;
public NexusWebsocket()
{
this.ssoUrl = $"https://www.nexusmods.com/sso?id={connectionUUID}&application={connectionSlug}";
}
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
var result = new NexusConnectionResult();
_socket = new ClientWebSocket();
try
{
await _socket.ConnectAsync(ssoWebsocketURI, cancellationToken);
var initialData = new
{
id = connectionUUID,
token = (string?)null,
protocol = 2
};
string json = JsonSerializer.Serialize(initialData);
var bytes = Encoding.UTF8.GetBytes(json);
await _socket.SendAsync(
new ArraySegment(bytes), WebSocketMessageType.Text, true, cancellationToken
);
// ping every 30 seconds as requested by docs
_pingTimer = new System.Timers.Timer(30000);
_pingTimer.Elapsed += async (_, __) =>
{
if (_socket?.State == WebSocketState.Open)
{
try
{
await _socket.SendAsync(
new ArraySegment(Array.Empty()), WebSocketMessageType.Text, true, CancellationToken.None
);
}
catch
{
_pingTimer?.Stop();
}
}
else
{
_pingTimer?.Stop();
}
};
_pingTimer.AutoReset = true;
_pingTimer.Start();
// Receive data
var buffer = new byte[4096];
while (_socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
var recv = await _socket.ReceiveAsync(
new ArraySegment(buffer), cancellationToken
);
if (recv.MessageType == WebSocketMessageType.Close) break;
var msg = Encoding.UTF8.GetString(buffer, 0, recv.Count);
Program.helper.Log($"[Nexus SSO] Received data: {msg}", Helper.Status.Debug);
var response = JsonSerializer.Deserialize(msg);
if (response != null && response.Success && response.Data != null)
{
// ignore ConnectionToken
if (response.Data.ConnectionToken != null && response.Data.ApiKey == null)
{
continue;
}
result.Message = "Successfully obtained API key";
result.ApiKey = response.Data.ApiKey;
_hasResolved = true;
await _socket.CloseAsync(
WebSocketCloseStatus.NormalClosure, "got key", CancellationToken.None
);
break;
}
else
{
result.Error = "Received invalid message";
_hasResolved = true;
await _socket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"invalid",
CancellationToken.None
);
break;
}
}
}
catch (Exception ex)
{
Program.helper.Log($"[Nexus SSO] Exception: {ex}", Helper.Status.Debug);
if (!_hasResolved)
{
result.Error = ex.Message;
_hasResolved = true;
}
}
finally
{
_pingTimer?.Stop();
if (_socket?.State == WebSocketState.Open)
{
await _socket.CloseAsync(
WebSocketCloseStatus.NormalClosure, "shutdown", CancellationToken.None
);
}
_socket?.Dispose();
}
return result;
}
}
}
================================================
FILE: Stardrop/Utilities/Pathing.cs
================================================
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace Stardrop.Utilities
{
public static class Pathing
{
internal static string defaultGamePath;
internal static string defaultModPath;
internal static string defaultHomePath;
internal static void SetHomePath(string homePath)
{
defaultHomePath = Path.Combine(homePath, "Stardrop", "Data");
}
internal static void SetSmapiPath(string smapiPath, bool useDefaultModPath = false)
{
if (smapiPath is not null)
{
defaultGamePath = smapiPath;
if (useDefaultModPath)
{
defaultModPath = Path.Combine(smapiPath, "Mods");
}
}
}
internal static void SetModPath(string modPath)
{
if (modPath is not null)
{
defaultModPath = modPath;
}
}
internal static string GetLogFolderPath()
{
return Path.Combine(defaultHomePath, "Logs");
}
internal static string GetSettingsPath()
{
return Path.Combine(defaultHomePath, "Settings.json");
}
public static string GetProfilesFolderPath()
{
return Path.Combine(defaultHomePath, "Profiles");
}
public static string GetSelectedModsFolderPath()
{
return Path.Combine(defaultHomePath, "Selected Mods");
}
public static string GetSmapiPath()
{
return Path.Combine(defaultGamePath, RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "StardewModdingAPI.exe" : "StardewModdingAPI.dll");
}
internal static string GetSmapiLogFolderPath()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "StardewValley", "ErrorLogs");
}
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs");
}
public static string GetCacheFolderPath()
{
return Path.Combine(defaultHomePath, "Cache");
}
public static string GetVersionCachePath()
{
return Path.Combine(GetCacheFolderPath(), "Versions.json");
}
internal static string GetKeyCachePath()
{
return Path.Combine(GetCacheFolderPath(), "Keys.json");
}
internal static string GetDataCachePath()
{
return Path.Combine(GetCacheFolderPath(), "Data.json");
}
public static string GetNotionCachePath()
{
return Path.Combine(GetCacheFolderPath(), "Notion.json");
}
public static string GetLinksCachePath()
{
return Path.Combine(GetCacheFolderPath(), "Links.json");
}
public static string GetNexusPath()
{
return Path.Combine(defaultHomePath, "Nexus");
}
public static string GetThumbnailsPath()
{
return Path.Combine(defaultHomePath, "Thumbnails", "Nexus");
}
public static string GetSmapiUpgradeFolderPath()
{
return Path.Combine(defaultHomePath, "SMAPI");
}
}
}
================================================
FILE: Stardrop/Utilities/SimpleObscure.cs
================================================
using System.IO;
using System.Security.Cryptography;
namespace Stardrop.Utilities
{
internal class SimpleObscure
{
internal byte[] Key { get; set; }
internal byte[] Vector { get; set; }
public SimpleObscure()
{
using (Aes aes = Aes.Create())
{
Key = aes.Key;
Vector = aes.IV;
}
}
internal static byte[] Encrypt(string plainText, byte[] Key, byte[] IV)
{
byte[] encrypted;
using (Aes aes = Aes.Create())
{
aes.Key = Key;
aes.IV = IV;
// Create an encryptor to perform the stream transform.
ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
// Create the streams used for encryption.
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(plainText);
}
encrypted = msEncrypt.ToArray();
}
}
}
return encrypted;
}
internal static string Decrypt(byte[] cipherText, byte[] Key, byte[] IV)
{
string plaintext = null;
using (Aes aes = Aes.Create())
{
aes.Key = Key;
aes.IV = IV;
// Create a decryptor to perform the stream transform.
ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
// Create the streams used for decryption.
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
plaintext = srDecrypt.ReadToEnd();
}
}
}
}
return plaintext;
}
}
}
================================================
FILE: Stardrop/Utilities/Translation.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace Stardrop.Utilities
{
internal class Translation : INotifyPropertyChanged
{
public enum Language
{
English,
Chinese,
French,
German,
Hungarian,
Italian,
Japanese,
Korean,
Portuguese,
Russian,
Spanish,
Thai,
Turkish,
Ukrainian
}
public enum LanguageAbbreviation
{
@default,
zh,
fr,
de,
hu,
it,
ja,
ko,
pt,
ru,
es,
th,
tr,
uk
}
public Dictionary LanguageNameToAbbreviations = new();
public Dictionary AbbreviationsToLanguageName = new();
private Language _selectedLanguage = Language.English;
private Dictionary> _languageTranslations = new();
private const string IndexerName = "Item";
private const string IndexerArrayName = "Item[]";
public Translation()
{
int index = 0;
foreach (Language language in Enum.GetValues(typeof(Language)))
{
LanguageNameToAbbreviations[language] = (LanguageAbbreviation)index;
AbbreviationsToLanguageName[(LanguageAbbreviation)index] = language;
index++;
}
}
public string GetLanguageFromAbbreviation(string abbreviation)
{
if (Enum.TryParse(typeof(LanguageAbbreviation), abbreviation, out var languageAbbreviation))
{
if (AbbreviationsToLanguageName.ContainsKey((LanguageAbbreviation)languageAbbreviation))
{
return AbbreviationsToLanguageName[(LanguageAbbreviation)languageAbbreviation].ToString();
}
}
return Language.English.ToString();
}
public Language GetLanguage(string language)
{
if (Enum.TryParse(typeof(Language), language, out var parsedLanguage))
{
return (Language)parsedLanguage;
}
return Language.English;
}
public void SetLanguage(string language)
{
if (Enum.TryParse(typeof(Language), language, out var parsedLanguage))
{
SetLanguage((Language)parsedLanguage);
}
}
public void SetLanguage(Language language)
{
_selectedLanguage = language;
Invalidate();
}
public void LoadTranslations()
{
// Load the languages
foreach (string fileFullName in Directory.EnumerateFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "i18n"), "*.json"))
{
try
{
var fileName = Path.GetFileNameWithoutExtension(fileFullName);
if (Enum.TryParse(typeof(LanguageAbbreviation), fileName, out var language))
{
_languageTranslations[(LanguageAbbreviation)language] = JsonSerializer.Deserialize>(File.ReadAllText(fileFullName), new JsonSerializerOptions { AllowTrailingCommas = true, ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true });
Program.helper.Log($"Loaded language {Path.GetFileNameWithoutExtension(fileFullName)}", Helper.Status.Debug);
}
}
catch (Exception ex)
{
Program.helper.Log($"Unable to load translation at {Path.GetFileNameWithoutExtension(fileFullName)}: {ex}", Helper.Status.Warning);
}
}
}
public void LoadTranslations(Language language)
{
// Set the language
SetLanguage(language);
LoadTranslations();
}
public List GetAvailableTranslations()
{
List availableLanguages = new();
foreach (var abbreviation in _languageTranslations.Keys.Where(l => AbbreviationsToLanguageName.ContainsKey(l)))
{
availableLanguages.Add(AbbreviationsToLanguageName[abbreviation]);
}
return availableLanguages;
}
public string Get(string key)
{
var languageAbbreviation = LanguageNameToAbbreviations[_selectedLanguage];
if (_languageTranslations.ContainsKey(languageAbbreviation) && _languageTranslations[languageAbbreviation].ContainsKey(key))
{
return _languageTranslations[languageAbbreviation][key];
}
else if (_languageTranslations.ContainsKey(LanguageAbbreviation.@default) && _languageTranslations[LanguageAbbreviation.@default].ContainsKey(key))
{
return _languageTranslations[LanguageAbbreviation.@default][key];
}
return $"(No translation provided for key {key})";
}
public string this[string key]
{
get
{
return Get(key);
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void Invalidate()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerName));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(IndexerArrayName));
}
}
}
================================================
FILE: Stardrop/ViewLocator.cs
================================================
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Stardrop.ViewModels;
using System;
namespace Stardrop
{
public class ViewLocator : IDataTemplate
{
public IControl Build(object data)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
else
{
return new TextBlock { Text = "Not Found: " + name };
}
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}
================================================
FILE: Stardrop/ViewModels/DownloadPanelViewModel.cs
================================================
using Avalonia.Controls;
using DynamicData;
using DynamicData.Aggregation;
using DynamicData.Alias;
using DynamicData.Binding;
using ReactiveUI;
using Stardrop.Models.Data;
using Stardrop.Utilities.External;
using System;
using System.Collections.ObjectModel;
using System.Linq;
namespace Stardrop.ViewModels
{
public class DownloadPanelViewModel : ViewModelBase
{
private ObservableCollection _downloads = new();
public ObservableCollection Downloads { get => _downloads; set => this.RaiseAndSetIfChanged(ref _downloads, value); }
public IObservable InProgressDownloads { get; init; }
public DownloadPanelViewModel(NexusClient? nexusClient)
{
Nexus.ClientChanged += NexusClientChanged;
if (nexusClient is not null)
{
RegisterEventHandlers(nexusClient);
}
// Count failed and canceled downloads toward this value, because those still need to be
// handled in some way by the user
InProgressDownloads = Downloads
.ToObservableChangeSet(t => t.ModUri)
.AutoRefresh(x => x.DownloadStatus, scheduler: RxApp.MainThreadScheduler)
.Filter(x => x.DownloadStatus != ModDownloadStatus.Successful)
.Count();
}
private void NexusClientChanged(NexusClient? oldClient, NexusClient? newClient)
{
if (oldClient is not null)
{
// Cancel all downloads and clear the dictionary, so we don't have zombie downloads from an old client lingering
foreach (var download in Downloads)
{
// Trigger the cancel command, and ignore any return values (as it has none)
download.CancelCommand.Execute().Subscribe();
}
ClearEventHandlers(oldClient);
Downloads.Clear();
}
if (newClient is not null)
{
RegisterEventHandlers(newClient);
}
}
private void RegisterEventHandlers(NexusClient nexusClient)
{
nexusClient.DownloadStarted += DownloadStarted;
nexusClient.DownloadProgressChanged += DownloadProgressChanged;
nexusClient.DownloadCompleted += DownloadCompleted;
nexusClient.DownloadFailed += DownloadFailed;
}
private void ClearEventHandlers(NexusClient nexusClient)
{
nexusClient.DownloadStarted -= DownloadStarted;
nexusClient.DownloadProgressChanged -= DownloadProgressChanged;
nexusClient.DownloadCompleted -= DownloadCompleted;
nexusClient.DownloadFailed -= DownloadFailed;
}
private void DownloadStarted(object? sender, ModDownloadStartedEventArgs e)
{
var existingDownload = Downloads.FirstOrDefault(x => x.ModUri == e.Uri);
if (existingDownload is not null)
{
// If the user is trying to download the same file twice, it's *probably* because they
// want to retry a failed download.
// But just in case, check to see if the existing download is still in-progress. If it is, do nothing.
// We don't want to stop a user's 95% download because they accidentally hit the "download again please" button!
if (existingDownload.DownloadStatus == ModDownloadStatus.NotStarted
|| existingDownload.DownloadStatus == ModDownloadStatus.InProgress)
{
return;
}
// If it does exist, and isn't in a progress state, they're probably trying to redownload a failed download.
// Since we use the URI as our unique ID, we shouldn't have two items with the same URI in the list,
// so clear out the old one.
Downloads.Remove(existingDownload);
}
var downloadVM = new ModDownloadViewModel(e.Uri, e.Name, e.Size, e.DownloadCancellationSource);
downloadVM.RemovalRequested += DownloadRemovalRequested;
Downloads.Add(downloadVM);
}
private void DownloadProgressChanged(object? sender, ModDownloadProgressEventArgs e)
{
var download = Downloads.SingleOrDefault(x => x.ModUri == e.Uri);
if (download is not null)
{
download.DownloadStatus = ModDownloadStatus.InProgress;
download.DownloadedBytes = e.TotalBytes;
}
}
private void DownloadCompleted(object? sender, ModDownloadCompletedEventArgs e)
{
var download = Downloads.SingleOrDefault(x => x.ModUri == e.Uri);
if (download is not null)
{
download.DownloadStatus = ModDownloadStatus.Successful;
}
}
private void DownloadFailed(object? sender, ModDownloadFailedEventArgs e)
{
var download = Downloads.SingleOrDefault(x => x.ModUri == e.Uri);
if (download is not null)
{
download.DownloadStatus = ModDownloadStatus.Failed;
}
}
private void DownloadRemovalRequested(object? sender, EventArgs _)
{
if (sender is not ModDownloadViewModel downloadVM)
{
return;
}
downloadVM.RemovalRequested -= DownloadRemovalRequested;
Downloads.Remove(downloadVM);
}
// Designer-only constructor
public DownloadPanelViewModel()
{
if (!Design.IsDesignMode)
{
throw new Exception("This constructor should only be called in design mode.");
}
var inProgressDownload = new ModDownloadViewModel(
new Uri("https://www.fakeurl.com/testMod"),
"Fake Test Mod Download",
1024 * 1024,
new()
);
inProgressDownload.DownloadStatus = ModDownloadStatus.InProgress;
inProgressDownload.DownloadedBytes = inProgressDownload.SizeBytes!.Value / 2;
Downloads.Add(inProgressDownload);
var succeededDownload = new ModDownloadViewModel(
new Uri("https://www.fakeSuccess.com"),
"Fake Succeeded Download",
1234,
new()
);
succeededDownload.DownloadStatus = ModDownloadStatus.Successful;
succeededDownload.DownloadedBytes = 1234;
Downloads.Add(succeededDownload);
var failedDownload = new ModDownloadViewModel(
new Uri("https://www.differentFakeUrl.com"),
"Failed Fake Download",
1024 * 1024 * 1024,
new()
);
failedDownload.DownloadedBytes = failedDownload.SizeBytes!.Value / 3;
failedDownload.DownloadStatus = ModDownloadStatus.Failed;
Downloads.Add(failedDownload);
var cancelledDownload = new ModDownloadViewModel(
new Uri("https://www.cancelledFake.com"),
"Cancelled Fake Download",
1024 * 1024 * 5,
new()
);
cancelledDownload.DownloadedBytes = cancelledDownload.SizeBytes!.Value / 4;
cancelledDownload.DownloadStatus = ModDownloadStatus.Canceled;
Downloads.Add(cancelledDownload);
var indeterminateInProgressDownload = new ModDownloadViewModel(
new Uri("https://www.inProgressMystery.com"),
"In Progress Download of Unknown Size",
null,
new()
);
indeterminateInProgressDownload.DownloadedBytes = 1024 * 1024 * 2;
indeterminateInProgressDownload.DownloadStatus = ModDownloadStatus.InProgress;
Downloads.Add(indeterminateInProgressDownload);
}
}
}
================================================
FILE: Stardrop/ViewModels/FlexibleOptionWindowViewModel.cs
================================================
using ReactiveUI;
namespace Stardrop.ViewModels
{
public class FlexibleOptionWindowViewModel : ViewModelBase
{
private string _messageText;
public string MessageText { get { return _messageText; } set { this.RaiseAndSetIfChanged(ref _messageText, value); } }
private string _firstButtonText;
public string FirstButtonText { get { return _firstButtonText; } set { this.RaiseAndSetIfChanged(ref _firstButtonText, value); } }
private string _secondButtonText;
public string SecondButtonText { get { return _secondButtonText; } set { this.RaiseAndSetIfChanged(ref _secondButtonText, value); } }
private string _thirdButtonText;
public string ThirdButtonText { get { return _thirdButtonText; } set { this.RaiseAndSetIfChanged(ref _thirdButtonText, value); } }
private bool _isFirstButtonVisible;
public bool IsFirstButtonVisible { get { return _isFirstButtonVisible; } set { this.RaiseAndSetIfChanged(ref _isFirstButtonVisible, value); } }
private bool _isSecondButtonVisible;
public bool IsSecondButtonVisible { get { return _isSecondButtonVisible; } set { this.RaiseAndSetIfChanged(ref _isSecondButtonVisible, value); } }
private bool _isThirdButtonVisible;
public bool IsThirdButtonVisible { get { return _isThirdButtonVisible; } set { this.RaiseAndSetIfChanged(ref _isThirdButtonVisible, value); } }
}
}
================================================
FILE: Stardrop/ViewModels/MainWindowViewModel.cs
================================================
using Avalonia.Collections;
using Avalonia.Controls;
using DynamicData;
using Json.More;
using ReactiveUI;
using Stardrop.Models;
using Stardrop.Models.Data;
using Stardrop.Models.Data.Enums;
using Stardrop.Models.SMAPI;
using Stardrop.Utilities;
using Stardrop.Utilities.External;
using Stardrop.Utilities.Internal;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
namespace Stardrop.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private string ChromeHint { get; set; } = "NoChrome";
private bool HasSystemDecorations { get; set; } = true;
private bool ShowTitle { get; set; } = true;
private bool ShowMainMenu { get; set; } = true;
private bool ShowWindowMenu { get; set; } = true;
private DataGridPathGroupDescription _modPathGrouping = new DataGridPathGroupDescription(nameof(Mod.Path));
private DataGridPathGroupDescription _rootPathGrouping = new DataGridPathGroupDescription(nameof(Mod.RootPath));
private DataGridPathGroupDescription _frameworkGrouping = new DataGridPathGroupDescription(nameof(Mod.FrameworkID));
private string _dragOverColor = "#ff9f2a";
public string DragOverColor { get { return _dragOverColor; } set { this.RaiseAndSetIfChanged(ref _dragOverColor, value); } }
private bool _isLocked;
public bool IsLocked { get { return _isLocked; } set { this.RaiseAndSetIfChanged(ref _isLocked, value); } }
private bool _isCheckingForUpdates;
public bool IsCheckingForUpdates { get { return _isCheckingForUpdates; } set { this.RaiseAndSetIfChanged(ref _isCheckingForUpdates, value); } }
public ObservableCollection Mods { get; set; }
private int _enabledModCount;
public int EnabledModCount { get { return _enabledModCount; } set { this.RaiseAndSetIfChanged(ref _enabledModCount, value); } }
private int _actualModCount;
public int ActualModCount { get { return _actualModCount; } set { this.RaiseAndSetIfChanged(ref _actualModCount, value); } }
public DataGridCollectionView DataView { get; set; }
private DisplayFilter _disabledModFilter;
public DisplayFilter DisabledModFilter { get { return _disabledModFilter; } set { _disabledModFilter = value; UpdateFilter(); } }
private bool _showUpdatableMods;
public bool ShowUpdatableMods { get { return _showUpdatableMods; } set { _showUpdatableMods = value; UpdateFilter(); } }
private bool _showEndorsements;
public bool ShowEndorsements { get { return _showEndorsements; } set { this.RaiseAndSetIfChanged(ref _showEndorsements, value); } }
private bool _showInstalls;
public bool ShowInstalls { get { return _showInstalls; } set { this.RaiseAndSetIfChanged(ref _showInstalls, value); } }
private string _filterText;
public string FilterText { get { return _filterText; } set { _filterText = value; UpdateFilter(); } }
private List _columnFilter;
public List ColumnFilter { get { return _columnFilter; } set { _columnFilter = value; UpdateFilter(); } }
private string _updateStatusText = Program.translation.Get("ui.main_window.button.update_status.generic");
public string UpdateStatusText { get { return _updateStatusText; } set { this.RaiseAndSetIfChanged(ref _updateStatusText, value); } }
private string _downloadsButtonText;
public string DownloadsButtonText { get => _downloadsButtonText; set => this.RaiseAndSetIfChanged(ref _downloadsButtonText, value); }
private int _modsWithCachedUpdates;
public int ModsWithCachedUpdates { get { return _modsWithCachedUpdates; } set { this.RaiseAndSetIfChanged(ref _modsWithCachedUpdates, value); } }
public string Version { get; set; }
private string _nexusStatus = String.Concat("Nexus Mods: ", Program.translation.Get("internal.disconnected"));
public string NexusStatus { get { return _nexusStatus; } set { this.RaiseAndSetIfChanged(ref _nexusStatus, String.Concat("Nexus Mods: ", value)); } }
private string _nexusLimits;
public string NexusLimits { get { return _nexusLimits; } set { this.RaiseAndSetIfChanged(ref _nexusLimits, value); } }
private string _smapiVersion;
public string SmapiVersion { get { return String.IsNullOrEmpty(_smapiVersion) ? Program.translation.Get("ui.main_window.labels.unknown_SMAPI") : $"v{_smapiVersion}"; } set { this.RaiseAndSetIfChanged(ref _smapiVersion, value); } }
public bool ShowSaveProfileChanges { get { return _showSaveProfileChanges; } set { this.RaiseAndSetIfChanged(ref _showSaveProfileChanges, value); } }
private bool _showSaveProfileChanges;
public bool AreModGroupsEnabled { get { return _areModGroupsEnabled; } set { this.RaiseAndSetIfChanged(ref _areModGroupsEnabled, value); } }
private bool _areModGroupsEnabled = Program.settings.ModGroupingMethod != ModGrouping.None;
public bool ShowModThumbnails { get { return _showModThumbnails; } set { this.RaiseAndSetIfChanged(ref _showModThumbnails, value); } }
private bool _showModThumbnails = Program.settings.ShowModThumbnails;
public string ModGroupsStateButtonText { get { return _modGroupsStateButtonText; } set { this.RaiseAndSetIfChanged(ref _modGroupsStateButtonText, value); } }
private string _modGroupsStateButtonText = Program.settings.ModGroupingMethod != ModGrouping.None ? Program.translation.Get("ui.main_window.buttons.mod_groups_state.collapse") : Program.translation.Get("ui.main_window.buttons.mod_groups_state.expand");
public MainWindowViewModel(string modsFilePath, string version)
{
DiscoverMods(modsFilePath);
Version = $"v{version}";
SmapiVersion = Program.settings.GameDetails?.SmapiVersion;
// Create data view
DataView = new DataGridCollectionView(Mods, isDataSorted: false, isDataInGroupOrder: false);
DataView.SortDescriptions.CollectionChanged += DataViewSortDescription_CollectionChanged;
UpdateFilter();
DataView.SortDescriptions.Add(DataGridSortDescription.FromPath(nameof(Mod.Name), ListSortDirection.Ascending));
// Do OS specific setup
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
ChromeHint = "Default";
ShowMainMenu = false;
ShowWindowMenu = false;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
ChromeHint = "Default";
ShowWindowMenu = false;
ShowTitle = false;
}
}
public void OpenBrowser(string url)
{
if (String.IsNullOrEmpty(url))
{
return;
}
try
{
using Process process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url :
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "open" : "xdg-open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : $"\"{url}\"",
CreateNoWindow = true,
UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
});
}
catch (Exception ex)
{
Program.helper.Log($"Failed to utilize OpenBrowser with the url ({url}): {ex}");
}
}
public void ChangeColumnVisibility(MenuItem column)
{
if (column is null)
{
return;
}
var modGrid = column.FindControl("modGrid");
if (modGrid is null)
{
return;
}
if (column.Classes.Contains("ColumnInactive"))
{
SetColumnVisibility(column, modGrid, true);
}
else
{
SetColumnVisibility(column, modGrid, false);
}
}
public void SetColumnVisibility(MenuItem column, DataGrid modGrid, bool isActive)
{
// Get the local data
ClientData localDataCache = new ClientData();
if (File.Exists(Pathing.GetDataCachePath()))
{
localDataCache = JsonSerializer.Deserialize(File.ReadAllText(Pathing.GetDataCachePath()), new JsonSerializerOptions { AllowTrailingCommas = true });
}
if (isActive)
{
if (modGrid.Columns.Any(c => c.Header is TextBlock textBlock && textBlock.Text == (string)column.Header))
{
column.Classes.Remove("ColumnInactive");
column.Classes.Add("ColumnActive");
modGrid.Columns.First(c => c.Header is TextBlock textBlock && textBlock.Text == (string)column.Header).IsVisible = true;
localDataCache.ColumnActiveStates[(string)column.Header] = true;
}
}
else
{
if (modGrid.Columns.Any(c => c.Header is TextBlock textBlock && textBlock.Text == (string)column.Header))
{
column.Classes.Remove("ColumnActive");
column.Classes.Add("ColumnInactive");
modGrid.Columns.First(c => c.Header is TextBlock textBlock && textBlock.Text == (string)column.Header).IsVisible = false;
localDataCache.ColumnActiveStates[(string)column.Header] = false;
}
}
// Cache the local data
File.WriteAllText(Pathing.GetDataCachePath(), JsonSerializer.Serialize(localDataCache, new JsonSerializerOptions() { WriteIndented = true }));
}
public bool ParentFolderContainsPeriod(string oldestAncestorPath, DirectoryInfo? directoryInfo)
{
if (directoryInfo is null)
{
return false;
}
else if (directoryInfo.Name[0] == '.')
{
return true;
}
var ancestorFolder = directoryInfo.Parent;
while (ancestorFolder is not null && !ancestorFolder.FullName.Equals(oldestAncestorPath, StringComparison.OrdinalIgnoreCase))
{
if (ancestorFolder.Name[0] == '.')
{
return true;
}
ancestorFolder = ancestorFolder.Parent;
}
return false;
}
public List GetManifestFiles(DirectoryInfo modDirectory)
{
List manifests = new List();
foreach (var directory in modDirectory.EnumerateDirectories())
{
try
{
var localManifest = directory.EnumerateFiles().FirstOrDefault(file => file.Name.Equals("manifest.json", StringComparison.OrdinalIgnoreCase));
if (localManifest is null)
{
manifests.AddRange(GetManifestFiles(directory));
}
else
{
manifests.Add(localManifest);
}
}
catch (Exception ex)
{
Program.helper.Log($"There was an error when attempting to get the manifest.json within the directory ({(directory is null ? String.Empty : directory.FullName)}): {ex}", Helper.Status.Alert);
}
}
return manifests;
}
public void DiscoverMods(string modsFilePath)
{
if (Mods is null)
{
Mods = new ObservableCollection();
}
Mods.Clear();
if (modsFilePath is null || !Directory.Exists(modsFilePath))
{
return;
}
// Get cached key data
List modKeysCache = new List();
if (File.Exists(Pathing.GetKeyCachePath()))
{
try
{
modKeysCache = JsonSerializer.Deserialize>(File.ReadAllText(Pathing.GetKeyCachePath()), new JsonSerializerOptions { AllowTrailingCommas = true });
}
catch (Exception ex)
{
Program.helper.Log($"Failed to parse cached mod keys: {ex}", Helper.Status.Alert);
}
}
// Get the local data
ClientData localDataCache = new ClientData();
if (File.Exists(Pathing.GetDataCachePath()))
{
try
{
localDataCache = JsonSerializer.Deserialize(File.ReadAllText(Pathing.GetDataCachePath()), new JsonSerializerOptions { AllowTrailingCommas = true });
}
catch (Exception ex)
{
Program.helper.Log($"Failed to parse client data: {ex}", Helper.Status.Alert);
}
}
foreach (var fileInfo in GetManifestFiles(new DirectoryInfo(modsFilePath)))
{
if (fileInfo.DirectoryName is null || (Program.settings.IgnoreHiddenFolders && ParentFolderContainsPeriod(modsFilePath, fileInfo.Directory)))
{
continue;
}
try
{
var manifest = ManifestParser.GetData(File.ReadAllText(fileInfo.FullName));
if (manifest is null || String.IsNullOrEmpty(manifest.UniqueID))
{
Program.helper.Log($"The manifest.json was empty or not deserializable from {fileInfo.DirectoryName}", Helper.Status.Alert);
continue;
}
var mod = new Mod(manifest, fileInfo, manifest.UniqueID, manifest.Version, manifest.Name, manifest.Description, manifest.Author);
if (manifest.ContentPackFor is not null && modKeysCache is not null)
{
var dependencyKey = modKeysCache.FirstOrDefault(m => m.UniqueId.Equals(manifest.ContentPackFor.UniqueID, StringComparison.OrdinalIgnoreCase));
mod.FrameworkID = manifest.ContentPackFor.UniqueID;
mod.Requirements.Add(new ManifestDependency(manifest.ContentPackFor.UniqueID, manifest.ContentPackFor.MinimumVersion, true) { Name = dependencyKey is null ? manifest.ContentPackFor.UniqueID : dependencyKey.Name });
}
if (manifest.Dependencies is not null && modKeysCache is not null)
{
foreach (var dependency in manifest.Dependencies)
{
if (mod.Requirements.Any(r => r.UniqueID.Equals(dependency.UniqueID, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
var dependencyKey = modKeysCache.FirstOrDefault(m => m.UniqueId.Equals(dependency.UniqueID, StringComparison.OrdinalIgnoreCase));
mod.Requirements.Add(new ManifestDependency(dependency.UniqueID, dependency.MinimumVersion, dependency.IsRequired) { Name = dependencyKey is null ? dependency.UniqueID : dependencyKey.Name });
}
}
if (modKeysCache is not null && modKeysCache.Any(m => m.UniqueId.Equals(mod.UniqueId, StringComparison.OrdinalIgnoreCase)))
{
mod.ModPageUri = modKeysCache.First(m => m.UniqueId.Equals(mod.UniqueId, StringComparison.OrdinalIgnoreCase)).PageUrl;
}
if (localDataCache is not null && localDataCache.ModInstallData is not null && localDataCache.ModInstallData.Any(m => m.UniqueId.Equals(mod.UniqueId, StringComparison.OrdinalIgnoreCase)))
{
mod.InstallTimestamp = localDataCache.ModInstallData.First(m => m.UniqueId.Equals(mod.UniqueId, StringComparison.OrdinalIgnoreCase)).InstallTimestamp;
mod.LastUpdateTimestamp = localDataCache.ModInstallData.First(m => m.UniqueId.Equals(mod.UniqueId, StringComparison.OrdinalIgnoreCase)).LastUpdateTimestamp;
}
// Check if any config file exists
var configPath = Path.Combine(fileInfo.DirectoryName, "config.json");
if (File.Exists(configPath) && new FileInfo(configPath) is FileInfo configInfo && configInfo is not null)
{
mod.Config = new Config() { UniqueId = mod.UniqueId, FilePath = configInfo.FullName, LastWriteTimeUtc = configInfo.LastWriteTimeUtc, Data = File.ReadAllText(configInfo.FullName) };
}
// Add or update the mod
if (!Mods.Any(m => m.UniqueId.Equals(manifest.UniqueID, StringComparison.OrdinalIgnoreCase)))
{
Mods.Add(mod);
}
else if (Mods.FirstOrDefault(m => m.UniqueId.Equals(manifest.UniqueID, StringComparison.OrdinalIgnoreCase) && m.Version.CompareSortOrderTo(mod.Version) < 0) is Mod oldMod && oldMod is not null)
{
// Replace old mod with newer one
int oldModIndex = Mods.IndexOf(Mods.First(m => m.UniqueId.Equals(manifest.UniqueID, StringComparison.OrdinalIgnoreCase) && m.Version.CompareSortOrderTo(mod.Version) < 0));
Mods[oldModIndex] = mod;
}
}
catch (Exception ex)
{
Program.helper.Log($"Unable to load the manifest.json from {fileInfo.DirectoryName}: {ex}", Helper.Status.Alert);
}
}
if (Program.settings.ShowModThumbnails)
{
UpdateThumbnails();
}
// Update the local data
var modInstallData = new List();
foreach (var mod in Mods.Where(m => m is not null))
{
if (mod.InstallTimestamp is null)
{
mod.InstallTimestamp = DateTime.Now;
}
modInstallData.Add(new ModInstallData() { UniqueId = mod.UniqueId, InstallTimestamp = mod.InstallTimestamp.Value, LastUpdateTimestamp = mod.LastUpdateTimestamp });
}
localDataCache.ModInstallData = modInstallData;
// Cache the local data
File.WriteAllText(Pathing.GetDataCachePath(), JsonSerializer.Serialize(localDataCache, new JsonSerializerOptions() { WriteIndented = true }));
EvaluateRequirements();
DiscoverConfigs(modsFilePath, useArchive: true);
HideRequiredMods();
ActualModCount = Mods.Count(m => !m.IsHidden);
}
public void HideRequiredMods()
{
var requiredModIds = new List { "SMAPI.ConsoleCommands", "SMAPI.ErrorHandler", "SMAPI.SaveBackup" };
foreach (var mod in Mods.Where(m => requiredModIds.Any(id => id.Equals(m.UniqueId, StringComparison.OrdinalIgnoreCase))))
{
mod.IsHidden = true;
mod.IsEnabled = true;
}
// Update the EnabledModCount
EnabledModCount = Mods.Where(m => m.IsEnabled && !m.IsHidden).Count();
// Update data grid grouping
UpdateDataGridGrouping();
}
public void EvaluateRequirements()
{
// Get cached key data
List modKeysCache = new List();
if (File.Exists(Pathing.GetKeyCachePath()))
{
modKeysCache = JsonSerializer.Deserialize>(File.ReadAllText(Pathing.GetKeyCachePath()), new JsonSerializerOptions { AllowTrailingCommas = true });
}
// Flag any missing requirements
foreach (var mod in Mods)
{
try
{
foreach (var requirement in mod.Requirements.Where(r => r.IsRequired))
{
if (!Mods.Any(m => m.UniqueId.Equals(requirement.UniqueID, StringComparison.OrdinalIgnoreCase)) || Mods.First(m => m.UniqueId.Equals(requirement.UniqueID, StringComparison.OrdinalIgnoreCase)) is Mod matchedMod && matchedMod.IsModOutdated(requirement.MinimumVersion))
{
requirement.IsMissing = true;
if (modKeysCache is not null)
{
var dependencyKey = modKeysCache.FirstOrDefault(m => m.UniqueId.Equals(requirement.UniqueID, StringComparison.OrdinalIgnoreCase));
requirement.Name = dependencyKey is null ? requirement.UniqueID : dependencyKey.Name;
}
}
}
mod.NotifyPropertyChanged("Requirements");
mod.NotifyPropertyChanged("MissingRequirements");
}
catch (Exception ex)
{
Program.helper.Log($"Failed to check requirements for {mod.Name} due to the following error: {ex}");
}
}
}
public List GetConfigFiles(DirectoryInfo modDirectory)
{
List configs = new List();
foreach (var directory in modDirectory.EnumerateDirectories())
{
var localConfigs = directory.EnumerateFiles("config.json");
if (localConfigs.Count() == 0)
{
configs.AddRange(GetConfigFiles(directory));
continue;
}
var localConfig = localConfigs.First();
if (localConfig.Directory is not null && localConfig.Directory.EnumerateFiles("manifest.json", SearchOption.TopDirectoryOnly).Count() == 1)
{
configs.Add(localConfig);
}
}
return configs;
}
public void DiscoverConfigs(string modsFilePath, bool useArchive = false)
{
if (modsFilePath is null || !Directory.Exists(modsFilePath))
{
return;
}
foreach (var fileInfo in GetConfigFiles(new DirectoryInfo(modsFilePath)))
{
if (fileInfo.DirectoryName is null || (Program.settings.IgnoreHiddenFolders && ParentFolderContainsPeriod(modsFilePath, fileInfo.Directory)))
{
continue;
}
var mod = Mods.FirstOrDefault(m => m.ModFileInfo is not null && m.ModFileInfo.DirectoryName == fileInfo.DirectoryName);
if (mod is null)
{
continue;
}
else if (useArchive && mod.Config is not null)
{
if (fileInfo.LastWriteTimeUtc <= mod.Config.LastWriteTimeUtc)
{
continue;
}
mod.Config.Data = File.ReadAllText(fileInfo.FullName);
mod.Config.LastWriteTimeUtc = fileInfo.LastWriteTimeUtc;
}
else
{
mod.Config = new Config() { UniqueId = mod.UniqueId, FilePath = fileInfo.FullName, LastWriteTimeUtc = fileInfo.LastWriteTimeUtc, Data = File.ReadAllText(fileInfo.FullName) };
}
}
}
internal List GetPendingConfigUpdates(Profile profile, bool excludeMissingConfigs = false, bool useArchiveAsBase = false)
{
// Merge any existing preserved configs
List pendingConfigUpdates = new List();
foreach (var modId in profile.EnabledModIds.Select(id => id.ToLower()))
{
var mod = Mods.FirstOrDefault(m => m.UniqueId.Equals(modId, StringComparison.OrdinalIgnoreCase));
if (mod is null || mod.ModFileInfo is null)
{
continue;
}
try
{
if (profile.PreservedModConfigs.ContainsKey(modId))
{
// Write the archived config, if the current one doesn't exist
if (mod.Config is null)
{
if (excludeMissingConfigs || String.IsNullOrEmpty(mod.ModFileInfo.DirectoryName))
{
continue;
}
mod.Config = new Config() { UniqueId = modId, FilePath = Path.Combine(mod.ModFileInfo.DirectoryName, "config.json"), Data = JsonTools.ParseDocumentToString(profile.PreservedModConfigs[modId]) };
pendingConfigUpdates.Add(mod.Config);
}
else
{
// Merge the config
var currentJson = mod.Config.Data;
var archivedJson = JsonTools.ParseDocumentToString(profile.PreservedModConfigs[modId]);
if (JsonDocumentEqualityComparer.Instance.Equals(JsonDocument.Parse(mod.Config.Data), profile.PreservedModConfigs[modId]) is false)
{
// JsonTools.Merge will preserve the originalJson values, but will add new properties from archivedJson
string mergedJson = String.Empty;
if (useArchiveAsBase is false)
{
mergedJson = JsonTools.Merge(archivedJson, currentJson, false); ;
}
else
{
mergedJson = JsonTools.Merge(currentJson, archivedJson, false);
}
// Apply the changes to the config file
//Program.helper.Log($"The mod {modId} does not have its current configuration preserved\nCurrent:\n{currentJson}\nArchived:\n{archivedJson}", Helper.Status.Warning);
pendingConfigUpdates.Add(new Config() { UniqueId = modId, FilePath = mod.Config.FilePath, Data = mergedJson });
}
}
}
else if (mod.Config is not null)
{
pendingConfigUpdates.Add(new Config() { UniqueId = modId, FilePath = mod.Config.FilePath, Data = mod.Config.Data });
}
}
catch (Exception ex)
{
Program.helper.Log($"Failed to process config.json for mod {modId}: {ex}", Helper.Status.Warning);
}
}
return pendingConfigUpdates;
}
internal async void UpdateEndorsements()
{
if (Nexus.Client is null)
{
return;
}
var endorsements = await Nexus.Client.GetEndorsements();
foreach (var mod in Mods.Where(m => m.HasUpdateKeys() && endorsements.Any(e => e.Id == m.NexusModId)))
{
mod.IsEndorsed = endorsements.First(e => e.Id == mod.NexusModId).IsEndorsed();
}
}
internal async void UpdateThumbnails()
{
// Get all existing thumbnails
IEnumerable nexusModThumbnails = new List();
var thumbnailDirectory = new DirectoryInfo(Pathing.GetThumbnailsPath());
if (thumbnailDirectory.Exists)
{
nexusModThumbnails = thumbnailDirectory.EnumerateFiles();
}
var modsWithWebpages = Mods.Where(m => m.NexusModId is not null).ToList();
foreach (var mod in modsWithWebpages)
{
var thumbnail = nexusModThumbnails.FirstOrDefault(t => mod.NexusModId is not null && Path.GetFileNameWithoutExtension(t.Name).Equals(mod.NexusModId.ToString(), StringComparison.OrdinalIgnoreCase));
if (thumbnail is not null)
{
mod.NexusModThumbnailPath = thumbnail.FullName;
}
else if (Nexus.Client is not null && mod.NexusModThumbnailPath is null)
{
mod.NexusModThumbnailPath = await Nexus.Client.DownloadThumbnail((int)mod.NexusModId);
}
}
}
internal void ReadModConfigs(Profile profile)
{
ReadModConfigs(profile, GetPendingConfigUpdates(profile));
}
internal void ReadModConfigs(Profile profile, List pendingConfigUpdates)
{
foreach (var configInfo in pendingConfigUpdates)
{
try
{
profile.PreservedModConfigs[configInfo.UniqueId] = JsonDocument.Parse(configInfo.Data);
}
catch (Exception ex)
{
Program.helper.Log($"Failed to read config for the mod {configInfo.UniqueId} due to the following error:\n{ex}");
}
}
}
internal bool WriteModConfigs(Profile profile)
{
return WriteModConfigs(profile, GetPendingConfigUpdates(profile, useArchiveAsBase: true));
}
internal bool WriteModConfigs(Profile profile, List pendingConfigUpdates)
{
if (pendingConfigUpdates.Count == 0)
{
return false;
}
// Merge any existing preserved configs
foreach (var configInfo in pendingConfigUpdates.Where(c => profile.PreservedModConfigs.ContainsKey(c.UniqueId.ToLower())))
{
try
{
var fileInfo = new FileInfo(configInfo.FilePath);
if (!Directory.Exists(fileInfo.DirectoryName))
{
continue;
}
// Apply the changes to the config file
File.WriteAllText(configInfo.FilePath, configInfo.Data);
}
catch (Exception ex)
{
Program.helper.Log($"Failed to write config for the mod {configInfo.UniqueId} due to the following error:\n{ex}");
}
}
return true;
}
public void EnableModsByProfile(Profile profile)
{
foreach (var mod in Mods)
{
mod.IsEnabled = false;
if (profile.EnabledModIds.Any(id => id.Equals(mod.UniqueId, StringComparison.OrdinalIgnoreCase)))
{
mod.IsEnabled = true;
}
}
HideRequiredMods();
// Update the EnabledModCount
EnabledModCount = Mods.Where(m => m.IsEnabled && !m.IsHidden).Count();
}
public void ForceModState(Profile profile, List mods, bool modEnableState = false)
{
foreach (var mod in Mods)
{
if (mods.Any(m => m.UniqueId.Equals(mod.UniqueId, StringComparison.OrdinalIgnoreCase)) is false)
{
continue;
}
mod.IsEnabled = modEnableState;
}
// Update the EnabledModCount
EnabledModCount = Mods.Where(m => m.IsEnabled && !m.IsHidden).Count();
}
internal void UpdateDataGridGrouping()
{
if (DataView is not null)
{
DataGridPathGroupDescription? currentGroupingMethod = null;
switch (Program.settings.ModGroupingMethod)
{
case ModGrouping.Folder:
currentGroupingMethod = _modPathGrouping;
break;
case ModGrouping.FolderCondensed:
currentGroupingMethod = _rootPathGrouping;
break;
case ModGrouping.ContentPack:
currentGroupingMethod = _frameworkGrouping;
break;
}
foreach (var grouping in DataView.GroupDescriptions.ToList())
{
if (grouping == currentGroupingMethod)
{
continue;
}
DataView.GroupDescriptions.Remove(grouping);
}
if (currentGroupingMethod is not null && DataView.GroupDescriptions.Contains(currentGroupingMethod) is false)
{
DataView.GroupDescriptions.Add(currentGroupingMethod);
}
HandleModGroupingSorting();
}
}
internal void UpdateFilter()
{
if (DataView is not null)
{
UpdateDataGridGrouping();
DataView.Filter = null;
DataView.Filter = ModFilter;
}
}
private bool ModFilter(object item)
{
var mod = item as Mod;
if (mod is null)
{
return false;
}
if (mod.IsHidden)
{
return false;
}
if (_disabledModFilter == DisplayFilter.ShowEnabled && !mod.IsEnabled)
{
return false;
}
else if (_disabledModFilter == DisplayFilter.ShowDisabled && mod.IsEnabled)
{
return false;
}
else if (_disabledModFilter == DisplayFilter.RequireConfig && !mod.HasConfig)
{
return false;
}
if (_showUpdatableMods && String.IsNullOrEmpty(mod.ParsedStatus))
{
return false;
}
if (String.IsNullOrEmpty(_filterText) || _columnFilter is null || !_columnFilter.Any())
{
return true;
}
if (!String.IsNullOrEmpty(_filterText) && _columnFilter.Any())
{
var filterTextNoWhitespace = _filterText.Replace(" ", String.Empty);
if (_columnFilter.Contains(Program.translation.Get("ui.main_window.combobox.mod_name")) && mod.Name.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (_columnFilter.Contains(Program.translation.Get("ui.main_window.combobox.group")))
{
ModGrouping modGroupingMethod = Program.settings.ModGroupingMethod;
switch (Program.settings.ModGroupingMethod)
{
case ModGrouping.Folder:
if (mod.Path.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase) is true)
{
return true;
}
break;
case ModGrouping.FolderCondensed:
if (mod.RootPath.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase) is true)
{
return true;
}
break;
case ModGrouping.ContentPack:
if (mod.FrameworkID is not null && mod.FrameworkID.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase) is true)
{
return true;
}
break;
}
}
if (_columnFilter.Contains(Program.translation.Get("ui.main_window.combobox.top_level_group")))
{
ModGrouping modGroupingMethod = Program.settings.ModGroupingMethod;
switch (Program.settings.ModGroupingMethod)
{
case ModGrouping.Folder:
case ModGrouping.FolderCondensed:
if (mod.RootPath is not null && mod.RootPath.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase) is true)
{
return true;
}
break;
case ModGrouping.ContentPack:
if (mod.FrameworkID is not null && mod.FrameworkID.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase) is true)
{
return true;
}
break;
}
}
if (_columnFilter.Contains(Program.translation.Get("ui.main_window.combobox.author")) && mod.Author.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (_columnFilter.Contains(Program.translation.Get("ui.main_window.combobox.requirements")) && ((mod.HardRequirements is not null && mod.HardRequirements.Any(r => r.Name is null || r.Name.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase))) || (mod.MissingRequirements is not null && mod.MissingRequirements.Any(r => r.Name is null || r.Name.Replace(" ", String.Empty).Contains(filterTextNoWhitespace, StringComparison.OrdinalIgnoreCase)))))
{
return true;
}
}
return false;
}
private void DataViewSortDescription_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
HandleModGroupingSorting();
}
private void HandleModGroupingSorting()
{
switch (Program.settings.ModGroupingMethod)
{
case ModGrouping.None:
break;
case ModGrouping.Folder:
if (DataView.SortDescriptions.Any(d => d.PropertyPath == nameof(Mod.Path)) is false)
{
DataView.SortDescriptions.Add(DataGridSortDescription.FromPath(nameof(Mod.Path), ListSortDirection.Ascending));
}
break;
case ModGrouping.FolderCondensed:
if (DataView.SortDescriptions.Any(d => d.PropertyPath == nameof(Mod.RootPath)) is false)
{
DataView.SortDescriptions.Add(DataGridSortDescription.FromPath(nameof(Mod.RootPath), ListSortDirection.Ascending));
}
break;
case ModGrouping.ContentPack:
if (DataView.SortDescriptions.Any(d => d.PropertyPath == nameof(Mod.FrameworkID)) is false)
{
DataView.SortDescriptions.Add(DataGridSortDescription.FromPath(nameof(Mod.FrameworkID), ListSortDirection.Ascending));
}
break;
}
}
}
}
================================================
FILE: Stardrop/ViewModels/MessageWindowViewModel.cs
================================================
using ReactiveUI;
namespace Stardrop.ViewModels
{
public class MessageWindowViewModel : ViewModelBase
{
private string _messageText;
public string MessageText { get { return _messageText; } set { this.RaiseAndSetIfChanged(ref _messageText, value); } }
private string _positiveButtonText;
public string PositiveButtonText { get { return _positiveButtonText; } set { this.RaiseAndSetIfChanged(ref _positiveButtonText, value); } }
private string _negativeButtonText;
public string NegativeButtonText { get { return _negativeButtonText; } set { this.RaiseAndSetIfChanged(ref _negativeButtonText, value); } }
}
}
================================================
FILE: Stardrop/ViewModels/ModDownloadViewModel.cs
================================================
using ReactiveUI;
using System;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
namespace Stardrop.ViewModels
{
public enum ModDownloadStatus
{
NotStarted,
InProgress,
Successful,
Canceled,
Failed
}
public class ModDownloadViewModel : ViewModelBase
{
private readonly DateTimeOffset _startTime;
private readonly CancellationTokenSource _downloadCancellationSource;
// Communicates up to the parent panel that the user wants to remove this download from the list.
public event EventHandler? RemovalRequested = null!;
// --Set-once properties--
public Uri ModUri { get; init; }
// --Bindable properties--
private string _name;
public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); }
private long? _sizeBytes;
public long? SizeBytes { get => _sizeBytes; set => this.RaiseAndSetIfChanged(ref _sizeBytes, value); }
private long _downloadedBytes;
public long DownloadedBytes { get => _downloadedBytes; set => this.RaiseAndSetIfChanged(ref _downloadedBytes, value); }
private ModDownloadStatus _downloadStatus = ModDownloadStatus.NotStarted;
public ModDownloadStatus DownloadStatus { get => _downloadStatus; set => this.RaiseAndSetIfChanged(ref _downloadStatus, value); }
// --Composite or dependent properties--
private readonly ObservableAsPropertyHelper _completion = null!;
public double Completion => _completion.Value;
private readonly ObservableAsPropertyHelper _isSizeUnknown = null!;
public bool IsSizeUnknown => _isSizeUnknown.Value;
private readonly ObservableAsPropertyHelper _downloadSpeedLabel = null!;
public string DownloadSpeedLabel => _downloadSpeedLabel.Value;
private readonly ObservableAsPropertyHelper _downloadProgressLabel = null!;
public string DownloadProgressLabel => _downloadProgressLabel.Value;
// --Commands--
public ReactiveCommand CancelCommand { get; }
public ReactiveCommand RemoveCommand { get; }
public ModDownloadViewModel(Uri modUri, string name, long? sizeInBytes, CancellationTokenSource downloadCancellationSource)
{
_startTime = DateTimeOffset.UtcNow;
ModUri = modUri;
_name = name;
_sizeBytes = sizeInBytes;
_downloadedBytes = 0;
_downloadCancellationSource = downloadCancellationSource;
CancelCommand = ReactiveCommand.Create(Cancel);
RemoveCommand = ReactiveCommand.Create(Remove);
// SizeBytes null-ness to IsSizeUnknown converison
this.WhenAnyValue(x => x.SizeBytes)
.Select(x => x.HasValue is false)
.ToProperty(this, x => x.IsSizeUnknown, out _isSizeUnknown);
// DownloadedBytes to DownloadSpeedLabel conversion
this.WhenAnyValue(x => x.DownloadedBytes)
.Sample(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler)
.Select(bytes =>
{
double elapsedSeconds = (DateTimeOffset.UtcNow - _startTime).TotalSeconds;
double bytesPerSecond = bytes / elapsedSeconds;
if (bytesPerSecond > 1024 * 1024) // MB
{
return $"{(bytesPerSecond / (1024 * 1024)):N2} {Program.translation.Get("internal.measurements.megabytes_per_second")}";
}
else if (bytesPerSecond > 1024) // KB
{
return $"{(bytesPerSecond / 1024):N2} {Program.translation.Get("internal.measurements.kilobytes_per_second")}";
}
else // Bytes
{
return $"{bytesPerSecond:N0} {Program.translation.Get("internal.measurements.bytes_per_second")}";
}
}).ToProperty(this, x => x.DownloadSpeedLabel, out _downloadSpeedLabel);
// DownloadedBytes and SizeBytes to DownloadProgressLabel conversion
this.WhenAnyValue(x => x.DownloadedBytes, x => x.SizeBytes)
.Sample(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler)
.Select(((long Bytes, long? Total) x) =>
{
string bytesString = ToHumanReadable(x.Bytes);
if (x.Total is null)
{
return $"{bytesString} / ??? {Program.translation.Get("internal.measurements.megabytes_size")}";
}
else
{
string totalString = ToHumanReadable(x.Total!.Value);
return $"{bytesString} / {totalString}";
}
static string ToHumanReadable(long bytes)
{
if (bytes > 1024 * 1024) // MB
{
return $"{(bytes / (1024.0 * 1024.0)):N2} {Program.translation.Get("internal.measurements.megabytes_size")}";
}
else if (bytes > 1024) // KB
{
return $"{(bytes / 1024.0):N2} {Program.translation.Get("internal.measurements.kilobytes_size")}";
}
else
{
return $"{bytes:N0} {Program.translation.Get("internal.measurements.bytes_size")}";
}
}
}).ToProperty(this, x => x.DownloadProgressLabel, out _downloadProgressLabel);
if (SizeBytes.HasValue)
{
// DownloadedBytes to Completion conversion
this.WhenAnyValue(x => x.DownloadedBytes)
.Sample(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler)
.Select(x => (DownloadedBytes / (double)SizeBytes) * 100)
.ToProperty(this, x => x.Completion, out _completion);
}
}
private void Cancel()
{
_downloadCancellationSource.Cancel();
DownloadStatus = ModDownloadStatus.Canceled;
}
private void Remove()
{
RemovalRequested?.Invoke(this, EventArgs.Empty);
}
}
}
================================================
FILE: Stardrop/ViewModels/ProfileEditorViewModel.cs
================================================
using Stardrop.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
namespace Stardrop.ViewModels
{
public class ProfileEditorViewModel : ViewModelBase
{
public ObservableCollection Profiles { get; set; }
public List OldProfiles { get; set; }
public string ToolTip_Save { get; set; }
public string ToolTip_Cancel { get; set; }
private readonly string _profileFilePath;
public ProfileEditorViewModel(string profilesFilePath)
{
OldProfiles = new List();
Profiles = new ObservableCollection();
_profileFilePath = profilesFilePath;
DirectoryInfo profileDirectory = new DirectoryInfo(_profileFilePath);
foreach (var fileInfo in profileDirectory.GetFiles("*.json", SearchOption.AllDirectories))
{
if (fileInfo.DirectoryName is null)
{
continue;
}
try
{
var profile = JsonSerializer.Deserialize(File.ReadAllText(fileInfo.FullName), new JsonSerializerOptions { AllowTrailingCommas = true });
if (profile is null)
{
Program.helper.Log($"The profile file {fileInfo.Name} was empty or not deserializable from {fileInfo.DirectoryName}", Utilities.Helper.Status.Alert);
continue;
}
Profiles.Add(profile);
}
catch (Exception ex)
{
Program.helper.Log($"Unable to load the profile file {fileInfo.Name} from {fileInfo.DirectoryName}: {ex}", Utilities.Helper.Status.Alert);
}
}
if (!Profiles.Any(p => p.Name == Program.defaultProfileName))
{
var defaultProfile = new Profile(Program.defaultProfileName) { IsProtected = true };
Profiles.Insert(0, defaultProfile);
CreateProfile(defaultProfile);
}
else if (Profiles.IndexOf(Profiles.First(p => p.Name == Program.defaultProfileName)) != 0)
{
// Move the default profile to the top
Profiles.Move(Profiles.IndexOf(Profiles.First(p => p.Name == Program.defaultProfileName)), 0);
}
OldProfiles = Profiles.ToList();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
ToolTip_Save = Program.translation.Get("ui.settings_window.tooltips.save_changes");
ToolTip_Cancel = Program.translation.Get("ui.settings_window.tooltips.cancel_changes");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
// TEMPORARY FIX: Due to bug with Avalonia on Linux platforms, tooltips currently cause crashes when they disappear
// To work around this, tooltips are purposely not displayed
}
}
internal void CreateProfile(Profile profile, bool force = false)
{
string fileFullName = Path.Combine(_profileFilePath, profile.Name + ".json");
if (File.Exists(fileFullName) && !force)
{
Program.helper.Log($"Attempted to create an already existing profile file ({profile.Name}) at the path {fileFullName}", Utilities.Helper.Status.Warning);
return;
}
File.WriteAllText(fileFullName, JsonSerializer.Serialize(profile, new JsonSerializerOptions() { WriteIndented = true }));
}
internal void DeleteProfile(Profile profile)
{
string fileFullName = Path.Combine(_profileFilePath, profile.Name + ".json");
if (!File.Exists(fileFullName))
{
Program.helper.Log($"Attempted to delete a non-existent profile file ({profile.Name}) at the path {fileFullName}", Utilities.Helper.Status.Warning);
return;
}
File.Delete(fileFullName);
}
internal void UpdateProfile(Profile profile, List enabledModIds)
{
int profileIndex = Profiles.IndexOf(profile);
if (profileIndex == -1)
{
return;
}
Profiles[profileIndex].EnabledModIds = enabledModIds;
CreateProfile(profile, true);
}
}
}
================================================
FILE: Stardrop/ViewModels/SettingsWindowViewModel.cs
================================================
using Stardrop.Models;
using Stardrop.Utilities;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Stardrop.ViewModels
{
public class SettingsWindowViewModel : ViewModelBase
{
// Setting bindings
public string SMAPIPath { get { return Program.settings.SMAPIFolderPath; } set { Program.settings.SMAPIFolderPath = value; Pathing.SetSmapiPath(Program.settings.SMAPIFolderPath, String.IsNullOrEmpty(Program.settings.ModFolderPath)); } }
public string ModFolderPath { get { return Program.settings.ModFolderPath; } set { Program.settings.ModFolderPath = value; Pathing.SetModPath(Program.settings.ModFolderPath); } }
public string ModInstallPath { get { return Program.settings.ModInstallPath; } set { Program.settings.ModInstallPath = value; } }
public bool IgnoreHiddenFolders { get { return Program.settings.IgnoreHiddenFolders; } set { Program.settings.IgnoreHiddenFolders = value; } }
public bool IsAskingBeforeAcceptingNXM { get { return Program.settings.IsAskingBeforeAcceptingNXM; } set { Program.settings.IsAskingBeforeAcceptingNXM = value; } }
public bool EnableProfileSpecificModConfigs { get { return Program.settings.EnableProfileSpecificModConfigs; } set { Program.settings.EnableProfileSpecificModConfigs = value; } }
public bool EnableModsOnAdd { get { return Program.settings.EnableModsOnAdd; } set { Program.settings.EnableModsOnAdd = value; } }
public bool AlwaysAskToDelete { get { return Program.settings.AlwaysAskToDelete; } set { Program.settings.AlwaysAskToDelete = value; } }
public bool ShouldAutomaticallySaveProfileChanges { get { return Program.settings.ShouldAutomaticallySaveProfileChanges; } set { Program.settings.ShouldAutomaticallySaveProfileChanges = value; } }
public bool ShowModThumbnails { get { return Program.settings.ShowModThumbnails; } set { Program.settings.ShowModThumbnails = value; } }
public List Themes { get; set; } = new List();
// Tooltips
public string ToolTip_SMAPI { get; set; }
public string ToolTip_ModFolder { get; set; }
public string ToolTip_ModInstall { get; set; }
public string ToolTip_Theme { get; set; }
public string ToolTip_Language { get; set; }
public string ToolTip_Grouping { get; set; }
public string ToolTip_IgnoreHiddenFolders { get; set; }
public string ToolTip_PreferredServer { get; set; }
public string ToolTip_NXMAssociation { get; set; }
public string ToolTip_AlwaysAskNXMFiles { get; set; }
public string ToolTip_EnableProfileSpecificModConfigs { get; set; }
public string ToolTip_EnableModsOnAdd { get; set; }
public string ToolTip_ShouldAutomaticallySaveProfileChanges { get; set; }
public string ToolTip_ShowModThumbnails { get; set; }
public string ToolTip_Save { get; set; }
public string ToolTip_Cancel { get; set; }
// Other UI controls
public bool ShowMainMenu { get; set; }
public bool ShowNXMAssociationButton { get; set; }
public bool ShowNexusServers { get; set; }
public SettingsWindowViewModel()
{
ToolTip_SMAPI = Program.translation.Get("ui.settings_window.tooltips.smapi");
ToolTip_ModFolder = Program.translation.Get("ui.settings_window.tooltips.mod_folder_path");
ToolTip_ModInstall = Program.translation.Get("ui.settings_window.tooltips.mod_install_path");
ToolTip_Theme = Program.translation.Get("ui.settings_window.tooltips.theme");
ToolTip_Language = Program.translation.Get("ui.settings_window.tooltips.language");
ToolTip_Grouping = Program.translation.Get("ui.settings_window.tooltips.grouping");
ToolTip_IgnoreHiddenFolders = Program.translation.Get("ui.settings_window.tooltips.ignore_hidden_folders");
ToolTip_PreferredServer = Program.translation.Get("ui.settings_window.tooltips.preferred_server");
ToolTip_NXMAssociation = Program.translation.Get("ui.settings_window.tooltips.nxm_file_association");
ToolTip_AlwaysAskNXMFiles = Program.translation.Get("ui.settings_window.tooltips.always_ask_nxm_files");
ToolTip_EnableProfileSpecificModConfigs = Program.translation.Get("ui.settings_window.tooltips.enable_profile_specific_configs");
ToolTip_EnableModsOnAdd = Program.translation.Get("ui.settings_window.tooltips.enable_mods_on_add");
ToolTip_ShouldAutomaticallySaveProfileChanges = Program.translation.Get("ui.settings_window.tooltips.automatically_save_profile_changes");
ToolTip_ShowModThumbnails = Program.translation.Get("ui.settings_window.tooltips.show_mod_thumbnails");
ToolTip_Save = Program.translation.Get("ui.settings_window.tooltips.save_changes");
ToolTip_Cancel = Program.translation.Get("ui.settings_window.tooltips.cancel_changes");
ShowMainMenu = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
ShowNXMAssociationButton = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
ShowNexusServers = Program.settings.NexusDetails is not null && Program.settings.NexusDetails.IsPremium;
}
}
}
================================================
FILE: Stardrop/ViewModels/ViewModelBase.cs
================================================
using ReactiveUI;
namespace Stardrop.ViewModels
{
public class ViewModelBase : ReactiveObject
{
}
}
================================================
FILE: Stardrop/ViewModels/WarningWindowViewModel.cs
================================================
using ReactiveUI;
namespace Stardrop.ViewModels
{
public class WarningWindowViewModel : ViewModelBase
{
private string _warningText;
public string WarningText { get { return _warningText; } set { this.RaiseAndSetIfChanged(ref _warningText, value); } }
private string _buttonText;
public string ButtonText { get { return _buttonText; } set { this.RaiseAndSetIfChanged(ref _buttonText, value); } }
private bool _isButtonVisible;
public bool IsButtonVisible { get { return _isButtonVisible; } set { this.RaiseAndSetIfChanged(ref _isButtonVisible, value); } }
private bool _isProgressBarVisible;
public bool IsProgressBarVisible { get { return _isProgressBarVisible; } set { this.RaiseAndSetIfChanged(ref _isProgressBarVisible, value); } }
private double _progressBarValue;
public double ProgressBarValue { get { return _progressBarValue; } set { this.RaiseAndSetIfChanged(ref _progressBarValue, value); } }
public WarningWindowViewModel()
{
}
}
}
================================================
FILE: Stardrop/Views/DownloadPanel.axaml
================================================
================================================
FILE: Stardrop/Views/DownloadPanel.axaml.cs
================================================
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Stardrop.Utilities.External;
using Stardrop.ViewModels;
namespace Stardrop.Views;
public partial class DownloadPanel : UserControl
{
private DownloadPanelViewModel _viewModel = null!;
public DownloadPanel()
{
AvaloniaXamlLoader.Load(this);
if (Design.IsDesignMode)
{
// Normally, the background gets handled by the hosting flyout's FlyoutPresenterClasses
// Since design mode doesn't have a flyout to host this, we do it manually
Background = new SolidColorBrush(new Color(0xFF, 0x03, 0x13, 0x32));
return;
}
_viewModel = new DownloadPanelViewModel(Nexus.Client);
DataContext = _viewModel;
}
}
================================================
FILE: Stardrop/Views/FlexibleOptionWindow.axaml
================================================
================================================
FILE: Stardrop/Views/FlexibleOptionWindow.axaml.cs
================================================
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Stardrop.Models.Data.Enums;
using Stardrop.ViewModels;
using System;
namespace Stardrop.Views
{
public partial class FlexibleOptionWindow : Window
{
private readonly FlexibleOptionWindowViewModel _viewModel;
public FlexibleOptionWindow()
{
InitializeComponent();
// Set the main window view
_viewModel = new FlexibleOptionWindowViewModel();
DataContext = _viewModel;
#if DEBUG
this.AttachDevTools();
#endif
}
public FlexibleOptionWindow(string messageText, string? firstButtonText = null, string? secondButtonText = null, string? thirdButtonText = null) : this()
{
_viewModel.MessageText = messageText;
if (String.IsNullOrEmpty(firstButtonText) is false)
{
_viewModel.FirstButtonText = firstButtonText;
_viewModel.IsFirstButtonVisible = true;
}
if (String.IsNullOrEmpty(secondButtonText) is false)
{
_viewModel.SecondButtonText = secondButtonText;
_viewModel.IsSecondButtonVisible = true;
}
if (String.IsNullOrEmpty(thirdButtonText) is false)
{
_viewModel.ThirdButtonText = thirdButtonText;
_viewModel.IsThirdButtonVisible = true;
}
this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
this.SizeToContent = SizeToContent.Height;
}
private void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
Button? button = sender as Button;
if (button is null)
{
return;
}
if (button.Content.Equals(_viewModel.FirstButtonText))
{
this.Close(Choice.First);
}
else if (button.Content.Equals(_viewModel.SecondButtonText))
{
this.Close(Choice.Second);
}
else if (button.Content.Equals(_viewModel.ThirdButtonText))
{
this.Close(Choice.Third);
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
================================================
FILE: Stardrop/Views/MainWindow.axaml
================================================
================================================
FILE: Stardrop/Views/MainWindow.axaml.cs
================================================
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using DynamicData;
using DynamicData.Binding;
using Semver;
using SharpCompress.Archives;
using SharpCompress.Common;
using Stardrop.Models;
using Stardrop.Models.Data;
using Stardrop.Models.Data.Enums;
using Stardrop.Models.Nexus.Web;
using Stardrop.Models.SMAPI;
using Stardrop.Models.SMAPI.Web;
using Stardrop.Utilities;
using Stardrop.Utilities.External;
using Stardrop.Utilities.Internal;
using Stardrop.ViewModels;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using static Stardrop.Models.SMAPI.Web.ModEntryMetadata;
namespace Stardrop.Views
{
public partial class MainWindow : Window
{
private readonly MainWindowViewModel _viewModel;
private readonly ProfileEditorViewModel _editorView;
private DispatcherTimer _searchBoxTimer;
private DispatcherTimer _smapiProcessTimer;
private DispatcherTimer _lockSentinel;
private DispatcherTimer _nxmSentinel;
// Tracking related
private bool _shiftPressed;
private bool _ctrlPressed;
private string _lockReason;
// Session related
private LastSessionData _lastSessionDate;
public MainWindow()
{
InitializeComponent();
// Set the main window view
_viewModel = new MainWindowViewModel(Pathing.defaultModPath, Program.ApplicationVersion);
DataContext = _viewModel;
// Set the path according to the environmental variable SMAPI_MODS_PATH
// SMAPI_MODS_PATH is set via the profile dropdown on the UI
var modGrid = this.FindControl("modGrid");
modGrid.IsReadOnly = true;
modGrid.LoadingRow += (sender, e) => { e.Row.Header = e.Row.GetIndex() + 1; };
modGrid.Items = _viewModel.DataView;
modGrid.LoadingRowGroup += ModGrid_LoadingRowGroup;
AddHandler(DragDrop.DropEvent, Drop);
AddHandler(DragDrop.DragOverEvent, (sender, e) =>
{
_viewModel.DragOverColor = "#1cff96";
});
AddHandler(DragDrop.DragLeaveEvent, (sender, e) =>
{
_viewModel.DragOverColor = "#ff9f2a";
});
// Get the local data
ClientData localDataCache = new ClientData();
if (File.Exists(Pathing.GetDataCachePath()))
{
try
{
localDataCache = JsonSerializer.Deserialize(File.ReadAllText(Pathing.GetDataCachePath()), new JsonSerializerOptions { AllowTrailingCommas = true });
}
catch
{
localDataCache = new ClientData();
}
}
// Set the application's position and size
if (localDataCache.LastSessionData is not null)
{
_lastSessionDate = localDataCache.LastSessionData;
}
// Sets the grid's column visibility, based on previous session
if (localDataCache.ColumnActiveStates is not null)
{
var gridColumnContextMenu = this.FindControl("gridColumnContextMenu");
foreach (MenuItem column in gridColumnContextMenu.Items)
{
string columnName = (string)column.Header;
if (localDataCache.ColumnActiveStates.ContainsKey(columnName))
{
_viewModel.SetColumnVisibility(column, modGrid, localDataCache.ColumnActiveStates[columnName]);
}
}
}
// Handle the mainMenu bar for drag and related events
var menuBorder = this.FindControl("menuBorder");
menuBorder.PointerPressed += MainBar_PointerPressed;
menuBorder.DoubleTapped += MainBar_DoubleTapped;
// HEADER: "Value cannot be null. (Parameter 'path1')" error clears removing the below chunk
// Set profile list
_editorView = new ProfileEditorViewModel(Pathing.GetProfilesFolderPath());
var profileComboBox = this.FindControl("profileComboBox");
profileComboBox.Items = _editorView.Profiles;
profileComboBox.SelectedIndex = 0;
if (_editorView.Profiles.FirstOrDefault(p => p.Name == Program.settings.LastSelectedProfileName) is Profile oldProfile && oldProfile is not null)
{
profileComboBox.SelectedItem = oldProfile;
}
profileComboBox.SelectionChanged += ProfileComboBox_SelectionChanged;
// Update selected mods
var profile = profileComboBox.SelectedItem as Profile;
_viewModel.EnableModsByProfile(profile);
// Check if we have any cached updates for mods
if (_viewModel.IsCheckingForUpdates is false)
{
_viewModel.UpdateStatusText = Program.translation.Get("ui.main_window.button.update_status.updating");
CheckForModUpdates(_viewModel.Mods.ToList(), useCache: true);
}
else
{
CheckForModUpdates(_viewModel.Mods.ToList(), probe: true);
}
// Start sentinel for watching NXM files
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) is false)
{
_nxmSentinel = new DispatcherTimer();
_nxmSentinel.Interval = new TimeSpan(TimeSpan.TicksPerMillisecond * 1000);
_nxmSentinel.Tick += _nxmSentinelTimer_Tick;
_nxmSentinel.Start();
}
// Start sentinel for watching for lock state
_lockSentinel = new DispatcherTimer();
_lockSentinel.Interval = new TimeSpan(TimeSpan.TicksPerMillisecond * 100);
_lockSentinel.Tick += _lockSentinelTimer_Tick;
_lockSentinel.Start();
// FOOTER: "Value cannot be null. (Parameter 'path1')" error clears removing the above chunk
// Handle buttons
this.FindControl