├── NoodleManagerXTests ├── Usings.cs ├── Mods │ ├── json │ │ ├── invalid_version.json │ │ ├── mods_many_versions.json │ │ └── current_list.json │ ├── TestParseModInfoList.cs │ ├── TestUtils.cs │ └── TestRealFiles.cs └── NoodleManagerXTests.csproj ├── NoodleManagerX ├── icon-nm.ico ├── Assets │ ├── NoodleBanner.png │ ├── ProgressBar.png │ ├── NoodleManager.png │ ├── Included │ │ ├── icon-nm.ico │ │ └── searchbar.svg │ ├── SearchSortFilter.png │ ├── Fonts │ │ ├── NotoSans-Bold.ttf │ │ └── NotoSans-Regular.ttf │ ├── Icons │ │ ├── youtube.xaml │ │ ├── play.xaml │ │ ├── maximize.xaml │ │ ├── stop.xaml │ │ ├── minimize.xaml │ │ ├── bracket.xaml │ │ ├── twitter.xaml │ │ ├── youtube.svg │ │ ├── blacklist.xaml │ │ ├── close.xaml │ │ ├── search.xaml │ │ ├── quest.xaml │ │ ├── twitch.xaml │ │ ├── pc.xaml │ │ ├── download.xaml │ │ ├── twitter.svg │ │ ├── delete.xaml │ │ ├── quest.svg │ │ ├── minimize.svg │ │ ├── pc.svg │ │ ├── stop.svg │ │ ├── play.svg │ │ ├── bracket.svg │ │ ├── maximize.svg │ │ ├── twitch.svg │ │ ├── blacklist.svg │ │ ├── close.svg │ │ ├── search.svg │ │ ├── download.svg │ │ ├── delete.svg │ │ ├── github.xaml │ │ └── github.svg │ ├── pagination.svg │ └── QuestAccess.svg ├── Resources │ ├── classdata.tpk │ ├── UpdateHelper.exe │ ├── classdata_large.tpk │ ├── Resources.Designer.cs │ └── Resources.resx ├── References │ └── MediaDevices.dll ├── FodyWeavers.xml ├── nuget.config ├── Mods │ ├── ModPage.cs │ ├── ModVersionSelection.cs │ └── ModDownloadSource.cs ├── Models │ ├── Mods │ │ ├── ModInfoList.cs │ │ ├── ModDependencyInfo.cs │ │ ├── ModInfo.cs │ │ └── ModVersion.cs │ ├── Playlists │ │ ├── PlaylistMap.cs │ │ ├── PlaylistItem.cs │ │ ├── PlaylistFile.cs │ │ └── PlaylistHandler.cs │ ├── Settings.cs │ ├── AvatarHandler.cs │ ├── Stages │ │ ├── StageFile.cs │ │ ├── StageHandler.cs │ │ ├── StageFileInfo.cs │ │ └── StageItem.cs │ ├── MtpDevice.cs │ ├── PlaybackHandler.cs │ ├── LocalItem.cs │ ├── DownloadScheduler.cs │ ├── MapHandler.cs │ └── StorageAbstraction.cs ├── App.axaml ├── Properties │ └── PublishProfiles │ │ ├── Windows_x64.pubxml │ │ ├── Osx_x64.pubxml │ │ └── Linux_x64.pubxml ├── App.axaml.cs ├── Utils │ └── SemVersionJsonConverter.cs ├── MessageBox.axaml ├── Program.cs ├── FodyWeavers.xsd ├── MessageBox.axaml.cs ├── NoodleManagerX.csproj ├── MainWindow.axaml.cs └── ThirdParty │ └── MelonLoader │ └── UnityInformationHandler.cs ├── UpdateHelper ├── UpdateHelper.csproj ├── Properties │ └── PublishProfiles │ │ └── MainPublishProfile.pubxml └── Program.cs ├── README.md ├── .gitattributes ├── NoodleManagerX.sln ├── .gitignore └── QuestAccess.svg /NoodleManagerXTests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; -------------------------------------------------------------------------------- /NoodleManagerX/icon-nm.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/icon-nm.ico -------------------------------------------------------------------------------- /NoodleManagerX/Assets/NoodleBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Assets/NoodleBanner.png -------------------------------------------------------------------------------- /NoodleManagerX/Assets/ProgressBar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Assets/ProgressBar.png -------------------------------------------------------------------------------- /NoodleManagerX/Resources/classdata.tpk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Resources/classdata.tpk -------------------------------------------------------------------------------- /NoodleManagerX/Assets/NoodleManager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Assets/NoodleManager.png -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Included/icon-nm.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Assets/Included/icon-nm.ico -------------------------------------------------------------------------------- /NoodleManagerX/Assets/SearchSortFilter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Assets/SearchSortFilter.png -------------------------------------------------------------------------------- /NoodleManagerX/References/MediaDevices.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/References/MediaDevices.dll -------------------------------------------------------------------------------- /NoodleManagerX/Resources/UpdateHelper.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Resources/UpdateHelper.exe -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Fonts/NotoSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Assets/Fonts/NotoSans-Bold.ttf -------------------------------------------------------------------------------- /NoodleManagerX/Resources/classdata_large.tpk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Resources/classdata_large.tpk -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Fonts/NotoSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tommaier123/NoodleManagerX/HEAD/NoodleManagerX/Assets/Fonts/NotoSans-Regular.ttf -------------------------------------------------------------------------------- /NoodleManagerX/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /NoodleManagerX/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /NoodleManagerX/Mods/ModPage.cs: -------------------------------------------------------------------------------- 1 | using NoodleManagerX.Models; 2 | using NoodleManagerX.Models.Mods; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace NoodleManagerX.Mods 10 | { 11 | class ModPage : GenericPage 12 | { 13 | public List Mods { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Mods/ModInfoList.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace NoodleManagerX.Models.Mods 9 | { 10 | public class ModInfoList 11 | { 12 | [JsonProperty("availableMods")] 13 | public List AvailableMods { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /NoodleManagerX/App.axaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NoodleManagerXTests/Mods/json/invalid_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "availableMods": [ 3 | { 4 | "id": "TestBadVersion", 5 | "name": "Test invalid version", 6 | "author": "bookdude13", 7 | "description": "Invalid semver", 8 | "versions": [ 9 | { 10 | "version": "1.2.3.4", 11 | "downloadUrl": "localhost", 12 | "dependencies": [] 13 | } 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /NoodleManagerX/Models/Playlists/PlaylistMap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace NoodleManagerX.Models.Playlists 8 | { 9 | [Serializable] 10 | public struct PlaylistMap 11 | { 12 | public string hash; 13 | 14 | public string name; 15 | 16 | public string author; 17 | 18 | public string beatmapper; 19 | 20 | public int difficulty; 21 | 22 | public float trackDuration; 23 | 24 | public long addedTime; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NoodleManagerX/Mods/ModVersionSelection.cs: -------------------------------------------------------------------------------- 1 | using NoodleManagerX.Models.Mods; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace NoodleManagerX.Mods 9 | { 10 | public class ModVersionSelection 11 | { 12 | public string ModId { get; private set; } 13 | public ModVersion ModVersion { get; private set; } 14 | 15 | public ModVersionSelection(string modId, ModVersion modVersion) 16 | { 17 | ModId = modId; 18 | ModVersion = modVersion; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Settings.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using ReactiveUI.Fody.Helpers; 3 | using System.Runtime.Serialization; 4 | 5 | namespace NoodleManagerX.Models 6 | { 7 | [DataContract] 8 | class Settings : ReactiveObject 9 | { 10 | [Reactive][DataMember] public string synthDirectory { get; set; } = ""; 11 | [Reactive][DataMember] public bool allowConverts { get; set; } = false; 12 | [Reactive][DataMember] public bool ignoreUpdates { get; set; } = false; 13 | [Reactive][DataMember] public bool getBetas { get; set; } = false; 14 | [Reactive][DataMember] public int previewVolume { get; set; } = 50; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /NoodleManagerX/Properties/PublishProfiles/Windows_x64.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\publish\Windows 10 | FileSystem 11 | net6.0-windows 12 | win-x64 13 | true 14 | true 15 | true 16 | 17 | -------------------------------------------------------------------------------- /NoodleManagerX/Properties/PublishProfiles/Osx_x64.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\publish\Osx 10 | FileSystem 11 | net5.0 12 | osx-x64 13 | true 14 | True 15 | 16 | -------------------------------------------------------------------------------- /NoodleManagerX/App.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls.ApplicationLifetimes; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace NoodleManagerX 6 | { 7 | public class App : Application 8 | { 9 | public override void Initialize() 10 | { 11 | AvaloniaXamlLoader.Load(this); 12 | } 13 | 14 | public override void OnFrameworkInitializationCompleted() 15 | { 16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) 17 | { 18 | desktop.MainWindow = new MainWindow(); 19 | } 20 | 21 | base.OnFrameworkInitializationCompleted(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NoodleManagerX/Properties/PublishProfiles/Linux_x64.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\publish\Linux 10 | FileSystem 11 | net5.0 12 | linux-x64 13 | true 14 | True 15 | 16 | -------------------------------------------------------------------------------- /UpdateHelper/UpdateHelper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net5.0-windows 6 | en 7 | true 8 | true 9 | link 10 | embedded 11 | AnyCPU;x84 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /UpdateHelper/Properties/PublishProfiles/MainPublishProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | C:\Users\Max\source\repos\tommaier123\NoodleManagerX\NoodleManagerX\Resources 10 | FileSystem 11 | net5.0-windows 12 | win-x86 13 | True 14 | False 15 | true 16 | 17 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Playlists/PlaylistItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace NoodleManagerX.Models.Playlists 9 | { 10 | [DataContract] 11 | public class PlaylistItem : GenericItem 12 | { 13 | [DataMember] public List items { get; set; } 14 | public string duration { get { return items != null ? items.Count().ToString() : "0"; } } 15 | 16 | public override string target { get; set; } = "CustomPlaylists"; 17 | public override ItemType itemType { get; set; } = ItemType.Playlist; 18 | } 19 | 20 | [DataContract] 21 | public class PlaylistEntryItem 22 | { 23 | [DataMember] public int id { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Mods/ModDependencyInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NoodleManagerX.Utils; 3 | using Semver; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace NoodleManagerX.Models.Mods 11 | { 12 | public class ModDependencyInfo 13 | { 14 | [JsonProperty("id", Required = Required.Always)] 15 | public string Id { get; set; } = "N/A"; 16 | 17 | [JsonProperty("minVersion", Required = Required.Always)] 18 | [JsonConverter(typeof(SemVersionJsonConverter))] 19 | public SemVersion MinVersion { get; set; } = null; 20 | 21 | [JsonProperty("maxVersion", Required = Required.Default)] 22 | [JsonConverter(typeof(SemVersionJsonConverter))] 23 | public SemVersion MaxVersion { get; set; } = null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NoodleManagerX/Mods/ModDownloadSource.cs: -------------------------------------------------------------------------------- 1 | using NoodleManagerX.Models; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace NoodleManagerX.Mods 9 | { 10 | class ModDownloadSource 11 | { 12 | public static string GetModItemBaseDownloadUrl() 13 | { 14 | return $"{GetModsBaseUrl()}/Downloads"; 15 | } 16 | 17 | public static string GetModsBaseUrl() 18 | { 19 | return $"https://raw.githubusercontent.com/bookdude13/SRModsList/{GetModListBranch()}/SynthRiders"; 20 | } 21 | 22 | private static string GetModListBranch() 23 | { 24 | var usingBetas = MainViewModel.s_instance?.settings?.getBetas ?? false; 25 | return usingBetas ? "dev" : "master"; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NoodleManagerX/Utils/SemVersionJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Semver; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace NoodleManagerX.Utils 10 | { 11 | public class SemVersionJsonConverter : JsonConverter 12 | { 13 | public override SemVersion ReadJson(JsonReader reader, Type objectType, SemVersion existingValue, bool hasExistingValue, JsonSerializer serializer) 14 | { 15 | string s = (string)reader.Value; 16 | var parsed = SemVersion.Parse(s, SemVersionStyles.OptionalMinorPatch); 17 | return parsed; 18 | } 19 | 20 | public override void WriteJson(JsonWriter writer, SemVersion value, JsonSerializer serializer) 21 | { 22 | writer.WriteValue(value.ToString()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Mods/ModInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace NoodleManagerX.Models.Mods 9 | { 10 | public class ModInfo 11 | { 12 | [JsonProperty("id", Required = Required.Always)] 13 | public string Id { get; set; } = "N/A"; 14 | 15 | [JsonProperty("name", Required = Required.Always)] 16 | public string Name { get; set; } = "N/A"; 17 | 18 | [JsonProperty("author", Required = Required.Always)] 19 | public string Author { get; set; } = "N/A"; 20 | 21 | [JsonProperty("description", Required = Required.Always)] 22 | public string Description { get; set; } = ""; 23 | 24 | [JsonProperty("versions", Required = Required.Always)] 25 | public List Versions { get; set; } = new List(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NoodleManagerXTests/Mods/TestParseModInfoList.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NoodleManagerX.Models.Mods; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace NoodleManagerXTests.Mods 10 | { 11 | internal class TestParseModInfoList 12 | { 13 | [Test] 14 | public void Test_ParseModInfoList_VariousValidVersions() 15 | { 16 | string rawJson = TestUtils.GetTestJsonFileContents("mods_many_versions.json"); 17 | ModInfoList? modList = JsonConvert.DeserializeObject(rawJson); 18 | Assert.That(modList, Is.Not.Null); 19 | } 20 | 21 | [Test] 22 | public void Test_ParseModInfoList_BadOnlyMajor() 23 | { 24 | string rawJson = TestUtils.GetTestJsonFileContents("invalid_version.json"); 25 | Assert.Throws(() => JsonConvert.DeserializeObject(rawJson)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/youtube.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/play.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NoodleManagerX/MessageBox.axaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /NoodleManagerX/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.ReactiveUI; 3 | using Avalonia.Svg.Skia; 4 | using System; 5 | 6 | namespace NoodleManagerX 7 | { 8 | class Program 9 | { 10 | // Initialization code. Don't use any Avalonia, third-party APIs or any 11 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized 12 | // yet and stuff might break. 13 | public static void Main(string[] args) => BuildAvaloniaApp() 14 | .StartWithClassicDesktopLifetime(args); 15 | 16 | // Avalonia configuration, don't remove; also used by visual designer. 17 | public static AppBuilder BuildAvaloniaApp() 18 | { 19 | #if DEBUG 20 | //so the designer doesn't break 21 | GC.KeepAlive(typeof(SvgImageExtension).Assembly); 22 | GC.KeepAlive(typeof(Avalonia.Svg.Skia.Svg).Assembly); 23 | #endif 24 | 25 | 26 | return AppBuilder.Configure() 27 | .UseReactiveUI() 28 | .UsePlatformDetect() 29 | .LogToTrace(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/AvatarHandler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.Runtime.Serialization; 4 | 5 | namespace NoodleManagerX.Models 6 | { 7 | class AvatarHandler : GenericHandler 8 | { 9 | public override ItemType itemType { get; set; } = ItemType.Avatar; 10 | public override string apiEndpoint { get; set; } = "https://synthriderz.com/api/models/avatars"; 11 | public override string folder { get; set; } = "Avatars"; 12 | public override string[] extensions { get; set; } = { ".vrm" }; 13 | 14 | public override dynamic DeserializePage(string json) 15 | { 16 | return JsonConvert.DeserializeObject(json); 17 | } 18 | } 19 | 20 | [DataContract] 21 | class AvatarItem : GenericItem 22 | { 23 | public override string target { get; set; } = "Avatars"; 24 | public override ItemType itemType { get; set; } = ItemType.Avatar; 25 | } 26 | 27 | #pragma warning disable 0649 28 | class AvatarPage : GenericPage 29 | { 30 | public List data; 31 | } 32 | #pragma warning restore 0649 33 | } 34 | 35 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/maximize.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/stop.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Stages/StageFile.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.Serialization; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace NoodleManagerX.Models.Stages 10 | { 11 | [Serializable] 12 | public struct StageFile 13 | { 14 | [DataMember] 15 | [JsonProperty("id")] 16 | public int Id; 17 | 18 | [DataMember] 19 | [JsonProperty("filename")] 20 | public string Filename; 21 | 22 | [DataMember] 23 | [JsonProperty("extension")] 24 | public string Extension; 25 | 26 | [DataMember] 27 | [JsonProperty("filesize")] 28 | public int Size; 29 | 30 | [DataMember] 31 | [JsonProperty("visibility")] 32 | public string Visibility; 33 | 34 | [DataMember] 35 | [JsonProperty("cdn_url")] 36 | public string Url; 37 | 38 | [DataMember] 39 | [JsonProperty("download_count")] 40 | public int DownloadCount; 41 | 42 | [DataMember] 43 | [JsonProperty("created_at")] 44 | public DateTime CreatedAt; 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/minimize.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/bracket.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Stages/StageHandler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NoodleManagerX.Models.Playlists; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | using System.Runtime.Serialization; 7 | using System.Threading.Tasks; 8 | 9 | namespace NoodleManagerX.Models.Stages 10 | { 11 | class StageHandler : GenericHandler 12 | { 13 | public override ItemType itemType { get; set; } = ItemType.Stage; 14 | public override string apiEndpoint { get; set; } = "https://synthriderz.com/api/models/stages"; 15 | public override string folder { get; set; } = "CustomStages"; 16 | public override string join { get; set; } = "files&join=files.file&join=experience_beatmap&join=id"; 17 | public override string[] extensions { get; set; } = { ".stage", ".stagedroid"}; 18 | 19 | public override Task LoadLocalItems() 20 | { 21 | return base.LoadLocalItems(); 22 | } 23 | 24 | public override dynamic DeserializePage(string json) 25 | { 26 | return JsonConvert.DeserializeObject(json); 27 | } 28 | } 29 | 30 | #pragma warning disable 0649 31 | class StagePage : GenericPage 32 | { 33 | public List data; 34 | } 35 | #pragma warning restore 0649 36 | } 37 | 38 | -------------------------------------------------------------------------------- /NoodleManagerX/FodyWeavers.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. 12 | 13 | 14 | 15 | 16 | A comma-separated list of error codes that can be safely ignored in assembly verification. 17 | 18 | 19 | 20 | 21 | 'false' to turn off automatic generation of the XML Schema file. 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Mods/ModVersion.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NoodleManagerX.Models; 3 | using NoodleManagerX.Utils; 4 | using Semver; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Runtime.Serialization; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace NoodleManagerX.Models.Mods 13 | { 14 | public class ModVersion 15 | { 16 | [JsonProperty("version", Required = Required.Always)] 17 | [JsonConverter(typeof(SemVersionJsonConverter))] 18 | public SemVersion Version { get; set; } = null; 19 | 20 | [JsonProperty("downloadUrl", Required = Required.Always)] 21 | public string DownloadUrl { get; set; } = ""; 22 | 23 | [JsonProperty("dependencies", Required = Required.Always)] 24 | public List Dependencies { get; set; } = new List(); 25 | 26 | // TODO make this nested. Assumes max depth of 1 for dependencies right now 27 | public bool HasDependency(string dependencyModId) 28 | { 29 | foreach (var dependency in this.Dependencies) 30 | { 31 | if (dependencyModId == dependency.Id) 32 | { 33 | return true; 34 | } 35 | } 36 | 37 | return false; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/twitter.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Playlists/PlaylistFile.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace NoodleManagerX.Models.Playlists 7 | { 8 | /// 9 | /// This is the PlaylistItem used in Synth Riders, saved to .playlist file 10 | /// 11 | [Serializable] 12 | public struct PlaylistFile 13 | { 14 | [JsonProperty("dataString")] 15 | public List Songs; 16 | 17 | public int SelectedIconIndex; 18 | 19 | public int SelectedTexture; 20 | 21 | [JsonProperty("namePlaylist")] 22 | public string Name; 23 | 24 | [JsonProperty("description")] 25 | public string Description; 26 | 27 | [JsonProperty("gradientTop")] 28 | public string GradientTop; 29 | 30 | [JsonProperty("gradientDown")] 31 | public string GradientDown; 32 | 33 | [JsonProperty("colorTitle")] 34 | public string TitleColorString; 35 | 36 | [JsonProperty("colorTexture")] 37 | public string TextureColorString; 38 | 39 | [JsonProperty("creationDate")] 40 | public string CreationDate; 41 | 42 | public bool IsEqual(PlaylistFile target) 43 | { 44 | if (Songs == null || target.Songs == null || Songs.Count != target.Songs.Count) 45 | { 46 | return false; 47 | } 48 | 49 | if (Name.Equals(target.Name)) 50 | { 51 | return Songs.SequenceEqual(target.Songs); 52 | } 53 | 54 | return false; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/blacklist.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/close.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/search.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Stages/StageFileInfo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NoodleManagerX.Models.Playlists; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Runtime.Serialization; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace NoodleManagerX.Models.Stages 11 | { 12 | [Serializable] 13 | public class StageFileInfo 14 | { 15 | [DataMember] 16 | [JsonProperty("id")] 17 | public int Id; 18 | 19 | /// 20 | /// Can be "normal", "spin" or "experience" 21 | /// 22 | [DataMember] 23 | [JsonProperty("mode")] 24 | public string Mode; 25 | 26 | /// 27 | /// Can be "pc" or "quest" 28 | /// 29 | [DataMember] 30 | [JsonProperty("platform")] 31 | public string Platform; 32 | 33 | [DataMember] 34 | [JsonProperty("type")] 35 | public string StageType; 36 | 37 | [DataMember] 38 | [JsonProperty("version")] 39 | public int Version; 40 | 41 | [DataMember] 42 | [JsonProperty("file")] 43 | public StageFile File; 44 | 45 | public bool IsPc() 46 | { 47 | return Platform == "pc"; 48 | } 49 | 50 | public bool IsQuest() 51 | { 52 | return Platform == "quest"; 53 | } 54 | 55 | public bool IsNormalStage() 56 | { 57 | return Mode == "normal"; 58 | } 59 | 60 | public bool IsSpinStage() 61 | { 62 | return Mode == "spin"; 63 | } 64 | 65 | public bool IsExperienceStage() 66 | { 67 | return Mode == "experience"; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/quest.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /NoodleManagerXTests/NoodleManagerXTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0-windows 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | PreserveNewest 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/twitch.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/pc.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/download.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NoodleManagerX 2 | [![Release](https://img.shields.io/github/release/tommaier123/NoodleManagerX.svg)](https://github.com/tommaier123/NoodleManagerX/releases/latest) 3 | [![Downloads](https://img.shields.io/github/downloads/tommaier123/NoodleManagerX/total)](https://github.com/tommaier123/NoodleManagerX/releases/latest) 4 | [![Twitch](https://img.shields.io/twitch/status/Nova_Max_?style=social)](https://www.twitch.tv/Nova_Max_) 5 | [![Twitter](https://img.shields.io/twitter/follow/Nova_Max_?style=social)](https://twitter.com/Nova_Max_) 6 | 7 | ## I am no longer actively maintaining this project. Therefore, I can give no guarantee about support, bug fixes, or security issues coming from the end-of-life .NET runtime or any libraries. Use at your own risk. 8 | ### DM me on Discord to be added as a maintainer. 9 | 10 | This is a standalone app to download Synth Riders custom content for PC and Quest. 11 | Other android based headsets should also work but have not been tested. 12 | A version for Mac and Linux is being worked on. 13 | 14 | ### [Click to download](https://github.com/tommaier123/NoodleManagerX/releases/latest) and check out the [Wiki](https://github.com/tommaier123/NoodleManagerX/wiki) on how to install or use the NoodleManagerX 15 | 16 | 17 | 18 | ## Features: 19 | - Quest support over MTP, no developer mode necessary (should also work for other android based headsets) 20 | - Pagination (access all songs) 21 | - Searching, sorting and filtering 22 | - Improved speed due to multi-threading 23 | - Progress bar for downloads 24 | - Multi download with get page and get all 25 | - Blacklist 26 | - Audio preview 27 | - Removing corrupted maps automatically 28 | - Redownloading updated maps automatically 29 | - Map timestamps are set so the ordering in game is correct (only on PC) 30 | - Auto updating 31 | - Fixed all known bugs of the old NoodleManager 32 | 33 | ## Beta Features 34 | - Playlists 35 | - Avatars 36 | - Stages 37 | - Mods 38 | 39 | ## Work in progress: 40 | - Cross-platform 70% 41 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/delete.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /NoodleManagerXTests/Mods/TestUtils.cs: -------------------------------------------------------------------------------- 1 | using NoodleManagerX.Models.Mods; 2 | using Semver; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace NoodleManagerXTests.Mods 10 | { 11 | public class TestUtils 12 | { 13 | public static string GetTestJsonFileContents(string filename) 14 | { 15 | var fullPath = Path.Combine(".", "Mods", "json", filename); 16 | return File.ReadAllText(fullPath); 17 | } 18 | 19 | public static ModInfo CreateTestMod(string id, List versions) 20 | { 21 | return new ModInfo 22 | { 23 | Id = id, 24 | Name = id + " name", 25 | Author = "Foo", 26 | Description = "Some mod", 27 | Versions = versions, 28 | }; 29 | } 30 | 31 | public static ModVersion CreateTestModVersion(string version, List? dependencies = null) 32 | { 33 | return new ModVersion 34 | { 35 | Version = SemVersion.Parse(version, SemVersionStyles.Any), 36 | DownloadUrl = "localhost", 37 | Dependencies = dependencies ?? new(), 38 | }; 39 | } 40 | 41 | public static ModDependencyInfo CreateTestDependency(string id, string minVersion, string? maxVersion = null) 42 | { 43 | return new ModDependencyInfo 44 | { 45 | Id = id, 46 | MinVersion = SemVersion.Parse(minVersion, SemVersionStyles.Any), 47 | MaxVersion = maxVersion == null ? null : SemVersion.Parse(maxVersion, SemVersionStyles.Any), 48 | }; 49 | } 50 | public static void AssertVersionsEqual(SemVersion v1, string v2) 51 | { 52 | var semVersion2 = SemVersion.Parse(v2, SemVersionStyles.Any); 53 | Assert.That(v1.ComparePrecedenceTo(semVersion2), Is.EqualTo(0), $"SemVersion {v1} != {v2}"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /NoodleManagerXTests/Mods/json/mods_many_versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "availableMods": [ 3 | { 4 | "id": "SRVoting", 5 | "name": "SRVoting", 6 | "author": "bookdude13", 7 | "description": "Add voting buttons for custom maps in-game", 8 | "versions": [ 9 | { 10 | "version": "1.2.0", 11 | "downloadUrl": "https://github.com/bookdude13/SRVoting/releases/download/v1.2.0/SRVoting_v1.2.0.zip", 12 | "dependencies": [ 13 | { 14 | "id": "SRModCore", 15 | "minVersion": "1.0.0" 16 | } 17 | ] 18 | } 19 | ] 20 | }, 21 | { 22 | "id": "SRModCore", 23 | "name": "SRModCore", 24 | "author": "bookdude13", 25 | "description": "Shared library for various SynthRiders mods", 26 | "versions": [ 27 | { 28 | "version": "1.0.0", 29 | "downloadUrl": "https://github.com/bookdude13/SRModCore/releases/download/v1.0.0/SRModCore_v1.0.0.zip", 30 | "dependencies": [] 31 | } 32 | ] 33 | }, 34 | { 35 | "id": "TestMajor", 36 | "name": "Test versions major", 37 | "author": "bookdude13", 38 | "description": "Only major version", 39 | "versions": [ 40 | { 41 | "version": "1", 42 | "downloadUrl": "localhost", 43 | "dependencies": [] 44 | } 45 | ] 46 | }, 47 | { 48 | "id": "TestFull", 49 | "name": "Test versions full", 50 | "author": "bookdude13", 51 | "description": "Full version", 52 | "versions": [ 53 | { 54 | "version": "1.2.3", 55 | "downloadUrl": "localhost", 56 | "dependencies": [] 57 | } 58 | ] 59 | }, 60 | { 61 | "id": "TestAlpha", 62 | "name": "Test versions alpha", 63 | "author": "bookdude13", 64 | "description": "Alpha version", 65 | "versions": [ 66 | { 67 | "version": "1.2.3-alpha", 68 | "downloadUrl": "localhost", 69 | "dependencies": [] 70 | } 71 | ] 72 | } 73 | ] 74 | } -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/quest.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 40 | 44 | 52 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/pc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 38 | 40 | 44 | 52 | 56 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/bracket.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 49 | 51 | 52 | 54 | image/svg+xml 55 | 57 | 58 | 59 | 60 | 64 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/maximize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /NoodleManagerXTests/Mods/TestRealFiles.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NoodleManagerX.Models.Mods; 3 | using NoodleManagerX.Mods; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace NoodleManagerXTests.Mods 11 | { 12 | internal class TestRealFiles 13 | { 14 | [Test] 15 | public void Test_LatestVersion_AllAutoSelect_ParsedAndResolved() 16 | { 17 | string rawJson = TestUtils.GetTestJsonFileContents("current_list.json"); 18 | ModInfoList? modList = JsonConvert.DeserializeObject(rawJson); 19 | Assert.That(modList, Is.Not.Null); 20 | 21 | var graph = new ModDependencyGraph(); 22 | graph.LoadMods(modList.AvailableMods); 23 | 24 | var selections = new List 25 | { 26 | new ModVersionSelection("SRModsList", null), 27 | new ModVersionSelection("Trashbin", null), 28 | new ModVersionSelection("MultiplayerSongGrabber", null), 29 | new ModVersionSelection("CustomSounds", null), 30 | new ModVersionSelection("SRVoting", null), 31 | new ModVersionSelection("SRPerformanceMeter", null), 32 | new ModVersionSelection("SynthRiders-Websockets-Mod", null), 33 | }; 34 | graph.Resolve(selections); 35 | 36 | Assert.Multiple(() => 37 | { 38 | Assert.That(graph.State, Is.EqualTo(ModDependencyGraph.ResolvedState.RESOLVED)); 39 | Assert.That(graph.ResolvedVersions, Has.Count.EqualTo(8)); 40 | TestUtils.AssertVersionsEqual(graph.ResolvedVersions["SRModsList"].Version, "1.0.1"); 41 | TestUtils.AssertVersionsEqual(graph.ResolvedVersions["Trashbin"].Version, "1.2.0"); 42 | TestUtils.AssertVersionsEqual(graph.ResolvedVersions["MultiplayerSongGrabber"].Version, "1.0.0"); 43 | TestUtils.AssertVersionsEqual(graph.ResolvedVersions["CustomSounds"].Version, "1.1.0"); 44 | TestUtils.AssertVersionsEqual(graph.ResolvedVersions["SRVoting"].Version, "1.2.0"); 45 | TestUtils.AssertVersionsEqual(graph.ResolvedVersions["SRPerformanceMeter"].Version, "1.2.1"); 46 | TestUtils.AssertVersionsEqual(graph.ResolvedVersions["SynthRiders-Websockets-Mod"].Version, "1.0.2"); 47 | TestUtils.AssertVersionsEqual(graph.ResolvedVersions["SRModCore"].Version, "1.0.0"); 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/twitch.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 38 | 42 | 43 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/blacklist.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 61 | 67 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 68 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 48 | 50 | 51 | 53 | image/svg+xml 54 | 56 | 57 | 58 | 59 | 63 | 70 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /NoodleManagerX/MessageBox.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Controls; 2 | using Avalonia.Markup.Xaml; 3 | using NoodleManagerX.Models; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace MsgBox 8 | { 9 | public class MessageBox : Window 10 | { 11 | public enum MessageBoxButtons 12 | { 13 | Ok, 14 | OkCancel, 15 | YesNo, 16 | YesNoCancel 17 | } 18 | 19 | public enum MessageBoxResult 20 | { 21 | Ok, 22 | Cancel, 23 | Yes, 24 | No 25 | } 26 | 27 | public MessageBox() 28 | { 29 | AvaloniaXamlLoader.Load(this); 30 | } 31 | 32 | public static async Task Show(Window parent, string text, string title, MessageBoxButtons buttons) 33 | { 34 | var msgbox = new MessageBox() 35 | { 36 | Title = title 37 | }; 38 | msgbox.FindControl("Text").Text = text; 39 | var buttonPanel = msgbox.FindControl("Buttons"); 40 | 41 | var res = MessageBoxResult.Ok; 42 | 43 | void AddButton(string caption, MessageBoxResult r, bool def = false) 44 | { 45 | var btn = new Button { Content = caption }; 46 | btn.Click += (_, __) => 47 | { 48 | res = r; 49 | msgbox.Close(); 50 | }; 51 | buttonPanel.Children.Add(btn); 52 | if (def) 53 | res = r; 54 | } 55 | 56 | if (buttons == MessageBoxButtons.Ok || buttons == MessageBoxButtons.OkCancel) 57 | AddButton("Ok", MessageBoxResult.Ok, true); 58 | if (buttons == MessageBoxButtons.YesNo || buttons == MessageBoxButtons.YesNoCancel) 59 | { 60 | AddButton("Yes", MessageBoxResult.Yes); 61 | AddButton("No", MessageBoxResult.No, true); 62 | } 63 | 64 | if (buttons == MessageBoxButtons.OkCancel || buttons == MessageBoxButtons.YesNoCancel) 65 | AddButton("Cancel", MessageBoxResult.Cancel, true); 66 | 67 | 68 | var tcs = new TaskCompletionSource(); 69 | msgbox.Closed += delegate { tcs.TrySetResult(res); }; 70 | try 71 | { 72 | if (parent != null) 73 | { 74 | await msgbox.ShowDialog(parent); 75 | } 76 | else 77 | { 78 | msgbox.Show(); 79 | } 80 | } 81 | catch (Exception ex) 82 | { 83 | MainViewModel.Log(ex.Message); 84 | } 85 | 86 | return await tcs.Task; 87 | } 88 | 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/pagination.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 44 | 49 | 54 | 1/24 65 | 66 | 67 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 68 | 73 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Playlists/PlaylistHandler.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Threading; 2 | using DynamicData; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Runtime.Serialization; 10 | using System.Threading.Tasks; 11 | 12 | namespace NoodleManagerX.Models.Playlists 13 | { 14 | class PlaylistHandler : GenericHandler 15 | { 16 | public override ItemType itemType { get; set; } = ItemType.Playlist; 17 | public override string join { get; set; } = "items||id"; 18 | public override string apiEndpoint { get; set; } = "https://synthriderz.com/api/playlists"; 19 | public override string folder { get; set; } = "CustomPlaylists"; 20 | public override string[] extensions { get; set; } = { ".playlist" }; 21 | 22 | public override dynamic DeserializePage(string json) 23 | { 24 | return JsonConvert.DeserializeObject(json); 25 | } 26 | 27 | public override Task LoadLocalItems() 28 | { 29 | return base.LoadLocalItems(); 30 | } 31 | 32 | private PlaylistFile? GetPlaylistFileFromPath(string path) 33 | { 34 | try 35 | { 36 | using Stream stream = StorageAbstraction.ReadFile(path); 37 | using StreamReader contents = new StreamReader(stream); 38 | JsonSerializer serializer = new JsonSerializer(); 39 | PlaylistFile playlist = (PlaylistFile)serializer.Deserialize(contents, typeof(PlaylistFile)); 40 | return playlist; 41 | } 42 | catch (Exception e) { MainViewModel.Log(MethodBase.GetCurrentMethod(), e); } 43 | 44 | return null; 45 | } 46 | 47 | public override Task GetLocalItem(string path) 48 | { 49 | try 50 | { 51 | if (StorageAbstraction.FileExists(path)) 52 | { 53 | PlaylistFile? playlist = GetPlaylistFileFromPath(path); 54 | if (playlist == null) 55 | { 56 | MainViewModel.Log($"Failed to load playlist {path} from file"); 57 | return Task.FromResult(false); 58 | } 59 | 60 | var localItem = new LocalItem(-1, playlist.Value.Name, Path.GetFileName(path), StorageAbstraction.GetLastWriteTime(path), itemType); 61 | 62 | MainViewModel.s_instance.localItems.Add(localItem); 63 | return Task.FromResult(true); 64 | } 65 | } 66 | catch (Exception e) { MainViewModel.Log(MethodBase.GetCurrentMethod(), e); } 67 | return Task.FromResult(false); 68 | } 69 | } 70 | 71 | #pragma warning disable 0649 72 | class PlaylistPage : GenericPage 73 | { 74 | public List data; 75 | } 76 | #pragma warning restore 0649 77 | 78 | struct PlaylistNameFile 79 | { 80 | public string Name { get; set; } 81 | public string FilePath { get; set; } 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /NoodleManagerX.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NoodleManagerX", "NoodleManagerX\NoodleManagerX.csproj", "{5B4DBD3E-3920-43F7-96B5-00CC7737E702}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UpdateHelper", "UpdateHelper\UpdateHelper.csproj", "{1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoodleManagerXTests", "NoodleManagerXTests\NoodleManagerXTests.csproj", "{54F5ADCB-3C93-41F0-BDC8-2522F0274347}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Release|Any CPU = Release|Any CPU 17 | Release|x64 = Release|x64 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {5B4DBD3E-3920-43F7-96B5-00CC7737E702}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {5B4DBD3E-3920-43F7-96B5-00CC7737E702}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {5B4DBD3E-3920-43F7-96B5-00CC7737E702}.Debug|x64.ActiveCfg = Debug|x64 23 | {5B4DBD3E-3920-43F7-96B5-00CC7737E702}.Debug|x64.Build.0 = Debug|x64 24 | {5B4DBD3E-3920-43F7-96B5-00CC7737E702}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {5B4DBD3E-3920-43F7-96B5-00CC7737E702}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {5B4DBD3E-3920-43F7-96B5-00CC7737E702}.Release|x64.ActiveCfg = Release|x64 27 | {5B4DBD3E-3920-43F7-96B5-00CC7737E702}.Release|x64.Build.0 = Release|x64 28 | {1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}.Debug|x64.ActiveCfg = Debug|x64 31 | {1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}.Debug|x64.Build.0 = Debug|x64 32 | {1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}.Release|x64.ActiveCfg = Release|x64 35 | {1C53E0DB-F28A-4601-B6AE-CFBDE4A24901}.Release|x64.Build.0 = Release|x64 36 | {54F5ADCB-3C93-41F0-BDC8-2522F0274347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {54F5ADCB-3C93-41F0-BDC8-2522F0274347}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {54F5ADCB-3C93-41F0-BDC8-2522F0274347}.Debug|x64.ActiveCfg = Debug|Any CPU 39 | {54F5ADCB-3C93-41F0-BDC8-2522F0274347}.Debug|x64.Build.0 = Debug|Any CPU 40 | {54F5ADCB-3C93-41F0-BDC8-2522F0274347}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {54F5ADCB-3C93-41F0-BDC8-2522F0274347}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {54F5ADCB-3C93-41F0-BDC8-2522F0274347}.Release|x64.ActiveCfg = Release|Any CPU 43 | {54F5ADCB-3C93-41F0-BDC8-2522F0274347}.Release|x64.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(ExtensibilityGlobals) = postSolution 49 | SolutionGuid = {B9C91276-92F7-4161-8EB5-1848234BF357} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /UpdateHelper/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Security.Principal; 6 | using System.Threading; 7 | using static System.Environment; 8 | 9 | namespace UpdateHelper 10 | { 11 | internal class Program 12 | { 13 | static void Main(string[] args) 14 | { 15 | try 16 | { 17 | if (args.Length >= 1) 18 | { 19 | bool isElevated; 20 | using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) 21 | { 22 | WindowsPrincipal principal = new WindowsPrincipal(identity); 23 | isElevated = principal.IsInRole(WindowsBuiltInRole.Administrator); 24 | } 25 | if (isElevated) Log("Running as administrator"); 26 | else Log("Running as user"); 27 | 28 | string targetPath = args[0].Trim('\"'); 29 | 30 | string tmpPath = Path.Combine(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName), "NoodleManagerX.exe"); 31 | Log("Updating from " + tmpPath + " to " + targetPath); 32 | 33 | bool canWrite = false; 34 | int attempts = 0; 35 | while (!canWrite && attempts < 20) 36 | { 37 | using (var fs = new FileStream(targetPath, FileMode.Open)) 38 | { 39 | canWrite = fs.CanWrite; 40 | } 41 | if (!canWrite) 42 | { 43 | Thread.Sleep(200); 44 | attempts++; 45 | } 46 | } 47 | 48 | File.Move(tmpPath, targetPath, true); 49 | 50 | Process proc = new Process(); 51 | proc.StartInfo.FileName = targetPath; 52 | proc.StartInfo.UseShellExecute = true; 53 | proc.Start(); 54 | 55 | Log("Updated successful"); 56 | } 57 | } 58 | catch (Exception e) { Log(MethodBase.GetCurrentMethod(), e); } 59 | } 60 | 61 | public static void Log(MethodBase m, Exception e) 62 | { 63 | Log("Error " + m.Name + " " + e.Message + Environment.NewLine + e.TargetSite + Environment.NewLine + e.StackTrace); 64 | } 65 | 66 | public static void Log(string message) 67 | { 68 | try 69 | { 70 | Console.WriteLine(message); 71 | 72 | string directory = Path.Combine(Environment.GetFolderPath(SpecialFolder.ApplicationData), "NoodleManagerX"); 73 | if (!Directory.Exists(directory)) 74 | { 75 | Directory.CreateDirectory(directory); 76 | } 77 | 78 | using (StreamWriter sw = File.AppendText(Path.Combine(directory, "UpdateLog.txt"))) 79 | { 80 | sw.Write(DateTime.Now.ToString("dd'.'MM HH':'mm':'ss") + " " + message + Environment.NewLine); 81 | } 82 | } 83 | catch { } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/MtpDevice.cs: -------------------------------------------------------------------------------- 1 | using MediaDevices; 2 | using System; 3 | using System.Linq; 4 | using Path = System.IO.Path; 5 | 6 | namespace NoodleManagerX.Models 7 | { 8 | static class MtpDevice 9 | { 10 | public static MediaDevice device; 11 | public static string path = ""; 12 | public static bool connected = false; 13 | 14 | public static void Connect(bool isCommand = false) 15 | { 16 | if (!connected) 17 | { 18 | var devices = MediaDevice.GetDevices(); 19 | MainViewModel.Log(devices.Count() + " mtp devices connected"); 20 | foreach (MediaDevice d in devices) 21 | { 22 | try 23 | { 24 | d.Connect(); 25 | string[] directories = d.GetDirectories(@"\"); 26 | foreach (string directory in directories) 27 | { 28 | string dir = Path.Combine(directory, "SynthRidersUC"); 29 | if (d.DirectoryExists(dir)) 30 | { 31 | MainViewModel.Log("Synth Riders device found " + d.FriendlyName); 32 | device = d; 33 | path = dir; 34 | connected = true; 35 | MainViewModel.s_instance.questConnected = true; 36 | d.DeviceRemoved += DeviceRemoved; 37 | if (isCommand) MainViewModel.s_instance.ReloadLocalSources(); 38 | return; 39 | } 40 | } 41 | d.Disconnect(); 42 | } 43 | catch { } 44 | } 45 | if (isCommand) 46 | { 47 | if (devices.Count() > 0) 48 | { 49 | MainViewModel.s_instance.OpenErrorDialog("Found " + devices.Count() + " MTP devices but none of them had the SynthRidersUC folder:" + Environment.NewLine + String.Join(Environment.NewLine, devices.Select(x => x.FriendlyName + " (" + x.Description + ")")) + Environment.NewLine + "If your device is not listed above make sure to allow storage access on the headset"); 50 | } 51 | else 52 | { 53 | MainViewModel.s_instance.OpenErrorDialog("No MTP devices found" + Environment.NewLine + "Make sure to allow storage access on the headset"); 54 | } 55 | } 56 | } 57 | } 58 | 59 | private static void DeviceRemoved(object sender, MediaDeviceEventArgs e) 60 | { 61 | Disconnected(); 62 | } 63 | 64 | private static void Disconnected() 65 | { 66 | MainViewModel.Log("Quest disconnected"); 67 | device.DeviceRemoved -= DeviceRemoved; 68 | connected = false; 69 | MainViewModel.s_instance.questConnected = false; 70 | MainViewModel.s_instance.ReloadLocalSources(); 71 | } 72 | 73 | public static void Disconnect() 74 | { 75 | if (connected) 76 | { 77 | device.Disconnect(); 78 | if (!MainViewModel.s_instance.closing) Disconnected(); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 47 | 49 | 50 | 52 | image/svg+xml 53 | 55 | 56 | 57 | 58 | 59 | 63 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /NoodleManagerX/NoodleManagerX.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | 6 | 7 | WinExe 8 | 9 | 10 | 11 | net6.0-windows 12 | icon-nm.ico 13 | en 14 | true 15 | true 16 | link 17 | embedded 18 | AnyCPU;x64 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | True 55 | True 56 | Resources.resx 57 | 58 | 59 | 60 | 61 | ResXFileCodeGenerator 62 | Resources.Designer.cs 63 | 64 | 65 | 66 | 77 | 78 | -------------------------------------------------------------------------------- /NoodleManagerX/MainWindow.axaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Data.Converters; 4 | using Avalonia.Input; 5 | using Avalonia.Input.Raw; 6 | using Avalonia.Markup.Xaml; 7 | using Avalonia.Media; 8 | using NoodleManagerX.Models; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Globalization; 12 | using System.Linq; 13 | 14 | namespace NoodleManagerX 15 | { 16 | public class MainWindow : Window 17 | { 18 | public static MainWindow s_instance; 19 | public static BrushConverter BrushConverter = new BrushConverter(); 20 | 21 | private Grid blackBar; 22 | private bool lastleftclick = false; 23 | private bool lastHandled = false; 24 | private PixelPoint lastposition; 25 | 26 | public MainWindow() 27 | { 28 | s_instance = this; 29 | 30 | this.DataContext = new MainViewModel(); 31 | 32 | InitializeComponent(); 33 | 34 | blackBar = this.FindControl("BlackBar"); 35 | 36 | TextBox searchBox = this.FindControl("SearchBox"); 37 | searchBox.KeyDown += SearchBoxKeyEvent; 38 | 39 | #if DEBUG 40 | this.AttachDevTools(); 41 | #endif 42 | #pragma warning disable 0618 43 | Application.Current.InputManager.Process.Subscribe(x => 44 | { 45 | if (x is RawPointerEventArgs rawpointerevent) 46 | { 47 | MouseDevice mouse = (MouseDevice)x.Device; 48 | bool leftclick = rawpointerevent.InputModifiers == RawInputModifiers.LeftMouseButton; 49 | 50 | //Point mouseonclient = this.PointToClient(mouse.Position); 51 | 52 | if (!lastleftclick && leftclick && blackBar.IsPointerOver)//rising edge 53 | { 54 | lastleftclick = true; 55 | lastposition = mouse.Position; 56 | lastHandled = x.Handled; 57 | mouse = (MouseDevice)x.Device; 58 | } 59 | else if (lastleftclick && leftclick)//hold 60 | { 61 | if (!lastHandled) 62 | { 63 | PixelPoint p = new PixelPoint(this.Position.X + mouse.Position.X - lastposition.X, this.Position.Y + mouse.Position.Y - (int)lastposition.Y); 64 | lastposition = mouse.Position; 65 | this.Position = p; 66 | } 67 | } 68 | else//falling edge 69 | { 70 | lastleftclick = false; 71 | lastHandled = false; 72 | } 73 | } 74 | }); 75 | #pragma warning disable 0618 76 | } 77 | 78 | private void SearchBoxKeyEvent(object sender, KeyEventArgs e) 79 | { 80 | if (e.Key.Equals(Key.Enter)) 81 | { 82 | ((MainViewModel)this.DataContext).GetPage(); 83 | } 84 | } 85 | 86 | private void InitializeComponent() 87 | { 88 | AvaloniaXamlLoader.Load(this); 89 | } 90 | 91 | public static Brush GetBrush(string colorResource) 92 | { 93 | try 94 | { 95 | return s_instance.Resources[colorResource] as Brush; 96 | } 97 | catch { } 98 | 99 | return (Brush)BrushConverter.ConvertFromString("#ff00ff"); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/PlaybackHandler.cs: -------------------------------------------------------------------------------- 1 | using NAudio.Wave; 2 | using System; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | using YoutubeExplode; 8 | 9 | 10 | namespace NoodleManagerX.Models 11 | { 12 | static class PlaybackHandler 13 | { 14 | public static MapItem currentlyPlaying; 15 | private static YoutubeClient youtubeClient = new YoutubeClient(); 16 | private const string regex = @"^.*((youtu\.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*"; 17 | private static WaveOutEvent waveout; 18 | 19 | public static void Play(MapItem item) 20 | { 21 | Task.Run(async () => 22 | { 23 | if (currentlyPlaying == item) 24 | { 25 | Stop(); 26 | return; 27 | } 28 | string url = item.youtube_url; 29 | if (!String.IsNullOrEmpty(url)) 30 | { 31 | GroupCollection matches = Regex.Match(url, regex).Groups; 32 | string id = matches[matches.Count - 1].Value; 33 | 34 | if (currentlyPlaying != null) Stop(); 35 | currentlyPlaying = item; 36 | item.playing = true; 37 | MapItem playing = currentlyPlaying; 38 | 39 | try 40 | { 41 | var streamManifest = await youtubeClient.Videos.Streams.GetManifestAsync(id); 42 | 43 | var streamInfo = streamManifest.GetAudioOnlyStreams().OrderBy(x => x.Bitrate).FirstOrDefault(); 44 | 45 | using (var mf = new MediaFoundationReader(streamInfo.Url)) 46 | using (waveout = new WaveOutEvent()) 47 | { 48 | waveout.Init(mf); 49 | SetVolume(MainViewModel.s_instance.previewVolume); 50 | waveout.Play(); 51 | while (waveout.PlaybackState == PlaybackState.Playing) 52 | { 53 | await Task.Delay(100); 54 | } 55 | } 56 | StopPlaying(playing); 57 | } 58 | catch (Exception e) 59 | { 60 | MainViewModel.Log(MethodBase.GetCurrentMethod(), e); 61 | Stop(); 62 | } 63 | } 64 | }); 65 | } 66 | 67 | public static void SetVolume(int volume) 68 | { 69 | if (waveout != null) 70 | { 71 | waveout.Volume = volume / 100f; 72 | } 73 | } 74 | 75 | public static void Stop() 76 | { 77 | 78 | try 79 | { 80 | if (waveout != null) 81 | { 82 | waveout.Stop(); 83 | } 84 | } 85 | catch (Exception e) 86 | { 87 | MainViewModel.Log(MethodBase.GetCurrentMethod(), e); 88 | } 89 | finally 90 | { 91 | StopPlaying(currentlyPlaying); 92 | } 93 | } 94 | 95 | private static void StopPlaying(MapItem playing) 96 | { 97 | if (playing != null) playing.playing = false; 98 | if (currentlyPlaying == playing) 99 | { 100 | currentlyPlaying = null; 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /NoodleManagerX/Resources/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace NoodleManagerX.Resources { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NoodleManagerX.Resources.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized resource of type System.Byte[]. 65 | /// 66 | internal static byte[] classdata { 67 | get { 68 | object obj = ResourceManager.GetObject("classdata", resourceCulture); 69 | return ((byte[])(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Byte[]. 75 | /// 76 | internal static byte[] classdata_large { 77 | get { 78 | object obj = ResourceManager.GetObject("classdata_large", resourceCulture); 79 | return ((byte[])(obj)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/github.xaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/LocalItem.cs: -------------------------------------------------------------------------------- 1 | using NoodleManagerX.Models.Mods; 2 | using NoodleManagerX.Models.Stages; 3 | using NoodleManagerX.Mods; 4 | using Semver; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace NoodleManagerX.Models 13 | { 14 | public class LocalItem 15 | { 16 | public LocalItem(int id, string hash, string filename, DateTime modifiedTime, ItemType itemType) 17 | { 18 | this.id = id; 19 | this.hash = hash; 20 | this.filename = filename; 21 | this.modifiedTime = modifiedTime; 22 | this.itemType = itemType; 23 | } 24 | 25 | 26 | public int id = -1; 27 | public string hash = ""; 28 | public string filename = ""; 29 | public DateTime modifiedTime = new DateTime(); 30 | public ItemType itemType = ItemType.init; 31 | 32 | public SemVersion ItemVersion 33 | { 34 | get 35 | { 36 | if (String.IsNullOrEmpty(filename)) 37 | { 38 | return null; 39 | } 40 | 41 | if (filename.StartsWith(hash + "_") && filename.EndsWith(".synthmod")) 42 | { 43 | var versionStr = filename.Substring(hash.Length + 1, filename.Length - hash.Length - 1 - ".synthmod".Length); 44 | try 45 | { 46 | return SemVersion.Parse(versionStr, SemVersionStyles.Any); 47 | } 48 | catch (Exception ex) 49 | { 50 | MainViewModel.Log($"Failed to parse version '{versionStr}' for mod {hash}"); 51 | } 52 | } 53 | return null; 54 | } 55 | } 56 | 57 | public bool CheckEquality(GenericItem item, bool checkHash = false) 58 | { 59 | if (item != null && itemType == item.itemType) 60 | { 61 | if (itemType == ItemType.Map && id != -1) 62 | { 63 | return this.id == item.id && (!checkHash || hash == ((MapItem)item).hash); 64 | } 65 | else if (itemType == ItemType.Mod && id != -1) 66 | { 67 | // id isn't set for mods, so always compare with hash (which is set to the modinfo id) 68 | // Version is located within filename. Filename is always {modId}_{version}.synthmod 69 | var modItem = (ModItem)item; 70 | var isSameVersion = ItemVersion == null || 71 | modItem.InstalledVersion?.Version == null || 72 | ItemVersion.ComparePrecedenceTo(modItem.InstalledVersion?.Version) == 0; 73 | return hash == modItem.ModInfo?.Id && isSameVersion; 74 | } 75 | else if (itemType == ItemType.Playlist) 76 | { 77 | // The hash is set to the playlistName from within the playlist file. 78 | // This matches the GenericItem.name field when pulling from the Z site 79 | return hash == item.name; 80 | } 81 | else if (itemType == ItemType.Stage) 82 | { 83 | // Consider them equal if the local filename matches _any_ of the stage's files 84 | var stageItem = (StageItem)item; 85 | return stageItem.Files.Where(fileInfo => fileInfo.File.Filename == filename).Any(); 86 | } 87 | else 88 | { 89 | return filename == item.filename; 90 | } 91 | } 92 | return false; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /NoodleManagerXTests/Mods/json/current_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "availableMods": [ 3 | { 4 | "id": "SRModsList", 5 | "name": "SRModsList", 6 | "author": "Mondanzo", 7 | "description": "Add a new button to the main menu to open a mod list", 8 | "versions": [ 9 | { 10 | "version": "1.0.1", 11 | "downloadUrl": "https://github.com/Mondanzo/SRModsList/releases/download/1.0.1/SRModsList.dll", 12 | "dependencies": [] 13 | } 14 | ] 15 | }, 16 | { 17 | "id": "Trashbin", 18 | "name": "Trashbin", 19 | "author": "Goinn", 20 | "description": "Adds a button to delete songs for Synth Riders [PCVR]. Deleted songs will be added to NoodleManagerX's blacklist", 21 | "versions": [ 22 | { 23 | "version": "1.2.0", 24 | "downloadUrl": "https://github.com/Goinn/Trashbin/releases/download/V1.2.0/Trashbin.dll", 25 | "dependencies": [] 26 | } 27 | ] 28 | }, 29 | { 30 | "id": "MultiplayerSongGrabber", 31 | "name": "MultiplayerSongGrabber", 32 | "author": "Goinn", 33 | "description": "Download missing custom songs with the click of a button inside a multiplayer lobby! For Synth Riders [PCVR]", 34 | "versions": [ 35 | { 36 | "version": "1.0.0", 37 | "downloadUrl": "https://github.com/Goinn/MultiplayerSongGrabber/releases/download/V1.0.0/MultiplayerSongGrabber.dll", 38 | "dependencies": [] 39 | } 40 | ] 41 | }, 42 | { 43 | "id": "CustomSounds", 44 | "name": "CustomSounds", 45 | "author": "Goinn", 46 | "description": "Allows you to customize most sounds from Synth Riders [PCVR]", 47 | "versions": [ 48 | { 49 | "version": "1.1.0", 50 | "downloadUrl": "https://github.com/Goinn/CustomSounds/releases/download/v1.1.0/CustomSound.dll", 51 | "dependencies": [] 52 | } 53 | ] 54 | }, 55 | { 56 | "id": "SRModCore", 57 | "name": "SRModCore", 58 | "author": "bookdude13", 59 | "description": "Shared library used by various SynthRiders mods", 60 | "versions": [ 61 | { 62 | "version": "1.0.0", 63 | "downloadUrl": "https://github.com/bookdude13/SRModCore/releases/download/v1.0.0/SRModCore_v1.0.0.zip", 64 | "dependencies": [] 65 | } 66 | ] 67 | }, 68 | { 69 | "id": "SRVoting", 70 | "name": "SRVoting", 71 | "author": "bookdude13", 72 | "description": "In-game up/down voting UI for custom songs", 73 | "versions": [ 74 | { 75 | "version": "1.2.0", 76 | "downloadUrl": "https://github.com/bookdude13/SRVoting/releases/download/v1.2.0/SRVoting_v1.2.0.zip", 77 | "dependencies": [ 78 | { 79 | "id": "SRModCore", 80 | "minVersion": "1.0.0" 81 | } 82 | ] 83 | } 84 | ] 85 | }, 86 | { 87 | "id": "SRPerformanceMeter", 88 | "name": "PerformanceMeter", 89 | "author": "bookdude13", 90 | "description": "View additional playthrough statistics/info in-game after each song", 91 | "versions": [ 92 | { 93 | "version": "1.2.1", 94 | "downloadUrl": "https://github.com/bookdude13/SRPerformanceMeter/releases/download/v1.2.1/PerformanceMeter_v1.2.1.zip", 95 | "dependencies": [ 96 | { 97 | "id": "SynthRiders-Websockets-Mod", 98 | "minVersion": "1.0.2" 99 | } 100 | ] 101 | } 102 | ] 103 | }, 104 | { 105 | "id": "SynthRiders-Websockets-Mod", 106 | "name": "SynthRiders-Websockets-Mod", 107 | "author": "KK964", 108 | "description": "Emits game events through websockets", 109 | "versions": [ 110 | { 111 | "version": "1.0.2", 112 | "downloadUrl": "https://github.com/KK964/SynthRiders-Websockets-Mod/releases/download/v1.0.2/SynthRidersWebsockets.zip", 113 | "dependencies": [] 114 | } 115 | ] 116 | } 117 | ] 118 | } -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/DownloadScheduler.cs: -------------------------------------------------------------------------------- 1 | using Avalonia.Threading; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Collections.Specialized; 6 | using System.Threading.Tasks; 7 | 8 | namespace NoodleManagerX.Models 9 | { 10 | static class DownloadScheduler 11 | { 12 | public static ObservableCollection queue = new ObservableCollection(); 13 | public static ObservableCollection downloading = new ObservableCollection(); 14 | 15 | public const int downloadTasks = 4; 16 | public const int maxDownloadAttempts = 3; 17 | public static int toDownload = 0; 18 | 19 | private static volatile bool running = false; 20 | 21 | static DownloadScheduler() 22 | { 23 | queue.CollectionChanged += QueueChanged; 24 | } 25 | 26 | public static void Download(GenericItem item) 27 | { 28 | var supportedDownloadTypes = new HashSet { ItemType.Map, ItemType.Mod, ItemType.Playlist, ItemType.Stage, ItemType.Avatar }; 29 | if (supportedDownloadTypes.Contains(item.itemType) && !item.blacklisted) 30 | { 31 | toDownload++; 32 | item.downloadAttempts = 1; 33 | queue.Add(item); 34 | } 35 | else 36 | { 37 | Console.WriteLine("Not queueing item since it's not a map, playlist, stage or mod"); 38 | } 39 | } 40 | 41 | public static void Requeue(GenericItem item) 42 | { 43 | if (item.downloadAttempts < maxDownloadAttempts) 44 | { 45 | MainViewModel.Log("Requeueing " + item.filename); 46 | item.downloadAttempts++; 47 | queue.Add(item); 48 | } 49 | else 50 | { 51 | MainViewModel.Log("Timeout " + item.filename); 52 | } 53 | } 54 | 55 | public static void Remove(GenericItem item) 56 | { 57 | downloading.Remove(item); 58 | if (downloading.Count == 0 && !MainViewModel.s_instance.closing)//if the database is saved while closing it gets corrupted 59 | { 60 | _ = MainViewModel.s_instance.SaveLocalItems(); 61 | _ = Dispatcher.UIThread.InvokeAsync(() => 62 | { 63 | MainViewModel.s_instance.progress = 0; 64 | MainViewModel.s_instance.progressText = null; 65 | }); 66 | } 67 | else 68 | { 69 | _ = Dispatcher.UIThread.InvokeAsync(() => 70 | { 71 | MainViewModel.s_instance.progress = Math.Min(100 - (int)((queue.Count + downloading.Count) / (toDownload * 0.01f)), 1); 72 | MainViewModel.s_instance.progressText = "Downloading: " + MainViewModel.s_instance.progress + "% [" + downloading.Count + 1 + "](" + (toDownload - queue.Count) + "/" + toDownload + ")"; 73 | }); 74 | } 75 | } 76 | 77 | private static void QueueChanged(object sender, NotifyCollectionChangedEventArgs e) 78 | { 79 | Task.Run(async () => 80 | { 81 | if (queue.Count > 0 && !running) 82 | { 83 | running = true; 84 | await Task.Delay(100);//not sure why this is necessary for requeueing. Seems like queuecount is !=0 but the queue is empty? 85 | MainViewModel.Log("Started queue check"); 86 | while (queue.Count > 0) 87 | { 88 | if (downloading.Count < downloadTasks) 89 | { 90 | if (queue[0] != null && !queue[0].downloading) 91 | { 92 | _ = Task.Run(queue[0].Download); 93 | downloading.Add(queue[0]); 94 | } 95 | queue.Remove(queue[0]); 96 | } 97 | else 98 | { 99 | await Task.Delay(500); 100 | } 101 | } 102 | running = false; 103 | MainViewModel.Log("Stopped queue check"); 104 | } 105 | }); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /NoodleManagerX/ThirdParty/MelonLoader/UnityInformationHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Runtime.InteropServices; 5 | using System.Runtime.CompilerServices; 6 | using System.Linq; 7 | using AssetsTools.NET; 8 | using AssetsTools.NET.Extra; 9 | using NoodleManagerX.Models; 10 | 11 | namespace NoodleManagerX.ThirdParty.MelonLoader 12 | { 13 | // Taken from https://github.com/LavaGang/MelonLoader/blob/master/MelonLoader/InternalUtils/UnityInformationHandler.cs 14 | // Uses the Apache 2.0 License 15 | // Unnecessary checks and fallback data retrieval logic removed 16 | public static class UnityInformationHandler 17 | { 18 | public static string GameName { get; private set; } = "UNKNOWN"; 19 | public static string GameDeveloper { get; private set; } = "UNKNOWN"; 20 | public static string GameVersion { get; private set; } = "0"; 21 | 22 | internal static void Setup(string gameDataDirectory) 23 | { 24 | //AssetsManager assetsManager = new AssetsManager(); 25 | //ReadGameInfo(gameDataDirectory, assetsManager); 26 | //assetsManager.UnloadAll(); 27 | } 28 | 29 | private static void ReadGameInfo(string gameDataDirectory, AssetsManager assetsManager) 30 | { 31 | AssetsFileInstance instance = null; 32 | try 33 | { 34 | string bundlePath = Path.Combine(gameDataDirectory, "globalgamemanagers"); 35 | if (!File.Exists(bundlePath)) 36 | { 37 | MainViewModel.Log("Couldn't find globalgamemanagers file for game version"); 38 | return; 39 | } 40 | 41 | instance = assetsManager.LoadAssetsFile(bundlePath, true); 42 | if (instance == null) 43 | { 44 | MainViewModel.Log("Couldn't load assets file"); 45 | return; 46 | } 47 | 48 | assetsManager.LoadIncludedClassPackage(); 49 | if (!instance.file.typeTree.hasTypeTree) 50 | { 51 | assetsManager.LoadClassDatabaseFromPackage(instance.file.typeTree.unityVersion); 52 | } 53 | 54 | List assetFiles = instance.table.GetAssetsOfType(129); 55 | if (assetFiles.Count > 0) 56 | { 57 | AssetFileInfoEx playerSettings = assetFiles.First(); 58 | 59 | AssetTypeInstance assetTypeInstance = null; 60 | try 61 | { 62 | assetTypeInstance = assetsManager.GetTypeInstance(instance, playerSettings); 63 | } 64 | catch (Exception ex) 65 | { 66 | assetsManager.LoadIncludedLargeClassPackage(); 67 | assetsManager.LoadClassDatabaseFromPackage(instance.file.typeTree.unityVersion); 68 | assetTypeInstance = assetsManager.GetTypeInstance(instance, playerSettings); 69 | } 70 | 71 | if (assetTypeInstance != null) 72 | { 73 | AssetTypeValueField playerSettings_baseField = assetTypeInstance.GetBaseField(); 74 | 75 | AssetTypeValueField bundleVersion = playerSettings_baseField.Get("bundleVersion"); 76 | if (bundleVersion != null) 77 | { 78 | GameVersion = bundleVersion.GetValue().AsString(); 79 | } 80 | 81 | AssetTypeValueField companyName = playerSettings_baseField.Get("companyName"); 82 | if (companyName != null) 83 | { 84 | GameDeveloper = companyName.GetValue().AsString(); 85 | } 86 | 87 | AssetTypeValueField productName = playerSettings_baseField.Get("productName"); 88 | if (productName != null) 89 | { 90 | GameName = productName.GetValue().AsString(); 91 | } 92 | } 93 | } 94 | } 95 | catch (Exception ex) 96 | { 97 | MainViewModel.Log("Failed to Initialize Assets Manager! " + ex.Message); 98 | } 99 | 100 | if (instance != null) 101 | { 102 | instance.file.Close(); 103 | } 104 | } 105 | 106 | public static ClassDatabasePackage LoadIncludedClassPackage(this AssetsManager assetsManager) 107 | { 108 | ClassDatabasePackage classPackage = null; 109 | using (MemoryStream mstream = new MemoryStream(Resources.Resources.classdata)) 110 | classPackage = assetsManager.LoadClassPackage(mstream); 111 | return classPackage; 112 | } 113 | 114 | public static ClassDatabasePackage LoadIncludedLargeClassPackage(this AssetsManager assetsManager) 115 | { 116 | ClassDatabasePackage classPackage = null; 117 | using (MemoryStream mstream = new MemoryStream(Resources.Resources.classdata_large)) 118 | classPackage = assetsManager.LoadClassPackage(mstream); 119 | return classPackage; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/Included/searchbar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 32 | 39 | 43 | 47 | 51 | 55 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 107 | 111 | 115 | 119 | 120 | 121 | 145 | 147 | 148 | 150 | image/svg+xml 151 | 153 | 154 | 155 | 156 | 157 | 161 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/Stages/StageItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Mime; 6 | using System.Net; 7 | using System.Reflection; 8 | using System.Runtime.Serialization; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using System.Web; 12 | using NoodleManagerX.Models.Mods; 13 | using System.Net.Http; 14 | using Newtonsoft.Json; 15 | using Avalonia.Threading; 16 | 17 | namespace NoodleManagerX.Models.Stages 18 | { 19 | [DataContract] 20 | class StageItem : GenericItem 21 | { 22 | public override string target { get; set; } = "CustomStages"; 23 | public override ItemType itemType { get; set; } = ItemType.Stage; 24 | 25 | [DataMember] 26 | [JsonProperty("files")] 27 | public List Files { get; set; } = new List(); 28 | 29 | [DataMember] 30 | [JsonProperty("experience_beatmap")] 31 | public MapItem ExperienceBeatmap = null; 32 | 33 | public List GetDownloadFilesForCurrentDevice() 34 | { 35 | return Files 36 | .Where(file => MtpDevice.connected ? file.IsQuest() : file.IsPc()) 37 | .Select(file => file.File) 38 | .ToList(); 39 | } 40 | 41 | public override void Delete() 42 | { 43 | bool deletedAll = true; 44 | var deviceFiles = GetDownloadFilesForCurrentDevice(); 45 | deviceFiles.ForEach(stageFile => deletedAll = Delete(stageFile.Filename) && deletedAll); 46 | if (deletedAll) 47 | { 48 | _ = Dispatcher.UIThread.InvokeAsync(() => 49 | { 50 | downloaded = false; 51 | }); 52 | } 53 | } 54 | 55 | public override async Task UpdateDownloaded(bool forceUpdate = false) 56 | { 57 | var numDownloaded = 0; 58 | var expectedFiles = GetDownloadFilesForCurrentDevice(); 59 | foreach (var file in expectedFiles) 60 | { 61 | List matchingLocalItems = MainViewModel.s_instance.localItems 62 | .Where(x => x != null && x.filename == file.Filename) 63 | .ToList(); 64 | matchingLocalItems.Sort(); 65 | 66 | if (matchingLocalItems.Count() > 0) 67 | { 68 | if (matchingLocalItems.Count() > 1) 69 | { 70 | matchingLocalItems = matchingLocalItems.Where(x => x.itemType == itemType).OrderByDescending(x => x.modifiedTime).ToList(); 71 | foreach (LocalItem l in matchingLocalItems.Skip(1)) 72 | { 73 | MainViewModel.Log("Old version deleted of " + l.filename); 74 | _ = Delete(l.filename); 75 | } 76 | } 77 | 78 | numDownloaded++; 79 | 80 | filename = matchingLocalItems[0].filename; 81 | needsUpdate = DateTime.Compare(updatedAt, matchingLocalItems[0].modifiedTime) > 0; 82 | if (needsUpdate) MainViewModel.Log("Date difference detected for " + this.filename); 83 | } 84 | } 85 | 86 | if (expectedFiles.Count == 0) 87 | { 88 | visible = false; 89 | } 90 | if (numDownloaded == expectedFiles.Count) 91 | { 92 | await Dispatcher.UIThread.InvokeAsync(() => 93 | { 94 | downloaded = true; 95 | }); 96 | } 97 | else 98 | { 99 | await Dispatcher.UIThread.InvokeAsync(() => 100 | { 101 | downloaded = false; 102 | needsUpdate = false; 103 | }); 104 | } 105 | 106 | if (needsUpdate || forceUpdate) 107 | { 108 | DownloadScheduler.Download(this); 109 | } 110 | } 111 | 112 | public override void UpdateBlacklisted() 113 | { 114 | var platformFiles = GetDownloadFilesForCurrentDevice(); 115 | foreach (var file in platformFiles) 116 | { 117 | if (!String.IsNullOrEmpty(file.Filename) && MainViewModel.s_instance.blacklist.Contains(file.Filename)) 118 | { 119 | _ = Dispatcher.UIThread.InvokeAsync(() => 120 | { 121 | blacklisted = true; 122 | }); 123 | return; 124 | } 125 | } 126 | 127 | // Not found in blacklist 128 | _ = Dispatcher.UIThread.InvokeAsync(() => 129 | { 130 | blacklisted = false; 131 | }); 132 | } 133 | 134 | protected async override Task RawDownloadAndSave() 135 | { 136 | var path = ""; 137 | 138 | // Try to download all modes of this stage. 139 | // Use the first mode in the list as the identifier (path) 140 | 141 | var downloadFiles = GetDownloadFilesForCurrentDevice(); 142 | foreach (StageFile stageFile in downloadFiles) 143 | { 144 | try 145 | { 146 | string url = stageFile.Url; 147 | if (url == null) 148 | { 149 | url = "https://synthriderz.com" + download_url + "?file_id=" + stageFile.Id; 150 | } 151 | 152 | using HttpClient client = new HttpClient(); 153 | using var rawResponse = await client.GetStreamAsync(url); 154 | using MemoryStream str = await CopyStreamToMemoryStream(rawResponse); 155 | if (String.IsNullOrEmpty(filename)) 156 | { 157 | filename = stageFile.Filename; 158 | } 159 | 160 | path = Path.Combine(target, stageFile.Filename); 161 | await StorageAbstraction.WriteFile(str, path); 162 | } 163 | catch (Exception e) 164 | { 165 | MainViewModel.Log(MethodBase.GetCurrentMethod(), e); 166 | } 167 | } 168 | 169 | return path; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /NoodleManagerX/Resources/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | classdata.tpk;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 123 | 124 | 125 | classdata_large.tpk;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 126 | 127 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/MapHandler.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using ReactiveUI; 4 | using ReactiveUI.Fody.Helpers; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO.Compression; 8 | using System.Reactive; 9 | using System.Reflection; 10 | using System.Runtime.Serialization; 11 | using System.Threading.Tasks; 12 | using MemoryStream = System.IO.MemoryStream; 13 | using Path = System.IO.Path; 14 | using Stream = System.IO.Stream; 15 | 16 | namespace NoodleManagerX.Models 17 | { 18 | class MapHandler : GenericHandler 19 | { 20 | public override ItemType itemType { get; set; } = ItemType.Map; 21 | public override Dictionary queryFields { get; set; } = new Dictionary() { { "text_search", "$tsQuery" }, { "user.username", "$contL" } }; 22 | public override string select { get; set; } = "title,artist,mapper,duration,difficulties,hash,youtube_url,beat_saber_convert"; 23 | public override string selectDownload { get; set; } = "hash,beat_saber_convert"; 24 | public override string apiEndpoint { get; set; } = "https://synthriderz.com/api/beatmaps"; 25 | public override string folder { get; set; } = "CustomSongs"; 26 | public override string[] extensions { get; set; } = { ".synth" }; 27 | 28 | public override async Task GetLocalItem(string path) 29 | { 30 | try 31 | { 32 | if (StorageAbstraction.FileExists(path)) 33 | { 34 | using (Stream stream = StorageAbstraction.ReadFile(path)) 35 | using (ZipArchive archive = new ZipArchive(stream)) 36 | { 37 | foreach (ZipArchiveEntry entry in archive.Entries) 38 | { 39 | if (entry.FullName == "synthriderz.meta.json") 40 | { 41 | using (System.IO.StreamReader sr = new System.IO.StreamReader(entry.Open())) 42 | { 43 | LocalItem localItem = JsonConvert.DeserializeObject(await sr.ReadToEndAsync()); 44 | localItem.filename = Path.GetFileName(path); 45 | localItem.modifiedTime = StorageAbstraction.GetLastWriteTime(path); 46 | localItem.itemType = ItemType.Map; 47 | MainViewModel.s_instance.localItems.Add(localItem); 48 | return true; 49 | } 50 | } 51 | } 52 | 53 | if (MainViewModel.s_instance.pruning) 54 | { 55 | MainViewModel.Log("Deleting old file without metadata " + Path.GetFileName(path)); 56 | StorageAbstraction.DeleteFile(path); 57 | } 58 | } 59 | } 60 | } 61 | catch (Exception e) 62 | { 63 | MainViewModel.Log(MethodBase.GetCurrentMethod(), e); 64 | MainViewModel.Log("Deleting corrupted file " + Path.GetFileName(path)); 65 | try { StorageAbstraction.DeleteFile(path); } 66 | catch (Exception ee) { MainViewModel.Log(MethodBase.GetCurrentMethod(), ee); } 67 | } 68 | 69 | return false; 70 | } 71 | 72 | public override dynamic DeserializePage(string json) 73 | { 74 | return JsonConvert.DeserializeObject(json); 75 | } 76 | } 77 | 78 | [DataContract] 79 | class MapItem : GenericItem 80 | { 81 | public override string target { get; set; } = "CustomSongs"; 82 | public override ItemType itemType { get; set; } = ItemType.Map; 83 | 84 | [DataMember] public string title { get; set; } 85 | [DataMember] public string artist { get; set; } 86 | [DataMember] public string mapper { get; set; } 87 | [DataMember] public string duration { get; set; } 88 | [DataMember] public string[] difficulties { get; set; } 89 | [DataMember] public string hash { get; set; } 90 | [DataMember] public string youtube_url { get; set; } 91 | [Reactive] public bool playing { get; set; } = false; 92 | public ReactiveCommand playPreviewCommand { get; set; } 93 | 94 | public override string display_title 95 | { 96 | get { return title; } 97 | } 98 | public override string display_creator 99 | { 100 | get { return mapper; } 101 | } 102 | public override string display_preview 103 | { 104 | get { return youtube_url; } 105 | } 106 | public override string[] display_difficulties 107 | { 108 | get { return difficulties; } 109 | } 110 | 111 | [OnDeserialized] 112 | private void OnDeserializedMethod(StreamingContext context) 113 | { 114 | if (filename != null && PlaybackHandler.currentlyPlaying?.filename == filename) 115 | { 116 | playing = PlaybackHandler.currentlyPlaying.playing; 117 | PlaybackHandler.currentlyPlaying = this; 118 | } 119 | 120 | playPreviewCommand = ReactiveCommand.Create((() => 121 | { 122 | PlaybackHandler.Play(this); 123 | })); 124 | } 125 | 126 | public override async Task CopyStreamToMemoryStream(Stream stream, bool closeOriginal = true) 127 | { 128 | MemoryStream ms = await base.CopyStreamToMemoryStream(stream); 129 | 130 | using (ZipArchive archive = new ZipArchive(ms, ZipArchiveMode.Update, true)) 131 | { 132 | foreach (ZipArchiveEntry zipEntry in archive.Entries) 133 | { 134 | if (zipEntry.FullName == "synthriderz.meta.json") 135 | { 136 | return ms; 137 | } 138 | } 139 | 140 | MainViewModel.Log("Creating metadata for " + filename); 141 | JObject metadata = new JObject(new JProperty("id", id), new JProperty("hash", hash)); 142 | 143 | ZipArchiveEntry entry = archive.CreateEntry("synthriderz.meta.json"); 144 | using (System.IO.StreamWriter writer = new System.IO.StreamWriter(entry.Open())) 145 | { 146 | writer.Write(metadata.ToString(Formatting.None)); 147 | } 148 | 149 | return ms; 150 | } 151 | } 152 | } 153 | 154 | #pragma warning disable 0649 155 | class MapPage : GenericPage 156 | { 157 | public List data; 158 | } 159 | #pragma warning restore 0649 160 | } 161 | -------------------------------------------------------------------------------- /NoodleManagerX/Models/StorageAbstraction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace NoodleManagerX.Models 7 | { 8 | 9 | class StorageAbstraction 10 | { 11 | static object MtpDeviceLock = new object(); 12 | 13 | public static async Task WriteFile(MemoryStream stream, string path) 14 | { 15 | if (DirectoryExists(Path.GetDirectoryName(path))) 16 | { 17 | if (FileExists(path)) 18 | { 19 | DeleteFile(path); 20 | } 21 | 22 | stream.Position = 0; 23 | 24 | if (MtpDevice.connected) 25 | {//remove temp file once media devices works properely with memory stream 26 | string tempFile = Path.GetTempFileName(); 27 | using (FileStream fs = new FileStream(tempFile, FileMode.Create, FileAccess.ReadWrite)) 28 | { 29 | await stream.CopyToAsync(fs); 30 | fs.Position = 0; 31 | 32 | lock (MtpDeviceLock) 33 | { 34 | MtpDevice.device.UploadFile(fs, Path.Combine(MtpDevice.path, path)); 35 | } 36 | } 37 | try { File.Delete(tempFile); } catch { }//really doesn't matter if something goes wrong, windows will clean temp files automatically 38 | } 39 | else 40 | { 41 | using (FileStream file = new FileStream(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path), FileMode.Create, FileAccess.Write)) 42 | { 43 | await stream.CopyToAsync(file); 44 | } 45 | } 46 | stream.Flush(); 47 | stream.Close(); 48 | } 49 | } 50 | 51 | public static async Task CreateDirectory(string path) 52 | { 53 | if (MtpDevice.connected) 54 | { 55 | lock (MtpDeviceLock) 56 | { 57 | MtpDevice.device.CreateDirectory(Path.Combine(MtpDevice.path, path)); 58 | } 59 | } 60 | else 61 | { 62 | Directory.CreateDirectory(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path)); 63 | } 64 | } 65 | 66 | public static Stream ReadFile(string path) 67 | { 68 | if (MtpDevice.connected) 69 | { 70 | lock (MtpDeviceLock) 71 | { 72 | Stream ms = new MemoryStream(); 73 | MtpDevice.device.DownloadFile(Path.Combine(MtpDevice.path, path), ms); 74 | ms.Position = 0; 75 | return ms; 76 | } 77 | } 78 | else 79 | { 80 | return new FileStream(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path), FileMode.Open, FileAccess.Read); 81 | } 82 | } 83 | 84 | public static bool FileExists(string path) 85 | { 86 | if (MtpDevice.connected) 87 | { 88 | lock (MtpDeviceLock) 89 | { 90 | return MtpDevice.device.FileExists(Path.Combine(MtpDevice.path, path)); 91 | } 92 | } 93 | else 94 | { 95 | return File.Exists(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path)); 96 | } 97 | } 98 | 99 | public static string GetFullComputerPath(string pathRelativeToSynthDir) 100 | { 101 | return Path.Combine(MainViewModel.s_instance.settings.synthDirectory, pathRelativeToSynthDir); 102 | } 103 | 104 | public static void DeleteFile(string path) 105 | { 106 | if (MtpDevice.connected) 107 | { 108 | lock (MtpDeviceLock) 109 | { 110 | MtpDevice.device.DeleteFile(Path.Combine(MtpDevice.path, path)); 111 | } 112 | } 113 | else 114 | { 115 | File.Delete(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path)); 116 | } 117 | } 118 | 119 | public static bool DirectoryExists(string path) 120 | { 121 | if (MtpDevice.connected) 122 | { 123 | lock (MtpDeviceLock) 124 | { 125 | return MtpDevice.device.DirectoryExists(Path.Combine(MtpDevice.path, path)); 126 | } 127 | } 128 | else 129 | { 130 | return Directory.Exists(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path)); 131 | } 132 | } 133 | 134 | public static string[] GetFilesInDirectory(string path) 135 | { 136 | if (DirectoryExists(path)) 137 | { 138 | if (MtpDevice.connected) 139 | { 140 | lock (MtpDeviceLock) 141 | { 142 | return MtpDevice.device.GetFiles(Path.Combine(MtpDevice.path, path)).Select(x => Path.Combine(path, Path.GetFileName(x))).ToArray(); 143 | } 144 | } 145 | else 146 | { 147 | return Directory.GetFiles(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path)).Select(x => Path.Combine(path, Path.GetFileName(x))).ToArray(); 148 | } 149 | } 150 | else return new string[0]; 151 | } 152 | 153 | public static DateTime GetLastWriteTime(string path) 154 | { 155 | if (MtpDevice.connected) 156 | { 157 | lock (MtpDeviceLock) 158 | { 159 | return MtpDevice.device.GetFileInfo(Path.Combine(MtpDevice.path, path)).LastWriteTime.Value; 160 | } 161 | } 162 | else 163 | { 164 | return File.GetLastWriteTime(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path)); 165 | } 166 | } 167 | 168 | public static void SetLastWriteTime(DateTime timestamp, string path) 169 | { 170 | if (MtpDevice.connected) 171 | { 172 | //no idea how to change timestamp over mtp 173 | } 174 | else 175 | { 176 | File.SetLastWriteTime(Path.Combine(MainViewModel.s_instance.settings.synthDirectory, path), timestamp); 177 | } 178 | } 179 | 180 | public static bool CanDownload(bool silent = false) 181 | { 182 | if (MtpDevice.connected) return true; 183 | else 184 | { 185 | return MainViewModel.s_instance.CheckDirectory(MainViewModel.s_instance.settings.synthDirectory, !silent); 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | # *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /QuestAccess.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 44 | 46 | 53 | 54 | 59 | 61 | 67 | 75 | 81 | 85 | 89 | 93 | 97 | 103 | 104 | 105 | 108 | 116 | Deny 127 | 128 | The connected device will be able to access files on this headset 144 | 147 | 155 | Allow 166 | 167 | 175 | Allow access to data 186 | 187 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /NoodleManagerX/Assets/QuestAccess.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 44 | 46 | 53 | 54 | 59 | 61 | 67 | 75 | 81 | 85 | 89 | 93 | 97 | 103 | 104 | 105 | 108 | 116 | Deny 127 | 128 | The connected device will be able to access files on this headset 144 | 147 | 155 | Allow 166 | 167 | 175 | Allow access to data 186 | 187 | 198 | 199 | 200 | --------------------------------------------------------------------------------